From 1764457835c20c325fbd8da5b9019f7688c23c8e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 5 Mar 2019 15:08:38 +0100 Subject: [PATCH] DATAJDBC-326 - Polishing. Add Javadoc. Tweak naming and factory methods. Add equals/hashcode to Identifier. Extend tests. Original pull request: #118. --- .../data/jdbc/core/DataAccessStrategy.java | 14 +- .../jdbc/core/DefaultDataAccessStrategy.java | 20 +- .../jdbc/core/DefaultJdbcInterpreter.java | 7 +- .../core/convert/JdbcIdentifierBuilder.java | 18 +- .../mybatis/MyBatisDataAccessStrategy.java | 29 +-- .../core/DefaultJdbcInterpreterUnitTests.java | 7 +- .../core/JdbcIdentifierBuilderUnitTests.java | 45 +---- .../data/relational/domain/Identifier.java | 173 +++++++++++++++--- .../domain/IdentifierUnitTests.java | 83 ++++++++- 9 files changed, 269 insertions(+), 127 deletions(-) diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java index ce731ce1..1ff274c2 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DataAccessStrategy.java @@ -17,9 +17,9 @@ package org.springframework.data.jdbc.core; import java.util.Map; -import org.springframework.data.relational.domain.Identifier; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; import org.springframework.lang.Nullable; /** @@ -40,25 +40,25 @@ public interface DataAccessStrategy { * to get referenced are contained in this map. Must not be {@code null}. * @param the type of the instance. * @return the id generated by the database if any. - * - * @deprecated use {@link #insert(Object, Class, Identifier)} instead. + * @deprecated since 1.1, use {@link #insert(Object, Class, Identifier)} instead. */ @Deprecated Object insert(T instance, Class domainType, Map additionalParameters); - /** * Inserts a the data of a single entity. Referenced entities don't get handled. * * @param instance the instance to be stored. Must not be {@code null}. * @param domainType the type of the instance. Must not be {@code null}. - * @param identifier information about data that needs to be considered for the insert but which is not part of the entity. - * Namely references back to a parent entity and key/index columns for entities that are stored in a {@link Map} or {@link java.util.List}. + * @param identifier information about data that needs to be considered for the insert but which is not part of the + * entity. Namely references back to a parent entity and key/index columns for entities that are stored in a + * {@link Map} or {@link java.util.List}. * @param the type of the instance. * @return the id generated by the database if any. + * @since 1.1 */ default Object insert(T instance, Class domainType, Identifier identifier){ - return insert(instance, domainType, identifier.getParametersByName()); + return insert(instance, domainType, identifier.toMap()); } /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java index 2f304441..4b3a48cf 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultDataAccessStrategy.java @@ -29,7 +29,6 @@ import java.util.stream.StreamSupport; import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPath; @@ -59,10 +58,6 @@ import org.springframework.util.Assert; @RequiredArgsConstructor public class DefaultDataAccessStrategy implements DataAccessStrategy { - private static final String ENTITY_NEW_AFTER_INSERT = "Entity [%s] still 'new' after insert. Please set either" - + " the id property in a BeforeInsert event handler, or ensure the database creates a value and your " - + "JDBC driver returns it."; - private final @NonNull SqlGeneratorSource sqlGeneratorSource; private final @NonNull RelationalMappingContext context; private final @NonNull RelationalConverter converter; @@ -89,7 +84,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { */ @Override public Object insert(T instance, Class domainType, Map additionalParameters) { - return insert(instance, domainType, JdbcIdentifierBuilder.from(additionalParameters).build()); + return insert(instance, domainType, Identifier.from(additionalParameters)); } /* @@ -102,10 +97,9 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { KeyHolder holder = new GeneratedKeyHolder(); RelationalPersistentEntity persistentEntity = getRequiredPersistentEntity(domainType); - Map parameters = new LinkedHashMap<>(); - identifier.forEach(identifierValue -> { - parameters.put(identifierValue.getName(), - converter.writeValue(identifierValue.getValue(), ClassTypeInformation.from(identifierValue.getTargetType()))); + Map parameters = new LinkedHashMap<>(identifier.size()); + identifier.forEach((name, value, type) -> { + parameters.put(name, converter.writeValue(value, ClassTypeInformation.from(type))); }); MapSqlParameterSource parameterSource = getPropertyMap(instance, persistentEntity, ""); @@ -298,7 +292,7 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { return result; } - private MapSqlParameterSource getPropertyMap(final S instance, RelationalPersistentEntity persistentEntity, + private MapSqlParameterSource getPropertyMap(S instance, RelationalPersistentEntity persistentEntity, String prefix) { MapSqlParameterSource parameters = new MapSqlParameterSource(); @@ -314,8 +308,8 @@ public class DefaultDataAccessStrategy implements DataAccessStrategy { if (property.isEmbedded()) { Object value = propertyAccessor.getProperty(property); - final RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getType()); - final MapSqlParameterSource additionalParameters = getPropertyMap((T) value, + RelationalPersistentEntity embeddedEntity = context.getPersistentEntity(property.getType()); + MapSqlParameterSource additionalParameters = getPropertyMap((T) value, (RelationalPersistentEntity) embeddedEntity, prefix + property.getEmbeddedPrefix()); parameters.addValues(additionalParameters.getValues()); } else { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java index b30ca91e..ab15fd71 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreter.java @@ -21,7 +21,6 @@ import java.util.Collections; import java.util.Map; import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; -import org.springframework.data.relational.domain.Identifier; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.conversion.DbAction; import org.springframework.data.relational.core.conversion.DbAction.Delete; @@ -37,6 +36,7 @@ import org.springframework.data.relational.core.conversion.Interpreter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; import org.springframework.lang.Nullable; /** @@ -142,7 +142,7 @@ class DefaultJdbcInterpreter implements Interpreter { accessStrategy.deleteAll(deleteAllRoot.getEntityType()); } - private Identifier getParentKeys(DbAction.WithDependingOn action) { + private Identifier getParentKeys(DbAction.WithDependingOn action) { DbAction.WithEntity dependingOn = action.getDependingOn(); @@ -152,7 +152,8 @@ class DefaultJdbcInterpreter implements Interpreter { JdbcIdentifierBuilder identifier = JdbcIdentifierBuilder // .forBackReferences(action.getPropertyPath(), id); - for (Map.Entry, Object> qualifier : action.getQualifiers().entrySet()) { + for (Map.Entry, Object> qualifier : action.getQualifiers() + .entrySet()) { identifier = identifier.withQualifier(qualifier.getKey(), qualifier.getValue()); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java index 181bddbb..1757df32 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcIdentifierBuilder.java @@ -15,14 +15,14 @@ */ package org.springframework.data.jdbc.core.convert; -import java.util.Map; - import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; import org.springframework.data.relational.domain.Identifier; import org.springframework.lang.Nullable; /** + * Builder for {@link Identifier}. Mainly for internal use within the framework + * * @author Jens Schauder * @since 1.1 */ @@ -38,23 +38,13 @@ public class JdbcIdentifierBuilder { return new JdbcIdentifierBuilder(Identifier.empty()); } - public static JdbcIdentifierBuilder from(Map additionalParameters) { - - Identifier[] identifier = new Identifier[] { Identifier.empty() }; - - additionalParameters - .forEach((k, v) -> identifier[0] = identifier[0].add(k, v, v == null ? Object.class : v.getClass())); - - return new JdbcIdentifierBuilder(identifier[0]); - } - /** * Creates ParentKeys with backreference for the given path and value of the parents id. */ public static JdbcIdentifierBuilder forBackReferences(PersistentPropertyPath path, @Nullable Object value) { - Identifier identifier = Identifier.simple( // + Identifier identifier = Identifier.of( // path.getRequiredLeafProperty().getReverseColumnName(), // value, // getLastIdProperty(path).getColumnType() // @@ -66,7 +56,7 @@ public class JdbcIdentifierBuilder { public JdbcIdentifierBuilder withQualifier(PersistentPropertyPath path, Object value) { RelationalPersistentProperty leafProperty = path.getRequiredLeafProperty(); - identifier = identifier.add(leafProperty.getKeyColumn(), value, leafProperty.getQualifierColumnType()); + identifier = identifier.withPart(leafProperty.getKeyColumn(), value, leafProperty.getQualifierColumnType()); return this; } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java index 27c04a40..0677a15e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/mybatis/MyBatisDataAccessStrategy.java @@ -22,17 +22,18 @@ import java.util.Map; import org.apache.ibatis.session.SqlSession; import org.mybatis.spring.SqlSessionTemplate; + import org.springframework.data.jdbc.core.CascadingDataAccessStrategy; import org.springframework.data.jdbc.core.DataAccessStrategy; import org.springframework.data.jdbc.core.DefaultDataAccessStrategy; import org.springframework.data.jdbc.core.DelegatingDataAccessStrategy; -import org.springframework.data.relational.domain.Identifier; import org.springframework.data.jdbc.core.SqlGeneratorSource; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentProperty; +import org.springframework.data.relational.domain.Identifier; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.util.Assert; @@ -124,7 +125,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { this.namespaceStrategy = namespaceStrategy; } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#insert(java.lang.Object, java.lang.Class, java.util.Map) */ @@ -144,13 +145,13 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { @Override public Object insert(T instance, Class domainType, Identifier identifier) { - MyBatisContext myBatisContext = new MyBatisContext(null, instance, domainType, identifier.getParametersByName()); + MyBatisContext myBatisContext = new MyBatisContext(null, instance, domainType, identifier.toMap()); sqlSession().insert(namespace(domainType) + ".insert", myBatisContext); return myBatisContext.getId(); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#update(java.lang.Object, java.lang.Class) */ @@ -161,7 +162,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { new MyBatisContext(null, instance, domainType, Collections.emptyMap())) != 0; } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, java.lang.Class) */ @@ -172,7 +173,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { new MyBatisContext(id, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#delete(java.lang.Object, org.springframework.data.mapping.PersistentPropertyPath) */ @@ -185,7 +186,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteAll(java.lang.Class) */ @@ -198,7 +199,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { ); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#deleteAll(org.springframework.data.mapping.PersistentPropertyPath) */ @@ -214,7 +215,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { ); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findById(java.lang.Object, java.lang.Class) */ @@ -224,7 +225,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { new MyBatisContext(id, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAll(java.lang.Class) */ @@ -234,7 +235,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { new MyBatisContext(null, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllById(java.lang.Iterable, java.lang.Class) */ @@ -244,7 +245,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { new MyBatisContext(ids, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#findAllByProperty(java.lang.Object, org.springframework.data.relational.core.mapping.RelationalPersistentProperty) */ @@ -255,7 +256,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { new MyBatisContext(rootId, null, property.getType(), Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#existsById(java.lang.Object, java.lang.Class) */ @@ -265,7 +266,7 @@ public class MyBatisDataAccessStrategy implements DataAccessStrategy { new MyBatisContext(id, null, domainType, Collections.emptyMap())); } - /* + /* * (non-Javadoc) * @see org.springframework.data.jdbc.core.DataAccessStrategy#count(java.lang.Class) */ diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java index be9a14ae..164a8b33 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/DefaultJdbcInterpreterUnitTests.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.*; import org.junit.Test; import org.mockito.ArgumentCaptor; + import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.DbAction.Insert; @@ -68,7 +69,7 @@ public class DefaultJdbcInterpreterUnitTests { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().getParameters()) // + assertThat(argumentCaptor.getValue().getParts()) // .extracting("name", "value", "targetType") // .containsExactly(tuple(BACK_REFERENCE, CONTAINER_ID, Long.class)); } @@ -83,7 +84,7 @@ public class DefaultJdbcInterpreterUnitTests { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().getParameters()) // + assertThat(argumentCaptor.getValue().getParts()) // .extracting("name", "value", "targetType") // .containsExactly(tuple(BACK_REFERENCE, CONTAINER_ID, Long.class)); } @@ -98,7 +99,7 @@ public class DefaultJdbcInterpreterUnitTests { ArgumentCaptor argumentCaptor = ArgumentCaptor.forClass(Identifier.class); verify(dataAccessStrategy).insert(eq(element), eq(Element.class), argumentCaptor.capture()); - assertThat(argumentCaptor.getValue().getParameters()) // + assertThat(argumentCaptor.getValue().getParts()) // .extracting("name", "value", "targetType") // .containsExactly(tuple(BACK_REFERENCE, CONTAINER_ID, Long.class)); } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java index 20c3454f..d7213940 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/JdbcIdentifierBuilderUnitTests.java @@ -18,16 +18,13 @@ package org.springframework.data.jdbc.core; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.jdbc.core.PropertyPathUtils.*; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; -import org.assertj.core.groups.Tuple; import org.jetbrains.annotations.NotNull; import org.junit.Test; + import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.JdbcIdentifierBuilder; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; @@ -44,44 +41,12 @@ public class JdbcIdentifierBuilderUnitTests { JdbcMappingContext context = new JdbcMappingContext(); - @Test // DATAJDBC-326 - public void parametersWithStringKeysUseTheValuesType() { - - HashMap parameters = new HashMap<>(); - parameters.put("one", "eins"); - parameters.put("two", 2L); - - Identifier identifier = JdbcIdentifierBuilder.from(parameters).build(); - - assertThat(identifier.getParameters()) // - .extracting("name", "value", "targetType") // - .containsExactlyInAnyOrder( // - tuple("one", "eins", String.class), // - tuple("two", 2L, Long.class) // - ); - } - - @Test // DATAJDBC-326 - public void parametersWithStringKeysUseObjectAsTypeForNull() { - - HashMap parameters = new HashMap<>(); - parameters.put("one", null); - - Identifier identifier = JdbcIdentifierBuilder.from(parameters).build(); - - assertThat(identifier.getParameters()) // - .extracting("name", "value", "targetType") // - .containsExactly( // - tuple("one", null, Object.class) // - ); - } - @Test // DATAJDBC-326 public void parametersWithPropertyKeysUseTheParentPropertyJdbcType() { Identifier identifier = JdbcIdentifierBuilder.forBackReferences(getPath("child"), "eins").build(); - assertThat(identifier.getParameters()) // + assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // .containsExactly( // tuple("dummy_entity", "eins", UUID.class) // @@ -98,7 +63,7 @@ public class JdbcIdentifierBuilderUnitTests { .withQualifier(path, "map-key-eins") // .build(); - assertThat(identifier.getParameters()) // + assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // .containsExactlyInAnyOrder( // tuple("dummy_entity", "parent-eins", UUID.class), // @@ -116,7 +81,7 @@ public class JdbcIdentifierBuilderUnitTests { .withQualifier(path, "list-index-eins") // .build(); - assertThat(identifier.getParameters()) // + assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // .containsExactlyInAnyOrder( // tuple("dummy_entity", "parent-eins", UUID.class), // @@ -131,7 +96,7 @@ public class JdbcIdentifierBuilderUnitTests { .forBackReferences(getPath("embeddable.child"), "parent-eins") // .build(); - assertThat(identifier.getParameters()) // + assertThat(identifier.getParts()) // .extracting("name", "value", "targetType") // .containsExactly( // tuple("embeddable", "parent-eins", UUID.class) // diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/Identifier.java b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/Identifier.java index 3afbeadc..18dc6c6b 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/domain/Identifier.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/domain/Identifier.java @@ -17,60 +17,165 @@ package org.springframework.data.relational.domain; import lombok.AccessLevel; import lombok.AllArgsConstructor; +import lombok.EqualsAndHashCode; +import lombok.ToString; import lombok.Value; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.function.Consumer; + +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; /** - * {@literal Identifier} represents a multi part id of an entity. Parts or all of the entity might not have a - * representation as a property in the entity but might only be derived from other entities referencing it. - * + * {@literal Identifier} represents a composite id of an entity that may be composed of one or many parts. Parts or all + * of the entity might not have a representation as a property in the entity but might only be derived from other + * entities referencing it. + * * @author Jens Schauder + * @author Mark Paluch * @since 1.1 */ +@EqualsAndHashCode +@ToString public final class Identifier { - private final List keys; + private static final Identifier EMPTY = new Identifier(Collections.emptyList()); - private Identifier(List keys) { - this.keys = keys; + private final List parts; + + private Identifier(List parts) { + this.parts = parts; } - static public Identifier empty() { - return new Identifier(Collections.emptyList()); + /** + * Returns an empty {@link Identifier}. + * + * @return an empty {@link Identifier}. + */ + public static Identifier empty() { + return EMPTY; } - static public Identifier simple(String name, Object value, Class targetType) { + /** + * Creates an {@link Identifier} from {@code name}, {@code value}, and a {@link Class target type}. + * + * @param name must not be {@literal null} or empty. + * @param value + * @param targetType must not be {@literal null}. + * @return the {@link Identifier} for {@code name}, {@code value}, and a {@link Class target type}. + */ + public static Identifier of(String name, Object value, Class targetType) { + + Assert.hasText(name, "Name must not be empty!"); + Assert.notNull(targetType, "Target type must not be null!"); + return new Identifier(Collections.singletonList(new SingleIdentifierValue(name, value, targetType))); } - public Identifier add(String name, Object value, Class targetType) { + /** + * Creates an {@link Identifier} from a {@link Map} of name to value tuples. + * + * @param map must not be {@literal null}. + * @return the {@link Identifier} from a {@link Map} of name to value tuples. + */ + public static Identifier from(Map map) { - List keys = new ArrayList<>(this.keys); - keys.add(new SingleIdentifierValue(name, value, targetType)); - return new Identifier(keys); + Assert.notNull(map, "Map must not be null!"); + + if (map.isEmpty()) { + return empty(); + } + + List values = new ArrayList<>(); + + map.forEach((k, v) -> { + + values.add(new SingleIdentifierValue(k, v, v != null ? ClassUtils.getUserClass(v) : Object.class)); + }); + + return new Identifier(Collections.unmodifiableList(values)); } - @Deprecated - public Map getParametersByName() { + /** + * Creates a new {@link Identifier} from the current instance and sets the value for {@code key}. Existing key + * definitions for {@code name} are overwritten if they already exist. + * + * @param name must not be {@literal null} or empty. + * @param value + * @param targetType must not be {@literal null}. + * @return the {@link Identifier} containing all existing keys and the key part for {@code name}, {@code value}, and a + * {@link Class target type}. + */ + public Identifier withPart(String name, Object value, Class targetType) { - HashMap result = new HashMap<>(); - forEach(v -> result.put(v.name, v.value)); + Assert.hasText(name, "Name must not be empty!"); + Assert.notNull(targetType, "Target type must not be null!"); + + boolean overwritten = false; + List keys = new ArrayList<>(this.parts.size() + 1); + + for (SingleIdentifierValue singleValue : this.parts) { + + if (singleValue.getName().equals(name)) { + overwritten = true; + keys.add(new SingleIdentifierValue(singleValue.getName(), value, targetType)); + } else { + keys.add(singleValue); + } + } + + if (!overwritten) { + keys.add(new SingleIdentifierValue(name, value, targetType)); + } + + return new Identifier(Collections.unmodifiableList(keys)); + } + + /** + * Returns a {@link Map} containing the identifier name to value tuples. + * + * @return a {@link Map} containing the identifier name to value tuples. + */ + public Map toMap() { + + Map result = new LinkedHashMap<>(); + forEach((name, value, type) -> result.put(name, value)); return result; } - public Collection getParameters() { - return keys; + /** + * @return the {@link SingleIdentifierValue key parts}. + */ + public Collection getParts() { + return this.parts; } - public void forEach(Consumer consumer) { - getParameters().forEach(consumer); + /** + * Performs the given action for each element of the {@link Identifier} until all elements have been processed or the + * action throws an exception. Unless otherwise specified by the implementing class, actions are performed in the + * order of iteration (if an iteration order is specified). Exceptions thrown by the action are relayed to the caller. + * + * @param consumer the action, must not be {@literal null}. + */ + public void forEach(IdentifierConsumer consumer) { + + Assert.notNull(consumer, "IdentifierConsumer must not be null"); + + getParts().forEach(it -> consumer.accept(it.name, it.value, it.targetType)); + } + + /** + * Returns the number of key parts in this collection. + * + * @return the number of key parts in this collection. + */ + public int size() { + return this.parts.size(); } /** @@ -78,14 +183,32 @@ public final class Identifier { * store the element in the database. * * @author Jens Schauder - * @since 1.1 */ @Value @AllArgsConstructor(access = AccessLevel.PRIVATE) - public static class SingleIdentifierValue { + static class SingleIdentifierValue { String name; Object value; Class targetType; } + + /** + * Represents an operation that accepts identifier key parts (name, value and {@link Class target type}) defining a + * contract to consume {@link Identifier} values. + * + * @author Mark Paluch + */ + @FunctionalInterface + public interface IdentifierConsumer { + + /** + * Performs this operation on the given arguments. + * + * @param name + * @param value + * @param targetType + */ + void accept(String name, Object value, Class targetType); + } } diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/domain/IdentifierUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/domain/IdentifierUnitTests.java index 488b839a..57f58f6a 100644 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/domain/IdentifierUnitTests.java +++ b/spring-data-relational/src/test/java/org/springframework/data/relational/domain/IdentifierUnitTests.java @@ -15,23 +15,90 @@ */ package org.springframework.data.relational.domain; +import static org.assertj.core.api.Assertions.*; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + import org.junit.Test; -import java.util.AbstractMap; - -import static org.assertj.core.api.Assertions.assertThat; - /** + * Unit tests for {@link Identifier}. + * * @author Jens Schauder + * @author Mark Paluch */ public class IdentifierUnitTests { @Test // DATAJDBC-326 public void getParametersByName() { - Identifier identifier = Identifier.simple("aName", "aValue", String.class);; + Identifier identifier = Identifier.of("aName", "aValue", String.class); - assertThat(identifier.getParametersByName()) - .containsExactly(new AbstractMap.SimpleEntry<>("aName", "aValue")); + assertThat(identifier.toMap()).hasSize(1).containsEntry("aName", "aValue"); } -} \ No newline at end of file + + @Test // DATAJDBC-326 + public void parametersWithStringKeysUseObjectAsTypeForNull() { + + HashMap parameters = new HashMap<>(); + parameters.put("one", null); + + Identifier identifier = Identifier.from(parameters); + + assertThat(identifier.getParts()) // + .extracting("name", "value", "targetType") // + .containsExactly( // + tuple("one", null, Object.class) // + ); + } + + @Test // DATAJDBC-326 + public void createsIdentifierFromMap() { + + Identifier identifier = Identifier.from(Collections.singletonMap("aName", "aValue")); + + assertThat(identifier.toMap()).hasSize(1).containsEntry("aName", "aValue"); + } + + @Test // DATAJDBC-326 + public void withAddsNewEntries() { + + Identifier identifier = Identifier.from(Collections.singletonMap("aName", "aValue")).withPart("foo", "bar", + String.class); + + assertThat(identifier.toMap()).hasSize(2).containsEntry("aName", "aValue").containsEntry("foo", "bar"); + } + + @Test // DATAJDBC-326 + public void withOverridesExistingEntries() { + + Identifier identifier = Identifier.from(Collections.singletonMap("aName", "aValue")).withPart("aName", "bar", + String.class); + + assertThat(identifier.toMap()).hasSize(1).containsEntry("aName", "bar"); + } + + @Test // DATAJDBC-326 + public void forEachIteratesOverKeys() { + + List keys = new ArrayList<>(); + + Identifier.from(Collections.singletonMap("aName", "aValue")).forEach((name, value, targetType) -> keys.add(name)); + + assertThat(keys).containsOnly("aName"); + } + + @Test // DATAJDBC-326 + public void equalsConsidersEquality() { + + Identifier one = Identifier.from(Collections.singletonMap("aName", "aValue")); + Identifier two = Identifier.from(Collections.singletonMap("aName", "aValue")); + Identifier three = Identifier.from(Collections.singletonMap("aName", "different")); + + assertThat(one).isEqualTo(two); + assertThat(one).isNotEqualTo(three); + } +}