Skip to content

DATACMNS-1699 - Add Embedded annotation. #480

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<version>2.5.0-SNAPSHOT</version>
<version>2.5.0-DATACMNS-1699-SNAPSHOT</version>

<name>Spring Data Core</name>

Expand Down
136 changes: 136 additions & 0 deletions src/main/java/org/springframework/data/annotation/Embedded.java
Original file line number Diff line number Diff line change
@@ -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/...).
* <p />
* 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.
* <p />
* {@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.
*
* <pre class="code">
* &#64;Embedded.Nullable private Address address;
* </pre>
*
* as alternative to the more verbose
*
* <pre class="code">
* &#64;Embedded(onEmpty = USE_NULL) &#64;javax.annotation.Nonnull(when = When.MAYBE) private Address address;
* </pre>
*
* @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.
*
* <pre class="code">
* &#64;Embedded.Empty private Address address;
* </pre>
*
* as alternative to the more verbose
*
* <pre class="code">
* &#64;Embedded(onEmpty = USE_EMPTY) &#64;javax.annotation.Nonnull(when = When.NEVER) private Address address;
* </pre>
*
* @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 "";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,17 @@
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;
import java.util.Map;

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;
Expand All @@ -33,6 +37,7 @@
* @author Oliver Gierke
* @author Mark Paluch
* @author Jens Schauder
* @author Christoph Strobl
*/
public interface PersistentProperty<P extends PersistentProperty<P>> {

Expand Down Expand Up @@ -276,6 +281,24 @@ default Association<P> 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))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a property is not embedded, it is never nullable. If that is intended, it should be described in the documentation and the superfluous parens around the first && term should be removed.

Or the expression should get fixed. Also we should have tests for the not embedded case.

&& !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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
*/
Expand Down Expand Up @@ -222,8 +223,7 @@ private Pair<DefaultPersistentPropertyPath<P>, 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 <T> Collection<PersistentPropertyPath<P>> from(TypeInformation<T> type, Predicate<? super P> filter,
Expand All @@ -236,6 +236,11 @@ private <T> Collection<PersistentPropertyPath<P>> from(TypeInformation<T> type,
}

E entity = context.getRequiredPersistentEntity(actualType);
return from(entity, filter, traversalGuard, basePath);
}

private Collection<PersistentPropertyPath<P>> from(E entity, Predicate<? super P> filter, Predicate<P> traversalGuard,
DefaultPersistentPropertyPath<P> basePath) {
Set<PersistentPropertyPath<P>> properties = new HashSet<>();

PropertyHandler<P> propertyTester = persistentProperty -> {
Expand All @@ -254,7 +259,7 @@ private <T> Collection<PersistentPropertyPath<P>> from(TypeInformation<T> type,
}

if (traversalGuard.and(IS_ENTITY).test(persistentProperty)) {
properties.addAll(from(typeInformation, filter, traversalGuard, currentPath));
properties.addAll(from(context.getPersistentEntity(persistentProperty), filter, traversalGuard, currentPath));
}
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Expand All @@ -52,7 +54,13 @@ public CamelCaseSplittingFieldNamingStrategy(String delimiter) {
@Override
public String getFieldName(PersistentProperty<?> property) {

List<String> parts = ParsingUtils.splitCamelCaseToLower(property.getName());
List<String> 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<String> result = new ArrayList<>();

for (String part : parts) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Class<? extends Annotation>, Annotation> getAnnotationCache(SamplePersistentProperty property) {
return (Map<Class<? extends Annotation>, Annotation>) ReflectionTestUtils.getField(property, "annotationCache");
Expand Down Expand Up @@ -477,4 +496,10 @@ interface NoField {

String getFirstname();
}

static class WithEmbeddedField {

@Embedded.Nullable Sample nullableEmbeddedField;
@Embedded.Empty Sample emptyEmbeddedField;
}
}
Loading