001/*
002 * Copyright (C) 2014 Konik.io
003 *
004 * This file is part of Konik library.
005 *
006 * Konik library is free software: you can redistribute it and/or modify
007 * it under the terms of the GNU Affero General Public License as published by
008 * the Free Software Foundation, either version 3 of the License, or
009 * (at your option) any later version.
010 *
011 * Konik library is distributed in the hope that it will be useful,
012 * but WITHOUT ANY WARRANTY; without even the implied warranty of
013 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
014 * GNU Affero General Public License for more details.
015 *
016 * You should have received a copy of the GNU Affero General Public License
017 * along with Konik library.  If not, see <http://www.gnu.org/licenses/>.
018 */
019package io.konik.itext.xmp;
020
021import static io.konik.itext.xmp.XmpZfNs.ZF_NS;
022import static javax.xml.xpath.XPathConstants.NODE;
023import io.konik.exception.TransformationWarning;
024
025import java.io.ByteArrayInputStream;
026import java.io.ByteArrayOutputStream;
027import java.io.IOException;
028import java.io.InputStream;
029
030import javax.inject.Named;
031import javax.inject.Singleton;
032import javax.xml.parsers.DocumentBuilder;
033import javax.xml.parsers.DocumentBuilderFactory;
034import javax.xml.parsers.ParserConfigurationException;
035import javax.xml.transform.Result;
036import javax.xml.transform.Source;
037import javax.xml.transform.Transformer;
038import javax.xml.transform.TransformerConfigurationException;
039import javax.xml.transform.TransformerException;
040import javax.xml.transform.TransformerFactory;
041import javax.xml.transform.TransformerFactoryConfigurationError;
042import javax.xml.transform.dom.DOMSource;
043import javax.xml.transform.stream.StreamResult;
044import javax.xml.xpath.XPath;
045import javax.xml.xpath.XPathExpressionException;
046import javax.xml.xpath.XPathFactory;
047
048import org.w3c.dom.DOMException;
049import org.w3c.dom.Document;
050import org.w3c.dom.Element;
051import org.w3c.dom.Node;
052import org.xml.sax.SAXException;
053
054/**
055 * The Xmp Appender.
056 */
057@Named
058@Singleton
059public class XmpAppender {
060
061   private final XPath xpath;
062   private DocumentBuilder documentBuilder;
063   private Transformer transformer;
064
065   /**
066    * Instantiates a new XMP appender.
067    */
068   public XmpAppender() {
069      XPathFactory factory = XPathFactory.newInstance();
070      xpath = factory.newXPath();
071      //java bug workaround
072      System.setProperty("com.sun.org.apache.xml.internal.dtm.DTMManager", "com.sun.org.apache.xml.internal.dtm.ref.DTMManagerDefault");
073   }
074   
075   /**
076    * Append the ZUGFeRD XMP info to the existing content.
077    *
078    * @param xmpData content to append to
079    * @param info the xmp info entity
080    * @return the resulting xmp content or xmpData if nothing needed to be added
081    * @throws TransformationWarning the transformation warning
082    */
083   public byte[] append(final byte[] xmpData, final ZfXmpInfo info) throws TransformationWarning {
084      try {
085         Document xmpDocument = getDocument(xmpData);
086         if (hasNoZfEntry(xmpDocument)) {
087            appendZfExtensionToDocument(xmpDocument);
088            appendZfInfoToDocumnt(xmpDocument, info);
089            return transformToOutPutStream(xmpDocument).toByteArray();
090         }
091         return xmpData;
092      } catch (Exception e) {
093         throw new TransformationWarning("Could not append ZF information to PDFs XMP data: "+ e.getLocalizedMessage(), e);
094      }
095   }
096
097   private Document getDocument(byte[] xmlContent) throws SAXException, IOException, ParserConfigurationException {
098      return getDocumentBuilder().parse(new ByteArrayInputStream(xmlContent));
099   }
100
101   /**
102    * Contains no ZUGFeRD entry in Document.
103    * 
104    * Check for:
105    * [source,xml]
106    * &lt;rdf:Description rdf:about=&quot;&quot; xmlns:zf=&quot;urn:ferd:pdfa:invoice:rc#&quot;&gt;
107    * 
108    * @param xmpDocument the xmp document we are checking on
109    * @return true, if has no Zf Entry
110    * @throws XPathExpressionException the x path expression exception
111    */
112   private boolean hasNoZfEntry(Document xmpDocument) throws XPathExpressionException  {
113      String value = xpath.evaluate("/xmpmeta/RDF/Description/DocumentType", xmpDocument);
114      return !"INVOICE".equalsIgnoreCase(value);
115   }
116
117   private Document appendZfExtensionToDocument(Document document) throws XPathExpressionException, DOMException, SAXException, IOException, ParserConfigurationException {
118      Node rdfNode = (Node) xpath.evaluate("/xmpmeta/RDF/Description/schemas/Bag", document, NODE);
119      Node importedZfNode = document.importNode(getExtensionNodeFromExtensionFile(), true);
120      rdfNode.appendChild(importedZfNode);
121      return document;
122   }
123
124   private Node getExtensionNodeFromExtensionFile() throws SAXException, IOException, ParserConfigurationException, XPathExpressionException{
125      InputStream zfExtensionIs = this.getClass().getResourceAsStream("/zfSchema/zf_extension.xmp");
126      Document zfExtension = getDocumentBuilder().parse(zfExtensionIs);
127      Node zfExtensionNode = (Node) xpath.evaluate("/RDF/Description/schemas/Bag/li", zfExtension, NODE);
128      return zfExtensionNode;
129   }
130
131   /**
132    * Append zf info.
133    *
134    * @param xmpDocument the xmp document
135    * @param info the info
136    * @return the byte array output stream
137    * @throws TransformerConfigurationException the transformer configuration exception
138    * @throws TransformerFactoryConfigurationError the transformer factory configuration error
139    * @throws TransformerException the transformer exception
140    * @throws ParserConfigurationException the parser configuration exception
141    * @throws XPathExpressionException the x path expression exception
142    */
143   private ByteArrayOutputStream appendZfInfoToDocumnt(Document xmpDocument, ZfXmpInfo info)
144         throws TransformerConfigurationException, TransformerFactoryConfigurationError, TransformerException,
145         ParserConfigurationException, XPathExpressionException {
146
147      Node descriptionNode = (Node) xpath.evaluate("/xmpmeta/RDF", xmpDocument, NODE);
148      Node zfDescriptionNode = createZfDescriptionNode(xmpDocument, info);
149      descriptionNode.appendChild(zfDescriptionNode);
150
151      return transformToOutPutStream(xmpDocument);
152   }
153
154   private Node createZfDescriptionNode(Document xmp, ZfXmpInfo info) {
155      Element node = xmp.createElement("rdf:Description");
156      node.setAttribute("xmlns:zf", ZF_NS.value);
157      node.setAttribute("rdf:about", "");
158      node.appendChild(xmp.createElement("zf:ConformanceLevel")).setTextContent(info.getConformanceLevel());
159      node.appendChild(xmp.createElement("zf:DocumentFileName")).setTextContent(info.getDocumentFileName());
160      node.appendChild(xmp.createElement("zf:DocumentType")).setTextContent(info.getDocumentType());
161      node.appendChild(xmp.createElement("zf:Version")).setTextContent(info.getVersion());
162      return node;
163   }
164
165   private ByteArrayOutputStream transformToOutPutStream(Document doc) throws TransformerFactoryConfigurationError,
166         TransformerConfigurationException, TransformerException {
167      ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
168      Source src = new DOMSource(doc);
169      Result dest = new StreamResult(outputStream);
170      getTransformer().transform(src, dest);
171      return outputStream;
172   }
173
174   private Transformer getTransformer() throws TransformerFactoryConfigurationError, TransformerConfigurationException {
175      if (transformer == null) {
176         TransformerFactory tranFactory = TransformerFactory.newInstance();
177         transformer = tranFactory.newTransformer();
178      }
179      return transformer;
180   }
181
182   private DocumentBuilder getDocumentBuilder() throws ParserConfigurationException {
183      if (documentBuilder == null) {
184         DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();
185         documentBuilder = docBuilderFactory.newDocumentBuilder();
186      }
187      return documentBuilder;
188   }
189}