pdfbox-trunk/xmpbox/src/main/java/org/apache/xmpbox/xml/XmpSerializer.java
/*****************************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.xmpbox.xml;

import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Result;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import org.apache.xmpbox.XMPMetadata;
import org.apache.xmpbox.XmpConstants;
import org.apache.xmpbox.schema.XMPSchema;
import org.apache.xmpbox.type.AbstractComplexProperty;
import org.apache.xmpbox.type.AbstractField;
import org.apache.xmpbox.type.AbstractSimpleProperty;
import org.apache.xmpbox.type.AbstractStructuredType;
import org.apache.xmpbox.type.ArrayProperty;
import org.apache.xmpbox.type.Attribute;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;

public class XmpSerializer
{

    private final TransformerFactory transformerFactory;
    private final DocumentBuilder documentBuilder;

    /**
     * Default constructor.
     */
    @SuppressWarnings({ "squid:S2755" }) // self-created XML
    public XmpSerializer()
    {
        this(TransformerFactory.newInstance(), DocumentBuilderFactory.newInstance());
    }

    /**
     * Constructor to be used if other factories than the default ones are needed.
     * 
     * @param transformerFactory     transformer factory to be used
     * @param documentBuilderFactory document builder factory to be used
     */
    public XmpSerializer(TransformerFactory transformerFactory,
            DocumentBuilderFactory documentBuilderFactory)
    {
        this.transformerFactory = transformerFactory;
        // xml init
        try
        {
            documentBuilder = documentBuilderFactory.newDocumentBuilder();
        }
        catch (ParserConfigurationException e)
        {
            // never happens, because we don't call builderFactory#setAttribute
            throw new RuntimeException(e);
        }
    }

    public void serialize(XMPMetadata metadata, OutputStream os, boolean withXpacket) throws TransformerException
    {
        Document doc = documentBuilder.newDocument();
        // fill document
        Element rdf = createRdfElement(doc, metadata, withXpacket);
        for (XMPSchema schema : metadata.getAllSchemas())
        {
            rdf.appendChild(serializeSchema(doc, schema));
        }
        // save
        save(doc, os, "UTF-8");
    }

    protected Element serializeSchema(Document doc, XMPSchema schema)
    {
        // prepare schema
        Element selem = doc.createElementNS(XmpConstants.RDF_NAMESPACE, "rdf:Description");
        selem.setAttributeNS(XmpConstants.RDF_NAMESPACE, "rdf:about", schema.getAboutValue());
        selem.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:" + schema.getPrefix(), schema.getNamespace());
        // the other attributes
        fillElementWithAttributes(selem, schema);
        // the content
        List<AbstractField> fields = schema.getAllProperties();
        serializeFields(doc, selem, fields,schema.getPrefix(), null, true);
        // return created schema
        return selem;
    }

    public void serializeFields(Document doc, Element parent, List<AbstractField> fields, String resourceNS, String prefix, boolean wrapWithProperty)
    {
        boolean usePrefix = prefix != null && !prefix.isEmpty();
        for (AbstractField field : fields)
        {
            if (field instanceof AbstractSimpleProperty)
            {
                AbstractSimpleProperty simple = (AbstractSimpleProperty) field;
                
                String localPrefix;
                
                if (usePrefix)
                {
                    localPrefix = prefix;
                }
                else
                {
                    localPrefix = simple.getPrefix();
                }
                
                Element esimple = doc.createElement(localPrefix + ":" + simple.getPropertyName());
                esimple.setTextContent(simple.getStringValue());
                List<Attribute> attributes = simple.getAllAttributes();
                for (Attribute attribute : attributes)
                {
                    esimple.setAttributeNS(attribute.getNamespace(), attribute.getName(), attribute.getValue());
                }
                parent.appendChild(esimple);
            }
            else if (field instanceof ArrayProperty)
            {
                ArrayProperty array = (ArrayProperty) field;
                // property
                Element asimple = doc.createElement(array.getPrefix() + ":" + array.getPropertyName());
                parent.appendChild(asimple);
                // attributes
                fillElementWithAttributes(asimple, array);
                // the array definition
                Element econtainer = doc.createElement(XmpConstants.DEFAULT_RDF_PREFIX + ":" + array.getArrayType());
                asimple.appendChild(econtainer);
                // for each element of the array
                List<AbstractField> innerFields = array.getAllProperties();
                serializeFields(doc, econtainer, innerFields,resourceNS, XmpConstants.DEFAULT_RDF_PREFIX, false);
            }
            else if (field instanceof AbstractStructuredType)
            {
                AbstractStructuredType structured = (AbstractStructuredType) field;
                List<AbstractField> innerFields = structured.getAllProperties();
                // property name attribute
                Element listParent = parent;
                if (wrapWithProperty)
                {
                    Element nstructured = doc
                            .createElement(resourceNS + ":" + structured.getPropertyName());
                    parent.appendChild(nstructured);
                    listParent = nstructured;
                }

                // element li
                Element estructured = doc.createElement(XmpConstants.DEFAULT_RDF_PREFIX + ":" + XmpConstants.LIST_NAME);
                listParent.appendChild(estructured);
                estructured.setAttribute("rdf:parseType", "Resource");

                // all properties
                serializeFields(doc, estructured, innerFields,resourceNS, null, true);
            }
            else
            {
                // XXX finish serialization classes
                System.err.println(">> TODO >> " + field.getClass());
            }
        }
    }

    private void fillElementWithAttributes(Element target, AbstractComplexProperty property)
    {
        // normalize the attributes list
        List<Attribute> toSerialize = normalizeAttributes(property);        

        toSerialize.forEach(attribute ->
        {
            if (XmpConstants.RDF_NAMESPACE.equals(attribute.getNamespace()))
            {
                target.setAttribute(XmpConstants.DEFAULT_RDF_PREFIX + ":" + attribute.getName(), attribute.getValue());
            }
            else
            {
                target.setAttribute(attribute.getName(), attribute.getValue());
            }
        });

        property.getAllNamespacesWithPrefix().forEach((key, value) ->
                target.setAttribute(XMLConstants.XMLNS_ATTRIBUTE + ":" + value, key));
    }

    /** Normalize the list of attributes.
     * 
     * Attributes which match a schema property are serialized as child elements
     * so only return the ones which do not match a schema property
     * 
     * @param property the property that needs to be inspected
     * @return the list of attributed for serializing
     */
    private List<Attribute> normalizeAttributes(AbstractComplexProperty property)
    {
        List<Attribute> attributes = property.getAllAttributes();
        

        List<Attribute> toSerialize = new ArrayList<>();
        List<AbstractField> fields = property.getAllProperties();
                
        for (Attribute attribute : attributes)
        {
            boolean matchesField = false;
            for (AbstractField field : fields)
            {
                if (attribute.getName().compareTo(field.getPropertyName()) == 0)
                {
                    matchesField = true;
                    break;
                }
            }
            if (!matchesField)
            {
                toSerialize.add(attribute);
            }
        }
        return toSerialize;
        
    }

    protected Element createRdfElement(Document doc, XMPMetadata metadata, boolean withXpacket)
    {
        // starting xpacket
        if (withXpacket)
        {
            ProcessingInstruction beginXPacket = doc.createProcessingInstruction("xpacket",
                    "begin=\"" + metadata.getXpacketBegin() + "\" id=\"" + metadata.getXpacketId() + "\"");
            doc.appendChild(beginXPacket);
        }
        // meta element
        Element xmpmeta = doc.createElementNS("adobe:ns:meta/", "x:xmpmeta");
        xmpmeta.setAttributeNS(XMLConstants.XMLNS_ATTRIBUTE_NS_URI, "xmlns:x", "adobe:ns:meta/");
        doc.appendChild(xmpmeta);
        // ending xpacket
        if (withXpacket)
        {
            ProcessingInstruction endXPacket = doc.createProcessingInstruction("xpacket",
                    "end=\"" + metadata.getEndXPacket() + "\"");
            doc.appendChild(endXPacket);
        }
        // rdf element
        Element rdf = doc.createElementNS(XmpConstants.RDF_NAMESPACE, "rdf:RDF");
        // rdf.setAttributeNS(XMPSchema.NS_NAMESPACE, qualifiedName, value)
        xmpmeta.appendChild(rdf);
        // return the rdf element where all will be put
        return rdf;
    }

    /**
     * Save the XML document to an output stream.
     * 
     * @param doc       The XML document to save.
     * @param outStream The stream to save the document to.
     * @param encoding  The encoding to save the file as.
     * 
     * @throws TransformerException If there is an error while saving the XML.
     */
    private void save(Node doc, OutputStream outStream, String encoding) throws TransformerException
    {
        Transformer transformer = transformerFactory.newTransformer();
        // human readable
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        // indent elements
        transformer.setOutputProperty("{http://xml.apache.org/xslt}indent-amount", "2");
        // encoding
        transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        // initialize StreamResult with File object to save to file
        Result result = new StreamResult(outStream);
        DOMSource source = new DOMSource(doc);
        // save
        transformer.transform(source, result);
    }
}
