/**************************************************************
 *
 * 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.
 *
 *************************************************************/



import java.io.FileWriter;
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.io.PrintWriter;
import java.util.Vector;
import java.util.Properties;

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

import org.w3c.dom.Node;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
 *  This class will diff 2 Xml files.
 *
 *  @author   Stephen Mak
 */

public final class XmlDiff {

    private static final String PROPSFILE = "XmlDiff.properties";
    private static final String FILE1 = "XmlDiff.file1";
    private static final String FILE2 = "XmlDiff.file2";
    private static final String OUTPUT= "XmlDiff.output";
    private static final String IGNORE_TAGS= "XmlDiff.tags";

    private Properties props_ = null;
    private static PrintWriter writer_ = null;
    private String[] tags_ = null;
    private String file1_ = null;
    private String file2_ = null;

    /**
     *  Constructor.  Load the properties file.
     */

    public XmlDiff() throws IOException {

        Class c = this.getClass();
        InputStream is = c.getResourceAsStream(PROPSFILE);
        BufferedInputStream bis = new BufferedInputStream(is);
        props_ = new Properties();
        props_.load(bis);
        bis.close();

        String file1 = props_.getProperty(FILE1, "");
        String file2 = props_.getProperty(FILE2, "");
        String tagsString = props_.getProperty(IGNORE_TAGS, "");
        String output = props_.getProperty("debug.output", "System.out");
        setOutput(output);
        tags_ = parseTags(tagsString);
    }

    /**
     * diff 2 xml, but overwrite the property file's file1/2 setting with
     * the input argument
     */
    public boolean diff(String file1, String file2)  throws IOException {
        file1_ = file1;
        file2_ = file2;
        return diff();
    }

    public boolean diff() throws IOException {

        boolean result = false;

        writer_.println("parsing "+ file1_ + "...");
        // parse the Xml file
        Document doc1 = parseXml(file1_);

        writer_.println("parsing "+ file1_ + "...");
        Document doc2 = parseXml(file2_);

        if (doc1 != null && doc2 != null) {
          writer_.println("diffing "+ file1_ + " & " + file2_ + "...");
          result = compareNode(doc1, doc2);
        }
        return result;
    }

    private void diffLog(String errMsg, Node node1, Node node2) {

        String node1Str = "";
        String node2Str = "";

        if (node1 != null) {
            node1Str = "[Type]:" + nodeInfo(node1) +
                       " [Name]:" + node1.getNodeName();
            if (node1.getNodeValue() != null)
                node1Str += " [Value]:" + node1.getNodeValue();
        }

        if (node2 != null) {
            node2Str = "[Type]:" + nodeInfo(node2) +
                       " [Name]:" + node2.getNodeName();
            if (node2.getNodeValue() != null)
                node2Str += " [Value]:" + node2.getNodeValue();
        }

        writer_.println(errMsg);
        writer_.println("  Node1 - " + node1Str);
        writer_.println("  Node2 - " + node2Str);
    }

    private String nodeInfo(Node node) {

        String str = null;
        switch (node.getNodeType()) {

            case Node.ELEMENT_NODE:
                str = "ELEMENT";
                break;
            case Node.ATTRIBUTE_NODE:
                str = "ATTRIBUTE";
                break;
            case Node.TEXT_NODE:
                str = "TEXT";
                break;
            case Node.CDATA_SECTION_NODE:
                str = "CDATA_SECTION";
                break;
            case Node.ENTITY_REFERENCE_NODE:
                str = "ENTITY_REFERENCE";
                break;
            case Node.ENTITY_NODE:
                str = "ENTITY";
                break;
            case Node.PROCESSING_INSTRUCTION_NODE:
                str = "PROCESSING_INSTRUCTION";
                break;
            case Node.COMMENT_NODE:
                str = "COMMENT";
                break;
            case Node.DOCUMENT_NODE:
                str = "DOCUMENT";
                break;
            case Node.DOCUMENT_TYPE_NODE:
                str = "DOCUMENT_TYPE";
                break;
            case Node.DOCUMENT_FRAGMENT_NODE:
                str = "DOCUMENT_FRAGMENT";
                break;
            case Node.NOTATION_NODE:
                str = "NOTATION";
                break;
        }
        return str;
    }

    private boolean ignoreTag(String nodeName) {


        if (tags_ != null) {
            for (int i = 0; i < tags_.length; i++) {
                if (tags_[i].equals(nodeName))
                    return true;
            }
        }
        return false;
    }

    // for future use if we want to compare attributes
    private boolean attributesEqual(Node node1, Node node2) {
        return true;
    }

    private boolean compareNode(Node node1, Node node2) {
        boolean equal = false;

        while (true) {

            if (node1 == null && node2 == null) {
                equal = true;
                break;
            } else if (node1 == null || node2 == null) {
                diffLog("DIFF: one of the node is null", node1, node2);
                break;
            }

            if (node1.getNodeType() != node2.getNodeType()) {
                diffLog("DIFF: nodetype is different", node1, node2);
                break;
            }

            if (node1.getNodeName() == null && node2.getNodeName() == null) {
                // empty
            } else if (node1.getNodeName() == null ||
                       node2.getNodeName() == null) {
                diffLog("DIFF: one of the nodeName is null", node1, node2);
                break;
            } else if (!node1.getNodeName().equals(node2.getNodeName())) {
                diffLog("DIFF: nodeName is different", node1, node2);
                break;
            }

            if (ignoreTag(node1.getNodeName())) {
                diffLog("DIFF: Some tag(s) is ignored", node1, node2);
                equal = true;
                break;
            }

            if (node1.getNodeValue() == null && node2.getNodeValue() == null) {
                // empty
            } else if (node1.getNodeValue() == null ||
                       node2.getNodeValue() == null) {
                diffLog("DIFF: one of the nodevalue is null", node1, node2);
                break;
            } else if (!node1.getNodeValue().equals(node2.getNodeValue())) {
                diffLog("DIFF: nodeValue is different", node1, node2);
                break;
            }

            // try to compare attributes if necessary
            if (!attributesEqual(node1, node2))
                break;

            NodeList node1Children = node1.getChildNodes();
            NodeList node2Children = node2.getChildNodes();

            // number of children have to be the same
            if (node1Children == null && node2Children == null) {
                equal = true;
                break;
            }

            if (node1Children == null || node2Children == null) {
                diffLog("DIFF: one node's children is null", node1, node2);
                break;
            }

            if (node1Children.getLength() != node2Children.getLength())  {
                diffLog("DIFF: num of children is different", node1, node2);
                break;
            }

            // compare all the childrens
            equal = true;

            for (int i = 0; i < node1Children.getLength(); i++) {
                if (!compareNode(node1Children.item(i),
                                 node2Children.item(i))) {
                    equal = false;
                    break;
                }
            }
            break;
        }

        return equal;
    }

    private Document parseXml (String filename) throws IOException {

        Document w3cDocument = null;

        FileInputStream fis;

        try {
            fis = new FileInputStream(filename);
        } catch (FileNotFoundException ex) {
            ex.printStackTrace(writer_);
            writer_.println(ex.getMessage());
            return w3cDocument;
        }

        /** factory for DocumentBuilder objects */
        DocumentBuilderFactory factory = null;
        factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        factory.setValidating(false);

        /** DocumentBuilder object */
        DocumentBuilder builder = null;

        try {
            builder = factory.newDocumentBuilder();
        } catch (ParserConfigurationException ex) {
            ex.printStackTrace(writer_);
            writer_.println(ex.getMessage());
            return null;
        }


        builder.setErrorHandler(
            new org.xml.sax.ErrorHandler() {
                // ignore fatal errors (an exception is guaranteed)
                public void fatalError(SAXParseException e)
                    throws SAXException {
                    throw e;
                }

                public void error(SAXParseException e)
                    throws SAXParseException {
                    // make sure validation error is thrown.
                    throw e;
                }

                public void warning(SAXParseException e)
                    throws SAXParseException {
                }
            }
        );

        try {
            w3cDocument = builder.parse(fis);
            w3cDocument.getDocumentElement().normalize();
        } catch (SAXException ex) {
            ex.printStackTrace(writer_);
            writer_.println(ex.getMessage());
            return w3cDocument;
        }

        return w3cDocument;
    }

    private String [] parseTags(String tagsString) {
        Vector tagsVector = new Vector();
        if (tagsString.length() == 0)
            return null;

        int start = 0;
        int end = 0;
        // break the tag string into a vector of strings by words
        for (end = tagsString.indexOf(" ", start);
             end != -1 ;
             start = end + 1, end = tagsString.indexOf(" ", start)) {
            tagsVector.add(tagsString.substring(start,end));
        }

        tagsVector.add(tagsString.substring(start,tagsString.length()));

        // convert the vector to array
        String[] tags= new String[tagsVector.size()];
        tagsVector.copyInto(tags);

        return tags;
    }


    /**
     *  Set the output to the specified argument.
     *  This method is only used internally to prevent
     *  invalid string parameter.
     *
     *  @param   str   output specifier
     */
    private static void setOutput(String str) {

        if (writer_ == null) {

            if (str.equals("System.out")) {

                setOutput(System.out);

            } else if (str.equals("System.err")) {

                setOutput(System.err);

            } else {

                try {

                    setOutput(new FileWriter(str));

                } catch (IOException e) {

                    e.printStackTrace(System.err);
                }
            }
        }
    }

    /**
     *  Set the output to an OutputStream object.
     *
     *  @param   stream   OutputStream object
     */

    private static void setOutput(OutputStream stream) {

        setOutput(new OutputStreamWriter(stream));
    }

    /**
     *  Set the Writer object to manage the output.
     *
     *  @param   w   Writer object to write out
     */

    private static void setOutput(Writer w) {

        if (writer_ != null) {

            writer_.close();
        }

        writer_ = new PrintWriter(new BufferedWriter(w), true);
    }

    public static void main(String args[]) throws IOException {

        if (args.length != 0 && args.length != 2) {
            System.out.println("Usage: XmlDiff [<file1> <file2>].");
            return;
        }

        XmlDiff xmldiff = new XmlDiff();

        boolean same = false;
        if (args.length == 2) {
            same = xmldiff.diff(args[0], args[1]);
        } else {
            same = xmldiff.diff();
        }

        System.out.println("Diff result: " + same);
    }
}