From 1ec64c61103a637494eb602174c255d5187bafe4 Mon Sep 17 00:00:00 2001 From: Ulrich Zielhofer Date: Fri, 17 Dec 2021 08:48:04 +0100 Subject: [PATCH 1/2] solves #227: create both non-shaded and shaded jar for library, while not breaking CLI --- .gitignore | 3 +- Mustang-CLI/pom.xml | 13 ++ .../org/mustangproject/commandline/CliIT.java | 47 +++++ Mustang-CLI/src/test/resources/cii.xml | 191 ++++++++++++++++++ library/pom.xml | 3 +- validator/pom.xml | 6 + 6 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 Mustang-CLI/src/test/java/org/mustangproject/commandline/CliIT.java create mode 100644 Mustang-CLI/src/test/resources/cii.xml diff --git a/.gitignore b/.gitignore index ece11c66..2702c983 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ target/ pom.xml.* bin/ /.gradle/ +log/ # ignore intelliJ foobar *.iml @@ -14,4 +15,4 @@ bin/ # ignore test files ZUGFeRD-*-test -dependency-reduced-pom.xml \ No newline at end of file +dependency-reduced-pom.xml diff --git a/Mustang-CLI/pom.xml b/Mustang-CLI/pom.xml index 41357a94..3cb92a76 100644 --- a/Mustang-CLI/pom.xml +++ b/Mustang-CLI/pom.xml @@ -181,6 +181,19 @@ + + org.apache.maven.plugins + maven-failsafe-plugin + 2.22.2 + + + + integration-test + verify + + + + diff --git a/Mustang-CLI/src/test/java/org/mustangproject/commandline/CliIT.java b/Mustang-CLI/src/test/java/org/mustangproject/commandline/CliIT.java new file mode 100644 index 00000000..43a1a2d1 --- /dev/null +++ b/Mustang-CLI/src/test/java/org/mustangproject/commandline/CliIT.java @@ -0,0 +1,47 @@ +package org.mustangproject.commandline; + +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertTrue; + +public class CliIT { + + @Test + public void testCii2Ubl() throws Exception { + Path output = Paths.get("target/ubl.xml"); + Files.deleteIfExists(output); + Path jar = Files.newDirectoryStream(Paths.get("target"), "Mustang-CLI-*.jar").iterator().next(); + ProcessBuilder pb = new ProcessBuilder("java", "-jar", jar.toString(), + "--action", "ubl", "--source", "src/test/resources/cii.xml", "--out", + output.toString()); + pb.redirectErrorStream(true); + Process process = pb.start(); + String result = getOutput(process); + process.waitFor(10, TimeUnit.SECONDS); + if (!result.isEmpty()) { + System.out.println(result); + } + assertTrue(new String(Files.readAllBytes(output), StandardCharsets.UTF_8).contains("Invoice")); + } + + private String getOutput(Process process) throws IOException { + BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + builder.append(System.getProperty("line.separator")); + } + return builder.toString(); + } + +} diff --git a/Mustang-CLI/src/test/resources/cii.xml b/Mustang-CLI/src/test/resources/cii.xml new file mode 100644 index 00000000..44ce2bd5 --- /dev/null +++ b/Mustang-CLI/src/test/resources/cii.xml @@ -0,0 +1,191 @@ + + + + + + urn:cen.eu:en16931:2017 + + + + RE-20201121/508 + 380 + + 20201121 + + + + + + 1 + + + Design (hours) + Of a sample invoice + + + + 160.0000 + 1.0000 + + + 160.0000 + 1.0000 + + + + 1.0000 + + + + VAT + S + 7.00 + + + 160.00 + + + + + + 2 + + + Ballons + various colors, ~2000ml + + + + 0.7900 + 1.0000 + + + 0.7900 + 1.0000 + + + + 400.0000 + + + + VAT + S + 19.00 + + + 316.00 + + + + + + 3 + + + Hot air „heiße Luft“ (litres) + + + + + 0.0250 + 1.0000 + + + 0.0250 + 1.0000 + + + + 800.0000 + + + + VAT + S + 19.00 + + + 20.00 + + + + + AB321 + + Bei Spiel GmbH + + 12345 + Ecke 12 + Stadthausen + DE + + + DE136695976 + + + + 2 + Theodor Est + + 88802 + Bahnstr. 42 + Spielkreis + DE + + + + + + + 20201110 + + + + + RE-20201121/508 + EUR + + 42 + Bank transfer + + DE88200800000970375700 + Max Mustermann + + + COBADEFFXXX + + + + 11.20 + VAT + 160.00 + S + 7.00 + + + 63.84 + VAT + 336.00 + S + 19.00 + + + Zahlbar ohne Abzug bis 12.12.2020 + + 20201212 + + + + 496.00 + 0.00 + 0.00 + 496.00 + 75.04 + 571.04 + 0.00 + 571.04 + + + + diff --git a/library/pom.xml b/library/pom.xml index 5d7be630..6e5103a5 100644 --- a/library/pom.xml +++ b/library/pom.xml @@ -122,6 +122,7 @@ org.xmlunit xmlunit-assertj 2.6.3 + test @@ -153,7 +154,7 @@ maven-shade-plugin 3.2.1 - false + true false diff --git a/validator/pom.xml b/validator/pom.xml index affdd31c..aa3b9ad0 100644 --- a/validator/pom.xml +++ b/validator/pom.xml @@ -74,6 +74,11 @@ ph-schematron 5.6.0 + + com.helger + ph-jaxb + 9.5.4 + org.mustangproject library @@ -83,6 +88,7 @@ org.xmlunit xmlunit-core 2.6.3 + test org.xmlunit From 8b0d6623ed2322b62209f2dc24f643683003bf69 Mon Sep 17 00:00:00 2001 From: Ivan Vaklinov Date: Sat, 18 Dec 2021 00:09:47 +0200 Subject: [PATCH 2/2] Multiple changes to do with XRechnung and other improvements: 1. Getting a profile by name disregarding teh ZUGFeRD version 2. Allowing the XRechnung version to be explicitly set in the PDF meta-data (must match the version in the attached XML). 3. Change/fix for data relationship in case of MINIMUM and BASICWL profile. See documentation ZUGFeRD211_EN/ Documentation/ZUGFeRD-2.1.1 - Specification_TA_Part-A.pdf 4. Error handling improvements (exceptions not suppressed) --- .../org/mustangproject/ZUGFeRD/Profiles.java | 27 +++++ .../ZUGFeRD/XMPSchemaZugferd.java | 13 +- .../ZUGFeRD/ZUGFeRDExporterFromA3.java | 112 +++++++++++++++--- 3 files changed, 132 insertions(+), 20 deletions(-) diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java b/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java index dc0d6e4f..f91a6979 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java @@ -57,4 +57,31 @@ public class Profiles { return getByName(name, ZUGFeRDExporterFromA3.DefaultZUGFeRDVersion); } + + /** + * Obtains a profile by name, disregarding version. First searches among + * 1.x profiles and fatre that among 2.x profiles. + * + * @param name profile name to search for + * + * @return a profile matching the requested name + */ + public static Profile getByNameDisregardingVersion(String name) + { + Profile result = null; + + result = zf1Map.get(name.toUpperCase()); + + if (result == null) + { + result = zf2Map.get(name.toUpperCase()); + } + + if (result==null) + { + throw new RuntimeException("Profile " + name + " not found"); + } + + return result; + } } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java b/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java index bb3e3a75..c8eadb32 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/XMPSchemaZugferd.java @@ -40,8 +40,10 @@ public class XMPSchemaZugferd extends XMPSchema { * @param URN the xml URI for the XMP * @param prefix the xml namespace prefix for the XMP, zf for ZUGFeRD, fx for Factur-X * @param filename the filename of the invoice + * @param version meta-data version to set. May be null, and then it is set automatically */ - public XMPSchemaZugferd(XMPMetadata metadata, int zfVersion, boolean isFacturX, Profile conformanceLevel, String URN, String prefix, String filename) { + public XMPSchemaZugferd(XMPMetadata metadata, int zfVersion, boolean isFacturX, Profile conformanceLevel, + String URN, String prefix, String filename, String version) { super(metadata, URN, prefix, "ZUGFeRD Schema"); setAboutAsSimple(""); @@ -50,9 +52,12 @@ public class XMPSchemaZugferd extends XMPSchema { setTextPropertyValue("ConformanceLevel", conformanceLevelValue); setTextPropertyValue("DocumentType", "INVOICE"); setTextPropertyValue("DocumentFileName", filename); - String version="1.0"; - if ((zfVersion==2)&&(!isFacturX)) { - version="2p0"; + + if (version == null) { + version = "1.0"; + if ((zfVersion==2)&&(!isFacturX)) { + version="2p0"; + } } setTextPropertyValue("Version", version); } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java index 4113811d..f74f8d00 100644 --- a/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDExporterFromA3.java @@ -108,7 +108,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte @Deprecated protected String subject; - private PDDocument doc; + protected PDDocument doc; private HashMap additionalXMLs = new HashMap(); @@ -116,6 +116,11 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte protected int ZFVersion = DefaultZUGFeRDVersion; private boolean attachZUGFeRDHeaders = true; + // Specific metaData version in case of XRechnung. We need it to be settable + // by the caller if necessary. + protected String XRechnungVersion = null; // Default XRechnung as of late 2021 is 2p0 + + /** * Makes A PDF/A3a-compliant document from a PDF-A1 compliant document (on the * metadata level, this will not e.g. convert graphics to JPG-2000) @@ -162,13 +167,18 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte * @return the URN of the namespace */ public String getNamespaceForVersion(int ver) { + // In the case of XRechnung, it is the same as Factur-X + if ((ver >= 2) && (this.profile != null) && + this.profile.getName().equalsIgnoreCase(Profiles.getByName("XRECHNUNG").getName())) + { + return "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"; + } else if (isFacturX) { return "urn:factur-x:pdfa:CrossIndustryDocument:invoice:1p0#"; } else if (ver == 1) { return "urn:ferd:pdfa:CrossIndustryDocument:invoice:1p0#"; } else if (ver == 2) { return "urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0#"; - } else { throw new IllegalArgumentException("Version not supported"); } @@ -180,6 +190,12 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte * @return the namespace prefix as string, without colon */ public String getPrefixForVersion(int ver) { + // In the case of XRechnung, it is the same as Factur-X + if ((ver >= 2) && (this.profile != null) && + this.profile.getName().equalsIgnoreCase(Profiles.getByName("XRECHNUNG").getName())) + { + return "fx"; + } else if (isFacturX) { return "fx"; } else { @@ -187,6 +203,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte } } + /*** * internal helper: return the name of the file attachment for the given zf/fx version * @param ver the zf/fx version @@ -194,13 +211,17 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte * @return the filename of the file to be embedded */ public String getFilenameForVersion(int ver, Profile profile) { - if (isFacturX) { + boolean isXRechnung = + (ver >= 2) && (this.profile != null) && + this.profile.getName().equalsIgnoreCase(Profiles.getByName("XRECHNUNG").getName()); + + if (isFacturX && (!isXRechnung)) { return "factur-x.xml"; } else { if (ver == 1) { return "ZUGFeRD-invoice.xml"; } else { - if (profile.getName().equals("XRECHNUNG")) { + if (isXRechnung) { return "xrechnung.xml"; } else { return "zugferd-invoice.xml"; @@ -222,6 +243,19 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte return this; } + /** + * Sets a specific XRechnung version from outside. This version needs to be present in the + * meta-data as well. The caller may wish to generate a specific version of XRechnung + * depending on the time period etc. + * + * @param XRechnungVersion the XRechnung version + */ + public void setXRechnungSpecificVersion(String XRechnungVersion) + { + this.XRechnungVersion = XRechnungVersion; + } + + /*** * Generate ZF2.0/2.1 files with filename zugferd-invoice.xml instead of factur-x.xml */ @@ -302,6 +336,24 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte } } + + /** + * Embeds an external file (generic - any type allowed) in the PDF. + * The embedding is done in the default PDF document. + * + * @param filename name of the file that will become attachment name in the PDF + * @param relationship how the file relates to the content, e.g. "Alternative" + * @param description Human-readable description of the file content + * @param subType type of the data e.g. could be "text/xml" - mime like + * @param data the binary data of the file/attachment + * @throws java.io.IOException if anything is wrong with filename + */ + public void PDFAttachGenericFile(String filename, String relationship, String description, + String subType, byte[] data) throws IOException { + PDFAttachGenericFile(this.doc, filename, relationship, description, subType, data); + } + + /** * Embeds an external file (generic - any type allowed) in the PDF. * @@ -468,10 +520,19 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte */ protected void addXMP(XMPMetadata metadata) { + String metaDataVersion = null; // default will be used + + // The XRechnung version may be settable from outside. + if ((this.XRechnungVersion != null) && (this.profile != null) && + this.profile.getName().equalsIgnoreCase(Profiles.getByName("XRECHNUNG").getName())) + { + metaDataVersion = this.XRechnungVersion; + } + if (attachZUGFeRDHeaders) { XMPSchemaZugferd zf = new XMPSchemaZugferd(metadata, ZFVersion, isFacturX, xmlProvider.getProfile(), getNamespaceForVersion(ZFVersion), getPrefixForVersion(ZFVersion), - getFilenameForVersion(ZFVersion, xmlProvider.getProfile())); + getFilenameForVersion(ZFVersion, xmlProvider.getProfile()), metaDataVersion); metadata.addSchema(zf); } @@ -481,8 +542,9 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte metadata.addSchema(pdfaex); } - private void removeCidSet(PDDocumentCatalog catalog, PDDocument doc) { - + private void removeCidSet(PDDocumentCatalog catalog, PDDocument doc) + throws IOException + { // https://github.com/ZUGFeRD/mustangproject/issues/249 COSName cidSet = COSName.getPDFName("CIDSet"); @@ -503,12 +565,12 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte if (typedFont.getDescendantFont() instanceof PDCIDFontType2) { PDCIDFontType2 f = (PDCIDFontType2) typedFont.getDescendantFont(); PDFontDescriptor fontDescriptor = pdFont.getFontDescriptor(); - + fontDescriptor.getCOSObject().removeItem(cidSet); } } } catch (IOException e) { - e.printStackTrace(); + throw e; } // do stuff with the font } @@ -568,7 +630,21 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte prepareDocument(); xmlProvider.generateXML(trans); String filename = getFilenameForVersion(ZFVersion, xmlProvider.getProfile()); - PDFAttachGenericFile(doc, filename, "Alternative", + + String relationship = "Alternative"; + // ZUGFeRD 2.1.1 Technical Supplement | Part A | 2.2.2. Data Relationship + // See documentation ZUGFeRD211_EN/Documentation/ZUGFeRD-2.1.1 - Specification_TA_Part-A.pdf + // https://www.ferd-net.de/standards/zugferd-2.1.1/index.html + if (ZFVersion >= 2) + { + if (this.profile.getName().equalsIgnoreCase(Profiles.getByName("MINIMUM").getName()) || + this.profile.getName().equalsIgnoreCase(Profiles.getByName("BASICWL").getName())) + { + relationship = "Data"; + } + } + + PDFAttachGenericFile(doc, filename, relationship, "Invoice metadata conforming to ZUGFeRD standard (http://www.ferd-net.de/front_content.php?idcat=231&lang=4)", "text/xml", xmlProvider.getXML()); @@ -583,14 +659,16 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte * Reads the XMPMetadata from the PDDocument, if it exists. * Otherwise creates XMPMetadata. */ - protected XMPMetadata getXmpMetadata() { + protected XMPMetadata getXmpMetadata() + throws IOException + { PDMetadata meta = doc.getDocumentCatalog().getMetadata(); - if (meta != null) { + if ((meta != null) && (meta.getLength() > 0)) { try { DomXmpParser xmpParser = new DomXmpParser(); return xmpParser.parse(meta.toByteArray()); - } catch (XmpParsingException | IOException e) { - // TODO use logging or handle the error somehow + } catch (XmpParsingException e) { + throw new IOException(e); } } return XMPMetadata.createXMPMetadata(); @@ -724,7 +802,9 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte /** * Adds an OutputIntent and the sRGB color profile if no OutputIntent exist */ - protected void addSRGBOutputIntend() { + protected void addSRGBOutputIntend() + throws IOException + { if (!doc.getDocumentCatalog().getOutputIntents().isEmpty()) { return; } @@ -740,7 +820,7 @@ public class ZUGFeRDExporterFromA3 extends XRExporter implements IZUGFeRDExporte doc.getDocumentCatalog().addOutputIntent(intent); } } catch (IOException e) { - // TODO use logging or handle the error somehow + throw e; } }