Java tutorial
/*************************************************************** * This file is part of the [fleXive](R) framework. * * Copyright (c) 1999-2014 * UCS - unique computing solutions gmbh (http://www.ucs.at) * All rights reserved * * The [fleXive](R) project is free software; you can redistribute * it and/or modify it under the terms of the GNU Lesser General Public * License version 2.1 or higher as published by the Free Software Foundation. * * The GNU Lesser General Public License can be found at * http://www.gnu.org/licenses/lgpl.html. * A copy is found in the textfile LGPL.txt and important notices to the * license from the author are found in LICENSE.txt distributed with * these libraries. * * This library 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. * * For further information about UCS - unique computing solutions gmbh, * please see the company website: http://www.ucs.at * * For further information about [fleXive](R), please see the * project website: http://www.flexive.org * * * This copyright notice MUST APPEAR in all copies of the file! ***************************************************************/ package com.flexive.ejb.beans.configuration; import com.flexive.core.Database; import com.flexive.core.DatabaseConst; import com.flexive.core.storage.StorageManager; import com.flexive.ejb.mbeans.FxCache; import com.flexive.shared.CacheAdmin; import com.flexive.shared.FxContext; import com.flexive.shared.FxSharedUtils; import com.flexive.shared.SimpleCacheStats; import com.flexive.shared.cache.FxCacheException; import com.flexive.shared.configuration.DivisionData; import com.flexive.shared.configuration.ParameterScope; import com.flexive.shared.configuration.SystemParameters; import com.flexive.shared.exceptions.FxApplicationException; import com.flexive.shared.exceptions.FxLoadException; import com.flexive.shared.exceptions.FxNoAccessException; import com.flexive.shared.interfaces.GlobalConfigurationEngine; import com.flexive.shared.interfaces.GlobalConfigurationEngineLocal; import com.flexive.shared.mbeans.FxCacheMBean; import com.flexive.shared.mbeans.MBeanHelper; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import javax.annotation.sql.DataSourceDefinition; import javax.annotation.sql.DataSourceDefinitions; import javax.ejb.*; import javax.management.InstanceAlreadyExistsException; import javax.management.MBeanRegistrationException; import javax.management.NotCompliantMBeanException; import javax.management.ObjectName; import javax.naming.NamingException; import java.io.Serializable; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.PreparedStatement; import java.sql.SQLException; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import static com.flexive.core.DatabaseConst.TBL_CONFIG_GLOBAL; /** * Global configuration MBean. * * <p> This bean also configures a default data source for Java EE 6 containers, using an embedded (file-based) H2 database. * This will be used as a fallback <strong>for division 1</strong> if no database connection is configured. * Also, a fallback global data source is configured.</p> * * <p>The data source uses a relative path for the database file, * so it will usually be created somewhere in the application server's directory. This has the advantage over * an absolute URL that multiple installations are possible for the same user, although all applications * in that application server will necessarily share the same default data source. * </p> * * @author Daniel Lichtenberger (daniel.lichtenberger@flexive.com), UCS - unique computing solutions gmbh (http://www.ucs.at) */ @DataSourceDefinitions({ @DataSourceDefinition(name = GlobalConfigurationEngineBean.DEFAULT_DS, className = "org.h2.jdbcx.JdbcDataSource", properties = "URL=jdbc:h2:h2/flexive;SCHEMA=flexive;LOCK_TIMEOUT=10000;TRACE_LEVEL_FILE=0", user = "sa", password = "sa", transactional = true // url="jdbc:h2:h2/flexive;SCHEMA=flexive;LOCK_TIMEOUT=10000;TRACE_LEVEL_FILE=0" ), @DataSourceDefinition(name = GlobalConfigurationEngineBean.DEFAULT_DS + "NoTX", className = "org.h2.jdbcx.JdbcDataSource", properties = "URL=jdbc:h2:h2/flexive;SCHEMA=flexive;LOCK_TIMEOUT=10000;TRACE_LEVEL_FILE=0", user = "sa", password = "sa", transactional = false // url="jdbc:h2:h2/flexive;SCHEMA=flexive;LOCK_TIMEOUT=10000;TRACE_LEVEL_FILE=0" ), @DataSourceDefinition(name = GlobalConfigurationEngineBean.DEFAULT_DS_CONFIG, className = "org.h2.jdbcx.JdbcDataSource", properties = "URL=jdbc:h2:h2/flexive;SCHEMA=flexiveConfiguration;LOCK_TIMEOUT=10000;TRACE_LEVEL_FILE=0", user = "sa", password = "sa", transactional = true // url="jdbc:h2:h2/flexive;SCHEMA=flexiveConfiguration;LOCK_TIMEOUT=10000;TRACE_LEVEL_FILE=0" ), @DataSourceDefinition(name = GlobalConfigurationEngineBean.DEFAULT_DS_INIT, className = "org.h2.jdbcx.JdbcDataSource", properties = "URL=jdbc:h2:h2/flexive;LOCK_TIMEOUT=10000;TRACE_LEVEL_FILE=0", user = "sa", password = "sa", transactional = false // url="jdbc:h2:h2/flexive;SCHEMA=flexive;LOCK_TIMEOUT=10000;TRACE_LEVEL_FILE=0" ) }) @TransactionManagement(TransactionManagementType.CONTAINER) @TransactionAttribute(TransactionAttributeType.REQUIRED) @Stateless(name = "GlobalConfigurationEngine", mappedName = "GlobalConfigurationEngine") public class GlobalConfigurationEngineBean extends GenericConfigurationImpl implements GlobalConfigurationEngine, GlobalConfigurationEngineLocal { /** * The default data source bound in JEE 6 containers. */ public static final String DEFAULT_DS = "java:global/flexive-ejb/DefaultDS"; /** * The default configuration data source bound in JEE 6 containers. */ public static final String DEFAULT_DS_CONFIG = "java:global/flexive-ejb/DefaultConfigurationDS"; /** * An extra data source without a schema for initialization. */ public static final String DEFAULT_DS_INIT = "java:global/flexive-ejb/DefaultInitDS"; /** * Maximum number of cached domains per hit/miss cache * This should be at least roughly equal to the number of configured * domains since the miss cache will likely be thrashed otherwise. */ private static final int MAX_CACHED_DOMAINS = 1000; /** * Cache path for storing config parameters */ private static final String CACHE_CONFIG = "/globalconfig/"; /** * Cache path for storing other values */ private static final String CACHE_BEAN = "/globalconfigMBean/"; /** * Cache path suffix for storing division data */ private static final String CACHE_DIVISIONS = "divisionData"; /** * Cache key for the timestamp of the last change in the division mapping tables. */ private static final String CACHE_TIMESTAMP = "timestamp"; private static final Log LOG = LogFactory.getLog(GlobalConfigurationEngineBean.class); /** * Cached local copy of divisions, must be cleared if the cache is cleared */ private static final List<DivisionData> divisions = new CopyOnWriteArrayList<DivisionData>(); private static final AtomicLong divisionsTimestamp = new AtomicLong(-1); /** * Cache for mapping domain names to division IDs. Cleared when the division cache is cleared. */ private static final ConcurrentMap<String, Integer> domainCache = new ConcurrentHashMap<String, Integer>( MAX_CACHED_DOMAINS); private static final AtomicLong domainCacheTimestamp = new AtomicLong(-1); /** * Simple cache stats (displayed on shutdown) */ private static final SimpleCacheStats cacheStats = new SimpleCacheStats("Global get"); /** * {@inheritDoc} */ @Override public void create() throws Exception { // System.out.println("************ Creating global config ***************"); } /** * {@inheritDoc} */ @Override public void destroy() throws Exception { System.out.println("Global config cache stats: "); System.out.println(cacheStats.toString()); System.out.println(); } // implement Configuration methods @Override protected ParameterScope getDefaultScope() { return ParameterScope.GLOBAL; } /** * {@inheritDoc} */ @Override protected Connection getConnection() throws SQLException { return Database.getGlobalDbConnection(); } /** * Get the global configuration table name including the correct schema * * @param con an open and valid connection to determine the correct storage vendor * @return global configuration table name including the escaped schema */ private String getConfigurationTable(Connection con) { try { if (StorageManager.getStorageImpl(con.getMetaData().getDatabaseProductName()).requiresConfigSchema()) { if (DatabaseConst.getConfigSchema().endsWith(".")) return DatabaseConst.getConfigSchema() + TBL_CONFIG_GLOBAL; else return DatabaseConst.getConfigSchema() + "." + TBL_CONFIG_GLOBAL; } } catch (SQLException e) { LOG.warn(e); } return TBL_CONFIG_GLOBAL; } /** * {@inheritDoc} */ @Override protected PreparedStatement getSelectStatement(Connection conn, String path, String key) throws SQLException { String sql = "SELECT cvalue FROM " + getConfigurationTable(conn) + " WHERE cpath=? and ckey=?"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, path); stmt.setString(2, key); return stmt; } /** * {@inheritDoc} */ @Override protected PreparedStatement getSelectStatement(Connection conn, String path) throws SQLException { String sql = "SELECT ckey, cvalue FROM " + getConfigurationTable(conn) + " WHERE cpath=?"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, path); return stmt; } @Override protected PreparedStatement getSelectStatement(Connection conn) throws SQLException { throw new UnsupportedOperationException("Select of ALL parameters not supported in global configuration."); } /** * {@inheritDoc} */ @Override protected PreparedStatement getUpdateStatement(Connection conn, String path, String key, String value, String className) throws SQLException, FxNoAccessException { if (!isAuthorized()) { throw new FxNoAccessException("ex.configuration.update.perm.global"); } // TODO: support className/getAll() in global configuration? String sql = "UPDATE " + getConfigurationTable(conn) + " SET cvalue=? WHERE cpath=? AND ckey=?"; PreparedStatement stmt = conn.prepareStatement(sql); StorageManager.setBigString(stmt, 1, value); stmt.setString(2, path); stmt.setString(3, key); return stmt; } /** * {@inheritDoc} */ @Override protected PreparedStatement getInsertStatement(Connection conn, String path, String key, String value, String className) throws SQLException, FxNoAccessException { if (!isAuthorized()) { throw new FxNoAccessException("ex.configuration.update.perm.global"); } // TODO: support className/getAll() in global configuration? String sql = "INSERT INTO " + getConfigurationTable(conn) + "(cpath, ckey, cvalue) VALUES (?, ?, ?)"; PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, path); stmt.setString(2, key); StorageManager.setBigString(stmt, 3, value); return stmt; } /** * {@inheritDoc} */ @Override protected PreparedStatement getDeleteStatement(Connection conn, String path, String key) throws SQLException, FxNoAccessException { if (!isAuthorized()) { throw new FxNoAccessException("ex.configuration.delete.perm.global"); } String sql = "DELETE FROM " + getConfigurationTable(conn) + " WHERE cpath=? " + (key != null ? " AND ckey=?" : ""); PreparedStatement stmt = conn.prepareStatement(sql); stmt.setString(1, path); if (key != null) { stmt.setString(2, key); } return stmt; } /** * {@inheritDoc} */ @Override protected String getCachePath(String path) { // global parameters have no dynamic context return CACHE_CONFIG + path; } // add global configuration-specific methods /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public int[] getDivisionIds() throws FxApplicationException { try { // check cache int[] cachedDivisionIds = (int[]) getCache(getBeanPath(CACHE_DIVISIONS), "allDivisions"); if (cachedDivisionIds != null) { return cachedDivisionIds; } } catch (FxCacheException e) { LOG.error("Cache failure (ignored): " + e.getMessage(), e); } // get list of all configured divisions Map<String, String> domainMappings = getAll(SystemParameters.GLOBAL_DIVISIONS_DOMAINS); int[] divisionIds = new int[domainMappings.keySet().size()]; int ctr = 0; for (String divisionId : domainMappings.keySet()) { divisionIds[ctr++] = Integer.parseInt(divisionId); } Arrays.sort(divisionIds); putCache(getBeanPath(CACHE_DIVISIONS), "allDivisions", divisionIds, false); return divisionIds; } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public DivisionData[] getDivisions() throws FxApplicationException { final long timestamp = getTimestamp(); if (divisions.isEmpty() || divisionsTimestamp.get() < timestamp) { synchronized (divisions) { if (divisions.isEmpty()) { divisionsTimestamp.set(timestamp); final int[] divisionIds = getDivisionIds(); final List<DivisionData> divisionList = new ArrayList<DivisionData>(divisionIds.length); for (int divisionId : divisionIds) { try { divisionList.add(getDivisionData(divisionId)); } catch (Exception e) { LOG.error("Invalid division data (ignored): " + e.getMessage()); } } divisions.addAll(divisionList); } } } return divisions.toArray(new DivisionData[divisions.size()]); } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public DivisionData getDivisionData(int division) throws FxApplicationException { try { DivisionData data = (DivisionData) getCache(getBeanPath(CACHE_DIVISIONS), "" + division); if (data != null) { return data; } } catch (FxCacheException e) { LOG.error("Cache failure (ignored): " + e.getMessage(), e); } // get datasource String dataSource = get(SystemParameters.GLOBAL_DATASOURCES, "" + division); String domainRegEx = get(SystemParameters.GLOBAL_DIVISIONS_DOMAINS, "" + division); DivisionData data = createDivisionData(division, dataSource, domainRegEx); // put in cache putCache(getBeanPath(CACHE_DIVISIONS), "" + division, data, false); return data; } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public DivisionData createDivisionData(int divisionId, String dataSource, String domainRegEx) { String dbVendor = "unknown"; String dbVersion = "unknown"; String dbDriverVersion = "unknown"; boolean available = false; Connection con = null; try { // lookup non-transactional datasource to avoid issues with the default JEE6 data source in Glassfish con = Database.getDataSource(dataSource + "NoTX").getConnection(); DatabaseMetaData dbmd = con.getMetaData(); dbVendor = dbmd.getDatabaseProductName(); dbVersion = dbmd.getDatabaseProductVersion(); dbDriverVersion = dbmd.getDriverName() + " " + dbmd.getDriverVersion(); available = true; } catch (NamingException e) { LOG.error("Failed to get datasource " + dataSource + " (flagged inactive)"); } catch (SQLException e) { if (LOG.isDebugEnabled()) { LOG.debug("Failed to get database meta information: " + e.getMessage(), e); } } finally { Database.closeObjects(GlobalConfigurationEngineBean.class, con, null); } return new DivisionData(divisionId, available, dataSource, domainRegEx, dbVendor, dbVersion, dbDriverVersion); } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public int getDivisionId(String serverName) throws FxApplicationException { final long timestamp = getTimestamp(); if (domainCacheTimestamp.get() >= timestamp) { Integer cachedDivisionId = domainCache.get(serverName); if (cachedDivisionId != null) { return cachedDivisionId; } } else { synchronized (domainCache) { domainCache.clear(); domainCacheTimestamp.set(timestamp); } } DivisionData[] divisionIds = getDivisions(); int divisionId = -1; for (DivisionData division : divisionIds) { if (division.isMatchingDomain(serverName)) { divisionId = division.getId(); break; } } synchronized (domainCache) { if (domainCache.size() > MAX_CACHED_DOMAINS) { domainCache.clear(); } domainCache.put(serverName, divisionId); } return divisionId; } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public void saveDivisions(List<? extends DivisionData> divisions) throws FxApplicationException { removeAll(SystemParameters.GLOBAL_DATASOURCES); removeAll(SystemParameters.GLOBAL_DIVISIONS_DOMAINS); clearDivisionCache(); for (DivisionData division : divisions) { // remove the "java:" prefix that may have been appended on some containers, use the canonical JDBC string final String storedDataSource = division.getDataSource().replaceFirst("^java:", ""); // store parameters put(SystemParameters.GLOBAL_DATASOURCES, String.valueOf(division.getId()), storedDataSource); put(SystemParameters.GLOBAL_DIVISIONS_DOMAINS, String.valueOf(division.getId()), division.getDomainRegEx()); } updateTimestamp(); } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public String getRootLogin() throws FxApplicationException { return get(SystemParameters.GLOBAL_ROOT_LOGIN); } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public String getRootPassword() throws FxApplicationException { return get(SystemParameters.GLOBAL_ROOT_PASSWORD); } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public boolean isMatchingRootPassword(String userPassword) throws FxApplicationException { String hashedPassword = getRootPassword(); return getHashedPassword(userPassword).matches(hashedPassword); } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public void setRootLogin(String value) throws FxApplicationException { put(SystemParameters.GLOBAL_ROOT_LOGIN, value); } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public void setRootPassword(String value) throws FxApplicationException { put(SystemParameters.GLOBAL_ROOT_PASSWORD, getHashedPassword(value)); } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public void clearDivisionCache() { FxCacheMBean cache = CacheAdmin.getInstance(); try { // clear local caches synchronized (divisions) { divisions.clear(); } synchronized (domainCache) { domainCache.clear(); } // clear shared cache cache.globalRemove(getBeanPath(CACHE_DIVISIONS)); } catch (FxCacheException e) { LOG.error("Failed to clear cache: " + e.getMessage(), e); } } /** * {@inheritDoc} */ @Override @TransactionAttribute(TransactionAttributeType.REQUIRED) public void registerCacheMBean(ObjectName name) throws MBeanRegistrationException, NotCompliantMBeanException, InstanceAlreadyExistsException { //TODO: some exception handling and cross checks for prev. registrations wouldn't hurt ... //TODO: maybe create a system beans and move this method there MBeanHelper.locateServer().registerMBean(new FxCache(), name); } /** * Get the complete cache path for miscellaneous internal paths. * * @param path cache path to be stored * @return the complete cache path for miscellaneous internal paths. */ private String getBeanPath(String path) { return CACHE_BEAN + path; } /** * {@inheritDoc} */ @Override protected void putCache(String path, String key, Serializable value, boolean fromUpdate) { // put parameter in the global (without division data) cache try { CacheAdmin.getInstance().globalPut(path, key, value); } catch (FxCacheException e) { LOG.error("Failed to update cache (ignored): " + e.getMessage()); } } /** * {@inheritDoc} */ @Override protected void deleteCache(String path, String key) { try { CacheAdmin.getInstance().globalRemove(path, key); } catch (FxCacheException e) { LOG.error("Failed to update cache (ignored): " + e.getMessage()); } } /** * {@inheritDoc} */ @Override protected void deleteCache(String path) { try { CacheAdmin.getInstance().globalRemove(path); } catch (FxCacheException e) { LOG.error("Failed to update cache (ignored): " + e.getMessage()); } } /** * {@inheritDoc} */ @Override protected Serializable getCache(String path, String key) throws FxCacheException { FxCacheMBean cache = CacheAdmin.getInstance(); return (Serializable) cache.globalGet(path, key); } /** * {@inheritDoc} */ @Override protected void logCacheHit(String path, String key) { cacheStats.addHit(); } /** * {@inheritDoc} */ @Override protected void logCacheMiss(String path, String key) { cacheStats.addMiss(); } /** * Return the timestamp of the last modification on the division data table. * * @return the timestamp of the last modification on the division data table. */ private long getTimestamp() { Long timestamp; try { timestamp = (Long) getCache(getBeanPath(""), CACHE_TIMESTAMP); } catch (FxCacheException e) { throw new FxLoadException(e).asRuntimeException(); } if (timestamp == null) { timestamp = System.currentTimeMillis(); putCache(getBeanPath(""), CACHE_TIMESTAMP, timestamp, false); } return timestamp; } /** * Update the division data table timestamp. */ private void updateTimestamp() { putCache(getBeanPath(""), CACHE_TIMESTAMP, System.currentTimeMillis(), true); } /** * Returns true if the calling user is authorized for manipulating * the global configuration. * * @return true if the calling user is authorized for manipulating * the global configuration. */ private boolean isAuthorized() { return FxContext.get().isGlobalAuthenticated(); } /** * Compute the hashed password for the given input. * * @param userPassword the password to be hashed * @return the hashed password for the given input. */ private String getHashedPassword(String userPassword) { return FxSharedUtils.hashPassword(31289, "global-system-user", userPassword); } }