/*
 * 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.openjpa.jdbc.conf;

import java.sql.Connection;
import java.sql.ResultSet;
import java.util.Locale;

import javax.sql.DataSource;

import org.apache.openjpa.conf.OpenJPAConfigurationImpl;
import org.apache.openjpa.jdbc.identifier.DBIdentifierUtil;
import org.apache.openjpa.jdbc.kernel.BatchingConstraintUpdateManager;
import org.apache.openjpa.jdbc.kernel.BatchingOperationOrderUpdateManager;
import org.apache.openjpa.jdbc.kernel.EagerFetchModes;
import org.apache.openjpa.jdbc.kernel.JDBCBrokerFactory;
import org.apache.openjpa.jdbc.kernel.LRSSizes;
import org.apache.openjpa.jdbc.kernel.PessimisticLockManager;
import org.apache.openjpa.jdbc.kernel.UpdateManager;
import org.apache.openjpa.jdbc.meta.MappingDefaults;
import org.apache.openjpa.jdbc.meta.MappingRepository;
import org.apache.openjpa.jdbc.schema.DataSourceFactory;
import org.apache.openjpa.jdbc.schema.DriverDataSource;
import org.apache.openjpa.jdbc.schema.SchemaFactory;
import org.apache.openjpa.jdbc.sql.DBDictionary;
import org.apache.openjpa.jdbc.sql.DBDictionaryFactory;
import org.apache.openjpa.jdbc.sql.MaxDBDictionary;
import org.apache.openjpa.jdbc.sql.SQLFactory;
import org.apache.openjpa.kernel.BrokerImpl;
import org.apache.openjpa.kernel.StoreContext;
import org.apache.openjpa.lib.conf.IntValue;
import org.apache.openjpa.lib.conf.ObjectValue;
import org.apache.openjpa.lib.conf.PluginValue;
import org.apache.openjpa.lib.conf.ProductDerivations;
import org.apache.openjpa.lib.conf.StringListValue;
import org.apache.openjpa.lib.conf.StringValue;
import org.apache.openjpa.lib.jdbc.ConnectionDecorator;
import org.apache.openjpa.lib.jdbc.DecoratingDataSource;
import org.apache.openjpa.lib.jdbc.JDBCListener;
import org.apache.openjpa.lib.log.Log;
import org.apache.openjpa.lib.util.Localizer;
import org.apache.openjpa.lib.util.StringUtil;
import org.apache.openjpa.meta.MetaDataFactory;
import org.apache.openjpa.util.UserException;

/**
 * Default implementation of the {@link JDBCConfiguration} interface.
 *
 * @author Marc Prud'hommeaux
 * @author Abe White
 */
public class JDBCConfigurationImpl
    extends OpenJPAConfigurationImpl
    implements JDBCConfiguration {

    public StringValue schema;
    public StringListValue schemas;
    public IntValue transactionIsolation;
    public IntValue resultSetType;
    public IntValue fetchDirection;
    public FetchModeValue eagerFetchMode;
    public FetchModeValue subclassFetchMode;
    public IntValue lrsSize;
    public StringValue synchronizeMappings;
    public ObjectValue jdbcListenerPlugins;
    public ObjectValue connectionDecoratorPlugins;
    public PluginValue dbdictionaryPlugin;
    public ObjectValue updateManagerPlugin;
    public ObjectValue schemaFactoryPlugin;
    public ObjectValue sqlFactoryPlugin;
    public ObjectValue mappingDefaultsPlugin;
    public PluginValue driverDataSourcePlugin;
    public MappingFactoryValue mappingFactoryPlugin;
    public ObjectValue identifierUtilPlugin;

    // used internally
    private String firstUser = null;
    private String firstPass = null;
    private DecoratingDataSource dataSource = null;
    private DecoratingDataSource dataSource2 = null;

    private static final Localizer _loc = Localizer.forPackage(JDBCConfigurationImpl.class);

    /**
     * Default constructor. Attempts to load default properties.
     */
    public JDBCConfigurationImpl() {
        this(true);
    }

    /**
     * Constructor.
     *
     * @param loadGlobals whether to attempt to load the global properties
     */
    public JDBCConfigurationImpl(boolean loadGlobals) {
        this(true, loadGlobals);
    }

    /**
     * Constructor.
     *
     * @param derivations whether to apply product derivations
     * @param loadGlobals whether to attempt to load the global properties
     */
    public JDBCConfigurationImpl(boolean derivations, boolean loadGlobals) {
        super(false, false);
        String[] aliases;

        schema = addString("jdbc.Schema");
        schemas = addStringList("jdbc.Schemas");

        transactionIsolation = addInt("jdbc.TransactionIsolation");
        aliases = new String[]{
            "default", String.valueOf(-1),
            "none", String.valueOf(Connection.TRANSACTION_NONE),
            "read-committed", String.valueOf
            (Connection.TRANSACTION_READ_COMMITTED),
            "read-uncommitted", String.valueOf
            (Connection.TRANSACTION_READ_UNCOMMITTED),
            "repeatable-read", String.valueOf
            (Connection.TRANSACTION_REPEATABLE_READ),
            "serializable", String.valueOf(Connection.TRANSACTION_SERIALIZABLE)
        };
        transactionIsolation.setAliases(aliases);
        transactionIsolation.setDefault(aliases[0]);
        transactionIsolation.set(-1);
        transactionIsolation.setAliasListComprehensive(true);

        resultSetType = addInt("jdbc.ResultSetType");
        aliases = new String[]{
            "forward-only", String.valueOf(ResultSet.TYPE_FORWARD_ONLY),
            "scroll-sensitive", String.valueOf
            (ResultSet.TYPE_SCROLL_SENSITIVE),
            "scroll-insensitive", String.valueOf
            (ResultSet.TYPE_SCROLL_INSENSITIVE),
        };
        resultSetType.setAliases(aliases);
        resultSetType.setDefault(aliases[0]);
        resultSetType.set(ResultSet.TYPE_FORWARD_ONLY);
        resultSetType.setAliasListComprehensive(true);

        fetchDirection = addInt("jdbc.FetchDirection");
        aliases = new String[]{
            "forward", String.valueOf(ResultSet.FETCH_FORWARD),
            "reverse", String.valueOf(ResultSet.FETCH_REVERSE),
            "unknown", String.valueOf(ResultSet.FETCH_UNKNOWN),
        };
        fetchDirection.setAliases(aliases);
        fetchDirection.setDefault(aliases[0]);
        fetchDirection.set(ResultSet.FETCH_FORWARD);
        fetchDirection.setAliasListComprehensive(true);

        eagerFetchMode = new FetchModeValue("jdbc.EagerFetchMode");
        eagerFetchMode.setDefault(FetchModeValue.EAGER_PARALLEL);
        eagerFetchMode.set(EagerFetchModes.EAGER_PARALLEL);
        addValue(eagerFetchMode);

        subclassFetchMode = new FetchModeValue("jdbc.SubclassFetchMode");
        subclassFetchMode.setDefault(FetchModeValue.EAGER_JOIN);
        subclassFetchMode.set(EagerFetchModes.EAGER_JOIN);
        addValue(subclassFetchMode);

        lrsSize = addInt("jdbc.LRSSize");
        aliases = new String[]{
            "query", String.valueOf(LRSSizes.SIZE_QUERY),
            "unknown", String.valueOf(LRSSizes.SIZE_UNKNOWN),
            "last", String.valueOf(LRSSizes.SIZE_LAST),
        };
        lrsSize.setAliases(aliases);
        lrsSize.setDefault(aliases[0]);
        lrsSize.set(LRSSizes.SIZE_QUERY);
        lrsSize.setAliasListComprehensive(true);

        synchronizeMappings = addString("jdbc.SynchronizeMappings");
        aliases = new String[]{ "false", null };
        synchronizeMappings.setAliases(aliases);
        synchronizeMappings.setDefault(aliases[0]);

        jdbcListenerPlugins = addPluginList("jdbc.JDBCListeners");
        jdbcListenerPlugins.setInstantiatingGetter("getJDBCListenerInstances");

        connectionDecoratorPlugins = addPluginList
            ("jdbc.ConnectionDecorators");
        connectionDecoratorPlugins.setInstantiatingGetter
            ("getConnectionDecoratorInstances");

        dbdictionaryPlugin = addPlugin("jdbc.DBDictionary", true);
        aliases = new String[]{
            "access", org.apache.openjpa.jdbc.sql.AccessDictionary.class.getName(),
            "db2", org.apache.openjpa.jdbc.sql.DB2Dictionary.class.getName(),
            "derby", org.apache.openjpa.jdbc.sql.DerbyDictionary.class.getName(),
            "empress", org.apache.openjpa.jdbc.sql.EmpressDictionary.class.getName(),
            "foxpro", org.apache.openjpa.jdbc.sql.FoxProDictionary.class.getName(),
            "h2", org.apache.openjpa.jdbc.sql.H2Dictionary.class.getName(),
            "hsql", org.apache.openjpa.jdbc.sql.HSQLDictionary.class.getName(),
            "informix", org.apache.openjpa.jdbc.sql.InformixDictionary.class.getName(),
            "ingres", org.apache.openjpa.jdbc.sql.IngresDictionary.class.getName(),
            "jdatastore", org.apache.openjpa.jdbc.sql.JDataStoreDictionary.class.getName(),
            "mariadb", org.apache.openjpa.jdbc.sql.MariaDBDictionary.class.getName(),
            "mysql", org.apache.openjpa.jdbc.sql.MySQLDictionary.class.getName(),
            "herddb", org.apache.openjpa.jdbc.sql.HerdDBDictionary.class.getName(),
            "oracle", org.apache.openjpa.jdbc.sql.OracleDictionary.class.getName(),
            "pointbase", org.apache.openjpa.jdbc.sql.PointbaseDictionary.class.getName(),
            "postgres", org.apache.openjpa.jdbc.sql.PostgresDictionary.class.getName(),
            "soliddb", org.apache.openjpa.jdbc.sql.SolidDBDictionary.class.getName(),
            "sqlserver", org.apache.openjpa.jdbc.sql.SQLServerDictionary.class.getName(),
            "sybase", org.apache.openjpa.jdbc.sql.SybaseDictionary.class.getName(),
            "maxdb", MaxDBDictionary.class.getName(),
        };
        dbdictionaryPlugin.setAliases(aliases);
        dbdictionaryPlugin.setInstantiatingGetter("getDBDictionaryInstance");

        updateManagerPlugin = addPlugin("jdbc.UpdateManager", true);
        aliases = new String[]{
            "default",
            BatchingConstraintUpdateManager.class.getName(),
            "operation-order",
            "org.apache.openjpa.jdbc.kernel.OperationOrderUpdateManager",
            "constraint",
            "org.apache.openjpa.jdbc.kernel.ConstraintUpdateManager",
            "batching-constraint",
            BatchingConstraintUpdateManager.class.getName(),
            "batching-operation-order",
            BatchingOperationOrderUpdateManager.class.getName(),
        };
        updateManagerPlugin.setAliases(aliases);
        updateManagerPlugin.setDefault(aliases[0]);
        updateManagerPlugin.setString(aliases[0]);
        updateManagerPlugin.setInstantiatingGetter("getUpdateManagerInstance");

        driverDataSourcePlugin = addPlugin("jdbc.DriverDataSource", false);
        aliases = new String[]{
            "auto", "org.apache.openjpa.jdbc.schema.AutoDriverDataSource",
            "simple", "org.apache.openjpa.jdbc.schema.SimpleDriverDataSource",
            "dbcp", "org.apache.openjpa.jdbc.schema.DBCPDriverDataSource"
        };
        driverDataSourcePlugin.setAliases(aliases);
        driverDataSourcePlugin.setDefault(aliases[0]);
        driverDataSourcePlugin.setString(aliases[0]);

        schemaFactoryPlugin = addPlugin("jdbc.SchemaFactory", true);
        aliases = new String[]{
            "dynamic", "org.apache.openjpa.jdbc.schema.DynamicSchemaFactory",
            "native", "org.apache.openjpa.jdbc.schema.LazySchemaFactory",
            "file", "org.apache.openjpa.jdbc.schema.FileSchemaFactory",
            "table", "org.apache.openjpa.jdbc.schema.TableSchemaFactory",
            // deprecated alias
            "db", "org.apache.openjpa.jdbc.schema.TableSchemaFactory",
        };
        schemaFactoryPlugin.setAliases(aliases);
        schemaFactoryPlugin.setDefault(aliases[0]);
        schemaFactoryPlugin.setString(aliases[0]);
        schemaFactoryPlugin.setInstantiatingGetter("getSchemaFactoryInstance");

        sqlFactoryPlugin = addPlugin("jdbc.SQLFactory", true);
        aliases = new String[]{
            "default", "org.apache.openjpa.jdbc.sql.SQLFactoryImpl",
        };
        sqlFactoryPlugin.setAliases(aliases);
        sqlFactoryPlugin.setDefault(aliases[0]);
        sqlFactoryPlugin.setString(aliases[0]);
        sqlFactoryPlugin.setInstantiatingGetter("getSQLFactoryInstance");

        mappingFactoryPlugin = new MappingFactoryValue("jdbc.MappingFactory");
        addValue(mappingFactoryPlugin);

        mappingDefaultsPlugin = addPlugin("jdbc.MappingDefaults", true);
        aliases = new String[]{
            "default", "org.apache.openjpa.jdbc.meta.MappingDefaultsImpl",
        };
        mappingDefaultsPlugin.setAliases(aliases);
        mappingDefaultsPlugin.setDefault(aliases[0]);
        mappingDefaultsPlugin.setString(aliases[0]);
        mappingDefaultsPlugin.setInstantiatingGetter
            ("getMappingDefaultsInstance");

        // set up broker factory defaults
        brokerFactoryPlugin.setAlias("jdbc", JDBCBrokerFactory.class.getName());
        brokerFactoryPlugin.setDefault("jdbc");
        brokerFactoryPlugin.setString("jdbc");

        // set new default for mapping repos
        metaRepositoryPlugin.setAlias("default",
            "org.apache.openjpa.jdbc.meta.MappingRepository");
        metaRepositoryPlugin.setDefault("default");
        metaRepositoryPlugin.setString("default");

        // set new default for lock manager
        lockManagerPlugin.setAlias("pessimistic",
            PessimisticLockManager.class.getName());
        lockManagerPlugin.setDefault("pessimistic");
        lockManagerPlugin.setString("pessimistic");

        // native savepoint manager options
        savepointManagerPlugin.setAlias("jdbc",
            "org.apache.openjpa.jdbc.kernel.JDBC3SavepointManager");

        // set new aliases and defaults for sequence
        seqPlugin.setAliases(JDBCSeqValue.ALIASES);
        seqPlugin.setDefault(JDBCSeqValue.ALIASES[0]);
        seqPlugin.setString(JDBCSeqValue.ALIASES[0]);

        // This plug-in is declared in superclass but defined here
        // because PreparedQueryCache is currently available for JDBC
        // backend only
        preparedQueryCachePlugin = addPlugin("jdbc.QuerySQLCache", true);
        aliases = new String[] {
            "true", "org.apache.openjpa.jdbc.kernel.PreparedQueryCacheImpl",
            "false", null
        };
        preparedQueryCachePlugin.setAliases(aliases);
        preparedQueryCachePlugin.setAliasListComprehensive(true);
        preparedQueryCachePlugin.setDefault(aliases[0]);
        preparedQueryCachePlugin.setClassName(aliases[1]);
        preparedQueryCachePlugin.setDynamic(true);
        preparedQueryCachePlugin.setInstantiatingGetter(
                "getQuerySQLCacheInstance");

        finderCachePlugin = addPlugin("jdbc.FinderCache", true);
        aliases = new String[] {
            "true", "org.apache.openjpa.jdbc.kernel.FinderCacheImpl",
            "false", null
        };
        finderCachePlugin.setAliases(aliases);
        finderCachePlugin.setAliasListComprehensive(true);
        finderCachePlugin.setDefault(aliases[0]);
        finderCachePlugin.setClassName(aliases[1]);
        finderCachePlugin.setDynamic(true);
        finderCachePlugin.setInstantiatingGetter("getFinderCacheInstance");

        identifierUtilPlugin = addPlugin("jdbc.IdentifierUtil", true);
        aliases = new String[] {
            "default", "org.apache.openjpa.jdbc.identifier.DBIdentifierUtilImpl" };
        identifierUtilPlugin.setAliases(aliases);
        identifierUtilPlugin.setDefault(aliases[0]);
        identifierUtilPlugin.setString(aliases[0]);
        identifierUtilPlugin.setInstantiatingGetter("getIdentifierUtilInstance");

        supportedOptions().add(OPTION_QUERY_SQL);
        supportedOptions().add(OPTION_JDBC_CONNECTION);
        supportedOptions().remove(OPTION_VALUE_INCREMENT);
        supportedOptions().remove(OPTION_NULL_CONTAINER);

        if (derivations)
            ProductDerivations.beforeConfigurationLoad(this);
        if (loadGlobals)
            loadGlobals();
    }

    /**
     * Copy constructor
     */
    public JDBCConfigurationImpl(JDBCConfiguration conf) {
        this(true, false);
        if (conf != null)
            fromProperties(conf.toProperties(false));
    }

    @Override
    public void setSchema(String schema) {
        this.schema.setString(schema);
    }

    @Override
    public String getSchema() {
        return schema.getString();
    }

    @Override
    public void setSchemas(String schemas) {
        this.schemas.setString(schemas);
    }

    @Override
    public String getSchemas() {
        return schemas.getString();
    }

    @Override
    public void setSchemas(String[] schemas) {
        this.schemas.set(schemas);
    }

    @Override
    public String[] getSchemasList() {
        return schemas.get();
    }

    @Override
    public void setTransactionIsolation(String transactionIsolation) {
        this.transactionIsolation.setString(transactionIsolation);
    }

    @Override
    public String getTransactionIsolation() {
        return transactionIsolation.getString();
    }

    @Override
    public void setTransactionIsolation(int transactionIsolation) {
        this.transactionIsolation.set(transactionIsolation);
    }

    @Override
    public int getTransactionIsolationConstant() {
        return transactionIsolation.get();
    }

    @Override
    public void setResultSetType(String resultSetType) {
        this.resultSetType.setString(resultSetType);
    }

    @Override
    public String getResultSetType() {
        return resultSetType.getString();
    }

    @Override
    public void setResultSetType(int resultSetType) {
        this.resultSetType.set(resultSetType);
    }

    @Override
    public int getResultSetTypeConstant() {
        return resultSetType.get();
    }

    @Override
    public void setFetchDirection(String fetchDirection) {
        this.fetchDirection.setString(fetchDirection);
    }

    @Override
    public String getFetchDirection() {
        return fetchDirection.getString();
    }

    @Override
    public void setFetchDirection(int fetchDirection) {
        this.fetchDirection.set(fetchDirection);
    }

    @Override
    public int getFetchDirectionConstant() {
        return fetchDirection.get();
    }

    @Override
    public void setEagerFetchMode(String eagerFetchMode) {
        this.eagerFetchMode.setString(eagerFetchMode);
    }

    @Override
    public String getEagerFetchMode() {
        return eagerFetchMode.getString();
    }

    @Override
    public void setEagerFetchMode(int eagerFetchMode) {
        this.eagerFetchMode.set(eagerFetchMode);
    }

    @Override
    public int getEagerFetchModeConstant() {
        return eagerFetchMode.get();
    }

    @Override
    public void setSubclassFetchMode(String subclassFetchMode) {
        this.subclassFetchMode.setString(subclassFetchMode);
    }

    @Override
    public String getSubclassFetchMode() {
        return subclassFetchMode.getString();
    }

    @Override
    public void setSubclassFetchMode(int subclassFetchMode) {
        this.subclassFetchMode.set(subclassFetchMode);
    }

    @Override
    public int getSubclassFetchModeConstant() {
        return subclassFetchMode.get();
    }

    @Override
    public void setLRSSize(String lrsSize) {
        this.lrsSize.setString(lrsSize);
    }

    @Override
    public String getLRSSize() {
        return lrsSize.getString();
    }

    @Override
    public void setLRSSize(int lrsSize) {
        this.lrsSize.set(lrsSize);
    }

    @Override
    public int getLRSSizeConstant() {
        return lrsSize.get();
    }

    @Override
    public void setSynchronizeMappings(String synchronizeMappings) {
        this.synchronizeMappings.set(synchronizeMappings);
    }

    @Override
    public String getSynchronizeMappings() {
        return synchronizeMappings.get();
    }

    @Override
    public void setJDBCListeners(String jdbcListeners) {
        jdbcListenerPlugins.setString(jdbcListeners);
    }

    @Override
    public String getJDBCListeners() {
        return jdbcListenerPlugins.getString();
    }

    @Override
    public void setJDBCListeners(JDBCListener[] listeners) {
        jdbcListenerPlugins.set(listeners);
    }

    @Override
    public JDBCListener[] getJDBCListenerInstances() {
        if (jdbcListenerPlugins.get() == null)
            jdbcListenerPlugins.instantiate(JDBCListener.class, this);
        return (JDBCListener[]) jdbcListenerPlugins.get();
    }

    @Override
    public void setConnectionDecorators(String connectionDecorators) {
        connectionDecoratorPlugins.setString(connectionDecorators);
    }

    @Override
    public String getConnectionDecorators() {
        return connectionDecoratorPlugins.getString();
    }

    @Override
    public void setConnectionDecorators(ConnectionDecorator[] decorators) {
        connectionDecoratorPlugins.set(decorators);
    }

    @Override
    public ConnectionDecorator[] getConnectionDecoratorInstances() {
        if (connectionDecoratorPlugins.get() == null) {
            connectionDecoratorPlugins.instantiate
                (ConnectionDecorator.class, this);
        }
        return (ConnectionDecorator[]) connectionDecoratorPlugins.get();
    }

    @Override
    public void setDBDictionary(String dbdictionary) {
        dbdictionaryPlugin.setString(dbdictionary);
    }

    @Override
    public String getDBDictionary() {
        return dbdictionaryPlugin.getString();
    }

    @Override
    public void setDBDictionary(DBDictionary dbdictionary) {
        // we can't allow the dictionary to be set after the connection
        // factory, due to initialization issues
        if (connectionFactory.get() != null
            || connectionFactory2.get() != null)
            throw new IllegalStateException();

        dbdictionaryPlugin.set(dbdictionary);
    }

    @Override
    public DBDictionary getDBDictionaryInstance() {
        // lock on connection factory name, since getting the connection
        // factory and getting the dictionary have to use the same locks to
        // prevent deadlock since they call each other
        DBDictionary dbdictionary = (DBDictionary) dbdictionaryPlugin.get();
        if (dbdictionary == null) {
            String clsName = dbdictionaryPlugin.getClassName();
            String props = dbdictionaryPlugin.getProperties();
            if (!StringUtil.isEmpty(clsName)) {
                dbdictionary = DBDictionaryFactory.newDBDictionary
                    (this, clsName, props);
            } else {
                // if the dictionary class isn't set, try to guess from
                // connection URL and driver name
                dbdictionary = DBDictionaryFactory.calculateDBDictionary
                    (this, getConnectionURL(), getConnectionDriverName(),
                        props);

                // if the url and driver name aren't enough, connect to
                // the DB and use the connection metadata
                if (dbdictionary == null) {
                    Log log = getLog(LOG_JDBC);
                    if (log.isTraceEnabled()) {
                        Localizer loc = Localizer.forPackage
                            (JDBCConfigurationImpl.class);
                        log.trace(loc.get("connecting-for-dictionary"));
                    }

                    // use the base connection factory rather than the
                    // configured data source b/c the data source relies
                    // on passing the connection through the dictionary,
                    // resulting in infinite loops
                    DataSource ds = createConnectionFactory();
                    dbdictionary = DBDictionaryFactory.newDBDictionary
                        (this, getDataSource(null, ds), props);
                }
            }
            dbdictionaryPlugin.set(dbdictionary, true);
        }
        return dbdictionary;
    }

    @Override
    public void setUpdateManager(String updateManager) {
        updateManagerPlugin.setString(updateManager);
    }

    @Override
    public String getUpdateManager() {
        return updateManagerPlugin.getString();
    }

    @Override
    public void setUpdateManager(UpdateManager updateManager) {
        updateManagerPlugin.set(updateManager);
    }

    @Override
    public UpdateManager getUpdateManagerInstance() {
        if (updateManagerPlugin.get() == null)
            updateManagerPlugin.instantiate(UpdateManager.class, this);
        return (UpdateManager) updateManagerPlugin.get();
    }

    @Override
    public void setDriverDataSource(String driverDataSource) {
        driverDataSourcePlugin.setString(driverDataSource);
    }

    @Override
    public String getDriverDataSource() {
        return driverDataSourcePlugin.getString();
    }

    @Override
    public DriverDataSource newDriverDataSourceInstance() {
        return (DriverDataSource) driverDataSourcePlugin.
            instantiate(DriverDataSource.class, this);
    }

    @Override
    public void setSchemaFactory(String schemaFactory) {
        schemaFactoryPlugin.setString(schemaFactory);
    }

    @Override
    public String getSchemaFactory() {
        return schemaFactoryPlugin.getString();
    }

    @Override
    public void setSchemaFactory(SchemaFactory schemaFactory) {
        schemaFactoryPlugin.set(schemaFactory);
    }

    @Override
    public SchemaFactory getSchemaFactoryInstance() {
        if (schemaFactoryPlugin.get() == null)
            schemaFactoryPlugin.instantiate(SchemaFactory.class, this);
        return (SchemaFactory) schemaFactoryPlugin.get();
    }

    @Override
    public void setSQLFactory(String sqlFactory) {
        sqlFactoryPlugin.setString(sqlFactory);
    }

    @Override
    public String getSQLFactory() {
        return sqlFactoryPlugin.getString();
    }

    @Override
    public void setSQLFactory(SQLFactory sqlFactory) {
        sqlFactoryPlugin.set(sqlFactory);
    }

    @Override
    public SQLFactory getSQLFactoryInstance() {
        if (sqlFactoryPlugin.get() == null)
            sqlFactoryPlugin.instantiate(SQLFactory.class, this);
        return (SQLFactory) sqlFactoryPlugin.get();
    }

    @Override
    public String getMappingFactory() {
        return mappingFactoryPlugin.getString();
    }

    @Override
    public void setMappingFactory(String mapping) {
        mappingFactoryPlugin.setString(mapping);
    }

    @Override
    public MetaDataFactory newMetaDataFactoryInstance() {
        return mappingFactoryPlugin.instantiateMetaDataFactory(this,
            metaFactoryPlugin, getMapping());
    }

    @Override
    public void setMappingDefaults(String mapping) {
        this.mappingDefaultsPlugin.setString(mapping);
    }

    @Override
    public String getMappingDefaults() {
        return mappingDefaultsPlugin.getString();
    }

    @Override
    public void setMappingDefaults(MappingDefaults mapping) {
        mappingDefaultsPlugin.set(mapping);
    }

    @Override
    public MappingDefaults getMappingDefaultsInstance() {
        if (mappingDefaultsPlugin.get() == null)
            mappingDefaultsPlugin.instantiate(MappingDefaults.class, this);
        return (MappingDefaults) mappingDefaultsPlugin.get();
    }

    @Override
    public MappingRepository getMappingRepositoryInstance() {
        return (MappingRepository) getMetaDataRepositoryInstance();
    }

    @Override
    public MappingRepository newMappingRepositoryInstance() {
        return (MappingRepository) newMetaDataRepositoryInstance();
    }

    @Override
    public BrokerImpl newBrokerInstance(String user, String pass) {
        BrokerImpl broker = super.newBrokerInstance(user, pass);

        // record first non-null broker user and pass in case no global settings
        if (broker != null && user != null && firstUser == null) {
            firstUser = user;
            firstPass = pass;
        }
        return broker;
    }

    @Override
    public Object getConnectionFactory() {
        // override to configure data source
        if (dataSource == null) {
            DecoratingDataSource ds = createConnectionFactory();
            dataSource = DataSourceFactory.installDBDictionary
                (getDBDictionaryInstance(), ds, this, false);
        }
        return dataSource;
    }

    @Override
    public void setConnectionFactory(Object factory) {
        // there's a lot of one-time initialization involved for
        // connection factories, so ignore resets
        if (factory == connectionFactory.get())
            return;

        // override to configure data source
        if (factory != null) {
            // need to ensure it is decorated before we set the dict
            DecoratingDataSource ds =
                setupConnectionFactory((DataSource) factory, false);
            dataSource = DataSourceFactory.installDBDictionary
                (getDBDictionaryInstance(), ds, this, false);
        } else
            connectionFactory.set(null);
    }

    /**
     * Ensure that the specified DataSource is decorated and set in the cache.
     */
    private DecoratingDataSource setupConnectionFactory(DataSource ds,
        boolean factory2) {
        if (ds == null)
            return null;

        DecoratingDataSource dds;
        if (ds instanceof DecoratingDataSource)
            dds = (DecoratingDataSource) ds;
        else
            dds = DataSourceFactory.decorateDataSource(ds, this, factory2);

        if (!factory2 && connectionFactory.get() != ds)
            connectionFactory.set(dds, true);
        else if (factory2 && connectionFactory2.get() != ds)
            connectionFactory2.set(dds, true);

        return dds;
    }

    @Override
    public Object getConnectionFactory2() {
        // override to configure data source
        if (dataSource2 == null) {
            // superclass will lookup from JNDI.
            Object obj = super.getConnectionFactory2();
            DataSource ds = null;
            if (obj != null) {
                if (obj instanceof DataSource)
                    ds = (DataSource) obj;
                else {
                    Log log = getLog(LOG_JDBC);
                    if (log.isTraceEnabled()) {
                        Localizer loc = Localizer.forPackage(JDBCConfigurationImpl.class);
                        log.trace(loc.get("unknown-datasource", getConnectionFactory2Name(),
                            obj.getClass().getName()));
                    }
                }
            }

            if (ds == null) {
                // the driver name is always required, so if not specified,
                // then no connection factory 2
                String driver = getConnection2DriverName();
                if (!StringUtil.isEmpty(driver))
                    ds = DataSourceFactory.newDataSource(this, true);
            }
            if (ds != null) {
                DecoratingDataSource dds =
                    setupConnectionFactory(ds, true); // before dict
                dataSource2 = DataSourceFactory.installDBDictionary
                    (getDBDictionaryInstance(), dds, this, true);
            }
        }
        return dataSource2;
    }

    @Override
    public void setConnectionFactory2(Object factory) {
        if (factory == connectionFactory2.get())
            return;

        // override to configure data source
        if (factory != null) {
            // need to ensure it is decorated before we set the dict
            DecoratingDataSource ds = setupConnectionFactory((DataSource)
                factory, true);
            dataSource2 = DataSourceFactory.installDBDictionary
                (getDBDictionaryInstance(), ds, this, true);
        } else
            connectionFactory2.set(null);
    }

    /**
     * Create the connection factory if necessary.
     */
    public DecoratingDataSource createConnectionFactory() {
        DataSource ds = (DataSource) connectionFactory.get();
        Log log = getLog(LOG_JDBC);
        if (ds != null) {
            if (log.isTraceEnabled())
                log.trace("createConnectionFactory: DataSource:" + ds);

            return setupConnectionFactory(ds, false);
        }

        if (log.isTraceEnabled())
            log.trace("createConnectionFactory: connectionFactory not created yet, attempt JNDI lookup...");

        ds = (DataSource) super.getConnectionFactory(); // JNDI lookup
        if (ds == null) {
            if (log.isTraceEnabled())
                log.trace("createConnectionFactory: JNDI lookup failed, attempt DataSource properties...");
            ds = DataSourceFactory.newDataSource(this, false);
        }

        if (log.isTraceEnabled())
            log.trace("createConnectionFactory: DataSource="+ds);

        return setupConnectionFactory(ds, false);
    }

    @Override
    public DataSource getDataSource(StoreContext ctx) {
        Log log = getLog(LOG_RUNTIME);
        DataSource ds = null;

        if(ctx != null && StringUtil.isNotEmpty(ctx.getConnectionFactoryName())) {
            ds =  getDataSource(ctx, (DataSource) ctx.getConnectionFactory());
            // fail fast if a cfName has been provided, but was not available in JNDI
            if (ds == null) {
                throw new UserException(_loc.get("invalid-datasource", ctx.getConnectionFactoryName())).setFatal(true);
            }
            if(! (ds instanceof DecoratingDataSource)) {
                ds = DataSourceFactory.decorateDataSource(ds, this, false);
            }
            if (log.isTraceEnabled()) {
                log.trace("Found datasource1: " + ds + " from StoreContext using jndiName: "
                    + ctx.getConnectionFactory2Name());
            }
            return ds;
        }
        else {
            ds = getDataSource(ctx, (DataSource) getConnectionFactory());
            if (log.isTraceEnabled()) {
                log.trace("Found datasource1: " + ds + " from configuration. StoreContext: " + ctx );
            }
            return ds;
        }
    }

    @Override
    public DataSource getDataSource2(StoreContext ctx) {
        Log log = getLog(LOG_RUNTIME);
        DataSource ds = null;

        // Try to obtain from the StoreContext first.
        if (ctx != null && StringUtil.isNotEmpty(ctx.getConnectionFactory2Name())) {
            ds = (DataSource) ctx.getConnectionFactory2();
            if (ds == null) {
                // fail fast. If the non-jta-data-source is configured on the context we want an immediate error.
                throw new UserException(_loc.get("invalid-datasource", ctx.getConnectionFactory2Name())).setFatal(true);
            }
            if(! (ds instanceof DecoratingDataSource)) {
                ds = DataSourceFactory.decorateDataSource(ds, this, false);
            }
            if (log.isTraceEnabled()) {
                log.trace("Found datasource2: " + ds + " from StoreContext using jndiName: "
                    + ctx.getConnectionFactory2Name());
            }
            return ds;
        }

        // If not set on context or value from context is not available try cf2 from config
        else{
            ds = (DataSource) getConnectionFactory2();
            if (log.isTraceEnabled()) {
                log.trace("Found datasource 2: "+ ds + " from config. StoreContext: " + ctx);
            }
        }

        // fallback to cf1 / datasource1
        if (ds == null) {
            if(log.isTraceEnabled()) {
                log.trace("Trying datasource1");
            }
            return getDataSource(ctx);
        }

        // prefer the global connection 2 auth info if given
        String user = getConnection2UserName();
        String pass = getConnection2Password();
        if (user == null && pass == null) {
            // no global auth info; use the context if given, or the first
            // context if not
            if (ctx == null) {
                user = firstUser;
                pass = firstPass;
            } else {
                user = ctx.getConnectionUserName();
                pass = ctx.getConnectionPassword();
            }
        }
        return DataSourceFactory.defaultsDataSource(ds, user, pass);
    }

    /**
     * This version allows us to pass in which data source to wrap internally;
     * useful during initialization before the connection factory is
     * completely configured.
     */
    private DataSource getDataSource(StoreContext ctx, DataSource ds) {
        String user, pass;
        if (ctx == null) {
            // if no context, default to the global auth info, or the auth info
            // of the first context if none
            user = getConnectionUserName();
            if (user == null)
                user = firstUser;
            pass = getConnectionPassword();
            if (pass == null)
                pass = firstPass;
        } else {
            // use the context's auth info
            user = ctx.getConnectionUserName();
            pass = ctx.getConnectionPassword();
        }
        return DataSourceFactory.defaultsDataSource(ds, user, pass);
    }

    /**
     * Free the data sources.
     */
    @Override
    protected void preClose() {
        if (dataSource != null) {
            getDBDictionaryInstance().closeDataSource(dataSource);
            connectionFactory.set(null, true); // so super doesn't close it
        }
        if (dataSource2 != null) {
            getDBDictionaryInstance().closeDataSource(dataSource);
            connectionFactory2.set(null, true); // so super doesn't close it
        }
        super.preClose();
    }

    @Override
    protected boolean isInvalidProperty(String propName) {
        if (super.isInvalidProperty(propName))
            return true;

        // handle openjpa.jdbc.SomeMisspelledProperty, but not
        // openjpa.someotherimplementation.SomeProperty
        String[] prefixes = ProductDerivations.getConfigurationPrefixes();
        for (String prefix : prefixes)
            if (propName.toLowerCase(Locale.ENGLISH).startsWith(prefix + ".jdbc"))
                return true;
        return false;
    }

    @Override
    public String getIdentifierUtil() {
        return identifierUtilPlugin.getString();
    }

    @Override
    public DBIdentifierUtil getIdentifierUtilInstance() {
        if (identifierUtilPlugin.get() == null)
            identifierUtilPlugin.instantiate(DBIdentifierUtil.class, this);
        return (DBIdentifierUtil) identifierUtilPlugin.get();
    }

    @Override
    public void setIdentifierUtil(DBIdentifierUtil util) {
        identifierUtilPlugin.set(util);
    }

}
