Java tutorial
/* * iteraplan is an IT Governance web application developed by iteratec, GmbH * Copyright (C) 2004 - 2014 iteratec, GmbH * * This program is free software; you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License version 3 as published by * the Free Software Foundation with the addition of the following permission * added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED * WORK IN WHICH THE COPYRIGHT IS OWNED BY ITERATEC, ITERATEC DISCLAIMS THE * WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with this program; if not, see http://www.gnu.org/licenses or write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, * MA 02110-1301 USA. * * You can contact iteratec GmbH headquarters at Inselkammerstr. 4 * 82008 Munich - Unterhaching, Germany, or at email address info@iteratec.de. * * The interactive user interfaces in modified source and object code versions * of this program must display Appropriate Legal Notices, as required under * Section 5 of the GNU Affero General Public License version 3. * * In accordance with Section 7(b) of the GNU Affero General Public License * version 3, these Appropriate Legal Notices must retain the display of the * "iteraplan" logo. If the display of the logo is not reasonably * feasible for technical reasons, the Appropriate Legal Notices must display * the words "Powered by iteraplan". */ package de.iteratec.iteraplan.persistence; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.sql.Connection; import java.sql.SQLException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.sql.DataSource; import org.apache.commons.dbcp.BasicDataSource; import org.springframework.beans.factory.InitializingBean; import org.springframework.jdbc.datasource.AbstractDataSource; import org.springframework.jdbc.datasource.lookup.DataSourceLookup; import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup; import de.iteratec.iteraplan.common.Constants; import de.iteratec.iteraplan.common.Logger; import de.iteratec.iteraplan.common.UserContext; import de.iteratec.iteraplan.common.util.IteraplanProperties; /** * This class provides functionality to dynamically add a {@link javax.sql.DataSource} at * runtime to route different users to different databases. The code's majority is based on * Spring's {@link org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource}. * <p> * Note that this class permits adding any type of object as a data source, but the default * implementation supports {@link javax.sql.DataSource}s and String objects which will be * resolved by means of a {@link #setDataSourceLookup(DataSourceLookup) DataSourceLookup}. * <p> * Spring's implementation is limited to reading and storing bean definitions of data * sources injected at container startup into a map of possible target data sources. * <p> * To avoid storing unlimited numbers of data sources, the current implementation provides * a method to add data sources of type {@link CachableBasicDataSource}. These carry an * access identifier to decide which data source to remove from the map of target data * sources. * <p> * See also: http://blog.interface21.com/main/2007/01/23/dynamic-datasource-routing/ */ public class RoutingDataSource extends AbstractDataSource implements InitializingBean { private static final Logger LOGGER = Logger.getIteraplanLogger(RoutingDataSource.class); /** {@link #setTargetDataSources()} */ private Map<Object, DataSource> targetDataSources; private Map<Object, DataSource> resolvedDataSources; private static final Object RESOLVED_DATA_SOURCES_LOCK = new Object(); /** {@link #setDataSourceLookup(DataSourceLookup)} */ private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); /** {@link #setCloseDataSources(String) } */ private Map<String, String> closeDataSources; // time stamp when the last log entry about pool utilization was made; the timestamp is used to throttle logging private long previousLoggingTimestamp; /** * Adds a data source of type {@link CachableBasicDataSource} to the map of * {@link #setTargetDataSources(Map) target data sources}. * * @param key * The lookup key the data source is mapped to. * @param value * The data source. */ public void addCachableBasicDataSource(Object key, DataSource value) { if (!(value instanceof CachableBasicDataSource)) { throw new IllegalArgumentException("The parameter 'value' must be of type 'CachableDataSource'."); } int maximumCacheSize = IteraplanProperties .getIntProperty(IteraplanProperties.MAXIMUM_SIMULTANEOUS_DATASOURCES); if (maximumCacheSize <= 0) { throw new IllegalArgumentException("The cache size property must have a value greater than zero."); } // This code block needs to be synchronized because only ONE thread at a time must be allowed // to change the shared map of target data sources. Otherwise race conditions could occur. // All other accesses to targetDataSources can remain without explicit synchronization, because they // operate on ConcurrentHashMap. synchronized (this) { if (targetDataSources.size() >= maximumCacheSize) { logger.info("Cache exceeded: Trying to remove least recently accessed data source."); long timestampToRemove = System.currentTimeMillis(); Object lookupKeyToRemove = null; DataSource dataSourceToRemove = null; for (Map.Entry<Object, DataSource> entry : this.targetDataSources.entrySet()) { Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); // The MASTER data source must not be removed from the map. if (lookupKey.equals(Constants.MASTER_DATA_SOURCE)) { continue; } if (!(dataSource instanceof CachableBasicDataSource)) { throw new IllegalStateException( "Target data source is of type '" + dataSource.getClass().getSimpleName() + "', but must be of type 'CachableDataSource'"); } long timestamp = ((CachableBasicDataSource) dataSource).getTimestamp(); if (timestamp < timestampToRemove) { timestampToRemove = timestamp; lookupKeyToRemove = lookupKey; dataSourceToRemove = dataSource; logger.trace("The currently oldest timestamp is " + timestampToRemove + " for the data source with the lookup key " + lookupKeyToRemove); } } logger.info("Removing data source '" + lookupKeyToRemove + "' with timestamp " + timestampToRemove); if (lookupKeyToRemove != null) { targetDataSources.remove(lookupKeyToRemove); } if (dataSourceToRemove != null) { destroyDataSource(dataSourceToRemove); } } } logger.info("Adding key '" + key + "' to the map of data sources."); targetDataSources.put(key, value); updateDataSourceRouter(); } /* * (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ public void afterPropertiesSet() { if (targetDataSources == null) { throw new IllegalArgumentException("The property 'targetDataSources' must not be null."); } updateDataSourceRouter(); } /** * Callback method to hook into the Spring Bean lifecylce. * <p> * This implementation closes all data sources managed by this bean. * <p> * @see #setCloseDataSources(Map) */ public void destroy() { for (Object dataSource : resolvedDataSources.values()) { destroyDataSource(dataSource); } } /* * (non-Javadoc) * @see javax.sql.DataSource#getConnection() */ public Connection getConnection() throws SQLException { return determineTargetDataSource().getConnection(); } /* * (non-Javadoc) * @see javax.sql.DataSource#getConnection(java.lang.String, java.lang.String) */ public Connection getConnection(String username, String password) throws SQLException { return determineTargetDataSource().getConnection(username, password); } /** * Checks if the given lookup key is already contained in the data source router. * * @param key * The key to look for in the data source router. * * @return * {@code True}, if the key is contained. {@code False}, otherwise. */ public boolean isKeyContained(String key) { return resolvedDataSources.containsKey(key); } /** * In order to support different implementations of {@link javax.sql.DataSource}s, a map may be * specified which maps the simple class names of theses implementations to their respective close * methods, i.e. the methods that free up any resources occupied by the data source. These methods * are called via {@link #destroy() reflection}. If some implementation does not provide a close * method, nothing will be done before the Spring container shuts down. * <p> * Note that supported close methods do not support parameters. * * @param closeDataSources * See method description. */ public void setCloseDataSources(Map<String, String> closeDataSources) { this.closeDataSources = closeDataSources; } /** * Specify the {@link DataSourceLookup} implementation to use for resolving data source name * strings in the {@link #setTargetDataSources targetDataSources} map. * <p> * The property defaults to a {@link JndiDataSourceLookup}, allowing the JNDI names of application * server data sources to be specified directly. */ public void setDataSourceLookup(DataSourceLookup dataSourceLookup) { this.dataSourceLookup = (dataSourceLookup != null ? dataSourceLookup : new JndiDataSourceLookup()); } /** * Specify a map of available target data sources. Data sources are mapped by a lookup key. The value * may contain one of the following: * <ul> * <li>An instance of {@link javax.sql.DataSource}.</li> * <li>A data source name string (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}.</li> * <ul> * The key may be of arbitrary type; this class implements the generic lookup process only. The * concrete key representation will be handled by {@link #resolveSpecifiedLookupKey(Object)} and * {@link #determineCurrentLookupKey()}. */ public void setTargetDataSources(Map<Object, DataSource> targetDataSources) { this.targetDataSources = new ConcurrentHashMap<Object, DataSource>(targetDataSources); } /** * Reflectively calls a method specified in the {@link #closeDataSources} map that closes all * resources occupied by the given data source. The matching between this object and the close * method is done via the simple class name of the passed-in object. Note that for now this * implementation only supports close methods without parameters. * * @param dataSource * The data source object to close. */ private void destroyDataSource(Object dataSource) { String simpleName = dataSource.getClass().getSimpleName(); if (closeDataSources.containsKey(simpleName)) { String closeMethod = closeDataSources.get(simpleName); // Close data sources via reflection. try { Method method = dataSource.getClass().getMethod(closeMethod); method.invoke(dataSource); logger.info("Data source of type " + simpleName + " closed."); } catch (NoSuchMethodException e) { logger.error("Exception: Destroying data source '" + dataSource + "'.", e); } catch (IllegalArgumentException e) { logger.error("Exception: Destroying data source '" + dataSource + "'.", e); } catch (IllegalAccessException e) { logger.error("Exception: Destroying data source '" + dataSource + "'.", e); } catch (InvocationTargetException e) { logger.error("Exception: Destroying data source '" + dataSource + "'.", e); } } } /** * Updates the data source router in case new data sources have been added. */ private void updateDataSourceRouter() { synchronized (RESOLVED_DATA_SOURCES_LOCK) { // no need to synchronize access on targetDataSources, as it is a ConcurrentHashMap if (resolvedDataSources == null) { resolvedDataSources = new ConcurrentHashMap<Object, DataSource>(targetDataSources.size()); } resolvedDataSources.clear(); for (Map.Entry<Object, DataSource> entry : this.targetDataSources.entrySet()) { Object lookupKey = resolveSpecifiedLookupKey(entry.getKey()); DataSource dataSource = resolveSpecifiedDataSource(entry.getValue()); resolvedDataSources.put(lookupKey, dataSource); } } } /** * The current lookup key is determined via the thread-bound {@link UserContext} object of * iteraplan. This object holds a {@link UserContext#getDataSource() dataSource} property * which contains the lookup key to the data source to be used. * <p> * Allows for arbitrary keys. The returned key needs to match the stored lookup key type, as * resolved by the {@link #resolveSpecifiedLookupKey} method. */ protected Object determineCurrentLookupKey() { UserContext uc = UserContext.getCurrentUserContext(); if (uc != null) { String lookupKey = uc.getDataSource(); logger.debug("Data source: " + lookupKey); return lookupKey; } else { logger.debug("Data source: MASTER"); return Constants.MASTER_DATA_SOURCE; } } /** * Retrieve the current target data source. * <ul> * <li>Updates the data source router in case new data sources have been added.</li> * <li>Determines the {@link #determineCurrentLookupKey() current lookup key}</li> * <li>Performs a lookup in the {@link #setTargetDataSources targetDataSources} map</li> * * @see #determineCurrentLookupKey() */ protected DataSource determineTargetDataSource() { if (resolvedDataSources == null) { throw new IllegalStateException("The data source router has not been initialized."); } // Retrieve the current target data source. Object lookupKey = determineCurrentLookupKey(); DataSource dataSource = resolvedDataSources.get(lookupKey); if (dataSource == null) { throw new IllegalStateException( "Target data source for lookup key [" + lookupKey + "] cannot be determined."); } if (LOGGER.isDebugEnabled()) { logPoolUtilization(dataSource, lookupKey.toString()); } return dataSource; } /** * Write current utilization values for the passed data source source to the log (on DEBUG level). Log entries are written in intervals of at least * five seconds. This is intended so that the log is not flooded. * * @param ds * The DataSource to inspect. Right now, only {@link BasicDataSource} is supported, other implementations will be silently skipped. * @param dbname * The name of the DataSource, as it is registered in the RoutingDataSource. */ @SuppressWarnings("boxing") protected void logPoolUtilization(DataSource ds, String dbname) { if (!(ds instanceof BasicDataSource)) { // cannot retrieve values from other types, so skip return; } long currentTime = System.currentTimeMillis(); // log at most every five seconds if (currentTime - previousLoggingTimestamp < 5000) { return; } previousLoggingTimestamp = currentTime; BasicDataSource bds = (BasicDataSource) ds; int bdsNumActive = bds.getNumActive(); int bdsMaxActive = bds.getMaxActive(); int bdsNumIdle = bds.getNumIdle(); LOGGER.debug("DS {0}, {1} active, {2} idle, {3} max total", dbname, bdsNumActive, bdsNumIdle, bdsMaxActive); } /** * Resolve the specified object into a {@link javax.sql.DataSource} instance. * <p> * The default implementation handles: * <ul> * <li>Instances of {@link javax.sql.DataSource}</li> * <li>Data source names (to be resolved via a {@link #setDataSourceLookup DataSourceLookup}).</li> * * @param dataSource * The data source object as specified in the {@link #setTargetDataSources targetDataSources} map. * * @return * The resolved {@link javax.sql.DataSource} (Never <code>null</code>). * * @throws IllegalArgumentException * If the given object is of an unsupported type. */ protected DataSource resolveSpecifiedDataSource(Object dataSource) { if (dataSource instanceof DataSource) { return (DataSource) dataSource; } else if (dataSource instanceof String) { return dataSourceLookup.getDataSource((String) dataSource); } else { throw new IllegalArgumentException( "Illegal data source type - only [javax.sql.DataSource] and String supported: " + dataSource); } } /** * Resolves the given lookup key, as specified in the {@link #setTargetDataSources * targetDataSources} map, into the actual lookup key to be used for matching with * the {@link #determineCurrentLookupKey() current lookup key}. * <p> * The default implementation simply returns the given key as-is. * * @param lookupKey * The lookup key object as specified by the user. * * @return * The lookup key as needed for matching */ protected Object resolveSpecifiedLookupKey(Object lookupKey) { return lookupKey; } public java.util.logging.Logger getParentLogger() { return null; } }