/**
 * 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.camel.component.xmlsecurity.processor;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.UUID;

import javax.xml.crypto.AlgorithmMethod;
import javax.xml.crypto.KeySelector;
import javax.xml.crypto.dom.DOMStructure;
import javax.xml.crypto.dsig.CanonicalizationMethod;
import javax.xml.crypto.dsig.DigestMethod;
import javax.xml.crypto.dsig.Reference;
import javax.xml.crypto.dsig.SignatureMethod;
import javax.xml.crypto.dsig.SignedInfo;
import javax.xml.crypto.dsig.Transform;
import javax.xml.crypto.dsig.XMLObject;
import javax.xml.crypto.dsig.XMLSignature;
import javax.xml.crypto.dsig.XMLSignatureException;
import javax.xml.crypto.dsig.XMLSignatureFactory;
import javax.xml.crypto.dsig.dom.DOMSignContext;
import javax.xml.crypto.dsig.keyinfo.KeyInfo;
import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec;
import javax.xml.crypto.dsig.spec.TransformParameterSpec;
import javax.xml.crypto.dsig.spec.XPathFilterParameterSpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.validation.Schema;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.apache.camel.Exchange;
import org.apache.camel.Message;
import org.apache.camel.component.xmlsecurity.api.KeyAccessor;
import org.apache.camel.component.xmlsecurity.api.SignatureType;
import org.apache.camel.component.xmlsecurity.api.XmlSignatureConstants;
import org.apache.camel.component.xmlsecurity.api.XmlSignatureException;
import org.apache.camel.component.xmlsecurity.api.XmlSignatureFormatException;
import org.apache.camel.component.xmlsecurity.api.XmlSignatureHelper;
import org.apache.camel.component.xmlsecurity.api.XmlSignatureInvalidKeyException;
import org.apache.camel.component.xmlsecurity.api.XmlSignatureNoKeyException;
import org.apache.camel.component.xmlsecurity.api.XmlSignatureProperties;
import org.apache.camel.processor.validation.DefaultValidationErrorHandler;
import org.apache.camel.processor.validation.ValidatorErrorHandler;
import org.apache.camel.util.IOHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Creates from the message body a XML signature element which is returned in
 * the message body of the output message. Enveloped, enveloping XML, and
 * detached signatures are supported.
 * <p>
 * In the enveloped XML signature case, the method
 * {@link XmlSignerConfiguration#getParentLocalName()} must not return
 * <code>null</code>. In this case the parent element must be contained in the
 * XML document provided by the message body and the signature element is added
 * as last child element of the parent element. If a KeyInfo instance is
 * provided by the {@link KeyAccessor} and
 * {@link XmlSignerConfiguration#getAddKeyInfoReference()} is <code>true</code>,
 * then also a reference to the KeyInfo element is added. The generated XML
 * signature has the following structure:
 * 
 * <pre>
 * {@code
 * <[parent element]>
 *     ...
 *      <Signature Id="[signature_id]">
 *          <SignedInfo>
 *                <Reference URI=""> 
 *                      <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
 *                      (<Transform>)*
 *                      <DigestMethod>
 *                      <DigestValue>
 *                </Reference>
 *                (<Reference URI="#[keyinfo_Id]">
 *                      <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
 *                      <DigestMethod>
 *                      <DigestValue>
 *                </Reference>)?
 *                <!-- further references possible, see XmlSignerConfiguration#setProperties(XmlSignatureProperties) -->
 *         </SignedInfo>
 *         <SignatureValue>
 *         (<KeyInfo Id="[keyinfo_id]">)?
 *         <!-- Object elements possible, see XmlSignerConfiguration#setProperties(XmlSignatureProperties) -->
 *     </Signature>
 * </[parent element]>
 * }
 * </pre>
 * <p>
 * In the enveloping XML signature case, the generated XML signature has the
 * following structure:
 * 
 * <pre>
 *  {@code
 *  <Signature Id="[signature_id]">
 *     <SignedInfo>
 *            <Reference URI="#[object_id]" type="[optional_type_value]"> 
 *                  (<Transform>)*
 *                  <DigestMethod>
 *                  <DigestValue>
 *            </Reference>
 *            (<Reference URI="#[keyinfo_id]">
 *                  <Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
 *                  <DigestMethod>
 *                  <DigestValue>
 *            </Reference>)?
 *             <!-- further references possible, see XmlSignerConfiguration#setProperties(XmlSignatureProperties) -->
 *     </SignedInfo>
 *     <SignatureValue>
 *     (<KeyInfo Id="[keyinfo_id]">)?
 *     <Object Id="[object_id]"/>
 *     <!-- further Object elements possible, see XmlSignerConfiguration#setProperties(XmlSignatureProperties) -->
 * </Signature>   
 *  }
 * </pre>
 * 
 * In the enveloping XML signature case, also message bodies containing plain
 * text are supported. This must be indicated via the header
 * {@link XmlSignatureConstants#HEADER_MESSAGE_IS_PLAIN_TEXT} or via the
 * configuration {@link XmlSignerConfiguration#getPlainText()}.
 * 
 * <p>
 * Detached signatures where the signature element is a sibling element to the
 * signed element are supported. Those elements can be signed which have ID
 * attributes. The elements to be signed must be specified via xpath expressions
 * (see {@link XmlSignerConfiguration#setXpathsToIdAttributes(List)}) and the
 * XML schema must be provided via the schema resource URI (see method
 * {@link XmlSignerConfiguration#setSchemaResourceUri(String)}. Elements with
 * deeper hierarchy level are signed first. This procedure can result in nested
 * signatures.
 * 
 * <p>
 * In all cases, the digest algorithm is either read from the configuration
 * method {@link XmlSignerConfiguration#getDigestAlgorithm()} or calculated from
 * the signature algorithm (
 * {@link XmlSignerConfiguration#getSignatureAlgorithm()}. The optional
 * transforms are read from {@link XmlSignerConfiguration#getTransformMethods()}
 * .
 * <p>
 * In all cases, you can add additional references and objects which contain
 * properties for the XML signature, see
 * {@link XmlSignerConfiguration#setProperties(XmlSignatureProperties)}.
 */

public class XmlSignerProcessor extends XmlSignatureProcessor {

    private static final Logger LOG = LoggerFactory.getLogger(XmlSignerProcessor.class);

    private static final String SHA512 = "sha512";
    private static final String SHA384 = "sha384";
    private static final String SHA256 = "sha256";
    private static final String SHA224 = "sha224";
    private static final String SHA1 = "sha1";
    private static final String RIPEMD160 = "ripemd160";

    private static final String HTTP_WWW_W3_ORG_2001_04_XMLDSIG_MORE_SHA224 = 
        "http://www.w3.org/2001/04/xmldsig-more#sha224"; // see RFC 4051
    
    private static final String HTTP_WWW_W3_ORG_2001_04_XMLDSIG_MORE_SHA384 = 
        "http://www.w3.org/2001/04/xmldsig-more#sha384"; // see RFC 4051

    private final XmlSignerConfiguration config;
    
    public XmlSignerProcessor(XmlSignerConfiguration config) {
        this.config = config;
    }

    @Override
    public XmlSignerConfiguration getConfiguration() {
        return config;
    }

    @Override
    public void process(Exchange exchange) throws Exception { //NOPMD

        try {
            LOG.debug("XML signature generation started using algorithm {} and canonicalization method {}", getConfiguration()
                    .getSignatureAlgorithm(), getConfiguration().getCanonicalizationMethod().getAlgorithm());

            // lets setup the out message before we invoke the signing
            // so that it can mutate it if necessary
            Message out = exchange.getOut();
            out.copyFrom(exchange.getIn());

            Document outputDoc = sign(out);

            ByteArrayOutputStream outStream = new ByteArrayOutputStream();
            XmlSignatureHelper.transformNonTextNodeToOutputStream(outputDoc, outStream, omitXmlDeclaration(out), getConfiguration().getOutputXmlEncoding());
            byte[] data = outStream.toByteArray();
            out.setBody(data);
            setOutputEncodingToMessageHeader(out);
            clearMessageHeaders(out);
            LOG.debug("XML signature generation finished");
        } catch (Exception e) {
            // remove OUT message, as an exception occurred
            exchange.setOut(null);
            throw e;
        }
    }

    protected Document sign(final Message out) throws Exception { //NOPMD

        try {
            XMLSignatureFactory fac;
            // Try to install the Santuario Provider - fall back to the JDK provider if this does
            // not work
            try {
                fac = XMLSignatureFactory.getInstance("DOM", "ApacheXMLDSig");
            } catch (NoSuchProviderException ex) {
                fac = XMLSignatureFactory.getInstance("DOM");
            }

            final Node node = getMessageBodyNode(out);

            if (getConfiguration().getKeyAccessor() == null) {
                throw new XmlSignatureNoKeyException(
                    "Key accessor is missing for XML signature generation. Specify a key accessor in the configuration.");
            }
            
            final KeySelector keySelector = getConfiguration().getKeyAccessor().getKeySelector(out);
            if (keySelector == null) {
                throw new XmlSignatureNoKeyException(
                        "Key selector is missing for XML signature generation. Specify a key selector in the configuration.");
            }

            SignatureType signatureType = determineSignatureType(out);

            final List<String> contentReferenceUris = getContentReferenceUris(out, signatureType, node);

            Node lastParent = null;
            // per content reference URI a signature is built; for enveloped and enveloping there is only one content reference URI;
            // only in the detached case there can be several
            for (final String contentReferenceUri : contentReferenceUris) {

                // the method KeyAccessor.getKeyInfo must be called after the method KeyAccessor.getKeySelector, this is part of the interface contract!
                // and this method must be called within the loop over the content reference URIs, because for each signature the key info ID must be different
                final KeyInfo keyInfo = getConfiguration().getKeyAccessor().getKeyInfo(out, node, fac.getKeyInfoFactory());

                String signatureId = getConfiguration().getSignatureId();
                if (signatureId == null) {
                    signatureId = "_" + UUID.randomUUID().toString();
                } else if (signatureId.isEmpty()) {
                    // indicator that no signature Id attribute shall be generated
                    signatureId = null;
                }

                // parent only relevant for enveloped or detached signature
                Node parent = getParentOfSignature(out, node, contentReferenceUri, signatureType);

                if (parent == null) {
                    // for enveloping signature, create new document 
                    parent = XmlSignatureHelper.newDocumentBuilder(Boolean.TRUE).newDocument();
                }
                lastParent = parent;

                XmlSignatureProperties.Input input = new InputBuilder().contentDigestAlgorithm(getDigestAlgorithmUri()).keyInfo(keyInfo)
                        .message(out).messageBodyNode(node).parent(parent).signatureAlgorithm(getConfiguration().getSignatureAlgorithm())
                        .signatureFactory(fac).signatureId(signatureId).contentReferenceUri(contentReferenceUri)
                        .signatureType(signatureType)
                        .prefixForXmlSignatureNamespace(getConfiguration().getPrefixForXmlSignatureNamespace()).build();

                XmlSignatureProperties.Output properties = getSignatureProperties(input);

                
                // the signature properties can overwrite the signature Id
                if (properties != null && properties.getSignatureId() != null && !properties.getSignatureId().isEmpty()) {
                    signatureId = properties.getSignatureId();
                }

                List<? extends XMLObject> objects = getObjects(input, properties);
                List<? extends Reference> refs = getReferences(input, properties, getKeyInfoId(keyInfo));

                SignedInfo si = createSignedInfo(fac, refs);

                DOMSignContext dsc = createAndConfigureSignContext(parent, keySelector);

                XMLSignature signature = fac.newXMLSignature(si, keyInfo, objects, signatureId, null);
                // generate the signature
                signature.sign(dsc);
            }

            return XmlSignatureHelper.getDocument(lastParent);

        } catch (XMLSignatureException se) {
            if (se.getCause() instanceof InvalidKeyException) {
                throw new XmlSignatureInvalidKeyException(se.getMessage(), se);
            } else {
                throw new XmlSignatureException(se);
            }
        } catch (GeneralSecurityException e) {
            // like NoSuchAlgorithmException, InvalidAlgorithmParameterException, NoSuchProviderException
            throw new XmlSignatureException(e);
        }

    }

    private SignatureType determineSignatureType(Message message) throws XmlSignatureException {
        if (getConfiguration().getParentLocalName() != null && getConfiguration().getParentXpath() != null) {
            throw new XmlSignatureException(
                    "The configuration of the XML signer component is wrong. The parent local name "
                            + getConfiguration().getParentLocalName()
                            + " and the parent XPath " + getConfiguration().getParentXpath().getXPath() + " are specified. You must not specify both parameters.");

        }

        boolean isEnveloped = getConfiguration().getParentLocalName() != null || getConfiguration().getParentXpath() != null;

        boolean isDetached = getXpathToIdAttributes(message).size() > 0;

        if (isEnveloped && isDetached) {
            if (getConfiguration().getParentLocalName() != null) {
                throw new XmlSignatureException(
                    "The configuration of the XML signer component is wrong. The parent local name "
                            + getConfiguration().getParentLocalName()
                            + " for an enveloped signature and the XPATHs to ID attributes for a detached signature are specified. You must not specify both parameters.");
            } else {
                throw new XmlSignatureException(
                        "The configuration of the XML signer component is wrong. The parent XPath "
                                + getConfiguration().getParentXpath().getXPath()
                                + " for an enveloped signature and the XPATHs to ID attributes for a detached signature are specified. You must not specify both parameters.");

            }
        }

        SignatureType result;
        if (isEnveloped) {
            result = SignatureType.enveloped;
        } else if (isDetached) {
            if (getSchemaResourceUri(message) == null) {
                throw new XmlSignatureException(
                        "The configruation of the XML Signature component is wrong: No XML schema specified in the detached case");
            }
            result = SignatureType.detached;
        } else {
            result = SignatureType.enveloping;
        }

        LOG.debug("Signature type: {}", result);
        return result;

    }

    protected List<XPathFilterParameterSpec> getXpathToIdAttributes(Message message) {

        @SuppressWarnings("unchecked")
        List<XPathFilterParameterSpec> result = (List<XPathFilterParameterSpec>) message
                .getHeader(XmlSignatureConstants.HEADER_XPATHS_TO_ID_ATTRIBUTES);
        if (result == null) {
            result = getConfiguration().getXpathsToIdAttributes();
        }
        return result;
    }

    protected XmlSignatureProperties.Output getSignatureProperties(XmlSignatureProperties.Input input) throws Exception { //NOPMD
        XmlSignatureProperties propGetter = getConfiguration().getProperties();
        XmlSignatureProperties.Output propsOutput = null;
        if (propGetter != null) {
            propsOutput = propGetter.get(input);
        }
        return propsOutput;
    }

    private DOMSignContext createAndConfigureSignContext(Node parent, KeySelector keySelector) {
        DOMSignContext dsc = new DOMSignContext(keySelector, parent);
        // set namespace prefix for "http://www.w3.org/2000/09/xmldsig#" according to best practice described in http://www.w3.org/TR/xmldsig-bestpractices/#signing-xml-without-namespaces
        if (getConfiguration().getPrefixForXmlSignatureNamespace() != null
                && !getConfiguration().getPrefixForXmlSignatureNamespace().isEmpty()) {
            dsc.putNamespacePrefix("http://www.w3.org/2000/09/xmldsig#", getConfiguration().getPrefixForXmlSignatureNamespace());
        }
        dsc.putNamespacePrefix("http://www.w3.org/2001/10/xml-exc-c14n#", "ec");
        setCryptoContextProperties(dsc);
        setUriDereferencerAndBaseUri(dsc);
        return dsc;
    }

    protected Boolean omitXmlDeclaration(Message message) {
        Boolean omitXmlDeclaration = message.getHeader(XmlSignatureConstants.HEADER_OMIT_XML_DECLARATION, Boolean.class);
        if (omitXmlDeclaration == null) {
            omitXmlDeclaration = getConfiguration().getOmitXmlDeclaration();
        }
        if (omitXmlDeclaration == null) {
            omitXmlDeclaration = Boolean.FALSE;
        }
        LOG.debug("Omit XML declaration: {}", omitXmlDeclaration);
        return omitXmlDeclaration;
    }

    protected SignedInfo createSignedInfo(XMLSignatureFactory fac, List<? extends Reference> refs) throws Exception { //NOPMD
        return fac.newSignedInfo(fac.newCanonicalizationMethod(getConfiguration().getCanonicalizationMethod().getAlgorithm(),
                (C14NMethodParameterSpec) getConfiguration().getCanonicalizationMethod().getParameterSpec()),
                getSignatureMethod(getConfiguration().getSignatureAlgorithm(), fac), refs);
    }

    private SignatureMethod getSignatureMethod(String signatureAlgorithm, XMLSignatureFactory fac) throws NoSuchAlgorithmException,
            InvalidAlgorithmParameterException {
        return fac.newSignatureMethod(signatureAlgorithm, null);
    }

    protected Node getMessageBodyNode(Message message) throws Exception { //NOPMD
        InputStream is = message.getMandatoryBody(InputStream.class);

        Boolean isPlainText = isPlainText(message);

        Node node;
        if (isPlainText != null && isPlainText) {
            node = getTextNode(message, is);
        } else {
            ValidatorErrorHandler errorHandler = new DefaultValidationErrorHandler();
            Schema schema = getSchemaForSigner(message, errorHandler);
            Document doc = parseInput(is, getConfiguration().getDisallowDoctypeDecl(), schema, errorHandler);
            errorHandler.handleErrors(message.getExchange(), schema, null); // throws ValidationException
            node = doc.getDocumentElement();
            LOG.debug("Root element of document to be signed: {}", node);
        }
        return node;
    }

    protected Schema getSchemaForSigner(Message message, ValidatorErrorHandler errorHandler) throws XmlSignatureException, SAXException,
            IOException {
        Schema schema;
        String schemaResourceUri = getSchemaResourceUri(message);
        if (schemaResourceUri == null) {
            schema = null;
        } else {
            schema = getSchema(message);
        }
        return schema;
    }

    protected Boolean isPlainText(Message message) {
        Boolean isPlainText = message.getHeader(XmlSignatureConstants.HEADER_MESSAGE_IS_PLAIN_TEXT, Boolean.class);
        if (isPlainText == null) {
            isPlainText = getConfiguration().getPlainText();
        }
        LOG.debug("Is plain text: {}", isPlainText);
        return isPlainText;
    }

    protected Element getParentOfSignature(Message inMessage, Node messageBodyNode, String contentReferenceURI, SignatureType sigType)
        throws Exception { //NOPMD
        if (SignatureType.enveloping == sigType) {
            // enveloping case
            return null;
        }
        if (messageBodyNode.getParentNode() == null || messageBodyNode.getParentNode().getNodeType() != Node.DOCUMENT_NODE) {
            throw new XmlSignatureFormatException(
                    "Incomming message has wrong format: It is not an XML document. Cannot create an enveloped or detached XML signature.");
        }
        Document doc = (Document) messageBodyNode.getParentNode();
        if (SignatureType.detached == sigType) {
            return getParentForDetachedCase(doc, inMessage, contentReferenceURI);
        } else {
            // enveloped case
            return getParentForEnvelopedCase(doc, inMessage);
        }

    }
    
    protected Element getParentForEnvelopedCase(Document doc, Message inMessage) throws Exception { //NOPMD
        if (getConfiguration().getParentXpath() != null) {
            XPathFilterParameterSpec xp = getConfiguration().getParentXpath();
            XPathExpression exp;
            try {
                exp = XmlSignatureHelper.getXPathExpression(xp);
            } catch (XPathExpressionException e) {
                throw new XmlSignatureException("The parent XPath " + getConfiguration().getParentXpath().getXPath() + " is wrongly configured: The XPath " + xp.getXPath() + " is invalid.", e);
            }
            NodeList list = (NodeList) exp.evaluate(doc.getDocumentElement(), XPathConstants.NODESET);
            if (list == null || list.getLength() == 0) {
                throw new XmlSignatureException("The parent XPath " + xp.getXPath() + " returned no result. Check the configuration of the XML signer component.");
            }
            int length = list.getLength();
            for (int i = 0; i < length; i++) {
                Node node = list.item(i);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    // return the first element
                    return (Element)node;
                }
            }
            throw new XmlSignatureException("The parent XPath " + xp.getXPath() + " returned no element. Check the configuration of the XML signer component.");
        } else {
            // parent local name is not null!
            NodeList parents = doc.getElementsByTagNameNS(getConfiguration().getParentNamespace(), getConfiguration().getParentLocalName());
            if (parents == null || parents.getLength() == 0) {
                throw new XmlSignatureFormatException(
                        String.format(
                                "Incoming message has wrong format: The parent element with the local name %s and the namespace %s was not found in the message to build an enveloped XML signature.",
                                getConfiguration().getParentLocalName(), getConfiguration().getParentNamespace()));
            }
            // return the first element
            return (Element) parents.item(0);
        }
    }

    private Element getParentForDetachedCase(Document doc, Message inMessage, String referenceUri) throws XmlSignatureException {
        String elementId = referenceUri;
        if (elementId.startsWith("#")) {
            elementId = elementId.substring(1);
        }
        Element el = doc.getElementById(elementId);
        if (el == null) {
            // should not happen because has been checked before
            throw new IllegalStateException("No element found for element ID " + elementId);
        }
        LOG.debug("Sibling element of the detached XML Signature with reference URI {}: {}  {} ",
                new Object[] {referenceUri, el.getLocalName(), el.getNamespaceURI() });
        Element result = getParentElement(el);
        if (result != null) {
            return result;
        } else {
            throw new XmlSignatureException(
                    "Either the configuration of the XML Signature component is wrong or the incoming document has an invalid structure: The element "
                            + el.getLocalName() + "{" + el.getNamespaceURI() + "} which is referenced by the reference URI " + referenceUri
                            + " has no parent element. The element must have a parent element in the configured detached case.");
        }
    }

    private Element getParentElement(Node node) {
        int counter = 0;
        while (node != null && counter < 10000) {
            // counter is for avoiding security attacks
            Node parent = node.getParentNode();
            if (parent != null && parent.getNodeType() == Node.ELEMENT_NODE) {
                return (Element) parent;
            }
            node = parent;
            counter++;
        }
        return null;
    }

    protected List<? extends Reference> getReferences(XmlSignatureProperties.Input input, XmlSignatureProperties.Output properties,
            String keyInfoId) throws Exception { //NOPMD

        String referenceId = properties == null ? null : properties.getContentReferenceId();
        // Create Reference with URI="#<objectId>" for enveloping signature, URI="" for enveloped signature, and URI = <value from configuration> for detached signature and the transforms
        Reference ref = createReference(input.getSignatureFactory(), input.getContentReferenceUri(),
                getContentReferenceType(input.getMessage()), input.getSignatureType(), referenceId);
        Reference keyInfoRef = createKeyInfoReference(input.getSignatureFactory(), keyInfoId, input.getContentDigestAlgorithm());

        int propsRefsSize = properties == null || properties.getReferences() == null || properties.getReferences().isEmpty() ? 0
                : properties.getReferences().size();
        int size = keyInfoRef == null ? propsRefsSize + 1 : propsRefsSize + 2;
        List<Reference> referenceList = new ArrayList<Reference>(size);
        referenceList.add(ref);
        if (keyInfoRef != null) {
            referenceList.add(keyInfoRef);
        }
        if (properties != null && properties.getReferences() != null && !properties.getReferences().isEmpty()) {
            referenceList.addAll(properties.getReferences());
        }
        return referenceList;
    }

    protected List<? extends XMLObject> getObjects(XmlSignatureProperties.Input input, XmlSignatureProperties.Output properties)
        throws Exception { //NOPMD

        if (SignatureType.enveloped == input.getSignatureType() || SignatureType.detached == input.getSignatureType()) {
            if (properties == null || properties.getObjects() == null) {
                return Collections.emptyList();
            }
            return properties.getObjects();
        }

        // enveloping signature --> add additional object
        final String objectId = getConfiguration().getContentObjectId();
        LOG.debug("Object Content Id {}", objectId);

        XMLObject obj = createXMLObject(input.getSignatureFactory(), input.getMessageBodyNode(), objectId);
        if (properties == null || properties.getObjects() == null || properties.getObjects().isEmpty()) {
            return Collections.singletonList(obj);
        }
        List<XMLObject> result = new ArrayList<XMLObject>(properties.getObjects().size() + 1);
        result.add(obj);
        result.addAll(properties.getObjects());
        return result;
    }

    private Node getTextNode(Message inMessage, InputStream is) throws IOException, ParserConfigurationException, XmlSignatureException {
        LOG.debug("Message body to be signed is plain text");
        String encoding = getMessageEncoding(inMessage);
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        IOHelper.copyAndCloseInput(is, bos);
        try {
            String text = new String(bos.toByteArray(), encoding);
            return XmlSignatureHelper.newDocumentBuilder(true).newDocument().createTextNode(text);
        } catch (UnsupportedEncodingException e) {
            throw new XmlSignatureException(String.format("The message encoding %s is not supported.", encoding), e);
        }
    }

    protected String getMessageEncoding(Message inMessage) {
        String encoding = inMessage.getHeader(XmlSignatureConstants.HEADER_PLAIN_TEXT_ENCODING, String.class);
        if (encoding == null) {
            encoding = getConfiguration().getPlainTextEncoding();
        }
        LOG.debug("Messge encoding: {}", encoding);
        return encoding;
    }

    protected Document parseInput(InputStream is, Boolean disallowDoctypeDecl, Schema schema, ErrorHandler errorHandler)
        throws ParserConfigurationException, IOException, XmlSignatureFormatException {
        try {
            DocumentBuilder db = XmlSignatureHelper.newDocumentBuilder(disallowDoctypeDecl, schema);
            db.setErrorHandler(errorHandler);
            return db.parse(is);
        } catch (SAXException e) {
            throw new XmlSignatureFormatException(
                    "XML signature generation not possible. Sent message is not an XML document. Check the sent message.", e);
        } finally {
            IOHelper.close(is, "input stream");
        }
    }

    protected Reference createReference(XMLSignatureFactory fac, String uri, String type, SignatureType sigType, String id)
        throws InvalidAlgorithmParameterException, XmlSignatureException {
        try {
            List<Transform> transforms = getTransforms(fac, sigType);
            Reference ref = fac.newReference(uri, fac.newDigestMethod(getDigestAlgorithmUri(), null), transforms, type, id);
            return ref;
        } catch (NoSuchAlgorithmException e) {
            throw new XmlSignatureException("Wrong algorithm specified in the configuration.", e);
        }
    }

    protected String getContentReferenceType(Message message) {
        String type = message.getHeader(XmlSignatureConstants.HEADER_CONTENT_REFERENCE_TYPE, String.class);
        if (type == null) {
            type = getConfiguration().getContentReferenceType();
        }
        LOG.debug("Content reference type: {}", type);
        return type;
    }

    protected List<String> getContentReferenceUris(Message message, SignatureType signatureType, Node messageBodyNode)
        throws XmlSignatureException, XPathExpressionException {

        List<String> result;
        if (SignatureType.enveloping == signatureType) {
            String uri = "#" + getConfiguration().getContentObjectId();
            result = Collections.singletonList(uri);
        } else if (SignatureType.enveloped == signatureType) {
            // only for enveloped the parameter content reference URI is used
            String uri = message.getHeader(XmlSignatureConstants.HEADER_CONTENT_REFERENCE_URI, String.class);
            if (uri == null) {
                uri = getConfiguration().getContentReferenceUri();
            }
            if (uri == null) {
                uri = "";
            }
            result = Collections.singletonList(uri);
        } else if (SignatureType.detached == signatureType) {
            result = getContentReferenceUrisForDetachedCase(message, messageBodyNode);
        } else {
            // should not occur
            throw new IllegalStateException("Signature type " + signatureType + " not supported");
        }

        LOG.debug("Content reference URI(s): {}", result);
        return result;
    }

    private List<String> getContentReferenceUrisForDetachedCase(Message message, Node messageBodyNode) throws XmlSignatureException,
            XPathExpressionException {
        List<XPathFilterParameterSpec> xpathsToIdAttributes = getXpathToIdAttributes(message);
        if (xpathsToIdAttributes.isEmpty()) {
            // should not happen, has already been checked earlier
            throw new IllegalStateException("List of XPATHs to ID attributes is empty in detached signature case");
        }
        List<ComparableNode> result = new ArrayList<ComparableNode>(xpathsToIdAttributes.size());
        for (XPathFilterParameterSpec xp : xpathsToIdAttributes) {
            XPathExpression exp;
            try {
                exp = XmlSignatureHelper.getXPathExpression(xp);
            } catch (XPathExpressionException e) {
                throw new XmlSignatureException("The configured xpath expression " + xp.getXPath() + " is invalid.", e);
            }
            NodeList list = (NodeList) exp.evaluate(messageBodyNode, XPathConstants.NODESET);
            if (list == null) {
                //assume optional element, XSD validation has been done before
                LOG.warn("No ID attribute found for xpath expression {}. Therfore this xpath expression will be ignored.", xp.getXPath());
                continue;
            }
            int length = list.getLength();
            for (int i = 0; i < length; i++) {
                Node node = list.item(i);
                if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
                    Attr attr = (Attr) node;
                    String value = attr.getValue();
                    // check that attribute is ID attribute
                    Element element = messageBodyNode.getOwnerDocument().getElementById(value);
                    if (element == null) {
                        throw new XmlSignatureException(
                                "Wrong configured xpath expression for ID attributes: The evaluation of the xpath expression "
                                        + xp.getXPath() + " resulted in an attribute which is not of type ID. The attribute value is "
                                        + value + ".");
                    }
                    result.add(new ComparableNode(element, "#" + value));
                    LOG.debug("ID attribute with value {} found for xpath {}", value, xp.getXPath());
                } else {
                    throw new XmlSignatureException(
                            "Wrong configured xpath expression for ID attributes: The evaluation of the xpath expression " + xp.getXPath()
                                    + " returned a node which was not of type Attribute.");
                }
            }
        }
        if (result.size() == 0) {
            throw new XmlSignatureException(
                    "No element to sign found in the detached case. No node found for the configured xpath expressions "
                            + toString(xpathsToIdAttributes)
                            + ". Either the configuration of the XML signature component is wrong or the incoming message has not the correct structure.");
        }
        // sort so that elements with deeper hierarchy level are treated first
        Collections.sort(result);
        return ComparableNode.getReferenceUris(result);
    }

    private String toString(List<XPathFilterParameterSpec> xpathsToIdAttributes) {
        StringBuilder result = new StringBuilder();
        int counter = 0;
        for (XPathFilterParameterSpec xp : xpathsToIdAttributes) {
            counter++;
            result.append(xp.getXPath());
            if (counter < xpathsToIdAttributes.size()) {
                result.append(", ");
            }
        }
        return result.toString();
    }

    protected XMLObject createXMLObject(XMLSignatureFactory fac, Node node, String id) {
        return fac.newXMLObject(Collections.singletonList(new DOMStructure(node)), id, null, null);
    }

    private List<Transform> getTransforms(XMLSignatureFactory fac, SignatureType sigType) throws NoSuchAlgorithmException,
            InvalidAlgorithmParameterException {
        List<AlgorithmMethod> configuredTrafos = getConfiguration().getTransformMethods();
        if (SignatureType.enveloped == sigType) {
            // add enveloped transform if necessary
            if (configuredTrafos.size() > 0) {
                if (!containsEnvelopedTransform(configuredTrafos)) {
                    configuredTrafos = new ArrayList<AlgorithmMethod>(configuredTrafos.size() + 1);
                    configuredTrafos.add(XmlSignatureHelper.getEnvelopedTransform());
                    configuredTrafos.addAll(getConfiguration().getTransformMethods());
                }
            } else {
                // add enveloped and C14N trafo
                configuredTrafos = new ArrayList<AlgorithmMethod>(2);
                configuredTrafos.add(XmlSignatureHelper.getEnvelopedTransform());
                configuredTrafos.add(XmlSignatureHelper.getCanonicalizationMethod(CanonicalizationMethod.INCLUSIVE));
            }
        }

        List<Transform> transforms = new ArrayList<Transform>(configuredTrafos.size());
        for (AlgorithmMethod trafo : configuredTrafos) {
            Transform transform = fac.newTransform(trafo.getAlgorithm(), (TransformParameterSpec) trafo.getParameterSpec());
            transforms.add(transform);
            LOG.debug("Transform method: {}", trafo.getAlgorithm());
        }
        return transforms;
    }

    private boolean containsEnvelopedTransform(List<AlgorithmMethod> configuredTrafos) {
        for (AlgorithmMethod m : configuredTrafos) {
            if (Transform.ENVELOPED.equals(m.getAlgorithm())) {
                return true;
            }
        }
        return false;
    }

    protected String getDigestAlgorithmUri() throws XmlSignatureException {

        String result = getConfiguration().getDigestAlgorithm();
        if (result == null) {
            String signatureAlgorithm = getConfiguration().getSignatureAlgorithm();
            if (signatureAlgorithm != null) {
                if (signatureAlgorithm.contains(SHA1)) {
                    result = DigestMethod.SHA1;
                } else if (signatureAlgorithm.contains(SHA224)) {
                    result = HTTP_WWW_W3_ORG_2001_04_XMLDSIG_MORE_SHA224;
                } else if (signatureAlgorithm.contains(SHA256)) {
                    result = DigestMethod.SHA256;
                } else if (signatureAlgorithm.contains(SHA384)) {
                    result = HTTP_WWW_W3_ORG_2001_04_XMLDSIG_MORE_SHA384;
                } else if (signatureAlgorithm.contains(SHA512)) {
                    result = DigestMethod.SHA512;
                } else if (signatureAlgorithm.contains(RIPEMD160)) {
                    return DigestMethod.RIPEMD160;
                }
            }
        }
        if (result != null) {
            LOG.debug("Digest algorithm: {}", result);
            return result;
        }
        throw new XmlSignatureException(
                "Digest algorithm missing for XML signature generation. Specify the digest algorithm in the configuration.");
    }

    protected Reference createKeyInfoReference(XMLSignatureFactory fac, String keyInfoId, String digestAlgorithm) throws Exception { //NOPMD

        if (keyInfoId == null) {
            return null;
        }
        if (getConfiguration().getAddKeyInfoReference() == null) {
            return null;
        }

        if (!getConfiguration().getAddKeyInfoReference()) {
            return null;
        }

        LOG.debug("Creating reference to key info element with Id: {}", keyInfoId);
        List<Transform> transforms = new ArrayList<Transform>(1);
        Transform transform = fac.newTransform(CanonicalizationMethod.INCLUSIVE, (TransformParameterSpec) null);
        transforms.add(transform);
        return fac.newReference("#" + keyInfoId, fac.newDigestMethod(digestAlgorithm, null), transforms, null, null);
    }

    private String getKeyInfoId(KeyInfo keyInfo) throws Exception { //NOPMD
        if (keyInfo == null) {
            return null;
        }
        return keyInfo.getId();
    }
    
    protected void setOutputEncodingToMessageHeader(Message message) {
        if (getConfiguration().getOutputXmlEncoding() != null) {
            message.setHeader(Exchange.CHARSET_NAME, getConfiguration().getOutputXmlEncoding());
        }
    }


    private static class InputBuilder {

        private XMLSignatureFactory signatureFactory;

        private String signatureAlgorithm;

        private Node parent;

        private Node messageBodyNode;

        private Message message;

        private KeyInfo keyInfo;

        private String contentDigestAlgorithm;

        private String signatureId;

        private String contentReferenceUri;

        private SignatureType signatureType;

        private String prefixForXmlSignatureNamespace;

        public InputBuilder signatureFactory(XMLSignatureFactory signatureFactory) {
            this.signatureFactory = signatureFactory;
            return this;
        }

        public InputBuilder signatureAlgorithm(String signatureAlgorithm) {
            this.signatureAlgorithm = signatureAlgorithm;
            return this;
        }

        public InputBuilder parent(Node parent) {
            this.parent = parent;
            return this;
        }

        public InputBuilder messageBodyNode(Node messageBodyNode) {
            this.messageBodyNode = messageBodyNode;
            return this;
        }

        public InputBuilder message(Message message) {
            this.message = message;
            return this;
        }

        public InputBuilder keyInfo(KeyInfo keyInfo) {
            this.keyInfo = keyInfo;
            return this;
        }

        public InputBuilder contentDigestAlgorithm(String contentDigestAlgorithm) {
            this.contentDigestAlgorithm = contentDigestAlgorithm;
            return this;
        }

        public InputBuilder signatureId(String signatureId) {
            this.signatureId = signatureId;
            return this;
        }

        public InputBuilder contentReferenceUri(String contentReferenceUri) {
            this.contentReferenceUri = contentReferenceUri;
            return this;
        }

        public InputBuilder signatureType(SignatureType signatureType) {
            this.signatureType = signatureType;
            return this;
        }

        public InputBuilder prefixForXmlSignatureNamespace(String prefixForXmlSignatureNamespace) {
            this.prefixForXmlSignatureNamespace = prefixForXmlSignatureNamespace;
            return this;
        }

        public XmlSignatureProperties.Input build() {
            return new XmlSignatureProperties.Input() {

                @Override
                public XMLSignatureFactory getSignatureFactory() {
                    return signatureFactory;
                }

                @Override
                public String getSignatureAlgorithm() {
                    return signatureAlgorithm;
                }

                @Override
                public Node getParent() {
                    return parent;
                }

                @Override
                public Node getMessageBodyNode() {
                    return messageBodyNode;
                }

                @Override
                public Message getMessage() {
                    return message;
                }

                @Override
                public KeyInfo getKeyInfo() {
                    return keyInfo;
                }

                @Override
                public String getContentDigestAlgorithm() {
                    return contentDigestAlgorithm;
                }

                @Override
                public String getSignatureId() {
                    return signatureId;
                }

                @Override
                public String getContentReferenceUri() {
                    return contentReferenceUri;
                }

                @Override
                public SignatureType getSignatureType() {
                    return signatureType;
                }

                @Override
                public String getPrefixForXmlSignatureNamespace() {
                    return prefixForXmlSignatureNamespace;
                }

            };
        }

    }

    /** Compares nodes by their hierarchy level. */
    static class ComparableNode implements Comparable<ComparableNode> {

        private final String referenceUri;
        private final int level;

        ComparableNode(Element node, String referenceUri) {
            this.referenceUri = referenceUri;
            level = calculateLevel(node);
        }

        private int calculateLevel(Element node) {
            int counter = 0;
            for (Node n = node; n != null; n = n.getParentNode()) {
                if (Node.ELEMENT_NODE == n.getNodeType()) {
                    counter++;
                    if (counter > 10000) {
                        // prevent security attack
                        throw new IllegalStateException("Hierachy level is limited to 10000");
                    }
                }
            }
            return counter;
        }

        @Override
        public int compareTo(ComparableNode o) {
            return o.level - level;
        }

        String getReferenceUri() {
            return referenceUri;
        }

        static List<String> getReferenceUris(List<ComparableNode> input) {
            List<String> result = new ArrayList<String>(input.size());
            for (ComparableNode cn : input) {
                result.add(cn.getReferenceUri());
            }
            return result;
        }

    }

}
