Use ModelMapper to flatten to list of ids - java

I am trying to use ModelMapper to map a collection of sub-objects to a list of id numbers.
public class Child {
private int id;
private String childName;
}
public class Parent {
private Set<Child> children;
private String parentName;
}
public class ParentDto {
private int[] children; // Or use ArrayList<Integer>
private String parentName;
}
How do I tell ModelMapper to flatten the set of Child objects to an array of id numbers?
My first attempt is this but does not seem correct:
modelMapper.addMappings(new PropertyMap<Parent, ParentDto>() {
#Override
protected void configure() {
using(ctx -> ctx.getSource().stream().map(Parent::getId).collect(Collectors.toList())
.map().setChildren(source.getId()));
};
});
Type listType = new TypeToken<ArrayList<ParentDto>>() {}.getType();
ArrayList<ParentDto> parentDtos = new ArrayList<>();
parentDtos = modelMapper.map(parents, listType);
The setChildren call seems like it needs to be map().add() but that does not work either.
Ideas are welcome.

One way of doing this is to create a Set<Child> to int[] converter and use it to map Parent.children to ParentDTO.children:
ModelMapper mapper = new ModelMapper();
Converter<Set<Child>, int[]> childSetToIntArrayConverter =
ctx -> ctx.getSource()
.stream()
.mapToInt(Child::getId)
.toArray();
mapper.createTypeMap(Parent.class, ParentDto.class)
.addMappings(map -> map
.using(childSetToIntArrayConverter)
.map(
Parent::getChildren,
ParentDto::setChildren
)
);
Here is the complete demo (using lombok 1.18.10 and modelmapper 2.3.5):
import lombok.AllArgsConstructor;
import lombok.Data;
import org.modelmapper.Converter;
import org.modelmapper.ModelMapper;
import java.util.Set;
public class Main {
public static void main(String[] args) {
Parent parent = new Parent();
parent.setParentName("Parent");
parent.setChildren(Set.of(
new Child(1, "A"),
new Child(2, "B"),
new Child(3, "C")
));
ModelMapper mapper = new ModelMapper();
Converter<Set<Child>, int[]> childSetToIntArrayConverter =
ctx -> ctx.getSource()
.stream()
.mapToInt(Child::getId)
.toArray();
mapper.createTypeMap(Parent.class, ParentDto.class)
.addMappings(map -> map
.using(childSetToIntArrayConverter)
.map(
Parent::getChildren,
ParentDto::setChildren
)
);
ParentDto dto = mapper.map(parent, ParentDto.class);
System.out.println(parent);
System.out.println(dto);
}
#Data
#AllArgsConstructor
public static class Child {
private int id;
private String childName;
}
#Data
public static class Parent {
private Set<Child> children;
private String parentName;
}
#Data
public static class ParentDto {
private int[] children;
private String parentName;
}
}
Output
Main.Parent(children=[Main.Child(id=3, childName=C), Main.Child(id=2, childName=B), Main.Child(id=1, childName=A)], parentName=Parent)
Main.ParentDto(children=[3, 2, 1], parentName=Parent)

Related

Java & Mapstruct serialization with recursion

For some context, I am developing a Java Web application with Spring & Lombok (Rest API, controllers, etc).
And for JSON API responses I am using Mapstruct to perform the serializations between internal models and the final external ones.
But I cannot figure out how to parse the following example:
Where we can have a Tree.
public class Tree {
List<Leaf> leafs;
}
And inside of it we have Leafs. But each Leaf as inside of it a list of Leafs.
public class Leaf {
String name;
List<Leaf> children;
}
So what I would need Mapstruct to do is, serialize the Tree into a TreeResponse.
public class TreeResponse {
List<LeafResponse> leafs;
}
And all Leafs inside the Tree into LeafResponse, along with the Leaf list inside the "father" Leaf.
public class LeafResponse {
String name;
List<LeafResponse> children;
}
How can I achieve this in Mapstruct? Thanks
Leaving all classes and simple test that finally worked.
Tree.java
#Builder
#Getter
#AllArgsConstructor
#FieldDefaults(level = PUBLIC)
public class Tree {
String name;
List<Leaf> leafs;
}
Leaf.java
#Builder
#Getter
#AllArgsConstructor
#FieldDefaults(level = PUBLIC)
public class Leaf {
String name;
List<Leaf> children;
}
TreeResponse.java
#Getter
#Setter
#FieldDefaults(level = PUBLIC)
public class TreeResponse {
String name;
List<LeafResponse> leafs;
}
LeafResponse.java
#Getter
#Setter
#FieldDefaults(level = PUBLIC)
public class LeafResponse {
String name;
List<LeafResponse> children;
}
Mappers
#Mapper
public interface TreeMapper {
#Mapping(target = "name", source = "entity.name")
TreeResponse map(Tree entity);
}
#Mapper
public interface LeafMapperSecond {
LeafResponse map(Leaf entity);
List<LeafResponse> map(List<Leaf> entity);
}
Test
private TreeMapper treeMapper = Mappers.getMapper(TreeMapper.class);
#Test
public void test() {
List<Leaf> leafs = new ArrayList<>();
leafs.add(Leaf.builder().name("Leaf 1").build());
leafs.add(
Leaf.builder()
.name("Leaf 2")
.children(
Arrays.asList(
Leaf.builder()
.name("Leaf Children 1")
.children(
Arrays.asList(
Leaf.builder()
.name("Leaf Children 1.1")
.build(),
Leaf.builder()
.name("Leaf Children 1.2")
.build()))
.build(),
Leaf.builder().name("Leaf Children 2").build()))
.build());
Tree tree = Tree.builder().name("tree name").leafs(leafs).build();
TreeResponse treeResponse = treeMapper.map(tree);
assertEquals(treeResponse.name, "tree name");
assertEquals(treeResponse.leafs.size(), 2);
LeafResponse leafWithChildren =
treeResponse.leafs.stream()
.filter(l -> l.name.equals("Leaf 2"))
.findFirst()
.orElse(null);
assertNotNull(leafWithChildren);
assertEquals(leafWithChildren.getChildren().size(), 2);
LeafResponse leafWithSubChildren =
leafWithChildren.children.stream()
.filter(l -> l.name.equals("Leaf Children 1"))
.findFirst()
.orElse(null);
assertNotNull(leafWithSubChildren);
assertEquals(leafWithChildren.getChildren().size(), 2);
}

How do I get Jackson library to use my default initialized value when deserializing null value?

I have this JSON input:
{
"teachers": null,
"students": [],
"janitors": ["J1", "J2"]
}
It will be mapped to this School object.
public class School {
// Child JSON arrays reflecting JSON input
private List<String> teachers = new ArrayList<>();
private List<String> students = new ArrayList<>();
private List<String> janitors = new ArrayList<>();
// Getters and Setters
public List<String> getTeachers() {
return teachers;
}
public void setTeachers(List<String> teachers) {
this.teachers = teachers;
}
public List<String> getStudents() {
return students;
}
public void setStudents(List<String> students) {
this.students = students;
}
public List<String> getJanitors() {
return janitors;
}
public void setJanitors(List<String> janitors) {
this.janitors = janitors;
}
}
This is my mapper configuration so far:
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
After the Jackson library deserializes the JSON input, I get that School.teachers = null despite initializing it as an array. My reason for initializing these arrays is to avoid unnecessary null checks.
How can I get the Jackson deserializer to ignore null values or ignore null nodes that it cannot map to?
You can use #JsonInclude(JsonInclude.Include.NON_NULL) at your class like.
import com.fasterxml.jackson.annotation.JsonInclude;
#JsonInclude(JsonInclude.Include.NON_NULL)
public class School {
// Child JSON arrays reflecting JSON input
private List<String> teachers = new ArrayList<>();
private List<String> students = new ArrayList<>();
private List<String> janitors = new ArrayList<>();
}
You can also use it at the field level; in case you want some fields to be nullable.
import com.fasterxml.jackson.annotation.JsonInclude;
public class School {
// Child JSON arrays reflecting JSON input
#JsonInclude(JsonInclude.Include.NON_NULL)
private List<String> teachers = new ArrayList<>();
#JsonInclude(JsonInclude.Include.NON_NULL)
private List<String> students = new ArrayList<>();
private List<String> janitors = new ArrayList<>();
}
So in the above scenario if janitors is null it would be displayed as null.

Orika Mapping for unmodifiable List

I have got two immutable bean classes for which I am using Orika mapping to copy the values from one to other.
However, when I am trying to copy the unmodifiableList through Orika mapping, it fails by throwing below exception:
java.lang.UnsupportedOperationException
at ma.glasnost.orika.ExceptionUtility.newMappingException(ExceptionUtility.java:55)
at ma.glasnost.orika.impl.MapperFacadeImpl.map(MapperFacadeImpl.java:681)
at ma.glasnost.orika.impl.MapperFacadeImpl.map(MapperFacadeImpl.java:650)
at com.myproject.OrikaTest.testEmployeeMapping
I have provided the code below with which you can replicate the same issue:
EmployeeDto class:
public final class EmployeeDto {
private final int id;
private final String name;
private final List<String> previousGrades;
public EmployeeDto(int id, String name, List<String> previousGrades) {
this.id = id;//validations removed
this.name = name;//validations removed
//Commented unmodifiableList as it does not work
//this.previousGrades = Collections.unmodifiableList(previousGrades);
this.previousGrades = previousGrades;
}
public int getId() {
return id;
}
public String getName() {
return name;
}
public List<String> getPreviousGrades() {
//tried like this, even this does not work
return Collections.unmodifiableList(previousGrades);
}
}
Employee class:
public final class Employee {
//ditto AS EmployeeDto
}
OrikaTest class:
public class OrikaTest {
private static final MapperFactory mapperFactory =
new DefaultMapperFactory.Builder().build();
private static final MapperFacade mapperFacade =
mapperFactory.getMapperFacade();
#Test
public void testEmployeeMapping() {
List<String> employeeGrades = Arrays.asList("A", "B");
Employee employee = new Employee(1234, "John", employeeGrades);
EmployeeDto employeeDto = mapperFacade.map(employee, EmployeeDto.class);
//tests using assertEquals
Assert.assertEquals(employeeGrades, employeeDto.getPreviousGrades());
}
}
I could find a link here on this subject, but it was not that clear as alternatives were not explained properly.
So, can you help with an example or any workaround on how to copy the unmodifiableList through Orika mapping?
If the case is that Orika must be able to mutate the list components of the mapped objects then the following could be a work around:
Add a freeze method to your objects. When the objects are created they are in a mutable state. After freeze has been called it is no longer possible to mutate the objects.
It could be implemented like this:
public final class EmployeeDto {
private final int id;
private final String name;
private List<String> previousGrades;
public EmployeeDto(int id, String name, List<String> previousGrades) {
this.id = id;//validations removed
this.name = name;//validations removed
this.previousGrades = previousGrades;
}
public void freeze() {
previousGrades = Collections.unmodifiableList(previousGrades)
}
public List<String> getPreviousGrades() {
return previousGrades;
}
}

JAXB Element With Specific Type But Unknown Names

I have a root Xml document (name = "Entity") that contains one known Xml element (name = "Header") and another Xml element of unknown name but it is known to have an inner XmlElement(name="label")
Here are possible Xmls:
<Entity>
<Header>this is a header</Header>
<a>
<label>this is element A</label>
<otherElements/>
</a>
</Entity>
<Entity>
<Header>this is a different header</Header>
<b>
<label>this is some other element of name b</label>
<others/>
</b>
</Entity>
Here are my JAXB annotated classes:
#XmlRootElement(name = "Entity")
#XmlAccessorType(XmlAccessType.NONE)
public class Entity {
#XmlElement(name = "Header")
private Header header;
#XmlElements( {
#XmlElement(name = "a", type=LabelledElement.A.class),
#XmlElement(name = "b", type=LabelledElement.B.class)
} )
private LabelledElement labelledElement;
// constructors, getters, setters...
}
#XmlAccessorType(XmlAccessType.NONE)
public abstract class LabelledElement {
#XmlElement
private String label;
#XmlAnyElement
private List<Element> otherElements;
public static class A extends LabelledElement {}
public static class B extends LabelledElement {}
}
This was working great! But then I noticed that it isn't only <a> and <b>
It could be <c>, <asd> and even <anything>...
So listing the XmlElement(name = "xyz", type = LabelledElement.xyz.class) is obviously not the right solution.
All I care about is Entity#getLabelledElement()#getLabel() no matter what the LabelledElement name is.
Is this even possible with JAXB?
With EclipseLink JAXB Implementation (MOXy), this should work :
#XmlRootElement(name = "Entity")
#XmlSeeAlso({LabelledElement.class}) //Might not be necessary
#XmlAccessorType(XmlAccessType.NONE)
public class Entity {
#XmlElement(name = "Header")
private Header header;
#XmlPath("child::*[position() = 2]")
#XmlJavaTypeAdapter(MapAdapter.class)
private Map<String,LabelledElement> labelledElementMap;
public LabelledElement getLabelledElement(){
return labelledElementMap.values().get(0);
}
// constructors, getters, setters...
}
The MapAdapter class :
public class MapAdapter extends XmlAdapter<MapAdapter.AdaptedMap, Map<String, LabelledElement>> {
public static class AdaptedMap {
#XmlVariableNode("key")
List<LabbeledElement> entries = new ArrayList<LabbeledElement>();
}
public static class AdaptedEntry {
#XmlTransient
public String key;
#XmlElement
public LabelledElement value;
}
#Override
public AdaptedMap marshal(Map<String, LabelledElement> map) throws Exception {
AdaptedMap adaptedMap = new AdaptedMap();
for(Entry<String, LabelledElement> entry : map.entrySet()) {
AdaptedEntry adaptedEntry = new AdaptedEntry();
adaptedEntry.key = entry.getKey();
adaptedEntry.value = entry.getValue();
adaptedMap.entries.add(adaptedEntry);
}
return adaptedMap;
}
#Override
public Map<String, LabelledElement> unmarshal(AdaptedMap adaptedMap) throws Exception {
List<AdaptedEntry> adaptedEntries = adaptedMap.entries;
Map<String, LabelledElement> map = new HashMap<String, LabelledElement>();
for(AdaptedEntry adaptedEntry : adaptedEntries) {
map.put(adaptedEntry.key, adaptedEntry.value);
}
return map;
}
}
For reference, my solution is inspired by this link.
Apparently it's possible with EclipseLink JAXB (MOXy) implementation, which allows you annotate an interface variable providing a Factory class and method to be used when binding XML to Java, see this answer.
(edited to provide an example of this approach)
For instance, you instead of having an abstract class LabelledElement, have an interface LabelledElement:
public interface LabelledElement {
String getLabel();
}
and then have classes A and B implement it like this:
import javax.xml.bind.annotation.XmlElement;
public class A implements LabelledElement{
private String label;
#Override
#XmlElement(name="label")
public String getLabel() {
return label;
}
}
and Entity class annotated like this:
#XmlRootElement(name = "Entity")
#XmlAccessorType(XmlAccessType.NONE)
public class Entity {
#XmlElement(name = "Header")
private Header header;
#XmlRootElement
#XmlType(
factoryClass=Factory.class,
factoryMethod="createLabelledElement")
private LabelledElement labelledElement;
// constructors, getters, setters...
}
Then, as the answer I linked to suggests, you need a Factory class like:
import java.lang.reflect.*;
import java.util.*;
public class Factory {
public A createA() {
return createInstance(A.class);
}
public B createB() {
return createInstance(B.class);;
}
private <T> T createInstance(Class<T> anInterface) {
return (T) Proxy.newProxyInstance(anInterface.getClassLoader(), new Class[] {anInterface}, new InterfaceInvocationHandler());
}
private static class InterfaceInvocationHandler implements InvocationHandler {
private Map<String, Object> values = new HashMap<String, Object>();
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
String methodName = method.getName();
if(methodName.startsWith("get")) {
return values.get(methodName.substring(3));
} else {
values.put(methodName.substring(3), args[0]);
return null;
}
}
}
}

How to group element in ArrayList and divide into three List

I have an entity class
class Entity {
private String customer;
private String product;
private String productDetail;
}
I have an ArrayList<Entity> including many records, for example record in my list:
customer product productDetail
A A1 A11
A A1 A12
A A2 A21
A A2 A22
B B1 B11
B B2 B21
C C1 C11
C C1 C12
I have 3 entities below
class ProductDetail{
private String details;
}
class Product{
private String product;
private List<ProductDetail> detailList;
}
class Customer{
private String customer;
private List<Product> productList;
}
I want to loop through my ArrayList<Entity> and group records into Customer class, Customer class includes productList, and productList includes detailList.
Please suggest me a solution.
Update my code:
Customer entity:
public class CustomerEntity {
private int customer;
private List<ProductEntity> productList;
public int getCustomer() {
return customer;
}
public void setCustomer(int customer) {
this.customer = customer;
}
public List<ProductEntity> getProductList() {
return productList;
}
public void setProductList(List<ProductEntity> productList) {
this.productList = productList;
}
}
Product Entity:
public class ProductEntity {
private int product;
private List<ProductDetailEntity> detailList;
public int getProduct() {
return product;
}
public void setProduct(int product) {
this.product = product;
}
public List<ProductDetailEntity> getDetailList() {
return detailList;
}
public void setDetailList(List<ProductDetailEntity> detailList) {
this.detailList = detailList;
}
}
Product detail entity:
public class ProductDetailEntity {
private int details;
public int getDetails() {
return details;
}
public void setDetails(int details) {
this.details = details;
}
}
My dummy code test:
public class TestList {
public static void main(String[] args) {
TestEntity testEntity;
List<TestEntity> list = new ArrayList<TestEntity>();
// Create Dummy Data
testEntity = new TestEntity();
testEntity.setCustomer("A");
testEntity.setProduct("A1");
testEntity.setProductDetail("A11");
list.add(testEntity);
testEntity = new TestEntity();
testEntity.setCustomer("A");
testEntity.setProduct("A1");
testEntity.setProductDetail("A12");
list.add(testEntity);
testEntity = new TestEntity();
testEntity.setCustomer("A");
testEntity.setProduct("A2");
testEntity.setProductDetail("A21");
list.add(testEntity);
testEntity = new TestEntity();
testEntity.setCustomer("A");
testEntity.setProduct("A2");
testEntity.setProductDetail("A22");
list.add(testEntity);
testEntity = new TestEntity();
testEntity.setCustomer("B");
testEntity.setProduct("B1");
testEntity.setProductDetail("B11");
list.add(testEntity);
testEntity = new TestEntity();
testEntity.setCustomer("B");
testEntity.setProduct("B2");
testEntity.setProductDetail("B21");
list.add(testEntity);
testEntity = new TestEntity();
testEntity.setCustomer("C");
testEntity.setProduct("C1");
testEntity.setProductDetail("C11");
list.add(testEntity);
testEntity = new TestEntity();
testEntity.setCustomer("C");
testEntity.setProduct("C1");
testEntity.setProductDetail("C12");
list.add(testEntity);
Map<String, List<TestEntity>> groupByCustomerMap = new LinkedHashMap<String, List<TestEntity>>();
List<TestEntity> tempEntityList;
TestEntity tempEntity;
// Group record by customer
for (TestEntity item : list) {
String customer = item.getCustomer();
tempEntityList = new ArrayList<TestEntity>();
tempEntity = new TestEntity();
tempEntity.setCustomer(customer);
tempEntity.setProductDetail(item.getProductDetail());
tempEntity.setProduct(item.getProduct());
tempEntityList.add(tempEntity);
if (!groupByCustomerMap.containsKey(customer)) {
groupByCustomerMap.put(customer, tempEntityList);
} else {
groupByCustomerMap.get(customer).addAll(tempEntityList);
}
}
// I think from groupByCustomerMap, read ProductDetail and group by product
// Pleaes suggest me next solution
}
}
A typical approach that I would try is the following:
Create an empty list of customers
Create an empty list of products
Iterate over input array list consisting of Entity elements.
If the customer contained in the current entity is not already in the list of customers, then create and add the customer. Then check for the product. If the customer was created, then the productList should be empty and the product needs to be added. If the customer was already existing, check for the existence of the product in the list of products....
For filling the list of products you can choose a similar approach. The basic checks are the same.
I could propose next decision:
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
/**
* Created by smv on 19/09/16.
*/
public class MainTest {
#AllArgsConstructor
#ToString
#Data
class Entity {
private String customer;
private String product;
private String productDetail;
}
#AllArgsConstructor
#ToString
#Data
class ProductDetail{
private String details;
}
#AllArgsConstructor
#ToString
#Data
class Product{
private String product;
private List<ProductDetail> detailList;
}
#AllArgsConstructor
#ToString
#Data
class Customer{
private String customer;
private List<Product> productList;
}
#Test
public void run() throws Exception {
ArrayList<Entity> entities = new ArrayList<>();
entities.add(new Entity("A", "A1", "A11"));
entities.add(new Entity("A", "A1", "A12"));
entities.add(new Entity("A", "A2", "A21"));
entities.add(new Entity("A", "A2", "A22"));
entities.add(new Entity("B", "B1", "B11"));
entities.add(new Entity("B", "B2", "B21"));
entities.add(new Entity("C", "C1", "C11"));
entities.add(new Entity("C", "C1", "C12"));
ArrayList<Customer> customers = new ArrayList<>();
entities.forEach(entity -> {
Customer customer = customers.stream().filter(c -> c.getCustomer().equals(entity.getCustomer())).findFirst().orElse(new Customer(entity.getCustomer(), new ArrayList<>()));
if (!customers.contains(customer)) {
customers.add(customer);
}
Product product = customer.getProductList().stream().filter(p -> p.getProduct().equals(entity.getProduct())).findFirst().orElse(new Product(entity.getProduct(), new ArrayList<>()));
if (!customer.getProductList().contains(product)) {
customer.getProductList().add(product);
}
ProductDetail productDetail = product.getDetailList().stream().filter(pd -> pd.getDetails().equals(entity.getProductDetail())).findFirst().orElse(new ProductDetail(entity.getProductDetail()));
if (!product.getDetailList().contains(productDetail)) {
product.getDetailList().add(productDetail);
}
});
customers.forEach(s -> System.out.println(s));
}
}
I think it is not the best variant, but it's worked. I think there must be more elegant way to replace if in code.
UPD
Annotations on data beans it is Lombok lib annotations that provide the convenient way to declare a bean. It will work in java 7 too.
#AllArgsConstructor - adds the constructor with all fields.
#Data - generate getters/setters for all fields.
#ToString - add the method that returns the string representation of the bean.
Further syntax suitable for java 8 only. This is function List.forEach with the lambda expression, obviously, it iterates the list and can be replaced with for loop.
Each data bean is found or created using the Java 8 stream API (filter with predicate expression) and Optional type (orElse).
So this line:
Customer customer = customers.stream().filter(c -> c.getCustomer().equals(entity.getCustomer())).findFirst().orElse(new Customer(entity.getCustomer(), new ArrayList<>()));
can be replaced in java 7 with:
Customer customer = null;
for (Customer c: customers ) {
if(c.getCustomer().equals(entity.getCustomer())) {
customer = c;
break;
}
}
if (customer == null) {
customer = new Customer(entity.getCustomer(), new ArrayList<>())
}
and so on..
Here is my implementation of your task as my own CustomerList class.
The API of the class is ctor CustomerList(List<Entity> data) and the main method List<Customer> getCustomers() which does real work.
The implementation is based on Java 8 Collector conception. Internally it uses 3 specialized functions: ACCUMULATOR, COMBINER and FINISHER.
The whole idea of the implementation is transforming input List<Entity> into tree-style data structure Map<String, Map<String, Set<String>>. The outer map represents a customer name to products mapping. The internal map represents a product name to product details mapping. And Set<String> represents a set of product detail names. ACCUMULATOR is used for doing this transformation.
FINISHER is used to transform this internal tree-style structure to output List<Customer> data.
The collector uses COMBINER to merge more then one internal tree-style structures for concurrent processing. I haven't tested this but I think the collector can do this processing with simple data.parallelStream().collect(COLLECTOR) statement change.
public class CustomerList {
private static final BiConsumer<Map<String, Map<String, Set<String>>>, Entity> ACCUMULATOR =
(m, e) ->
m.computeIfAbsent(e.getCustomer(), k -> new HashMap<>())
.computeIfAbsent(e.getProduct(), l -> new HashSet<>())
.add(e.getProductDetail());
private static final BinaryOperator<Map<String, Map<String, Set<String>>>> COMBINER =
(m1, m2) -> {
Map<String, Map<String, Set<String>>> r = new HashMap<>(m1);
for (Map.Entry<String, Map<String, Set<String>>> e : m2.entrySet())
r.merge(e.getKey(), e.getValue(), CustomerList::mergeProducts);
return r;
};
private static final Function<Map<String, Map<String, Set<String>>>, List<Customer>> FINISHER =
CustomerList::createCustomerList;
private static final Collector<Entity, Map<String, Map<String, Set<String>>>, List<Customer>> COLLECTOR =
Collector.of(HashMap::new, ACCUMULATOR, COMBINER, FINISHER);
private final List<Entity> data;
public CustomerList(List<Entity> data) {
this.data = data;
}
public List<Customer> getCustomers() {
return data.stream().collect(COLLECTOR);
}
private static Map<String, Set<String>> mergeProducts(Map<String, Set<String>> m1, Map<String, Set<String>> m2) {
Map<String, Set<String>> r = new HashMap<>(m1);
for (Map.Entry<String, Set<String>> e : m2.entrySet())
r.merge(e.getKey(), e.getValue(), CustomerList::mergeDescriptions);
return r;
}
private static Set<String> mergeDescriptions(Set<String> s1, Set<String> s2) {
Set<String> r = new HashSet<>(s1);
r.addAll(s2);
return r;
}
private static List<Customer> createCustomerList(Map<String, Map<String, Set<String>>> customers) {
return customers.entrySet().stream()
.map(e -> new Customer(e.getKey(), createProductList(e.getValue())))
.collect(Collectors.toList());
}
private static List<Product> createProductList(Map<String, Set<String>> products) {
return products.entrySet().stream()
.map(e -> new Product(e.getKey(), createDetailsList(e.getValue())))
.collect(Collectors.toList());
}
private static List<ProductDetail> createDetailsList(Set<String> details) {
return details.stream()
.map(e -> new ProductDetail(e))
.collect(Collectors.toList());
}
}
The demo
I was intrigued by the problem, so i put a few hours into it and came up with the following:
List<Entity> input = asList(
new Entity("A", "A1", "A11"),
new Entity("A", "A1", "A12"),
new Entity("A", "A2", "A21"),
new Entity("A", "A2", "A22"),
new Entity("B", "B1", "B11"),
new Entity("B", "B2", "B21"),
new Entity("C", "C1", "C11"),
new Entity("C", "C1", "C12"));
List<Customer> output = input.stream().collect(
toTree(Entity::getCustomer, Customer::new, toList(),
toTree(Entity::getProduct, Product::new, toList(),
mapping(Entity::getProductDetail, mapping(ProductDetail::new, toList())))));
System.out.println(output);
The implementation
And the implementation of toTree looks like this:
<E, K, C, P, R> Collector<E, ?, R> toTree(Function<E, K> keyMapper,
BiFunction<K, C, P> keyWithChildrenMerger,
Collector<P, ?, R> parentsCollector,
Collector<E, ?, C> childrenCollector) {
return collectingAndThen(
groupingBy(keyMapper, LinkedHashMap::new, childrenCollector),
keysWithChildren -> keysWithChildren.entrySet().stream()
.map(entryFunction(keyWithChildrenMerger))
.collect(parentsCollector));
}
<K, V, R> Function<Map.Entry<K, V>, R> entryFunction(BiFunction<K, V, R> keyValueFunction) {
return entry -> keyValueFunction.apply(entry.getKey(), entry.getValue());
}
It's a Collector that takes input E (Entities in our case), groups them on a key K, collects their children C, merges keys K with their children C to parents P, and collects them to some result, R (in our case, List<P>).
The explanation
What we're doing here is collecting the entities to a tree that is a List of Customers, that each contain a List of Products, that each contain a List of ProductDetails.
Building the tree from the bottom up
It does this by grouping the customers on Entity::getCustomer and the products on Entity::getProduct, and mapping entities to ProductDetails, before creating the tree from the bottom up, by invoking the constructor Product::new with all its ProductDetails, and the constructor Customer::new with all its Products.
The model
The model classes are as you'd expect:
class Entity {
private final String customer;
private final String product;
private final String productDetail;
public Entity(String customer, String product, String productDetail) {
this.customer = customer;
this.product = product;
this.productDetail = productDetail;
}
public String getCustomer() {
return customer;
}
public String getProduct() {
return product;
}
public String getProductDetail() {
return productDetail;
}
}
class ProductDetail {
private String details;
public ProductDetail(String details) {
this.details = details;
}
#Override
public String toString() {
return details;
}
}
class Product {
private String product;
private List<ProductDetail> details;
public Product(String product, List<ProductDetail> productDetails) {
this.product = product;
details = productDetails;
}
#Override
public String toString() {
return "Product{" + product +
", " + details +
'}';
}
}
class Customer {
private String customer;
private List<Product> products;
public Customer(String customer, List<Product> products) {
this.customer = customer;
this.products = products;
}
#Override
public String toString() {
return "Customer{" + customer +
", " + products +
'}';
}
}

Categories

Resources