diff --git a/pom.xml b/pom.xml index 9be85dfb76..f0f583b51e 100644 --- a/pom.xml +++ b/pom.xml @@ -339,6 +339,13 @@ 0.1.4 test + + + org.jmolecules.integrations + jmolecules-spring + ${jmolecules-integration} + true + diff --git a/src/main/java/org/springframework/data/convert/CustomConversions.java b/src/main/java/org/springframework/data/convert/CustomConversions.java index a7fe4fd67a..5fe96bea8e 100644 --- a/src/main/java/org/springframework/data/convert/CustomConversions.java +++ b/src/main/java/org/springframework/data/convert/CustomConversions.java @@ -78,6 +78,7 @@ public class CustomConversions { defaults.addAll(JodaTimeConverters.getConvertersToRegister()); defaults.addAll(Jsr310Converters.getConvertersToRegister()); defaults.addAll(ThreeTenBackPortConverters.getConvertersToRegister()); + defaults.addAll(JMoleculesConverters.getConvertersToRegister()); DEFAULT_CONVERTERS = Collections.unmodifiableList(defaults); } diff --git a/src/main/java/org/springframework/data/convert/JMoleculesConverters.java b/src/main/java/org/springframework/data/convert/JMoleculesConverters.java new file mode 100644 index 0000000000..1f496f02d5 --- /dev/null +++ b/src/main/java/org/springframework/data/convert/JMoleculesConverters.java @@ -0,0 +1,69 @@ +/* + * 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.convert; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +import org.jmolecules.spring.AssociationToPrimitivesConverter; +import org.jmolecules.spring.IdentifierToPrimitivesConverter; +import org.jmolecules.spring.PrimitivesToAssociationConverter; +import org.jmolecules.spring.PrimitivesToIdentifierConverter; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; +import org.springframework.util.ClassUtils; + +/** + * Registers jMolecules converter implementations with {@link CustomConversions} if the former is on the classpath. + * + * @author Oliver Drotbohm + * @since 2.5 + */ +public class JMoleculesConverters { + + private static final boolean JMOLECULES_PRESENT = ClassUtils.isPresent( + "org.jmolecules.spring.IdentifierToPrimitivesConverter", + JMoleculesConverters.class.getClassLoader()); + + /** + * Returns all jMolecules-specific converters to be registered. + * + * @return will never be {@literal null}. + */ + public static Collection getConvertersToRegister() { + + if (!JMOLECULES_PRESENT) { + return Collections.emptyList(); + } + + List converters = new ArrayList<>(); + + Supplier conversionService = () -> DefaultConversionService.getSharedInstance(); + + IdentifierToPrimitivesConverter toPrimitives = new IdentifierToPrimitivesConverter(conversionService); + PrimitivesToIdentifierConverter toIdentifier = new PrimitivesToIdentifierConverter(conversionService); + + converters.add(toPrimitives); + converters.add(toIdentifier); + converters.add(new AssociationToPrimitivesConverter<>(toPrimitives)); + converters.add(new PrimitivesToAssociationConverter<>(toIdentifier)); + + return converters; + } +} diff --git a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java index 06a289690a..321503ad97 100644 --- a/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java @@ -43,9 +43,13 @@ public abstract class AbstractPersistentProperty

> implements PersistentProperty

{ private static final Field CAUSE_FIELD; + private static final Class ASSOCIATION_TYPE; static { + CAUSE_FIELD = ReflectionUtils.findRequiredField(Throwable.class, "cause"); + ASSOCIATION_TYPE = ReflectionUtils.loadIfPresent("org.jmolecules.ddd.types.Association", + AbstractPersistentProperty.class.getClassLoader()); } private final String name; @@ -241,7 +245,8 @@ public boolean isImmutable() { */ @Override public boolean isAssociation() { - return isAnnotationPresent(Reference.class); + return isAnnotationPresent(Reference.class) // + || ASSOCIATION_TYPE != null && ASSOCIATION_TYPE.isAssignableFrom(rawType); } /* diff --git a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java index 6c6294bd7a..b7eb902e8e 100644 --- a/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java @@ -70,7 +70,8 @@ public abstract class AnnotationBasedPersistentProperty

isWritable = Lazy .of(() -> !isTransient() && !isAnnotationPresent(ReadOnlyProperty.class)); - private final Lazy isReference = Lazy.of(() -> !isTransient() && isAnnotationPresent(Reference.class)); + private final Lazy isReference = Lazy.of(() -> !isTransient() // + && (isAnnotationPresent(Reference.class) || super.isAssociation())); private final Lazy isId = Lazy.of(() -> isAnnotationPresent(Id.class)); private final Lazy isVersion = Lazy.of(() -> isAnnotationPresent(Version.class)); diff --git a/src/main/java/org/springframework/data/util/ReflectionUtils.java b/src/main/java/org/springframework/data/util/ReflectionUtils.java index 3946db7c7a..54a5062ae3 100644 --- a/src/main/java/org/springframework/data/util/ReflectionUtils.java +++ b/src/main/java/org/springframework/data/util/ReflectionUtils.java @@ -475,4 +475,21 @@ public static Object getPrimitiveDefault(Class type) { throw new IllegalArgumentException(String.format("Primitive type %s not supported!", type)); } + /** + * Loads the class with the given name using the given {@link ClassLoader}. + * + * @param name the name of the class to be loaded. + * @param classLoader the {@link ClassLoader} to use to load the class. + * @return the {@link Class} or {@literal null} in case the class can't be loaded for any reason. + * @since 2.5 + */ + @Nullable + public static Class loadIfPresent(String name, ClassLoader classLoader) { + + try { + return ClassUtils.forName(name, classLoader); + } catch (Exception o_O) { + return null; + } + } } diff --git a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java index c37cbc2512..adf63a721e 100644 --- a/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java +++ b/src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.convert; import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; import java.text.DateFormat; @@ -28,6 +29,8 @@ import java.util.Map; import java.util.function.Predicate; +import org.jmolecules.ddd.types.Association; +import org.jmolecules.ddd.types.Identifier; import org.joda.time.DateTime; import org.junit.jupiter.api.Test; import org.springframework.aop.framework.ProxyFactory; @@ -272,6 +275,24 @@ void doesNotSkipUserConverterConverterEvenWhenConfigurationWouldNotAllowIt() { verify(registry).addConverter(any(LocalDateTimeToDateConverter.class)); } + @Test // GH-2315 + void addsAssociationConvertersByDefault() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, Collections.emptyList()); + + assertThat(conversions.hasCustomWriteTarget(Association.class)).isTrue(); + assertThat(conversions.hasCustomReadTarget(Object.class, Association.class)).isTrue(); + } + + @Test // GH-2315 + void addsIdentifierConvertersByDefault() { + + CustomConversions conversions = new CustomConversions(StoreConversions.NONE, Collections.emptyList()); + + assertThat(conversions.hasCustomWriteTarget(Identifier.class)).isTrue(); + assertThat(conversions.hasCustomReadTarget(String.class, Identifier.class)).isTrue(); + } + private static Class createProxyTypeFor(Class type) { ProxyFactory factory = new ProxyFactory(); diff --git a/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java index 462b818a94..e17b8ec7b0 100755 --- a/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java @@ -224,6 +224,14 @@ void returnsAccessorsForGenericReturnType() { assertThat(property.getGetter()).isNotNull(); } + @Test // GH-2315 + void detectsJMoleculesAssociation() { + + SamplePersistentProperty property = getProperty(JMolecules.class, "association"); + + assertThat(property.isAssociation()).isTrue(); + } + private BasicPersistentEntity getEntity(Class type) { return new BasicPersistentEntity<>(ClassTypeInformation.from(type)); } @@ -344,11 +352,6 @@ public boolean isVersionProperty() { return false; } - @Override - public boolean isAssociation() { - return false; - } - @Override protected Association createAssociation() { return null; @@ -387,4 +390,8 @@ static class Sample { class TreeMapWrapper { TreeMap> map; } + + class JMolecules { + org.jmolecules.ddd.types.Association association; + } } diff --git a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java index f5daba5210..35c8db2a92 100755 --- a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java @@ -26,6 +26,7 @@ import java.util.Optional; import java.util.stream.Stream; +import org.jmolecules.ddd.types.Association; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AliasFor; @@ -293,6 +294,11 @@ public void missingRequiredFieldThrowsException() { .withMessageContaining(NoField.class.getName()); } + @Test // GH-2315 + void detectesJMoleculesAssociation() { + assertThat(getProperty(JMolecules.class, "association").isAssociation()).isTrue(); + } + @SuppressWarnings("unchecked") private Map, Annotation> getAnnotationCache(SamplePersistentProperty property) { return (Map, Annotation>) ReflectionTestUtils.getField(property, "annotationCache"); @@ -414,8 +420,7 @@ public String getProperty() { @Retention(RetentionPolicy.RUNTIME) @Target(value = { FIELD, METHOD, ANNOTATION_TYPE }) @Id - public @interface MyId { - } + public @interface MyId {} static class FieldAccess { String name; @@ -477,4 +482,8 @@ interface NoField { String getFirstname(); } + + static class JMolecules { + Association association; + } }