diff --git a/src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java b/src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java new file mode 100644 index 00000000..2b3c8aa9 --- /dev/null +++ b/src/main/java/org/springframework/data/r2dbc/repository/query/RowDataConverter.java @@ -0,0 +1,220 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.r2dbc.repository.query; + +import io.r2dbc.spi.Row; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.converter.ConverterFactory; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToStringConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToUuidConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToZonedDateTimeConverter; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.UUID; + +/** + * Base class for row data converter. + * + * @author Hebert Coelho + */ +abstract class RowDataConverter { + private RowDataConverter() { + } + + /** + * @return A list of the registered converters + */ + static Collection getConvertersToRegister() { + List converters = new ArrayList<>(); + + converters.add(RowToBooleanConverter.INSTANCE); + converters.add(RowToLocalDateConverter.INSTANCE); + converters.add(RowToLocalDateTimeConverter.INSTANCE); + converters.add(RowToLocalTimeConverter.INSTANCE); + converters.add(RowToOffsetDateTimeConverter.INSTANCE); + converters.add(RowToStringConverter.INSTANCE); + converters.add(RowToUuidConverter.INSTANCE); + converters.add(RowToZonedDateTimeConverter.INSTANCE); + + return converters; + } + + /** + * Simple singleton to convert {@link Row}s to their {@link Boolean} representation. + * + * @author Hebert Coelho + */ + public enum RowToBooleanConverter implements Converter { + INSTANCE; + + @Override + public Boolean convert(Row row) { + return row.get(0, Boolean.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link LocalDate} representation. + * + * @author Hebert Coelho + */ + public enum RowToLocalDateConverter implements Converter { + INSTANCE; + + @Override + public LocalDate convert(Row row) { + return row.get(0, LocalDate.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link LocalDateTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToLocalDateTimeConverter implements Converter { + INSTANCE; + + @Override + public LocalDateTime convert(Row row) { + return row.get(0, LocalDateTime.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link LocalTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToLocalTimeConverter implements Converter { + INSTANCE; + + @Override + public LocalTime convert(Row row) { + return row.get(0, LocalTime.class); + } + } + + /** + * Singleton converter factory to convert the first column of a {@link Row} to a {@link Number}. + *

+ * Support Number classes including Byte, Short, Integer, Float, Double, Long, BigInteger, BigDecimal. This class + * delegates to {@link NumberUtils#convertNumberToTargetClass(Number, Class)} to perform the conversion. + * + * @see Byte + * @see Short + * @see Integer + * @see Long + * @see java.math.BigInteger + * @see Float + * @see Double + * @see java.math.BigDecimal + * + * @author Hebert Coelho + */ + public enum RowToNumberConverterFactory implements ConverterFactory { + INSTANCE; + + @Override + public Converter getConverter(Class targetType) { + Assert.notNull(targetType, "Target type must not be null"); + return new RowToNumber<>(targetType); + } + + private static final class RowToNumber implements Converter { + private final Class targetType; + + RowToNumber(Class targetType) { + this.targetType = targetType; + } + + @Override + public T convert(Row source) { + + Object object = source.get(0, targetType); + + return (object != null ? NumberUtils.convertNumberToTargetClass((Number) object, this.targetType) : null); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link OffsetDateTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToOffsetDateTimeConverter implements Converter { + INSTANCE; + + @Override + public OffsetDateTime convert(Row row) { + return row.get(0, OffsetDateTime.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link String} representation. + * + * @author Hebert Coelho + */ + public enum RowToStringConverter implements Converter { + INSTANCE; + + @Override + public String convert(Row row) { + return row.get(0, String.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link UUID} representation. + * + * @author Hebert Coelho + */ + public enum RowToUuidConverter implements Converter { + INSTANCE; + + @Override + public UUID convert(Row row) { + return row.get(0, UUID.class); + } + } + + /** + * Simple singleton to convert {@link Row}s to their {@link ZonedDateTime} representation. + * + * @author Hebert Coelho + */ + public enum RowToZonedDateTimeConverter implements Converter { + INSTANCE; + + @Override + public ZonedDateTime convert(Row row) { + return row.get(0, ZonedDateTime.class); + } + } + } +} diff --git a/src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java b/src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java new file mode 100644 index 00000000..413553cf --- /dev/null +++ b/src/test/java/org/springframework/data/r2dbc/repository/query/RowDataConverterTests.java @@ -0,0 +1,125 @@ +package org.springframework.data.r2dbc.repository.query; + +import io.r2dbc.spi.Row; +import org.junit.Test; +import org.springframework.core.convert.converter.Converter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToBooleanConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToLocalDateConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToLocalDateTimeConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToLocalTimeConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToOffsetDateTimeConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToStringConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToUuidConverter; +import org.springframework.data.r2dbc.repository.query.RowDataConverter.RowToNumberConverterFactory.RowToZonedDateTimeConverter; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class RowDataConverterTests { + private static final int TOTAL_REGISTERED_CONVERTERS = 8; + + @Test + public void isReturningAllCreatedConverts() { + assertThat(RowDataConverter.getConvertersToRegister().size()) + .isEqualTo(TOTAL_REGISTERED_CONVERTERS); + } + + @Test + public void isConvertingBoolean() { + Row row = mock(Row.class); + when(row.get(0, Boolean.class)).thenReturn(true); + + assertTrue(RowToBooleanConverter.INSTANCE.convert(row)); + } + + @Test + public void isConvertingLocalDate() { + LocalDate now = LocalDate.now(); + Row row = mock(Row.class); + when(row.get(0, LocalDate.class)).thenReturn(now); + + assertThat(RowToLocalDateConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingLocalDateTime() { + LocalDateTime now = LocalDateTime.now(); + Row row = mock(Row.class); + when(row.get(0, LocalDateTime.class)).thenReturn(now); + + assertThat(RowToLocalDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingLocalTime() { + LocalTime now = LocalTime.now(); + Row row = mock(Row.class); + when(row.get(0, LocalTime.class)).thenReturn(now); + + assertThat(RowToLocalTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingOffsetDateTime() { + OffsetDateTime now = OffsetDateTime.now(); + Row row = mock(Row.class); + when(row.get(0, OffsetDateTime.class)).thenReturn(now); + + assertThat(RowToOffsetDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingString() { + String value = "aValue"; + Row row = mock(Row.class); + when(row.get(0, String.class)).thenReturn(value); + + assertThat(RowToStringConverter.INSTANCE.convert(row)).isEqualTo(value); + } + + @Test + public void isConvertingUUID() { + UUID value = UUID.randomUUID(); + Row row = mock(Row.class); + when(row.get(0, UUID.class)).thenReturn(value); + + assertThat(RowToUuidConverter.INSTANCE.convert(row)).isEqualTo(value); + } + + @Test + public void isConvertingZonedDateTime() { + ZonedDateTime now = ZonedDateTime.now(); + Row row = mock(Row.class); + when(row.get(0, ZonedDateTime.class)).thenReturn(now); + + assertThat(RowToZonedDateTimeConverter.INSTANCE.convert(row)).isEqualTo(now); + } + + @Test + public void isConvertingNumber() { + Row row = mock(Row.class); + when(row.get(0, Integer.class)).thenReturn(33); + + final Converter converter = RowToNumberConverterFactory.INSTANCE.getConverter(Integer.class); + + assertThat(converter.convert(row)).isEqualTo(33); + } + + @Test + public void isRaisingExceptionForInvalidNumber() { + assertThatIllegalArgumentException().isThrownBy( + () -> RowToNumberConverterFactory.INSTANCE.getConverter(null) + ); + } +}