From 23116606152faa592cf7ad25c0ecbb25902d21b6 Mon Sep 17 00:00:00 2001 From: marcus-talbot42 Date: Tue, 20 Sep 2022 10:51:50 +0200 Subject: [PATCH] Implementation of #46 - Implemented Equalizer, which implements a type check, which allows BeanMapper#map(Collection, Collection) and BeanMapper#map(Map, Map) to only map compatible collections. - Implemented BeanMapper#map(Collection, Collection), which maps a source collection, to a target collection, if both the elements of the source and target implement Equalizer and use the same class for their ID. - Implemented BeanMapper#map(Map, Map), which maps a source map, to a target map, if both the values of the source and target implement Equalizer and use the same class for their key/ID. - Both methods also map elements to the target that do not have a counterpart, simply mapping those elements to the target class. - Updated CHANGELOG.md - Added tests --- CHANGELOG.md | 2 + src/main/java/io/beanmapper/BeanMapper.java | 54 ++++++++++++++++ .../core/collections/Equalizer.java | 24 +++++++ .../java/io/beanmapper/BeanMapperTest.java | 63 +++++++++++++++++++ .../DissimilarEqualizeableEntity.java | 39 ++++++++++++ .../EqualizeableEntity.java | 46 ++++++++++++++ .../OtherEqualizeableEntity.java | 48 ++++++++++++++ 7 files changed, 276 insertions(+) create mode 100644 src/main/java/io/beanmapper/core/collections/Equalizer.java create mode 100644 src/test/java/io/beanmapper/map_to_target_collection/DissimilarEqualizeableEntity.java create mode 100644 src/test/java/io/beanmapper/map_to_target_collection/EqualizeableEntity.java create mode 100644 src/test/java/io/beanmapper/map_to_target_collection/OtherEqualizeableEntity.java diff --git a/CHANGELOG.md b/CHANGELOG.md index f830e22f..e1993ee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Issue [#137](https://github.com/42BV/beanmapper/issues/137) **https://github.com/42BV/beanmapper/issues/137**; Mapping a class with a getter that returns an Optional, would fail, as an Optional can typically not be mapped to the target class. Fixed implementing an OptionalToObjectConverter, which handles unpacking an Optional, and additionally delegates further conversion back to the BeanMapper. +- 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. ## [3.2.0] - 2022-09-15 diff --git a/src/main/java/io/beanmapper/BeanMapper.java b/src/main/java/io/beanmapper/BeanMapper.java index e208f62c..d840ab25 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,56 @@ 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 of the ID of each element + * @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 , T extends Equalizer> 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) + 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 , V2 extends Equalizer> Map map(Map source, Map target, Class targetClass) { + Map result = new HashMap<>(); + for (var entry : source.entrySet()) { + V2 val = target.get(entry.getKey()); + if (val == null) + result.put(entry.getKey(), map(entry.getValue(), targetClass)); + else + result.put(entry.getKey(), map(entry.getValue(), target.get(entry.getKey()))); + } + 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/core/collections/Equalizer.java b/src/main/java/io/beanmapper/core/collections/Equalizer.java new file mode 100644 index 00000000..848b33a6 --- /dev/null +++ b/src/main/java/io/beanmapper/core/collections/Equalizer.java @@ -0,0 +1,24 @@ +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). + * @param the type of the ID within the subclass + */ +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); + } + + ID getId(); + +} diff --git a/src/test/java/io/beanmapper/BeanMapperTest.java b/src/test/java/io/beanmapper/BeanMapperTest.java index f59c5237..fe2f4d0e 100644 --- a/src/test/java/io/beanmapper/BeanMapperTest.java +++ b/src/test/java/io/beanmapper/BeanMapperTest.java @@ -10,10 +10,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; +import java.lang.reflect.InvocationTargetException; import java.time.LocalDate; 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 +38,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 +1740,64 @@ void mapToOptionalEmpty() { assertFalse(personView.isPresent()); } + @Test + void testMapEqualizableEntities() { + 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() { + 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() { + 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() { + 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..066ac173 --- /dev/null +++ b/src/test/java/io/beanmapper/map_to_target_collection/DissimilarEqualizeableEntity.java @@ -0,0 +1,39 @@ +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() { + } + + @Override + 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..2d3d9323 --- /dev/null +++ b/src/test/java/io/beanmapper/map_to_target_collection/EqualizeableEntity.java @@ -0,0 +1,46 @@ +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) { + return Objects.equals(this.id, target.getId()); + } +} 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..a4d1688c --- /dev/null +++ b/src/test/java/io/beanmapper/map_to_target_collection/OtherEqualizeableEntity.java @@ -0,0 +1,48 @@ +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 this.id.equals(target.getId()); + } + + @Override + 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; + } +}