package com.wechat.sdk.utils;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Map;

import javax.xml.namespace.QName;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

/**
 * xpath解析xml
 * <pre>
 *     文档地址：
 *     http://www.w3school.com.cn/xpath/index.asp
 * </pre>
 */
public class XmlHelper {
    private final XPath path;
    private final Document doc;

    private XmlHelper(InputSource inputSource) throws ParserConfigurationException, SAXException, IOException {
//        DocumentBuilderFactory dbf = getDocumentBuilderFactory();
//        DocumentBuilder db = dbf.newDocumentBuilder();
        DocumentBuilder db = getSafeDocument();// 使用安全的Dom解析器
        doc = db.parse(inputSource);
        path = getXPathFactory().newXPath();
    }
    private DocumentBuilder getSafeDocument() throws ParserConfigurationException {
    	DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
    	String FEATURE = null;
    	try {
    		// This is the PRIMARY defense. If DTDs (doctypes) are disallowed, almost all XML entity attacks are prevented
    		// Xerces 2 only - http://xerces.apache.org/xerces2-j/features.html#disallow-doctype-decl
    		FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
    		dbf.setFeature(FEATURE, true);
    		
    		// If you can't completely disable DTDs, then at least do the following:
    		// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-general-entities
    		// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-general-entities
    		// JDK7+ - http://xml.org/sax/features/external-general-entities 
    		FEATURE = "http://xml.org/sax/features/external-general-entities";
    		dbf.setFeature(FEATURE, false);
    		
    		// Xerces 1 - http://xerces.apache.org/xerces-j/features.html#external-parameter-entities
    		// Xerces 2 - http://xerces.apache.org/xerces2-j/features.html#external-parameter-entities
    		// JDK7+ - http://xml.org/sax/features/external-parameter-entities 
    		FEATURE = "http://xml.org/sax/features/external-parameter-entities";
    		dbf.setFeature(FEATURE, false);
    		
    		// Disable external DTDs as well
    		FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
    		dbf.setFeature(FEATURE, false);
    		
    		// and these as well, per Timothy Morgan's 2014 paper: "XML Schema, DTD, and Entity Attacks"
    		dbf.setXIncludeAware(false);
    		dbf.setExpandEntityReferences(false);
    		
    		// And, per Timothy Morgan: "If for some reason support for inline DOCTYPEs are a requirement, then 
    		// ensure the entity settings are disabled (as shown above) and beware that SSRF attacks
    		// (http://cwe.mitre.org/data/definitions/918.html) and denial 
    		// of service attacks (such as billion laughs or decompression bombs via "jar:") are a risk."
    		
    		// remaining parser logic
    	} catch (ParserConfigurationException e) {
    		// This should catch a failed setFeature feature
//    		logger.info("ParserConfigurationException was thrown. The feature '" +
//    		FEATURE + "' is probably not supported by your XML processor.");
    	}
    	return dbf.newDocumentBuilder();
    }

    private static XmlHelper create(InputSource inputSource) {
        try {
            return new XmlHelper(inputSource);
        } catch (ParserConfigurationException e) {
            throw new RuntimeException(e);
        } catch (SAXException e) {
            throw new RuntimeException(e);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static XmlHelper of(InputStream is) {
        InputSource inputSource = new InputSource(is);
        return create(inputSource);
    }

    public static XmlHelper of(File file) {
        InputSource inputSource = new InputSource(file.toURI().toASCIIString());
        return create(inputSource);
    }

    public static XmlHelper of(String xmlStr) {
        StringReader sr = new StringReader(xmlStr);
        InputSource inputSource = new InputSource(sr);
        XmlHelper xmlHelper = create(inputSource);
        IOUtils.closeQuietly(sr);
        return xmlHelper;
    }

    private Object evalXPath(String expression, Object item, QName returnType) {
        item = null == item ? doc : item;
        try {
            return path.evaluate(expression, item, returnType);
        } catch (XPathExpressionException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获取String
     * @param expression 路径
     * @return String
     */
    public String getString(String expression) {
        return (String) evalXPath(expression, null, XPathConstants.STRING);
    }

    /**
     * 获取Boolean
     * @param expression 路径
     * @return String
     */
    public Boolean getBoolean(String expression) {
        return (Boolean) evalXPath(expression, null, XPathConstants.BOOLEAN);
    }

    /**
     * 获取Number
     * @param expression 路径
     * @return {Number}
     */
    public Number getNumber(String expression) {
        return (Number) evalXPath(expression, null, XPathConstants.NUMBER);
    }

    /**
     * 获取某个节点
     * @param expression 路径
     * @return {Node}
     */
    public Node getNode(String expression) {
        return (Node) evalXPath(expression, null, XPathConstants.NODE);
    }

    /**
     * 获取子节点
     * @param expression 路径
     * @return NodeList
     */
    public NodeList getNodeList(String expression) {
        return (NodeList) evalXPath(expression, null, XPathConstants.NODESET);
    }


    /**
     * 获取String
     * @param node 节点
     * @param expression 相对于node的路径
     * @return String
     */
    public String getString(Object node, String expression) {
        return (String) evalXPath(expression, node, XPathConstants.STRING);
    }

    /**
     * 获取
     * @param node 节点
     * @param expression 相对于node的路径
     * @return String
     */
    public Boolean getBoolean(Object node, String expression) {
        return (Boolean) evalXPath(expression, node, XPathConstants.BOOLEAN);
    }

    /**
     * 获取
     * @param node 节点
     * @param expression 相对于node的路径
     * @return {Number}
     */
    public Number getNumber(Object node, String expression) {
        return (Number) evalXPath(expression, node, XPathConstants.NUMBER);
    }

    /**
     * 获取某个节点
     * @param node 节点
     * @param expression 路径
     * @return {Node}
     */
    public Node getNode(Object node, String expression) {
        return (Node) evalXPath(expression, node, XPathConstants.NODE);
    }

    /**
     * 获取子节点
     * @param node 节点
     * @param expression 相对于node的路径
     * @return NodeList
     */
    public NodeList getNodeList(Object node, String expression) {
        return (NodeList) evalXPath(expression, node, XPathConstants.NODESET);
    }

    /**
     * 针对没有嵌套节点的简单处理
     * @return map集合
     */
    public Map<String, String> toMap() {
        Element root = doc.getDocumentElement();
        Map<String, String> params = new HashMap<String, String>();

        // 将节点封装成map形式
        NodeList list = root.getChildNodes();
        for (int i = 0; i < list.getLength(); i++) {
            Node node = list.item(i);
            params.put(node.getNodeName(), node.getTextContent());
        }
        // 含有空白符会生成一个#text参数
        params.remove("#text");
        return params;
    }

    private static DocumentBuilderFactory getDocumentBuilderFactory(){
        return XmlHelper.XmlHelperHolder.documentBuilderFactory;
    }

    private static XPathFactory getXPathFactory() {
        return  XmlHelper.XmlHelperHolder.xPathFactory;
    }

    /**
     * 内部类单例
     */
    private static class XmlHelperHolder {
        private static DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
        private static XPathFactory xPathFactory = XPathFactory.newInstance();
    }

}
