From 207f977cbd41301651198ecce35bd365db85ca50 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Fri, 13 Sep 2024 13:14:02 +0200 Subject: [PATCH 1/3] Add DesignatedProductClassification for SpecifiedTradeProduct Also contains refactoring of the XML parsing for the `Item` class. Signed-off-by: Jan N. Klug --- .../java/org/mustangproject/ClassCode.java | 103 ++++++ .../DesignatedProductClassification.java | 71 ++++ .../main/java/org/mustangproject/Item.java | 331 ++++-------------- .../main/java/org/mustangproject/Product.java | 96 ++++- .../mustangproject/ReferencedDocument.java | 12 + .../java/org/mustangproject/SchemedID.java | 5 + .../java/org/mustangproject/XMLTools.java | 7 +- .../IDesignatedProductClassification.java | 42 +++ .../ZUGFeRD/IZUGFeRDExportableProduct.java | 7 + .../ZUGFeRD/ZUGFeRD2PullProvider.java | 14 + .../java/org/mustangproject/util/NodeMap.java | 138 ++++++++ 11 files changed, 560 insertions(+), 266 deletions(-) create mode 100644 library/src/main/java/org/mustangproject/ClassCode.java create mode 100644 library/src/main/java/org/mustangproject/DesignatedProductClassification.java create mode 100644 library/src/main/java/org/mustangproject/ZUGFeRD/IDesignatedProductClassification.java create mode 100644 library/src/main/java/org/mustangproject/util/NodeMap.java diff --git a/library/src/main/java/org/mustangproject/ClassCode.java b/library/src/main/java/org/mustangproject/ClassCode.java new file mode 100644 index 00000000..b47cbdf6 --- /dev/null +++ b/library/src/main/java/org/mustangproject/ClassCode.java @@ -0,0 +1,103 @@ +/** + * ********************************************************************* + *

+ * Copyright (c) 2024 Jan N. Klug + *

+ * Use is subject to license terms. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *

+ * See the License for the specific language governing permissions and + * limitations under the License. + *

+ * ********************************************************************** + */ +package org.mustangproject; + +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; + +/** + * A schemed classification for products. The scheme can be anything defined in UNTDID 7143. + */ +public class ClassCode { + private final String listID; + private final String code; + private String listVersionID; + + /** + * A UNTDID 7143 schemed classification code + * + * @param listID the scheme from UNTDID 7143 + * @param code the classification code + * @param listVersionID an (optional) version of the scheme + */ + public ClassCode(String listID, String code, String listVersionID) { + this.listID = listID; + this.code = code; + this.listVersionID = listVersionID; + } + + /** + * A UNTDID 7143 schemed classification code + * + * @param listID the scheme from UNTDID 7143 + * @param code the classification code + */ + public ClassCode(String listID, String code) { + this(listID, code, null); + } + + /** + * Set the version for the scheme returned by {@link #getListID()} + */ + public void setListVersionID(String listVersionID) { + this.listVersionID = listVersionID; + } + + /** + * Get the scheme (according to UNTDID 7143) that describes the value returned by {@link #getCode()}, + * potentially versioned by {@link #getListVersionID()} + * + * @return the scheme + */ + public String getListID() { + return listID; + } + + /** + * Get the code that (following the scheme returned by {@link #getListID()}) describes the product + * + * @return the classification code itself + */ + public String getCode() { + return code; + } + + /** + * Get the (optional) version for the scheme returned by {@link #getListID()} + * + * @return the version or {@code null} if not set + */ + public String getListVersionID() { + return listVersionID; + } + + public static ClassCode fromNode(Node node) { + NamedNodeMap attrs = node.getAttributes(); + if (attrs != null && attrs.getNamedItem("listID") != null) { + ClassCode classCode = new ClassCode(attrs.getNamedItem("listID").getNodeValue(), node.getTextContent()); + if (attrs.getNamedItem("listVersionID") != null) { + classCode.setListVersionID(attrs.getNamedItem("listVersionID").getNodeValue()); + } + return classCode; + } + return null; + } +} diff --git a/library/src/main/java/org/mustangproject/DesignatedProductClassification.java b/library/src/main/java/org/mustangproject/DesignatedProductClassification.java new file mode 100644 index 00000000..ac74bf36 --- /dev/null +++ b/library/src/main/java/org/mustangproject/DesignatedProductClassification.java @@ -0,0 +1,71 @@ +/** + * ********************************************************************* + *

+ * Copyright (c) 2024 Jan N. Klug + *

+ * Use is subject to license terms. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *

+ * See the License for the specific language governing permissions and + * limitations under the License. + *

+ * ********************************************************************** + */ +package org.mustangproject; + +import org.mustangproject.ZUGFeRD.IDesignatedProductClassification; + +/** + * An implementation of {@link IDesignatedProductClassification} for describing a {@link org.mustangproject.Product} + * + */ +public class DesignatedProductClassification implements IDesignatedProductClassification { + private final ClassCode classCode; + private String className; + + /** + * A schemed product descriptor + * + * @param classCode an UNTDID 7143 schemed class code + * @param className a verbal description of the class code + */ + public DesignatedProductClassification(ClassCode classCode, String className) { + this.classCode = classCode; + this.className = className; + } + + /** + * A schemed product descriptor + * + * @param classCode an UNTDID 7143 schemed class code + */ + public DesignatedProductClassification(ClassCode classCode) { + this(classCode, null); + } + + @Override + public ClassCode getClassCode() { + return classCode; + } + + @Override + public String getClassName() { + return className; + } + + /** + * Set the human-readable name of the class code + * + * @param className the name of the class code (can be {@code null}) + */ + public void setClassName(String className) { + this.className = className; + } +} diff --git a/library/src/main/java/org/mustangproject/Item.java b/library/src/main/java/org/mustangproject/Item.java index d4d417c6..1057f617 100644 --- a/library/src/main/java/org/mustangproject/Item.java +++ b/library/src/main/java/org/mustangproject/Item.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import org.mustangproject.ZUGFeRD.IReferencedDocument; import org.mustangproject.ZUGFeRD.IZUGFeRDAllowanceCharge; import org.mustangproject.ZUGFeRD.IZUGFeRDExportableItem; +import org.mustangproject.util.NodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -18,17 +19,22 @@ import java.util.Date; @JsonIgnoreProperties(ignoreUnknown = true) public class Item implements IZUGFeRDExportableItem { - protected BigDecimal price, quantity, tax, grossPrice, lineTotalAmount; + protected BigDecimal price = BigDecimal.ZERO; + protected BigDecimal quantity; + protected BigDecimal tax; + protected BigDecimal grossPrice; + protected BigDecimal lineTotalAmount; protected BigDecimal basisQuantity = BigDecimal.ONE; - protected Date detailedDeliveryPeriodFrom = null, detailedDeliveryPeriodTo = null; + protected Date detailedDeliveryPeriodFrom = null; + protected Date detailedDeliveryPeriodTo = null; protected String id; protected String referencedLineID = null; protected Product product; protected ArrayList notes = null; protected ArrayList referencedDocuments = null; protected ArrayList additionalReference = null; - protected ArrayList Allowances = new ArrayList<>(), - Charges = new ArrayList<>(); + protected ArrayList Allowances = new ArrayList<>(); + protected ArrayList Charges = new ArrayList<>(); /*** * default constructor @@ -42,7 +48,6 @@ public class Item implements IZUGFeRDExportableItem { this.product = product; } - /*** * empty constructor * do not use, but might be used e.g. by jackson @@ -51,269 +56,77 @@ public class Item implements IZUGFeRDExportableItem { } public Item(NodeList itemChilds, boolean recalcPrice) { - String price = "0"; - String basisQuantity = "1"; - String name = ""; - String sellerAssignedID = null; - String description = ""; - SchemedID gid = null; - String quantity = "0"; - String vatPercent = null; - String lineTotal = "0"; - String unitCode = "0"; - String referencedLineID = null; + NodeMap itemMap = new NodeMap(itemChilds); - ArrayList rdocs = null; - ArrayList addRefs = null; + itemMap.getAsNodeMap("Item").ifPresent(icnm -> { + // ubl + //we need: name description unitcode + //and we additionally have vat% + setProduct(new Product()); + icnm.getAsString("Name").ifPresent(product::setName); + icnm.getAsNodeMap("ClassifiedTaxCategory").flatMap(m -> m.getAsString("Percent")) + .ifPresent(vatPercent -> product.setVATPercent(new BigDecimal(vatPercent.trim()))); + }); - // nodes.item(i).getTextContent())) { + itemMap.getAsNodeMap("Price").ifPresent(icnm -> { + // ubl + // PriceAmount with currencyID and BaseQuantity with unitCode + icnm.getAsString("PriceAmount").ifPresent(amount -> setPrice(new BigDecimal(amount.trim()))); + icnm.getAsString("BaseQuantity").ifPresent(baseQuantity -> setQuantity(new BigDecimal(baseQuantity.trim()))); + }); - for (int itemChildIndex = 0; itemChildIndex < itemChilds.getLength(); itemChildIndex++) { - String lineTrade = itemChilds.item(itemChildIndex).getLocalName(); - if ((lineTrade != null) && (lineTrade.equals("Item"))) { - // ubl - //we need: name description unitcode - //and we additionally have vat% - NodeList UBLitemChilds = itemChilds.item(itemChildIndex).getChildNodes(); - for (Node currentUBLItemChildNode : XMLTools.asList(UBLitemChilds)) { + itemMap.getNode("InvoicedQuantity").ifPresent(icn -> { + // ubl + setQuantity(new BigDecimal(icn.getTextContent().trim())); + product.setUnit(icn.getAttributes().getNamedItem("unitCode").getNodeValue()); + }); - if ((currentUBLItemChildNode.getLocalName() != null) && (currentUBLItemChildNode.getLocalName().equals("Name"))) { - name = currentUBLItemChildNode.getTextContent(); - } - if ((currentUBLItemChildNode.getLocalName() != null) && (currentUBLItemChildNode.getLocalName().equals("ClassifiedTaxCategory"))) { - for (Node currentUBLTaxChildNode : XMLTools.asList(currentUBLItemChildNode.getChildNodes())) { - if ((currentUBLTaxChildNode.getLocalName() != null) && (currentUBLTaxChildNode.getLocalName().equals("Percent"))) { - vatPercent = currentUBLTaxChildNode.getTextContent(); - } - } + itemMap.getAsNodeMap("SpecifiedLineTradeAgreement", "SpecifiedSupplyChainTradeAgreement").ifPresent(icnm -> { + icnm.getAllNodes("AdditionalReferencedDocument").map(ReferencedDocument::fromNode) + .forEach(this::addReferencedDocument); + + icnm.getAsNodeMap("BuyerOrderReferencedDocument") + .flatMap(bordNodes -> bordNodes.getAsString("LineID")) + .ifPresent(this::addReferencedLineID); + + icnm.getAsNodeMap("NetPriceProductTradePrice").ifPresent(npptpNodes -> { + npptpNodes.getAsString("ChargeAmount") + .ifPresent(amount -> setPrice(new BigDecimal(amount.trim()))); + npptpNodes.getAsString("BasisQuantity") + .ifPresent(bq -> setBasisQuantity(new BigDecimal(bq.trim()))); + }); + }); + + itemMap.getNode("SpecifiedTradeProduct").map(Product::new).ifPresent(this::setProduct); + + // RequestedQuantity is for Order-X, BilledQuantity for FX and ZF + itemMap.getAsNodeMap("SpecifiedLineTradeDelivery", "SpecifiedSupplyChainTradeDelivery") + .flatMap(icnm -> icnm.getNode("BilledQuantity", "RequestedQuantity", "DespatchedQuantity")) + .ifPresent(bq -> { + setQuantity(new BigDecimal(bq.getTextContent().trim())); + if (bq.hasAttributes()) { + Node unitAttr = bq.getAttributes().getNamedItem("unitCode"); + if (unitAttr != null) { + product.setUnit(unitAttr.getNodeValue()); } } + }); + + itemMap.getAsNodeMap("SpecifiedLineTradeSettlement", "SpecifiedSupplyChainTradeSettlement").ifPresent(icnm -> { + icnm.getAsNodeMap("ApplicableTradeTax") + .map(cnm -> cnm.getAsStringOrNull("RateApplicablePercent", "ApplicablePercent")) + .ifPresent(vatPercent -> product.setVATPercent(new BigDecimal(vatPercent.trim()))); + + if (recalcPrice && !BigDecimal.ZERO.equals(quantity)) { + icnm.getAsNodeMap("SpecifiedTradeSettlementLineMonetarySummation") + .map(cnm -> cnm.getAsStringOrNull("LineTotalAmount")) + .ifPresent(lineTotal -> setPrice(new BigDecimal(lineTotal.trim()).divide(quantity, 4, RoundingMode.HALF_UP))); } - if ((lineTrade != null) && (lineTrade.equals("Price"))) { - // ubl - // PriceAmount with currencyID and BaseQuantity with unitCode - NodeList UBLpriceChilds = itemChilds.item(itemChildIndex).getChildNodes(); - for (Node currentUBLPriceChildNode : XMLTools.asList(UBLpriceChilds)) { - if ((currentUBLPriceChildNode.getLocalName() != null) && (currentUBLPriceChildNode.getLocalName().equals("PriceAmount"))) { - price = currentUBLPriceChildNode.getTextContent(); - } - if ((currentUBLPriceChildNode.getLocalName() != null) && (currentUBLPriceChildNode.getLocalName().equals("BaseQuantity"))) { - basisQuantity = currentUBLPriceChildNode.getTextContent(); - } - } - - } - if ((lineTrade != null) && (lineTrade.equals("InvoicedQuantity"))) { - // ubl - quantity = itemChilds.item(itemChildIndex).getTextContent(); - unitCode = itemChilds.item(itemChildIndex).getAttributes() - .getNamedItem("unitCode").getNodeValue(); - } - if ((lineTrade != null) && (lineTrade.equals("SpecifiedLineTradeAgreement") - || lineTrade.equals("SpecifiedSupplyChainTradeAgreement"))) { - NodeList tradeLineChilds = itemChilds.item(itemChildIndex).getChildNodes(); - for (int tradeLineChildIndex = 0; tradeLineChildIndex < tradeLineChilds - .getLength(); tradeLineChildIndex++) { - - if ((tradeLineChilds.item(tradeLineChildIndex).getLocalName() != null) && tradeLineChilds - .item(tradeLineChildIndex).getLocalName().equals("AdditionalReferencedDocument")) { - String IssuerAssignedID = ""; - String TypeCode = ""; - String ReferenceTypeCode = ""; - - NodeList refDocChilds = tradeLineChilds.item(tradeLineChildIndex).getChildNodes(); - for (int refDocIndex = 0; refDocIndex < refDocChilds.getLength(); refDocIndex++) { - String localName = refDocChilds.item(refDocIndex).getLocalName(); - if ((localName != null) && (localName.equals("IssuerAssignedID"))) { - IssuerAssignedID = refDocChilds.item(refDocIndex).getTextContent(); - } - if ((localName != null) && (localName.equals("TypeCode"))) { - TypeCode = refDocChilds.item(refDocIndex).getTextContent(); - } - if ((localName != null) && (localName.equals("ReferenceTypeCode"))) { - ReferenceTypeCode = refDocChilds.item(refDocIndex).getTextContent(); - } - } - - ReferencedDocument rd = new ReferencedDocument(IssuerAssignedID, TypeCode, - ReferenceTypeCode); - if (rdocs == null) { - rdocs = new ArrayList<>(); - } - rdocs.add(rd); - - } - - if ((tradeLineChilds.item(tradeLineChildIndex).getLocalName() != null) && tradeLineChilds.item(tradeLineChildIndex).getLocalName().equals("BuyerOrderReferencedDocument")) { - NodeList docChilds = tradeLineChilds.item(tradeLineChildIndex).getChildNodes(); - for (int docIndex = 0; docIndex < docChilds.getLength(); docIndex++) { - String localName = docChilds.item(docIndex).getLocalName(); - if ((localName != null) && (localName.equals("LineID"))) { - referencedLineID = docChilds.item(docIndex).getTextContent(); - } - } - } - - if ((tradeLineChilds.item(tradeLineChildIndex).getLocalName() != null) && tradeLineChilds - .item(tradeLineChildIndex).getLocalName().equals("NetPriceProductTradePrice")) { - NodeList netChilds = tradeLineChilds.item(tradeLineChildIndex).getChildNodes(); - for (int netIndex = 0; netIndex < netChilds.getLength(); netIndex++) { - if ((netChilds.item(netIndex).getLocalName() != null) - && (netChilds.item(netIndex).getLocalName().equals("ChargeAmount"))) { - price = netChilds.item(netIndex).getTextContent();// ChargeAmount - - } - if ((netChilds.item(netIndex).getLocalName() != null) - && ((netChilds.item(netIndex).getLocalName().equals("BasisQuantity")) || (netChilds.item(netIndex).getLocalName().equals("InvoicedQuantity")))) { - basisQuantity = netChilds.item(netIndex).getTextContent();// ChargeAmount - - } - } - } - } - } - if ((lineTrade != null) && (lineTrade.equals("SpecifiedLineTradeDelivery") - || lineTrade.equals("SpecifiedSupplyChainTradeDelivery"))) { - NodeList tradeLineChilds = itemChilds.item(itemChildIndex).getChildNodes(); - for (int tradeLineChildIndex = 0; tradeLineChildIndex < tradeLineChilds - .getLength(); tradeLineChildIndex++) { - String tradeName = tradeLineChilds.item(tradeLineChildIndex).getLocalName(); - if ((tradeName != null) - && (tradeName.equals("BilledQuantity") || tradeName.equals("RequestedQuantity") - || tradeName.equals("DespatchedQuantity"))) { - // RequestedQuantity is for Order-X, BilledQuantity for FX and ZF - quantity = tradeLineChilds.item(tradeLineChildIndex).getTextContent(); - unitCode = tradeLineChilds.item(tradeLineChildIndex).getAttributes() - .getNamedItem("unitCode").getNodeValue(); - } - } - } - if ((lineTrade != null) && (lineTrade.equals("SpecifiedTradeProduct"))) { - NodeList tradeProductChilds = itemChilds.item(itemChildIndex).getChildNodes(); - for (int tradeProductChildIndex = 0; tradeProductChildIndex < tradeProductChilds - .getLength(); tradeProductChildIndex++) { - if ((tradeProductChilds.item(tradeProductChildIndex).getLocalName() != null) - && (tradeProductChilds.item(tradeProductChildIndex).getLocalName() - .equals("Name"))) { - name = tradeProductChilds.item(tradeProductChildIndex).getTextContent(); - } - if ((tradeProductChilds.item(tradeProductChildIndex).getLocalName() != null) - && (tradeProductChilds.item(tradeProductChildIndex).getLocalName() - .equals("SellerAssignedID"))) { - sellerAssignedID = tradeProductChilds.item(tradeProductChildIndex).getTextContent(); - } - if ((tradeProductChilds.item(tradeProductChildIndex).getLocalName() != null) - && (tradeProductChilds.item(tradeProductChildIndex).getLocalName() - .equals("GlobalID"))) { - if (tradeProductChilds.item(tradeProductChildIndex).getAttributes() - .getNamedItem("schemeID") != null) { - gid = new SchemedID() - .setScheme(tradeProductChilds.item(tradeProductChildIndex).getAttributes() - .getNamedItem("schemeID").getNodeValue()) - .setId(tradeProductChilds.item(tradeProductChildIndex).getTextContent()); - } - - } - } - } - if ((lineTrade != null) && (lineTrade.equals("SpecifiedLineTradeSettlement") - || lineTrade.equals("SpecifiedSupplyChainTradeSettlement"))) { - NodeList tradeSettlementChilds = itemChilds.item(itemChildIndex).getChildNodes(); - for (int tradeSettlementChildIndex = 0; tradeSettlementChildIndex < tradeSettlementChilds - .getLength(); tradeSettlementChildIndex++) { - - String tradeSettlementName = tradeSettlementChilds.item(tradeSettlementChildIndex) - .getLocalName(); - if (tradeSettlementName != null) { - if (tradeSettlementName.equals("ApplicableTradeTax")) { - NodeList taxChilds = tradeSettlementChilds.item(tradeSettlementChildIndex) - .getChildNodes(); - for (int taxChildIndex = 0; taxChildIndex < taxChilds - .getLength(); taxChildIndex++) { - String taxChildName = taxChilds.item(taxChildIndex).getLocalName(); - if ((taxChildName != null) && (taxChildName.equals("RateApplicablePercent") - || taxChildName.equals("ApplicablePercent"))) { - vatPercent = taxChilds.item(taxChildIndex).getTextContent(); - } - } - } - - if (tradeSettlementName.equals("SpecifiedTradeSettlementLineMonetarySummation")) { - NodeList totalChilds = tradeSettlementChilds.item(tradeSettlementChildIndex) - .getChildNodes(); - for (int totalChildIndex = 0; totalChildIndex < totalChilds - .getLength(); totalChildIndex++) { - if ((totalChilds.item(totalChildIndex).getLocalName() != null) && (totalChilds - .item(totalChildIndex).getLocalName().equals("LineTotalAmount"))) { - lineTotal = totalChilds.item(totalChildIndex).getTextContent(); - } - } - } - - if (tradeSettlementName.equals("AdditionalReferencedDocument")) { - String IssuerAssignedID = ""; - String TypeCode = ""; - String ReferenceTypeCode = ""; - - NodeList refDocChilds = tradeSettlementChilds.item(tradeSettlementChildIndex).getChildNodes(); - for (int refDocIndex = 0; refDocIndex < refDocChilds.getLength(); refDocIndex++) { - String localName = refDocChilds.item(refDocIndex).getLocalName(); - if ((localName != null) && (localName.equals("IssuerAssignedID"))) { - IssuerAssignedID = refDocChilds.item(refDocIndex).getTextContent(); - } - if ((localName != null) && (localName.equals("TypeCode"))) { - TypeCode = refDocChilds.item(refDocIndex).getTextContent(); - } - if ((localName != null) && (localName.equals("ReferenceTypeCode"))) { - ReferenceTypeCode = refDocChilds.item(refDocIndex).getTextContent(); - } - } - - ReferencedDocument rd = new ReferencedDocument(IssuerAssignedID, TypeCode, - ReferenceTypeCode); - if (addRefs == null) { - addRefs = new ArrayList<>(); - } - addRefs.add(rd); - - } - - } - } - } - } - BigDecimal prc = new BigDecimal(price.trim()); - BigDecimal qty = new BigDecimal(quantity.trim()); - if ((recalcPrice) && (!qty.equals(BigDecimal.ZERO))) { - prc = new BigDecimal(lineTotal.trim()).divide(qty, 4, RoundingMode.HALF_UP); - } - Product p = new Product(name, description, unitCode, - vatPercent == null ? null : new BigDecimal(vatPercent.trim())); - if (gid != null) { - p.addGlobalID(gid); - } - if (sellerAssignedID != null) { - p.setSellerAssignedID(sellerAssignedID); - } - setProduct(p); - setPrice(prc); - setQuantity(qty); - setBasisQuantity(new BigDecimal(basisQuantity)); - if (rdocs != null) { - for (ReferencedDocument rdoc : rdocs) { - addReferencedDocument(rdoc); - } - } - if (addRefs != null) { - for (ReferencedDocument rdoc : addRefs) { - addAdditionalReference(rdoc); - } - } - addReferencedLineID( referencedLineID ); + icnm.getAllNodes("AdditionalReferencedDocument").map(ReferencedDocument::fromNode).forEach(this::addAdditionalReference); + }); } - public Item addReferencedLineID(String s) { referencedLineID = s; return this; @@ -544,6 +357,7 @@ public class Item implements IZUGFeRDExportableItem { * this will be included in a BillingSpecifiedPeriod element * @return the beginning of the delivery period */ + @Override public Date getDetailedDeliveryPeriodFrom() { return detailedDeliveryPeriodFrom; } @@ -553,6 +367,7 @@ public class Item implements IZUGFeRDExportableItem { * this will be included in a BillingSpecifiedPeriod element * @return the end of the delivery period */ + @Override public Date getDetailedDeliveryPeriodTo() { return detailedDeliveryPeriodTo; } diff --git a/library/src/main/java/org/mustangproject/Product.java b/library/src/main/java/org/mustangproject/Product.java index df5e329f..1e92fbfd 100644 --- a/library/src/main/java/org/mustangproject/Product.java +++ b/library/src/main/java/org/mustangproject/Product.java @@ -1,10 +1,17 @@ package org.mustangproject; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import org.mustangproject.ZUGFeRD.IDesignatedProductClassification; import org.mustangproject.ZUGFeRD.IZUGFeRDExportableProduct; +import org.mustangproject.util.NodeMap; +import org.w3c.dom.Node; import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; +import java.util.List; +import java.util.Map; /*** * describes a product, good or service used in an invoice item line @@ -18,7 +25,8 @@ public class Product implements IZUGFeRDExportableProduct { protected boolean isIntraCommunitySupply = false; protected SchemedID globalId = null; protected String countryOfOrigin = null; - protected HashMap attributes = null; + protected HashMap attributes = new HashMap<>(); + protected List classifications = new ArrayList<>(); /*** * default constructor @@ -34,6 +42,42 @@ public class Product implements IZUGFeRDExportableProduct { this.VATPercent = VATPercent; } + public Product(Node node) { + NodeMap nodeMap = new NodeMap(node); + + nodeMap.getNode("GlobalID").ifPresent(idNode -> { + if (idNode.hasAttributes() + && idNode.getAttributes().getNamedItem("schemeID") != null) { + globalId = new SchemedID() + .setScheme(idNode.getAttributes().getNamedItem("schemeID").getNodeValue()) + .setId(idNode.getTextContent()); + } + }); + + nodeMap.getAsString("SellerAssignedID").ifPresent(this::setSellerAssignedID); + nodeMap.getAsString("BuyerAssignedID").ifPresent(this::setBuyerAssignedID); + nodeMap.getAsString("Name").ifPresent(this::setName); + nodeMap.getAsString("Description").ifPresent(this::setDescription); + + nodeMap.getAsNodeMap("ApplicableProductCharacteristic").ifPresent(apcNodes -> { + String key = apcNodes.getAsStringOrNull("Description"); + String value = apcNodes.getAsStringOrNull("Value"); + if (key != null && value != null) { + if (attributes == null) { + attributes = new HashMap<>(); + } + attributes.put(key, value); + } + }); + + nodeMap.getAsNodeMap("DesignatedProductClassification").ifPresent(dpcNodes -> { + String className = dpcNodes.getAsStringOrNull("ClassName"); + dpcNodes.getNode("ClassCode").map(ClassCode::fromNode).ifPresent(classCode -> + classifications.add(new DesignatedProductClassification(classCode, className))); + }); + + nodeMap.getAsString("OriginTradeCounty").ifPresent(this::setCountryOfOrigin); + } /*** * empty constructor @@ -202,19 +246,57 @@ public class Product implements IZUGFeRDExportableProduct { @Override public HashMap getAttributes() { - return this.attributes; + if (attributes.isEmpty()) { + return null; + } else { + return this.attributes; + } } - public Product setAttributes(HashMap attributes) { - this.attributes = attributes; + public Product setAttributes(Map attributes) { + this.attributes.clear(); + if (attributes != null) { + this.attributes.putAll(attributes); + } return this; } public Product addAttribute(String name, String value ) { - if ( this.attributes == null ) { - this.attributes = new HashMap<>(); - } this.attributes.put(name, value); return this; } + + @Override + public IDesignatedProductClassification[] getClassifications() { + if (classifications.isEmpty()) { + return null; + } else { + return classifications.toArray(new IDesignatedProductClassification[0]); + } + } + + /** + * Replace the current set of {@link IDesignatedProductClassification}s with a new set + * + * @param classifications the new set of classifications + * @return the modified object + */ + public Product setClassifications(IDesignatedProductClassification[] classifications) { + this.classifications.clear(); + if (classifications != null) { + this.classifications.addAll(Arrays.asList(classifications)); + } + return this; + } + + /** + * Add a {@link IDesignatedProductClassification} classification + * + * @param classification the classification + * @return the modified object + */ + public Product addClassification(IDesignatedProductClassification classification) { + this.classifications.add(classification); + return this; + } } diff --git a/library/src/main/java/org/mustangproject/ReferencedDocument.java b/library/src/main/java/org/mustangproject/ReferencedDocument.java index 423c1bc5..4d2151ca 100644 --- a/library/src/main/java/org/mustangproject/ReferencedDocument.java +++ b/library/src/main/java/org/mustangproject/ReferencedDocument.java @@ -1,6 +1,8 @@ package org.mustangproject; import org.mustangproject.ZUGFeRD.IReferencedDocument; +import org.mustangproject.util.NodeMap; +import org.w3c.dom.Node; public class ReferencedDocument implements IReferencedDocument { @@ -58,4 +60,14 @@ public class ReferencedDocument implements IReferencedDocument { public String getReferenceTypeCode() { return referenceTypeCode; } + + public static ReferencedDocument fromNode(Node node) { + if (!node.hasChildNodes()) { + return null; + } + NodeMap nodes = new NodeMap(node); + return new ReferencedDocument(nodes.getAsStringOrNull("IssuerAssignedID"), + nodes.getAsStringOrNull("TypeCode"), + nodes.getAsStringOrNull("ReferenceTypeCode")); + } } diff --git a/library/src/main/java/org/mustangproject/SchemedID.java b/library/src/main/java/org/mustangproject/SchemedID.java index d3071850..9df8a2d9 100644 --- a/library/src/main/java/org/mustangproject/SchemedID.java +++ b/library/src/main/java/org/mustangproject/SchemedID.java @@ -31,4 +31,9 @@ public class SchemedID { setId(id); } + @Override + public String toString() { + return "SchemedID{scheme='" + scheme + "', id='" + id + "'}"; + } + } diff --git a/library/src/main/java/org/mustangproject/XMLTools.java b/library/src/main/java/org/mustangproject/XMLTools.java index af44d904..c882e573 100644 --- a/library/src/main/java/org/mustangproject/XMLTools.java +++ b/library/src/main/java/org/mustangproject/XMLTools.java @@ -5,9 +5,15 @@ import java.io.InputStream; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.AbstractList; +import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.RandomAccess; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; import org.apache.commons.io.IOUtils; import org.dom4j.io.XMLWriter; @@ -23,7 +29,6 @@ public class XMLTools extends XMLWriter { @Override public String escapeElementEntities(String s) { return super.escapeElementEntities(s); - } public static List asList(NodeList n) { diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IDesignatedProductClassification.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IDesignatedProductClassification.java new file mode 100644 index 00000000..327fb085 --- /dev/null +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IDesignatedProductClassification.java @@ -0,0 +1,42 @@ +/** + * ********************************************************************* + *

+ * Copyright (c) 2024 Jan N. Klug + *

+ * Use is subject to license terms. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *

+ * See the License for the specific language governing permissions and + * limitations under the License. + *

+ * ********************************************************************** + */ +package org.mustangproject.ZUGFeRD; + +import org.mustangproject.ClassCode; + +/** + * A product classification that allows to describe a product. Classification codes are schemed by the available systems in UNTDID 7143. + */ +public interface IDesignatedProductClassification { + /** + * Classification code + * + * @return the classification code + */ + ClassCode getClassCode(); + + /** + * an optional, human-readable description of the classifcation code + * + * @return the name or {@code null} if not set + */ + default String getClassName() { return null; } +} diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableProduct.java b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableProduct.java index 049bb546..ae9eb5b8 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableProduct.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/IZUGFeRDExportableProduct.java @@ -165,4 +165,11 @@ public interface IZUGFeRDExportableProduct { default HashMap getAttributes() { return null; } + + /** + * Detailed information about the product + * + * @return an array containing the product classifications or {@code null} if not set + */ + default IDesignatedProductClassification[] getClassifications() { return null; } } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java index f98674c4..16e3906d 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2PullProvider.java @@ -423,6 +423,20 @@ public class ZUGFeRD2PullProvider implements IXMLProvider { XMLTools.encodeXML(currentItem.getProduct().getDescription()) + ""; } + if (currentItem.getProduct().getClassifications() != null && currentItem.getProduct().getClassifications().length > 0) { + for (IDesignatedProductClassification classification : currentItem.getProduct().getClassifications()) { + xml += "" + + ""; + if (classification.getClassName() != null) { + xml += "" + XMLTools.encodeXML(classification.getClassName()) + ""; + } + xml += ""; + } + } if (currentItem.getProduct().getAttributes() != null) { for ( Entry entry : currentItem.getProduct().getAttributes().entrySet() ) { xml += "" + diff --git a/library/src/main/java/org/mustangproject/util/NodeMap.java b/library/src/main/java/org/mustangproject/util/NodeMap.java new file mode 100644 index 00000000..abe73d12 --- /dev/null +++ b/library/src/main/java/org/mustangproject/util/NodeMap.java @@ -0,0 +1,138 @@ +/** + * ********************************************************************* + *

+ * Copyright (c) 2024 Jan N. Klug + *

+ * Use is subject to license terms. + *

+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0. + *

+ * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + *

+ * See the License for the specific language governing permissions and + * limitations under the License. + *

+ * ********************************************************************** + */ +package org.mustangproject.util; + +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +/** + * The {@link NodeMap} contains a {@link Map} representation of DOM object + *

+ * It can be constructed either from the children of a single {@link Node} or a {@link NodeList}. + */ +public class NodeMap { + private final Map map; + + /** + * Create a new {@link NodeMap} + *

+ * The {@link Node} that is passed to the constructor must not be null. + * The {@link NodeMap} will be empty when no child nodes are present. + * + * @param node the node that shall be represented by this {@link NodeMap} + * @throws IllegalArgumentException when argument is null + */ + public NodeMap(Node node) { + if (node == null) { + throw new IllegalArgumentException("node cannot be null"); + } + if (!node.hasChildNodes()) { + map = Map.of(); + } else { + map = mapNodeList(node.getChildNodes()); + } + } + + public NodeMap(NodeList nodeList) { + if (nodeList == null) { + throw new IllegalArgumentException("nodeList cannot be null"); + } + map = mapNodeList(nodeList); + } + + /** + * Get matching node by {@code LocalName} + *

+ * In case more than one node matches, it is not guaranteed that the first match is selected + * + * @param localNames one or more {@code LocalName}s + * @return the matching node + */ + public Optional getNode(String... localNames) { + return getAllNodes(localNames).findAny(); + } + + /** + * Get a {@link NodeMap} of the child of a matching node by {@code LocalName} + *

+ * In case more than one node matches, it is not guaranteed that the first match is selected + * + * @param localNames one or more {@code LocalName}s + * @return the {@link NodeMap} representation of the matching node + */ + public Optional getAsNodeMap(String... localNames) { + return getNode(localNames).filter(Node::hasChildNodes).map(NodeMap::new); + } + + /** + * Get the text content of a matching node + *

+ * In case more than one node matches, it is not guaranteed that the first match is selected + * + * @param localNames one or more {@code LocalName}s + * @return the text content of the matching node + */ + public Optional getAsString(String... localNames) { + return getNode(localNames).map(Node::getTextContent); + } + + /** + * Get the text content of a matching node + *

+ * In case more than one node matches, it is not guaranteed that the first match is selected + * + * @param localNames one or more {@code LocalName}s + * @return the text content of the matching node or {@code null} when no matching node could be found + */ + public String getAsStringOrNull(String... localNames) { + return getAsString(localNames).orElse(null); + } + + /** + * Get all matching nodes + * + * @param localNames one or more {@code LocalName}s + * @return a {@code Stream} that contains all matching nodes (can be empty if nothing matches) + */ + public Stream getAllNodes(String... localNames) { + List localNamesList = Arrays.asList(localNames); + return map.entrySet().stream().filter(e -> localNamesList.contains(e.getKey())).map(Map.Entry::getValue); + } + + private Map mapNodeList(NodeList nodeList) { + return IntStream.range(0, nodeList.getLength()).mapToObj(nodeList::item) + .filter(node -> node != null && node.getLocalName() != null) + .collect(Collectors.toMap(Node::getLocalName, node -> node)); + } + + @Override + public String toString() { + return map.toString(); + } +} From fdf7211184b347067d14b9e494f49790fa355112 Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Fri, 13 Sep 2024 14:16:15 +0200 Subject: [PATCH 2/3] improvements Signed-off-by: Jan N. Klug --- .../main/java/org/mustangproject/Item.java | 28 +++++++------- .../java/org/mustangproject/XMLTools.java | 37 ------------------- .../java/org/mustangproject/util/NodeMap.java | 13 +++++++ 3 files changed, 26 insertions(+), 52 deletions(-) diff --git a/library/src/main/java/org/mustangproject/Item.java b/library/src/main/java/org/mustangproject/Item.java index 1057f617..9f568a6d 100644 --- a/library/src/main/java/org/mustangproject/Item.java +++ b/library/src/main/java/org/mustangproject/Item.java @@ -64,15 +64,15 @@ public class Item implements IZUGFeRDExportableItem { //and we additionally have vat% setProduct(new Product()); icnm.getAsString("Name").ifPresent(product::setName); - icnm.getAsNodeMap("ClassifiedTaxCategory").flatMap(m -> m.getAsString("Percent")) - .ifPresent(vatPercent -> product.setVATPercent(new BigDecimal(vatPercent.trim()))); + icnm.getAsNodeMap("ClassifiedTaxCategory").flatMap(m -> m.getAsBigDecimal("Percent")) + .ifPresent(product::setVATPercent); }); itemMap.getAsNodeMap("Price").ifPresent(icnm -> { // ubl // PriceAmount with currencyID and BaseQuantity with unitCode - icnm.getAsString("PriceAmount").ifPresent(amount -> setPrice(new BigDecimal(amount.trim()))); - icnm.getAsString("BaseQuantity").ifPresent(baseQuantity -> setQuantity(new BigDecimal(baseQuantity.trim()))); + icnm.getAsBigDecimal("PriceAmount").ifPresent(this::setPrice); + icnm.getAsBigDecimal("BaseQuantity").ifPresent(this::setBasisQuantity); }); itemMap.getNode("InvoicedQuantity").ifPresent(icn -> { @@ -82,19 +82,17 @@ public class Item implements IZUGFeRDExportableItem { }); itemMap.getAsNodeMap("SpecifiedLineTradeAgreement", "SpecifiedSupplyChainTradeAgreement").ifPresent(icnm -> { - icnm.getAllNodes("AdditionalReferencedDocument").map(ReferencedDocument::fromNode) - .forEach(this::addReferencedDocument); - icnm.getAsNodeMap("BuyerOrderReferencedDocument") .flatMap(bordNodes -> bordNodes.getAsString("LineID")) .ifPresent(this::addReferencedLineID); icnm.getAsNodeMap("NetPriceProductTradePrice").ifPresent(npptpNodes -> { - npptpNodes.getAsString("ChargeAmount") - .ifPresent(amount -> setPrice(new BigDecimal(amount.trim()))); - npptpNodes.getAsString("BasisQuantity") - .ifPresent(bq -> setBasisQuantity(new BigDecimal(bq.trim()))); + npptpNodes.getAsBigDecimal("ChargeAmount").ifPresent(this::setPrice); + npptpNodes.getAsBigDecimal("BasisQuantity").ifPresent(this::setBasisQuantity); }); + + icnm.getAllNodes("AdditionalReferencedDocument").map(ReferencedDocument::fromNode) + .forEach(this::addReferencedDocument); }); itemMap.getNode("SpecifiedTradeProduct").map(Product::new).ifPresent(this::setProduct); @@ -114,13 +112,13 @@ public class Item implements IZUGFeRDExportableItem { itemMap.getAsNodeMap("SpecifiedLineTradeSettlement", "SpecifiedSupplyChainTradeSettlement").ifPresent(icnm -> { icnm.getAsNodeMap("ApplicableTradeTax") - .map(cnm -> cnm.getAsStringOrNull("RateApplicablePercent", "ApplicablePercent")) - .ifPresent(vatPercent -> product.setVATPercent(new BigDecimal(vatPercent.trim()))); + .flatMap(cnm -> cnm.getAsBigDecimal("RateApplicablePercent", "ApplicablePercent")) + .ifPresent(product::setVATPercent); if (recalcPrice && !BigDecimal.ZERO.equals(quantity)) { icnm.getAsNodeMap("SpecifiedTradeSettlementLineMonetarySummation") - .map(cnm -> cnm.getAsStringOrNull("LineTotalAmount")) - .ifPresent(lineTotal -> setPrice(new BigDecimal(lineTotal.trim()).divide(quantity, 4, RoundingMode.HALF_UP))); + .flatMap(cnm -> cnm.getAsBigDecimal("LineTotalAmount")) + .ifPresent(lineTotal -> setPrice(lineTotal.divide(quantity, 4, RoundingMode.HALF_UP))); } icnm.getAllNodes("AdditionalReferencedDocument").map(ReferencedDocument::fromNode).forEach(this::addAdditionalReference); diff --git a/library/src/main/java/org/mustangproject/XMLTools.java b/library/src/main/java/org/mustangproject/XMLTools.java index c882e573..8ee9fe5a 100644 --- a/library/src/main/java/org/mustangproject/XMLTools.java +++ b/library/src/main/java/org/mustangproject/XMLTools.java @@ -4,21 +4,9 @@ import java.io.IOException; import java.io.InputStream; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.AbstractList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.RandomAccess; -import java.util.stream.Collectors; -import java.util.stream.IntStream; -import java.util.stream.Stream; import org.apache.commons.io.IOUtils; import org.dom4j.io.XMLWriter; -import org.w3c.dom.Node; -import org.w3c.dom.NodeList; public class XMLTools extends XMLWriter { @Override @@ -31,30 +19,6 @@ public class XMLTools extends XMLWriter { return super.escapeElementEntities(s); } - public static List asList(NodeList n) { - return n.getLength() == 0 ? - Collections.emptyList() : new NodeListWrapper(n); - } - - static final class NodeListWrapper extends AbstractList - implements RandomAccess { - private final NodeList list; - - NodeListWrapper(NodeList l) { - list = l; - } - - @Override - public Node get(int index) { - return list.item(index); - } - - @Override - public int size() { - return list.getLength(); - } - } - public static String nDigitFormat(BigDecimal value, int scale) { /* * I needed 123,45, locale independent.I tried @@ -75,7 +39,6 @@ public class XMLTools extends XMLWriter { } - public static String encodeXML(CharSequence s) { if (s == null) { return ""; diff --git a/library/src/main/java/org/mustangproject/util/NodeMap.java b/library/src/main/java/org/mustangproject/util/NodeMap.java index abe73d12..373781f7 100644 --- a/library/src/main/java/org/mustangproject/util/NodeMap.java +++ b/library/src/main/java/org/mustangproject/util/NodeMap.java @@ -23,6 +23,7 @@ package org.mustangproject.util; import org.w3c.dom.Node; import org.w3c.dom.NodeList; +import java.math.BigDecimal; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -102,6 +103,18 @@ public class NodeMap { return getNode(localNames).map(Node::getTextContent); } + /** + * Get the text content of a matching node + *

+ * In case more than one node matches, it is not guaranteed that the first match is selected + * + * @param localNames one or more {@code LocalName}s + * @return the text content of the matching node, converted to BigDecimal + */ + public Optional getAsBigDecimal(String... localNames) { + return getNode(localNames).map(Node::getTextContent).map(s->new BigDecimal(s.trim())); + } + /** * Get the text content of a matching node *

From e8b8e0ff07778b3cbc6deb2dd0aec755ecbb602a Mon Sep 17 00:00:00 2001 From: "Jan N. Klug" Date: Fri, 13 Sep 2024 14:40:12 +0200 Subject: [PATCH 3/3] fix for multiple elements Signed-off-by: Jan N. Klug --- .../java/org/mustangproject/util/NodeMap.java | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/library/src/main/java/org/mustangproject/util/NodeMap.java b/library/src/main/java/org/mustangproject/util/NodeMap.java index 373781f7..6afbb911 100644 --- a/library/src/main/java/org/mustangproject/util/NodeMap.java +++ b/library/src/main/java/org/mustangproject/util/NodeMap.java @@ -24,7 +24,9 @@ import org.w3c.dom.Node; import org.w3c.dom.NodeList; import java.math.BigDecimal; +import java.util.ArrayList; import java.util.Arrays; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -38,7 +40,7 @@ import java.util.stream.Stream; * It can be constructed either from the children of a single {@link Node} or a {@link NodeList}. */ public class NodeMap { - private final Map map; + private final Map> map = new HashMap<>(); /** * Create a new {@link NodeMap} @@ -53,10 +55,8 @@ public class NodeMap { if (node == null) { throw new IllegalArgumentException("node cannot be null"); } - if (!node.hasChildNodes()) { - map = Map.of(); - } else { - map = mapNodeList(node.getChildNodes()); + if (node.hasChildNodes()) { + mapNodeList(node.getChildNodes()); } } @@ -64,7 +64,7 @@ public class NodeMap { if (nodeList == null) { throw new IllegalArgumentException("nodeList cannot be null"); } - map = mapNodeList(nodeList); + mapNodeList(nodeList); } /** @@ -135,13 +135,13 @@ public class NodeMap { */ public Stream getAllNodes(String... localNames) { List localNamesList = Arrays.asList(localNames); - return map.entrySet().stream().filter(e -> localNamesList.contains(e.getKey())).map(Map.Entry::getValue); + return map.entrySet().stream().filter(e -> localNamesList.contains(e.getKey())).flatMap(e -> e.getValue().stream()); } - private Map mapNodeList(NodeList nodeList) { - return IntStream.range(0, nodeList.getLength()).mapToObj(nodeList::item) + private void mapNodeList(NodeList nodeList) { + IntStream.range(0, nodeList.getLength()).mapToObj(nodeList::item) .filter(node -> node != null && node.getLocalName() != null) - .collect(Collectors.toMap(Node::getLocalName, node -> node)); + .forEach(node -> map.computeIfAbsent(node.getLocalName(), k -> new ArrayList<>()).add(node)); } @Override