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