diff --git a/pom.xml b/pom.xml index eeaa0b9e93..cae7dc4252 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 03d6a5c2a0..004f714bc2 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index af9ad0904e..3800c394dd 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java new file mode 100644 index 0000000000..f8d7ea0b56 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java @@ -0,0 +1,120 @@ +/* + * Copyright 2021 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 + * + * https://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.jdbc.core.convert; + +import java.util.Collections; +import java.util.Set; + +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.converter.GenericConverter; +import org.springframework.data.convert.ReadingConverter; +import org.springframework.data.convert.WritingConverter; +import org.springframework.data.jdbc.core.mapping.AggregateReference; +import org.springframework.lang.Nullable; + +/** + * Converters for aggregate references. They need a {@link ConversionService} in order to delegate the conversion of the + * content of the {@link AggregateReference}. + * + * @author Jens Schauder + * @since 2.6 + */ +class AggregateReferenceConverters { + /** + * Prevent instantiation. + */ + private AggregateReferenceConverters() {} + + /** + * Converts from an AggregateReference to its id, leaving the conversion of the id to the ultimate target type to the + * delegate {@link ConversionService}. + */ + @WritingConverter + static class AggregateReferenceToSimpleTypeConverter implements GenericConverter { + + private final ConversionService delegate; + + AggregateReferenceToSimpleTypeConverter(ConversionService delegate) { + this.delegate = delegate; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(AggregateReference.class, Object.class)); + } + + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceDescriptor, TypeDescriptor targetDescriptor) { + + if (source == null) { + return null; + } + + // if the target type is an AggregateReference we just going to assume it is of the correct type, + // because it was already converted. + Class objectType = targetDescriptor.getObjectType(); + if (objectType.isAssignableFrom(AggregateReference.class)) { + return source; + } + + Object id = ((AggregateReference) source).getId(); + + if (id == null) { + throw new IllegalStateException( + String.format("Aggregate references id must not be null when converting to %s from %s to %s", source, + sourceDescriptor, targetDescriptor)); + } + + return delegate.convert(id, TypeDescriptor.valueOf(id.getClass()), targetDescriptor); + } + } + + /** + * Convert any simple type to an {@link AggregateReference}. If the {@literal targetDescriptor} contains information + * about the generic type id will properly get converted to the desired type by the delegate + * {@link ConversionService}. + */ + @ReadingConverter + static class SimpleTypeToAggregateReferenceConverter implements GenericConverter { + + private final ConversionService delegate; + + SimpleTypeToAggregateReferenceConverter(ConversionService delegate) { + this.delegate = delegate; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, AggregateReference.class)); + } + + @Override + public Object convert(@Nullable Object source, TypeDescriptor sourceDescriptor, TypeDescriptor targetDescriptor) { + + if (source == null) { + return null; + } + + ResolvableType componentType = targetDescriptor.getResolvableType().getGenerics()[1]; + TypeDescriptor targetType = TypeDescriptor.valueOf(componentType.resolve()); + Object convertedId = delegate.convert(source, TypeDescriptor.valueOf(source.getClass()), targetType); + + return AggregateReference.to(convertedId); + } + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java index 84b3fc665d..4a2a13945a 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java @@ -26,7 +26,9 @@ import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.mapping.AggregateReference; @@ -220,16 +222,12 @@ public Object readValue(@Nullable Object value, TypeInformation type) { } if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) { - return getConversionService().convert(value, type.getType()); - } - - if (AggregateReference.class.isAssignableFrom(type.getType())) { - if (type.getType().isAssignableFrom(value.getClass())) { - return value; - } + TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(value.getClass()); + TypeDescriptor targetDescriptor = typeInformationToTypeDescriptor(type); - return readAggregateReference(value, type); + return getConversionService().convert(value, sourceDescriptor, + targetDescriptor); } if (value instanceof Array) { @@ -243,12 +241,11 @@ public Object readValue(@Nullable Object value, TypeInformation type) { return super.readValue(value, type); } - @SuppressWarnings("ConstantConditions") - private Object readAggregateReference(@Nullable Object value, TypeInformation type) { + private static TypeDescriptor typeInformationToTypeDescriptor(TypeInformation type) { - TypeInformation idType = type.getSuperTypeInformation(AggregateReference.class).getTypeArguments().get(1); + Class[] generics = type.getTypeArguments().stream().map(TypeInformation::getType).toArray(Class[]::new); - return AggregateReference.to(readValue(value, idType)); + return new TypeDescriptor(ResolvableType.forClassWithGenerics(type.getType(), generics), null, null); } /* @@ -263,10 +260,6 @@ public Object writeValue(@Nullable Object value, TypeInformation type) { return null; } - if (AggregateReference.class.isAssignableFrom(value.getClass())) { - return writeValue(((AggregateReference) value).getId(), type); - } - return super.writeValue(value, type); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java index e91329b8b6..a0a6809c1b 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java @@ -15,11 +15,15 @@ */ package org.springframework.data.jdbc.core.convert; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; @@ -36,8 +40,19 @@ */ public class JdbcCustomConversions extends CustomConversions { - private static final Collection STORE_CONVERTERS = Collections - .unmodifiableCollection(Jsr310TimestampBasedConverters.getConvertersToRegister()); + private static final Collection STORE_CONVERTERS; + + static { + + List converters = new ArrayList<>(Jsr310TimestampBasedConverters.getConvertersToRegister()); + + ConversionService conversionService = DefaultConversionService.getSharedInstance(); + converters.add(new AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter(conversionService)); + converters.add(new AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter(conversionService)); + + STORE_CONVERTERS = Collections.unmodifiableCollection(converters); + + } private static final StoreConversions STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER, STORE_CONVERTERS); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java new file mode 100644 index 0000000000..a585f2eefa --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2021 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 + * + * https://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.jdbc.core.convert; + +import org.junit.jupiter.api.Test; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.TypeDescriptor; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.data.jdbc.core.mapping.AggregateReference; + +import static org.assertj.core.api.Assertions.*; + +/** + * Tests for converters from an to {@link org.springframework.data.jdbc.core.mapping.AggregateReference}. + * + * @author Jens Schauder + */ +class AggregateReferenceConvertersUnitTests { + + AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter simpleToAggregate = new AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter(DefaultConversionService.getSharedInstance()); + AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter aggregateToSimple = new AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter(DefaultConversionService.getSharedInstance()); + + @Test // #992 + void convertsFromSimpleValue() { + + ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class, String.class, Integer.class); + final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23), new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null)); + + assertThat(converted).isEqualTo(AggregateReference.to(23)); + } + + @Test // #992 + void convertsFromSimpleValueThatNeedsSeparateConversion() { + + ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class, String.class, Long.class); + final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23), new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null)); + + assertThat(converted).isEqualTo(AggregateReference.to(23L)); + } + + @Test // #992 + void convertsFromSimpleValueWithMissingTypeInformation() { + + final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23), TypeDescriptor.valueOf(AggregateReference.class)); + + assertThat(converted).isEqualTo(AggregateReference.to(23)); + } + + @Test // #992 + void convertsToSimpleValue() { + + final AggregateReference source = AggregateReference.to(23); + + final Object converted = aggregateToSimple.convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(Integer.class)); + + assertThat(converted).isEqualTo(23); + } + + @Test // #992 + void convertsToSimpleValueThatNeedsSeparateConversion() { + + final AggregateReference source = AggregateReference.to(23); + + final Object converted = aggregateToSimple.convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(Long.class)); + + assertThat(converted).isEqualTo(23L); + } + + +} \ No newline at end of file diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 4e42a006ec..bf0984075d 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java index fad388e718..d83bad48d9 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java @@ -19,7 +19,9 @@ import java.util.Optional; import java.util.function.Function; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.convert.CustomConversions;