From 23c184c1fb4254818d6fe5f89e9f33cb6250dc80 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 30 Nov 2020 10:13:15 +0100 Subject: [PATCH 1/2] DATACMNS-1699 - Prepare issue branch. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index d395a08985..718b738416 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-commons - 2.5.0-SNAPSHOT + 2.5.0-DATACMNS-1699-SNAPSHOT Spring Data Core From 6848bfa06f102975e4ed60e286d91832f6e4be56 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 30 Nov 2020 13:48:14 +0100 Subject: [PATCH 2/2] DATACMNS-1699 - Add Embedded annotation. Add core annotation for embedded object support and consider a potential prefix when using FieldNamingStrategies. The actual store specific implementation is done in the individual modules that make sure the mapping context provides means to detect embedded types and provide this information to the PersistentPropertyPathFactory now using PersistentEntities instead of the TypeInformation to construct mapped paths. --- .../data/annotation/Embedded.java | 136 ++++++++++++++++++ .../data/mapping/PersistentProperty.java | 23 +++ .../PersistentPropertyPathFactory.java | 11 +- ...CamelCaseSplittingFieldNamingStrategy.java | 10 +- .../PropertyNameFieldNamingStrategy.java | 15 +- ...ationBasedPersistentPropertyUnitTests.java | 25 ++++ ...reviatingFieldNamingStrategyUnitTests.java | 28 +++- ...pertyNameFieldNamingStrategyUnitTests.java | 68 +++++++++ ...SnakeCaseFieldNamingStrategyUnitTests.java | 28 +++- 9 files changed, 335 insertions(+), 9 deletions(-) create mode 100644 src/main/java/org/springframework/data/annotation/Embedded.java create mode 100644 src/test/java/org/springframework/data/mapping/model/PropertyNameFieldNamingStrategyUnitTests.java diff --git a/src/main/java/org/springframework/data/annotation/Embedded.java b/src/main/java/org/springframework/data/annotation/Embedded.java new file mode 100644 index 0000000000..eb110e059c --- /dev/null +++ b/src/main/java/org/springframework/data/annotation/Embedded.java @@ -0,0 +1,136 @@ +/* + * Copyright 2020 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.annotation.meta.When; + +import org.springframework.core.annotation.AliasFor; + +/** + * The annotation to configure a value object as embedded in the current data structure (table/collection/...). + *

+ * Depending on the {@link OnEmpty value} of {@link #onEmpty()} the property is set to {@literal null} or an empty + * instance in the case all embedded values are {@literal null} when reading from the result set. + * + * @author Christoph Strobl + * @since 2.5 + */ +@Documented +@Retention(value = RetentionPolicy.RUNTIME) +@Target(value = { ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.METHOD }) +public @interface Embedded { + + /** + * Set the load strategy for the embedded object if all contained fields yield {@literal null} values. + *

+ * {@link Nullable @Embedded.Nullable} and {@link Empty @Embedded.Empty} offer shortcuts for this. + * + * @return never {@link} null. + */ + OnEmpty onEmpty(); + + /** + * @return prefix for columns in the embedded value object. An empty {@link String} by default. + */ + String prefix() default ""; + + /** + * Load strategy to be used {@link Embedded#onEmpty()}. + * + * @author Christoph Strobl + */ + enum OnEmpty { + USE_NULL, USE_EMPTY + } + + /** + * Shortcut for a nullable embedded property. + * + *

+	 * @Embedded.Nullable private Address address;
+	 * 
+ * + * as alternative to the more verbose + * + *
+	 * @Embedded(onEmpty = USE_NULL) @javax.annotation.Nonnull(when = When.MAYBE) private Address address;
+	 * 
+ * + * @author Christoph Strobl + * @see Embedded#onEmpty() + */ + @Embedded(onEmpty = OnEmpty.USE_NULL) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.METHOD }) + @javax.annotation.Nonnull(when = When.MAYBE) + @interface Nullable { + + /** + * @return prefix for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "prefix") + String prefix() default ""; + + /** + * @return value for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "prefix") + String value() default ""; + } + + /** + * Shortcut for an empty embedded property. + * + *
+	 * @Embedded.Empty private Address address;
+	 * 
+ * + * as alternative to the more verbose + * + *
+	 * @Embedded(onEmpty = USE_EMPTY) @javax.annotation.Nonnull(when = When.NEVER) private Address address;
+	 * 
+ * + * @author Christoph Strobl + * @see Embedded#onEmpty() + */ + @Embedded(onEmpty = OnEmpty.USE_EMPTY) + @Documented + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.FIELD, ElementType.METHOD }) + @javax.annotation.Nonnull(when = When.NEVER) + @interface Empty { + + /** + * @return prefix for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "prefix") + String prefix() default ""; + + /** + * @return value for columns in the embedded value object. An empty {@link String} by default. + */ + @AliasFor(annotation = Embedded.class, attribute = "prefix") + String value() default ""; + } +} diff --git a/src/main/java/org/springframework/data/mapping/PersistentProperty.java b/src/main/java/org/springframework/data/mapping/PersistentProperty.java index 33d9dd9881..611a4b7bb8 100644 --- a/src/main/java/org/springframework/data/mapping/PersistentProperty.java +++ b/src/main/java/org/springframework/data/mapping/PersistentProperty.java @@ -16,6 +16,7 @@ package org.springframework.data.mapping; import java.lang.annotation.Annotation; +import java.lang.annotation.ElementType; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.Collection; @@ -23,6 +24,9 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.data.annotation.Embedded; +import org.springframework.data.annotation.Embedded.OnEmpty; +import org.springframework.data.util.NullableUtils; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -33,6 +37,7 @@ * @author Oliver Gierke * @author Mark Paluch * @author Jens Schauder + * @author Christoph Strobl */ public interface PersistentProperty

> { @@ -276,6 +281,24 @@ default Association

getRequiredAssociation() { */ boolean isAssociation(); + /** + * @return {@literal true} if the property should be embedded. + * @since 2.5 + */ + default boolean isEmbedded() { + return isEntity() && findAnnotation(Embedded.class) != null; + } + + /** + * @return {@literal true} if the property generally allows {@literal null} values; + * @since 2.5 + */ + default boolean isNullable() { + + return (isEmbedded() && findAnnotation(Embedded.class).onEmpty().equals(OnEmpty.USE_NULL)) + && !NullableUtils.isNonNull(getField(), ElementType.FIELD); + } + /** * Returns the component type of the type if it is a {@link java.util.Collection}. Will return the type of the key if * the property is a {@link java.util.Map}. diff --git a/src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java b/src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java index 9ffcfb886e..c09d2e4060 100644 --- a/src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java +++ b/src/main/java/org/springframework/data/mapping/context/PersistentPropertyPathFactory.java @@ -42,6 +42,7 @@ * A factory implementation to create {@link PersistentPropertyPath} instances in various ways. * * @author Oliver Gierke + * @author Christoph Strobl * @since 2.1 * @soundtrack Cypress Hill - Boom Biddy Bye Bye (Fugees Remix, Unreleased & Revamped) */ @@ -222,8 +223,7 @@ private Pair, E> getPair(DefaultPersistentPrope return null; } - TypeInformation type = property.getTypeInformation().getRequiredActualType(); - return Pair.of(path.append(property), iterator.hasNext() ? context.getRequiredPersistentEntity(type) : entity); + return Pair.of(path.append(property), iterator.hasNext() ? context.getRequiredPersistentEntity(property) : entity); } private Collection> from(TypeInformation type, Predicate filter, @@ -236,6 +236,11 @@ private Collection> from(TypeInformation type, } E entity = context.getRequiredPersistentEntity(actualType); + return from(entity, filter, traversalGuard, basePath); + } + + private Collection> from(E entity, Predicate filter, Predicate

traversalGuard, + DefaultPersistentPropertyPath

basePath) { Set> properties = new HashSet<>(); PropertyHandler

propertyTester = persistentProperty -> { @@ -254,7 +259,7 @@ private Collection> from(TypeInformation type, } if (traversalGuard.and(IS_ENTITY).test(persistentProperty)) { - properties.addAll(from(typeInformation, filter, traversalGuard, currentPath)); + properties.addAll(from(context.getPersistentEntity(persistentProperty), filter, traversalGuard, currentPath)); } }; diff --git a/src/main/java/org/springframework/data/mapping/model/CamelCaseSplittingFieldNamingStrategy.java b/src/main/java/org/springframework/data/mapping/model/CamelCaseSplittingFieldNamingStrategy.java index 0bc854b1ea..a31de4a35d 100644 --- a/src/main/java/org/springframework/data/mapping/model/CamelCaseSplittingFieldNamingStrategy.java +++ b/src/main/java/org/springframework/data/mapping/model/CamelCaseSplittingFieldNamingStrategy.java @@ -18,6 +18,7 @@ import java.util.ArrayList; import java.util.List; +import org.springframework.data.annotation.Embedded; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.util.ParsingUtils; import org.springframework.util.Assert; @@ -28,6 +29,7 @@ * configured delimiter. Individual parts of the name can be manipulated using {@link #preparePart(String)}. * * @author Oliver Gierke + * @author Christoph Strobl * @since 1.9 */ public class CamelCaseSplittingFieldNamingStrategy implements FieldNamingStrategy { @@ -52,7 +54,13 @@ public CamelCaseSplittingFieldNamingStrategy(String delimiter) { @Override public String getFieldName(PersistentProperty property) { - List parts = ParsingUtils.splitCamelCaseToLower(property.getName()); + List parts = new ArrayList<>(ParsingUtils.splitCamelCaseToLower(property.getName())); + if(property.isEmbedded()) { + String prefix = property.findAnnotation(Embedded.class).prefix(); + if(StringUtils.hasText(prefix)) { + parts.add(0, prefix); + } + } List result = new ArrayList<>(); for (String part : parts) { diff --git a/src/main/java/org/springframework/data/mapping/model/PropertyNameFieldNamingStrategy.java b/src/main/java/org/springframework/data/mapping/model/PropertyNameFieldNamingStrategy.java index e214a8ebed..40497d86a3 100644 --- a/src/main/java/org/springframework/data/mapping/model/PropertyNameFieldNamingStrategy.java +++ b/src/main/java/org/springframework/data/mapping/model/PropertyNameFieldNamingStrategy.java @@ -15,13 +15,16 @@ */ package org.springframework.data.mapping.model; +import org.springframework.data.annotation.Embedded; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.util.StringUtils; /** * {@link FieldNamingStrategy} simply using the {@link PersistentProperty}'s name. * * @since 1.9 * @author Oliver Gierke + * @author Christoph Strobl */ public enum PropertyNameFieldNamingStrategy implements FieldNamingStrategy { @@ -32,6 +35,16 @@ public enum PropertyNameFieldNamingStrategy implements FieldNamingStrategy { * @see org.springframework.data.mapping.model.FieldNamingStrategy#getFieldName(org.springframework.data.mapping.PersistentProperty) */ public String getFieldName(PersistentProperty property) { - return property.getName(); + + if (!property.isEmbedded()) { + return property.getName(); + } + + String prefix = property.findAnnotation(Embedded.class).prefix(); + if (!StringUtils.hasText(prefix)) { + return property.getName(); + } + + return prefix + property.getName(); } } 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 a1205d0240..dd096c1501 100755 --- a/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/AnnotationBasedPersistentPropertyUnitTests.java @@ -32,6 +32,7 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.data.annotation.AccessType; import org.springframework.data.annotation.AccessType.Type; +import org.springframework.data.annotation.Embedded; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.ReadOnlyProperty; import org.springframework.data.annotation.Reference; @@ -293,6 +294,24 @@ public void missingRequiredFieldThrowsException() { .withMessageContaining(NoField.class.getName()); } + @Test // DATACMNS-1699 + public void detectsNullableEmbeddedAnnotation() { + + SamplePersistentProperty property = getProperty(WithEmbeddedField.class, "nullableEmbeddedField"); + + assertThat(property.isEmbedded()).isTrue(); + assertThat(property.isNullable()).isTrue(); + } + + @Test // DATACMNS-1699 + public void detectsEmptyEmbeddedAnnotation() { + + SamplePersistentProperty property = getProperty(WithEmbeddedField.class, "emptyEmbeddedField"); + + assertThat(property.isEmbedded()).isTrue(); + assertThat(property.isNullable()).isFalse(); + } + @SuppressWarnings("unchecked") private Map, Annotation> getAnnotationCache(SamplePersistentProperty property) { return (Map, Annotation>) ReflectionTestUtils.getField(property, "annotationCache"); @@ -477,4 +496,10 @@ interface NoField { String getFirstname(); } + + static class WithEmbeddedField { + + @Embedded.Nullable Sample nullableEmbeddedField; + @Embedded.Empty Sample emptyEmbeddedField; + } } diff --git a/src/test/java/org/springframework/data/mapping/model/CamelCaseAbbreviatingFieldNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/CamelCaseAbbreviatingFieldNamingStrategyUnitTests.java index f4a3bbff4d..e491bb508c 100755 --- a/src/test/java/org/springframework/data/mapping/model/CamelCaseAbbreviatingFieldNamingStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/CamelCaseAbbreviatingFieldNamingStrategyUnitTests.java @@ -15,20 +15,21 @@ */ package org.springframework.data.mapping.model; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.mockito.Mockito.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - +import org.springframework.data.annotation.Embedded; import org.springframework.data.mapping.PersistentProperty; /** * Unit tests for {@link CamelCaseAbbreviatingFieldNamingStrategy}. * * @author Oliver Gierke + * @author Christoph Strobl * @since 1.9 */ @ExtendWith(MockitoExtension.class) @@ -37,6 +38,7 @@ public class CamelCaseAbbreviatingFieldNamingStrategyUnitTests { FieldNamingStrategy strategy = new CamelCaseAbbreviatingFieldNamingStrategy(); @Mock PersistentProperty property; + @Mock Embedded embedded; @Test // DATACMNS-523 void abbreviatesToCamelCase() { @@ -45,6 +47,28 @@ void abbreviatesToCamelCase() { assertFieldNameForPropertyName("fooBARFooBar", "fbfb"); } + @Test // DATACMNS-1699 + void embeddedWithoutPrefix() { + + when(property.getName()).thenReturn("propertyName"); + when(property.isEmbedded()).thenReturn(true); + when(property.findAnnotation(eq(Embedded.class))).thenReturn(embedded); + when(embedded.prefix()).thenReturn(""); + + assertThat(strategy.getFieldName(property)).isEqualTo("pn"); + } + + @Test // DATACMNS-1699 + void embeddedWithPrefix() { + + when(property.getName()).thenReturn("propertyName"); + when(property.isEmbedded()).thenReturn(true); + when(property.findAnnotation(eq(Embedded.class))).thenReturn(embedded); + when(embedded.prefix()).thenReturn("prefix"); + + assertThat(strategy.getFieldName(property)).isEqualTo("ppn"); + } + private void assertFieldNameForPropertyName(String propertyName, String fieldName) { when(property.getName()).thenReturn(propertyName); diff --git a/src/test/java/org/springframework/data/mapping/model/PropertyNameFieldNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/PropertyNameFieldNamingStrategyUnitTests.java new file mode 100644 index 0000000000..7a2cf9c1f1 --- /dev/null +++ b/src/test/java/org/springframework/data/mapping/model/PropertyNameFieldNamingStrategyUnitTests.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 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.mapping.model; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.*; +import static org.mockito.Mockito.*; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.annotation.Embedded; +import org.springframework.data.mapping.PersistentProperty; + +/** + * @author Christoph Strobl + */ +@ExtendWith(MockitoExtension.class) +class PropertyNameFieldNamingStrategyUnitTests { + + @Mock PersistentProperty property; + @Mock Embedded embedded; + + @Test // DATACMNS-1699 + void simpleName() { + + when(property.getName()).thenReturn("propertyName"); + when(property.isEmbedded()).thenReturn(false); + + assertThat(PropertyNameFieldNamingStrategy.INSTANCE.getFieldName(property)).isEqualTo("propertyName"); + } + + @Test // DATACMNS-1699 + void embeddedWithoutPrefix() { + + when(property.getName()).thenReturn("propertyName"); + when(property.isEmbedded()).thenReturn(true); + when(property.findAnnotation(eq(Embedded.class))).thenReturn(embedded); + when(embedded.prefix()).thenReturn(""); + + assertThat(PropertyNameFieldNamingStrategy.INSTANCE.getFieldName(property)).isEqualTo("propertyName"); + } + + @Test // DATACMNS-1699 + void embeddedWithPrefix() { + + when(property.getName()).thenReturn("propertyName"); + when(property.isEmbedded()).thenReturn(true); + when(property.findAnnotation(eq(Embedded.class))).thenReturn(embedded); + when(embedded.prefix()).thenReturn("prefix-"); + + assertThat(PropertyNameFieldNamingStrategy.INSTANCE.getFieldName(property)).isEqualTo("prefix-propertyName"); + } + +} diff --git a/src/test/java/org/springframework/data/mapping/model/SnakeCaseFieldNamingStrategyUnitTests.java b/src/test/java/org/springframework/data/mapping/model/SnakeCaseFieldNamingStrategyUnitTests.java index aa8cd04703..dd26fc34c0 100755 --- a/src/test/java/org/springframework/data/mapping/model/SnakeCaseFieldNamingStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/mapping/model/SnakeCaseFieldNamingStrategyUnitTests.java @@ -15,14 +15,14 @@ */ package org.springframework.data.mapping.model; -import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.AssertionsForClassTypes.*; import static org.mockito.Mockito.*; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - +import org.springframework.data.annotation.Embedded; import org.springframework.data.mapping.PersistentProperty; /** @@ -30,6 +30,7 @@ * * @author Ryan Tenney * @author Oliver Gierke + * @author Christoph Strobl * @since 1.9 */ @ExtendWith(MockitoExtension.class) @@ -38,6 +39,7 @@ class SnakeCaseFieldNamingStrategyUnitTests { private FieldNamingStrategy strategy = new SnakeCaseFieldNamingStrategy(); @Mock PersistentProperty property; + @Mock Embedded embedded; @Test // DATACMNS-523 void rendersSnakeCaseFieldNames() { @@ -48,6 +50,28 @@ void rendersSnakeCaseFieldNames() { assertFieldNameForPropertyName("FOO_BAR", "foo_bar"); } + @Test // DATACMNS-1699 + void embeddedWithoutPrefix() { + + when(property.getName()).thenReturn("propertyName"); + when(property.isEmbedded()).thenReturn(true); + when(property.findAnnotation(eq(Embedded.class))).thenReturn(embedded); + when(embedded.prefix()).thenReturn(""); + + assertThat(strategy.getFieldName(property)).isEqualTo("property_name"); + } + + @Test // DATACMNS-1699 + void embeddedWithPrefix() { + + when(property.getName()).thenReturn("propertyName"); + when(property.isEmbedded()).thenReturn(true); + when(property.findAnnotation(eq(Embedded.class))).thenReturn(embedded); + when(embedded.prefix()).thenReturn("prefix"); + + assertThat(strategy.getFieldName(property)).isEqualTo("prefix_property_name"); + } + private void assertFieldNameForPropertyName(String propertyName, String fieldName) { when(property.getName()).thenReturn(propertyName);