diff --git a/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2Exporter.java b/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2Exporter.java
new file mode 100644
index 00000000..72c348f2
--- /dev/null
+++ b/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRD2Exporter.java
@@ -0,0 +1,698 @@
+package org.mustangproject.ZUGFeRD;
+/**
+ * Mustangproject's ZUGFeRD implementation
+ * ZUGFeRD exporter
+ * Licensed under the APLv2
+ * @date 2014-07-12
+ * @version 1.1
+ * @author jstaerk
+ * */
+
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.text.DecimalFormat;
+import java.text.DecimalFormatSymbols;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.GregorianCalendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.xml.transform.TransformerException;
+
+import org.apache.pdfbox.cos.COSArray;
+import org.apache.pdfbox.cos.COSBase;
+import org.apache.pdfbox.cos.COSDictionary;
+import org.apache.pdfbox.cos.COSName;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.pdmodel.PDDocumentCatalog;
+import org.apache.pdfbox.pdmodel.PDDocumentInformation;
+import org.apache.pdfbox.pdmodel.PDDocumentNameDictionary;
+import org.apache.pdfbox.pdmodel.PDEmbeddedFilesNameTreeNode;
+import org.apache.pdfbox.pdmodel.common.COSObjectable;
+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.xmpbox.XMPMetadata;
+
+
+
+public class ZUGFeRD2Exporter {
+ /***
+ * You will need Apache PDFBox. To use the ZUGFeRD exporter,
+ implement IZUGFeRDExportableTransaction in yourTransaction
+ (which will require you to implement Product, Item and Contact)
+ then call
+ doc = PDDocument.load(PDFfilename);
+ // automatically add Zugferd to all outgoing invoices
+ ZUGFeRDExporter ze = new ZUGFeRDExporter();
+ ze.PDFmakeA3compliant(doc, "Your application name",
+ System.getProperty("user.name"), true);
+ ze.PDFattachZugferdFile(doc, yourTransaction);
+
+ doc.save(PDFfilename);
+
+ * @author jstaerk
+ *
+ */
+
+
+
+ private class LineCalc {
+ private IZUGFeRDExportableItem currentItem=null;
+ private BigDecimal totalGross;
+ private BigDecimal itemTotalNetAmount;
+ private BigDecimal itemTotalVATAmount;
+
+ public LineCalc(IZUGFeRDExportableItem currentItem) {
+ this.currentItem=currentItem;
+ BigDecimal multiplicator=currentItem.getProduct().getVATPercent().divide(new BigDecimal(100)).add(new BigDecimal(1));
+// priceGross=currentItem.getPrice().multiply(multiplicator);
+ totalGross=currentItem.getPrice().multiply(multiplicator).multiply(currentItem.getQuantity());
+ itemTotalNetAmount=currentItem.getQuantity().multiply(currentItem.getPrice()).setScale(2,BigDecimal.ROUND_HALF_UP);
+ itemTotalVATAmount=totalGross.subtract(itemTotalNetAmount);
+ }
+
+ public BigDecimal getItemTotalNetAmount() {
+ return itemTotalNetAmount;
+ }
+
+ public BigDecimal getItemTotalVATAmount() {
+ return itemTotalVATAmount;
+ }
+
+ }
+
+
+
+ //// MAIN CLASS
+
+ private String conformanceLevel = "U";
+ private String versionStr = "1.2.0";
+
+ // BASIC, COMFORT etc - may be set from outside.
+ private String ZUGFeRDConformanceLevel = null;
+
+
+ /**
+ * Data (XML invoice) to be added to the ZUGFeRD PDF. It may be externally set, in which case passing a
+ * IZUGFeRDExportableTransaction is not necessary. By default it is null meaning the caller needs to
+ * pass a IZUGFeRDExportableTransaction for the XML to be populated.
+ */
+ byte[] zugferdData = null;
+ private boolean isTest;
+ IZUGFeRDExportableTransaction trans=null;
+
+
+ private String nDigitFormat(BigDecimal value, int scale) {
+ /*
+ * I needed 123,45, locale independent.I tried
+ * NumberFormat.getCurrencyInstance().format( 12345.6789 ); but that is
+ * locale specific.I also tried DecimalFormat df = new DecimalFormat(
+ * "0,00" ); df.setDecimalSeparatorAlwaysShown(true);
+ * df.setGroupingUsed(false); DecimalFormatSymbols symbols = new
+ * DecimalFormatSymbols(); symbols.setDecimalSeparator(',');
+ * symbols.setGroupingSeparator(' ');
+ * df.setDecimalFormatSymbols(symbols);
+ *
+ * but that would not switch off grouping. Although I liked very much
+ * the (incomplete) "BNF diagram" in
+ * http://docs.oracle.com/javase/tutorial/i18n/format/decimalFormat.html
+ * in the end I decided to calculate myself and take eur+sparator+cents
+ *
+ * This function will cut off, i.e. floor() subcent values Tests:
+ * System.err.println(utils.currencyFormat(new BigDecimal(0),
+ * ".")+"\n"+utils.currencyFormat(new BigDecimal("-1.10"),
+ * ",")+"\n"+utils.currencyFormat(new BigDecimal("-1.1"),
+ * ",")+"\n"+utils.currencyFormat(new BigDecimal("-1.01"),
+ * ",")+"\n"+utils.currencyFormat(new BigDecimal("20000123.3489"),
+ * ",")+"\n"+utils.currencyFormat(new BigDecimal("20000123.3419"),
+ * ",")+"\n"+utils.currencyFormat(new BigDecimal("12"), ","));
+ *
+ * results 0.00 -1,10 -1,10 -1,01 20000123,34 20000123,34 12,00
+ */
+ value=value.setScale( scale, BigDecimal.ROUND_HALF_UP ); // first, round so that e.g. 1.189999999999999946709294817992486059665679931640625 becomes 1.19
+ char[] repeat = new char[scale];
+ Arrays.fill(repeat, '0');
+
+ DecimalFormatSymbols otherSymbols = new DecimalFormatSymbols();
+ otherSymbols.setDecimalSeparator('.');
+ DecimalFormat dec = new DecimalFormat("0."+new String(repeat), otherSymbols);
+ return dec.format(value);
+
+ }
+
+ private String vatFormat(BigDecimal value) {
+ return nDigitFormat(value, 2);
+ }
+
+ private String currencyFormat(BigDecimal value) {
+ return nDigitFormat(value, 2);
+ }
+
+ private String priceFormat(BigDecimal value) {
+ return nDigitFormat(value, 4);
+ }
+ private String quantityFormat(BigDecimal value) {
+ return nDigitFormat(value, 4);
+ }
+
+ /**
+ All files are PDF/A-3, setConformance refers to the level conformance.
+
+ 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 void setConformanceLevel(String newLevel) {
+ conformanceLevel=newLevel;
+ }
+
+ /**
+ * enables the flag to indicate a test invoice in the XML structure
+ * */
+ public void setTest() {
+ isTest=true;
+ }
+
+ /**
+ * 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)
+ * */
+ public PDDocumentCatalog PDFmakeA3compliant(PDDocument doc, String producer, String creator,
+ boolean attachZugferdHeaders) throws IOException,
+ TransformerException {
+ String fullProducer=producer + " (via mustangproject.org " + versionStr + ")";
+ PDDocumentCatalog cat = doc.getDocumentCatalog();
+ PDMetadata metadata = new PDMetadata(doc);
+ cat.setMetadata(metadata);
+ // we're using the jempbox org.apache.jempbox.xmp.XMPMetadata version,
+ // not the xmpbox one
+ XMPMetadata xmp = new XMPMetadata();
+
+ XMPSchemaPDFAId pdfaid = new XMPSchemaPDFAId(xmp);
+ pdfaid.setAbout(""); //$NON-NLS-1$
+ xmp.addSchema(pdfaid);
+
+ XMPSchemaDublinCore dc = xmp.addDublinCoreSchema();
+ dc.addCreator(creator);
+ dc.setAbout(""); //$NON-NLS-1$
+
+ XMPSchemaBasic xsb = xmp.addBasicSchema();
+ xsb.setAbout(""); //$NON-NLS-1$
+
+ xsb.setCreatorTool(creator);
+ xsb.setCreateDate(GregorianCalendar.getInstance());
+ // PDDocumentInformation pdi=doc.getDocumentInformation();
+ PDDocumentInformation pdi = new PDDocumentInformation();
+ pdi.setProducer(fullProducer);
+ pdi.setAuthor(creator);
+ doc.setDocumentInformation(pdi);
+
+ XMPSchemaPDF pdf = xmp.addPDFSchema();
+ pdf.setProducer(fullProducer);
+ pdf.setAbout(""); //$NON-NLS-1$
+
+ /*
+ // Mandatory: PDF/A3-a is tagged PDF which has to be expressed using a
+ // MarkInfo dictionary (PDF A/3 Standard sec. 6.7.2.2)
+ PDMarkInfo markinfo = new PDMarkInfo();
+ markinfo.setMarked(true);
+ doc.getDocumentCatalog().setMarkInfo(markinfo);
+*/
+/*
+ *
+ To be on the safe side, we use level B without Markinfo because we can not
+ guarantee that the user correctly tagged the templates for the PDF.
+
+ * */
+ pdfaid.setConformance(conformanceLevel);//$NON-NLS-1$ //$NON-NLS-1$
+
+ pdfaid.setPart(3);
+
+ if (attachZugferdHeaders) {
+ addZugferdXMP(xmp); /*
+ * this is the only line where we do something
+ * Zugferd-specific, i.e. add PDF metadata
+ * specifically for Zugferd, not generically for
+ * a embedded file
+ */
+ }
+
+ metadata.importXMPMetadata(xmp);
+ return cat;
+ }
+
+ private String getZugferdXMLForTransaction(IZUGFeRDExportableTransaction trans) {
+ this.trans=trans;
+
+ SimpleDateFormat germanDateFormat = new SimpleDateFormat("dd.MM.yyyy"); //$NON-NLS-1$
+ SimpleDateFormat zugferdDateFormat = new SimpleDateFormat("yyyyMMdd"); //$NON-NLS-1$
+ String testBooleanStr="false";
+ if (isTest) {
+ testBooleanStr="true";
+
+ }
+ String senderReg="";
+ if (trans.getOwnOrganisationFullPlaintextInfo()!=null) {
+ senderReg=""
+ + "\n"
+ + " \n"
+ + trans.getOwnOrganisationFullPlaintextInfo()
+ + " \n"
+ + "REG\n"
+ + "\n";
+
+ }
+ String xml= "\n" //$NON-NLS-1$
+
+ + "\n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " urn:ferd:CrossIndustryDocument:invoice:1p0:comfort\n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " "+trans.getNumber()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " RECHNUNG\n" //$NON-NLS-1$
+ + " 380\n" //$NON-NLS-1$
+ + " "+zugferdDateFormat.format(trans.getIssueDate())+"\n" //date format was 20130605 //$NON-NLS-1$ //$NON-NLS-2$
+ + senderReg
+// + " \n"
+// + " \n"
+// + "Rechnung gemäß Bestellung Nr. 2013-471331 vom 01.03.2013.\n"
+// + "\n"
+// + " \n"
+// + " \n"
+// + " \n"
+// + " \n"
+// + "Es bestehen Rabatt- und Bonusvereinbarungen.\n"
+// + " \n"
+// + " AAK\n"
+// + " \n"
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+// + " AB-312\n"
+ + " \n" //$NON-NLS-1$
+// + " 4000001123452\n"
+ + " "+trans.getOwnOrganisationName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n"
+ + " "+trans.getOwnZIP()+"\n"
+ + " "+trans.getOwnStreet()+"\n"
+ + " "+trans.getOwnLocation()+"\n"
+ + " "+trans.getOwnCountry()+"\n"
+ + " \n"
+ + " \n" //$NON-NLS-1$
+ + " "+trans.getOwnTaxID()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " "+trans.getOwnVATID()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+// + " GE2020211\n"
+// + " 4000001987658\n"
+ + " "+trans.getRecipient().getName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+// + " \n"
+// + " xxx\n"
+// + " \n"
+ + " \n" //$NON-NLS-1$
+ + " "+trans.getRecipient().getZIP()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " "+trans.getRecipient().getStreet()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " "+trans.getRecipient().getLocation()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " "+trans.getRecipient().getCountry()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " "+trans.getRecipient().getVATID()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+// + " \n"
+// + " 20130301\n"
+// + " 2013-471331\n"
+// + " \n"
+ + " \n" //$NON-NLS-1$
+ + " \n"
+ + " \n"
+ + " "+zugferdDateFormat.format(trans.getDeliveryDate())+"\n"
+ + " \n"
+ /*
+ + " \n"
+ + " 20130603\n"
+ + " 2013-51112\n"
+ + " \n" */
+ + " \n"
+ + " \n" //$NON-NLS-1$
+ + " "+trans.getNumber()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " EUR\n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " 42\n" //$NON-NLS-1$
+ + " Überweisung\n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " "+trans.getOwnIBAN()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " "+trans.getOwnBIC()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " "+trans.getOwnBankName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n"; //$NON-NLS-1$
+
+
+
+ HashMap VATPercentAmountMap=getVATPercentAmountMap();
+ for (BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) {
+ VATAmount amount = VATPercentAmountMap.get(currentTaxPercent);
+ if (amount != null) {
+ xml += " \n" //$NON-NLS-1$
+ + " "+currencyFormat(amount.getCalculated())+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " VAT\n" //$NON-NLS-1$
+ + " "+currencyFormat(amount.getBasis())+"\n"
+ + " S\n" //$NON-NLS-1$
+ + " "+vatFormat(currentTaxPercent)+"\n" //$NON-NLS-1$
+ + " \n"; //$NON-NLS-1$
+
+
+
+ }
+ }
+/* xml+= "
+ + " \n"
+ + " false\n"
+ + " 10\n"
+ + " 1.00\n"
+ + " Sondernachlass\n"
+ + " \n"
+ + " VAT\n"
+ + " S\n"
+ + " 19\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " false\n"
+ + " 137.30\n"
+ + " 13.73\n"
+ + " Sondernachlass\n"
+ + " \n"
+ + " VAT\n"
+ + " S\n"
+ + " 7\n"
+ + " \n"
+ + " \n"
+ + " \n"
+ + " Versandkosten\n"
+ + " 5.80\n"
+ + " \n"
+ + " VAT\n"
+ + " S\n"
+ + " 7\n"
+ + " \n"
+ + " \n"*/
+
+ xml=xml+ " \n" //$NON-NLS-1$
+ + " Zahlbar ohne Abzug bis "+germanDateFormat.format(trans.getDueDate())+"\n"
+ + " "+zugferdDateFormat.format(trans.getDueDate())+"\n"//20130704 //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " "+currencyFormat(getTotal())+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " 0.00\n" //$NON-NLS-1$
+ + " 0.00\n" //$NON-NLS-1$
+// + " 5.80\n"
+// + " 14.73\n"
+ + " "+currencyFormat(getTotal())+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " "+currencyFormat(getTotalGross().subtract(getTotal()))+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " "+currencyFormat(getTotalGross())+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+// + " 0.00\n"
+ + " "+currencyFormat(getTotalGross())+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n"; //$NON-NLS-1$
+// + " \n"
+// + " \n"
+// + " \n"
+// + " Wir erlauben uns Ihnen folgende Positionen aus der Lieferung Nr. 2013-51112 in Rechnung zu stellen:\n"
+// + " \n"
+// + " \n"
+// + " \n";
+
+
+ int lineID=0;
+ for (IZUGFeRDExportableItem currentItem : trans.getZFItems()) {
+ lineID++;
+
+ LineCalc lc=new LineCalc(currentItem);
+ xml=xml+ " \n"+ //$NON-NLS-1$
+ " \n" //$NON-NLS-1$
+ + " "+lineID+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " "+priceFormat(currentItem.getPrice())+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " 1.0000\n" //$NON-NLS-1$ //$NON-NLS-2$
+// + " \n"
+// + " false\n"
+// + " 0.6667\n"
+// + " Rabatt\n"
+// + " \n"
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " "+priceFormat(currentItem.getPrice())+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " 1.0000\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+
+ + " \n" //$NON-NLS-1$
+ + " "+quantityFormat(currentItem.getQuantity())+"\n" //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " VAT\n" //$NON-NLS-1$
+ + " S\n" //$NON-NLS-1$
+ + " "+vatFormat(currentItem.getProduct().getVATPercent())+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " "+currencyFormat(lc.getItemTotalNetAmount())+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+ + " \n" //$NON-NLS-1$
+// + " 4012345001235\n"
+// + " KR3M\n"
+// + " 55T01\n"
+ + " "+currentItem.getProduct().getName()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " "+currentItem.getProduct().getDescription()+"\n" //$NON-NLS-1$ //$NON-NLS-2$
+ + " \n" //$NON-NLS-1$
+ + " \n"; //$NON-NLS-1$
+
+
+
+ }
+
+
+ xml=xml + " \n" //$NON-NLS-1$
+ + ""; //$NON-NLS-1$
+ return xml;
+ }
+
+
+ private BigDecimal getTotalGross() {
+
+ BigDecimal res=getTotal();
+ HashMap VATPercentAmountMap=getVATPercentAmountMap();
+ for (BigDecimal currentTaxPercent : VATPercentAmountMap.keySet()) {
+ VATAmount amount = VATPercentAmountMap.get(currentTaxPercent);
+ res=res.add(amount.getCalculated());
+ }
+
+
+ return res;
+ }
+
+ private BigDecimal getTotal() {
+ BigDecimal res=new BigDecimal(0);
+ for (IZUGFeRDExportableItem currentItem : trans.getZFItems()) {
+ LineCalc lc=new LineCalc(currentItem);
+ res=res.add(lc.getItemTotalNetAmount());
+ }
+ return res;
+ }
+
+ /**
+ * which taxes have been used with which amounts in this transaction,
+ * empty for no taxes, or e.g. 19=>190 and 7=>14 if 1000 Eur were applicable
+ * to 19% VAT (=>190 EUR VAT) and 200 EUR were applicable to 7% (=>14 EUR VAT)
+ * 190 Eur
+ * @return
+ *
+ */
+ private HashMap getVATPercentAmountMap() {
+ HashMap hm=new HashMap ();
+
+ for (IZUGFeRDExportableItem currentItem : trans.getZFItems()) {
+ BigDecimal percent=currentItem.getProduct().getVATPercent();
+ LineCalc lc=new LineCalc(currentItem);
+ VATAmount itemVATAmount=new VATAmount(lc.getItemTotalNetAmount(), lc.getItemTotalVATAmount() ) ;
+ VATAmount current=hm.get(percent);
+ if (current==null) {
+ hm.put(percent, itemVATAmount);
+ } else {
+ hm.put(percent, current.add(itemVATAmount));
+
+ }
+ }
+
+ return hm;
+ }
+
+ /**
+ * Embeds the Zugferd XML structure in a file named ZUGFeRD-invoice.xml.
+ *
+ * @param doc PDDocument to attach an XML invoice to
+ * @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)
+ */
+ public void PDFattachZugferdFile(PDDocument doc, IZUGFeRDExportableTransaction trans) throws IOException {
+
+ if (zugferdData == null) // XML ZUGFeRD data not set externally, needs to be built
+ {
+ // create a dummy file stream, this would probably normally be a
+ // FileInputStream
+
+ byte[] zugferdRaw = getZugferdXMLForTransaction(trans).getBytes("UTF-8"); //$NON-NLS-1$
+
+ if ((zugferdRaw[0]==(byte)0xEF)&&(zugferdRaw[1]==(byte)0xBB)&&(zugferdRaw[2]==(byte)0xBF)) {
+ // I don't like BOMs, lets remove it
+ zugferdData=new byte[zugferdRaw.length-3];
+ System.arraycopy(zugferdRaw,3,zugferdData,0,zugferdRaw.length-3);
+ } else {
+ zugferdData=zugferdRaw;
+ }
+ }
+
+
+ PDFAttachGenericFile(doc, "ZUGFeRD-invoice.xml", "Alternative", "Invoice metadata conforming to ZUGFeRD standard (http://www.ferd-net.de/front_content.php?idcat=231&lang=4)", "text/xml", zugferdData);
+ }
+
+
+ /**
+ * Embeds an external file (generic - any type allowed) in the PDF.
+ *
+ * @param doc PDDocument to attach the file to.
+ * @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
+ */
+ public void PDFAttachGenericFile(PDDocument doc, String filename, String relationship, String description, String subType, byte[] data)
+ throws IOException
+ {
+ PDComplexFileSpecification fs = new PDComplexFileSpecification();
+ fs.setFile(filename);
+
+ COSDictionary dict = fs.getCOSDictionary();
+ dict.setName("AFRelationship", relationship);
+ dict.setString("UF", filename);
+ dict.setString("Desc", description);
+
+ ByteArrayInputStream fakeFile = new ByteArrayInputStream(data);
+ PDEmbeddedFile ef = new PDEmbeddedFile(doc, fakeFile);
+ ef.setSubtype(subType);
+ ef.setSize(data.length);
+ ef.setCreationDate(new GregorianCalendar());
+
+ ef.setModDate(GregorianCalendar.getInstance());
+
+ fs.setEmbeddedFile(ef);
+
+ // In addition make sure the embedded file is set under /UF
+ dict = fs.getCOSDictionary();
+ COSDictionary efDict = (COSDictionary)dict.getDictionaryObject(COSName.EF);
+ COSBase lowerLevelFile = efDict.getItem(COSName.F);
+ efDict.setItem(COSName.UF, lowerLevelFile);
+
+ // now add the entry to the embedded file tree and set in the document.
+ PDDocumentNameDictionary names = new PDDocumentNameDictionary(doc.getDocumentCatalog());
+ PDEmbeddedFilesNameTreeNode efTree = names.getEmbeddedFiles();
+ if (efTree == null)
+ {
+ efTree = new PDEmbeddedFilesNameTreeNode();
+ }
+
+ Map namesMap = new HashMap();
+ Map oldNamesMap = efTree.getNames();
+ if (oldNamesMap != null)
+ {
+ for (String key : oldNamesMap.keySet())
+ {
+ namesMap.put(key, oldNamesMap.get(key));
+ }
+ }
+ namesMap.put(filename, fs);
+ efTree.setNames(namesMap);
+
+ names.setEmbeddedFiles(efTree);
+ doc.getDocumentCatalog().setNames(names);
+
+ // AF entry (Array) in catalog with the FileSpec
+ COSArray cosArray = (COSArray)doc.getDocumentCatalog().getCOSDictionary().getItem("AF");
+ if (cosArray == null)
+ {
+ cosArray = new COSArray();
+ }
+ cosArray.add(fs);
+ doc.getDocumentCatalog().getCOSDictionary().setItem("AF", cosArray);
+ }
+
+
+ /**
+ * Sets the ZUGFeRD XML data to be attached as a single byte array. This is useful for
+ * use-cases where the XML has already been produced by some external API or component.
+ *
+ * @param zugferdData XML data to be set as a byte array (XML file in raw form).
+ */
+ public void setZUGFeRDXMLData(byte[] zugferdData)
+ {
+ this.zugferdData = zugferdData;
+ }
+
+
+ /**
+ * Sets the ZUGFeRD conformance level (override).
+ *
+ * @param ZUGFeRDConformanceLevel the new conformance level
+ */
+ public void setZUGFeRDConformanceLevel(String ZUGFeRDConformanceLevel)
+ {
+ this.ZUGFeRDConformanceLevel = ZUGFeRDConformanceLevel;
+ }
+
+/***
+ * 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
+ */
+ private void addZugferdXMP(XMPMetadata metadata) {
+
+ XMPSchemaZugferd zf = new XMPSchemaZugferd(metadata, this.ZUGFeRDConformanceLevel);
+ zf.setAbout(""); //$NON-NLS-1$
+ metadata.addSchema(zf);
+
+ XMPSchemaPDFAExtensions pdfaex = new XMPSchemaPDFAExtensions(metadata);
+ pdfaex.setAbout(""); //$NON-NLS-1$
+ metadata.addSchema(pdfaex);
+
+ }
+
+}
diff --git a/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDMigrator.java b/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDMigrator.java
new file mode 100644
index 00000000..480867ee
--- /dev/null
+++ b/src/main/java/org/mustangproject/ZUGFeRD/ZUGFeRDMigrator.java
@@ -0,0 +1,108 @@
+package org.mustangproject.ZUGFeRD;
+
+public class ZUGFeRDMigrator {
+
+ public String migrateFromV1ToV2(String xml) {
+
+ // todo: attributes may also be in single quotes, this one hardcodedly expects
+ // double ones
+ xml = xml.replace("\"urn:ferd:CrossIndustryDocument:invoice:1p0",
+ "\"urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:13");
+ xml = xml.replace("urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:12",
+ "urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:20");
+ xml = xml.replace("urn:un:unece:uncefact:data:standard:UnqualifiedDataType:15",
+ "urn:un:unece:uncefact:data:standard:UnqualifiedDataType:20");
+ xml = xml.replace("rsm:CrossIndustryDocument", "rsm:CrossIndustryInvoice");
+ xml = xml.replace("rsm:SpecifiedExchangedDocumentContext", "rsm:CIExchangedDocumentContext");
+ xml = xml.replace("rsm:HeaderExchangedDocument", "rsm:CIIHExchangedDocument");
+ xml = xml.replace("SpecifiedSupplyChainTradeTransaction", "CIIHSupplyChainTradeTransaction");
+ xml = xml.replace("ram:GuidelineSpecifiedDocumentContextParameter",
+ "ram:GuidelineSpecifiedCIDocumentContextParameter");
+ xml = xml.replace("ram:IncludedNote", "ram:IncludedCINote");
+ xml = xml.replace("ram:ApplicableSupplyChainTradeAgreement", "ram:ApplicableCIIHSupplyChainTradeAgreement");
+ xml = xml.replace("ram:SellerTradeParty", "ram:SellerCITradeParty");
+ xml = xml.replace("ram:BuyerTradeParty", "ram:BuyerCITradeParty");
+ xml = xml.replace("ram:ApplicableSupplyChainTradeDelivery", "ram:ApplicableCIIHSupplyChainTradeDelivery");
+ xml = xml.replace("ram:ApplicableSupplyChainTradeSettlement", "ram:ApplicableCIIHSupplyChainTradeSettlement");
+ xml = xml.replace("ram:IncludedSupplyChainTradeLineItem", "ram:IncludedCIILSupplyChainTradeLineItem");
+ xml = xml.replace("ram:AssociatedDocumentLineDocument", "ram:AssociatedCIILDocumentLineDocument");
+ xml = xml.replace("ram:SpecifiedSupplyChainTradeDelivery", "ram:SpecifiedCIILSupplyChainTradeDelivery");
+ xml = xml.replace("ram:SpecifiedSupplyChainTradeSettlement", "ram:SpecifiedCIILSupplyChainTradeSettlement");
+ xml = xml.replace("ram:SpecifiedTradeProduct", "ram:SpecifiedCITradeProduct");
+ xml = xml.replace("ram:ActualDeliverySupplyChainEvent", "ram:ActualDeliveryCISupplyChainEvent");
+ xml = xml.replace("ram:SpecifiedTradeSettlementPaymentMeans", "ram:SpecifiedCITradeSettlementPaymentMeans");
+ xml = xml.replace("ram:PayeePartyCreditorFinancialAccount", "ram:PayeePartyCICreditorFinancialAccount");
+ xml = xml.replace("ram:PayeeSpecifiedCreditorFinancialInstitution",
+ "ram:PayeeSpecifiedCICreditorFinancialInstitution");
+ xml = xml.replace("ram:ApplicableTradeTax", "ram:ApplicableCITradeTax");
+ xml = xml.replace("ram:ApplicablePercent", "ram:RateApplicablePercent");
+ xml = xml.replace("ram:PostalTradeAddress", "ram:PostalCITradeAddress");
+
+ xml = xml.replace("ram:ApplicableTradePaymentDiscountTerms", "ram:ApplicableCITradePaymentDiscountTerms");
+ xml = xml.replace("ram:ApplicableProductCharacteristic", "ram:ApplicableCIProductCharacteristic");
+ xml = xml.replace("ram:ShipToTradeParty", "ram:ShipToCITradeParty");
+ xml = xml.replace("ram:ShipFromTradeParty", "ram:ShipFromCITradeParty");
+ xml = xml.replace("ram:ReceivableSpecifiedTradeAccountingAccount",
+ "ram:ReceivableSpecifiedCITradeAccountingAccount");
+ xml = xml.replace("ram:ContractReferencedDocument", "ram:ContractReferencedCIReferencedDocument");
+ // "ram:SpecifiedTradeAccountingAccount ram:SalesSpecifiedTradeAccountingAccount
+ // oder ReceivablesSpecifiedTradeAccountingAccount oder
+ // PurchaseSpecifiedTradeAccountingAccount
+ xml = xml.replace("ram:AdditionalReferencedDocument", "ram:AdditionalReferencedCIReferencedDocument");
+ xml = xml.replace("ram:TelephoneUniversalCommunication", "ram:TelephoneCIUniversalCommunication");
+ xml = xml.replace("ram:EmailURIUniversalCommunication", "ram:EmailURICIUniversalCommunication");
+ xml = xml.replace("ram:AdditionalReferencedDocument", "ram:AdditionalReferencedCIReferencedDocument");
+ xml = xml.replace("ram:IncludedReferencedProduct", "ram:IncludedReferencedProduct");
+
+ xml = xml.replace("ram:DefinedTradeContact", "ram:DefinedCITradeContact");
+ xml = xml.replace("ram:BillingSpecifiedPeriod", "ram:BillingCISpecifiedPeriod");
+ xml = xml.replace("ram:BuyerOrderReferencedDocument", "ram:BuyerOrderReferencedCIReferencedDocument");
+ xml = xml.replace("ram:DeliveryNoteReferencedDocument", "ram:DeliveryNoteReferencedCIReferencedDocument");
+ xml = xml.replace("ram:SpecifiedTradeAllowanceCharge", "ram:SpecifiedCITradeAllowanceCharge");
+ xml = xml.replace("ram:SpecifiedLogisticsServiceCharge", "ram:SpecifiedCILogisticsServiceCharge");
+ xml = xml.replace("ram:AppliedTradeAllowanceCharge", "ram:AppliedCITradeAllowanceCharge");
+ xml = xml.replace("ram:InvoiceeTradeParty", "ram:InvoiceeCITradeParty");
+ xml = xml.replace("ram:CategoryTradeTax", "ram:CategoryCITradeTax");
+ xml = xml.replace("ram:SpecifiedTaxRegistration", "ram:SpecifiedCITaxRegistration");
+ xml = xml.replace("ram:PostalTradeAddress", "ram:PostalCITradeAddress");
+ xml = xml.replace("ram:SpecifiedTradePaymentTerms", "ram:SpecifiedCITradePaymentTerms");
+ xml = xml.replace("ram:SpecifiedSupplyChainTradeAgreement", "ram:SpecifiedCIILSupplyChainTradeAgreement");
+ xml = xml.replace("ram:GrossPriceProductTradePrice", "ram:GrossPriceProductCITradePrice");
+ xml = xml.replace("ram:NetPriceProductTradePrice", "ram:NetPriceProductCITradePrice");
+ xml = xml.replaceAll("(?s)\\", "");
+ // remove manually for the time being:
+ // xml=xml.replaceAll("ram:TestIndicator>(.*?)/ram:TestIndicator>", "");
+ // one ram:SpecifiedCIILTradeSettlementMonetarySummation will have to be
+ // ram:SpecifiedCIIHTradeSettlementMonetarySummation afterwards
+
+ String summationClose = "";
+ int posFirstSummation = xml.indexOf(summationClose) + summationClose.length();
+ // if ram:SpecifiedTradeSettlementMonetarySummation were not found indexOf would
+ // return -1, therefore,
+ // to check if it
+ if (posFirstSummation > summationClose.length()) {
+ String xmlAfterFirstSummation = xml.substring(posFirstSummation);
+ String xmlBeforeIncludingFirstSummation = xml.substring(0, posFirstSummation);
+ // replace only once the header
+
+ xmlBeforeIncludingFirstSummation = xmlBeforeIncludingFirstSummation.replace(
+ "ram:SpecifiedTradeSettlementMonetarySummation",
+ "ram:SpecifiedCIIHTradeSettlementMonetarySummation");
+ // reconstruct the document now with a replaced first
+ // ram:SpecifiedTradeSettlementMonetarySummation to SpecifiedCIIH...
+ xml = xmlBeforeIncludingFirstSummation + xmlAfterFirstSummation;
+
+ }
+ // replace the rest of the ram:SpecifiedTradeSettlementMonetarySummation with
+ // the line value SpecifiedCIIL...
+ xml = xml.replace("ram:SpecifiedTradeSettlementMonetarySummation",
+ "ram:SpecifiedCIILTradeSettlementMonetarySummation");
+
+ // the rest of the ram:SpecifiedTradeSettlementMonetarySummation should be in
+ // ram:ApplicableSupplyChainTradeSettlement
+ // xml=xml.replaceAll(Pattern.quote("ram:SpecifiedTradeSettlementMonetarySummation"),
+ // "ram:SpecifiedCIILTradeSettlementMonetarySummation");
+ return xml;
+ }
+
+}