/*
 * Copyright The Apache Software Foundation.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.rahas.test.util;

import junit.framework.Assert;
import org.apache.axiom.om.*;
import org.apache.axiom.soap.*;
import org.apache.axis2.addressing.AddressingConstants;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.ConfigurationContextFactory;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.engine.AxisConfiguration;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.rahas.RahasConstants;
import org.apache.rahas.RahasData;
import org.apache.rahas.TrustException;
import org.apache.rahas.TrustUtil;
import org.apache.rahas.impl.util.CommonUtil;
import org.apache.ws.secpolicy.Constants;
import org.apache.wss4j.dom.engine.WSSecurityEngineResult;
import org.apache.wss4j.dom.WSConstants;
import org.apache.wss4j.common.ext.WSSecurityException;
import org.apache.wss4j.common.crypto.CryptoType;
import org.apache.wss4j.common.crypto.Crypto;
import org.apache.wss4j.common.crypto.CryptoFactory;
import org.apache.wss4j.dom.handler.WSHandlerConstants;
import org.apache.wss4j.dom.handler.WSHandlerResult;
import org.apache.wss4j.common.saml.builder.SAML1Constants;
import org.apache.wss4j.common.util.XMLUtils;
import org.opensaml.saml.common.xml.SAMLConstants;
import org.w3c.dom.DOMConfiguration;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.stream.FactoryConfigurationError;
import javax.xml.stream.XMLStreamReader;
import java.io.*;
import java.security.Principal;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;

/**
 * Utility class for tests.
 */
public class TestUtil {

    private static final Log log = LogFactory.getLog(TestUtil.class);

    // Directly copied from WSS4J
    public static final String SAMPLE_SOAP_MSG =
        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
        + "<SOAP-ENV:Envelope "
        +   "xmlns:SOAP-ENV=\"http://schemas.xmlsoap.org/soap/envelope/\" "
        +   "xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" "
        +   "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\">"
        +    "<SOAP-ENV:Header"
        +       " xmlns:wsse=\"http://schemas.xmlsoap.org/ws/2002/07/secext\""
        +       " xmlns:wsu=\"http://schemas.xmlsoap.org/ws/2002/07/utility\"/>"
        +   "<SOAP-ENV:Body>"
        +       "<add xmlns=\"http://ws.apache.org/counter/counter_port_type\">"
        +           "<value xmlns=\"\">15</value>"
        +       "</add>"
        +   "</SOAP-ENV:Body>"
        + "</SOAP-ENV:Envelope>";

    /**
     * Convert an SOAP Envelope as a String to a org.w3c.dom.Document.
     * Directly copied from WSS4J
     */
    public static org.w3c.dom.Document toSOAPPart(String xml) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);

        InputStream in = new ByteArrayInputStream(xml.getBytes());
        DocumentBuilder builder = factory.newDocumentBuilder();
        return builder.parse(in);
    }

    public static Crypto getCrypto() throws IOException, WSSecurityException, TrustException {

        File file = new File("src/test/resources/crypto.config");
        Assert.assertTrue(file.exists());

        Properties properties = new Properties();
        try {
            properties.load(new FileInputStream(file));
        } catch (IOException e) {
            log.error("Unable to open crypto configuration file");
            throw e;
        }

        Crypto crypto = CryptoFactory.getInstance(properties);

        X509Certificate[] certificates = CommonUtil.getCertificatesByAlias(crypto, "apache");
        Assert.assertEquals(certificates.length, 1);

        return crypto;

    }

    public static X509Certificate getDefaultCertificate() throws WSSecurityException, TrustException, IOException {
        Crypto crypto = getCrypto();

        CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
        cryptoType.setAlias("apache");

        return crypto.getX509Certificates(cryptoType)[0];
    }

    public static Document getTestDocument() throws Exception {

        InputStream is = new ByteArrayInputStream(SAMPLE_SOAP_MSG.getBytes());
        SOAPEnvelope envelope = createSOAPEnvelope(is);

        return TestUtil.getDocumentFromSOAPEnvelope(envelope, true);
    }

    public static SOAPEnvelope createSOAPEnvelope(InputStream in) throws Exception {
        OMXMLParserWrapper builder = OMXMLBuilderFactory.createSOAPModelBuilder(in, null);
        return (SOAPEnvelope) builder.getDocumentElement();
    }

    public static OMElement getRSTTemplate(String samlNamespace) throws Exception {
        OMFactory fac = OMAbstractFactory.getOMFactory();
        OMElement element = null;
        OMElement elem = fac.createOMElement(Constants.RST_TEMPLATE);
        TrustUtil.createTokenTypeElement(RahasConstants.VERSION_05_02, elem).setText(samlNamespace);
        TrustUtil.createKeyTypeElement(RahasConstants.VERSION_05_02, elem,
                RahasConstants.KEY_TYPE_SYMM_KEY);
        TrustUtil.createKeySizeElement(RahasConstants.VERSION_05_02, elem, 256);
        element = TrustUtil.createClaims(RahasConstants.VERSION_05_02, elem, "http://wso2.org");
        addClaimType(element, "http://wso2.org/claims/givenname");
        return elem;
    }

    private static void addClaimType(OMElement parent, String uri) {
        OMElement element = null;
        element = parent.getOMFactory().createOMElement(new QName("http://schemas.xmlsoap.org/ws/2005/05/identity", "ClaimType", "wsid"),
                parent);
        element.addAttribute(parent.getOMFactory().createOMAttribute("Uri", null, uri));
    }

    public static TestSTSClient createTestSTSClient(String samlVersion) throws Exception {

        ConfigurationContext configurationContext
                = ConfigurationContextFactory.createConfigurationContextFromFileSystem("src/test/resources/repo",
                "src/test/resources/repo/conf/client.axis2.xml");

        TestSTSClient stsClient = new TestSTSClient(configurationContext);

        stsClient.setRstTemplate(getRSTTemplate(samlVersion));
        stsClient.setAction(RahasConstants.WST_NS_05_02 + RahasConstants.RST_ACTION_SCT);

        return stsClient;

    }

    public static MessageContext createDummyMessageContext(String appliesTo) throws Exception {
        TestSTSClient stsClient = TestUtil.createTestSTSClient(SAMLConstants.SAML20_NS);
        OMElement requestSecurityToken = stsClient.createRST(appliesTo);

        MessageContext dummyMessageContext = new MessageContext();

        populateReceivedResults(dummyMessageContext);

        dummyMessageContext.setProperty(AddressingConstants.WS_ADDRESSING_VERSION,
                AddressingConstants.Submission.WSA_NAMESPACE);

        SOAPFactory factory = OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM).getSOAP11Factory();
        SOAPEnvelope envelope = factory.createSOAPEnvelope();

        SOAPBody soapBody = factory.createSOAPBody(envelope);
        soapBody.addChild(requestSecurityToken);

        dummyMessageContext.setEnvelope(envelope);

        dummyMessageContext.setAxisService(new AxisService("TestService"));


        AxisConfiguration axisConfiguration = new AxisConfiguration();
        dummyMessageContext.setConfigurationContext(new ConfigurationContext(axisConfiguration));

        return dummyMessageContext;
    }

    public static RahasData createTestRahasData(String appliesTo) throws Exception {
        return new RahasData(createDummyMessageContext(appliesTo));
    }

    private static void populateReceivedResults(MessageContext messageContext) throws Exception {
        List<WSSecurityEngineResult> wsSecEngineResults = new ArrayList<WSSecurityEngineResult>();
        WSSecurityEngineResult result = new WSSecurityEngineResult(WSConstants.SIGN);

        Principal principal = new Principal() {
            public String getName() {
                return "apache";
            }
        };

        result.put(WSSecurityEngineResult.TAG_PRINCIPAL, principal);
        result.put(WSSecurityEngineResult.TAG_X509_CERTIFICATE, getDefaultCertificate());

        wsSecEngineResults.add(result);

        WSHandlerResult handlerResult = new WSHandlerResult(null, wsSecEngineResults, null);

        List<WSHandlerResult> handlerResultList = new ArrayList<WSHandlerResult>();
        handlerResultList.add(handlerResult);

        messageContext.setProperty(WSHandlerConstants.RECV_RESULTS, handlerResultList);

    }

    /**
     * This is directly taken from rampart-core.
     * TODO we need to move these common code to a new module. Otherwise code will be duplicated.
     * We cannot use following method from rampart-core as it creates a cyclic dependency. Therefore we have
     * to live with following.
     * @param doc The document to convert.
     * @param useDoom Whether to use doom or not.
     * @return A SOAPEnvelope.
     * @throws WSSecurityException If an error occurred during conversion.
     */
    public static SOAPEnvelope getSOAPEnvelopeFromDOMDocument(Document doc, boolean useDoom)
            throws WSSecurityException {

        if(useDoom) {
            try {
                //Get processed headers
                SOAPEnvelope env = (SOAPEnvelope)doc.getDocumentElement();
                ArrayList processedHeaderQNames = new ArrayList();
                SOAPHeader soapHeader = env.getHeader();

                if(soapHeader != null) {
                    Iterator headerBlocs = soapHeader.getChildElements();
                    while (headerBlocs.hasNext()) {

                    	OMElement element = (OMElement)headerBlocs.next();
                    	SOAPHeaderBlock header = null;

                    	if (element instanceof SOAPHeaderBlock) {
                            header = (SOAPHeaderBlock) element;

                        // If a header block is not an instance of SOAPHeaderBlock, it means that
                        // it is a header we have added in rampart eg. EncryptedHeader and should
                        // be converted to SOAPHeaderBlock for processing
                    	} else {
                    		header = soapHeader.addHeaderBlock(element.getLocalName(), element.getNamespace());
                    		Iterator attrIter = element.getAllAttributes();
                    		while (attrIter.hasNext()) {
                    			OMAttribute attr = (OMAttribute)attrIter.next();
                    			header.addAttribute(attr.getLocalName(), attr.getAttributeValue(), attr.getNamespace());
                    		}
                    		Iterator nsIter  = element.getAllDeclaredNamespaces();
                    		while (nsIter.hasNext()) {
                    			OMNamespace ns =  (OMNamespace) nsIter.next();
                    			header.declareNamespace(ns);
                    		}
                    		// retrieve all child nodes (including any text nodes)
                    		// and re-attach to header block
                    		Iterator children = element.getChildren();
                    		while (children.hasNext()) {
                    			OMNode child = (OMNode)children.next();
                    			children.remove();
                    			header.addChild(child);
                    		}

                    		element.detach();

                    		soapHeader.build();

                    		header.setProcessed();

                    	}

                        if(header.isProcessed()) {
                            processedHeaderQNames.add(element.getQName());
                        }
                    }

                }
                XMLStreamReader reader = ((OMElement) doc.getDocumentElement())
                        .getXMLStreamReader();
                SOAPModelBuilder stAXSOAPModelBuilder = OMXMLBuilderFactory.createStAXSOAPModelBuilder(
                        reader);
                SOAPEnvelope envelope = stAXSOAPModelBuilder.getSOAPEnvelope();

                //Set the processed flag of the processed headers
                SOAPHeader header = envelope.getHeader();
                for (Iterator iter = processedHeaderQNames.iterator(); iter
                        .hasNext();) {
                    QName name = (QName) iter.next();
                    Iterator<SOAPHeaderBlock> omKids = header.getHeaderBlocksWithName(name);
                    if(omKids.hasNext()) {
                        omKids.next().setProcessed();
                    }
                }

                envelope.build();

                return envelope;

            } catch (FactoryConfigurationError e) {
		throw new WSSecurityException(
                    WSSecurityException.ErrorCode.FAILURE, e.getMessage());
            }
        } else {
            try {
                ByteArrayOutputStream os = new ByteArrayOutputStream();
                XMLUtils.elementToStream(doc.getDocumentElement(), os);
                ByteArrayInputStream bais =  new ByteArrayInputStream(os.toByteArray());

                SOAPModelBuilder stAXSOAPModelBuilder = OMXMLBuilderFactory.createSOAPModelBuilder(bais, null);
                return stAXSOAPModelBuilder.getSOAPEnvelope();
            } catch (Exception e) {
		throw new WSSecurityException(
                    WSSecurityException.ErrorCode.FAILURE, e, e.getMessage());
            }
        }
    }

    /**
     * TODO we need to move these common code to a new module. Otherwise code will be duplicated.
     * We cannot use following method from rampart-core as it creates a cyclic dependency. Therefore we have
     * to live with following.
	 * Creates a DOM Document using the SOAP Envelope.
	 * @param env An org.apache.axiom.soap.SOAPEnvelope instance
	 * @return Returns the DOM Document of the given SOAP Envelope.
	 * @throws Exception If an error occurred during conversion.
	 */
	public static Document getDocumentFromSOAPEnvelope(SOAPEnvelope env, boolean useDoom)
			throws WSSecurityException {
		try {
            if(env instanceof Element) {
                Element element = (Element)env;
                Document document = element.getOwnerDocument();
                // For outgoing messages, Axis2 only creates the SOAPEnvelope, but no document. If
                // the Axiom implementation also supports DOM, then the envelope (seen as a DOM
                // element) will have an owner document, but the document and the envelope have no
                // parent-child relationship. On the other hand, the input expected by WSS4J is
                // a document with the envelope as document element. Therefore we need to set the
                // envelope as document element on the owner document.
                if (element.getParentNode() != document) {
                    document.appendChild(element);
                }
                // If the Axiom implementation supports DOM, then it is possible/likely that the
                // DOM API was used to create the object model (or parts of it). In this case, the
                // object model is not necessarily well formed with respect to namespaces because
                // DOM doesn't generate namespace declarations automatically. This is an issue
                // because WSS4J/Santuario expects that all namespace declarations are present.
                // If this is not the case, then signature values or encryptions will be incorrect.
                // To avoid this, we normalize the document. Note that if we disable the other
                // normalizations supported by DOM, this is generally not a heavy operation.
                // In particular, the Axiom implementation is not required to expand the object
                // model (including OMSourcedElements) because the Axiom builder is required to
                // perform namespace repairing, so that no modifications to unexpanded parts of
                // the message are required.
                DOMConfiguration domConfig = document.getDomConfig();
                domConfig.setParameter("split-cdata-sections", Boolean.FALSE);
                domConfig.setParameter("well-formed", Boolean.FALSE);
                domConfig.setParameter("namespaces", Boolean.TRUE);
                document.normalizeDocument();
                return document;
            }

            if (useDoom) {
                env.build();

                // Workaround to prevent a bug in AXIOM where
                // there can be an incomplete OMElement as the first child body
                OMElement firstElement = env.getBody().getFirstElement();
                if (firstElement != null) {
                    firstElement.build();
                }

                //Get processed headers
                SOAPHeader soapHeader = env.getHeader();
                ArrayList processedHeaderQNames = new ArrayList();
                if(soapHeader != null) {
                    Iterator headerBlocs = soapHeader.getChildElements();
                    while (headerBlocs.hasNext()) {
                        SOAPHeaderBlock element = (SOAPHeaderBlock) headerBlocs.next();
                        if(element.isProcessed()) {
                            processedHeaderQNames.add(element.getQName());
                        }
                    }
                }

                SOAPModelBuilder stAXSOAPModelBuilder = OMXMLBuilderFactory.createStAXSOAPModelBuilder(
                        OMAbstractFactory.getMetaFactory(OMAbstractFactory.FEATURE_DOM),
                        env.getXMLStreamReader());
                SOAPEnvelope envelope = (stAXSOAPModelBuilder)
                        .getSOAPEnvelope();
                envelope.getParent().build();

                //Set the processed flag of the processed headers
                SOAPHeader header = envelope.getHeader();
                for (Iterator iter = processedHeaderQNames.iterator(); iter
                        .hasNext();) {
                    QName name = (QName) iter.next();
                    Iterator<SOAPHeaderBlock> omKids = header.getHeaderBlocksWithName(name);
                    if(omKids.hasNext()) {
                        omKids.next().setProcessed();
                    }
                }

                Element envElem = (Element) envelope;
                return envElem.getOwnerDocument();
            } else {
                ByteArrayOutputStream baos = new ByteArrayOutputStream();
                env.build();
                env.serialize(baos);
                ByteArrayInputStream bais = new ByteArrayInputStream(baos
                        .toByteArray());
                DocumentBuilderFactory factory = DocumentBuilderFactory
                        .newInstance();
                factory.setNamespaceAware(true);
                return factory.newDocumentBuilder().parse(bais);
            }
		} catch (Exception e) {
			throw new WSSecurityException(
					WSSecurityException.ErrorCode.FAILURE, e, "Error in converting SOAP Envelope to Document");
		}
	}
}
