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
- Made the decision whether to patch or patch&insert a configurable
  option. By default, set to only patch.
  • Loading branch information
marcus-talbot42 committed Sep 23, 2022
1 parent af478af commit 71af903
Show file tree
Hide file tree
Showing 11 changed files with 419 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,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
Expand Down
74 changes: 74 additions & 0 deletions src/main/java/io/beanmapper/BeanMapper.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
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.exceptions.BeanConversionException;
import io.beanmapper.strategy.MapStrategyType;

/**
Expand Down Expand Up @@ -90,6 +95,75 @@ 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 <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 <S extends Equalizer, T extends Equalizer> Collection<T> map(Collection<S> source, Collection<T> target) {
if (target.isEmpty()) {
throw new RuntimeException("");
}


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 && !this.configuration.getOnlyPatchExistingDuringCollectionToCollection()) {
if (target.isEmpty()) {
throw new BeanConversionException(entity.getClass(), target.getClass());
}
Class<T> clazz = (Class<T>) target.stream().findFirst().get().getClass();
set.add(map(entity, clazz));
}
}
return set;
}

public <S extends Equalizer, T extends Equalizer> Collection<T> map(Collection<S> source, Collection<T> target, Class<T> targetClass) {
// It's probably a better idea to pass the targetClass as a parameter. So this method will replace the method above this one.
return null;
}

/**
*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, V2 extends Equalizer> 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()) {
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
Expand Down
5 changes: 5 additions & 0 deletions src/main/java/io/beanmapper/config/BeanMapperBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/io/beanmapper/config/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
}
16 changes: 16 additions & 0 deletions src/main/java/io/beanmapper/config/CoreConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> getDownsizeTarget() { return null; }

Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -414,4 +425,9 @@ public void setUseNullValue(Boolean useNullValue) {
this.useNullValue = useNullValue;
}

@Override
public void setOnlyPatchExistingDuringCollectionToCollection(Boolean onlyPatchExistingDuringCollectionToCollection) {
this.onlyPatchExistingDuringCollectionToCollection = onlyPatchExistingDuringCollectionToCollection;
}

}
13 changes: 13 additions & 0 deletions src/main/java/io/beanmapper/config/OverrideConfiguration.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public class OverrideConfiguration implements Configuration {

private OverrideField<Boolean> flushEnabled;

private OverrideField<Boolean> onlyPatchExistingDuringCollectionToCollection;

public OverrideConfiguration(Configuration configuration) {
if (configuration == null) {
throw new ParentConfigurationPossiblyNullException("Developer error: the parent configuration may not be null");
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -397,4 +405,9 @@ public void setUseNullValue(Boolean useNullValue) {
this.useNullValue.set(useNullValue);
}

@Override
public void setOnlyPatchExistingDuringCollectionToCollection(Boolean onlyPatchExistingDuringCollectionToCollection) {
this.onlyPatchExistingDuringCollectionToCollection.set(onlyPatchExistingDuringCollectionToCollection);
}

}
20 changes: 20 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,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 <T> the class of the target
*/
default <T extends Equalizer> boolean isEqual(T target) {
return Objects.equals(this, target);
}
}
141 changes: 141 additions & 0 deletions src/test/java/io/beanmapper/BeanMapperTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -36,6 +37,9 @@
import io.beanmapper.exceptions.BeanNoLogicSecuredCheckSetException;
import io.beanmapper.exceptions.BeanNoRoleSecuredCheckSetException;
import io.beanmapper.exceptions.BeanNoSuchPropertyException;
import io.beanmapper.testmodel.map_to_target_collection.DissimilarEqualizeableEntity;
import io.beanmapper.testmodel.map_to_target_collection.EqualizeableEntity;
import io.beanmapper.testmodel.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 +1739,143 @@ void mapToOptionalEmpty() {
assertFalse(personView.isPresent());
}

@Test
void testMapEqualizableEntities_Patch() {
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)
);

this.beanMapper = this.beanMapper.wrap().setOnlyPatchExistingDuringCollectionToCollection(true).build();

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

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

@Test
void testMapEqualizableEntities_Insert() {
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);

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

@Test
void testMapEqualizeableEntityMapToDifferentMapOfDissimilarButEqualizeableEntities_Patch() {
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)
);

this.beanMapper = this.beanMapper.wrap().setOnlyPatchExistingDuringCollectionToCollection(true).build();

Map<Long, OtherEqualizeableEntity> 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<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_Patch() {
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_Patch() {
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);
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
Loading

0 comments on commit 71af903

Please sign in to comment.