/*****************************************************************
 *   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 org.apache.cayenne.conf;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

import org.apache.cayenne.CayenneRuntimeException;
import org.apache.cayenne.ConfigurationException;
import org.apache.cayenne.access.DataDomain;
import org.apache.cayenne.event.EventManager;
import org.apache.cayenne.util.ResourceLocator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * This class is an entry point to Cayenne. It loads all configuration files and
 * instantiates main Cayenne objects. Used as a singleton via the
 * {@link #getSharedConfiguration}method.
 * <p>
 * To use a custom subclass of Configuration, Java applications must call
 * {@link #initializeSharedConfiguration}with the subclass as argument. This will create
 * and initialize a Configuration singleton instance of the specified class. By default
 * {@link DefaultConfiguration}is instantiated.
 * </p>
 * 
 */
public abstract class Configuration {

    private static Log logObj = LogFactory.getLog(Configuration.class);

    public static final String DEFAULT_DOMAIN_FILE = "cayenne.xml";
    public static final Class<DefaultConfiguration> DEFAULT_CONFIGURATION_CLASS = DefaultConfiguration.class;

    protected static Configuration sharedConfiguration;

    /**
     * Lookup map that stores DataDomains with names as keys.
     */
    protected SortedMap<String, DataDomain> dataDomains = new TreeMap<String, DataDomain>();
    protected DataSourceFactory overrideFactory;
    protected ConfigStatus loadStatus = new ConfigStatus();
    protected String domainConfigurationName = DEFAULT_DOMAIN_FILE;
    protected boolean ignoringLoadFailures;
    protected ConfigLoaderDelegate loaderDelegate;
    protected ConfigSaverDelegate saverDelegate;
    protected ConfigurationShutdownHook configurationShutdownHook;
    protected Map<String, String> dataViewLocations = new HashMap<String, String>();
    protected String projectVersion;

    /**
     * @since 1.2
     */
    protected EventManager eventManager;

    /**
     * Use this method as an entry point to all Cayenne access objects.
     * <p>
     * Note that if you want to provide a custom Configuration, make sure you call one of
     * the {@link #initializeSharedConfiguration}methods before your application code has
     * a chance to call this method.
     */
    public synchronized static Configuration getSharedConfiguration() {
        if (Configuration.sharedConfiguration == null) {
            Configuration.initializeSharedConfiguration();
        }

        return Configuration.sharedConfiguration;
    }

    /**
     * Returns EventManager used by this configuration.
     * 
     * @since 1.2
     */
    public EventManager getEventManager() {
        return eventManager;
    }

    /**
     * Sets EventManager used by this configuration.
     * 
     * @since 1.2
     */
    public void setEventManager(EventManager eventManager) {
        this.eventManager = eventManager;
    }

    /**
     * Creates and initializes shared Configuration object. By default
     * {@link DefaultConfiguration}will be instantiated and assigned to a singleton
     * instance of Configuration.
     */
    public static void initializeSharedConfiguration() {
        Configuration.initializeSharedConfiguration(DEFAULT_CONFIGURATION_CLASS);
    }

    /**
     * Creates and initializes a shared Configuration object of a custom Configuration
     * subclass.
     */
    public static void initializeSharedConfiguration(
            Class<? extends Configuration> configurationClass) {
        Configuration conf = null;

        try {
            conf = configurationClass.newInstance();
        }
        catch (Exception ex) {
            logObj.error("Error creating shared Configuration: ", ex);
            throw new ConfigurationException("Error creating shared Configuration."
                    + ex.getMessage(), ex);
        }

        Configuration.initializeSharedConfiguration(conf);
    }

    /**
     * Sets the shared Configuration object to a new Configuration object. First calls
     * {@link #canInitialize}and - if permitted -{@link #initialize}followed by
     * {@link #didInitialize}.
     */
    public static void initializeSharedConfiguration(Configuration conf) {
        // check to see whether we can proceed
        if (!conf.canInitialize()) {
            throw new ConfigurationException("Configuration of class "
                    + conf.getClass().getName()
                    + " refused to be initialized.");
        }

        try {
            // initialize configuration
            conf.initialize();

            // call post-initialization hook
            conf.didInitialize();

            // set the initialized Configuration only after success
            Configuration.sharedConfiguration = conf;
        }
        catch (Exception ex) {
            throw new ConfigurationException(
                    "Error during Configuration initialization. " + ex.getMessage(),
                    ex);
        }
    }

    /**
     * Default constructor for new Configuration instances. Simply calls
     * {@link Configuration#Configuration(String)}.
     * 
     * @see Configuration#Configuration(String)
     */
    protected Configuration() {
        this(DEFAULT_DOMAIN_FILE);
    }

    /**
     * Default constructor for new Configuration instances using the given resource name
     * as the main domain file.
     */
    protected Configuration(String domainConfigurationName) {

        // set domain configuration name
        this.setDomainConfigurationName(domainConfigurationName);

        this.eventManager = new EventManager();
    }

    /**
     * Indicates whether {@link #initialize}can be called. Returning <code>false</code>
     * allows new instances to delay or refuse the initialization process. This
     * impementation returns true unconditionally.
     * 
     * @deprecated since 3.0 - this method is redundant, as subclasses can prevent
     *             initialization by overriding {@link #initialize()} and throwing an
     *             exception.
     */
    public boolean canInitialize() {
        return true;
    }

    /**
     * Initializes the new instance.
     * 
     * @throws Exception
     */
    public abstract void initialize() throws Exception;

    /**
     * Called after successful completion of {@link #initialize}. This implementation is
     * a noop.
     * 
     * @deprecated since 3.0 subclasses are recommended to override {@link #initialize()}.
     */
    public void didInitialize() {
        // noop
    }

    /**
     * Returns the resource locator used for finding and loading resources.
     * 
     * @deprecated since 3.0 use/override {@link #getResourceFinder()}.
     */
    protected ResourceLocator getResourceLocator() {
        ResourceFinder finder = getResourceFinder();
        if (finder == null) {
            return null;
        }

        if (finder instanceof ResourceLocator) {
            return (ResourceLocator) finder;
        }

        throw new IllegalStateException("ResourceFinder is not a ResourceLocator: "
                + finder);
    }

    /**
     * Returns {@link ResourceFinder} associated with this Configuration that is to be
     * used for config files lookup.
     * 
     * @since 3.0
     */
    protected abstract ResourceFinder getResourceFinder();

    /**
     * Returns a DataDomain as a stream or <code>null</code> if it cannot be found.
     * 
     * @deprecated since 3.0 This method is specific to subclass, so it should not be in
     *             the superclass.
     */
    protected InputStream getDomainConfiguration() {
        URL url = getResourceFinder().getResource(getDomainConfigurationName());
        try {
            return url != null ? url.openStream() : null;
        }
        catch (IOException e) {
            throw new ConfigurationException("Can't open config file URL: " + url, e);
        }
    }

    /**
     * Returns a DataMap with the given name or <code>null</code> if it cannot be found.
     */
    protected InputStream getMapConfiguration(String name) {
        URL url = getResourceFinder().getResource(name);
        try {
            return url != null ? url.openStream() : null;
        }
        catch (IOException e) {
            throw new ConfigurationException("Can't open config file URL: " + url, e);
        }
    }

    /**
     * See 'https://svn.apache.org/repos/asf/cayenne/dataviews/trunk' for DataViews code,
     * which is not a part of Cayenne since 3.0.
     * 
     * @deprecated since 3.0 as Cayenne no longer cares to read view config files.
     */
    protected InputStream getViewConfiguration(String location) {
        URL url = getResourceFinder().getResource(location);
        try {
            return url != null ? url.openStream() : null;
        }
        catch (IOException e) {
            throw new ConfigurationException("Can't open config file URL: " + url, e);
        }
    }

    /**
     * Returns the name of the main domain configuration resource. Defaults to
     * {@link Configuration#DEFAULT_DOMAIN_FILE}.
     */
    public String getDomainConfigurationName() {
        return domainConfigurationName;
    }

    /**
     * Sets the name of the main domain configuration resource.
     * 
     * @param domainConfigurationName the name of the resource that contains this
     *            Configuration's domain(s).
     */
    protected void setDomainConfigurationName(String domainConfigurationName) {
        this.domainConfigurationName = domainConfigurationName;
    }

    /**
     * @since 1.1
     */
    public String getProjectVersion() {
        return projectVersion;
    }

    /**
     * @since 1.1
     */
    public void setProjectVersion(String projectVersion) {
        this.projectVersion = projectVersion;
    }

    /**
     * Returns a DataSourceFactory that should override a given factory specified by
     * caller. Returns null if the user factory should not be overriden.
     * 
     * @since 3.0
     */
    public DataSourceFactory getDataSourceFactory(String userFactoryName) {
        // call the old implementation for backwards compatibility
        return getDataSourceFactory();
    }

    /**
     * Returns an internal DataSourceFactory that will override any settings configured in
     * XML. Subclasses may override this method to provide a special factory for
     * DataSource creation that will take precedence over any factories configured in a
     * Cayenne project.
     * 
     * @deprecated since 3.0 this method is no longer called when configuration is loaded.
     *             Instead {@link #getDataSourceFactory(String)} is invoked, and this is
     *             the method that should be overriden.
     */
    public DataSourceFactory getDataSourceFactory() {
        return overrideFactory;
    }

    /**
     * @deprecated since 3.0 as a more flexible mechanism for customizing
     *             DataSourceFactory is implemented. Note that the factory set via this
     *             method would still work, although using this method is discouraged.
     */
    public void setDataSourceFactory(DataSourceFactory overrideFactory) {
        this.overrideFactory = overrideFactory;
    }

    /**
     * Adds new DataDomain to the list of registered domains. Injects EventManager used by
     * this configuration into the domain.
     */
    public void addDomain(DataDomain domain) {
        if (domain == null) {
            throw new NullPointerException("Attempt to work with a null DataDomain.");
        }

        if (domain.getName() == null) {
            throw new NullPointerException("Attempt to add DataDomain with no name.");
        }

        DataDomain old = dataDomains.put(domain.getName(), domain);
        if (old != null && old != domain) {
            dataDomains.put(domain.getName(), old);
            throw new IllegalArgumentException("Attempt to overwrite domain with name "
                    + domain.getName());
        }

        // inject EventManager
        domain.setEventManager(getEventManager());

        logObj.debug("added domain: " + domain.getName());
    }

    /**
     * Returns registered domain matching <code>name</code> or <code>null</code> if no
     * such domain is found.
     */
    public DataDomain getDomain(String name) {
        return dataDomains.get(name);
    }

    /**
     * Returns default domain of this configuration. If no domains are configured,
     * <code>null</code> is returned. If more than one domain exists in this
     * configuration, a CayenneRuntimeException is thrown, indicating that the domain name
     * must be explicitly specified. In such cases {@link #getDomain(String name)}must be
     * used instead.
     */
    public DataDomain getDomain() {
        int size = dataDomains.size();
        if (size == 0) {
            return null;
        }
        else if (size == 1) {
            return dataDomains.values().iterator().next();
        }
        else {
            throw new CayenneRuntimeException(
                    "More than one domain is configured; use 'getDomain(String name)' instead.");
        }
    }

    /**
     * Unregisters DataDomain matching <code>name<code> from
     * this Configuration object. Note that any domain database
     * connections remain open, and it is a responsibility of a
     * caller to clean it up.
     */
    public void removeDomain(String name) {
        DataDomain domain = dataDomains.remove(name);

        if (domain != null) {
            domain.setEventManager(null);
        }
    }

    /**
     * Returns an unmodifiable collection of registered DataDomains sorted by domain name.
     */
    public Collection<DataDomain> getDomains() {
        return Collections.unmodifiableCollection(dataDomains.values());
    }

    /**
     * Returns whether to ignore any failures during map loading or not.
     * 
     * @return boolean
     */
    public boolean isIgnoringLoadFailures() {
        return this.ignoringLoadFailures;
    }

    /**
     * Sets whether to ignore any failures during map loading or not.
     * 
     * @param ignoringLoadFailures <code>true</code> or <code>false</code>
     */
    protected void setIgnoringLoadFailures(boolean ignoringLoadFailures) {
        this.ignoringLoadFailures = ignoringLoadFailures;
    }

    /**
     * Returns the load status.
     * 
     * @return ConfigStatus
     */
    public ConfigStatus getLoadStatus() {
        return this.loadStatus;
    }

    /**
     * Sets the load status.
     */
    protected void setLoadStatus(ConfigStatus status) {
        this.loadStatus = status;
    }

    /**
     * Returns a delegate used for controlling the loading of configuration elements.
     */
    public ConfigLoaderDelegate getLoaderDelegate() {
        return loaderDelegate;
    }

    /**
     * @since 1.1
     */
    public void setLoaderDelegate(ConfigLoaderDelegate loaderDelegate) {
        this.loaderDelegate = loaderDelegate;
    }

    /**
     * @since 1.2
     */
    public ConfigSaverDelegate getSaverDelegate() {
        return saverDelegate;
    }

    /**
     * @since 1.2
     */
    public void setSaverDelegate(ConfigSaverDelegate saverDelegate) {
        this.saverDelegate = saverDelegate;
    }

    /**
     * Initializes configuration with the location of data views.
     * 
     * @since 1.1
     * @param dataViewLocations Map of DataView locations.
     */
    public void setDataViewLocations(Map<String, String> dataViewLocations) {
        if (dataViewLocations == null)
            this.dataViewLocations = new HashMap<String, String>();
        else
            this.dataViewLocations = dataViewLocations;
    }

    /**
     * See 'https://svn.apache.org/repos/asf/cayenne/dataviews/trunk' for DataViews code,
     * which is not a part of Cayenne since 3.0.
     * 
     * @since 1.1
     */
    public Map<String, String> getDataViewLocations() {
        return dataViewLocations;
    }

    /**
     * Shutdowns all owned domains. Invokes DataDomain.shutdown().
     */
    public void shutdown() {
        for (DataDomain domain : getDomains()) {
            domain.shutdown();
        }

        if (eventManager != null) {
            eventManager.shutdown();
        }
    }

    private class ConfigurationShutdownHook extends Thread {

        @Override
        public void run() {
            shutdown();
        }
    }

    public void installConfigurationShutdownHook() {
        uninstallConfigurationShutdownHook();

        if (configurationShutdownHook == null) {
            configurationShutdownHook = new ConfigurationShutdownHook();
        }

        Runtime.getRuntime().addShutdownHook(configurationShutdownHook);
    }

    public void uninstallConfigurationShutdownHook() {
        if (configurationShutdownHook != null) {
            Runtime.getRuntime().removeShutdownHook(configurationShutdownHook);
        }
    }
}
