diff --git a/Mustang-CLI/src/main/java/org/mustangproject/commandline/Main.java b/Mustang-CLI/src/main/java/org/mustangproject/commandline/Main.java index f7624a2f..63776d73 100755 --- a/Mustang-CLI/src/main/java/org/mustangproject/commandline/Main.java +++ b/Mustang-CLI/src/main/java/org/mustangproject/commandline/Main.java @@ -560,7 +560,7 @@ public class Main { if (attachmentFilenames == null) { byte attachmentContents[]=null; String attachmentFilename, attachmentMime, attachmentDescription; - attachmentFilename = getFilenameFromUser("Attachment filename (empty for none)", "", "pdf", true, false); + attachmentFilename = getFilenameFromUser("Additional file attachments filename (empty for none)", "", "pdf", true, false); if (attachmentFilename.length()!=0) { attachmentContents=Files.readAllBytes(Paths.get(attachmentFilename)); attachmentMime= Files.probeContentType(Paths.get(attachmentFilename)); @@ -580,7 +580,7 @@ public class Main { if (format == null) { try { - format = getStringFromUser("Format (fx=Factur-X, zf=ZUGFeRD, ox=Order-X)", "zf", "fx|zf|ox"); + format = getStringFromUser("Format (fx=Factur-X, zf=ZUGFeRD, ox=Order-X)", "zf", "fx|zf|ox|dx"); } catch (Exception e) { LOGGER.error(e.getMessage(), e); } @@ -601,7 +601,7 @@ public class Main { if (zfProfile == null) { try { - if (format.equals("zf") && (zfIntVersion == 1) || (format.equals("ox"))) { + if ((format.equals("zf") && (zfIntVersion == 1)) || (format.equals("ox") || (format.equals("dx")))) { zfProfile = getStringFromUser("Profile (b)asic, (c)omfort or ex(t)ended", "t", "B|b|C|c|T|t"); } else { zfProfile = getStringFromUser( @@ -627,11 +627,14 @@ public class Main { throw new Exception("Factur-X is only available in version 1 (roughly corresponding to ZF2)"); } - if (((format.equals("zf")) && (zfIntVersion == 1))||(format.equals("ox"))) { + if (((format.equals("zf")) && (zfIntVersion == 1))||(format.equals("ox")||(format.equals("dx")))) { EStandard standard=EStandard.facturx; if (format.equals("ox")) { standard=EStandard.orderx; } + if (format.equals("dx")) { + standard=EStandard.deliverx; + } if (zfProfile.equals("b")) { zfConformanceLevelProfile = Profiles.getByName(standard, "BASIC", zfIntVersion); @@ -674,6 +677,14 @@ public class Main { ((OXExporterFromA1)ze).ignorePDFAErrors(); } + } else if (format.equals("dx")) { + ze = new DXExporterFromA1().setProducer("Mustang-cli") + .setZUGFeRDVersion(zfIntVersion) + .setCreator(System.getProperty("user.name")).setProfile(zfConformanceLevelProfile); + if (ignoreInputErrors) { + ((DXExporterFromA1)ze).ignorePDFAErrors(); + } + } else { ze = new ZUGFeRDExporterFromA1().setProducer("Mustang-cli") .setZUGFeRDVersion(zfIntVersion) diff --git a/library/src/main/java/org/mustangproject/EStandard.java b/library/src/main/java/org/mustangproject/EStandard.java index 69c43648..0326f7c8 100644 --- a/library/src/main/java/org/mustangproject/EStandard.java +++ b/library/src/main/java/org/mustangproject/EStandard.java @@ -1,6 +1,6 @@ package org.mustangproject; public enum EStandard { - facturx, orderx, zugferd, cii, ubl + facturx, orderx, deliverx, zugferd, cii, ubl } diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/DXExporterFromA1.java b/library/src/main/java/org/mustangproject/ZUGFeRD/DXExporterFromA1.java new file mode 100644 index 00000000..ab8566cd --- /dev/null +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/DXExporterFromA1.java @@ -0,0 +1,140 @@ +/** ********************************************************************** + * + * Copyright 2020 Jochen Staerk + * + * 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.apache.pdfbox.preflight.PreflightDocument; +import org.apache.pdfbox.preflight.exception.ValidationException; +import org.apache.pdfbox.preflight.parser.PreflightParser; + +import javax.activation.DataSource; +import java.io.IOException; +import java.io.InputStream; + +public class DXExporterFromA1 extends DXExporterFromA3 implements IZUGFeRDExporter { + protected boolean ignorePDFAErrors = false; + + public DXExporterFromA1 ignorePDFAErrors() { + this.ignorePDFAErrors = true; + return this; + } + + private static boolean isValidA1(DataSource dataSource) throws IOException { + return getPDFAParserValidationResult(new PreflightParser(dataSource)); + } + /*** + * internal helper function: get namespace for order-x + * @param ver the order-x version + * @return the URN of the namespace + */ + public String getNamespaceForVersion(int ver) { + return "urn:factur-x:pdfa:CrossIndustryDocument:1p0#"; + } + /*** + * internal helper: returns the namespace prefix for the given order-x version number + * @param ver the ox version + * @return the namespace prefix as string, without colon + */ + public String getPrefixForVersion(int ver) { + return "fx"; + } + + private static boolean getPDFAParserValidationResult(PreflightParser parser) throws IOException { + /* + * Parse the PDF file with PreflightParser that inherits from the + * NonSequentialParser. Some additional controls are present to check a set of + * PDF/A requirements. (Stream length consistency, EOL after some Keyword...) + */ + parser.parse();// might add a Format.PDF_A1A as parameter and iterate through A1 and A3 + + try (PreflightDocument document = parser.getPreflightDocument()) { + /* + * Once the syntax validation is done, the parser can provide a + * PreflightDocument (that inherits from PDDocument) This document process the + * end of PDF/A validation. + */ + + document.validate(); + + // Get validation result + return document.getResult().isValid(); + } catch (ValidationException e) { + /* + * the parse method can throw a SyntaxValidationException if the PDF file can't + * be parsed. In this case, the exception contains an instance of + * ValidationResult + */ + return false; + } + } + + + public DXExporterFromA1 setProfile(Profile p) { + return (DXExporterFromA1)super.setProfile(p); + } + public DXExporterFromA1 setProfile(String profileName) { + return (DXExporterFromA1)super.setProfile(profileName); + } + + public boolean ensurePDFIsValid(final DataSource dataSource) throws IOException { + if (!ignorePDFAErrors && !isValidA1(dataSource)) { + throw new IOException("File is not a valid PDF/A input file"); + } + return true; + } + + public DXExporterFromA1() { + setZUGFeRDVersion(ZUGFeRDExporterFromA3.DefaultZUGFeRDVersion); + + } + + public DXExporterFromA1 load(String pdfFilename) throws IOException { + return (DXExporterFromA1) super.load(pdfFilename); + } + public DXExporterFromA1 load(byte[] pdfBinary) throws IOException { + return (DXExporterFromA1) super.load(pdfBinary); + } + public DXExporterFromA1 load(InputStream pdfSource) throws IOException{ + return (DXExporterFromA1) super.load(pdfSource); + } + public DXExporterFromA1 setCreator(String creator) { + return (DXExporterFromA1) super.setCreator(creator); + } + public DXExporterFromA1 setConformanceLevel(PDFAConformanceLevel newLevel) { + return (DXExporterFromA1) super.setConformanceLevel(newLevel); + } + public DXExporterFromA1 setProducer(String producer){ + return (DXExporterFromA1) super.setProducer(producer); + } + public DXExporterFromA1 setZUGFeRDVersion(int version){ + return (DXExporterFromA1) super.setZUGFeRDVersion(version); + } + public DXExporterFromA1 setXML(byte[] zugferdData) throws IOException{ + return (DXExporterFromA1) super.setXML(zugferdData); + } + + public DXExporterFromA1 disableAutoClose(boolean disableAutoClose){ + return (DXExporterFromA1) super.disableAutoClose(disableAutoClose); + } + public DXExporterFromA1 convertOnly() { + setAttachZUGFeRDHeaders(false); + return this; + } + +} diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/DXExporterFromA3.java b/library/src/main/java/org/mustangproject/ZUGFeRD/DXExporterFromA3.java new file mode 100644 index 00000000..eb54d5a1 --- /dev/null +++ b/library/src/main/java/org/mustangproject/ZUGFeRD/DXExporterFromA3.java @@ -0,0 +1,696 @@ +/** + * ********************************************************************* + *
+ * Copyright 2020 Jochen Staerk + *
+ * 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.apache.pdfbox.cos.*;
+import org.apache.pdfbox.io.IOUtils;
+import org.apache.pdfbox.pdmodel.*;
+import org.apache.pdfbox.pdmodel.common.PDMetadata;
+import org.apache.pdfbox.pdmodel.common.filespecification.PDComplexFileSpecification;
+import org.apache.pdfbox.pdmodel.common.filespecification.PDEmbeddedFile;
+import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDMarkInfo;
+import org.apache.pdfbox.pdmodel.documentinterchange.logicalstructure.PDStructureTreeRoot;
+import org.apache.pdfbox.pdmodel.graphics.color.PDOutputIntent;
+import org.apache.pdfbox.preflight.utils.ByteArrayDataSource;
+import org.apache.xmpbox.XMPMetadata;
+import org.apache.xmpbox.schema.AdobePDFSchema;
+import org.apache.xmpbox.schema.DublinCoreSchema;
+import org.apache.xmpbox.schema.PDFAIdentificationSchema;
+import org.apache.xmpbox.schema.XMPBasicSchema;
+import org.apache.xmpbox.type.ArrayProperty;
+import org.apache.xmpbox.type.BadFieldValueException;
+import org.apache.xmpbox.xml.DomXmpParser;
+import org.apache.xmpbox.xml.XmpParsingException;
+import org.apache.xmpbox.xml.XmpSerializer;
+import org.mustangproject.EStandard;
+import org.mustangproject.FileAttachment;
+
+import javax.activation.DataSource;
+import javax.activation.FileDataSource;
+import javax.xml.transform.TransformerException;
+import java.io.*;
+import java.util.*;
+
+public class DXExporterFromA3 extends ZUGFeRDExporterFromA3 {
+
+ protected PDFAConformanceLevel conformanceLevel = PDFAConformanceLevel.UNICODE;
+ protected ArrayList
+ * PDF/A-3 has three coformance levels, called "A", "U" and "B".
+ *
+ * PDF/A-3-B where B means only visually preservable, U -standard for Mustang-
+ * means visually and unicode preservable and A means full compliance, i.e.
+ * visually, unicode and structurally preservable and tagged PDF, i.e. useful
+ * metainformation for blind people.
+ *
+ * Feel free to pass "A" as new level if you know what you are doing :-)
+ */
+ public DXExporterFromA3 setConformanceLevel(PDFAConformanceLevel newLevel) {
+ conformanceLevel = newLevel;
+ return this;
+ }
+
+
+ public DXExporterFromA3 setCreator(String creator) {
+ this.creator = creator;
+ return this;
+ }
+
+ public DXExporterFromA3 setCreatorTool(String creatorTool) {
+ this.creatorTool = creatorTool;
+ return this;
+ }
+
+ public DXExporterFromA3 setProducer(String producer) {
+ this.producer = producer;
+ return this;
+ }
+
+ /**
+ * Sets the property orderXDocumentType.
+ *
+ * @param orderXDocumentType new value. Expected: ORDER, ORDER_RESPONSE, or ORDER_CHANGE
+ *
+ * @return this exporter
+ */
+ public DXExporterFromA3 setOrderXDocumentType(String orderXDocumentType)
+ {
+ this.orderXDocumentType = orderXDocumentType;
+
+ return this;
+ }
+
+ protected DXExporterFromA3 setAttachZUGFeRDHeaders(boolean attachHeaders) {
+ this.attachZUGFeRDHeaders = attachHeaders;
+ return this;
+ }
+
+
+ /**
+ * This will add both the RDF-indication which embedded file is Zugferd and the
+ * neccessary PDF/A schema extension description to be able to add this
+ * information to RDF
+ *
+ * @param metadata the PDFbox XMPMetadata object
+ */
+ protected void addXMP(XMPMetadata metadata) {
+
+ if (attachZUGFeRDHeaders) {
+ XMPSchemaZugferd zf = new XMPSchemaZugferd(metadata, 1, true, xmlProvider.getProfile(),
+ "urn:factur-x:pdfa:CrossIndustryDocument:1p0#", "fx",
+ "deliver-x.xml");
+ zf.setType(this.orderXDocumentType);
+
+ metadata.addSchema(zf);
+ // also add the schema extensions...
+ XMPSchemaPDFAExtensions pdfaex = new XMPSchemaPDFAExtensions(this, metadata, 1, attachZUGFeRDHeaders, EStandard.orderx);
+ pdfaex.setZUGFeRDVersion(1);
+ metadata.addSchema(pdfaex);
+ }
+
+ }
+
+
+ /**
+ * Embeds the Zugferd XML structure in a file named ZUGFeRD-invoice.xml.
+ *
+ * @param trans a IZUGFeRDExportableTransaction that provides the data-model to
+ * populate the XML. This parameter may be null, if so the XML data
+ * should hav ebeen set via
+ * setZUGFeRDXMLData(byte[] zugferdData)
+ * @throws IOException if anything is wrong with already loaded PDF
+ */
+ public IExporter setTransaction(IExportableTransaction trans) throws IOException {
+ this.trans = trans;
+ return prepare();
+ }
+
+ public IExporter prepare() throws IOException {
+ prepareDocument();
+ xmlProvider.generateXML(trans);
+ String filename = "deliver-x.xml";
+ PDFAttachGenericFile(doc, filename, "Alternative",
+ "Order metadata conforming to the Order-X standard (https://www.ferd-net.de/standards/order-x/index.html)",
+ "text/xml", xmlProvider.getXML());
+
+ for (FileAttachment attachment : fileAttachments) {
+ PDFAttachGenericFile(doc, attachment.getFilename(), attachment.getRelation(), attachment.getDescription(), attachment.getMimetype(), attachment.getData());
+ }
+
+ return this;
+ }
+
+ /**
+ * Reads the XMPMetadata from the PDDocument, if it exists.
+ * Otherwise creates XMPMetadata.
+ */
+ protected XMPMetadata getXmpMetadata() throws IOException {
+ PDMetadata meta = doc.getDocumentCatalog().getMetadata();
+ if ((meta != null) && (meta.getLength() > 0)) {
+ try {
+ DomXmpParser xmpParser = new DomXmpParser();
+ return xmpParser.parse(meta.toByteArray());
+ } catch (XmpParsingException | IOException e) {
+ throw new IOException(e);
+ }
+ }
+ return XMPMetadata.createXMPMetadata();
+ }
+
+ protected byte[] serializeXmpMetadata(XMPMetadata xmpMetadata) throws TransformerException {
+ ByteArrayOutputStream buffer = new ByteArrayOutputStream();
+ new XmpSerializer().serialize(xmpMetadata, buffer, true); // see https://github.com/ZUGFeRD/mustangproject/issues/44
+ return buffer.toByteArray();
+ }
+
+ /**
+ * Sets the producer if the overwrite flag is set or the producer is not already set.
+ * Sets the PDFVersion to 1.4 if the field is empty.
+ */
+ protected void writeAdobePDFSchema(XMPMetadata xmp) {
+ AdobePDFSchema pdf = getAdobePDFSchema(xmp);
+ if (overwrite || isEmpty(pdf.getProducer()))
+ pdf.setProducer(producer);
+ }
+
+ /**
+ * Returns the AdobePDFSchema from the XMPMetadata if it exists.
+ * If the overwrite flag is set or no AdobePDFSchema exists in the XMPMetadata, it is created, added and returned.
+ */
+ protected AdobePDFSchema getAdobePDFSchema(XMPMetadata xmp) {
+ AdobePDFSchema pdf = xmp.getAdobePDFSchema();
+ if (pdf != null)
+ if (overwrite)
+ xmp.removeSchema(pdf);
+ else
+ return pdf;
+ return xmp.createAndAddAdobePDFSchema();
+ }
+
+ protected void writePDFAIdentificationSchema(XMPMetadata xmp) {
+ PDFAIdentificationSchema pdfaid = getPDFAIdentificationSchema(xmp);
+ if (overwrite || isEmpty(pdfaid.getConformance())) {
+ try {
+ pdfaid.setConformance(conformanceLevel.getLetter());
+ } catch (BadFieldValueException ex) {
+ // This should be impossible, because it would occur only if an illegal
+ // conformance level is supplied,
+ // however the enum enforces that the conformance level is valid.
+ throw new RuntimeException(ex);
+ }
+ }
+ pdfaid.setPart(3);
+ }
+
+ protected PDFAIdentificationSchema getPDFAIdentificationSchema(XMPMetadata xmp) {
+ PDFAIdentificationSchema pdfaid = xmp.getPDFIdentificationSchema();
+ if (pdfaid != null)
+ if (overwrite)
+ xmp.removeSchema(pdfaid);
+ else
+ return pdfaid;
+ return xmp.createAndAddPFAIdentificationSchema();
+ }
+
+ protected void writeDublinCoreSchema(XMPMetadata xmp) {
+ DublinCoreSchema dc = getDublinCoreSchema(xmp);
+ if (dc.getFormat() == null)
+ dc.setFormat("application/pdf");
+ if ((overwrite || dc.getCreators() == null || dc.getCreators().isEmpty()) && creator != null)
+ dc.addCreator(creator);
+ if ((overwrite || dc.getDates() == null || dc.getDates().isEmpty()) && creator != null)
+ dc.addDate(Calendar.getInstance());
+
+ ArrayProperty titleProperty = dc.getTitleProperty();
+ if (titleProperty != null) {
+ if (overwrite && !isEmpty(title)) {
+ dc.removeProperty(titleProperty);
+ dc.setTitle(title);
+ } else if (titleProperty.getElementsAsString().stream().anyMatch("Untitled"::equalsIgnoreCase)) {
+ // remove unfitting ghostscript default
+ dc.removeProperty(titleProperty);
+ }
+ } else if (!isEmpty(title)) {
+ dc.setTitle(title);
+ }
+ }
+
+ protected DublinCoreSchema getDublinCoreSchema(XMPMetadata xmp) {
+ DublinCoreSchema dc = xmp.getDublinCoreSchema();
+ if (dc != null)
+ if (overwrite)
+ xmp.removeSchema(dc);
+ else
+ return dc;
+ return xmp.createAndAddDublinCoreSchema();
+ }
+
+ protected void writeXMLBasicSchema(XMPMetadata xmp) {
+ XMPBasicSchema xsb = getXmpBasicSchema(xmp);
+ if (overwrite || isEmpty(xsb.getCreatorTool()) || "UnknownApplication".equals(xsb.getCreatorTool()))
+ xsb.setCreatorTool(creatorTool);
+ if (overwrite || xsb.getCreateDate() == null)
+ xsb.setCreateDate(GregorianCalendar.getInstance());
+ }
+
+ protected XMPBasicSchema getXmpBasicSchema(XMPMetadata xmp) {
+ XMPBasicSchema xsb = xmp.getXMPBasicSchema();
+ if (xsb != null)
+ if (overwrite)
+ xmp.removeSchema(xsb);
+ else
+ return xsb;
+ return xmp.createAndAddXMPBasicSchema();
+ }
+
+ protected void writeDocumentInformation() {
+ String fullProducer = producer + " (via mustangproject.org " + Version.VERSION + ")";
+ PDDocumentInformation info = doc.getDocumentInformation();
+ if (overwrite || info.getCreationDate() == null)
+ info.setCreationDate(Calendar.getInstance());
+ if (overwrite || info.getModificationDate() == null)
+ info.setModificationDate(Calendar.getInstance());
+ if (overwrite || (isEmpty(info.getAuthor()) && !isEmpty(author)))
+ info.setAuthor(author);
+ if (overwrite || (isEmpty(info.getProducer()) && !isEmpty(fullProducer)))
+ info.setProducer(fullProducer);
+ if (overwrite || (isEmpty(info.getCreator()) && !isEmpty(creator)))
+ info.setCreator(creator);
+ if (overwrite || (isEmpty(info.getTitle()) && !isEmpty(title)))
+ info.setTitle(title);
+ if (overwrite || (isEmpty(info.getSubject()) && !isEmpty(subject)))
+ info.setSubject(subject);
+ }
+
+ /**
+ * Adds an OutputIntent and the sRGB color profile if no OutputIntent exist
+ */
+ protected void addSRGBOutputIntend() throws IOException {
+ if (!doc.getDocumentCatalog().getOutputIntents().isEmpty()) {
+ return;
+ }
+
+ try {
+ InputStream colorProfile = Thread.currentThread().getContextClassLoader().getResourceAsStream("sRGB.icc");
+ if (colorProfile != null) {
+ PDOutputIntent intent = new PDOutputIntent(doc, colorProfile);
+ intent.setInfo("sRGB IEC61966-2.1");
+ intent.setOutputCondition("sRGB IEC61966-2.1");
+ intent.setOutputConditionIdentifier("sRGB IEC61966-2.1");
+ intent.setRegistryName("http://www.color.org");
+ doc.getDocumentCatalog().addOutputIntent(intent);
+ }
+ } catch (IOException e) {
+ throw e;
+ }
+ }
+
+ /**
+ * Adds a MarkInfo element to the PDF if it doesn't already exist and sets it as marked.
+ */
+ protected void setMarked() {
+ PDDocumentCatalog catalog = doc.getDocumentCatalog();
+ if (catalog.getMarkInfo() == null) {
+ catalog.setMarkInfo(new PDMarkInfo(doc.getPages().getCOSObject()));
+ }
+ catalog.getMarkInfo().setMarked(true);
+ }
+
+ /**
+ * Adds a StructureTreeRoot element to the PDF if it doesn't already exist.
+ */
+ protected void addStructureTreeRoot() {
+ if (doc.getDocumentCatalog().getStructureTreeRoot() == null) {
+ doc.getDocumentCatalog().setStructureTreeRoot(new PDStructureTreeRoot());
+ }
+ }
+
+
+ /**
+ * @return if pdf file will be automatically closed after adding ZF
+ */
+ public boolean isAutoCloseDisabled() {
+ return disableAutoClose;
+ }
+
+ /**
+ * @param disableAutoClose prevent PDF file from being closed after adding ZF
+ */
+ public DXExporterFromA3 disableAutoClose(boolean disableAutoClose) {
+ this.disableAutoClose = disableAutoClose;
+ return this;
+ }
+
+ protected void setXMLProvider(IXMLProvider p) {
+ this.xmlProvider = p;
+ if (profile != null) {
+ xmlProvider.setProfile(profile);
+ }
+ }
+
+ @Override
+ public DXExporterFromA3 setZUGFeRDVersion(int version) {
+ OXPullProvider z2p = new OXPullProvider();
+ setXMLProvider(z2p);
+ return this;
+ }
+
+ /**
+ * Utility method inspired by apache commons-lang3 StringUtils.
+ *
+ * @param string the string to test
+ * @return true if the string is null or empty
+ */
+ private boolean isEmpty(String string) {
+ return string == null || string.isEmpty();
+ }
+}
diff --git a/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java b/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java
index 46a185d8..b2be449d 100644
--- a/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java
+++ b/library/src/main/java/org/mustangproject/ZUGFeRD/Profiles.java
@@ -49,19 +49,34 @@ public class Profiles {
{"EXTENDED", new Profile("EXTENDED", "urn:order-x.eu:1p0:extended")},
+ }).collect(Collectors.toMap(data -> (String) data[0], data -> (Profile) data[1]));
+ static Map