/*

   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.batik.test.xml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;

import java.net.URL;

import java.util.Calendar;

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

import org.apache.batik.test.TestReport;
import org.apache.batik.test.TestReportProcessor;
import org.apache.batik.test.TestSuite;
import org.apache.batik.test.TestException;

import org.apache.batik.constants.XMLConstants;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.w3c.dom.DOMImplementation;

/**
 * This implementation of the <code>TestReportProcessor</code> interface
 * converts the <code>TestReports</code> it processes into an
 * XML document that it outputs in a directory. The directory
 * used by the object can be configured at creation time.
 * <br>
 * The <code>XMLTestReportProcessor</code> can optionally notify a
 * report consumer of the XML file it created.
 *
 * @author <a href="mailto:vhardy@apache.org">Vincent Hardy</a>
 * @version $Id: XMLTestReportProcessor.java 1851346 2019-01-15 13:41:00Z ssteiner $
 */
public class XMLTestReportProcessor
    implements TestReportProcessor,
               XTRConstants, XMLConstants {
    /**
     * An <code>XMLReportConsumer</code> is notified every time a
     * new report is generated by an <code>XMLTestReportProcessor</code>
     */
    public interface XMLReportConsumer {
        /**
         * Invoked when new report has been generated.
         * @param xmlReport file containing the xml report
         * @param reportDirectory base directory where any resource relative
         *        to the report processing should be stored.
         */
        void onNewReport(File xmlReport,
                                File reportDirectory) throws Exception ;
    }

    /**
     * Error message if report directory does not exist.
     */
    public static final String ERROR_REPORT_DIRECTORY_UNUSABLE
        = "xml.XMLTestReportProcessor.error.report.directory.unusable";

    /**
     * Error message if report resources directory does not exist.
     */
    public static final String ERROR_REPORT_RESOURCES_DIRECTORY_UNUSABLE
        = "xml.XMLTestReportProcessor.error.report.resources.directory.unusable";

    /**
     * Default report directory
     */
    public static final String XML_TEST_REPORT_DEFAULT_DIRECTORY
        = Messages.formatMessage("XMLTestReportProcessor.config.xml.test.report.default.directory", null);

    /**
     * Directory where the XML report is created
     */
    public static final String XML_REPORT_DIRECTORY
        = Messages.formatMessage("XMLTestReportProcessor.xml.report.directory", null);

    /**
     * Directory where resources (e.g., images) referenced by the
     * XML report are copied.
     */
    public static final String XML_RESOURCES_DIRECTORY
        = Messages.formatMessage("XMLTestReportProcessor.xml.resources.directory", null);

    /**
     * Test report name
     */
    public static final String XML_TEST_REPORT_NAME
        = Messages.formatMessage("XMLTestReportProcessor.config.xml.test.report.name", null);

    /**
     * The XMLReportConsumer instance is notified whenever
     * this object generates a new report.
     */
    protected XMLReportConsumer consumer;

    /**
     * String encoding the date the report was generated.
     */
    protected String reportDate;

    /**
     * Directory into which this processor puts all files and resources.
     */
    protected File reportDirectory;

    /**
     * Directory into which XML files are created
     */
    protected File xmlDirectory;

    /**
     * Directory into whichr resources refered to by XML files are created
     */
    protected File xmlResourcesDirectory;

    /**
     * Default constructor
     */
    public XMLTestReportProcessor(){
    }

    /**
     * @param consumer consumer for the XML report generated
     *        by this object. May be null.
     */
    public XMLTestReportProcessor(XMLTestReportProcessor.XMLReportConsumer consumer){
        this.consumer = consumer;
    }

    /**
     * Recursively processes the input <code>TestReport</code> and
     * any of its children.
     */
    public void processReport(TestReport report)
        throws TestException {

        /**
         * First, create the directories for the
         * report and report resources
         */
        initializeReportDirectories();

        try {

            /**
             * Create a new document and build the root
             * <testReport> element. Then, process the
             * TestReports recursively.
             */
            DocumentBuilder docBuilder
                = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            DOMImplementation impl
                = docBuilder.getDOMImplementation();

            Document document = null;

            if(report.getTest() instanceof TestSuite){
                document = impl.createDocument(XTR_NAMESPACE_URI,
                                               XTR_TEST_SUITE_REPORT_TAG, null);
            }
            else {
                document = impl.createDocument(XTR_NAMESPACE_URI,
                                               XTR_TEST_REPORT_TAG, null);
            }

            Element root = document.getDocumentElement();

            root.setAttribute(XTR_DATE_ATTRIBUTE,
                                reportDate);

            processReport(report, root, document);

            File xmlReport = serializeReport(root);

            if(consumer != null){
                consumer.onNewReport(xmlReport, getReportDirectory());
            }

        } catch(Exception e) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            e.printStackTrace(pw);
            throw new TestException(INTERNAL_ERROR,
                                    new Object[] { e.getClass().getName(),
                                                   e.getMessage(),
                                                   sw.toString() },
                                    e);
        }
    }

    /**
     * Checks that the input File represents a directory that
     * can be used. If the directory does not exist, this method
     * will attempt to create it.
     */
    public void checkDirectory(File dir,
                               String errorCode)
        throws TestException {
        boolean dirOK = false;
        try{
            if(!dir.exists()){
                dirOK = dir.mkdir();
            }
            else if(dir.isDirectory()){
                dirOK = true;
            }
        }finally{
            if(!dirOK){
                throw new TestException(errorCode,
                                        new Object[] {dir.getAbsolutePath()},
                                        null);

            }
        }
    }

    /**
     * By default, the report directory is given by a configuration
     * variable. Each test run will create a sub directory with
     * the current date and time as the same. All the resources
     * created by the report processor are then put into that
     * "dated" directory.
     */
    public void initializeReportDirectories() throws TestException {
        //
        // Base report directory
        //
        File baseReportDir = new File(XML_TEST_REPORT_DEFAULT_DIRECTORY);
        checkDirectory(baseReportDir, ERROR_REPORT_DIRECTORY_UNUSABLE);

        //
        // Create sub-directory name based on date and time
        //
        Calendar c = Calendar.getInstance();
        String dirName = "" + c.get(Calendar.YEAR) + "."
            + makeTwoDigits  (c.get(Calendar.MONTH)+1) + "."
            + makeTwoDigits  (c.get(Calendar.DAY_OF_MONTH)) + "-"
            + makeTwoDigits  (c.get(Calendar.HOUR_OF_DAY)) + "h"
            + makeTwoDigits  (c.get(Calendar.MINUTE)) + "m"
            + makeTwoDigits  (c.get(Calendar.SECOND)) + "s";

        reportDate = dirName;
        reportDirectory = new File(baseReportDir, dirName);
        checkDirectory(reportDirectory, ERROR_REPORT_DIRECTORY_UNUSABLE);

        //
        // Now, create a sub-directory for XML files and
        // anotherone for resources
        //
        xmlDirectory = new File(reportDirectory, XML_REPORT_DIRECTORY);
        checkDirectory(xmlDirectory, ERROR_REPORT_DIRECTORY_UNUSABLE);

        xmlResourcesDirectory = new File(xmlDirectory, XML_RESOURCES_DIRECTORY);
        checkDirectory(xmlResourcesDirectory, ERROR_REPORT_DIRECTORY_UNUSABLE);
    }

    /**
     * Forces a two digit string
     */
    protected String makeTwoDigits(int i){
        if(i > 9){
            return "" + i;
        }
        else{
            return "0" + i;
        }
    }

    /**
     * Returns the report directory
     */
    public File getReportDirectory(){
        return reportDirectory;
    }

    /**
     * By default, the report resources directory is
     * given by a configuration variable.
     */
    public File getReportResourcesDirectory() {
        return xmlResourcesDirectory;
    }

    /**
     * Recursively processes the input <code>TestReport</code> adding
     * the report information to the input element.
     */
    protected void processReport(TestReport report,
                                 Element reportElement,
                                 Document reportDocument) throws IOException {
        if(report == null){
            throw new IllegalArgumentException();
        }

        reportElement.setAttribute(XTR_TEST_NAME_ATTRIBUTE,
                                   report.getTest().getName());

        String id = report.getTest().getQualifiedId();
        if( !"".equals(id) ){
            reportElement.setAttribute(XTR_ID_ATTRIBUTE,
                                       id
                                       );
        }

        String status = report.hasPassed()
            ? XTR_PASSED_VALUE
            : XTR_FAILED_VALUE;

        reportElement.setAttribute(XTR_STATUS_ATTRIBUTE,
                                   status);

        String className = report.getTest().getClass().getName();

        reportElement.setAttribute(XTR_CLASS_ATTRIBUTE,
                                   className);

        if(!report.hasPassed()){
            reportElement.setAttribute(XTR_ERROR_CODE_ATTRIBUTE,
                                       report.getErrorCode());
        }

        TestReport.Entry[] entries = report.getDescription();
        int n = entries != null ? entries.length : 0;

        if (n>0) {
            Element descriptionElement
                = reportDocument.createElementNS(null,
                                                 XTR_DESCRIPTION_TAG);

            reportElement.appendChild(descriptionElement);

            for(int i=0; i<n; i++){
                processEntry(entries[i],
                             descriptionElement,
                             reportDocument);

            }
        }
    }

    protected void processEntry(TestReport.Entry entry,
                                Element descriptionElement,
                                Document reportDocument) throws IOException {

        Object value = entry.getValue();
        String key   = entry.getKey();

        if(value instanceof TestReport){
            TestReport report = (TestReport)value;

            Element reportElement = null;

            if(report.getTest() instanceof TestSuite){
                reportElement
                    = reportDocument.createElementNS(XTR_NAMESPACE_URI,
                                                     XTR_TEST_SUITE_REPORT_TAG);
            }
            else{
                reportElement
                    = reportDocument.createElementNS(XTR_NAMESPACE_URI,
                                                     XTR_TEST_REPORT_TAG);
            }

            descriptionElement.appendChild(reportElement);
            processReport((TestReport)entry.getValue(),
                          reportElement,
                          reportDocument);
        }
        else if(value instanceof URL){
            Element entryElement
                = reportDocument.createElementNS(XTR_NAMESPACE_URI,
                                                 XTR_URI_ENTRY_TAG);

            descriptionElement.appendChild(entryElement);

            entryElement.setAttribute(XTR_KEY_ATTRIBUTE,
                                      key.toString());

            entryElement.setAttribute(XTR_VALUE_ATTRIBUTE,
                                      value.toString());

        }
        else if(value instanceof File){
            //
            // The entry is a potentially temporary File. Copy
            // the file to the repository and create a file entry
            // referencing that file copy.
            //
            File tmpFile = (File)value;

            File tmpFileCopy = createResourceFileForName(tmpFile.getName());

            copy(tmpFile, tmpFileCopy);

            Element entryElement
                = reportDocument.createElementNS(XTR_NAMESPACE_URI,
                                                 XTR_FILE_ENTRY_TAG);

            descriptionElement.appendChild(entryElement);

            entryElement.setAttribute(XTR_KEY_ATTRIBUTE,
                                      key.toString());

            entryElement.setAttribute(XTR_VALUE_ATTRIBUTE,
                                      tmpFileCopy.toURI().toURL().toString());

        }
        else {

            Element entryElement
                = reportDocument.createElementNS(XTR_NAMESPACE_URI,
                                                 XTR_GENERIC_ENTRY_TAG);

            descriptionElement.appendChild(entryElement);

            entryElement.setAttribute(XTR_KEY_ATTRIBUTE,
                                      key.toString());

            Attr a = reportDocument.createAttribute(XTR_VALUE_ATTRIBUTE);
            a.setValue(value!=null?value.toString():"null");
            entryElement.setAttributeNode(a);
        }
    }

    /**
     * Untility method. Creates a file in the resources directory
     * for the given name. If a file in that directory does not
     * exist yet, then it is used. Otherwise, a file with the same
     * name with a digit suffix is created. For example, if "myFile.png"
     * is requested, then "myFile.png" is created or "myFile&lt;n&gt;.png"
     * where &lt;n&gt; will be one or several digits.
     */
    protected File createResourceFileForName(String fileName){
        File r = new File(xmlResourcesDirectory, fileName);
        if(!r.exists()){
            return r;
        }
        else{
            return createResourceFileForName(fileName, 1);
        }
    }

    protected File createResourceFileForName(String fileName,
                                             int instance){
        // First, create a 'versioned' file name
        int n = fileName.lastIndexOf('.');
        String iFileName = fileName + instance;
        if(n != -1){
            iFileName = fileName.substring(0, n) + instance
                + fileName.substring(n, fileName.length());
        }

        File r = new File(xmlResourcesDirectory, iFileName);
        if(!r.exists()){
            return r;
        }
        else{
            return createResourceFileForName(fileName,
                                             instance + 1);
        }
    }

    /**
     * Utility method. Copies in to out
     */
    protected void copy(File in, File out) throws IOException {
        InputStream is = new BufferedInputStream(new FileInputStream(in));
        OutputStream os = new BufferedOutputStream(new FileOutputStream(out));

        final byte[] b = new byte[1024];
        int n = -1;
        while( (n = is.read(b)) != -1 ){
            os.write(b, 0, n);
        }

        is.close();
        os.close();
    }

    /**
     * Saves the XML document into a file
     */
    protected File serializeReport(Element reportElement) throws IOException {
        //
        // First, create a new File
        //
        File reportFile = new File(xmlDirectory,
                                   XML_TEST_REPORT_NAME);

        FileWriter fw = new FileWriter(reportFile);

        serializeElement(reportElement,
                         "",
                         fw);

        fw.close();

        return reportFile;
    }


    private static String EOL;

    private static String PROPERTY_LINE_SEPARATOR = "line.separator";
    private static String PROPERTY_LINE_SEPARATOR_DEFAULT = "\n";
    static {
        String  temp;
        try {
            temp = System.getProperty (PROPERTY_LINE_SEPARATOR,
                                       PROPERTY_LINE_SEPARATOR_DEFAULT);
        } catch (SecurityException e) {
            temp = PROPERTY_LINE_SEPARATOR_DEFAULT;
        }
        EOL = temp;
    }

    protected void serializeElement(Element element,
                                    String  prefix,
                                    Writer  writer) throws IOException {
        writer.write(prefix);
        writer.write(XML_OPEN_TAG_START);
        writer.write(element.getTagName());

        serializeAttributes(element,
                            writer);

        NodeList children = element.getChildNodes();
        if(children != null && children.getLength() > 0){
            writer.write(XML_OPEN_TAG_END_CHILDREN);
            writer.write(EOL);
            int n = children.getLength();
            for(int i=0; i<n; i++){
                Node child = children.item(i);
                if(child.getNodeType() == Node.ELEMENT_NODE){
                    serializeElement((Element)child,
                                     prefix + XML_TAB,
                                     writer);
                }
            }
            writer.write(prefix);
            writer.write(XML_CLOSE_TAG_START);
            writer.write(element.getTagName());
            writer.write(XML_CLOSE_TAG_END);
        }
        else{
            writer.write(XML_OPEN_TAG_END_NO_CHILDREN);
        }

        writer.write(EOL);

    }

    protected void serializeAttributes(Element element,
                                       Writer  writer) throws IOException{
        NamedNodeMap attributes = element.getAttributes();
        if (attributes != null){
            int nAttr = attributes.getLength();
            for(int i=0; i<nAttr; i++){
                Attr attr = (Attr)attributes.item(i);
                writer.write(XML_SPACE);
                writer.write(attr.getName());
                writer.write(XML_EQUAL_SIGN);
                writer.write(XML_DOUBLE_QUOTE);
                writer.write(encode(attr.getValue()));
                writer.write(XML_DOUBLE_QUOTE);
            }
        }
    }

    /**
     * Poor way of replacing '&lt;', '&gt;', '"', '&amp;' and '''
     * in attribute values.
     */
    protected String encode(String attrValue){
        StringBuffer sb = new StringBuffer(attrValue);
        replace(sb, XML_CHAR_AMP, XML_ENTITY_AMP);
        replace(sb, XML_CHAR_LT, XML_ENTITY_LT);
        replace(sb, XML_CHAR_GT, XML_ENTITY_GT);
        replace(sb, XML_CHAR_QUOT, XML_ENTITY_QUOT);
        replace(sb, XML_CHAR_APOS, XML_ENTITY_APOS);
        return sb.toString();
    }

    protected void replace(StringBuffer s,
                             char c,
                             String r){
        String v = s.toString() + 1;
        int i = v.length();

        while( (i=v.lastIndexOf(c, --i)) != -1 ){
            s.deleteCharAt(i);
            s.insert(i, r);
        }
    }
}
