/*
 * 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.struts2.util;

import org.apache.struts2.ActionContext;
import org.apache.struts2.ObjectFactory;
import org.apache.struts2.util.location.Location;
import org.apache.struts2.util.location.LocationAttributes;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXNotRecognizedException;
import org.xml.sax.SAXNotSupportedException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.sax.SAXTransformerFactory;
import javax.xml.transform.sax.TransformerHandler;
import java.util.Map;

/**
 * Helper class to create and retrieve information from location-enabled
 * DOM-trees.
 *
 * @since 1.2
 */
public class DomHelper {

    private static final Logger LOG = LogManager.getLogger(DomHelper.class);

    public static Location getLocationObject(Element element) {
        return LocationAttributes.getLocation(element);
    }

    /**
     * Creates a W3C Document that remembers the location of each element in
     * the source file. The location of element nodes can then be retrieved
     * using the {@link #getLocationObject(Element)} method.
     *
     * @param inputSource the inputSource to read the document from
     * @return the W3C Document
     */
    public static Document parse(InputSource inputSource) {
        return parse(inputSource, null);
    }


    /**
     * Creates a W3C Document that remembers the location of each element in
     * the source file. The location of element nodes can then be retrieved
     * using the {@link #getLocationObject(Element)} method.
     *
     * @param inputSource the inputSource to read the document from
     * @param dtdMappings a map of DTD names and public ids
     * @return the W3C Document
     */
    public static Document parse(InputSource inputSource, Map<String, String> dtdMappings) {

        SAXParserFactory factory = null;
        String parserProp = System.getProperty("xwork.saxParserFactory");
        if (parserProp != null) {
            try {
                ObjectFactory objectFactory = ActionContext.getContext().getContainer().getInstance(ObjectFactory.class);
                Class<?> clazz = objectFactory.getClassInstance(parserProp);
                factory = (SAXParserFactory) clazz.getDeclaredConstructor().newInstance();
            } catch (Exception e) {
                LOG.error("Unable to load saxParserFactory set by system property 'xwork.saxParserFactory': {}", parserProp, e);
            }
        }

        if (factory == null) {
            factory = SAXParserFactory.newInstance();
        }

        try {
            factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
        } catch (ParserConfigurationException | SAXNotRecognizedException | SAXNotSupportedException e) {
            throw new StrutsException("Unable to disable resolving external entities!", e);
        }

        factory.setValidating((dtdMappings != null));
        factory.setNamespaceAware(true);

        SAXParser parser;
        try {
            parser = factory.newSAXParser();
        } catch (Exception ex) {
            throw new StrutsException("Unable to create SAX parser", ex);
        }


        DOMBuilder builder = new DOMBuilder();

        // Enhance the sax stream with location information
        ContentHandler locationHandler = new LocationAttributes.Pipe(builder);

        try {
            parser.parse(inputSource, new StartHandler(locationHandler, dtdMappings));
        } catch (Exception ex) {
            throw new StrutsException(ex);
        }

        return builder.getDocument();
    }

    /**
     * The <code>DOMBuilder</code> is a utility class that will generate a W3C
     * DOM Document from SAX events.
     *
     * @author <a href="mailto:cziegeler@apache.org">Carsten Ziegeler</a>
     */
    static public class DOMBuilder implements ContentHandler {

        /**
         * The default transformer factory shared by all instances
         */
        protected static SAXTransformerFactory FACTORY;

        /**
         * The transformer factory
         */
        protected SAXTransformerFactory factory;

        /**
         * The result
         */
        protected DOMResult result;

        /**
         * The parentNode
         */
        protected Node parentNode;

        protected ContentHandler nextHandler;

        static {
            String parserProp = System.getProperty("xwork.saxTransformerFactory");
            if (parserProp != null) {
                try {
                    ObjectFactory objectFactory = ActionContext.getContext().getContainer().getInstance(ObjectFactory.class);
                    Class<?> clazz = objectFactory.getClassInstance(parserProp);
                    FACTORY = (SAXTransformerFactory) clazz.getDeclaredConstructor().newInstance();
                } catch (Exception e) {
                    LOG.error("Unable to load SAXTransformerFactory set by system property 'xwork.saxTransformerFactory': {}", parserProp, e);
                }
            }

            if (FACTORY == null) {
                FACTORY = (SAXTransformerFactory) TransformerFactory.newInstance();
            }
        }

        /**
         * Construct a new instance of this DOMBuilder.
         */
        public DOMBuilder() {
            this((Node) null);
        }

        /**
         * Construct a new instance of this DOMBuilder.
         *
         * @param factory the SAX transformer factory
         */
        public DOMBuilder(SAXTransformerFactory factory) {
            this(factory, null);
        }

        /**
         * Constructs a new instance that appends nodes to the given parent node.
         *
         * @param parentNode the parent node
         */
        public DOMBuilder(Node parentNode) {
            this(null, parentNode);
        }

        /**
         * Construct a new instance of this DOMBuilder.
         *
         * @param factory    the SAX transformer factory
         * @param parentNode the parent node
         */
        public DOMBuilder(SAXTransformerFactory factory, Node parentNode) {
            this.factory = factory == null ? FACTORY : factory;
            this.parentNode = parentNode;
            setup();
        }

        /**
         * Setup this instance transformer and result objects.
         */
        private void setup() {
            try {
                TransformerHandler handler = this.factory.newTransformerHandler();
                nextHandler = handler;
                if (this.parentNode != null) {
                    this.result = new DOMResult(this.parentNode);
                } else {
                    this.result = new DOMResult();
                }
                handler.setResult(this.result);
            } catch (javax.xml.transform.TransformerException local) {
                throw new StrutsException("Fatal-Error: Unable to get transformer handler", local);
            }
        }

        /**
         * Return the newly built Document.
         *
         * @return the W3C Document
         */
        public Document getDocument() {
            if (this.result == null || this.result.getNode() == null) {
                return null;
            } else if (this.result.getNode().getNodeType() == Node.DOCUMENT_NODE) {
                return (Document) this.result.getNode();
            } else {
                return this.result.getNode().getOwnerDocument();
            }
        }

        public void setDocumentLocator(Locator locator) {
            nextHandler.setDocumentLocator(locator);
        }

        public void startDocument() throws SAXException {
            nextHandler.startDocument();
        }

        public void endDocument() throws SAXException {
            nextHandler.endDocument();
        }

        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException {
            nextHandler.startElement(uri, loc, raw, attrs);
        }

        public void endElement(String arg0, String arg1, String arg2) throws SAXException {
            nextHandler.endElement(arg0, arg1, arg2);
        }

        public void startPrefixMapping(String arg0, String arg1) throws SAXException {
            nextHandler.startPrefixMapping(arg0, arg1);
        }

        public void endPrefixMapping(String arg0) throws SAXException {
            nextHandler.endPrefixMapping(arg0);
        }

        public void characters(char[] arg0, int arg1, int arg2) throws SAXException {
            nextHandler.characters(arg0, arg1, arg2);
        }

        public void ignorableWhitespace(char[] arg0, int arg1, int arg2) throws SAXException {
            nextHandler.ignorableWhitespace(arg0, arg1, arg2);
        }

        public void processingInstruction(String arg0, String arg1) throws SAXException {
            nextHandler.processingInstruction(arg0, arg1);
        }

        public void skippedEntity(String arg0) throws SAXException {
            nextHandler.skippedEntity(arg0);
        }
    }

    public static class StartHandler extends DefaultHandler {

        private final ContentHandler nextHandler;
        private final Map<String, String> dtdMappings;

        /**
         * Create a filter that is chained to another handler.
         *
         * @param next        the next handler in the chain.
         * @param dtdMappings map of DTD mappings
         */
        public StartHandler(ContentHandler next, Map<String, String> dtdMappings) {
            nextHandler = next;
            this.dtdMappings = dtdMappings;
        }

        @Override
        public void setDocumentLocator(Locator locator) {
            nextHandler.setDocumentLocator(locator);
        }

        @Override
        public void startDocument() throws SAXException {
            nextHandler.startDocument();
        }

        @Override
        public void endDocument() throws SAXException {
            nextHandler.endDocument();
        }

        @Override
        public void startElement(String uri, String loc, String raw, Attributes attrs) throws SAXException {
            nextHandler.startElement(uri, loc, raw, attrs);
        }

        @Override
        public void endElement(String arg0, String arg1, String arg2) throws SAXException {
            nextHandler.endElement(arg0, arg1, arg2);
        }

        @Override
        public void startPrefixMapping(String arg0, String arg1) throws SAXException {
            nextHandler.startPrefixMapping(arg0, arg1);
        }

        @Override
        public void endPrefixMapping(String arg0) throws SAXException {
            nextHandler.endPrefixMapping(arg0);
        }

        @Override
        public void characters(char[] arg0, int arg1, int arg2) throws SAXException {
            nextHandler.characters(arg0, arg1, arg2);
        }

        @Override
        public void ignorableWhitespace(char[] arg0, int arg1, int arg2) throws SAXException {
            nextHandler.ignorableWhitespace(arg0, arg1, arg2);
        }

        @Override
        public void processingInstruction(String arg0, String arg1) throws SAXException {
            nextHandler.processingInstruction(arg0, arg1);
        }

        @Override
        public void skippedEntity(String arg0) throws SAXException {
            nextHandler.skippedEntity(arg0);
        }

        @Override
        public InputSource resolveEntity(String publicId, String systemId) {
            if (dtdMappings != null && dtdMappings.containsKey(publicId)) {
                String dtdFile = dtdMappings.get(publicId);
                return new InputSource(ClassLoaderUtil.getResourceAsStream(dtdFile, DomHelper.class));
            } else {
                LOG.warn("Local DTD is missing for publicID: {} - defined mappings: {}", publicId, dtdMappings);
            }
            return null;
        }

        @Override
        public void warning(SAXParseException exception) {
        }

        @Override
        public void error(SAXParseException exception) throws SAXException {
            LOG.error("{} at ({}:{}:{})", exception.getMessage(), exception.getPublicId(), exception.getLineNumber(), exception.getColumnNumber(), exception);
            throw exception;
        }

        @Override
        public void fatalError(SAXParseException exception) throws SAXException {
            LOG.fatal("{} at ({}:{}:{})", exception.getMessage(), exception.getPublicId(), exception.getLineNumber(), exception.getColumnNumber(), exception);
            throw exception;
        }
    }

}
