diff --git a/CHANGELOG.md b/CHANGELOG.md index 628bf2e8..d5283ecd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. an Optional, and additionally delegates further conversion back to the BeanMapper. - DynamicClassGeneratorHelper was removed, due to returning null, and replaced with passing the baseclass for a generated class immediately to the constructor of GeneratedClass. +- Issue [#46](https://github.com/42BV/beanmapper/issues/46) **Map to Collection of Targets**; Added the possibility to map a Collection of Class A, immediately + to a Collection of Class B, if both Class A and B implement Equalizer with the same type of ID. Implemented the same for Maps. ## [4.0.0] - 2022-09-15 diff --git a/src/main/java/io/beanmapper/BeanMapper.java b/src/main/java/io/beanmapper/BeanMapper.java index e208f62c..faf7d467 100644 --- a/src/main/java/io/beanmapper/BeanMapper.java +++ b/src/main/java/io/beanmapper/BeanMapper.java @@ -1,5 +1,8 @@ package io.beanmapper; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; @@ -7,6 +10,7 @@ import io.beanmapper.config.BeanMapperBuilder; import io.beanmapper.config.Configuration; +import io.beanmapper.core.collections.Equalizer; import io.beanmapper.strategy.MapStrategyType; /** @@ -90,6 +94,61 @@ public Set map(Set set, Class elementInSetClass) { return (Set) mapCollection(set, elementInSetClass); } + /** + * Maps a Collection of elements into another, existing Collection of elements, updating the corresponding entities, and inserting elements without + * counterpart. + * @param source the source collection + * @param target the target collection + * @param targetClass the class-object representing the type of the elements in the target collection. + * @param the class type of the elements in the source collection, which must extend from Equalizer + * @param the class type of the elements in the target collection, which must extend from Equalizer + * @return the combination of the source and target collections, where the elements of the source collection have been mapped to the target class. + */ + public Collection map(Collection source, Collection target, Class targetClass) { + Set set = new HashSet<>(); + for (S entity : source) { + T t = null; + for (T otherEntity : target) { + if (entity.isEqual(otherEntity)) { + t = map(entity, otherEntity); + set.add(t); + break; + } + } + if (t == null && !this.configuration.getOnlyPatchExistingDuringCollectionToCollection()) + set.add(map(entity, targetClass)); + } + return set; + } + + /** + *Maps a Map of elements into another, existing Map of elements, updating the corresponding entities, and inserting elements without counterpart. + * @param source the source map + * @param target the target map + * @param targetClass the class-object representing the type of the elements in the target map. + * @param the class of the key/ID of each element. + * @param the class type of the elements in the source map, which must extend from Equalizer + * @param the class type of the elements in the target map, which must extend from Equalizer + * @return the combination of the source and target maps, where the elements of the source map have been mapped to the target class. + */ + public Map map(Map source, Map target, Class targetClass) { + Map result = new HashMap<>(); + for (var entry : source.entrySet()) { + boolean isMapped = false; + for (var targetEntry : target.entrySet()) { + if (entry.getValue().isEqual(targetEntry.getValue())) { + result.put(targetEntry.getKey(), this.map(entry.getValue(), targetClass)); + isMapped = true; + break; + } + } + if (!(isMapped || this.configuration.getOnlyPatchExistingDuringCollectionToCollection())) { + result.put(entry.getKey(), this.map(entry.getValue(), targetClass)); + } + } + return result; + } + /** * Maps the source map of elements to a new target map. Convenience operator * @param map the source map diff --git a/src/main/java/io/beanmapper/config/BeanMapperBuilder.java b/src/main/java/io/beanmapper/config/BeanMapperBuilder.java index fd0959c5..d68bd77d 100644 --- a/src/main/java/io/beanmapper/config/BeanMapperBuilder.java +++ b/src/main/java/io/beanmapper/config/BeanMapperBuilder.java @@ -195,6 +195,11 @@ public BeanMapperBuilder setUseNullValue() { return this; } + public BeanMapperBuilder setOnlyPatchExistingDuringCollectionToCollection(boolean onlyPatchExistingDuringCollectionToCollection) { + this.configuration.setOnlyPatchExistingDuringCollectionToCollection(onlyPatchExistingDuringCollectionToCollection); + return this; + } + public BeanMapper build() { BeanMapper beanMapper = new BeanMapper(configuration); // Custom collection handlers must be registered before default ones diff --git a/src/main/java/io/beanmapper/config/Configuration.java b/src/main/java/io/beanmapper/config/Configuration.java index 7d3b7e3f..d05a7089 100644 --- a/src/main/java/io/beanmapper/config/Configuration.java +++ b/src/main/java/io/beanmapper/config/Configuration.java @@ -212,6 +212,13 @@ public interface Configuration { */ Boolean getEnforceSecuredProperties(); + /** + * Property that determines whether the BeanMapper#map(Map, Map) and BeanMapper#map(Collection, Collection) should + * update existing objects and insert new objects without equal, or only update existing objects. + * @return determines if objects without equal should be mapped and inserted into collection or map. + */ + Boolean getOnlyPatchExistingDuringCollectionToCollection(); + /** * Add a converter class (must inherit from abstract BeanConverter class) to the beanMapper. * On mapping, the beanMapper will check for a suitable converter and use its from and @@ -419,4 +426,9 @@ public interface Configuration { */ void setUseNullValue(Boolean useNullValue); + /** + * Property that determines whether the BeanMapper#map(Map, Map) and BeanMapper#map(Collection, Collection) should + * update existing objects and insert new objects without equal, or only update existing objects. + */ + void setOnlyPatchExistingDuringCollectionToCollection(Boolean onlyPatchExistingDuringCollectionToCollection); } diff --git a/src/main/java/io/beanmapper/config/CoreConfiguration.java b/src/main/java/io/beanmapper/config/CoreConfiguration.java index 3e6ee7b9..c743cd4a 100644 --- a/src/main/java/io/beanmapper/config/CoreConfiguration.java +++ b/src/main/java/io/beanmapper/config/CoreConfiguration.java @@ -99,6 +99,12 @@ public class CoreConfiguration implements Configuration { */ private Boolean useNullValue = false; + /** + * Property that determines whether the BeanMapper#map(Map, Map) and BeanMapper#map(Collection, Collection) should + * update existing objects and insert new objects without equal, or only update existing objects. + */ + private Boolean onlyPatchExistingDuringCollectionToCollection = false; + @Override public List getDownsizeTarget() { return null; } @@ -254,6 +260,11 @@ public Boolean getEnforceSecuredProperties() { return enforceSecuredProperties; } + @Override + public Boolean getOnlyPatchExistingDuringCollectionToCollection() { + return this.onlyPatchExistingDuringCollectionToCollection; + } + @Override public void addConverter(BeanConverter converter) { this.beanConverters.add(converter); @@ -414,4 +425,9 @@ public void setUseNullValue(Boolean useNullValue) { this.useNullValue = useNullValue; } + @Override + public void setOnlyPatchExistingDuringCollectionToCollection(Boolean onlyPatchExistingDuringCollectionToCollection) { + this.onlyPatchExistingDuringCollectionToCollection = onlyPatchExistingDuringCollectionToCollection; + } + } diff --git a/src/main/java/io/beanmapper/config/OverrideConfiguration.java b/src/main/java/io/beanmapper/config/OverrideConfiguration.java index 64b4dc43..ea7af8c6 100644 --- a/src/main/java/io/beanmapper/config/OverrideConfiguration.java +++ b/src/main/java/io/beanmapper/config/OverrideConfiguration.java @@ -51,6 +51,8 @@ public class OverrideConfiguration implements Configuration { private OverrideField flushEnabled; + private OverrideField onlyPatchExistingDuringCollectionToCollection; + public OverrideConfiguration(Configuration configuration) { if (configuration == null) { throw new ParentConfigurationPossiblyNullException("Developer error: the parent configuration may not be null"); @@ -64,6 +66,7 @@ public OverrideConfiguration(Configuration configuration) { this.flushAfterClear = new OverrideField<>(configuration::isFlushAfterClear); this.flushEnabled = new OverrideField<>(configuration::isFlushEnabled); this.useNullValue = new OverrideField<>(configuration::getUseNullValue); + this.onlyPatchExistingDuringCollectionToCollection = new OverrideField<>(configuration::getOnlyPatchExistingDuringCollectionToCollection); } @Override @@ -247,6 +250,11 @@ public Boolean getEnforceSecuredProperties() { this.enforcedSecuredProperties; } + @Override + public Boolean getOnlyPatchExistingDuringCollectionToCollection() { + return onlyPatchExistingDuringCollectionToCollection.get(); + } + @Override public void addConverter(BeanConverter converter) { beanConverters.add(converter); @@ -397,4 +405,9 @@ public void setUseNullValue(Boolean useNullValue) { this.useNullValue.set(useNullValue); } + @Override + public void setOnlyPatchExistingDuringCollectionToCollection(Boolean onlyPatchExistingDuringCollectionToCollection) { + this.onlyPatchExistingDuringCollectionToCollection.set(onlyPatchExistingDuringCollectionToCollection); + } + } diff --git a/src/main/java/io/beanmapper/core/collections/Equalizer.java b/src/main/java/io/beanmapper/core/collections/Equalizer.java new file mode 100644 index 00000000..3245ba6f --- /dev/null +++ b/src/main/java/io/beanmapper/core/collections/Equalizer.java @@ -0,0 +1,20 @@ +package io.beanmapper.core.collections; + +import java.util.Objects; + +/** + * An interface that may be implemented by a class, to make the class compatible with BeanMapper#map(Collection, Collection) and BeanMapper#map(Map, Map). + */ +public interface Equalizer { + + /** + * Tests whether the two classes are equal, for the purposes of mapping the calling object, to the target object. Default implementation should be + * overridden for every implementing class. + * @param target the target instance, to which the calling instance is looking to be mapped. + * @return whether the caller and target are equal. + * @param the class of the target + */ + default boolean isEqual(T target) { + return Objects.equals(this, target); + } +} diff --git a/src/test/java/io/beanmapper/BeanMapperTest.java b/src/test/java/io/beanmapper/BeanMapperTest.java index f59c5237..9994b665 100644 --- a/src/test/java/io/beanmapper/BeanMapperTest.java +++ b/src/test/java/io/beanmapper/BeanMapperTest.java @@ -14,6 +14,7 @@ import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; @@ -36,6 +37,9 @@ import io.beanmapper.exceptions.BeanNoLogicSecuredCheckSetException; import io.beanmapper.exceptions.BeanNoRoleSecuredCheckSetException; import io.beanmapper.exceptions.BeanNoSuchPropertyException; +import io.beanmapper.map_to_target_collection.DissimilarEqualizeableEntity; +import io.beanmapper.map_to_target_collection.EqualizeableEntity; +import io.beanmapper.map_to_target_collection.OtherEqualizeableEntity; import io.beanmapper.testmodel.anonymous.Book; import io.beanmapper.testmodel.anonymous.BookForm; import io.beanmapper.testmodel.beanalias.NestedSourceWithAlias; @@ -1735,6 +1739,143 @@ void mapToOptionalEmpty() { assertFalse(personView.isPresent()); } + @Test + void testMapEqualizableEntities_Patch() { + Collection entityCollection = List.of( + new EqualizeableEntity(1L, "Henk", 25L), + new EqualizeableEntity(2L, "Piet", 27L), + new EqualizeableEntity(3L, "Klaas", 29L) + ); + Collection otherEqualizeableEntities = List.of( + new OtherEqualizeableEntity(1L), + new OtherEqualizeableEntity(2L) + ); + + this.beanMapper = this.beanMapper.wrap().setOnlyPatchExistingDuringCollectionToCollection(true).build(); + + Collection combined = this.beanMapper.map(entityCollection, otherEqualizeableEntities, OtherEqualizeableEntity.class); + + assertEquals(2, combined.size()); + } + + @Test + void testMapEqualizableEntities_Insert() { + Collection entityCollection = List.of( + new EqualizeableEntity(1L, "Henk", 25L), + new EqualizeableEntity(2L, "Piet", 27L), + new EqualizeableEntity(3L, "Klaas", 29L) + ); + Collection otherEqualizeableEntities = List.of( + new OtherEqualizeableEntity(1L), + new OtherEqualizeableEntity(2L) + ); + + Collection combined = this.beanMapper.map(entityCollection, otherEqualizeableEntities, OtherEqualizeableEntity.class); + + assertEquals(3, combined.size()); + } + + @Test + void testMapEqualizeableEntityMapToDifferentMapOfDissimilarButEqualizeableEntities_Patch() { + Map equalizeableEntityMap = Map.of( + 1L, new EqualizeableEntity(1L, "Henk", 25L), + 2L, new EqualizeableEntity(2L, "Piet", 27L), + 3L, new EqualizeableEntity(3L, "Klaas", 29L) + ); + Map otherEqualizeableEntityMap = Map.of( + 1L, new OtherEqualizeableEntity(1L), + 2L, new OtherEqualizeableEntity(2L) + ); + + this.beanMapper = this.beanMapper.wrap().setOnlyPatchExistingDuringCollectionToCollection(true).build(); + + Map combined = this.beanMapper.map(equalizeableEntityMap, otherEqualizeableEntityMap, OtherEqualizeableEntity.class); + + assertEquals(2, combined.size()); + + assertTrue(combined.containsKey(1L)); + assertTrue(combined.containsKey(2L)); + + assertEquals(2, combined.values().stream() + .filter(entity -> entity.getName().equals("Henk") || entity.getName().equals("Piet") || entity.getName().equals("Klaas")) + .count() + ); + } + + @Test + void testMapEqualizeableEntityMapToDifferentMapOfDissimilarButEqualizeableEntities_Insert() { + this.beanMapper = this.beanMapper.wrap().setOnlyPatchExistingDuringCollectionToCollection(false).build(); + Map equalizeableEntityMap = Map.of( + 1L, new EqualizeableEntity(1L, "Henk", 25L), + 2L, new EqualizeableEntity(2L, "Piet", 27L), + 3L, new EqualizeableEntity(3L, "Klaas", 29L) + ); + Map otherEqualizeableEntityMap = Map.of( + 1L, new OtherEqualizeableEntity(1L), + 2L, new OtherEqualizeableEntity(2L) + ); + + Map combined = this.beanMapper.map(equalizeableEntityMap, otherEqualizeableEntityMap, OtherEqualizeableEntity.class); + + assertEquals(3, combined.size()); + + assertTrue(combined.containsKey(1L)); + assertTrue(combined.containsKey(2L)); + assertTrue(combined.containsKey(3L)); + + assertEquals(3, combined.values().stream() + .filter(entity -> entity.getName().equals("Henk") || entity.getName().equals("Piet") || entity.getName().equals("Klaas")) + .count() + ); + } + + @Test + void testMapToMapShouldFailIfTargetMapIsNull() { + Map equalizeableEntityMap = Map.of( + 1L, new EqualizeableEntity(1L, "Henk", 25L), + 2L, new EqualizeableEntity(2L, "Piet", 27L), + 3L, new EqualizeableEntity(3L, "Klaas", 29L) + ); + Map nullMap = null; + assertThrows(NullPointerException.class, () -> beanMapper.map(equalizeableEntityMap, nullMap, OtherEqualizeableEntity.class)); + } + + @Test + void mapToDissimilarClass_Patch() { + Map equalizeableEntityMap = Map.of( + 1L, new EqualizeableEntity(1L, "Henk", 25L), + 2L, new EqualizeableEntity(2L, "Piet", 27L), + 3L, new EqualizeableEntity(3L, "Klaas", 29L) + ); + Map dissimilarEqualizeableEntityMap = Map.of(1L, new DissimilarEqualizeableEntity(1L)); + + Map combined = this.beanMapper.map(equalizeableEntityMap, dissimilarEqualizeableEntityMap, DissimilarEqualizeableEntity.class); + assertEquals(3, combined.size()); + } + + @Test + void mapCollectionToCollectionOfDissimilarClass_Patch() { + Collection entityCollection = List.of( + new EqualizeableEntity(1L, "Henk", 25L), + new EqualizeableEntity(2L, "Piet", 27L), + new EqualizeableEntity(3L, "Klaas", 29L) + ); + Collection dissimilarEqualizeableEntities = List.of(new DissimilarEqualizeableEntity(1L)); + + Collection combined = this.beanMapper.map(entityCollection, dissimilarEqualizeableEntities, DissimilarEqualizeableEntity.class); + assertEquals(3, combined.size()); + } + + @Test + void testMapToMapShouldFailIfSourceMapIsNull() { + Map nullMap = null; + Map otherEqualizeableEntityMap = Map.of( + 1L, new OtherEqualizeableEntity(1L), + 2L, new OtherEqualizeableEntity(2L) + ); + assertThrows(NullPointerException.class, () -> beanMapper.map(nullMap, otherEqualizeableEntityMap, OtherEqualizeableEntity.class)); + } + private MyEntity createMyEntity() { MyEntity child = new MyEntity(); child.value = "Piet"; diff --git a/src/test/java/io/beanmapper/map_to_target_collection/DissimilarEqualizeableEntity.java b/src/test/java/io/beanmapper/map_to_target_collection/DissimilarEqualizeableEntity.java new file mode 100644 index 00000000..e1936410 --- /dev/null +++ b/src/test/java/io/beanmapper/map_to_target_collection/DissimilarEqualizeableEntity.java @@ -0,0 +1,38 @@ +package io.beanmapper.map_to_target_collection; + +import io.beanmapper.core.collections.Equalizer; + +public class DissimilarEqualizeableEntity implements Equalizer { + + private Long id; + + private String name; + + public DissimilarEqualizeableEntity(Long id) { + this.id = id; + } + + public DissimilarEqualizeableEntity() { + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public void setName(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + @Override + public boolean isEqual(T target) { + return Equalizer.super.isEqual(target); + } +} diff --git a/src/test/java/io/beanmapper/map_to_target_collection/EqualizeableEntity.java b/src/test/java/io/beanmapper/map_to_target_collection/EqualizeableEntity.java new file mode 100644 index 00000000..cf1e7211 --- /dev/null +++ b/src/test/java/io/beanmapper/map_to_target_collection/EqualizeableEntity.java @@ -0,0 +1,53 @@ +package io.beanmapper.map_to_target_collection; + +import java.util.Objects; + +import io.beanmapper.core.collections.Equalizer; + +public class EqualizeableEntity implements Equalizer { + private Long id; + private String name; + + private Long age; + + public EqualizeableEntity(Long id, String name, Long age) { + this.id = id; + this.name = name; + } + + public Long getId() { + return this.id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public void setId(Long id) { + this.id = id; + } + + public void setAge(Long age) { + this.age = age; + } + + public Long getAge() { + return age; + } + + @Override + public boolean isEqual(T target) { + if (target instanceof EqualizeableEntity entity) { + return this.id.equals(entity.id); + } else if (target instanceof OtherEqualizeableEntity otherEntity) { + return this.id.equals(otherEntity.getId()); + } else if (target instanceof DissimilarEqualizeableEntity dissimilarEntity) { + return this.id.equals(dissimilarEntity.getId()); + } + return false; + } +} diff --git a/src/test/java/io/beanmapper/map_to_target_collection/OtherEqualizeableEntity.java b/src/test/java/io/beanmapper/map_to_target_collection/OtherEqualizeableEntity.java new file mode 100644 index 00000000..d13352f2 --- /dev/null +++ b/src/test/java/io/beanmapper/map_to_target_collection/OtherEqualizeableEntity.java @@ -0,0 +1,47 @@ +package io.beanmapper.map_to_target_collection; + +import io.beanmapper.core.collections.Equalizer; + +public class OtherEqualizeableEntity implements Equalizer { + + private Long id; + + private String name; + + private Long age; + + public OtherEqualizeableEntity(Long id) { + this.id = id; + } + + public OtherEqualizeableEntity() {} + + @Override + public boolean isEqual(T target) { + return Equalizer.super.isEqual(target); + } + + public Long getId() { + return this.id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Long getAge() { + return age; + } + + public void setAge(Long age) { + this.age = age; + } +}