/*
 * Copyright 1999-2004 The Apache Software Foundation.
 *
 * Licensed 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.
 */
/*
 * $Id: OutputPropertiesFactory.java,v 1.11 2005/06/01 19:17:08 minchau Exp $
 */
package org.apache.xml.serializer;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Enumeration;
import java.util.Properties;

import javax.xml.transform.OutputKeys;

import org.apache.xml.serializer.utils.MsgKey;
import org.apache.xml.serializer.utils.Utils;
import org.apache.xml.serializer.utils.WrappedRuntimeException;

/**
 * This class is a factory to generate a set of default properties
 * of key/value pairs that are used to create a serializer through the
 * factory {@link SerializerFactory SerilizerFactory}. 
 * The properties generated by this factory
 * may be modified to non-default values before the SerializerFactory is used to
 * create a Serializer.
 * <p>
 * The given output types supported are "xml", "text", and "html". 
 * These type strings can be obtained from the 
 * {@link Method Method} class in this package.
 * <p>
 * Other constants defined in this class are the non-standard property keys
 * that can be used to set non-standard property values on a java.util.Properties object
 * that is used to create or configure a serializer. Here are the non-standard keys:
 * <ul>
 * <li> <b>S_KEY_INDENT_AMOUNT </b> -
 * The non-standard property key to use to set the indentation amount.
 * The "indent" key needs to have a value of "yes", and this
 * properties value is a the number of whitespaces to indent by per
 * indentation level.
 * 
 * <li> <b>S_KEY_CONTENT_HANDLER </b> -
 * This non-standard property key is used to set the name of the fully qualified 
 * Java class that implements the ContentHandler interface. 
 * The output of the serializer will be SAX events sent to this an
 * object of this class.
 * 
 * <li> <b>S_KEY_ENTITIES </b> -
 * This non-standard property key is used to specify the name of the property file
 * that specifies character to entity reference mappings. A line in such a
 * file is has the name of the entity and the numeric (base 10) value
 * of the corresponding character, like this one: <br> quot=34 <br>
 * 
 * <li> <b>S_USE_URL_ESCAPING </b> -
 * This non-standard property key is used to set a value of "yes" if the href values for HTML serialization should
 *  use %xx escaping.
 * 
 * <li> <b>S_OMIT_META_TAG </b> -
 * This non-standard property key is used to set a value of "yes" if the META tag should be omitted where it would
 *  otherwise be supplied.
 * </ul>
 * 
 * @see SerializerFactory
 * @see Method
 * @see Serializer
 */
public final class OutputPropertiesFactory
{
    /** S_BUILTIN_EXTENSIONS_URL is a mnemonic for the XML Namespace 
     *(http://xml.apache.org/xalan) predefined to signify Xalan's
     * built-in XSLT Extensions. When used in stylesheets, this is often 
     * bound to the "xalan:" prefix.
     */
    private static final String 
      S_BUILTIN_EXTENSIONS_URL = "http://xml.apache.org/xalan"; 

    /**
     * The old built-in extension url. It is still supported for
     * backward compatibility.
     */
    private static final String 
      S_BUILTIN_OLD_EXTENSIONS_URL = "http://xml.apache.org/xslt"; 
      
    //************************************************************
    //*  PUBLIC CONSTANTS
    //************************************************************
    /** 
     * This is not a public API.
     * This is the built-in extensions namespace, 
     * reexpressed in {namespaceURI} syntax
     * suitable for prepending to a localname to produce a "universal
     * name".
     */
    public static final String S_BUILTIN_EXTENSIONS_UNIVERSAL =
        "{" + S_BUILTIN_EXTENSIONS_URL + "}";

    // Some special Xalan keys.

    /** 
     * The non-standard property key to use to set the
     * number of whitepaces to indent by, per indentation level,
     * if indent="yes".
     */
    public static final String S_KEY_INDENT_AMOUNT =
        S_BUILTIN_EXTENSIONS_UNIVERSAL + "indent-amount";
        
    /** 
     * The non-standard property key to use to set the
     * number of whitepaces to indent by, per indentation level,
     * if indent="yes".
     */
    public static final String S_KEY_LINE_SEPARATOR =
        S_BUILTIN_EXTENSIONS_UNIVERSAL + "line-separator";        

    /** This non-standard property key is used to set the name of the fully qualified 
     * Java class that implements the ContentHandler interface. 
     * Fully qualified name of class with a default constructor that
     *  implements the ContentHandler interface, where the result tree events
     *  will be sent to.
     */

    public static final String S_KEY_CONTENT_HANDLER =
        S_BUILTIN_EXTENSIONS_UNIVERSAL + "content-handler";

    /**
     * This non-standard property key is used to specify the name of the property file
     * that specifies character to entity reference mappings.
     */
    public static final String S_KEY_ENTITIES =
        S_BUILTIN_EXTENSIONS_UNIVERSAL + "entities";

    /** 
     * This non-standard property key is used to set a value of "yes" if the href values for HTML serialization should
     *  use %xx escaping. */
    public static final String S_USE_URL_ESCAPING =
        S_BUILTIN_EXTENSIONS_UNIVERSAL + "use-url-escaping";

    /** 
     * This non-standard property key is used to set a value of "yes" if the META tag should be omitted where it would
     *  otherwise be supplied.
     */
    public static final String S_OMIT_META_TAG =
        S_BUILTIN_EXTENSIONS_UNIVERSAL + "omit-meta-tag";

    /**
     * The old built-in extension namespace, this is not a public API.
     */
    public static final String S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL =
        "{" + S_BUILTIN_OLD_EXTENSIONS_URL + "}";

    /**
     * This is not a public API, it is only public because it is used
     * by outside of this package,
     * it is the length of the old built-in extension namespace.
     */
    public static final int S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL_LEN =
        S_BUILTIN_OLD_EXTENSIONS_UNIVERSAL.length();

    //************************************************************
    //*  PRIVATE CONSTANTS
    //************************************************************

    private static final String S_XSLT_PREFIX = "xslt.output.";
    private static final int S_XSLT_PREFIX_LEN = S_XSLT_PREFIX.length();
    private static final String S_XALAN_PREFIX = "org.apache.xslt.";
    private static final int S_XALAN_PREFIX_LEN = S_XALAN_PREFIX.length();

    /** Synchronization object for lazy initialization of the above tables. */
    private static Integer m_synch_object = new Integer(1);

    /** the directory in which the various method property files are located */
    private static final String PROP_DIR = "org/apache/xml/serializer/";
    /** property file for default XML properties */
    private static final String PROP_FILE_XML = "output_xml.properties";
    /** property file for default TEXT properties */
    private static final String PROP_FILE_TEXT = "output_text.properties";
    /** property file for default HTML properties */
    private static final String PROP_FILE_HTML = "output_html.properties";
    /** property file for default UNKNOWN (Either XML or HTML, to be determined later) properties */
    private static final String PROP_FILE_UNKNOWN = "output_unknown.properties";

    //************************************************************
    //*  PRIVATE STATIC FIELDS
    //************************************************************

    /** The default properties of all output files. */
    private static Properties m_xml_properties = null;

    /** The default properties when method="html". */
    private static Properties m_html_properties = null;

    /** The default properties when method="text". */
    private static Properties m_text_properties = null;

    /** The properties when method="" for the "unknown" wrapper */
    private static Properties m_unknown_properties = null;

    private static final Class
        ACCESS_CONTROLLER_CLASS = findAccessControllerClass();

    private static Class findAccessControllerClass() {
        try
        {
            // This Class was introduced in JDK 1.2. With the re-architecture of
            // security mechanism ( starting in JDK 1.2 ), we have option of
            // giving privileges to certain part of code using doPrivileged block.
            // In JDK1.1.X applications won't be having security manager and if
            // there is security manager ( in applets ), code need to be signed
            // and trusted for having access to resources.

            return Class.forName("java.security.AccessController");
        }
        catch (Exception e)
        {
            //User may be using older JDK ( JDK <1.2 ). Allow him/her to use it.
            // But don't try to use doPrivileged
        }

        return null;
    }

    /**
     * Creates an empty OutputProperties with the property key/value defaults specified by
     * a property file.  The method argument is used to construct a string of
     * the form output_[method].properties (for instance, output_html.properties).
     * The output_xml.properties file is always used as the base.
     * 
     * <p>Anything other than 'text', 'xml', and 'html', will
     * use the output_xml.properties file.</p>
     *
     * @param   method non-null reference to method name.
     *
     * @return Properties object that holds the defaults for the given method.
     */
    static public final Properties getDefaultMethodProperties(String method)
    {
        String fileName = null;
        Properties defaultProperties = null;
        // According to this article : Double-check locking does not work
        // http://www.javaworld.com/javaworld/jw-02-2001/jw-0209-toolbox.html
        try
        {
            synchronized (m_synch_object)
            {
                if (null == m_xml_properties) // double check
                {
                    fileName = PROP_FILE_XML;
                    m_xml_properties = loadPropertiesFile(fileName, null);
                }
            }

            if (method.equals(Method.XML))
            {
                defaultProperties = m_xml_properties;
            }
            else if (method.equals(Method.HTML))
            {
                if (null == m_html_properties) // double check
                {
                    fileName = PROP_FILE_HTML;
                    m_html_properties =
                        loadPropertiesFile(fileName, m_xml_properties);
                }

                defaultProperties = m_html_properties;
            }
            else if (method.equals(Method.TEXT))
            {
                if (null == m_text_properties) // double check
                {
                    fileName = PROP_FILE_TEXT;
                    m_text_properties =
                        loadPropertiesFile(fileName, m_xml_properties);
                    if (null
                        == m_text_properties.getProperty(OutputKeys.ENCODING))
                    {
                        String mimeEncoding = Encodings.getMimeEncoding(null);
                        m_text_properties.put(
                            OutputKeys.ENCODING,
                            mimeEncoding);
                    }
                }

                defaultProperties = m_text_properties;
            }
            else if (method.equals(org.apache.xml.serializer.Method.UNKNOWN))
            {
                if (null == m_unknown_properties) // double check
                {
                    fileName = PROP_FILE_UNKNOWN;
                    m_unknown_properties =
                        loadPropertiesFile(fileName, m_xml_properties);
                }

                defaultProperties = m_unknown_properties;
            }
            else
            {
                // TODO: Calculate res file from name.
                defaultProperties = m_xml_properties;
            }
        }
        catch (IOException ioe)
        {
            throw new WrappedRuntimeException(
                Utils.messages.createMessage(
                    MsgKey.ER_COULD_NOT_LOAD_METHOD_PROPERTY,
                    new Object[] { fileName, method }),
                ioe);
        }
        // wrap these cached defaultProperties in a new Property object just so
        // that the caller of this method can't modify the default values
        return new Properties(defaultProperties);
    }

    /**
     * Load the properties file from a resource stream.  If a
     * key name such as "org.apache.xslt.xxx", fix up the start of
     * string to be a curly namespace.  If a key name starts with
     * "xslt.output.xxx", clip off "xslt.output.".  If a key name *or* a
     * key value is discovered, check for \u003a in the text, and
     * fix it up to be ":", since earlier versions of the JDK do not
     * handle the escape sequence (at least in key names).
     *
     * @param resourceName non-null reference to resource name.
     * @param defaults Default properties, which may be null.
     */
    static private Properties loadPropertiesFile(
        final String resourceName,
        Properties defaults)
        throws IOException
    {

        // This static method should eventually be moved to a thread-specific class
        // so that we can cache the ContextClassLoader and bottleneck all properties file
        // loading throughout Xalan.

        Properties props = new Properties(defaults);

        InputStream is = null;
        BufferedInputStream bis = null;

        try
        {
            if (ACCESS_CONTROLLER_CLASS != null)
            {
                is = (InputStream) AccessController
                    .doPrivileged(new PrivilegedAction() {
                        public Object run()
                        {
                            return OutputPropertiesFactory.class
                                .getResourceAsStream(resourceName);
                        }
                    });
            }
            else
            {
                // User may be using older JDK ( JDK < 1.2 )
                is = OutputPropertiesFactory.class
                    .getResourceAsStream(resourceName);
            }

            bis = new BufferedInputStream(is);
            props.load(bis);
        }
        catch (IOException ioe)
        {
            if (defaults == null)
            {
                throw ioe;
            }
            else
            {
                throw new WrappedRuntimeException(
                    Utils.messages.createMessage(
                        MsgKey.ER_COULD_NOT_LOAD_RESOURCE,
                        new Object[] { resourceName }),
                    ioe);
                //"Could not load '"+resourceName+"' (check CLASSPATH), now using just the defaults ", ioe);
            }
        }
        catch (SecurityException se)
        {
            // Repeat IOException handling for sandbox/applet case -sc
            if (defaults == null)
            {
                throw se;
            }
            else
            {
                throw new WrappedRuntimeException(
                    Utils.messages.createMessage(
                        MsgKey.ER_COULD_NOT_LOAD_RESOURCE,
                        new Object[] { resourceName }),
                    se);
                //"Could not load '"+resourceName+"' (check CLASSPATH, applet security), now using just the defaults ", se);
            }
        }
        finally
        {
            if (bis != null)
            {
                bis.close();
            }
            if (is != null)
            {
                is.close();
            }
        }

        // Note that we're working at the HashTable level here,
        // and not at the Properties level!  This is important
        // because we don't want to modify the default properties.
        // NB: If fixupPropertyString ends up changing the property
        // name or value, we need to remove the old key and re-add
        // with the new key and value.  However, then our Enumeration
        // could lose its place in the HashTable.  So, we first
        // clone the HashTable and enumerate over that since the
        // clone will not change.  When we migrate to Collections,
        // this code should be revisited and cleaned up to use
        // an Iterator which may (or may not) alleviate the need for
        // the clone.  Many thanks to Padraig O'hIceadha
        // <padraig@gradient.ie> for finding this problem.  Bugzilla 2000.

        Enumeration keys = ((Properties) props.clone()).keys();
        while (keys.hasMoreElements())
        {
            String key = (String) keys.nextElement();
            // Now check if the given key was specified as a
            // System property. If so, the system property
            // overides the default value in the propery file.
            String value = null;
            try
            {
                value = System.getProperty(key);
            }
            catch (SecurityException se)
            {
                // No-op for sandbox/applet case, leave null -sc
            }
            if (value == null)
                value = (String) props.get(key);

            String newKey = fixupPropertyString(key, true);
            String newValue = null;
            try
            {
                newValue = System.getProperty(newKey);
            }
            catch (SecurityException se)
            {
                // No-op for sandbox/applet case, leave null -sc
            }
            if (newValue == null)
                newValue = fixupPropertyString(value, false);
            else
                newValue = fixupPropertyString(newValue, false);

            if (key != newKey || value != newValue)
            {
                props.remove(key);
                props.put(newKey, newValue);
            }

        }

        return props;
    }

    /**
     * Fix up a string in an output properties file according to
     * the rules of {@link #loadPropertiesFile}.
     *
     * @param s non-null reference to string that may need to be fixed up.
     * @return A new string if fixup occured, otherwise the s argument.
     */
    static private String fixupPropertyString(String s, boolean doClipping)
    {
        int index;
        if (doClipping && s.startsWith(S_XSLT_PREFIX))
        {
            s = s.substring(S_XSLT_PREFIX_LEN);
        }
        if (s.startsWith(S_XALAN_PREFIX))
        {
            s =
                S_BUILTIN_EXTENSIONS_UNIVERSAL
                    + s.substring(S_XALAN_PREFIX_LEN);
        }
        if ((index = s.indexOf("\\u003a")) > 0)
        {
            String temp = s.substring(index + 6);
            s = s.substring(0, index) + ":" + temp;

        }
        return s;
    }

}
