/**
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 1999-2003 (C) Intalio, Inc. All Rights Reserved.
 *
 *
 * $Id: BuilderConfiguration.java 6302 2006-10-14 05:51:42Z ekuns $
 */

package org.exolab.castor.builder;

//--Castor imports
import org.exolab.castor.util.Configuration;
import org.exolab.castor.util.LocalConfiguration;
import org.exolab.castor.xml.JavaNaming;

//--Java IO imports
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;

//--Java util imports
import java.util.Enumeration;
import java.util.Properties;
import java.util.Hashtable;
import java.util.StringTokenizer;

/**
 * The configuration for the SourceGenerator
 *
 * @author <a href="mailto:kvisco@intalio.com">Keith Visco</a>
 * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
 * @version $Revision: 6302 $ $Date: 2006-04-25 15:08:23 -0600 (Tue, 25 Apr 2006) $
 */
public class BuilderConfiguration {

    /**
     * Names of properties used in the configuration file.
     */
    public static class Property {
        /**
         * Property specifying whether or not to generate source code for bound
         * properties. Currently all properties will be treated as bound
         * properties if this flag is set to true. A value of 'true' enables
         * bound properties.
         *
         * <pre>
         * org.exolab.castor.builder.boundproperties
         * </pre>
         */
        public static final String BOUND_PROPERTIES = "org.exolab.castor.builder.boundproperties";

        /**
         * Property specifying whether to implement EnumeratedTypeAccess
         * interface for all generated enumerated type classes
         *
         * <pre>
         * org.exolab.castor.builder.enumTypeAccessInterface
         * </pre>
         */
        public static final String ENUM_TYPE_ACCESS_INTERFACE = "org.exolab.castor.builder.enumTypeAccessInterface";

        /**
         * Property specifying whether or not to generate source code for extra
         * collection methods.
         *
         * <pre>
         * org.exolab.castor.builder.extraCollectionMethods
         * </pre>
         */
        public static final String EXTRA_COLLECTION_METHODS = "org.exolab.castor.builder.extraCollectionMethods";

        /**
         * Property specifying the super class for all generated classes
         *
         * <pre>
         * org.exolab.castor.builder.superclass
         * </pre>
         */
        public static final String SUPER_CLASS = "org.exolab.castor.builder.superclass";

        /**
         * Property specifying how element's and type's are mapped into a Java
         * class hierarchy by the Source Generator. The value must contain one
         * of the following. 'element' outputs a Java class hierarchy based on
         * element names used in the XML Schema. This is the default. 'type'
         * outputs a Java class hierarchy based on the type information defined
         * in the XML Schema.
         *
         * <pre>
         * org.exolab.castor.builder.javaclassmapping
         * </pre>
         */
        public static final String JavaClassMapping = "org.exolab.castor.builder.javaclassmapping";

        /**
         * Property listing mapping between XML namespaces and Java packages.
         */
        public static final String NamespacePackagesOld = "org.exolab.castor.builder.nspackages";

        /**
         * Property listing mapping between XML namespaces and Java packages.
         */
        public static final String NamespacePackages = "org.exolab.castor.xml.nspackages";

        /**
         * Property specifying if we want to have the equals method generated
         * for each generated class
         */
        public static final String EqualsMethod = "org.exolab.castor.builder.equalsmethod";

        /**
         * Property specifying if we want to use Wrapper Objects instead of
         * primitives (eg java.lang.Float instead of float)
         */
        public static final String Wrapper = "org.exolab.castor.builder.primitivetowrapper";

        /**
         * Property specifying if we want to have a 'public static final String'
         * generated for each attribute and element name used within a class
         * descriptor
         */
        public static final String ClassDescFieldNames = "org.exolab.castor.builder.classdescfieldnames";

        /**
         * Property specifying whether the Java sources generated should be 1.4
         * or 5.0 compliant.
         */
        public static final String JavaVersion = "org.exolab.castor.builder.javaVersion";

        /**
         * The name of the configuration file.
         *
         * <pre>
         * castor.properties
         * </pre>
         */
        public static final String CONFIG_FILENAME_PROPERTY = "castorbuilder.properties";

        static final String ResourceName = "/org/exolab/castor/builder/castorbuilder.properties";
    } //--Property

    /**
     * String value of false
     */
    private static final String FALSE = "false";

    /**
     * String value of true
     */
    private static final String TRUE = "true";

    /**
     * String value of element binding property
     */
    private static final String ELEMENT_VALUE = "element";

    /**
     * String value of type binding property
     */
    private static final String TYPE_VALUE = "type";

    /**
     * The default properties loaded from the configuration file.
     */
    private Properties _defaultProps = null;

    private Properties _localProps = null;

    /**
     * Namespace URL to Java package mapping
     */
    private Hashtable _nspackages = new Hashtable();

    /**
     * schemaLocation to Java package mapping
     */
    private Hashtable _locpackages = new Hashtable();

    //------------------/

    /**
     * Creates a default BuilderConfiguration.
     */
    public BuilderConfiguration() {
        super();
        getDefault();
        _localProps = new Properties(_defaultProps);
    } //-- BuilderConfiguration

    /**
     * Returns the default configuration file. Changes to the returned
     * properties set will affect all Castor functions relying on the default
     * configuration.
     *
     * @return The default configuration
     */
    public synchronized Properties getDefault() {
        if (_defaultProps == null) {
            load();
        }
        return _defaultProps;
    } //-- getDefault

    /**
     * Returns a property from the default configuration file. Equivalent to
     * calling <tt>getProperty</tt> on the result of {@link #getDefault}.
     *
     * @param name
     *            The property name
     * @param defValue
     *            The property's default value
     * @return The property's value
     */
    public String getProperty(final String name, final String defValue) {
        return _localProps.getProperty(name, defValue);
    } //-- getProperty

    /**
     * Returns true if bound properties are enabled.
     * <p>
     * Enabling bound properties is controlled via the
     * org.exolab.castor.builder.boundproperties item in the
     * castorbuilder.properties file. The value is either 'true' or 'false'.
     *
     * @return true if bound properties are enabled.
     */
    public boolean boundPropertiesEnabled() {
        return TRUE.equalsIgnoreCase(_localProps.getProperty(Property.BOUND_PROPERTIES));
    } //-- boundPropertiesEnabled

    /**
     * Returns true if we generate an 'equals' method for each generated class.
     * <p>
     * Enabling this property is controlled via the
     * org.exolab.castor.builder.equalsmethod item in the
     * castorbuilder.properties file. The value is either 'true' or 'false'.
     *
     * @return true if bound properties are enabled.
     */
    public boolean equalsMethod() {
        return TRUE.equalsIgnoreCase(_localProps.getProperty(Property.EqualsMethod));
    } //-- equalsMethod

    /**
     * Sets the 'equalsmethod' property.
     *
     * @param equals The value we want to use.
     */
     public void setEqualsMethod(final boolean equals) {
        String value = (equals) ? TRUE : FALSE;
        _localProps.setProperty(Property.EqualsMethod, value);
     } //-- setEqualsMethod

    /**
     * Returns true if we generate a 'public static final String' for the name
     * of each attribute and element described by the class descriptor
     * <p>
     * Enabling this property is controlled via the
     * org.exolab.castor.builder.classdescfieldnames item in the
     * castorbuilder.properties file. The value is either 'true' or 'false'.
     *
     * @return true if bound properties are enabled.
     */
    public boolean classDescFieldNames() {
        return _localProps.getProperty(Property.ClassDescFieldNames, "").equalsIgnoreCase(TRUE);
    } //-- classDescFieldNames

    /**
     * Returns true if extra methods for collection fields should be generated.
     * Such methods include set/get methods for the actual collection in
     * addition to the array methods.
     * <p>
     * Enabling extra collection methods is controlled via the
     * org.exolab.castor.builder.extraCollectionMethods property in the
     * castorbuilder.properties file. The value is either 'true' or 'false'.
     *
     * @return true if extra collection methods are enabled.
     */
    public boolean generateExtraCollectionMethods() {
        return _localProps.getProperty(Property.EXTRA_COLLECTION_METHODS, "").equalsIgnoreCase(TRUE);
    } //-- generateExtraCollectionMethods

    /**
     * Sets the 'classDescFieldNames' property
     *
     * @param classDescFieldNames
     *            the value we want to ues
     */
     public void setClassDescFieldNames(final boolean classDescFieldNames) {
        String value = (classDescFieldNames) ? TRUE : FALSE;
        _localProps.getProperty(Property.ClassDescFieldNames, value);
     } //-- setClassDescFieldNames

     /**
      * Returns true if primitive types have to be used as Objects (eg.
      * replacing float by java.lang.Float).
      * @return true if primitive types have to be used as Objects.
      */
    public boolean usePrimitiveWrapper() {
        return _localProps.getProperty(Property.Wrapper, "").equalsIgnoreCase(TRUE);
    } //-- usePrimitiveWrapper

    /**
     * Sets the 'primitivetowrapper' property
     *
     * @param wrapper the value we want to use.
     */
    public void setPrimitiveWrapper(final boolean wrapper) {
        String value = (wrapper) ? TRUE : FALSE;
        _localProps.setProperty(Property.Wrapper, value);
    } //-- setPrimitiveWrapper

    /**
     * Returns true if we generate the implements EnumeratedTypeAccess interface
     * for enumerated type classes. The value is either 'true' or 'false'
     *
     * @return true if use enumerated type interface is enabled
     */
    public boolean useEnumeratedTypeInterface() {
        return TRUE.equalsIgnoreCase(_localProps.getProperty(Property.ENUM_TYPE_ACCESS_INTERFACE));
    } //-- useEnumeratedTypeInterface

    /**
     * Returns true if we generate the implements EnumeratedTypeAccess interface
     * for enumerated type classes. The value is either 'true' or 'false'
     *
     * @return true if use enumerated type interface is enabled
     */
    public boolean useJava50() {
        return "5.0".equalsIgnoreCase(_localProps.getProperty(Property.JavaVersion, "1.4"));
    } //-- useEnumeratedTypeInterface

    /**
     * Sets the 'enumTypeAccessInterface' property
     *
     * @param flag the value we want to use
     */
    public void setUseEnumeratedTypeInterface(final boolean flag) {
        String value = (flag) ? TRUE : FALSE;
        _localProps.setProperty(Property.ENUM_TYPE_ACCESS_INTERFACE, value);
    } //-- setUseEnumeratedTypeInterface

    /**
     * Tests the org.exolab.castor.builder.javaclassmapping property for the
     * 'element' value.
     *
     * @return True if the Source Generator is mapping schema elements to Java
     *         classes.
     */
    public boolean mappingSchemaElement2Java() {
        String value = _localProps.getProperty(Property.JavaClassMapping, "");
        return ELEMENT_VALUE.equalsIgnoreCase(value);
    } //-- mappingSchemaElement2Java

    /**
     * Tests the org.exolab.castor.builder.javaclassmapping property for the 'type' value.
     *
     * @return True if the Source Generator is mapping schema types to Java classes.
     */
    public boolean mappingSchemaType2Java() {
        String value = _localProps.getProperty(Property.JavaClassMapping, "");
        return TYPE_VALUE.equalsIgnoreCase(value);
    } //-- mappingSchemaType2Java

    /**
     * Overrides the current set of properties with the given properties. Once
     * the properties are set, only a copy will be uses, so any changes to the
     * given properties file after the fact will go unnoticed.
     *
     * @param properties
     *            the Properties file
     */
    public void setDefaultProperties(final Properties properties) {
        Properties defaults = null;
        if (properties == null) {
            defaults = _defaultProps;
        } else {
            defaults = new Properties(_defaultProps);
            Enumeration enumeration = properties.keys();
            while (enumeration.hasMoreElements()) {
                String name = (String) enumeration.nextElement();
                defaults.setProperty(name, properties.getProperty(name));
            }
        }
        _localProps = new Properties(defaults);
        processNamespacePackageMappings(_localProps.getProperty(Property.NamespacePackagesOld, ""));
        processNamespacePackageMappings(_localProps.getProperty(Property.NamespacePackages, ""));
    } //-- setDefaultProperties

    /**
     * Sets the namespace to package mapping.
     *
     * @param ns the namespace URI to map
     * @param packageName the package name
     */
    public void setNamespacePackageMapping(final String ns, final String packageName) {
        _nspackages.put(ns, packageName);
    } //-- setNamespcaePackageMapping

    /**
     * Sets the schemaLocation to package mapping.
     *
     * @param schemaLocation the schemaLocation to map
     * @param packageName the package name to map to
     */
    public void setLocationPackageMapping(final String schemaLocation, final String packageName) {
        _locpackages.put(schemaLocation, packageName);
    }

    /**
     * Called by {@link #getDefault} to load the configuration the first time.
     * Will not complain about inability to load configuration file from one of
     * the default directories, but if it cannot find the JAR's configuration
     * file, will throw a run time exception.
     */
    protected synchronized void load() {
        if (_defaultProps == null) {
            //-- load defaults from JAR
            _defaultProps = Configuration.loadProperties(Property.ResourceName, Property.CONFIG_FILENAME_PROPERTY);

            //-- load local defaults

            boolean found = false;

            // Get overriding configuration from the classpath, ignore if not found.
            // If found, merge any existing properties.
            try {
                InputStream is = SourceGenerator.class.getResourceAsStream("/" + Property.CONFIG_FILENAME_PROPERTY);
                if (is != null) {
                    found = true;
                    _defaultProps.load(is);
                    is.close();
                }
            } catch (Exception except) {
                //-- do nothing
            }

            //-- if not found, either it doesn't exist, or "." is not part of the
            //-- class path, try looking at local working directory
            if (!found) {
                try {
                    File file = new File(Property.CONFIG_FILENAME_PROPERTY);
                    if (file.exists() && file.canRead()) {
                        InputStream is = new FileInputStream(file);
                        _defaultProps.load(is);
                        is.close();
                    }
                } catch (Exception except) {
                    //-- do nothing
                }
            }
        }

        Configuration rtconf =  LocalConfiguration.getInstance();

        // Parse XML namespace and package list from both castor.properties and
        // castorbuilder.properties
        processNamespacePackageMappings(rtconf.getProperty(Property.NamespacePackagesOld, ""));
        processNamespacePackageMappings(rtconf.getProperty(Property.NamespacePackages, ""));
        processNamespacePackageMappings(_defaultProps.getProperty(Property.NamespacePackagesOld, ""));
        processNamespacePackageMappings(_defaultProps.getProperty(Property.NamespacePackages, ""));

        //-- backward compatibility with 0.9.3.9
        String prop = _defaultProps.getProperty(JavaNaming.UPPER_CASE_AFTER_UNDERSCORE_PROPERTY, null);
        if (prop != null) {
            JavaNaming.upperCaseAfterUnderscore = Boolean.valueOf(prop).booleanValue();
        }
    } //-- load

    /**
     * Gets a Java package to an XML namespace URL
     * @param nsURL the XML namespace URL to convert into a Java package name
     * @return a Java package name
     */
    public String lookupPackageByNamespace(String nsURL) {
        if (nsURL == null) {
            nsURL = "";
        }

        // Lookup Java package via NS
        String javaPackage = (String) _nspackages.get(nsURL);
        if (javaPackage == null) {
            return "";
        }
        return javaPackage;
    } //-- lookupPackageNamespace

    /**
     * Converts a schema location into a Java package.
     *
     * @param schemaLocation
     *            the Schema location to use to look up the Java package
     * @return a Java package name
     */
    public String lookupPackageByLocation(String schemaLocation) {
        if (schemaLocation == null) {
            return "";
        }

        // Lookup Java package via schemaLocation
        //--Full path
        String javaPackage = (String) _locpackages.get(schemaLocation);
        if (javaPackage == null) {
            //--maybe a relative schemaLocation was given
            while (schemaLocation.startsWith(".")) {
                 if (schemaLocation.startsWith("./")) {
                     schemaLocation = schemaLocation.substring(2);
                 } else if (schemaLocation.startsWith("../")) {
                     schemaLocation = schemaLocation.substring(3);
                 }
            }
            Enumeration keys = _locpackages.keys();
            boolean found = false;
            while (keys.hasMoreElements() && !found) {
                String key = (String) keys.nextElement();
                if (schemaLocation.endsWith(key)) {
                    javaPackage = (String) _locpackages.get(key);
                    found = true;
                }
            }
            if (javaPackage == null) {
                javaPackage = "";
            }
        }

        return javaPackage;
    } //-- lookupPackageLocation

    /**
     * processes the given String which contains namespace-to-package mappings
     *
     * @param mappings the namespace-to-package mappings
     */
    protected void processNamespacePackageMappings(final String mappings) {
        if (mappings == null) {
            return;
        }

        StringTokenizer tokens = new StringTokenizer(mappings, ",");
        while (tokens.hasMoreTokens()) {
            String token = tokens.nextToken();
            int sepIdx = token.indexOf('=');
            if (sepIdx < 0) {
                continue;
            }

            String ns = token.substring(0, sepIdx).trim();
            String javaPackage = token.substring(sepIdx + 1).trim();
            _nspackages.put(ns, javaPackage);
        }
    } //-- processNamespacePackageMappings

} //-- BuilderProperties
