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

import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.jorphan.logging.LoggingManager;
import org.apache.log.Logger;
import org.w3c.dom.Document;
import org.w3c.tidy.Tidy;
import org.xml.sax.ErrorHandler;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * This class provides a few utility methods for dealing with XML/XPath.
 */
public class XPathUtil {
	private static final Logger log = LoggingManager.getLoggerForClass();

	private XPathUtil() {
		super();
	}

	private static DocumentBuilderFactory documentBuilderFactory;

	/**
	 * Returns a suitable document builder factory.
	 * Caches the factory in case the next caller wants the same options.
	 * 
	 * @param validate should the parser validate documents?
	 * @param whitespace should the parser eliminate whitespace in element content?
	 * @param namespace should the parser be namespace aware?
	 * 
	 * @return javax.xml.parsers.DocumentBuilderFactory
	 */
	private static synchronized DocumentBuilderFactory makeDocumentBuilderFactory(boolean validate, boolean whitespace,
			boolean namespace) {
		if (XPathUtil.documentBuilderFactory == null || documentBuilderFactory.isValidating() != validate
				|| documentBuilderFactory.isNamespaceAware() != namespace
				|| documentBuilderFactory.isIgnoringElementContentWhitespace() != whitespace) {
			// configure the document builder factory
			documentBuilderFactory = DocumentBuilderFactory.newInstance();
			documentBuilderFactory.setValidating(validate);
			documentBuilderFactory.setNamespaceAware(namespace);
			documentBuilderFactory.setIgnoringElementContentWhitespace(whitespace);
		}
		return XPathUtil.documentBuilderFactory;
	}

	/**
	 * Create a DocumentBuilder using the makeDocumentFactory func.
	 * 
	 * @param validate should the parser validate documents?
	 * @param whitespace should the parser eliminate whitespace in element content?
	 * @param namespace should the parser be namespace aware?
	 * @return document builder
	 * @throws ParserConfigurationException
	 */
	public static synchronized DocumentBuilder makeDocumentBuilder(boolean validate, boolean whitespace, boolean namespace)
			throws ParserConfigurationException {
	    // N.B. the factory is re-usable, but not necessarily thread-safe, so
	    // the method is synchronized to protect the creation of the builder
		DocumentBuilder builder = makeDocumentBuilderFactory(validate, whitespace, namespace).newDocumentBuilder();
		builder.setErrorHandler(new MyErrorHandler(validate, false));
		return builder;
	}

	/**
	 * Utility function to get new Document
	 * 
	 * @param stream
	 *            Document Input stream
	 * @param validate
	 *            Validate Document (not Tidy)
	 * @param whitespace
	 *            Element Whitespace (not Tidy)
	 * @param namespace
	 *            Is Namespace aware.
	 * @param tolerant
	 *            Is tolerant - i.e. use the Tidy parser
	 *
	 * @return document
	 * @throws ParserConfigurationException
	 * @throws IOException
	 * @throws SAXException
	 * @throws TidyException
	 */
	public static Document makeDocument(InputStream stream, boolean validate, boolean whitespace, boolean namespace,
			boolean tolerant) throws ParserConfigurationException, SAXException, IOException, TidyException {
		return makeDocument(stream, validate, whitespace, namespace, tolerant, true, false, false, false);
		
	}

	/**
     * Utility function to get new Document
     * 
     * @param stream - Document Input stream
     * @param validate - Validate Document (not Tidy)
     * @param whitespace - Element Whitespace (not Tidy)
     * @param namespace - Is Namespace aware. (not Tidy)
     * @param tolerant - Is tolerant - i.e. use the Tidy parser
     * @param quiet - set Tidy quiet
     * @param showWarnings - set Tidy warnings
     * @param report_errors - throw TidyException if Tidy detects an error
     * @return document
     * @throws ParserConfigurationException
     * @throws SAXException
     * @throws IOException
     * @throws TidyException
     */
    public static Document makeDocument(InputStream stream, boolean validate, boolean whitespace, boolean namespace,
    		boolean tolerant, boolean quiet, boolean showWarnings, boolean report_errors)
            throws ParserConfigurationException, SAXException, IOException, TidyException {
                return makeDocument(stream, validate, whitespace, namespace,
                        tolerant, quiet, showWarnings, report_errors, false);
            }

    /**
	 * Utility function to get new Document
	 * 
	 * @param stream - Document Input stream
	 * @param validate - Validate Document (not Tidy)
	 * @param whitespace - Element Whitespace (not Tidy)
	 * @param namespace - Is Namespace aware. (not Tidy)
	 * @param tolerant - Is tolerant - i.e. use the Tidy parser
	 * @param quiet - set Tidy quiet
	 * @param showWarnings - set Tidy warnings
	 * @param report_errors - throw TidyException if Tidy detects an error
	 * @param isXml - is document already XML (Tidy only)
	 * @return document
	 * @throws ParserConfigurationException
	 * @throws SAXException
	 * @throws IOException
	 * @throws TidyException
	 */
	public static Document makeDocument(InputStream stream, boolean validate, boolean whitespace, boolean namespace,
			boolean tolerant, boolean quiet, boolean showWarnings, boolean report_errors, boolean isXml)
	        throws ParserConfigurationException, SAXException, IOException, TidyException {
		Document doc;
		if (tolerant) {
			doc = tidyDoc(stream, quiet, showWarnings, report_errors, isXml);
		} else {
			doc = makeDocumentBuilder(validate, whitespace, namespace).parse(stream);
		}
		return doc;
	}

	/**
	 * Create a document using Tidy
	 * 
	 * @param stream - input
	 * @param quiet - set Tidy quiet?
	 * @param showWarnings - show Tidy warnings?
	 * @param report_errors - log errors and throw TidyException?
	 * @param isXML - treat document as XML?
	 * @return the document
	 * 
	 * @throws TidyException if a ParseError is detected and report_errors is true
	 */
	private static Document tidyDoc(InputStream stream, boolean quiet, boolean showWarnings, boolean report_errors, 
	        boolean isXML) throws TidyException {
        StringWriter sw = new StringWriter();
		Tidy tidy = makeTidyParser(quiet, showWarnings, isXML, sw);
		Document doc = tidy.parseDOM(stream, null);
		doc.normalize();
		if (tidy.getParseErrors() > 0) {
			if (report_errors) {
	            log.error("TidyException: " + sw.toString());    
			    throw new TidyException(tidy.getParseErrors(),tidy.getParseWarnings());
			}
		    log.warn("Tidy errors: " + sw.toString());
		}
		return doc;
	}

	/**
	 * Create a Tidy parser with the specified settings.
	 * 
	 * @param quiet - set the Tidy quiet flag?
	 * @param showWarnings - show Tidy warnings?
	 * @param isXml - treat the content as XML?
	 * @param stringWriter - if non-null, use this for Tidy errorOutput
	 * @return the Tidy parser
	 */
    public static Tidy makeTidyParser(boolean quiet, boolean showWarnings, boolean isXml, StringWriter stringWriter) {
        Tidy tidy = new Tidy();
        tidy.setCharEncoding(org.w3c.tidy.Configuration.UTF8);
        tidy.setQuiet(quiet);
        tidy.setShowWarnings(showWarnings);
        tidy.setMakeClean(true);
        tidy.setXmlTags(isXml);
        if (stringWriter != null) {
            tidy.setErrout(new PrintWriter(stringWriter));
        }
        return tidy;
    }
 
	static class MyErrorHandler implements ErrorHandler {
		private final boolean val, tol;

		private final String type;

		MyErrorHandler(boolean validate, boolean tolerate) {
			val = validate;
			tol = tolerate;
			type = "Val=" + val + " Tol=" + tol;
		}

		public void warning(SAXParseException ex) throws SAXException {
			log.info("Type=" + type + " " + ex);
			if (val && !tol){
				throw new SAXException(ex);
			}
		}

		public void error(SAXParseException ex) throws SAXException {
			log.warn("Type=" + type + " " + ex);
			if (val && !tol) {
				throw new SAXException(ex);
			}
		}

		public void fatalError(SAXParseException ex) throws SAXException {
			log.error("Type=" + type + " " + ex);
			if (val && !tol) {
				throw new SAXException(ex);
			}
		}
	}
}