Skip to content

Commit 2f64304

Browse files
committed
Support for jMolecules' Association type.
We no recognize properties of type org.jmolecules.ddd.types.Association as associations in our PersistentProperty model. Also, we now register JMolecules Converter implementations for Association and Identifier in CustomConversions so that they can persisted like their embedded primitive value out of the box. Fixes #2315.
1 parent 3049307 commit 2f64304

9 files changed

+146
-9
lines changed

pom.xml

+7
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,13 @@
339339
<version>0.1.4</version>
340340
<scope>test</scope>
341341
</dependency>
342+
343+
<dependency>
344+
<groupId>org.jmolecules.integrations</groupId>
345+
<artifactId>jmolecules-spring</artifactId>
346+
<version>${jmolecules-integration}</version>
347+
<optional>true</optional>
348+
</dependency>
342349

343350
</dependencies>
344351

src/main/java/org/springframework/data/convert/CustomConversions.java

+1
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ public class CustomConversions {
7878
defaults.addAll(JodaTimeConverters.getConvertersToRegister());
7979
defaults.addAll(Jsr310Converters.getConvertersToRegister());
8080
defaults.addAll(ThreeTenBackPortConverters.getConvertersToRegister());
81+
defaults.addAll(JMoleculesConverters.getConvertersToRegister());
8182

8283
DEFAULT_CONVERTERS = Collections.unmodifiableList(defaults);
8384
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.convert;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.Collections;
21+
import java.util.List;
22+
import java.util.function.Supplier;
23+
24+
import org.jmolecules.spring.AssociationToPrimitivesConverter;
25+
import org.jmolecules.spring.IdentifierToPrimitivesConverter;
26+
import org.jmolecules.spring.PrimitivesToAssociationConverter;
27+
import org.jmolecules.spring.PrimitivesToIdentifierConverter;
28+
import org.springframework.core.convert.ConversionService;
29+
import org.springframework.core.convert.support.DefaultConversionService;
30+
import org.springframework.util.ClassUtils;
31+
32+
/**
33+
* Registers jMolecules converter implementations with {@link CustomConversions} if the former is on the classpath.
34+
*
35+
* @author Oliver Drotbohm
36+
* @since 2.5
37+
*/
38+
public class JMoleculesConverters {
39+
40+
private static final boolean JMOLECULES_PRESENT = ClassUtils.isPresent(
41+
"org.jmolecules.spring.IdentifierToPrimitivesConverter",
42+
JMoleculesConverters.class.getClassLoader());
43+
44+
/**
45+
* Returns all jMolecules-specific converters to be registered.
46+
*
47+
* @return will never be {@literal null}.
48+
*/
49+
public static Collection<Object> getConvertersToRegister() {
50+
51+
if (!JMOLECULES_PRESENT) {
52+
return Collections.emptyList();
53+
}
54+
55+
List<Object> converters = new ArrayList<>();
56+
57+
Supplier<ConversionService> conversionService = () -> DefaultConversionService.getSharedInstance();
58+
59+
IdentifierToPrimitivesConverter toPrimitives = new IdentifierToPrimitivesConverter(conversionService);
60+
PrimitivesToIdentifierConverter toIdentifier = new PrimitivesToIdentifierConverter(conversionService);
61+
62+
converters.add(toPrimitives);
63+
converters.add(toIdentifier);
64+
converters.add(new AssociationToPrimitivesConverter<>(toPrimitives));
65+
converters.add(new PrimitivesToAssociationConverter<>(toIdentifier));
66+
67+
return converters;
68+
}
69+
}

src/main/java/org/springframework/data/mapping/model/AbstractPersistentProperty.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,13 @@
4343
public abstract class AbstractPersistentProperty<P extends PersistentProperty<P>> implements PersistentProperty<P> {
4444

4545
private static final Field CAUSE_FIELD;
46+
private static final Class<?> ASSOCIATION_TYPE;
4647

4748
static {
49+
4850
CAUSE_FIELD = ReflectionUtils.findRequiredField(Throwable.class, "cause");
51+
ASSOCIATION_TYPE = ReflectionUtils.loadIfPresent("org.jmolecules.ddd.types.Association",
52+
AbstractPersistentProperty.class.getClassLoader());
4953
}
5054

5155
private final String name;
@@ -241,7 +245,8 @@ public boolean isImmutable() {
241245
*/
242246
@Override
243247
public boolean isAssociation() {
244-
return isAnnotationPresent(Reference.class);
248+
return isAnnotationPresent(Reference.class) //
249+
|| ASSOCIATION_TYPE != null && ASSOCIATION_TYPE.isAssignableFrom(rawType);
245250
}
246251

247252
/*

src/main/java/org/springframework/data/mapping/model/AnnotationBasedPersistentProperty.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,8 @@ public abstract class AnnotationBasedPersistentProperty<P extends PersistentProp
7070

7171
private final Lazy<Boolean> isWritable = Lazy
7272
.of(() -> !isTransient() && !isAnnotationPresent(ReadOnlyProperty.class));
73-
private final Lazy<Boolean> isReference = Lazy.of(() -> !isTransient() && isAnnotationPresent(Reference.class));
73+
private final Lazy<Boolean> isReference = Lazy.of(() -> !isTransient() //
74+
&& (isAnnotationPresent(Reference.class) || super.isAssociation()));
7475
private final Lazy<Boolean> isId = Lazy.of(() -> isAnnotationPresent(Id.class));
7576
private final Lazy<Boolean> isVersion = Lazy.of(() -> isAnnotationPresent(Version.class));
7677

src/main/java/org/springframework/data/util/ReflectionUtils.java

+17
Original file line numberDiff line numberDiff line change
@@ -475,4 +475,21 @@ public static Object getPrimitiveDefault(Class<?> type) {
475475
throw new IllegalArgumentException(String.format("Primitive type %s not supported!", type));
476476
}
477477

478+
/**
479+
* Loads the class with the given name using the given {@link ClassLoader}.
480+
*
481+
* @param name the name of the class to be loaded.
482+
* @param classLoader the {@link ClassLoader} to use to load the class.
483+
* @return the {@link Class} or {@literal null} in case the class can't be loaded for any reason.
484+
* @since 2.5
485+
*/
486+
@Nullable
487+
public static Class<?> loadIfPresent(String name, ClassLoader classLoader) {
488+
489+
try {
490+
return ClassUtils.forName(name, classLoader);
491+
} catch (Exception o_O) {
492+
return null;
493+
}
494+
}
478495
}

src/test/java/org/springframework/data/convert/CustomConversionsUnitTests.java

+21
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.convert;
1717

1818
import static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.ArgumentMatchers.*;
1920
import static org.mockito.Mockito.*;
2021

2122
import java.text.DateFormat;
@@ -28,6 +29,8 @@
2829
import java.util.Map;
2930
import java.util.function.Predicate;
3031

32+
import org.jmolecules.ddd.types.Association;
33+
import org.jmolecules.ddd.types.Identifier;
3134
import org.joda.time.DateTime;
3235
import org.junit.jupiter.api.Test;
3336
import org.springframework.aop.framework.ProxyFactory;
@@ -272,6 +275,24 @@ void doesNotSkipUserConverterConverterEvenWhenConfigurationWouldNotAllowIt() {
272275
verify(registry).addConverter(any(LocalDateTimeToDateConverter.class));
273276
}
274277

278+
@Test // GH-2315
279+
void addsAssociationConvertersByDefault() {
280+
281+
CustomConversions conversions = new CustomConversions(StoreConversions.NONE, Collections.emptyList());
282+
283+
assertThat(conversions.hasCustomWriteTarget(Association.class)).isTrue();
284+
assertThat(conversions.hasCustomReadTarget(Object.class, Association.class)).isTrue();
285+
}
286+
287+
@Test // GH-2315
288+
void addsIdentifierConvertersByDefault() {
289+
290+
CustomConversions conversions = new CustomConversions(StoreConversions.NONE, Collections.emptyList());
291+
292+
assertThat(conversions.hasCustomWriteTarget(Identifier.class)).isTrue();
293+
assertThat(conversions.hasCustomReadTarget(String.class, Identifier.class)).isTrue();
294+
}
295+
275296
private static Class<?> createProxyTypeFor(Class<?> type) {
276297

277298
ProxyFactory factory = new ProxyFactory();

src/test/java/org/springframework/data/mapping/model/AbstractPersistentPropertyUnitTests.java

+12-5
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,14 @@ void returnsAccessorsForGenericReturnType() {
224224
assertThat(property.getGetter()).isNotNull();
225225
}
226226

227+
@Test // GH-2315
228+
void detectsJMoleculesAssociation() {
229+
230+
SamplePersistentProperty property = getProperty(JMolecules.class, "association");
231+
232+
assertThat(property.isAssociation()).isTrue();
233+
}
234+
227235
private <T> BasicPersistentEntity<T, SamplePersistentProperty> getEntity(Class<T> type) {
228236
return new BasicPersistentEntity<>(ClassTypeInformation.from(type));
229237
}
@@ -344,11 +352,6 @@ public boolean isVersionProperty() {
344352
return false;
345353
}
346354

347-
@Override
348-
public boolean isAssociation() {
349-
return false;
350-
}
351-
352355
@Override
353356
protected Association<SamplePersistentProperty> createAssociation() {
354357
return null;
@@ -387,4 +390,8 @@ static class Sample {
387390
class TreeMapWrapper {
388391
TreeMap<String, TreeMap<String, String>> map;
389392
}
393+
394+
class JMolecules {
395+
org.jmolecules.ddd.types.Association association;
396+
}
390397
}

src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java

+11-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.Optional;
2727
import java.util.stream.Stream;
2828

29+
import org.jmolecules.ddd.types.Association;
2930
import org.junit.jupiter.api.BeforeEach;
3031
import org.junit.jupiter.api.Test;
3132
import org.springframework.core.annotation.AliasFor;
@@ -293,6 +294,11 @@ public void missingRequiredFieldThrowsException() {
293294
.withMessageContaining(NoField.class.getName());
294295
}
295296

297+
@Test // GH-2315
298+
void detectesJMoleculesAssociation() {
299+
assertThat(getProperty(JMolecules.class, "association").isAssociation()).isTrue();
300+
}
301+
296302
@SuppressWarnings("unchecked")
297303
private Map<Class<? extends Annotation>, Annotation> getAnnotationCache(SamplePersistentProperty property) {
298304
return (Map<Class<? extends Annotation>, Annotation>) ReflectionTestUtils.getField(property, "annotationCache");
@@ -414,8 +420,7 @@ public String getProperty() {
414420
@Retention(RetentionPolicy.RUNTIME)
415421
@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })
416422
@Id
417-
public @interface MyId {
418-
}
423+
public @interface MyId {}
419424

420425
static class FieldAccess {
421426
String name;
@@ -477,4 +482,8 @@ interface NoField {
477482

478483
String getFirstname();
479484
}
485+
486+
static class JMolecules {
487+
Association association;
488+
}
480489
}

0 commit comments

Comments
 (0)