/* XMLDoc.java
 * This class serves as a single point for constructing an XML Document object given the file name.
 *
 *@author Sada, Manmohan
 *@date March 2003
 *@version 1.4
 *@company WERU-GMPRC-USDA
 */
package usda.weru.mcrew;

import de.schlichtherle.truezip.file.TFile;

import de.schlichtherle.truezip.file.TFileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import javax.xml.parsers.*;

import org.apache.log4j.Logger;
import org.w3c.dom.*;
import org.w3c.dom.traversal.*;
import org.xml.sax.helpers.*;
import org.xml.sax.EntityResolver;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * Class that declares the different types of documents needed for XML documents 
 * to be proccessed and be able to parse through correctly in the application.
 */
public class XMLDoc {

    private static final Logger LOGGER = Logger.getLogger(XMLDoc.class);
    /**
     * This array of string holds the different DTD file names to be used as reference 
     * while processng the XML documents in order to structure the WEPS/MCREW data
     * to be used for analysis purpose in the application.
     */
    private static final String[] defaultDTDfiles = {"identity.dtd", "comment.dtd",
        "operation_db.dtd", "crop_db.dtd", "weps_man_db.dtd"};
    /**
     * This DTD file describes the type of data for each element that would be needed
     * for the WEPS management database.
     */
    private static final String sWepsManDBDTDfile = "weps_man_db.dtd";
    /**
     * This XSL style sheet file for the WEPS management data that describes the
     * way it needs to be tranformed to show each element from the XML file on an
     * HTML page.
     */
    private static final String sWepsManDBXSLfile = "weps_man_db.xsl";
    /**
     * This XSL style sheet file for the operation database that describes the
     * way it needs to be tranformed to show each element from the XML file on an
     * HTML page.
     */
    private static final String sOperationDBXSLfile = "operation_db.xsl";
    /**
     * This XSL style sheet file for the crop database that describes the
     * way it needs to be tranformed to show each element from the XML file on an
     * HTML page.
     */
    private static final String sCropDBXSLfile = "crop_db.xsl";
    /**
     * Tell what the target of the XML style sheet will be.
     */
    private static final String sStyleSheetTarget = "xml-stylesheet";
    /**
     * defines where the data for the style sheet would reside. Gives the link
     * for .html pages to be created during that time.
     */
    private static final String sStyleSheetData = "type=\"text/xsl\" href=";

    /**
     * Tells the index at which the operation DTD file in the array reside.
     */
    private static final int kOperationDBDTD = 2;
    /**
     * Tells the index at which the crop DTD file in the array reside.
     */
    private static final int kCropDBDTD = 3;
    /**
     * Tells the index at which the WEPS management DTD file in the array reside.
     */
    public static final int kWepsManDBDTD = 4;

    private static DocumentBuilder createBuilder() {
        try {
            DocumentBuilderFactory mFactory = DocumentBuilderFactory.newInstance();
            mFactory.setIgnoringElementContentWhitespace(true); // Ignore whitespace
            mFactory.setIgnoringComments(true); // Ignore comments
            mFactory.setValidating(true);
            mFactory.setNamespaceAware(true);

            DocumentBuilder mParser = mFactory.newDocumentBuilder();
            mParser.setErrorHandler(new McrewXMLHandler());
            mParser.setEntityResolver(new McrewEntityResolver());

            return mParser;
        } catch (ParserConfigurationException e) {
            LOGGER.error("Unable to create XML DocumentBuilder", e);
            return null;
        }
    }

    /**
     * Returns a newly constructed document object
     * @param pFilename Name to be used in creating/constructing a new DOM file.
     * @return The newly constructed DOM file
     */
    public static Document getDocument(String pFilename) {

        try {

            DocumentBuilder builder = createBuilder();

            if (builder != null) {
                TFile file = null;

                if ((new TFile(pFilename)).isAbsolute()) {
                    file = new TFile(pFilename);
                } else {
                    file = new TFile(".", pFilename);
                }

                TFileInputStream is = new TFileInputStream(file);

                Document doc = builder.parse(is);

                return doc;
            } else {
            }
        } catch (SAXException | IOException | IllegalArgumentException e) {
            LOGGER.error("Error reading file \"" + pFilename + "\"", e);
        }
        return null;
    }

    /**
     * Returns the document type for one of the operation, crop or management files
     */
    private static DocumentType getDocType(DOMImplementation domImpl, String pObjectName) {
        DocumentType docType = null;
        switch (pObjectName) {
            case XMLConstants.smanagement_template:
                docType = domImpl.createDocumentType(XMLConstants.sWepsManDBRootName, null, defaultDTDfiles[kWepsManDBDTD]);
                //return wepsmandbDocType;
                break;
            case XMLConstants.soperation:
                docType = domImpl.createDocumentType(XMLConstants.sOperationDBRootName, null, defaultDTDfiles[kOperationDBDTD]);
                //return wepsmandbDocType;
                break;
            case XMLConstants.scrop:
                docType = domImpl.createDocumentType(XMLConstants.sCropDBRootName, null, defaultDTDfiles[kCropDBDTD]);
                //return wepsmandbDocType;
                break;
            case XMLConstants.smanagement_skeleton:
                docType = domImpl.createDocumentType(XMLConstants.smanagement, null, null);
                break;
        }
        return docType;
    }

    /**
     * Creates an XML document with the required element name and data following the 
     * DTD and XSl guidelines for arranging the data in pre-described manner according 
     * to the standard DOM implementations.
     * @param pDocumentName The name with which the document should be created and saved.
     * @return The newly constructed DOM file
     */
    public static Document createDocument(String pDocumentName) {
        DocumentType docType;
        Document doc = null;
        ProcessingInstruction styleSheet;

        DocumentBuilder builder = createBuilder();
        if (builder == null) {
            return null;
        }
        DOMImplementation domImpl = builder.getDOMImplementation();
        if (domImpl == null) {
            return null;
        }

        docType = getDocType(domImpl, pDocumentName);
        if (docType == null) {
            //System.err.println("XMLDoc:createDocument():" + "DocType for " + pDocumentName + " is null");
            return null;
        }
        switch (pDocumentName) {
            case XMLConstants.smanagement_template:
                doc = domImpl.createDocument(null, XMLConstants.sWepsManDBRootName, docType);
                styleSheet = doc.createProcessingInstruction(sStyleSheetTarget, sStyleSheetData
                        + "\"" + sWepsManDBXSLfile + "\"");
                break;
            case XMLConstants.soperation:
                doc = domImpl.createDocument(null, XMLConstants.sOperationDBRootName, docType);
                styleSheet = doc.createProcessingInstruction(sStyleSheetTarget, sStyleSheetData
                        + "\"" + sOperationDBXSLfile + "\"");
                break;
            case XMLConstants.scrop:
                doc = domImpl.createDocument(null, XMLConstants.sCropDBRootName, docType);
                styleSheet = doc.createProcessingInstruction(sStyleSheetTarget, sStyleSheetData
                        + "\"" + sCropDBXSLfile + "\"");
                break;
            case XMLConstants.smanagement_skeleton:
                doc = domImpl.createDocument(null, XMLConstants.smanagement, docType);
                styleSheet = null;
                break;
            default:
                return null;
        }

        Node root = doc.getDocumentElement();
        if (styleSheet != null) {
            doc.insertBefore(styleSheet, root);
        }

        return doc;
    }

    /**
     * This method gives the text value of the text child for a given Node.
     * Eg. For a node testNode, use this function to extract its text data as
     * XMLDoc.getTextData(testNode);
     * @param pNode The node whose text data is requested.
     * @return Thetext data value itself.
     */
    public static String getTextData(Node pNode) {
        DocumentTraversal traversable;

        if (pNode.getOwnerDocument() == null) {
            traversable = (DocumentTraversal) pNode;
        } else {
            traversable = (DocumentTraversal) pNode.getOwnerDocument();
        }

        TreeWalker walker = traversable.createTreeWalker(pNode, NodeFilter.SHOW_ALL, null, false);
        Node nodeChild = walker.firstChild(); // nodeChild is 'identity' or 'actioname' or 'paramdefn'
        if (nodeChild != null) {
            if (nodeChild.hasChildNodes()) {
                //System.out.println("The node "+ pNode.getNodeName()+
                //" seems to have child nodes and not just a text node" + " @XMLDoc:getTextData()");
                return null;
            }
            String textData = nodeChild.getNodeValue();
            return XMLDoc.getStringFromXMLSafeString(textData.trim()); // Remove any adjacent empty spaces
        }
        //System.out.println("The node "+ pNode.getNodeName()+ " does not have any text data node" + " @XMLDoc:getTextData()");
        return null;
    }

    /**
     * This is a convenience method to set the text data of a text child for a given Node.
     * A new node can only be created w.r.t a Document, so we have the document parameter 
     * passed as argument to this method.
     * @param pNode Thenode whose data is to be set to the new value.
     * @param pText The new value passed as argument.
     * @param doc The document w.r.t whom the node is being created and the value set.
     */
    public static void setTextData(Node pNode, String pText, Document doc) {
        //System.out.println("XMLDoc :setTextData : The node name is : " + pNode.getNodeName()
        //+ "   The Node Value  is : " + pText);
        Node textNode = doc.createTextNode(getXMLSafeString(pText));
        pNode.appendChild(textNode);

    }

    /**
     *
     * @param s
     * @return
     */
    public static String getXMLSafeString(String s) {
        if (s == null) {
            return null;
        }
        String safe = s;
        safe = safe.replaceAll("&", "&amp;");
        safe = safe.replaceAll("\"", "&quot;");
        safe = safe.replaceAll("'", "&apos;");
        safe = safe.replaceAll("<", "&lt;");
        safe = safe.replaceAll(">", "&gt;");
        return safe;
    }

    /**
     *
     * @param safe
     * @return
     */
    public static String getStringFromXMLSafeString(String safe) {
        if (safe == null) {
            return null;
        }
        String s = safe;
        s = s.replaceAll("&quot;", "\"");
        s = s.replaceAll("&apos;", "'");
        s = s.replaceAll("&lt;", "<");
        s = s.replaceAll("&gt;", ">");
        s = s.replaceAll("&amp;", "&");
        return s;
    }
}

class McrewXMLHandler extends DefaultHandler {

    private static final Logger LOGGER = Logger.getLogger(XMLDoc.class);
    public McrewXMLHandler() {
        super();
    }

    @Override
    public void fatalError(SAXParseException exception) {
        //System.out.println("McrewXMLHandler:fatalError() ");

    }
    
    @Override
    public void error(SAXParseException exc)
    {
        LOGGER.error(exc);
 
    }
}

/* EntityResolver
 * Resolves the entityies i.e currrenlty this resolves all dtd refences
 */
class McrewEntityResolver implements EntityResolver {

    @Override
    public InputSource resolveEntity(String publicId, String systemId) {

        if (systemId == null) {
            //System.err.println("McrewEntityResolver:resolveEntiity() " + "SystemId is null!!");	
            return null;
        }

        String configDirPath = MCREWConfig.getConfigDir();

        TFile file = new TFile(systemId);
        String resolveFileName = file.getName();

        TFile file2 = new TFile(configDirPath, resolveFileName);
        if (file2.exists()) {
            try {
                return new InputSource(new java.io.FileInputStream(file2));
            } catch (FileNotFoundException ex) {
                //I've already checked for this.
            }
        }

        return null;
    }
} //end of class MCrewEntityResolver

