diff --git a/History.md b/History.md index c083b7ab..8925439d 100644 --- a/History.md +++ b/History.md @@ -2,6 +2,9 @@ - Enhance Charges/Allowances with reasonCode. #432 - Fix build warnings from editing and building. #415 - ZUGFeRDVisualizer.toPDF(): generate PDF/A-3b. #400 +- allow access to invoice attachments via ZUGFeRDInvoiceImporter zii.getEmbeddedFilenames()/zii.getEmbeddedFile(filename) + and XML (zii.getFileAttachments) + 2.12.0 ======= diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java index 5a2a7afd..6bf548a6 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDImporter.java @@ -21,12 +21,7 @@ import java.nio.file.Files; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; @@ -57,7 +52,7 @@ import org.w3c.dom.NodeList; import org.xml.sax.SAXException; public class ZUGFeRDImporter { - private static final Logger LOGGER = LoggerFactory.getLogger (ZUGFeRDImporter.class); + private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDImporter.class); /** * if metadata has been found @@ -67,6 +62,10 @@ public class ZUGFeRDImporter { * map filenames of additional XML files to their contents */ private final HashMap additionalXMLs = new HashMap<>(); + /** + * map filenames of all embedded files in the respective PDF + */ + private final HashMap PDFAttachments = new HashMap<>(); /** * Raw XML form of the extracted data - may be directly obtained. */ @@ -90,7 +89,7 @@ public class ZUGFeRDImporter { try (InputStream bis = Files.newInputStream(Paths.get(pdfFilename), StandardOpenOption.READ)) { extractLowLevel(bis); } catch (final IOException e) { - LOGGER.error ("Failed to extract ZUGFeRD data", e); + LOGGER.error("Failed to extract ZUGFeRD data", e); throw new ZUGFeRDExportException(e); } } @@ -100,12 +99,35 @@ public class ZUGFeRDImporter { try { extractLowLevel(pdfStream); } catch (final IOException e) { - LOGGER.error ("Failed to extract ZUGFeRD data", e); + LOGGER.error("Failed to extract ZUGFeRD data", e); throw new ZUGFeRDExportException(e); } } + /*** + * return the file names of all files embedded into the PDF + * @see for XML embedded files please use ZUGFeRDInvoiceImporter.getFileAttachments + * @return a Stringset + */ + public Set getEmbeddedFilenames() { + return PDFAttachments.keySet(); + } + + /*** + * returns the file contents of the specified filename embedded into the PDF + * @param filename String + * @return a bytearray, or null if the filename has not been fond + */ + public byte[] getEmbeddedFile(String filename) { + + if (PDFAttachments.containsKey(filename)) { + return PDFAttachments.get(filename); + } + return null; + } + + /** * Extracts a ZUGFeRD invoice from a PDF document represented by an input stream. Errors are reported via exception handling. * @@ -121,7 +143,7 @@ public class ZUGFeRDImporter { if (Arrays.equals(pad, pdfSignature)) { // we have a pdf - try (PDDocument doc = Loader.loadPDF(IOUtils.toByteArray (pdfStream))) { + try (PDDocument doc = Loader.loadPDF(IOUtils.toByteArray(pdfStream))) { // PDDocumentInformation info = doc.getDocumentInformation(); final PDDocumentNameDictionary names = new PDDocumentNameDictionary(doc.getDocumentCatalog()); //start @@ -174,10 +196,10 @@ public class ZUGFeRDImporter { /** * filenames for invoice data (ZUGFeRD v1 and v2, Factur-X) */ + final PDEmbeddedFile embeddedFile = fileSpec.getEmbeddedFile(); if ((filename.equals("ZUGFeRD-invoice.xml") || (filename.equals("zugferd-invoice.xml")) || filename.equals("factur-x.xml")) || filename.equals("xrechnung.xml") || filename.equals("order-x.xml") || filename.equals("cida.xml")) { containsMeta = true; - final PDEmbeddedFile embeddedFile = fileSpec.getEmbeddedFile(); // String embeddedFilename = filePath + filename; // File file = new File(filePath + filename); // System.out.println("Writing " + embeddedFilename); @@ -191,9 +213,9 @@ public class ZUGFeRDImporter { // fos.close(); } if (filename.startsWith("additional_data")) { - final PDEmbeddedFile embeddedFile = fileSpec.getEmbeddedFile(); additionalXMLs.put(filename, embeddedFile.toByteArray()); } + PDFAttachments.put(filename, embeddedFile.toByteArray()); } } @@ -214,12 +236,13 @@ public class ZUGFeRDImporter { public void setRawXML(byte[] rawXML) throws IOException { + this.containsMeta = true; this.rawXML = rawXML; this.version = null; try { setDocument(); } catch (ParserConfigurationException | SAXException e) { - LOGGER.error ("Failed to parse XML", e); + LOGGER.error("Failed to parse XML", e); throw new ZUGFeRDExportException(e); } } @@ -236,7 +259,7 @@ public class ZUGFeRDImporter { final XPath xpath = xpathFact.newXPath(); result = xpath.evaluate(xpathStr, document); } catch (final XPathExpressionException e) { - LOGGER.error ("Failed to evaluate XPath", e); + LOGGER.error("Failed to evaluate XPath", e); throw new ZUGFeRDExportException(e); } return result; @@ -268,7 +291,7 @@ public class ZUGFeRDImporter { */ public String getZUGFeRDProfil() { String guideline = extractString("//*[local-name() = 'GuidelineSpecifiedDocumentContextParameter']//*[local-name() = 'ID']"); - if(guideline.contains("xrechnung")) { + if (guideline.contains("xrechnung")) { return "XRECHNUNG"; } switch (guideline) { @@ -438,7 +461,7 @@ public class ZUGFeRDImporter { return extractString("//*[local-name() = 'SpecifiedTradeSettlementHeaderMonetarySummation']//*[local-name() = 'TotalPrepaidAmount']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return ""; } } @@ -476,7 +499,7 @@ public class ZUGFeRDImporter { return extractString("//*[local-name() = 'ExchangedDocument']//*[local-name() = 'IncludedNote']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return ""; } } @@ -507,7 +530,7 @@ public class ZUGFeRDImporter { return extractString("//*[local-name() = 'SpecifiedTradeSettlementHeaderMonetarySummation']//*[local-name() = 'LineTotalAmount']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return ""; } } @@ -530,7 +553,7 @@ public class ZUGFeRDImporter { return extractString("//*[local-name() = 'ActualDeliverySupplyChainEvent']//*[local-name() = 'OccurrenceDateTime']//*[local-name() = 'DateTimeString']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return ""; } } @@ -546,7 +569,7 @@ public class ZUGFeRDImporter { return extractString("//*[local-name() = 'ExchangedDocument']//*[local-name() = 'ID']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return ""; } } @@ -563,7 +586,7 @@ public class ZUGFeRDImporter { return extractString("//*[local-name() = 'ExchangedDocument']/*[local-name() = 'TypeCode']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return ""; } } @@ -580,7 +603,7 @@ public class ZUGFeRDImporter { return extractString("//*[local-name() = 'ApplicableHeaderTradeAgreement']/*[local-name() = 'BuyerReference']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return ""; } } @@ -784,7 +807,7 @@ public class ZUGFeRDImporter { static String convertStreamToString(java.io.InputStream is) { try { return IOUtils.toString(is, StandardCharsets.UTF_8); - } catch (IOException e) { + } catch (IOException e) { throw new UncheckedIOException(e); } } @@ -805,7 +828,7 @@ public class ZUGFeRDImporter { nl = getNodeListByPath("//*[local-name() = 'CrossIndustryInvoice']//*[local-name() = 'SupplyChainTradeTransaction']//*[local-name() = 'ApplicableHeaderTradeAgreement']//*[local-name() = 'BuyerTradeParty']//*[local-name() = 'PostalTradeAddress']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return null; } @@ -827,7 +850,7 @@ public class ZUGFeRDImporter { nl = getNodeListByPath("//*[local-name() = 'CrossIndustryInvoice']//*[local-name() = 'SupplyChainTradeTransaction']//*[local-name() = 'ApplicableHeaderTradeAgreement']//*[local-name() = 'SellerTradeParty']//*[local-name() = 'PostalTradeAddress']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return null; } @@ -849,7 +872,7 @@ public class ZUGFeRDImporter { nl = getNodeListByPath("//*[local-name() = 'CrossIndustryInvoice']//*[local-name() = 'SupplyChainTradeTransaction']//*[local-name() = 'ApplicableHeaderTradeDelivery']//*[local-name() = 'ShipToTradeParty']//*[local-name() = 'PostalTradeAddress']"); } } catch (final Exception e) { - // Exception was already logged + // Exception was already logged return null; } @@ -1062,7 +1085,7 @@ public class ZUGFeRDImporter { nl = getNodeListByPath("//*[local-name() = 'IncludedSupplyChainTradeLineItem']"); } catch (final Exception e) { - // Exception was already logged + // Exception was already logged } for (int i = 0; i < nl.getLength(); i++) { diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java index 5207bcf2..3654cee3 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDInvoiceImporter.java @@ -7,6 +7,7 @@ import java.nio.charset.StandardCharsets; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; +import java.util.Base64; import java.util.Date; import java.util.List; @@ -16,14 +17,7 @@ import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; -import org.mustangproject.Allowance; -import org.mustangproject.BankDetails; -import org.mustangproject.Charge; -import org.mustangproject.EStandard; -import org.mustangproject.Invoice; -import org.mustangproject.Item; -import org.mustangproject.TradeParty; -import org.mustangproject.XMLTools; +import org.mustangproject.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.Node; @@ -33,6 +27,7 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter { private static final Logger LOGGER = LoggerFactory.getLogger(ZUGFeRDInvoiceImporter.class.getCanonicalName()); // log private boolean recalcPrice = false; private boolean ignoreCalculationErrors = false; + private ArrayList fileAttachments=new ArrayList<>(); public ZUGFeRDInvoiceImporter() { super(); @@ -346,6 +341,16 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter { } + // now handling base64 encoded attachments AttachmentBinaryObject=CII, EmbeddedDocumentBinaryObject=UBL + xpr = xpath.compile("//*[local-name()=\"AttachmentBinaryObject\"]|//*[local-name()=\"EmbeddedDocumentBinaryObject\"]"); + NodeList attachmentNodes = (NodeList) xpr.evaluate(getDocument(), XPathConstants.NODESET); + for (int i = 0; i < attachmentNodes.getLength(); i++) { + FileAttachment fa=new FileAttachment(attachmentNodes.item(i).getAttributes().getNamedItem("filename").getNodeValue(),attachmentNodes.item(i).getAttributes().getNamedItem("mimeCode").getNodeValue(),"",Base64.getDecoder().decode(attachmentNodes.item(i).getTextContent())); + fileAttachments.add(fa); + // filename = "Aufmass.png" mimeCode = "image/png" + //EmbeddedDocumentBinaryObject cbc:EmbeddedDocumentBinaryObject mimeCode="image/png" filename="Aufmass.png" + } + // item level charges+allowances are not yet handled but a lower item price will // be read, // so the invoice remains arithmetically correct @@ -442,6 +447,15 @@ public class ZUGFeRDInvoiceImporter extends ZUGFeRDImporter { } + /*** + * + * @return the file attachments embedded in XML using base64, + * @see for PDF embedded files use getEmbeddedFilenames()/getEmbeddedFile() + */ + public List getFileAttachments() { + return fileAttachments; + } + /*** * This will parse a XML into a invoice object * diff --git a/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java b/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java index 0c85e1a8..a51642dc 100644 --- a/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java +++ b/library/src/test/java/org/mustangproject/ZUGFeRD/XRTest.java @@ -27,12 +27,16 @@ import org.junit.FixMethodOrder; import org.junit.runners.MethodSorters; +import javax.xml.xpath.XPathExpressionException; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; +import java.text.ParseException; +import java.util.Arrays; import java.util.Date; +import java.util.List; import static org.xmlunit.assertj.XmlAssert.assertThat; @@ -41,6 +45,7 @@ import static org.xmlunit.assertj.XmlAssert.assertThat; public class XRTest extends TestCase { final String TARGET_XML = "./target/testout-XR.xml"; final String TARGET_EDGE_XML = "./target/testout-XR-Edge.xml"; + public void testXRExport() { // the writing part @@ -54,13 +59,13 @@ public class XRTest extends TestCase { String theXML = new String(zf2p.getXML(), StandardCharsets.UTF_8); assertTrue(theXML.contains(" attachedFiles=zii.getFileAttachments(); + assertNotNull(attachedFiles); + assertEquals(attachedFiles.size(), 1); + + assertTrue(Arrays.equals(attachedFiles.get(0).getData(), b)); + + } public void testXRExportWithoutStreet() { @@ -130,13 +154,13 @@ public class XRTest extends TestCase { String theXML = new String(zf2p.getXML(), StandardCharsets.UTF_8); assertTrue(theXML.contains("