org.forgerock.openidm.repo.jdbc.impl.JDBCRepoService.java Source code

Java tutorial

Introduction

Here is the source code for org.forgerock.openidm.repo.jdbc.impl.JDBCRepoService.java

Source

/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright  2011 ForgeRock AS. All rights reserved.
 *
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at
 * http://forgerock.org/license/CDDLv1.0.html
 * See the License for the specific language governing
 * permission and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at http://forgerock.org/license/CDDLv1.0.html
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 */
package org.forgerock.openidm.repo.jdbc.impl;

import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Dictionary;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;

import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;

import org.apache.commons.lang3.StringUtils;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.ConfigurationPolicy;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Modified;
import org.apache.felix.scr.annotations.Properties;
import org.apache.felix.scr.annotations.Property;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.Service;
import org.codehaus.jackson.map.ObjectMapper;
import org.forgerock.json.fluent.JsonValue;
import org.forgerock.json.patch.JsonPatch;
import org.forgerock.json.resource.JsonResource;
import org.forgerock.openidm.config.EnhancedConfig;
import org.forgerock.openidm.config.InvalidException;
import org.forgerock.openidm.config.JSONEnhancedConfig;
import org.forgerock.openidm.crypto.CryptoService;
import org.forgerock.openidm.objset.BadRequestException;
import org.forgerock.openidm.objset.ConflictException;
import org.forgerock.openidm.objset.ForbiddenException;
import org.forgerock.openidm.objset.InternalServerErrorException;
import org.forgerock.openidm.objset.NotFoundException;
import org.forgerock.openidm.objset.ObjectSetException;
import org.forgerock.openidm.objset.ObjectSetJsonResource;
import org.forgerock.openidm.objset.Patch;
import org.forgerock.openidm.objset.PreconditionFailedException;
import org.forgerock.openidm.osgi.OsgiName;
import org.forgerock.openidm.osgi.ServiceUtil;
import org.forgerock.openidm.repo.QueryConstants;
import org.forgerock.openidm.repo.RepoBootService;
import org.forgerock.openidm.repo.RepositoryService;
import org.forgerock.openidm.repo.jdbc.DatabaseType;
import org.forgerock.openidm.repo.jdbc.ErrorType;
import org.forgerock.openidm.repo.jdbc.TableHandler;
import org.forgerock.openidm.repo.jdbc.impl.pool.DataSourceFactory;
import org.forgerock.openidm.util.Accessor;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceRegistration;
import org.osgi.service.component.ComponentContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Repository service implementation using JDBC
 *
 * @author aegloff
 * @author brmiller
 */
@Component(name = JDBCRepoService.PID, immediate = true, policy = ConfigurationPolicy.REQUIRE)
@Service(value = { RepositoryService.class, JsonResource.class })
// Omit the RepoBootService interface from the managed service
@Properties({ @Property(name = "service.description", value = "Repository Service using JDBC"),
        @Property(name = "service.vendor", value = "ForgeRock AS"),
        @Property(name = "openidm.router.prefix", value = "repo"), @Property(name = "db.type", value = "JDBC") })
public class JDBCRepoService extends ObjectSetJsonResource implements RepositoryService, RepoBootService {
    final static Logger logger = LoggerFactory.getLogger(JDBCRepoService.class);

    public static final String PID = "org.forgerock.openidm.repo.jdbc";

    /** CryptoService for detecting whether a value is encrypted */
    @Reference
    protected CryptoService cryptoService;

    ObjectMapper mapper = new ObjectMapper();

    private static ServiceRegistration sharedDataSource = null;

    // Keys in the JSON configuration
    public static final String CONFIG_CONNECTION = "connection";
    public static final String CONFIG_JNDI_NAME = "jndiName";
    public static final String CONFIG_JTA_NAME = "jtaName";
    public static final String CONFIG_DB_TYPE = "dbType";
    public static final String CONFIG_DB_DRIVER = "driverClass";
    public static final String CONFIG_DB_URL = "jdbcUrl";
    public static final String CONFIG_USER = "username";
    public static final String CONFIG_PASSWORD = "password";
    public static final String CONFIG_DB_SCHEMA = "defaultCatalog";
    public static final String CONFIG_MAX_BATCH_SIZE = "maxBatchSize";

    private boolean useDataSource;
    private String jndiName;
    private DataSource ds;
    private String dbDriver;
    private String dbUrl;
    private String user;
    private String password;

    private int maxTxRetry = 5;

    Map<String, TableHandler> tableHandlers;
    TableHandler defaultTableHandler;

    final EnhancedConfig enhancedConfig = new JSONEnhancedConfig();
    JsonValue config;

    /**
     * Gets an object from the repository by identifier. The returned object is not validated
     * against the current schema and may need processing to conform to an updated schema.
     * <p/>
     * The object will contain metadata properties, including object identifier {@code _id},
     * and object version {@code _rev} to enable optimistic concurrency supported by OrientDB and OpenIDM.
     *
     * @param fullId the identifier of the object to retrieve from the object set.
     * @return the requested object.
     * @throws NotFoundException   if the specified object could not be found.
     * @throws ForbiddenException  if access to the object is forbidden.
     * @throws BadRequestException if the passed identifier is invalid
     */
    public Map<String, Object> read(String fullId) throws ObjectSetException {
        String localId = getLocalId(fullId);
        String type = getObjectType(fullId);

        if (fullId == null || localId == null) {
            throw new NotFoundException(
                    "The repository requires clients to supply an identifier for the object to create. Full identifier: "
                            + fullId + " local identifier: " + localId);
        } else if (type == null) {
            throw new NotFoundException(
                    "The object identifier did not include sufficient information to determine the object type: "
                            + fullId);
        }

        Connection connection = null;
        Map<String, Object> result = null;
        try {
            connection = getConnection();
            connection.setAutoCommit(true); // Ensure this does not get transaction isolation handling
            TableHandler handler = getTableHandler(type);
            if (handler == null) {
                throw new ObjectSetException("No handler configured for resource type " + type);
            }
            result = handler.read(fullId, type, localId, connection);
        } catch (SQLException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("SQL Exception in read of {} with error code {}, sql state {}",
                        new Object[] { fullId, ex.getErrorCode(), ex.getSQLState(), ex });
            }
            throw new InternalServerErrorException("Reading object failed " + ex.getMessage(), ex);
        } catch (ObjectSetException ex) {
            logger.debug("ObjectSetException in read of {}", fullId, ex);
            throw ex;
        } catch (IOException ex) {
            logger.debug("IO Exception in read of {}", fullId, ex);
            throw new InternalServerErrorException("Conversion of read object failed", ex);
        } finally {
            CleanupHelper.loggedClose(connection);
        }

        return result;
    }

    /**
     * Creates a new object in the object set.
     * <p/>
     * This method sets the {@code _id} property to the assigned identifier for the object,
     * and the {@code _rev} property to the revised object version (For optimistic concurrency)
     *
     * @param fullId the client-generated identifier to use, or {@code null} if server-generated identifier is requested.
     * @param obj    the contents of the object to create in the object set.
     * @throws NotFoundException           if the specified id could not be resolved.
     * @throws ForbiddenException          if access to the object or object set is forbidden.
     * @throws PreconditionFailedException if an object with the same ID already exists.
     */
    public void create(String fullId, Map<String, Object> obj) throws ObjectSetException {
        String localId = getLocalId(fullId);
        String type = getObjectType(fullId);

        if (fullId == null || localId == null) {
            throw new NotFoundException(
                    "The repository requires clients to supply an identifier for the object to create. Full identifier: "
                            + fullId + " local identifier: " + localId);
        } else if (type == null) {
            throw new NotFoundException(
                    "The object identifier did not include sufficient information to determine the object type: "
                            + fullId);
        }

        Connection connection = null;
        boolean retry = false;
        int tryCount = 0;
        do {
            TableHandler handler = getTableHandler(type);
            if (handler == null) {
                throw new ObjectSetException("No handler configured for resource type " + type);
            }
            retry = false;
            ++tryCount;
            try {
                connection = getConnection();
                connection.setAutoCommit(false);

                handler.create(fullId, type, localId, obj, connection);

                connection.commit();
                logger.debug("Commited created object for id: {}", fullId);

            } catch (SQLException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("SQL Exception in create of {} with error code {}, sql state {}",
                            new Object[] { fullId, ex.getErrorCode(), ex.getSQLState(), ex });
                }
                rollback(connection);
                boolean alreadyExisted = handler.isErrorType(ex, ErrorType.DUPLICATE_KEY);
                if (alreadyExisted) {
                    throw new PreconditionFailedException(
                            "Create rejected as Object with same ID already exists and was detected. " + "("
                                    + ex.getErrorCode() + "-" + ex.getSQLState() + ")" + ex.getMessage(),
                            ex);
                }
                if (handler.isRetryable(ex, connection)) {
                    if (tryCount <= maxTxRetry) {
                        retry = true;
                        logger.debug("Retryable exception encountered, retry {}", ex.getMessage());
                    }
                }
                if (!retry) {
                    throw new InternalServerErrorException("Creating object failed " + "(" + ex.getErrorCode() + "-"
                            + ex.getSQLState() + ")" + ex.getMessage(), ex);
                }
            } catch (ObjectSetException ex) {
                logger.debug("ObjectSetException in create of {}", fullId, ex);
                rollback(connection);
                throw ex;
            } catch (java.io.IOException ex) {
                logger.debug("IO Exception in create of {}", fullId, ex);
                rollback(connection);
                throw new InternalServerErrorException("Conversion of object to create failed", ex);
            } catch (RuntimeException ex) {
                logger.debug("Runtime Exception in create of {}", fullId, ex);
                rollback(connection);
                throw new InternalServerErrorException(
                        "Creating object failed with unexpected failure: " + ex.getMessage(), ex);
            } finally {
                CleanupHelper.loggedClose(connection);
            }
        } while (retry);
    }

    /**
     * Updates the specified object in the object set.
     * <p/>
     * This implementation requires MVCC and hence enforces that clients state what revision they expect
     * to be updating
     * <p/>
     * If successful, this method updates metadata properties within the passed object,
     * including: a new {@code _rev} value for the revised object's version
     *
     * @param fullId the identifier of the object to be put, or {@code null} to request a generated identifier.
     * @param rev    the version of the object to update; or {@code null} if not provided.
     * @param obj    the contents of the object to put in the object set.
     * @throws ConflictException           if version is required but is {@code null}.
     * @throws ForbiddenException          if access to the object is forbidden.
     * @throws NotFoundException           if the specified object could not be found.
     * @throws PreconditionFailedException if version did not match the existing object in the set.
     * @throws BadRequestException         if the passed identifier is invalid
     */
    public void update(String fullId, String rev, Map<String, Object> obj) throws ObjectSetException {

        String localId = getLocalId(fullId);
        String type = getObjectType(fullId);

        if (rev == null) {
            throw new ConflictException("Object passed into update does not have revision it expects set.");
        }

        Connection connection = null;
        Integer previousIsolationLevel = null;
        boolean retry = false;
        int tryCount = 0;
        do {
            TableHandler handler = getTableHandler(type);
            if (handler == null) {
                throw new ObjectSetException("No handler configured for resource type " + type);
            }
            retry = false;
            ++tryCount;
            try {
                connection = getConnection();
                previousIsolationLevel = new Integer(connection.getTransactionIsolation());
                connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
                connection.setAutoCommit(false);

                handler.update(fullId, type, localId, rev, obj, connection);

                connection.commit();
                logger.debug("Commited updated object for id: {}", fullId);
            } catch (SQLException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("SQL Exception in update of {} with error code {}, sql state {}",
                            new Object[] { fullId, ex.getErrorCode(), ex.getSQLState(), ex });
                }
                rollback(connection);
                if (handler.isRetryable(ex, connection)) {
                    if (tryCount <= maxTxRetry) {
                        retry = true;
                        logger.debug("Retryable exception encountered, retry {}", ex.getMessage());
                    }
                }
                if (!retry) {
                    throw new InternalServerErrorException("Updating object failed " + ex.getMessage(), ex);
                }
            } catch (ObjectSetException ex) {
                logger.debug("ObjectSetException in update of {}", fullId, ex);
                rollback(connection);
                throw ex;
            } catch (java.io.IOException ex) {
                logger.debug("IO Exception in update of {}", fullId, ex);
                rollback(connection);
                throw new InternalServerErrorException("Conversion of object to update failed", ex);
            } catch (RuntimeException ex) {
                logger.debug("Runtime Exception in update of {}", fullId, ex);
                rollback(connection);
                throw new InternalServerErrorException(
                        "Updating object failed with unexpected failure: " + ex.getMessage(), ex);
            } finally {
                if (connection != null) {
                    try {
                        if (previousIsolationLevel != null) {
                            connection.setTransactionIsolation(previousIsolationLevel.intValue());
                        }
                    } catch (SQLException ex) {
                        logger.warn("Failure in resetting connection isolation level ", ex);
                    }
                    CleanupHelper.loggedClose(connection);
                }
            }
        } while (retry);
    }

    /**
     * Deletes the specified object from the object set.
     *
     * @param fullId the identifier of the object to be deleted.
     * @param rev    the version of the object to delete or {@code null} if not provided.
     * @throws NotFoundException           if the specified object could not be found.
     * @throws ForbiddenException          if access to the object is forbidden.
     * @throws ConflictException           if version is required but is {@code null}.
     * @throws PreconditionFailedException if version did not match the existing object in the set.
     */
    public void delete(String fullId, String rev) throws ObjectSetException {

        String localId = getLocalId(fullId);
        String type = getObjectType(fullId);

        if (rev == null) {
            throw new ConflictException("Object passed into delete does not have revision it expects set.");
        }

        Connection connection = null;
        boolean retry = false;
        int tryCount = 0;
        do {
            TableHandler handler = getTableHandler(type);
            if (handler == null) {
                throw new ObjectSetException("No handler configured for resource type " + type);
            }
            retry = false;
            ++tryCount;
            try {
                connection = getConnection();
                connection.setAutoCommit(false);

                handler.delete(fullId, type, localId, rev, connection);

                connection.commit();
                logger.debug("Commited deleted object for id: {}", fullId);
            } catch (IOException ex) {
                logger.debug("IO Exception in delete of {}", fullId, ex);
                rollback(connection);
                throw new InternalServerErrorException("Deleting object failed " + ex.getMessage(), ex);
            } catch (SQLException ex) {
                if (logger.isDebugEnabled()) {
                    logger.debug("SQL Exception in delete of {} with error code {}, sql state {}",
                            new Object[] { fullId, ex.getErrorCode(), ex.getSQLState(), ex });
                }
                rollback(connection);
                if (handler.isRetryable(ex, connection)) {
                    if (tryCount <= maxTxRetry) {
                        retry = true;
                        logger.debug("Retryable exception encountered, retry {}", ex.getMessage());
                    }
                }
                if (!retry) {
                    throw new InternalServerErrorException("Deleting object failed " + ex.getMessage(), ex);
                }
            } catch (ObjectSetException ex) {
                logger.debug("ObjectSetException in delete of {}", fullId, ex);
                rollback(connection);
                throw ex;
            } catch (RuntimeException ex) {
                logger.debug("Runtime Exception in delete of {}", fullId, ex);
                rollback(connection);
                throw new InternalServerErrorException(
                        "Deleting object failed with unexpected failure: " + ex.getMessage(), ex);
            } finally {
                CleanupHelper.loggedClose(connection);
            }
        } while (retry);
    }

    /**
     * Currently not supported by this implementation.
     * <p/>
     * Applies a patch (partial change) to the specified object in the object set.
     *
     * @param id    the identifier of the object to be patched.
     * @param rev   the version of the object to patch or {@code null} if not provided.
     * @param patch the partial change to apply to the object.
     * @throws ConflictException           if patch could not be applied object state or if version is required.
     * @throws ForbiddenException          if access to the object is forbidden.
     * @throws NotFoundException           if the specified object could not be found.
     * @throws PreconditionFailedException if version did not match the existing object in the set.
     */
    public void patch(String id, String rev, Patch patch) throws ObjectSetException {
        throw new UnsupportedOperationException();
    }

    /**
     * Performs the query on the specified object and returns the associated results.
     * <p/>
     * Queries are parametric; a set of named parameters is provided as the query criteria.
     * The query result is a JSON object structure composed of basic Java types.
     * <p/>
     * The returned map is structured as follow:
     * - The top level map contains meta-data about the query, plus an entry with the actual result records.
     * - The <code>QueryConstants</code> defines the map keys, including the result records (QUERY_RESULT)
     *
     * @param fullId identifies the object to query.
     * @param params the parameters of the query to perform.
     * @return the query results, which includes meta-data and the result records in JSON object structure format.
     * @throws NotFoundException   if the specified object could not be found.
     * @throws BadRequestException if the specified params contain invalid arguments, e.g. a query id that is not
     *                             configured, a query expression that is invalid, or missing query substitution tokens.
     * @throws ForbiddenException  if access to the object or specified query is forbidden.
     */
    public Map<String, Object> query(String fullId, Map<String, Object> params) throws ObjectSetException {
        // TODO: replace with common utility
        String type = fullId;
        logger.trace("Full id: {} Extracted type: {}", fullId, type);

        Map<String, Object> result = new HashMap<String, Object>();
        Connection connection = null;
        try {
            TableHandler handler = getTableHandler(type);
            if (handler == null) {
                throw new ObjectSetException("No handler configured for resource type " + type);
            }
            connection = getConnection();
            connection.setAutoCommit(true); // Ensure we do not implicitly start transaction isolation

            long start = System.currentTimeMillis();
            List<Map<String, Object>> docs = handler.query(type, params, connection);
            long end = System.currentTimeMillis();
            result.put(QueryConstants.QUERY_RESULT, docs);
            // TODO: split out conversion time
            //result.put(QueryConstants.STATISTICS_CONVERSION_TIME, Long.valueOf(convEnd-convStart));

            result.put(QueryConstants.STATISTICS_QUERY_TIME, Long.valueOf(end - start));

            if (logger.isDebugEnabled()) {
                logger.debug("Query result contains {} records, took {} ms and took {} ms to convert result.",
                        new Object[] { ((List) result.get(QueryConstants.QUERY_RESULT)).size(),
                                result.get(QueryConstants.STATISTICS_QUERY_TIME),
                                result.get(QueryConstants.STATISTICS_CONVERSION_TIME) });
            }
        } catch (SQLException ex) {
            if (logger.isDebugEnabled()) {
                logger.debug("SQL Exception in query of {} with error code {}, sql state {}",
                        new Object[] { fullId, ex.getErrorCode(), ex.getSQLState(), ex });
            }
            throw new InternalServerErrorException("Querying failed: " + ex.getMessage(), ex);
        } catch (ObjectSetException ex) {
            logger.debug("ObjectSetException in query of {}", fullId, ex);
            throw ex;
        } finally {
            CleanupHelper.loggedClose(connection);
        }
        return result;
    }

    public Map<String, Object> action(String fullId, Map<String, Object> params) throws ObjectSetException {
        throw new UnsupportedOperationException("JDBC repository does not support action");
    }

    // Utility method to cleanly roll back including logging
    private void rollback(Connection connection) {
        if (connection != null) {
            try {
                logger.debug("Rolling back transaction.");
                connection.rollback();
            } catch (SQLException ex) {
                logger.warn("Rolling back transaction reported failure ", ex);
            }
        }
    }

    // TODO: replace with common utility to handle ID, this is temporary
    private String getLocalId(String id) {
        String localId = null;
        int lastSlashPos = id.lastIndexOf("/");
        if (lastSlashPos > -1) {
            localId = id.substring(id.lastIndexOf("/") + 1);
        }
        logger.trace("Full id: {} Extracted local id: {}", id, localId);
        return localId;
    }

    // TODO: replace with common utility to handle ID, this is temporary
    private String getObjectType(String id) {
        String type = null;
        int lastSlashPos = id.lastIndexOf("/");
        if (lastSlashPos > -1) {
            int startPos = 0;
            // This should not be necessary as relative URI should not start with slash
            if (id.startsWith("/")) {
                startPos = 1;
            }
            type = id.substring(startPos, lastSlashPos);
            logger.trace("Full id: {} Extracted type: {}", id, type);
        }
        return type;
    }

    Connection getConnection() throws SQLException {
        if (useDataSource) {
            return ds.getConnection();
        } else {
            return DriverManager.getConnection(dbUrl, user, password);
        }
    }

    TableHandler getTableHandler(String type) {
        TableHandler handler = tableHandlers.get(type);
        if (handler != null) {
            return handler;
        } else {
            handler = defaultTableHandler;
            for (String key : tableHandlers.keySet()) {
                if (type.startsWith(key)) {
                    handler = tableHandlers.get(key);
                    logger.debug("Use table handler configured for {} for type {} ", key, type);
                }
            }
            // For future lookups remember the handler determined for this specific type
            tableHandlers.put(type, handler);
            return handler;
        }
    }

    /**
     * Populate and return a repository service that knows how to query and manipulate configuration.
     *
     * @param repoConfig the bootstrap configuration
     * @param context
     * @return the boot repository service. This instance is not managed by SCR and needs to be manually registered.
     */
    static RepoBootService getRepoBootService(JsonValue repoConfig, BundleContext context) {
        JDBCRepoService bootRepo = new JDBCRepoService();
        bootRepo.init(repoConfig, context);
        return bootRepo;
    }

    /**
     * Activates the JDBC Repository Service
     * 
     * @param compContext   The component context
     */
    @Activate
    void activate(ComponentContext compContext) {
        logger.debug("Activating Service with configuration {}", compContext.getProperties());
        try {
            config = enhancedConfig.getConfigurationAsJson(compContext);
        } catch (RuntimeException ex) {
            logger.warn("Configuration invalid and could not be parsed, can not start JDBC repository: "
                    + ex.getMessage(), ex);
            throw ex;
        }
        init(config, compContext.getBundleContext());

        logger.info("Repository started.");
    }

    /**
     * Deactivates the JDBC Repository Service
     * 
     * @param compContext   the component context
     */
    @Deactivate
    void deactivate(ComponentContext compContext) {
        logger.debug("Deactivating Service {}", compContext);
        logger.info("Repository stopped.");
    }

    /** 
     * Handles configuration updates without interrupting the service
     * 
     * @param compContext   the component context
     */
    @Modified
    void modified(ComponentContext compContext) throws Exception {
        logger.debug("Reconfiguring the JDBC Repository Service with configuration {}",
                compContext.getProperties());
        try {
            JsonValue newConfig = enhancedConfig.getConfigurationAsJson(compContext);
            if (hasConfigChanged(config, newConfig)) {
                deactivate(compContext);
                activate(compContext);
                logger.info("Reconfigured the JDBC Repository Service {}", compContext.getProperties());
            }
        } catch (Exception ex) {
            logger.warn("Configuration invalid, can not reconfigure the JDBC Repository Service.", ex);
            throw ex;
        }
    }

    /**
     * Compares the current configuration with a new configuration to determine if the
     * configuration has changed
     * 
     * @param existingConfig    the current configuration object
     * @param newConfig         the new configuration object
     * @return                  true if the configurations differ, false otherwise    
     */
    private boolean hasConfigChanged(JsonValue existingConfig, JsonValue newConfig) {
        return JsonPatch.diff(existingConfig, newConfig).size() > 0;
    }

    /**
     * Initializes the JDBC Repository Service with the supplied configuration
     * 
     * @param config            the configuration object
     * @param bundleContext     the bundle context
     * @throws InvalidException
     */
    void init(JsonValue config, BundleContext bundleContext) throws InvalidException {
        try {
            String enabled = config.get("enabled").asString();
            if ("false".equals(enabled)) {
                logger.debug("JDBC repository not enabled");
                throw new RuntimeException("JDBC repository not enabled.");
            }

            JsonValue connectionConfig = config.get(CONFIG_CONNECTION).isNull() ? config
                    : config.get(CONFIG_CONNECTION);

            maxTxRetry = connectionConfig.get("maxTxRetry").defaultTo(5).asInteger().intValue();

            // Data Source configuration
            jndiName = connectionConfig.get(CONFIG_JNDI_NAME).asString();
            String jtaName = connectionConfig.get(CONFIG_JTA_NAME).asString();
            if (jndiName != null && jndiName.trim().length() > 0) {
                // Get DB connection via JNDI
                logger.info("Using DB connection configured via Driver Manager");
                InitialContext ctx = null;
                try {
                    ctx = new InitialContext();
                } catch (NamingException ex) {
                    logger.warn("Getting JNDI initial context failed: " + ex.getMessage(), ex);
                }
                if (ctx == null) {
                    throw new InvalidException(
                            "Current platform context does not support lookup of repository DB via JNDI. "
                                    + " Configure DB initialization via direct " + CONFIG_DB_DRIVER
                                    + " configuration instead.");
                }

                useDataSource = true;
                ds = (DataSource) ctx.lookup(jndiName); // e.g. "java:comp/env/jdbc/MySQLDB"
            } else if (!StringUtils.isBlank(jtaName)) {
                // e.g. osgi:service/javax.sql.DataSource/(osgi.jndi.service.name=jdbc/openidm)
                OsgiName lookupName = OsgiName.parse(jtaName);
                Object service = ServiceUtil.getService(bundleContext, lookupName, null, true);
                if (service instanceof DataSource) {
                    useDataSource = true;
                    ds = (DataSource) service;
                } else {
                    throw new RuntimeException("DataSource can not be retrieved for: " + jtaName);
                }
            } else {
                // Get DB Connection via Driver Manager
                dbDriver = connectionConfig.get(CONFIG_DB_DRIVER).asString();
                if (dbDriver == null || dbDriver.trim().length() == 0) {
                    throw new InvalidException(
                            "Either a JNDI name (" + CONFIG_JNDI_NAME + "), " + "or a DB driver lookup ("
                                    + CONFIG_DB_DRIVER + ") needs to be configured to connect to a DB.");
                }
                dbUrl = connectionConfig.get(CONFIG_DB_URL).required().asString();
                user = connectionConfig.get(CONFIG_USER).required().asString();
                password = connectionConfig.get(CONFIG_PASSWORD).defaultTo("").asString();
                logger.info("Using DB connection configured via Driver Manager with Driver {} and URL", dbDriver,
                        dbUrl);
                try {
                    Class.forName(dbDriver);
                } catch (ClassNotFoundException ex) {
                    logger.error("Could not find configured database driver " + dbDriver + " to start repository ",
                            ex);
                    throw new InvalidException(
                            "Could not find configured database driver " + dbDriver + " to start repository ", ex);
                }
                Boolean enableConnectionPool = connectionConfig.get("enableConnectionPool").defaultTo(Boolean.FALSE)
                        .asBoolean();
                if (null == sharedDataSource) {
                    Dictionary<String, String> serviceParams = new Hashtable<String, String>(1);
                    serviceParams.put("osgi.jndi.service.name", "jdbc/openidm");
                    sharedDataSource = bundleContext.registerService(DataSource.class.getName(),
                            DataSourceFactory.newInstance(connectionConfig), serviceParams);
                }
                if (enableConnectionPool) {
                    ds = DataSourceFactory.newInstance(connectionConfig);
                    useDataSource = true;
                    logger.info("DataSource connection pool enabled.");
                } else {
                    logger.info("No DataSource connection pool enabled.");
                }
            }

            // Table handling configuration
            String dbSchemaName = connectionConfig.get(CONFIG_DB_SCHEMA).defaultTo(null).asString();
            JsonValue genericQueries = config.get("queries").get("genericTables");
            int maxBatchSize = connectionConfig.get(CONFIG_MAX_BATCH_SIZE).defaultTo(100).asInteger();

            tableHandlers = new HashMap<String, TableHandler>();
            //TODO Make safe the database type detection
            DatabaseType databaseType = DatabaseType.valueOf(
                    connectionConfig.get(CONFIG_DB_TYPE).defaultTo(DatabaseType.ANSI_SQL99.name()).asString());

            JsonValue defaultMapping = config.get("resourceMapping").get("default");
            if (!defaultMapping.isNull()) {
                defaultTableHandler = getGenericTableHandler(databaseType, defaultMapping, dbSchemaName,
                        genericQueries, maxBatchSize);
                logger.debug("Using default table handler: {}", defaultTableHandler);
            } else {
                logger.warn("No default table handler configured");
            }

            // Default the configuration table for bootstrap
            JsonValue defaultTableProps = new JsonValue(new HashMap());
            defaultTableProps.put("mainTable", "configobjects");
            defaultTableProps.put("propertiesTable", "configobjectproperties");
            defaultTableProps.put("searchableDefault", Boolean.FALSE);
            GenericTableHandler defaultConfigHandler = getGenericTableHandler(databaseType, defaultTableProps,
                    dbSchemaName, genericQueries, 1);
            tableHandlers.put("config", defaultConfigHandler);

            JsonValue genericMapping = config.get("resourceMapping").get("genericMapping");
            if (!genericMapping.isNull()) {
                for (String key : genericMapping.keys()) {
                    JsonValue value = genericMapping.get(key);
                    if (key.endsWith("/*")) {
                        // For matching purposes strip the wildcard at the end
                        key = key.substring(0, key.length() - 1);
                    }
                    TableHandler handler = getGenericTableHandler(databaseType, value, dbSchemaName, genericQueries,
                            maxBatchSize);

                    tableHandlers.put(key, handler);
                    logger.debug("For pattern {} added handler: {}", key, handler);
                }
            }

            JsonValue explicitQueries = config.get("queries").get("explicitTables");
            JsonValue explicitMapping = config.get("resourceMapping").get("explicitMapping");
            if (!explicitMapping.isNull()) {
                for (Object keyObj : explicitMapping.keys()) {
                    JsonValue value = explicitMapping.get((String) keyObj);
                    String key = (String) keyObj;
                    if (key.endsWith("/*")) {
                        // For matching purposes strip the wildcard at the end
                        key = key.substring(0, key.length() - 1);
                    }
                    TableHandler handler = getMappedTableHandler(databaseType, value,
                            value.get("table").required().asString(),
                            value.get("objectToColumn").required().asMap(), dbSchemaName, explicitQueries,
                            maxBatchSize);

                    tableHandlers.put(key, handler);
                    logger.debug("For pattern {} added handler: {}", key, handler);
                }
            }

        } catch (RuntimeException ex) {
            logger.warn("Configuration invalid, can not start JDBC repository.", ex);
            throw new InvalidException("Configuration invalid, can not start JDBC repository.", ex);
        } catch (NamingException ex) {
            throw new InvalidException("Could not find configured jndiName " + jndiName + " to start repository ",
                    ex);
        } catch (InternalServerErrorException ex) {
            throw new InvalidException("Could not initialize mapped table handler, can not start JDBC repository.",
                    ex);
        }

        Connection testConn = null;
        try {
            // Check if we can get a connection
            testConn = getConnection();
            testConn.setAutoCommit(true); // Ensure we do not implicitly start transaction isolation
        } catch (Exception ex) {
            logger.warn("JDBC Repository start-up experienced a failure getting a DB connection: " + ex.getMessage()
                    + ". If this is not temporary or resolved, Repository operation will be affected.", ex);
        } finally {
            if (testConn != null) {
                try {
                    testConn.close();
                } catch (SQLException ex) {
                    logger.warn("Failure during test connection close ", ex);
                }
            }
        }
    }

    GenericTableHandler getGenericTableHandler(DatabaseType databaseType, JsonValue tableConfig,
            String dbSchemaName, JsonValue queries, int maxBatchSize) {

        GenericTableHandler handler = null;

        // TODO: make pluggable
        switch (databaseType) {
        case DB2:
            handler = new DB2TableHandler(tableConfig, dbSchemaName, queries, maxBatchSize,
                    new DB2SQLExceptionHandler());
            break;
        case ORACLE:
            handler = new OracleTableHandler(tableConfig, dbSchemaName, queries, maxBatchSize,
                    new DefaultSQLExceptionHandler());
            break;
        case POSTGRESQL:
            handler = new PostgreSQLTableHandler(tableConfig, dbSchemaName, queries, maxBatchSize,
                    new DefaultSQLExceptionHandler());
            break;
        case MYSQL:
            handler = new GenericTableHandler(tableConfig, dbSchemaName, queries, maxBatchSize,
                    new MySQLExceptionHandler());
            break;
        case SQLSERVER:
            handler = new MSSQLTableHandler(tableConfig, dbSchemaName, queries, maxBatchSize,
                    new DefaultSQLExceptionHandler());
            break;
        default:
            handler = new GenericTableHandler(tableConfig, dbSchemaName, queries, maxBatchSize,
                    new DefaultSQLExceptionHandler());
        }
        return handler;
    }

    MappedTableHandler getMappedTableHandler(DatabaseType databaseType, JsonValue tableConfig, String table,
            Map objectToColumn, String dbSchemaName, JsonValue explicitQueries, int maxBatchSize)
            throws InternalServerErrorException {

        final Accessor<CryptoService> cryptoServiceAccessor = new Accessor<CryptoService>() {
            public CryptoService access() {
                return cryptoService;
            }
        };

        MappedTableHandler handler = null;

        // TODO: make pluggable
        switch (databaseType) {
        case DB2:
            handler = new MappedTableHandler(table, objectToColumn, dbSchemaName, explicitQueries,
                    new DB2SQLExceptionHandler(), cryptoServiceAccessor);
            break;
        case ORACLE:
            handler = new MappedTableHandler(table, objectToColumn, dbSchemaName, explicitQueries,
                    new DefaultSQLExceptionHandler(), cryptoServiceAccessor);
            break;
        case POSTGRESQL:
            handler = new MappedTableHandler(table, objectToColumn, dbSchemaName, explicitQueries,
                    new DefaultSQLExceptionHandler(), cryptoServiceAccessor);
            break;
        case MYSQL:
            handler = new MappedTableHandler(table, objectToColumn, dbSchemaName, explicitQueries,
                    new MySQLExceptionHandler(), cryptoServiceAccessor);
            break;
        case SQLSERVER:
            handler = new MSSQLMappedTableHandler(table, objectToColumn, dbSchemaName, explicitQueries,
                    new DefaultSQLExceptionHandler(), cryptoServiceAccessor);
            break;
        default:
            handler = new MappedTableHandler(table, objectToColumn, dbSchemaName, explicitQueries,
                    new DefaultSQLExceptionHandler(), cryptoServiceAccessor);
        }
        return handler;
    }
}