Skip to content

Commit

Permalink
Implementation of #46
Browse files Browse the repository at this point in the history
- 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
  • Loading branch information
marcus-talbot42 committed Sep 20, 2022
1 parent 74fde61 commit 2311660
Show file tree
Hide file tree
Showing 7 changed files with 276 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions src/main/java/io/beanmapper/BeanMapper.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
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;
import java.util.Set;

import io.beanmapper.config.BeanMapperBuilder;
import io.beanmapper.config.Configuration;
import io.beanmapper.core.collections.Equalizer;
import io.beanmapper.strategy.MapStrategyType;

/**
Expand Down Expand Up @@ -90,6 +94,56 @@ public <S, T> Set<T> map(Set<S> set, Class<T> elementInSetClass) {
return (Set<T>) 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 <ID> the class of the ID of each element
* @param <S> the class type of the elements in the source collection, which must extend from Equalizer<ID>
* @param <T> the class type of the elements in the target collection, which must extend from Equalizer<ID>
* @return the combination of the source and target collections, where the elements of the source collection have been mapped to the target class.
*/
public <ID, S extends Equalizer<ID>, T extends Equalizer<ID>> Collection<T> map(Collection<S> source, Collection<T> target, Class<T> targetClass) {
Set<T> 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 <K> the class of the key/ID of each element.
* @param <V1> the class type of the elements in the source map, which must extend from Equalizer<K>
* @param <V2> the class type of the elements in the target map, which must extend from Equalizer<K>
* @return the combination of the source and target maps, where the elements of the source map have been mapped to the target class.
*/
public <K, V1 extends Equalizer<K>, V2 extends Equalizer<K>> Map<K, V2> map(Map<K, V1> source, Map<K, V2> target, Class<V2> targetClass) {
Map<K, V2> 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
Expand Down
24 changes: 24 additions & 0 deletions src/main/java/io/beanmapper/core/collections/Equalizer.java
Original file line number Diff line number Diff line change
@@ -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 <ID> the type of the ID within the subclass
*/
public interface Equalizer<ID> {

/**
* 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 <T> the class of the target
*/
default <T extends Equalizer<ID>> boolean isEqual(T target) {
return Objects.equals(this, target);
}

ID getId();

}
63 changes: 63 additions & 0 deletions src/test/java/io/beanmapper/BeanMapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -1735,6 +1740,64 @@ void mapToOptionalEmpty() {
assertFalse(personView.isPresent());
}

@Test
void testMapEqualizableEntities() {
Collection<EqualizeableEntity> entityCollection = List.of(new EqualizeableEntity(1L, "Henk", 25L), new EqualizeableEntity(2L, "Piet", 27L), new EqualizeableEntity(3L, "Klaas", 29L));
Collection<OtherEqualizeableEntity> otherEqualizeableEntities = List.of(new OtherEqualizeableEntity(1L), new OtherEqualizeableEntity(2L));

Collection<OtherEqualizeableEntity> combined = this.beanMapper.map(entityCollection, otherEqualizeableEntities, OtherEqualizeableEntity.class);

assertEquals(3, combined.size());
}

@Test
void testMapEqualizeableEntityMapToDifferentMapOfDissimilarButEqualizeableEntities() {
Map<Long, EqualizeableEntity> equalizeableEntityMap = Map.of(1L, new EqualizeableEntity(1L, "Henk", 25L), 2L, new EqualizeableEntity(2L, "Piet", 27L), 3L, new EqualizeableEntity(3L, "Klaas", 29L));
Map<Long, OtherEqualizeableEntity> otherEqualizeableEntityMap = Map.of(1L, new OtherEqualizeableEntity(1L), 2L, new OtherEqualizeableEntity(2L));

Map<Long, OtherEqualizeableEntity> 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<Long, EqualizeableEntity> equalizeableEntityMap = Map.of(1L, new EqualizeableEntity(1L, "Henk", 25L), 2L, new EqualizeableEntity(2L, "Piet", 27L), 3L, new EqualizeableEntity(3L, "Klaas", 29L));
Map<Long, OtherEqualizeableEntity> nullMap = null;
assertThrows(NullPointerException.class, () -> beanMapper.map(equalizeableEntityMap, nullMap, OtherEqualizeableEntity.class));
}

@Test
void mapToDissimilarClass() {
Map<Long, EqualizeableEntity> equalizeableEntityMap = Map.of(1L, new EqualizeableEntity(1L, "Henk", 25L), 2L, new EqualizeableEntity(2L, "Piet", 27L), 3L, new EqualizeableEntity(3L, "Klaas", 29L));
Map<Long, DissimilarEqualizeableEntity> dissimilarEqualizeableEntityMap = Map.of(1L, new DissimilarEqualizeableEntity(1L));

Map<Long, DissimilarEqualizeableEntity> combined = this.beanMapper.map(equalizeableEntityMap, dissimilarEqualizeableEntityMap, DissimilarEqualizeableEntity.class);
assertEquals(3, combined.size());
}

@Test
void mapCollectionToCollectionOfDissimilarClass() {
Collection<EqualizeableEntity> entityCollection = List.of(new EqualizeableEntity(1L, "Henk", 25L), new EqualizeableEntity(2L, "Piet", 27L), new EqualizeableEntity(3L, "Klaas", 29L));
Collection<DissimilarEqualizeableEntity> dissimilarEqualizeableEntities = List.of(new DissimilarEqualizeableEntity(1L));

Collection<DissimilarEqualizeableEntity> combined = this.beanMapper.map(entityCollection, dissimilarEqualizeableEntities, DissimilarEqualizeableEntity.class);
assertEquals(3, combined.size());
}

@Test
void testMapToMapShouldFailIfSourceMapIsNull() {
Map<Long, EqualizeableEntity> nullMap = null;
Map<Long, OtherEqualizeableEntity> 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";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.beanmapper.map_to_target_collection;

import io.beanmapper.core.collections.Equalizer;

public class DissimilarEqualizeableEntity implements Equalizer<Long> {

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 <T extends Equalizer<Long>> boolean isEqual(T target) {
return Equalizer.super.isEqual(target);
}
}
Original file line number Diff line number Diff line change
@@ -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<Long> {
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 <T extends Equalizer<Long>> boolean isEqual(T target) {
return Objects.equals(this.id, target.getId());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package io.beanmapper.map_to_target_collection;

import io.beanmapper.core.collections.Equalizer;

public class OtherEqualizeableEntity implements Equalizer<Long> {

private Long id;

private String name;

private Long age;

public OtherEqualizeableEntity(Long id) {
this.id = id;
}

public OtherEqualizeableEntity() {}

@Override
public <T extends Equalizer<Long>> 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;
}
}

0 comments on commit 2311660

Please sign in to comment.