/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 * 
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License. You can obtain
 * a copy of the License at https://glassfish.dev.java.net/public/CDDL+GPL.html
 * or glassfish/bootstrap/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 * 
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at glassfish/bootstrap/legal/LICENSE.txt.
 * Sun designates this particular file as subject to the "Classpath" exception
 * as provided by Sun in the GPL Version 2 section of the License file that
 * accompanied this code.  If applicable, add the following below the License
 * Header, with the fields enclosed by brackets [] replaced by your own
 * identifying information: "Portions Copyrighted [year]
 * [name of copyright owner]"
 * 
 * Contributor(s):
 * 
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.enterprise.tools.verifier;

import java.io.OutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileInputStream;
import java.io.OutputStreamWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Enumeration;
import java.util.Vector;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

import com.sun.enterprise.logging.LogDomains;
import com.sun.enterprise.server.Constants;
import com.sun.enterprise.tools.verifier.util.VerifierConstants;
import com.sun.enterprise.tools.verifier.StringManagerHelper;

/**
 * This class is responsible for generating the final output report file in xml and txt file.
 *
 * @author Sudipto Ghosh
 */

public class ReportHandler {

    private final String TEST = "test"; // NOI18N
    private final String TEST_NAME = "test-name"; // NOI18N
    private final String TEST_DESC = "test-description"; // NOI18N
    private final String TEST_ASSERTION = "test-assertion"; // NOI18N
    private final String STATIC_VER = "static-verification"; // NOI18N
    private final String FAILED = "failed"; // NOI18N
    private final String PASSED = "passed"; // NOI18N
    private final String NOTAPPLICABLE = "not-applicable"; // NOI18N
    private final String WARNING = "warning"; // NOI18N

    private final String FAILNUMBER = "failure-number"; // NOI18N
    private final String WARNINGNUMBER = "warning-number"; // NOI18N
    private final String ERRORNUMBER = "error-number"; // NOI18N
    private final String FAILCOUNT = "failure-count"; // NOI18N

    private final String ERROR = "error"; // NOI18N
    private final String ERROR_NAME = "error-name"; // NOI18N
    private final String ERROR_DESC = "error-description"; // NOI18N
    private final String XSL_FILE = "textFormatForVerifierSS"; // NOI18N
    private String outputFileStr = null;

    private Element rootNode = null;
    private Document document;
    private String textResult; // verification result in TEXT form.
    private ResultManager resultMgr;
    private FrameworkContext frameworkContext;
    private Logger logger = LogDomains.getLogger(LogDomains.AVK_VERIFIER_LOGGER);

    /**
     * Verifier uses this constructor to generate test report.
     * @param frameworkContext
     */
    public ReportHandler(FrameworkContext frameworkContext) {
        this.frameworkContext = frameworkContext;
        this.resultMgr = frameworkContext.getResultManager();

        String onlyJarFile = new File(frameworkContext.getJarFileName()).getName();
        String outputDirName = frameworkContext.getOutputDirName();
        outputDirName = (outputDirName == null) ?
                "" : outputDirName + File.separator;
        if (frameworkContext.isUseTimeStamp()) {
            SimpleDateFormat dateFormatter = new SimpleDateFormat(
                    "yyyyMMddhhmmss"); // NOI18N
            outputFileStr = outputDirName + onlyJarFile +
                    dateFormatter.format(new Date());
        } else
            outputFileStr = outputDirName + onlyJarFile;
    }

    /**
     * This api is called from verfier framework to generate the final report
     *
     * @throws IOException
     */
    public void generateAllReports() throws IOException {
        try {
            createResultsDocument(frameworkContext.getReportLevel());
            writeToXmlFile();
            writeToTxtFile();
            writeToConsole();
        } catch (IOException e) {
            throw  e;
        }
    }

    /**
     * writes the final output report file to the console.
     */
    private void writeToConsole() {
        if (frameworkContext.isUsingGui())
            return;
        if (frameworkContext.isBackend()) {
            logger.log(Level.SEVERE, textResult);
        } else {
            logger.log(Level.INFO, getClass().getName() + ".resultSummary",
                new Object[]{new Integer(resultMgr.getFailedCount()),
                             new Integer(resultMgr.getWarningCount()),
                             new Integer(resultMgr.getErrorCount())});
        }
        if((resultMgr.getFailedCount() + resultMgr.getWarningCount()
            + resultMgr.getErrorCount()) != 0
            || frameworkContext.getReportLevel() == VerifierConstants.ALL)
            logger.log(Level.INFO, getClass().getName() +
                ".LookInResultsTestAssertions", // NOI18N
                new Object[]{outputFileStr + ".txt"}); // NOI18N
        else
            logger.log(Level.INFO, getClass().getName() +
                ".LookInResultsTestAssertions1"); // NOI18N
    }

    /**
     * This api initializes the document object and calls generate apis to add results
     * to the document. Finally failureCount() api is called to add the error, failure
     * and warning counts to the document.
     *
     * @param reportLevel
     * @throws IOException
     */
    private void createResultsDocument(int reportLevel) throws IOException {
        createDOMTree();
        if (reportLevel != VerifierConstants.FAIL)
            addResultsToDocument(WARNING, resultMgr.getWarningResults());
        if (reportLevel == VerifierConstants.ALL) {
            addResultsToDocument(PASSED, resultMgr.getOkayResults());
            addResultsToDocument(NOTAPPLICABLE, resultMgr.getNaResults());
        }

        addResultsToDocument(FAILED, resultMgr.getFailedResults());
        Vector error = resultMgr.getError();
        if (!error.isEmpty()) {
            for (int i = 0; i < error.size(); i++) {
                LogRecord lr = (LogRecord) error.get(i);
                generateErrors(lr);
            }
        }
        failureCount();
    }

    /**
     * create the new Document tree with root node <static-verification>
     *
     * @throws IOException
     */
    private void createDOMTree() throws IOException {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        DocumentBuilder builder = null;
        try {
            builder = factory.newDocumentBuilder();
        } catch (Exception e) {
            IOException ioe = new IOException(e.getMessage());
            ioe.initCause(e);
            throw ioe;
        }
        document = builder.newDocument();
        rootNode = document.createElement(STATIC_VER);
        document.appendChild(rootNode);
    }

    /**
     * This api adds each result to the document tree based on the status.
     *
     * @param status
     * @param resultVector
     */
    private void addResultsToDocument(String status, Vector resultVector) {
        for (int i = 0; i < resultVector.size(); i++) {
            Enumeration en;
            Result r = (Result) resultVector.get(i);
            String moduleName = r.getModuleName();
            if (status == FAILED) {
                en = r.getErrorDetails().elements();
            } else if (status == WARNING) {
                en = r.getWarningDetails().elements();
            } else if (status == PASSED)
                en = r.getGoodDetails().elements();
            else
                en = r.getNaDetails().elements();
            createNode(moduleName, status);
            addToDocument(moduleName, status, r, en);
        }
    }

    /**
     * This api is used to add the error logs into the document.
     *
     * @param record
     */
    private void generateErrors(LogRecord record) {
        Element errorNode = null;
        //start adding nodes to document
        //check if the node already exists. If not, add it.
        NodeList nodeList = document.getElementsByTagName(ERROR);
        if (nodeList.getLength() == 0) {
            errorNode = document.createElement(ERROR);
            rootNode.appendChild(errorNode);
        } else {
            errorNode = (Element) nodeList.item(0); //there is only 1 node with tag of errorNode value
        }
        Element excepName = getTextNode(ERROR_NAME, record.getMessage());
        errorNode.appendChild(excepName);
        if (record.getThrown() != null) {
            Element excepDescr = getTextNode(ERROR_DESC,
                    writeStackTraceToFile(record.getThrown()));
            errorNode.appendChild(excepDescr);
        }
    }

    /**
     * This method is responsible for creating nodes in the DOM tree like
     * <p>
     * <ejb>
     *   <failed></failed>
     * </ejb>
     * where moduleName is ejb and status is failed.
     *
     * @param moduleName
     * @param status
     */
    private void createNode(String moduleName, String status) {
        NodeList nodeList;
        Element moduleNode;
        nodeList = document.getElementsByTagName(moduleName);
        if (nodeList.getLength() == 0) {
            moduleNode = document.createElement(moduleName);
            rootNode.appendChild(moduleNode);
        } else {
            moduleNode = (Element) nodeList.item(0); //there is only 1 node with tag of moduleNode value
        }
        nodeList = moduleNode.getChildNodes();
        Element statusNode = null;

        if (nodeList.getLength() == 0) {
            statusNode = document.createElement(status);
            moduleNode.appendChild(statusNode);
        } else {
            for (int j = 0; j < nodeList.getLength(); j++) {
                if (((Element) nodeList.item(j)).getTagName().equals(status)) {
                    statusNode = (Element) nodeList.item(j);
                    break;
                }
            }
            if (statusNode == null) {
                statusNode = document.createElement(status);
                moduleNode.appendChild(statusNode);
            }
        }
    }

    /**
     * This method adds the result value to the appropriate location in the DOM
     * tree.
     * @param moduleName
     * @param status
     * @param r
     * @param en
     */
    private void addToDocument(String moduleName, String status, Result r,
                               Enumeration en) {
        if (r == null) return;
        NodeList nodeList;
        //this nodeList is the list of nodes below the moduleNode
        nodeList =
                document.getElementsByTagName(moduleName).item(0)
                .getChildNodes();
        Element statusNode = null;
        for (int j = 0; j < nodeList.getLength(); j++) {
            if (((Element) nodeList.item(j)).getTagName().equals(status)) {
                statusNode = (Element) nodeList.item(j);
                break;
            }
        }
        // now get the stuff and write out from result object r
        Element test = document.createElement(TEST);
        Element testName = getTextNode(TEST_NAME, r.getTestName());
        Element testAssertion = getTextNode(TEST_ASSERTION, r.getAssertion());
        // loop thru Details vector
        String string = "";
        while (en.hasMoreElements()) {
            string = string + (String) en.nextElement() + "\n"; // NOI18N
        }
        Element testDescr = getTextNode(TEST_DESC, string);
        test.appendChild(testName);
        test.appendChild(testAssertion);
        test.appendChild(testDescr);
        statusNode.appendChild(test);
    }

    /**
     * Convenience for creating a node <tag>text<tag>.
     *
     * @param tag
     * @param text
     * @return
     */
    private Element getTextNode(String tag, String text) {
        Element element = document.createElement(tag);
        element.appendChild(document.createTextNode(text));
        return element;
    }

    /**
     * wites the final result report to the output xml file
     *
     * @throws IOException
     */
    private void writeToXmlFile() throws IOException {
        FileOutputStream fos = null;
        try {
            File outputFile = extractResultsFileToTmpDir(
                    outputFileStr + ".xml"); // NOI18N
            DOMSource domSource = new DOMSource(document);
            TransformerFactory tfactory = TransformerFactory.newInstance();
            Transformer transformer = tfactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes"); // NOI18N
            transformer.setOutputProperty(OutputKeys.METHOD, "xml"); // NOI18N
            String encoding = System.getProperty("file.encoding");
            transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
            fos = new FileOutputStream(outputFile);
            transformer.transform(domSource, new StreamResult(fos));
        } catch (Exception e) {
            IOException ioe = new IOException(e.getMessage());
            ioe.initCause(e);
            throw ioe;
        } finally {
            try {
                if(fos != null)
                    fos.close();
            } catch (Exception e){}
        }
    }

    /**
     * writes the final result report to output txt file
     *
     * @throws IOException
     */
    private void writeToTxtFile() throws IOException {
        File xslFile = getLocalizedXSLFile();
        
        Document dynamicDocument = document;
        ByteArrayOutputStream output = new ByteArrayOutputStream();
        generateText(dynamicDocument, xslFile, output);
        textResult = output.toString("UTF-8");
        
        // dump to text file.
        File outputFile = extractResultsFileToTmpDir(outputFileStr + ".txt"); // NOI18N
        OutputStreamWriter fw = new OutputStreamWriter(
                                    new FileOutputStream(outputFile));
        fw.write(textResult);
        fw.close();
    }

    private File extractResultsFileToTmpDir(String jarFile) {
        File tmpJarFile = null;
        String fullFilename;
        tmpJarFile = new File(jarFile);
        fullFilename = tmpJarFile.getAbsolutePath();
        if (new File(fullFilename).getParent() != null) {
            (new File(new File(fullFilename).getParent())).mkdirs();
        }
        return tmpJarFile;
    }

    /**
     * Transforms the xml report to txt report.
     * @param xmlResult
     * @param stylesheet
     * @param summaryFile
     * @throws IOException
     */
    private void generateText(Document xmlResult, File stylesheet,
                              OutputStream output)
            throws IOException {
        // Produce Output:
        FileOutputStream fos = null;
        try {
            StreamSource styleSource;
            Transformer transformer;
            TransformerFactory tFactory = TransformerFactory.newInstance();
            if (stylesheet != null) {
                FileInputStream fis = new FileInputStream(stylesheet);
                styleSource = new StreamSource(fis);
                transformer = tFactory.newTransformer(styleSource);
            } else {
                transformer = tFactory.newTransformer();
            }
            DOMSource source = new DOMSource(xmlResult);
            StreamResult streamResult = new StreamResult(output);
            transformer.transform(source, streamResult);

        } catch (Exception e) {
            IOException ioe = new IOException(e.getMessage());
            ioe.initCause(e);
            throw ioe;
        } finally {
            try {
                if(fos != null)
                    fos.close();
            } catch (Exception e) {}
        }
    }

    private void failureCount() {
        int failedCount = resultMgr.getFailedCount();
        int warningCount = resultMgr.getWarningCount();
        int errorCount = resultMgr.getErrorCount();

        Element failureNode = null;
        NodeList nodeList = document.getElementsByTagName(FAILCOUNT);
        if (nodeList.getLength() == 0) {
            failureNode = document.createElement(FAILCOUNT);
            rootNode.appendChild(failureNode);
        } else {
            failureNode = (Element) nodeList.item(0);
        }

        nodeList = failureNode.getChildNodes();//document.getElementsByTagName(FAILED);
        Element failed_count = null;
        Element warning_count = null;
        Element error_count = null;

        if (nodeList.getLength() == 0) {
            failed_count =
                    getTextNode(FAILNUMBER,
                            new Integer(failedCount).toString());
            failureNode.appendChild(failed_count);

            warning_count =
                    getTextNode(WARNINGNUMBER,
                            new Integer((warningCount)).toString());
            failureNode.appendChild(warning_count);

            error_count =
                    getTextNode(ERRORNUMBER,
                            new Integer(errorCount).toString());
            failureNode.appendChild(error_count);
        } else {
            for (int j = 0; j < nodeList.getLength(); j++) {
                if (((Element) nodeList.item(j)).getTagName().equals(
                        FAILNUMBER)) {
                    failed_count = (Element) nodeList.item(j);
                    (failed_count.getFirstChild()).setNodeValue(
                            new Integer(failedCount).toString());
                }
                if (((Element) nodeList.item(j)).getTagName().equals(
                        WARNINGNUMBER)) {
                    warning_count = (Element) nodeList.item(j);
                    (warning_count.getFirstChild()).setNodeValue(
                            new Integer(warningCount).toString());
                }
                if (((Element) nodeList.item(j)).getTagName().equals(
                        ERRORNUMBER)) {
                    error_count = (Element) nodeList.item(j);
                    (error_count.getFirstChild()).setNodeValue(
                            new Integer(errorCount).toString());
                }
            }
            if (failed_count == null) {
                failed_count =
                        getTextNode(FAILNUMBER,
                                new Integer(failedCount).toString());
                failureNode.appendChild(failed_count);
            }
            if (warning_count == null) {
                warning_count =
                        getTextNode(WARNINGNUMBER,
                                new Integer(warningCount).toString());
                failureNode.appendChild(warning_count);
            }
            if (error_count == null) {
                error_count =
                        getTextNode(ERRORNUMBER,
                                new Integer(errorCount).toString());
                failureNode.appendChild(error_count);
            }
        }
    }

    /**
     * returns the error description for writing to the final report.
     * @param e
     * @return
     */
    private String writeStackTraceToFile(Throwable e) {
        StringWriter sw = new StringWriter();
        e.printStackTrace(new PrintWriter(sw));
        return sw.toString();
    }
    
    /**
     * <p>
     * @return the localized XSL file if it is present in the configuration
     * directory. If not, return the default one which is english
     * </p>
     */
    private File getLocalizedXSLFile() {
        String xslHome = System.getProperty(Constants.VERIFIER_XSL);
        if (xslHome == null) {
            xslHome = System.getProperty(Constants.INSTALL_ROOT) +
                    File.separator +
                    "lib" + // NOI18N
                    File.separator +
                    "verifier"; // NOI18N
        }
        Locale locale = Locale.getDefault();
        
        // check first with the language and country
        String xslFileName = xslHome + File.separator + XSL_FILE + "_" + locale.toString() + ".xsl"; // NOI18N
        File xslFile = new File(xslFileName);
        if (xslFile.exists()) {
            return xslFile;
        }
        // check now with the language
        xslFileName = xslHome + File.separator + XSL_FILE + "_" + locale.getLanguage() + ".xsl"; // NOI18N
        xslFile = new File(xslFileName);
        if (xslFile.exists()) {
            return xslFile;
        }
        // just take the english version now...
        xslFileName = xslHome + File.separator + XSL_FILE +  ".xsl"; // NOI18N
        xslFile = new File(xslFileName);
        return xslFile;
    }

}
