Удобная сложность для использования Stream API? - PullRequest
0 голосов
/ 28 декабря 2018

У меня есть документ JSON, который является результатом анализа нескольких файлов:

{
  "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"
          }
        },

Я хочу перебрать все products из раздела documents и установить пропущенный properties в products из offer section.

Предложение и документы на том же уровне глубины в JSON.

Для этого в Stream API реализовано следующее:

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работает отлично.Однако внедрение намного быстрее, чем поддержание в будущем.

Там два раза я использую фабричный метод Stream.of() для получения потока для двух сущностей на другом уровне.Кроме того, flatMap() используется в максимально возможной степени, + все нулевые проверки.

И вопрос не слишком ли сложен в этой реализации?

Должен ли он быть переработан и разделен на более мелкие части?Если да, как это должно быть с хорошими принципами программирования?

РЕШЕНИЕ:

Огромное спасибо nullpointer ответ.
Окончательное решение следующее:

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

Ответы [ 2 ]

0 голосов
/ 29 декабря 2018

Для каждого сеанса все свойства предлагаемых продуктов будут ссылаться на свойства последнего квалифицированного документа, верно?

Поскольку внутренний поток всегда будет иметь одинаковый результат независимо от текущего продукта документа.

Итак, исправляя это, я предложу следующий рефакторинг:

final class ValueWriter
{
    private final static ObjectMapper mapper = new ObjectMapper();

    static
    {
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
    }

    static String writeValue(final Object value) throws JsonProcessingException
    {
        return mapper.writeValueAsString(value);
    }
}

private Optional<Product> firstQualifiedDocumentProduct(final InsuranceDocumentsSession insuranceSession)
{
    return insuranceSession.getDocuments().stream()
        .filter(Objects::notNull)
        .map(Document::getProducts)
        .flatMap(Collection::stream)
        .filter(docProduct -> StringUtils.isNotEmpty(docProduct.getObjectName()))
        .filter(docProduct -> MapUtils.isNotEmpty(docProduct.getProperties()))
        .findFirst()
    ;
}

private void mergePropertiesToOffer(final InsuranceDocumentsSession insuranceSession)
{
    Validate.notNull(insuranceSession, "insurance session can't be null");

    if(insuranceSession.getOffer() == null) return;

    log.info("BEFORE_MERGE");

    final Optional<Product> qualifiedDocumentProduct = firstQualifiedDocumentProduct(insuranceSession);

    if (qualifiedDocumentProduct.isPresent())
    {
        insuranceSession.getOffer().getProducts().stream()
            .filter(Objects::nonNull)
            .filter(offerProduct -> MapUtils.isEmpty(offerProduct.getProperties()))
            .filter(offerProduct -> StringUtils.isNotEmpty(offerProduct.getObjectName()))
            .filter(offerProduct -> offerProduct.getObjectName().equals(qualifiedDocumentProduct.get().getObjectName()))
            .forEach(offerProduct ->
            {
                try
                {
                    log.info("BEFORE_PRODUCT: {}", ValueWriter.writeValueAsString(offerProduct));
                    offerProduct.setProperties(qualifiedDocumentProduct.get().getProperties());
                    log.info("BEFORE_PRODUCT: {}", ValueWriter.writeValueAsString(offerProduct));
                }
                catch (final JsonProcessingException e)
                {
                    log.error("Error converting product to offer: {}", e.getCause());
                }
            })
        ;
    }
}
0 голосов
/ 28 декабря 2018

Для начала, вы можете создать общие Predicate s для этих цепочек фильтров как

.filter(offerProduct -> MapUtils.isEmpty(offerProduct.getProperties()))
.filter(offerProduct -> StringUtils.isNotEmpty(offerProduct.getObjectName()))
.filter(offerProduct -> offerProduct.getObjectName().equals(docProduct.getObjectName()))

, вы можете написать Predicate так, чтобы

Predicate<OfferProduct> offerProductSelection = offerProduct -> MapUtils.isEmpty(offerProduct.getProperties())
                                    && StringUtils.isNotEmpty(offerProduct.getObjectName())
                                    && offerProduct.getObjectName().equals(docProduct.getObjectName());

изатем просто используйте это как отдельный фильтр

.filter(offerProductSelection);

Кстати, вы могли бы предпочтительно переместить его в метод, возвращающий boolean, а затем использовать его в фильтре.


Не является точным в отношении используемых типов данных и служебных классов, но ради представления вы можете сделать что-то вроде:

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());
}

Редактировать : На основании комментария

objectName может быть одинаковым для группы продуктов

. Вы можете обновить код для использования функции слияния следующим образом:

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
                            }));
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...