ModelMapper: Map collections to collections of other structure - java

I map business objects to entities and there are cases where the structure of an entity is different from the business objects.
I have userCategories which are stored in the business object RecipeBo as strings, because the BO does not have to know anything about the internal structure of the entities. These strings need to be mapped to a relation of Recipe and RecipeUserCategoryRel, in addition to it, another field, userId of RecipeBo needs to be mapped in this RecipeUserCategoryRel too.
My approach (which works) is to create a wrapper and manually create the relations by hand, but this looks like tinkering:
public class BoMapper
{
private final static ModelMapper modelMapper = new ModelMapper();
static
{
modelMapper.addMappings(new IngredientMap());
}
public static void map(Object from, Object to)
{
modelMapper.map(from, to);
if (from instanceof RecipeBo && to instanceof Recipe)
{
RecipeBo recipeBo = (RecipeBo)from;
List<String> userCategories = recipeBo.getUserCategories();
List<RecipeUserCategoryRel> recipeUserCategoryRels = new ArrayList<>();
for (String userCategory : userCategories)
{
recipeUserCategoryRels.add(new RecipeUserCategoryRel(userCategory, recipeBo.getUserId()));
}
Recipe recipe = (Recipe)to;
recipe.setRecipeUserCategoryRels(recipeUserCategoryRels);
}
}
}
Is there a better approach of that what I'm doing in BoMapper, e.g. using converters or something? The difficulty is to map each element of the list and add the userId field too.

ISSUE
This is a complex situation because you are getting userId from other hierarchy and not directly from the List. ModelMapper would map List to List but if you don't configure ModelMapper as LOOSE it will not be able to work.
modelMapper.getConfiguration()
.setMatchingStrategy(MatchingStrategies.LOOSE);
Anyway, In case you configure ModelMapper in that manner (LOOSE mode) it would map the List and put in String property of your Class RecipeUserCategoryRel (in this case for example userCategory if it is a String and considering userId is not a String) the others (I'm not pretty sure) I think it would be null.
SOLUTION
Well, I think the solution to your issue is to create a Converter and add it to your ModelMapper instance:
RecipeBO (Source) -> Recipe (Destination)
The code would be as bellow:
ModelMapper mapper = new ModelMapper();
Converter<RecipeBO, Recipe> converter = new Converter<RecipeBO,
Recipe>() {
#Override
public Recipe convert(MappingContext<RecipeBO, Recipe> context) {
RecipeBO source = context.getSource();
Recipe destination = new Recipe();
List<String> userCategoryValues = source.getUserCategories();
List<RecipeUserCategoryRel> userCategoryToMap = new ArrayList<RecipeUserCategoryRel>();
for(final String userCategory : userCategoryValues){
userCategoryToMap.add(new RecipeUserCategoryRel(userCategory,source.getUserId()));
}
destination.setRecipeUserCategoryRels(userCategoryToMap);
//... Map other properties if you need
return destination;
}
};
//Option 1
mapper.createTypeMap(RecipeBO.class, Recipe.class).setConverter(converter);
//If you add as a converter directly also works (I don't know which one is better,
//choose above option (createTypeMap + setConverter) or the next (addConverter)
//Option 2 -> mapper.addConverter(converter);
I've tested and It works!!
If I had a Recipe as next:
RecipeBO recipe = new RecipeBO();
recipe.setUserId("1");
String values[] = new String[] { "abc", "klm", "xyz", "pqr" };
List<String> list = Arrays.asList(values);
recipe.setUserCategories(list);
And a RecipeBO:
Recipe recipe = new Recipe();
List<RecipeUserCategoryRel> recipes = new ArrayList<>();
recipes.add(new RecipeUserCategoryRel("abc", "1"));
recipes.add(new RecipeUserCategoryRel("klm", "1"));
recipes.add(new RecipeUserCategoryRel("xyz", "1"));
recipes.add(new RecipeUserCategoryRel("pqr", "1"));
recipe.setRecipeUserCategoryRels(recipes);
When I map RecipeBO to Recipe:
Recipe actual = mapper.map(getRecipeBO(), Recipe.class);
I get the next Output:
OUTPUT:
- RecipeUserCategoryRel(userCategory=abc, userId=1)
- RecipeUserCategoryRel(userCategory=klm, userId=1)
- RecipeUserCategoryRel(userCategory=xyz, userId=1)
- RecipeUserCategoryRel(userCategory=pqr, userId=1)

Related

Java stream - Performant way to update hierarchical object

I need to update an internal object matching criteria. This internal object is deep inside a large object with a hierarchy. The object is something like
ObjectA {
List ObjectB {
List Object C{
int customerId;
String customerStatus;
}
}
}
I need to update "customerStatus" only if customerId is matched to "123".
This entire objectA is stored in the database as a single object (in the real world, this is a protobuf object. Therefore this object is not updated in place)
The non-stream way involves a bunch of loops
List<ObjectB> objectBList = objectA.getObjectBList();
List<ObjectB> updatedObjectBList = new ArrayList<>();
for(objectB: objectBList) {
List<ObjectC> objectCList = objectB.getObjectCList();
List<ObjectC> updatedObjectCList = new ArrayList<>();
for(objectC: objectCList) {
if(objectC.getCustomerId() == 123) {
objectC = createNewObjectCwithUpdatedStatus("UpdatedStatus");
}
updatedObjectCList.add(objectC);
}
updatedObjectBList.addObjectCList(updatedObjectCList);
}
updatedObjectA.addObjectBList(updatedObjectBList);
writeUpdateObjectA_to_storage(updatedObjectA);
Is there a way to write this multiple IF condition using streams option?
It's a bit unclear from your code why you are adding the lists back to the objects once you do the update. As far as I can see you are updating the c objects in place (i.e. they are mutable) so it's not clear why they need to be re-added to the A and B objects.
Assuming that's a mistake, you could just flatten out the hierarchy and then do the updates:
getObjectBList().stream().flatMap(ObjectB::getObjectCList)
.filter(c -> c.getCustomerId() == 123)
.forEach(c -> c.setCustomerStatus("updated"));
If there's a reason to create a new list then that can be achieved as well but how to do it best depends on why you want to do that.
This is another option if you don't want to flat it
// Say you have objA reference
objA.getObjectBList().forEach(objBList -> objBList.getObjectCList().
stream().filter(objCList-> objCList.getCustomerId() == 123)
.forEach(c -> c.setCustomerStatus("updated"));
If all objects are immutable, you can try following solution.
record C(int customerId, String customerStatus){}
record B(List<C> getObjectCList){}
record A(List<B> getObjectBList){}
public static void main(String[] args){
var objectA = new A(new ArrayList<>());
var newObjectBList = objectA.getObjectBList().stream().map(objectB -> {
var newObjectCList = objectB.getObjectCList().stream().map(objectC -> {
return objectC.customerId == 123 ? new C(objectC.customerId, "UpdatedStatus") : objectC;
}).toList();
return new B(newObjectCList);
}).toList();
var newObjectA = new A(newObjectBList);
}
Actually, this is a functional programming style.

How to remove part of a hashmap value?

So I'm building a group of dropdowns that rely upon each other and built a query to get the code and description for a Product Type, Family, and Model object. I used nested hashmaps to story all of the data and objects. This was fine because I can just call all of the information that I need from the hashmaps. However, when it comes to the REST API's, it's going to display all of the nested information for each of the hashmaps when I call them. For each map I have it's key, and then the value consists of a Code, Desc, and the hashmap of the next object.
So, it would be like:
Main hashmap
- Key
- value
-> code
-> desc
-> product family hashmap
-- key
-- value
--> code
--> desc
--> product model hashmap
--- key
--- value
---> code
---> desc
My main question is how can I either strip these additional hashmaps from being displayed in the json format when viewing the REST API via web browser? Or can/do I need to just completely strip the additional information altogether?
#Service
public class ProductDAOImpl implements ProductDAO {
#PersistenceContext
private EntityManager em;
#Override
public Map<String, ProductType> getProductTypeStructure() {
HashMap<String, ProductType> prodTypes = new HashMap<>();
Query q = em.createNativeQuery("<query>");
List<Object[]> prodTypeEntities = q.getResultList();
final String badData = "XX-BAD-XX";
ProductType prodType = new ProductType(badData, "");
ProductFamily prodFamily = new ProductFamily(badData, "");
for(Object[] prodTypeEntity : prodTypeEntities) {
if (prodTypeEntity[1] == null || prodTypeEntity[3] == null || prodTypeEntity[5] == null) {
continue;
}
String prodTypeCd = prodTypeEntity[0].toString().toUpperCase();
String prodTypeDesc = StringUtils.trimTrailingWhitespace(prodTypeEntity[1].toString()).toUpperCase();
String prodFamilyCd = prodTypeEntity[2].toString().toUpperCase();
String prodFamilyDesc = StringUtils.trimTrailingWhitespace(prodTypeEntity[3].toString()).toUpperCase();
String prodModelCd = prodTypeEntity[4].toString().toUpperCase();
String prodModelDesc = StringUtils.trimTrailingWhitespace(prodTypeEntity[5].toString()).toUpperCase();
if(!prodType.getCode().equalsIgnoreCase(prodTypeCd)) {
prodType = new ProductType(prodTypeCd, prodTypeDesc);
prodType.setProdFamilies(new HashMap<String, ProductFamily>());
prodTypes.put(prodType.getCode(), prodType);
prodFamily.setCode(badData);
}
if(!prodFamily.getCode().equalsIgnoreCase(prodFamilyCd)) {
prodFamily = new ProductFamily(prodFamilyCd, prodFamilyDesc);
prodFamily.setProdModels(new HashMap<String, ProductModel>());
prodType.getProdFamilies().put(prodFamily.getCode(), prodFamily);
}
prodFamily.getProdModels().put(prodModelCd, new ProductModel(prodModelCd, prodModelDesc));
}
return prodTypes;
}
}
If I understood your question correctly, I think a DTO object might be the answer here. You add to it only the values that the dropdown might need and return it from the REST API.
Here's more on DTOs.

What will be the best way to iterate the list as per business logic from performance point of view for the below described requirement

I have a list of entity. these are the response from db. I have another list of long. In the list of entity, each entity object has a filed called id. Those id will always be in ascending order.I need to traverse the entity list as per the order given through the list of long. Also I need to maintain another list of response object which will have few more fields than what we have in the entity list. I can not use transient also. The code below will give you an idea.
public List<ResponseObject> convert(List<EntityObject> entityList, List<Long> orderedIdList) {
List<ResponseObject> responseList = new ArrayList<>();
for (EntityObject object : entityList) {
ResponseObject responseObject = new ResponseObject();
responseObject.someSettermethod(object.someGettermethod());
/* other setters in responseObject which are not present in the entity object */
responseObject.otherSetters("some value");
responseList.add(responseObject);
};
return sortInOrder(responseList, orderedIdList);
}
private List<ResponseObject> sortInOrder(List<ResponseObject> responseList,List<Long> orderedIdList) {
List<ResponseObject> finalList = new ArrayList<>();
for(Long id : orderedIdList){
for(ResponseObject response : responseList){
if(response.getId().equals(id)){
finalList.add(response);
}
}
}
return finalList;
}
This is how it has been implemented for now. I would like to know if there is any better approach to enhance the performance to achieve the same output.
The sortInOrder method can be done faster than O(N^2):
Assuming, Ids are unique (let me know if its a wrong assumption):
Idea:
Create a map of Id to responseObject by iterating over the response list O(n).
Iterate over orderedIdList and check for id in map, if the id exists, add the value to response Object.
private List<ResponseObject> sortInOrder(List<ResponseObject> responseList,List<Long> orderedIdList) {
List<ResponseObject> finalList = new ArrayList<>();
Map<Long, ResponseObject> map = responseList.stream().collect(Collectors.toMap(ResponseObject::getId, respObj -> respObj));
for(Long id : orderedList) {
if(map.containsKey(id)) {
finalList.add(map.get(id));
}
}
return finalList;
}
If these lists aren't huge (like in many many thousands of entries), I wouldn't worry about performance. It's reasonable as it is and as long as you don't fail any specific performance requirements you shouldn't optimize your code for performance anyway.
You could on the other hand optimize your code for readability
by using a comparator to sort your list
by using the streams API to reduce the depth of your methods.
The comparator could be constructed using the ordering list and then comparing the indices of the ids from your resultList.
The comparator could look similar to this one:
private static class IndexComparator implements Comparator<Long> {
private final List<Long> order;
private IndexComparator(List<Long> order) {
this.order = order;
}
#Override
public int compare(Long id1, Long id2) {
return Comparator.comparingLong(order::indexOf).compare(id1, id2);
}
}
If you use map instead of a list like below, you can do it with complexity O(n) instead of O(n2)
public List<ResponseObject> convert(List<EntityObject> entityList, List<Long> orderedIdList) {
Map<Long, ResponseObject> responseMap = new HashMap<Long, ResponseObject>();
for (EntityObject object : entityList) {
ResponseObject responseObject = new ResponseObject();
responseObject.someSettermethod(object.someGettermethod());
/* other setters in responseObject which are not present in the entity object */
responseObject.otherSetters("some value");
responseMap.put(responseObject.getId(), responseObject);
};
return sortInOrder(responseMap, orderedIdList);
}
private List<ResponseObject> sortInOrder( Map<Long, ResponseObject> responseMap, List<Long> orderedIdList) {
List<ResponseObject> finalList = new ArrayList<>();
for(Long id : orderedIdList){
finalList.add(responseMap.get(id));
}
return finalList;
}

Creating an independent copy of the entity

I'm creating a new list. I write objects to the new list from the list of entities. Then I will clean items from the list of entities appropriately, which also results in the removal of objects from this new list.
final ContributionEntity contributionEntity = this.findContribution(contributionId, DataStatus.WAITING, user, MovieField.PHOTO);
final Set<Long> idsToAddBeforeCleanUp = contributionEntity.getIdsToAdd();
this.cleanUpIdsToAdd(contributionEntity.getIdsToAdd(), contribution.getIdsToAdd(), contributionEntity.getMovie().getPhotos());
private void cleanUpIdsToAdd(final Set<Long> idsToAddFromEntity, final Set<Long> idsToAddFromDto,
final List<? extends MovieInfoEntity> entities) {
for (final Iterator<Long> it = idsToAddFromEntity.iterator(); it.hasNext(); ) {
final Long id = it.next();
if (!idsToAddFromDto.contains(id)) {
it.remove();
this.delete(entities, id);
}
}
}
This code removes the entity from the list of subject photos contributionEntity , but also removes objects from the list idsToAddBeforeCleanUp.
How do I copy a list from an entity and make it independent of this entity? I do not want to delete items from the list idsToAddBeforeCleanUp.
final Set<Long> idsToAddBeforeCleanUp = new HashSet<>();
contributionEntity.getIdsToAdd().foreach(item -> idsToAddBeforeCleanUp.add(item));
You didn't really create a new set, you just made another reference to the set. With the code above you make a new list and should it work.

Collect values from list of POJO using Functional interfaces (lambdas)

How can I iterate over list of POJO classes for collecting result of some methods in a standard way to avoid copy past?
I want to have code like this:
//class 'Person' has methods: getNames(), getEmails()
List<Person> people = requester.getPeople(u.getId());
String names = merge(people, Person::getNames);
String emails = merge(people, Person::getEmails);
instead of such copy-pasted logic:
List<Person> people = requester.getPeople(u.getId());
Set<String> namesAll = new HashSet<>();
Set<String> emailsAll = new HashSet<>();
for (Person p : people) {
if(p.getNames()!=null) {
phonesAll.addAll(p.getNames());
}
if(p.getEmails()!=null) {
emailsAll.addAll(p.getEmails());
}
}
String names = Joiner.on(", ").skipNulls().join(namesAll);
String emails = Joiner.on(", ").skipNulls().join(emailsAll);
Thus, is it possible to implement some standard approach for iterating and processing special method of POJO in list that could be reused?
If I understand you correctly, you want something like this :
String names = people.stream().flatMap(p->p.getNames().stream()).distinct().collect(Collectors.joining(", "));
Now, if you want to save typing that line for each property, you can have this merge method as you suggested :
public static String merge (List<Person> people, Function<Person, Collection<String>> mapper)
{
return people.stream().flatMap(p->mapper.apply(p).stream()).distinct().collect(Collectors.joining(", "));
}
This would make your first snippet work.
Now, you can make this method generic :
public static <T> String merge (List<T> list, Function<T, Collection<String>> mapper)
{
return list.stream().flatMap(p->mapper.apply(p).stream()).distinct().collect(Collectors.joining(", "));
}
I think this should work (haven't tested it).

Categories

Resources