/*
 * JBoss, Home of Professional Open Source
 * Copyright 2006, Red Hat Middleware LLC, and individual contributors 
 * as indicated by the @author tags. 
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors. 
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License, v. 2.1.
 * This program is distributed in the hope that it will be useful, but WITHOUT A 
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License,
 * v.2.1 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, 
 * MA  02110-1301, USA.
 * 
 * (C) 2005-2006,
 * @author JBoss Inc.
 */
package com.arjuna.common.internal.util.propertyservice.plugins.io;

import com.arjuna.common.util.propertyservice.plugins.PropertyManagerIOPlugin;
import com.arjuna.common.util.propertyservice.propertycontainer.PropertyManagerPluginInterface;
import com.arjuna.common.util.exceptions.LoadPropertiesException;
import com.arjuna.common.util.propertyservice.PropertyManager;
import com.arjuna.common.util.propertyservice.StringPropertyReplacer;
import com.arjuna.common.util.FileLocator;
import com.arjuna.common.util.exceptions.SavePropertiesException;

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

import java.io.IOException;
import java.io.FileOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.Hashtable;
import java.util.Enumeration;
import java.util.StringTokenizer;
import java.util.ArrayList;
import java.util.Properties;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.w3c.dom.Node;
import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;

/*
 * Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003
 *
 * Arjuna Technologies Ltd.
 * Newcastle upon Tyne,
 * Tyne and Wear,
 * UK.
 *
 * $Id: XMLFilePlugin.java 2342 2006-03-30 13:06:17Z  $
 */

public class XMLFilePlugin implements PropertyManagerIOPlugin
{
	private final static String PROPERTIES_ELEMENT_NAME = "properties";
	private final static String DEPENDS_ATTRIBUTE_NAME = "depends";

	private final static String PROPERTY_ELEMENT_NAME = "property";
	private final static String NAME_ATTRIBUTE_NAME = "name";
	private final static String VALUE_ATTRIBUTE_NAME = "value";
	private final static String PROPERTY_TYPE_ATTRIBUTE_NAME = "type";

	private final static String SYSTEM_PROPERTY_TYPE_NAME = "system";

    private static DocumentBuilder _documentBuilder;

    /**
     * This hashtable is used to store the URI's of files already loaded.
     * The map is from PropertyManagerPluginInterface -> URI[]
     * A PropertyManagerPluginInterface can only load the properties from a
     * file once.
     */
    private static Hashtable    _loadedFiles = new Hashtable();

    private static void setURILoaded(PropertyManagerPluginInterface pcm, String uri)
    {
        ArrayList uris = (ArrayList)_loadedFiles.get(pcm);

        if ( uris == null )
        {
            uris = new ArrayList(1);
            _loadedFiles.put(pcm, uris);
        }

        if ( !uris.contains(uri) )
        {
            uris.add(uri);
        }
    }

    private static boolean isURILoaded(PropertyManagerPluginInterface pcm, String uri)
    {
        boolean loaded = false;

        ArrayList uris = (ArrayList)_loadedFiles.get(pcm);

        if ( uris != null )
        {
            loaded = uris.contains(uri);
        }

        return loaded;
    }

	/**
	 * This method loads the properties stored at the given <code>uri</code>.  The
	 * plugin uses the <code>PropertyManagerPluginInterface</code> to put the properties
	 * into the property manager.
	 *
	 * @param uri
	 * @param pcm
	 * @throws java.io.IOException
	 */
	public void load(String uri, PropertyManagerPluginInterface pcm, boolean verbose) throws LoadPropertiesException, IOException
	{
        /** Ensure we haven't loaded the URI before **/
        if ( !isURILoaded(pcm, uri) )
        {
            try
            {
                String filename = FileLocator.locateFile(uri);

				if ( filename != null && new File(filename).exists() )
				{
					Document doc = _documentBuilder.parse(filename);
					Element rootElement = doc.getDocumentElement();

					setURILoaded(pcm, uri);

					NodeList propertiesNodes = rootElement.getElementsByTagName(PROPERTIES_ELEMENT_NAME);

					Hashtable pmDependents = new Hashtable();
					Hashtable pms = new Hashtable();
					for (int count=0;count<propertiesNodes.getLength();count++)
					{
						Element propertiesNode = (Element)propertiesNodes.item(count);
                        Node nameNode = propertiesNode.getAttributeNode(NAME_ATTRIBUTE_NAME);
						String propertiesName = nameNode != null ? nameNode.getNodeValue() : null;
						Node dependsNode = propertiesNode.getAttributeNode(DEPENDS_ATTRIBUTE_NAME);
						String dependsName = dependsNode != null ? dependsNode.getNodeValue() : "";

						NodeList propertyNodes = propertiesNode.getElementsByTagName(PROPERTY_ELEMENT_NAME);

                        /** If the name is provided get the OM from the top level else just load into the current PM **/
						PropertyManagerPluginInterface pm = propertiesName != null ? pcm.getTopLevelPropertyManager().getChild(propertiesName) : pcm;

						/** If the child doesn't already exist create one **/
						if ( pm == null )
						{
							pm = pcm.createPropertyManager(propertiesName);
						}

						if ( verbose )
						{
							System.out.println("Properties loaded ("+uri+"):");
						}

						/** Associate the current URI with this property manager **/
						pm.setUri(uri);
						/** Associate this plugin with the property manager **/
						pm.setIOPluginClassname(this.getClass().getName());

						for (int nodeCount=0;nodeCount<propertyNodes.getLength();nodeCount++)
						{
							Element propertyNode = (Element)propertyNodes.item(nodeCount);
							String propertyName = propertyNode.getAttribute(NAME_ATTRIBUTE_NAME);
							String propertyValue = propertyNode.getAttribute(VALUE_ATTRIBUTE_NAME);
							String propertyType = propertyNode.getAttribute(PROPERTY_TYPE_ATTRIBUTE_NAME);

							if ( verbose )
							{
								System.out.println( propertyName +"="+ propertyValue );
							}
                            
                            // perform JBossAS style property substitutions. JBTM-369
                            propertyValue = StringPropertyReplacer.replaceProperties(propertyValue);

							/** Set the property but don't allow any system property to be overridden **/
							pm.setProperty(propertyName, propertyValue, false);

							/** If the property is specified as a System property and it's not already a system property **/
							if ( propertyType != null &&
							     propertyType.equalsIgnoreCase(SYSTEM_PROPERTY_TYPE_NAME) &&
								 System.getProperty(propertyName) == null )
							{
								/** Set this as a system property also **/
								System.setProperty(propertyName, propertyValue);
							}
						}

						/** If this has no dependents then it is a top-level module **/
						if ( dependsName.length() == 0 && pm != pcm)
						{
							pcm.addChild(pm);
						}

						pmDependents.put(pm, dependsName);

                        /** If these properties belond to a named set store them **/
                        if ( propertiesName != null )
                        {
						    pms.put(propertiesName, pm);
                        }
					}

					/** Ensure dependency tree is kept **/
					for (Enumeration e = pmDependents.keys();e.hasMoreElements();)
					{
						PropertyManagerPluginInterface pm = (PropertyManagerPluginInterface)e.nextElement();
						String depends = (String)pmDependents.get(pm);

						StringTokenizer strtok = new StringTokenizer(depends,",");

						while (strtok.hasMoreElements())
						{
							String dependantName = strtok.nextToken().trim();
							PropertyManager dependant = (PropertyManager)pms.get(dependantName);

                            if ( dependant == null )
                            {
                                throw new LoadPropertiesException("Dependency not found - property file invalid");
                            }
							pm.addParent(dependant);
						}
					}
				}
				else
				{
					throw new FileNotFoundException();
				}
            }
			catch (FileNotFoundException e)
			{
				if ( verbose )
				{
					System.err.println("Cannot load properties file: "+uri);
				}
			}
            catch (IOException e)
            {
                throw e;
            }
            catch (Exception e)
            {
                throw new LoadPropertiesException("Failed to open properties file: "+e);
            }
        }
	}

	/**
	 * This method saves the properties to the given <code>uri</code>.  The plugin
	 * uses the <code>PropertyManagerPluginInterface</code> to read the properties
	 * from the property manager.
	 *
	 * @param uri
	 * @param pcm
	 * @throws SavePropertiesException
	 * @throws IOException
	 */
	public void save(String uri, PropertyManagerPluginInterface pcm) throws SavePropertiesException, IOException
	{
        uri = uri != null ? null : pcm.getUri();

        if ( uri == null )
        {
            throw new SavePropertiesException("No uri is associated with this property manager");
        }

        Properties props = pcm.getLocalProperties();

        try
		{
			String filename = FileLocator.locateFile(uri);
			Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(filename);
			Element root = doc.getDocumentElement();
			String depends = null;

			/** Remove the properties node for this property manager from the tree **/
            NodeList propertiesNodes = root.getElementsByTagName(PROPERTIES_ELEMENT_NAME);

			for (int count=0;count<propertiesNodes.getLength();count++)
			{
				Element propertiesElement = (Element)propertiesNodes.item(count);
				String propertiesName = propertiesElement.getAttributeNode(NAME_ATTRIBUTE_NAME).getNodeValue();

				/** If this is the properties node we are looking for remove it **/
				if ( propertiesName.equals(pcm.getName()) )
				{
					root.removeChild(propertiesElement);
					depends = propertiesElement.getAttributeNode(DEPENDS_ATTRIBUTE_NAME).getNodeValue();
					break;
				}
			}

			/** Now add the new properties node **/
			Element newPropertiesElement = doc.createElement(PROPERTIES_ELEMENT_NAME);

			newPropertiesElement.setAttribute(NAME_ATTRIBUTE_NAME, pcm.getName());

			if ( depends != null)
			{
				newPropertiesElement.setAttribute(DEPENDS_ATTRIBUTE_NAME, depends);
			}

			for (Enumeration e = props.keys();e.hasMoreElements();)
			{
				String key = (String)e.nextElement();
				String value = props.getProperty(key);

				Element newPropertyElement = doc.createElement(PROPERTY_ELEMENT_NAME);
				newPropertyElement.setAttribute(NAME_ATTRIBUTE_NAME, key);
				newPropertyElement.setAttribute(VALUE_ATTRIBUTE_NAME, value);

				newPropertiesElement.appendChild(newPropertyElement);
			}

			root.appendChild(newPropertiesElement);

			/** Output new properties file **/
			FileOutputStream out;
			OutputFormat of = new OutputFormat(doc);
			of.setIndenting(true);
        	XMLSerializer srl = new XMLSerializer(out = new FileOutputStream(filename), of);

			srl.serialize(doc);

			out.close();
		}
		catch (java.io.IOException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new SavePropertiesException("Unexpected exception: "+e);
		}
	}

    static
    {
        try
        {
            _documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        }
        catch (Exception e)
        {
            throw new ExceptionInInitializerError("Failed to create document builder:"+e);
        }
    }
}
