Java Stream: How to map multiple value to a single DTO? - java

I have a List result in my Spring Boot service method as shown below:
Country:
String name;
List<State> states;
State:
String name;
Long population;
List<Town> towns;
Town:
String name;
I return Country list from my repository and it has all the related State and Town date for each Country. I want to map this data to a DTO as shown below:
public class CountryDTO {
private Long id;
private String name;
private Long population; //population sum of states by country
private List<Town> towns;
//constructor
}
So, how can I map my Country entity to this CountryDTO properly as explained above? I need the following data:
Country name
Sum of population by each country
Towns of each country
Update: Here is my service method where I tried to use Java Stream and then also ModelMapper, but cannot return desired values :(
List<CountryDTO> countries = countryRepository.findAll().stream()
.flatMap(x -> x.getStates().stream())
.map(x -> new CountryDTO(x.getCountry(), <-- need to return sum of population here -->, ObjectMapperUtils.mapAll(x.getTowns(), TownDTO.class)))
.toList();

You can use Stream#mapToInt to get all the state populations and .sum to add them up.
Stream#flatMap can be used to get all the towns of each state into a single stream.
List<CountryDTO> res = countries.stream().map(c -> new CountryDTO(c.getName(),
c.getStates().stream().mapToInt(State::getPopulation).sum(),
c.getStates().stream().flatMap(s -> s.getTowns().stream()).toList())).toList();

Didn't get your question completely, maybe it will be more helpful if you share the exact response returned by the APIs.
But, going by the use case, if you are getting a list in the response, you always have the option of stream the list, iterate each element, and then use conditional / grouping logics to get the data in the desired way.
For example;
Let's say, you have a list of Object type, for which you have a repository layer configured as ObjetRepo. Now, if you are getting a list of objects, you can stream it like this:
#Autowired
private ObjectRepo objectRepo;
public List<Object> method() {
List<Object> objects = new ArrayList<>();
objectRepo.findAll().forEach(objects::add);
// Here, instead of add, any other logic can be used.
// You can also write a method, and use that method here
// objectRepo.findAll().forEach(objects::someMethod);
return objects;
}
Hope this helps.

Related

Generate Map using list of list using stream in Java 8

I have the following domain classes Trip and Employee:
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Trip {
private Date startTime;
private Date endTime;
List<Employee> empList;
}
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Employee {
private String name;
private String empId;
}
I have a list of Trip instances. And I want to create a map of type Map<String,List<Trip>> associating id of each employee empId with a list of trips using Stream API.
Here's my attempt:
public static void main(String[] args) {
List<Trip> trips = new ArrayList<>();
Map<Stream<String>, List<Trip>> x = trips.stream()
.collect(Collectors.groupingBy(t -> t.getEmpList()
.stream().map(Employee::getEmpId)
));
}
How can I generate the map of the required type?
When the type of map is Map<String,List<Trip>> it gives me a compilation error:
Unresolved compilation problem: Type mismatch:
cannot convert from Map<Object,List<Trip>> to Map<String,List<Trip>>
To group the data by the property of a nested object and at the same time preserve a link to the enclosing object, you need to flatten the stream using an auxiliary object that would hold references to both employee id and enclosing Trip instance.
A Java 16 record would fit into this role perfectly well. If you're using an earlier JDK version, you can implement it a plain class (a quick and dirty way would be to use Map.Entry, but it decreases the readability, because of the faceless methods getKey() and getValue() require more effort to reason about the code). I will go with a record, because this option is the most convenient.
The following line is all we need (the rest would be automatically generated by the compiler):
public record TripEmployee(String empId, Trip trip) {}
The first step is to flatten the stream data and turn the Stream<Trip> into Stream<TripEmployee>. Since it's one-to-many transformation, we need to use flatMap() operation to turn each Employee instance into a TripEmployee.
And then we need to apply collect. In order to generate the resulting Map, we can make use of the collector groupingBy() with collector mapping() as a downstream. In collector mapping always requires a downstream collector and this case we need to provide toList().
List<Trip> trips = // initializing the list
Map<String, List<Trip>> empMap = trips.stream()
.flatMap(trip -> trip.getEmpList().stream()
.map(emp -> new TripEmployee(emp.getEmpId(), trip))
)
.collect(Collectors.groupingBy(
TripEmployee::empId,
Collectors.mapping(TripEmployee::trip,
Collectors.toList())
));
A Java 8 compliant solution is available via this Link
Not sure which Java version you are using but since you have mentioned Stream, I will assume Java 8 at least.
Second assumption, not sure why but looking at your code (using groupingBy ) you want the whole List<Trip> which you get against an empId in a Map.
To have the better understanding first look at this code (without Stream):
public Map<String, List<Trip>> doSomething(List<Trip> listTrip) {
List<Employee> employeeList = new ArrayList<>();
for (Trip trip : listTrip) {
employeeList.addAll(trip.getEmployee());
}
Map<String, List<Trip>> stringListMap = new HashMap<>();
for (Employee employee : employeeList) {
stringListMap.put(employee.getEmpId(), listTrip);
}
return stringListMap;
}
You can see I pulled an employeeList first , reason being your use case. And now you can see how easy was to create a map out of it.
You may use Set instead of List if you're worried about the duplicates.
So with StreamApi above code could be:
public Map<String, List<Trip>> doSomethingInStream(List<Trip> listTrip) {
List<Employee> employeeList = listTrip.stream().flatMap(e -> e.getEmployee().stream()).collect(Collectors.toList());
return employeeList.stream().collect(Collectors.toMap(Employee::getEmpId, employee -> listTrip));
}
You can take care of duplicates while creating map as well, as:
public Map<String, List<Trip>> doSomething3(List<Trip> listTrip) {
List<Employee> employeeList = listTrip.stream().flatMap(e -> e.getEmployee().stream()).collect(Collectors.toList());
return employeeList.stream().collect(Collectors.toMap(Employee::getEmpId, employee -> listTrip, (oldValue, newValue) -> newValue));
}
Like the first answer says, if you are Java 16+ using record will ease your task a lot in terms of model definition.
Using Java 8 stream
You can use the below approach to get the desired results using stream function groupingBy.
Since you have mentioned above to use java 8, so my solution is inclined to java 8 itself.
Logic:
Here,
First I have created an additional list of EmployeeTripMapping object
with Trip data corresponding to the empId by iterating the
listOfTrips.
I have used Collectors.groupingBy on the List<EmployeeTripMapping>
and grouped the data based on the empId and using Collectors.mapping
collect the list of Trip corresponding to the empId.
Few Suggestions:
Records in java 14 : As I can see in your problem statement, you
are using lombok
annotations to create getters, setters and constructors, so instead of
that we can replace our data classes
with records. Records are immutable classes that require only the type
and name of fields. We do not need to create constructor, getters,
setters, override toString() methods, override hashcode and equals
methods. Here
JavaTimeAPI in java 8: Instead of Date, you can use LocalDateTime available in java time API in java 8. Here
Code:
public class Test {
public static void main(String[] args) {
Trip t1 = new Trip(LocalDateTime.of(2022,10,28,9,00,00),
LocalDateTime.of(2022,10,28,18,00,00),
Arrays.asList(new Employee("emp1","id1")));
Trip t2 = new Trip(LocalDateTime.of(2021,10,28,9,00,00),
LocalDateTime.of(2021,10,28,18,00,00),
Arrays.asList(new Employee("emp1","id1")));
Trip t3 = new Trip(LocalDateTime.of(2020,10,28,9,00,00),
LocalDateTime.of(2020,10,28,18,00,00),
Arrays.asList(new Employee("emp2","id2")));
Trip t4 = new Trip(LocalDateTime.of(2019,10,28,9,00,00),
LocalDateTime.of(2019,10,28,18,00,00),
Arrays.asList(new Employee("emp2","id2")));
List<Trip> listOfTrips = Arrays.asList(t1,t2,t3,t4);
List<EmployeeTripMapping> empWithTripMapping = new ArrayList<>();
listOfTrips.forEach(x -> x.getEmpList().forEach(y ->
empWithTripMapping.add(new EmployeeTripMapping(y.getEmpId(),x))));
Map<String,List<Trip>> employeeTripGrouping = empWithTripMapping.stream()
.collect(Collectors.groupingBy(EmployeeTripMapping::getEmpId,
Collectors.mapping(EmployeeTripMapping::getTrip,
Collectors.toList())));
System.out.println(employeeTripGrouping);
}
}
EmployeeTripMapping.java
public class EmployeeTripMapping {
private String empId;
private Trip trip;
//getters and setters
}
Output:
{emp2=[Trip{startTime=2020-10-28T09:00, endTime=2020-10-28T18:00, empList=[Employee{empId='emp2', name='id2'}]}, Trip{startTime=2019-10-28T09:00, endTime=2019-10-28T18:00, empList=[Employee{empId='emp2', name='id2'}]}],
emp1=[Trip{startTime=2022-10-28T09:00, endTime=2022-10-28T18:00, empList=[Employee{empId='emp1', name='id1'}]}, Trip{startTime=2021-10-28T09:00, endTime=2021-10-28T18:00, empList=[Employee{empId='emp1', name='id1'}]}]}

Filter an object by specific properties java

So I have this class:
public class Seat {
private Long id;
private float positionX;
private float positionY;
private int numOfSeats;
private String label;
//getters and setters
}
I have List of Seat class on:
List<Seat> seatList = // get data from repository;
I also have this arraylist contains list of ids:
List<Long> idList; // for example : [1, 2, 3]
I want to filter seatList so that the filtered ArrayList does not contain a Seat object with id from idList, so I tried to use stream:
List<Seat> filteredSeat = seatList.stream()
.filter(seat -> {
// function to filter seat.getId() so it would return the Seat object with id that does not equals to ids from idList
})
.collect(Collectors.toList());
I cant find the correct function to do it. Does anyone have suggestion for me to try?
You want to use the overriden method from Collection#contains(Object) with the negation implying the id was not found in the List.
Set<Seat> filteredSeat = seatList.stream()
.filter(seat -> !idList.contains(seat.getId()))
.collect(Collectors.toSet());
Few notes:
You want to use Set<Long> instead of List<Long> for an efficient look-up. Moreover, it doesn't make sense to have duplicate values among ids, so Set is a good choice.
Collectors.toSet() results Set, so the Stream's return type is Set<Seat>.
The most simple solution for be a for-each loop in which you check each idList against the Seat's id.
perhaps something like
List<Seat> FilteredList;
for ( Seat CurSeat : seatList ){
for(int i = 0; i < idList.size(); i++){
and if the ID of CurSeat is part of idList, it doesn't get added to the new List.
This is definitely not the simplest way, but if you're looking for something easy, this is probably it.
Hope this helped!
Assuming you implement the equals method accordingly (like the doc mentions it), there is a much shorter solution:
seatList.stream()
.distinct()
.collect( Collectors.toList() );

Extract a fields data from a Map<Integer, Object> into a String

I have an Customer Object like below.
public class Custoemr {
private String Id;
Private String Name;
Private String Address;
Private String Description;
Setter/Getter;
toString;
}
This is Contained in Map<String, Customer> map, which contains the customerId and Object as key and value respectively. For analysis purposes, I need to collect all the customer description data in String to be written in a file.
To do that I need to Extract data from description in String and not List<String>.
I saw several examples on the internet which collects them as a List<String> but I need it in a single String.
Is there a way to extract the information without iterating I mean by using java Streams.
If I understood correctly:
yourMap.values()
.stream()
.map(Customer::getDescription)
.collect(Collectors.joining(","));

How to groupBy and Collect with Java 8?

I have a list of elements, let's call it "keywords", like this:
public class Keyword {
Long id;
String name;
String owner;
Date createdTime;
Double price;
Date metricDay;
Long position;
}
The thing is that there is a keyword for every single day. For example:
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.1", metricDay="11/11/1999", position=109}
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.3", metricDay="12/11/1999", position=108}
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.2", metricDay="13/11/1999", position=99}
Keyword{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", price="0.6", metricDay="13/11/1999", position=5}
Keyword{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", price="0.1", metricDay="14/11/1999", position=4}
Keyword{id=3, name="kw3", owner="Josh", createdTime="13/12/1992", price="0.1", metricDay="13/11/1999", position=8}
Then, from this list I would like to create a new list with all the metrics from all those different days on one single list. First, I created a class like this:
public class KeywordMetric {
Double price;
Date metricDay;
Long position;
}
And what I would like to archive is go from the first list, to a structure like this:
public class KeywordMeged {
Long id;
String name;
String owner;
List<KeywordMetric> metricList;
}
Example of what I expect:
KeywordMerged{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", metricList=[KeywordMetric{price=0.1,metricDay="11/11/1999",position=109},KeywordMetric{price=0.3,metricDay="12/11/1999",position=108},KeywordMetric{price=0.2,metricDay="13/11/1999",position=99}]
KeywordMerged{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", metricList=[KeywordMetric{price=0.6,metricDay="13/11/1999",position=5},KeywordMetric{price=0.1,metricDay="14/11/1999",position=4}]
KeywordMerged{id=3, name="kw3", owner="Josh", createdTime="13/12/1992", metricList=[KeywordMetric{price=0.1,metricDay="13/11/1999",position=8}]
I know how to do this with a lot of loops and mutable varibles, but I can't figure out how to do this with streams and lambda operations. I was able to group all related keywords by Id with this:
Map<Long, List<Keyword>> kwL = kwList.stream()
.collect(groupingBy(Keyword::getId))
And I know that with .forEach() I could iterate over that Map, but can't figure out how to make the collect() method of streams pass from List to KeywordMerged.
You can try to use the Collectors.toMap(...) instead. Where:
Keyword::getId is a key mapper function.
KeywordMerged.from(...) performs a transformation: Keyword => KeywordMerged
(left, right) -> { .. } combines metrics for entities with identical ids.
Collection<KeywordMerged> result = keywords.stream()
.collect(Collectors.toMap(
Keyword::getId,
k -> KeywordMerged.from(k), // you can replace this lambda with a method reference
(left, right) -> {
left.getMetricList().addAll(right.getMetricList());
return left;
}))
.values();
A transformation method might look something like this:
public class KeywordMerged {
public static KeywordMerged from(Keyword k) {
KeywordMetric metric = new KeywordMetric();
metric.setPrice(k.getPrice());
metric.setMetricDay(k.getMetricDay());
metric.setPosition(k.getPosition());
KeywordMerged merged = new KeywordMerged();
merged.setId(k.getId());
merged.setName(k.getName());
merged.setOwner(k.getOwner());
merged.setMetricList(new ArrayList<>(Arrays.asList(metric)));
return merged;
}
}
I think you've got the basic idea. So, refactor according to your needs...
A slightly different approach. First you collect the Map of keywords grouped by id:
Map<Integer, List<Keyword>> groupedData = keywords.stream()
.collect(Collectors.groupingBy(k -> k.getId()));
Further you convert your map to the list of desired format:
List<KeywordMerged> finalData = groupedData.entrySet().stream()
.map(k -> new KeywordMerged(k.getValue().get(0).getId(),
k.getValue().stream()
.map(v -> new KeywordMetric(v.getMetricDay(), v.getPrice(), getPosition()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
This will work on the grouped data, but transforming the map it will create KeywordMerged object, which as argument will receive id (you can extent it further yourself) and converted to List<KeywordMetric> previously grouped by ID data.
EDIT: I believe with some extraction to methods you can make it look much nicer :)

Merging list of two objects to create a third object on the basis of common attribute

I have two different csv files having data on two different entities and I have to merge two different csv files to create one on the basis of sql join type equijoin and left join.
so I have created first entity as class name Customer having attributes:
int CustomerId ;
String CustomerName;
int OrderId;
And List of object of this class like:
Customer c1 = new Customer(CustomerId, CustomerName, OrderId);
1 million objects..
List<Customer> cust = new ArrayList<>();
cust.add(c1);
cust.add(c2);
so on to make list of 1 million object.
Similarly, I have created class of second entity Order having attributes:
int orderId;
String orderName;
Date orderdate;
Order o1 = new Order(orderId, orderName, orderdate);
so on 1 million object
List<Oder> order = new ArrayList<>();
Now I need to merge both the object on the basis of orderId and generate third object having result class having all the attributes from both the classes described above.
Please suggest me solution using java stream 8 to map both the streams of list to create inner join and left join type example in the third new result class.
Aside from the getters, your Customer class should have the following method:
public boolean orderMatch(Order order) {
//fixed the attribute name so it would be in camelCase
return orderId == order.getId();
}
Of course, this implies that Order has a getId() getter method to get its id attribute.
Finally, you'll need a CustomerExtended class.
class CustomerExtended {
int customerId ;
String customerName;
Order customerOrder;
public CustomerExtended(Customer customer, Order order) {
customerId = customer.getId();
customerName = customer.getName();
customerOrder = order;
}
}
Now, you can create a Function which would search for the corresponding Order and append it to a Customer:
Function<Customer,CustomerExtended> extendCustomer = (c) ->{
//I used the more descriptive name orderList instead of o1.
Optional<Order> order = orderList.stream()
.filter(c::orderMatch)
.findFirst();
if(order.isPresent()) {
return new CustomerExtended(c,order.get());
}
return null;
};
And then you can apply it to the Customer list through a map.
List<CustomerExtended> newCustomerList = customerList.stream()
.map(c -> extendCustomer.apply(c))
.collect(Collectors.toList());
EDIT: A few last notes
There should be some check that the ID numbers are not duplicate either when adding the objects to the lists, or when the lists are populated.
For semantic purposes, The Customer object as it is should be renamed CustomerOrder or be separated into an object only for customer info and an object which would store the relation between customer and order.
The case where an order is not found should be better handled and throw an exception.

Categories