/*
 * This file is part of the DITA Open Toolkit project hosted on
 * Sourceforge.net. See the accompanying license.txt file for 
 * applicable licenses.
 */

/*
 * (c) Copyright IBM Corp. 2007 All Rights Reserved.
 */
package org.dita.dost.platform;

import org.apache.tools.ant.Task;
import java.io.File;
import java.io.IOException;
import java.io.StringReader;

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

import java.io.FileInputStream;
import java.util.Properties;
import java.io.InputStream;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.XMLCatalog;
import org.apache.tools.ant.util.FileUtils;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
/**
 * @author wxzhang
 *
 */
public final class XmlPropertyTask extends Task {

	/**
	 * 
	 */
	public XmlPropertyTask() {
		super();
	}
	private File src;
	private String prefix = "";
	private boolean validate = false;
	private final boolean collapseAttributes = false;
	private File rootDirectory = null;
	private final FileUtils fileUtils = FileUtils.getFileUtils();
	private final XMLCatalog xmlCatalog = new XMLCatalog();
	private final Properties props = new Properties();
	
    // XML loading and saving methods for Properties

    // The required DTD URI for exported properties
    private static final String PROPS_DTD_URI =
    "http://java.sun.com/dtd/properties.dtd";

    private static final String PROPS_DTD =
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" +
    "<!-- DTD for properties -->"                +
    "<!ELEMENT properties ( comment?, entry* ) >"+
    "<!ATTLIST properties"                       +
        " version CDATA #FIXED \"1.0\">"         +
    "<!ELEMENT comment (#PCDATA) >"              +
    "<!ELEMENT entry (#PCDATA) >"                +
    "<!ATTLIST entry "                           +
        " key CDATA #REQUIRED>";

    /**
     * Version number for the format of exported properties files.
     */
    private static final String EXTERNAL_XML_VERSION = "1.0";


	/**
	 * Initializes the task.
	 */

	@Override
	public void init() {
		super.init();
		xmlCatalog.setProject(getProject());
	}

	/**
	 * @return the xmlCatalog as the entityresolver.
	 */
	protected EntityResolver getEntityResolver() {
		return xmlCatalog;
	}

	/**
	 * Run the task.
	 * @throws BuildException The exception raised during task execution.
	 * @todo validate the source file is valid before opening, print a better error message
	 * @todo add a verbose level log message listing the name of the file being loaded
	 */
	@Override
	public void execute() throws BuildException {

		if (getFile() == null) {
			final String msg = "XmlProperty task requires a file attribute";
			throw new BuildException(msg);
		}
		InputStream in = null;
		try {
			log("Loading " + src.getAbsolutePath(), Project.MSG_VERBOSE);

			if (src.exists()) {
				in = new FileInputStream(src);
				load(in);
			} else {
				log("Unable to find property file: " + src.getAbsolutePath(),
						Project.MSG_VERBOSE);
			}
		}catch(final SAXException se){
			throw new BuildException(se);
		} catch (final IOException ioe) {
			// I/O error
			throw new BuildException(ioe);
		} finally {
			if (in != null) {
				try {
					in.close();
				} catch (IOException e) {
					log(e, Project.MSG_ERR);
				}
			}
		}
	}

	private void load( final InputStream in) throws IOException,SAXException{
        final Document doc = getLoadingDoc(in);
        final Element propertiesElement = (Element)doc.getChildNodes().item(1);
        final String xmlVersion = propertiesElement.getAttribute("version");
        if (xmlVersion.compareTo(EXTERNAL_XML_VERSION) > 0) {
			throw new SAXException(
                "Exported Properties file format version " + xmlVersion +
                " is not supported. This java installation can read" +
                " versions " + EXTERNAL_XML_VERSION + " or older. You" +
                " may need to install a newer version of JDK.");
		}
        final NodeList entries = propertiesElement.getChildNodes();
        final int numEntries = entries.getLength();
        final int start = numEntries > 0 && 
            entries.item(0).getNodeName().equals("comment") ? 1 : 0;
        for (int i=start; i<numEntries; i++) {
            final Element entry = (Element)entries.item(i);
            if (entry.hasAttribute("key")) {
                final Node n = entry.getFirstChild();
                final String val = (n == null) ? "" : n.getNodeValue();
                props.setProperty(entry.getAttribute("key"), val);
                addProperty(entry.getAttribute("key"),val,null);
            }
        }

	}

	static Document getLoadingDoc(final InputStream in) throws SAXException,
			IOException {
		final DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
		dbf.setIgnoringElementContentWhitespace(true);
		dbf.setValidating(true);
		dbf.setCoalescing(true);
		dbf.setIgnoringComments(true);
		try {
			final DocumentBuilder db = dbf.newDocumentBuilder();
			db.setEntityResolver(new Resolver());
			db.setErrorHandler(new EH());
			final InputSource is = new InputSource(in);
			return db.parse(is);
		} catch (final ParserConfigurationException x) {
			throw new Error(x);
		}
	}
    private static class Resolver implements EntityResolver {
        public InputSource resolveEntity(final String pid, final String sid)
            throws SAXException
        {
            if (sid.equals(PROPS_DTD_URI)) {
                InputSource is;
                is = new InputSource(new StringReader(PROPS_DTD));
                is.setSystemId(PROPS_DTD_URI);
                return is;
            }
            throw new SAXException("Invalid system identifier: " + sid);
        }
    }

    private static class EH implements ErrorHandler {
        public void error(final SAXParseException x) throws SAXException {
            throw x;
        }
        public void fatalError(final SAXParseException x) throws SAXException {
            throw x;
        }
        public void warning(final SAXParseException x) throws SAXException {
            throw x;
        }
    }
	/**
	 * Actually add the given property/value to the project
	 * after writing a log message.
	 */
	private void addProperty(final String name, final String value, final String id) {
		String msg = name + ":" + value;
		if (id != null) {
			msg += ("(id=" + id + ")");
		}
		log(msg, Project.MSG_DEBUG);
		getProject().setProperty(name, value);
		if (id != null) {
			getProject().addReference(id, value);
		}
	}

	/**
	 * The XML file to parse; required.
	 * @param src the file to parse
	 */
	public void setFile(final File src) {
		this.src = src;
	}

	/**
	 * the prefix to prepend to each property.
	 * @param prefix the prefix to prepend to each property
	 */
	public void setPrefix(final String prefix) {
		this.prefix = prefix.trim();
	}

	/**
	 * flag to validate the XML file; optional, default false.
	 * @param validate if true validate the XML file, default false
	 */
	public void setValidate(final boolean validate) {
		this.validate = validate;
	}

	/**
	 * The directory to use for resolving file references.
	 * Ignored if semanticAttributes is not set to true.
	 * @param rootDirectory the directory.
	 */
	public void setRootDirectory(final File rootDirectory) {
		this.rootDirectory = rootDirectory;
	}

	/**
	 * add an XMLCatalog as a nested element; optional.
	 * @param catalog the XMLCatalog to use
	 */
	public void addConfiguredXMLCatalog(final XMLCatalog catalog) {
		xmlCatalog.addConfiguredXMLCatalog(catalog);
	}

	/* Expose members for extensibility */

	/**
	 * @return the file attribute.
	 */
	protected File getFile() {
		return this.src;
	}

	/**
	 * @return the prefix attribute.
	 */
	protected String getPrefix() {
		return this.prefix;
	}

	/**
	 * @return the validate attribute.
	 */
	protected boolean getValidate() {
		return this.validate;
	}

	/**
	 * @return the collapse attributes attribute.
	 */
	protected boolean getCollapseAttributes() {
		return this.collapseAttributes;
	}

	/**
	 * @return the root directory attribute.
	 */
	protected File getRootDirectory() {
		return this.rootDirectory;
	}

	/**
	 * Let project resolve the file - or do it ourselves if
	 * rootDirectory has been set.
	 */
	private File resolveFile(final String fileName) {
		if (rootDirectory == null) {
			return getProject().resolveFile(fileName);
		}
		return fileUtils.resolveFile(rootDirectory, fileName);
	}

}