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 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);