I have JSON document which is a result of parsing for a bunch of files:
{
"offer": {
"clientName": "Tom",
"insuranceCompany": "INSURANCE",
"address": "GAMLE BONDALSVEGEN 53",
"renewalDate": "22.12.2018",
"startDate": "22.12.2017",
"too_old": false,
"products": [
{
"productType": "TRAVEL",
"objectName": "Reiseforsikring - Holen, Tom Andre",
"name": null,
"value": null,
"isExclude": false,
"monthPrice": null,
"yearPrice": 1637,
"properties": {}
}
]
},
"documents": [
{
"clientName": "Tom",
"insuranceCompany": "INSURANCE",
"fileName": "insurance_tom.pdf",
"address": "GAMLE BONDALSVEGEN 53",
"renewalDate": "22.12.2019",
"startDate": "22.12.2018",
"issuedDate": "20.11.2018",
"policyNumber": "6497777",
"products": [
{
"productType": "TRAVEL",
"objectName": "Reiseforsikring - Holen, Tom Andre",
"name": null,
"value": null,
"isExclude": false,
"monthPrice": null,
"yearPrice": 1921,
"properties": {
"TRAVEL_PRODUCT_NAME": "Reise Ekstra",
"TRAVEL_DURATION_TYPE": "DAYS",
"TRAVEL_TYPE": "FAMILY",
"TRAVEL_DURATION": "70",
"TRAVEL_INSURED_CLIENT_NAME": "Holen, Tom Andre, Familie"
}
},
I want to iterate throug all products from documents section and set missed properties to products from offer section.
Offer and documents at the same depth level at JSON.
Implementation for this with Stream API is following:
private void mergePropertiesToOffer(InsuranceDocumentsSession insuranceSession) {
Validate.notNull(insuranceSession, "insurance session can't be null");
if (insuranceSession.getOffer() == null) return;
log.info("BEFORE_MERGE");
// merge all properties by `objectName`
Stream.of(insuranceSession).forEach(session -> session.getDocuments().stream()
.filter(Objects::nonNull)
.flatMap(doc -> doc.getProducts().stream())
.filter(Objects::nonNull)
.filter(docProduct -> StringUtils.isNotEmpty(docProduct.getObjectName()))
.filter(docProduct -> MapUtils.isNotEmpty(docProduct.getProperties()))
.forEach(docProduct -> Stream.of(session.getOffer())
.flatMap(offer -> offer.getProducts().stream())
.filter(Objects::nonNull)
.filter(offerProduct -> MapUtils.isEmpty(offerProduct.getProperties()))
.filter(offerProduct -> StringUtils.isNotEmpty(offerProduct.getObjectName()))
.filter(offerProduct -> offerProduct.getObjectName().equals(docProduct.getObjectName()))
.forEach(offerProduct -> {
try {
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
log.info("BEFORE_PRODUCT: {}", mapper.writeValueAsString(offerProduct));
offerProduct.setProperties(docProduct.getProperties());
log.info("UPDATED_PRODUCT: {}", mapper.writeValueAsString(offerProduct));
} catch (JsonProcessingException e) {
log.error("Error converting product to offer: {}", e.getCause());
}
})));
log.info("AFTER_MERGE");
}
It works fine. However, implementing is much faster than maintaining in the future.
There two times I am using Stream.of() factory method for getting a stream for 2 entities at a different level. Also, flatMap() is used as much as possible, + all null checks.
And the question isn't this implementation too difficult?
Should it be refactored and divided into smaller parts? If yes how it should be with good programming principles?
SOLUTION:
Huge thanks to nullpointer answer.
Final solution is following:
Map<Integer, InsuranceProductDto> offerProductMap = session.getOffer().getProducts()
.stream()
.filter(this::validateOfferProduct)
.collect(Collectors.toMap(InsuranceProductDto::getYearPrice, Function.identity(), (first, second) -> first));
Map<Integer, InsuranceProductDto> documentsProductMap = session.getDocuments()
.stream()
.flatMap(d -> d.getProducts().stream())
.filter(this::validateDocumentProduct)
.collect(Collectors.toMap(InsuranceProductDto::getYearPrice, Function.identity(), (first, second) -> first));
documentsProductMap.forEach((docPrice, docProduct) -> {
if (offerProductMap.containsKey(docPrice)) {
offerProductMap.compute(docPrice, (s, offerProduct) -> {
setProductProperties(offerProduct, docProduct);
return offerProduct;
});
}
});
// after finishing execution `offerProductMap` contains updated products
To start off with, you can create a common Predicates for those chained filters as
.filter(offerProduct -> MapUtils.isEmpty(offerProduct.getProperties()))
.filter(offerProduct -> StringUtils.isNotEmpty(offerProduct.getObjectName()))
.filter(offerProduct -> offerProduct.getObjectName().equals(docProduct.getObjectName()))
you can write a Predicate such that
Predicate<OfferProduct> offerProductSelection = offerProduct -> MapUtils.isEmpty(offerProduct.getProperties())
&& StringUtils.isNotEmpty(offerProduct.getObjectName())
&& offerProduct.getObjectName().equals(docProduct.getObjectName());
and then simply use that as a single filter
.filter(offerProductSelection);
By the way, you could have preferably moved that to a method returning boolean and then used that in the filter.
Not precise for the sake of data types and utility classes used, but for the sake of representation, you could do something like :
private void mergePropertiesToOffer(InsuranceDocumentsSession insuranceSession) {
Validate.notNull(insuranceSession, "insurance session can't be null");
if (insuranceSession.getOffer() == null) return;
Map<String, InsuranceProductDto> offerProductMap = insuranceSession.getOffer().getProducts()
.stream()
.filter(this::validateOfferProduct)
.collect(Collectors.toMap(InsuranceProductDto::getObjectName, Function.identity())); // assuming 'objectName' to be unique
Map<String, InsuranceProductDto> documentsProductMap = insuranceSession.getDocuments()
.stream()
.filter(Objects::nonNull)
.flatMap(d -> d.getProducts().stream())
.filter(this::validateDocumentProduct)
.collect(Collectors.toMap(InsuranceProductDto::getObjectName, Function.identity())); // assuming 'objectName' to be unique
Map<String, Product> productsToProcess = new HashMap<>(documentsProductMap);
productsToProcess.forEach((k, v) -> {
if (offerProductMap.containsKey(k)) {
offerProductMap.compute(k, (s, product) -> {
Objects.requireNonNull(product).setProperties(v.getProperties());
return product;
});
}
});
// now the values of 'offerProductMap' is what you can set as an updated product list under offer
}
private boolean validateDocumentProduct(InsuranceProductDto product) {
return Objects.nonNull(product)
&& MapUtils.isNotEmpty(product.getProperties())
&& StringUtils.isNotEmpty(product.getObjectName());
}
private boolean validateOfferProduct(InsuranceProductDto offerProduct) {
return Objects.nonNull(offerProduct)
&& MapUtils.isEmpty(offerProduct.getProperties())
&& StringUtils.isNotEmpty(offerProduct.getObjectName());
}
Edit: Based on the comment,
objectName can be the same for a bunch of products
you can update the code to use the merge function as:
Map<String, InsuranceProductDto> offerProductMap = insuranceSession.getOffer().getProducts()
.stream()
.filter(this::validateOfferProduct)
.collect(Collectors.toMap(InsuranceProductDto::getObjectName, Function.identity(),
(a,b) -> {// logic to merge and return value for same keys
}));
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With