/**
 *
 *  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 commonj.sdo.impl;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.security.AccessController;
import java.security.PrivilegedAction;

import commonj.sdo.helper.CopyHelper;
import commonj.sdo.helper.DataFactory;
import commonj.sdo.helper.DataHelper;
import commonj.sdo.helper.EqualityHelper;
import commonj.sdo.helper.TypeHelper;
import commonj.sdo.helper.XMLHelper;
import commonj.sdo.helper.XSDHelper;

/**
 * A HelperProvider obtains specific default helpers and other
 * implementation-specific objects used by a Java implementation of SDO.
 *
 * @version $Rev$ $Date$
 */
public abstract class HelperProvider {
    /**
     * The default HelperProvider INSTANCE. This is located using the ClassLoader used
     * to load the HelperProvider class itself and if no default implementation is available
     * this field will be set to null.
     */
    public static final HelperProvider INSTANCE;

    /**
     * The name of the resource that is used for service location.
     */
    public static final String SERVICE_RESOURCE_NAME = "META-INF/services/commonj.sdo.impl.HelperProvider";

    /**
     * The name of the system property that will be checked for an implementation name.
     */
    public static final String PROPERTY_NAME = "commonj.sdo.impl.HelperProvider";

    static {
        // initialize the default instance using this class's classloader
        // set to null if none could be located (implies no default implementation)
        HelperProvider provider;
        try {
            provider = getInstance(HelperProvider.class.getClassLoader());
        } catch (NoHelperProviderException e) {
            provider = null;
        }
        INSTANCE = provider;
    }

    /**
     * Locate and instantiate a HelperProvider.
     * <p/>
     * Attempt to locate a HelperProvider using first the Thread's current context classloader and then,
     * if that is not set, not readable, or does not provide an implementation, using the classloader
     * used to load the HelperProvider class itself.
     * <p/>
     * A new instance is returned for each sucessful invocation.
     *
     * @return an implementation of HelperProvider
     * @throws NoHelperProviderException if no provider implementation was defined or it could not be instantiated
     */
    public static HelperProvider getInstance() throws NoHelperProviderException {
        String implName = getImplementationName();

        ClassLoader cl = getContextClassLoader();
        if (cl != null) {
            HelperProvider provider = loadImplementation(cl, implName);
            if (provider != null) {
                return provider;
            }
        }

        cl = HelperProvider.class.getClassLoader();
        HelperProvider provider = loadImplementation(cl, implName);
        if (provider != null) {
            return provider;
        }

        throw new NoHelperProviderException(implName);
    }


    /**
     * Locate and instantiate a HelperProvider using the supplied ClassLoader.
     * <p/>
     * The name of the implementation to use is determined by the value of the "commonj.sdo.impl.HelperProvider"
     * system property. If this is not set or this code does not have permission to read it then the name
     * will be retrieved from the META-INF/services/commonj.sdo.impl.HelperProvider resource as returned
     * by the supplied classloader as described in the
     * <a href="http://java.sun.com/j2se/1.5.0/docs/guide/jar/jar.html#Service%20Provider">JAR file specification</a>.
     * <p/>
     * A new instance is returned for each sucessful invocation.
     *
     * @param cl the classloader to use to locate and instantiate the implementation
     * @return the specified implementation of HelperProvider
     * @throws NoHelperProviderException if no provider implementation was defined or it could not be instantiated
     */
    public static HelperProvider getInstance(ClassLoader cl) throws NoHelperProviderException {
        String implName = getImplementationName();
        HelperProvider provider = loadImplementation(cl, implName);
        if (provider == null) {
            throw new NoHelperProviderException(implName);
        }
        return provider;
    }

    private static ClassLoader getContextClassLoader() {
        try {
            return (ClassLoader)AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return Thread.currentThread().getContextClassLoader();
                }
            });
        } catch (SecurityException e) {
            return null;
        }
    }

    private static HelperProvider loadImplementation(ClassLoader cl, String implName) throws NoHelperProviderException {
        // if no name is requested, locate using the supplied classloader
        if (implName == null) {
            implName = getImplementationName(cl);
        }
        // no implementation to try, return null
        if (implName == null) {
            return null;
        }

        // try an instantiate the implementation
        try {
            return (HelperProvider) cl.loadClass(implName).newInstance();
        } catch (InstantiationException e) {
            throw new NoHelperProviderException(implName, e);
        } catch (IllegalAccessException e) {
            throw new NoHelperProviderException(implName, e);
        } catch (ClassNotFoundException e) {
            throw new NoHelperProviderException(implName, e);
        }
    }

    private static String getImplementationName() {
        try {
            return (String)AccessController.doPrivileged(new PrivilegedAction() {
                public Object run() {
                    return System.getProperty(PROPERTY_NAME);
                }
            });
        } catch (SecurityException e) {
            return null;
        }
    }

    private static String getImplementationName(ClassLoader cl) {
        InputStream is = cl.getResourceAsStream(SERVICE_RESOURCE_NAME);
        if (is == null) {
            return null;
        }

        InputStreamReader in;
        try {
            in = new InputStreamReader(is, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new AssertionError("UTF-8 encoding not available");
        }

        try {
            BufferedReader reader = new BufferedReader(in, 128);
            try {
                String line;
                while ((line = reader.readLine()) != null) {
                    int i = line.indexOf('#');
                    if (i != -1) {
                        line = line.substring(0, i);
                    }
                    line = line.trim();
                    if (line.length() > 0) {
                        return line;
                    }
                }
                return null;
            } finally {
                reader.close();
            }
        } catch (IOException e) {
            throw new NoHelperProviderException(e);
        }
    }


    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // implementation specific methods for users that don't want to use the default implementation

    /**
     * Returns a CopyHelper obtained from this implementation.
     *
     * @return a CopyHelper obtained from this implementation
     */
    public abstract CopyHelper copyHelper();

    /**
     * Returns a DataFactory obtained from this implementation.
     *
     * @return a DataFactory obtained from this implementation
     */
    public abstract DataFactory dataFactory();

    /**
     * Returns a DataHelper obtained from this implementation.
     *
     * @return a DataHelper obtained from this implementation
     */
    public abstract DataHelper dataHelper();

    /**
     * Returns a EqualityHelper obtained from this implementation.
     *
     * @return a EqualityHelper obtained from this implementation
     */
    public abstract EqualityHelper equalityHelper();

    /**
     * Returns a TypeHelper obtained from this implementation.
     *
     * @return a TypeHelper obtained from this implementation
     */
    public abstract TypeHelper typeHelper();

    /**
     * Returns a XMLHelper obtained from this implementation.
     *
     * @return a XMLHelper obtained from this implementation
     */
    public abstract XMLHelper xmlHelper();

    /**
     * Returns a XSDHelper obtained from this implementation.
     *
     * @return a XSDHelper obtained from this implementation
     */
    public abstract XSDHelper xsdHelper();

    /**
     * Create a Resolvable using this implementation
     *
     * @return a Resolvable created using this implementation
     */
    public abstract ExternalizableDelegator.Resolvable resolvable();

    /**
     * Create a Resolvable using this implementation
     *
     * @param target the object to be resolved
     * @return a Resolvable created using this implementation
     */
    public abstract ExternalizableDelegator.Resolvable resolvable(Object target);


    ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
    // static helper methods required by the specification

    /**
     * Returns a CopyHelper obtained from the default HelperProvider.
     *
     * @return a CopyHelper obtained from the default HelperProvider
     */
    public static CopyHelper getCopyHelper() {
        return INSTANCE.copyHelper();
    }

    /**
     * Returns a DataFactory obtained from the default HelperProvider.
     *
     * @return a DataFactory obtained from the default HelperProvider
     */
    public static DataFactory getDataFactory() {
        return INSTANCE.dataFactory();
    }

    /**
     * Returns a DataHelper obtained from the default HelperProvider.
     *
     * @return a DataHelper obtained from the default HelperProvider
     */
    public static DataHelper getDataHelper() {
        return INSTANCE.dataHelper();
    }

    /**
     * Returns a EqualityHelper obtained from the default HelperProvider.
     *
     * @return a EqualityHelper obtained from the default HelperProvider
     */
    public static EqualityHelper getEqualityHelper() {
        return INSTANCE.equalityHelper();
    }

    /**
     * Returns a TypeHelper obtained from the default HelperProvider.
     *
     * @return a TypeHelper obtained from the default HelperProvider
     */
    public static TypeHelper getTypeHelper() {
        return INSTANCE.typeHelper();
    }

    /**
     * Returns a XMLHelper obtained from the default HelperProvider.
     *
     * @return a XMLHelper obtained from the default HelperProvider
     */
    public static XMLHelper getXMLHelper() {
        return INSTANCE.xmlHelper();
    }

    /**
     * Returns a XSDHelper obtained from the default HelperProvider.
     *
     * @return a XSDHelper obtained from the default HelperProvider
     */
    public static XSDHelper getXSDHelper() {
        return INSTANCE.xsdHelper();
    }

    /**
     * Create a Resolvable using the default HelperProvider
     *
     * @return a Resolvable created using the default HelperProvider
     */
    public static ExternalizableDelegator.Resolvable createResolvable() {
        return INSTANCE.resolvable();
    }

    /**
     * Create a Resolvable using the default HelperProvider
     *
     * @param target the object to be resolved
     * @return a Resolvable created using the default HelperProvider
     */
    public static ExternalizableDelegator.Resolvable createResolvable(Object target) {
        return INSTANCE.resolvable(target);
    }
}
