Java tutorial
/* * Copyright (C) 2013 Jajja Communications AB * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ package com.jajja.jorm; import java.io.Closeable; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Method; import java.net.URL; import java.sql.SQLException; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import javax.sql.DataSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * The database abstraction implemented for {@link Jorm} mapped records. Relies * on {@link javax.sql.DataSource} for data access to configured data bases. One * recommended implementation for easy configuration is * <tt>org.apache.commons.dbcp.BasicDataSource</tt> from the Apache project * <tt>commons-dbcp</tt>. * * @see Jorm * @see Record * @author Martin Korinth <martin.korinth@jajja.com> * @author Andreas Allerdahl <andreas.allerdahl@jajja.com> * @author Daniel Adolfsson <daniel.adolfsson@jajja.com> * @since 1.0.0 */ public class Database { private ThreadLocal<HashMap<String, Transaction>> transactions = new ThreadLocal<HashMap<String, Transaction>>(); private Map<String, DataSource> dataSources = new HashMap<String, DataSource>(); private ThreadLocal<HashMap<String, Context>> contextStack = new ThreadLocal<HashMap<String, Context>>(); private String globalDefaultContext = ""; private Map<String, String> defaultContext = new HashMap<String, String>(); private Logger log = LoggerFactory.getLogger(Database.class); private static volatile Database instance = new Database(); private static boolean configured; private Database() { } /** * Acts as singleton factory for bean configuration access. All other access * to databases should be static. * * @return the singleton database representation containing configured data * sources for databases. */ public static synchronized Database get() { return instance; } private HashMap<String, Transaction> getTransactions() { if (transactions.get() == null) { transactions.set(new HashMap<String, Transaction>()); } return transactions.get(); } private DataSource getDataSource(String database) { synchronized (dataSources) { return dataSources.get(database); } } /** * Configures all databases accessible through {@link Database#open(String)} * and {@link Database#close(String)}. Overrides any previous configuration. * * @param dataSources the named databases, each represented by a string and * a data source. */ public void setDataSources(Map<String, DataSource> dataSources) { this.dataSources = dataSources; } /** * Configures the named database by means of a data source. * * @param database the named database. * @param dataSource the data source corresponding to the named data base. */ public static void configure(String database, DataSource dataSource) { configure(database, dataSource, false); } /** * Configures the named database by means of a data source. * * @param database the named database. * @param dataSource the data source corresponding to the named data base. * @param isOverride a flag defining configuration as override if a current * configuration for the named database already exists. */ public static void configure(String database, DataSource dataSource, boolean isOverride) { if (!isOverride && isConfigured(database)) { throw new IllegalStateException("Named database '" + database + "' already configured!"); } instance.dataSources.put(database, dataSource); } /** * Determines whether a named database has been configured or not. * * @param database the named database. * @return true if the named database has been configured, false otherwise. */ public static boolean isConfigured(String database) { return instance.dataSources.containsKey(database); } /** * Ensures that a named database is configured by throwing an illegal state * exception if it is not. * * @param database the named database. * @throws IllegalStateException when the named database has not been * configured. */ public static void ensureConfigured(String database) { if (!isConfigured(database)) { throw new IllegalStateException("Named database '" + database + "' has no configured data source!"); } } /** * Opens a thread local transaction for the given database name. If an open * transaction already exists, it is reused. This method is idempotent when * called from the same thread. * * @param database the name of the database. * @return the open transaction. */ public static Transaction open(String database) { ensureConfigured(); database = context(database).effectiveName(); HashMap<String, Transaction> transactions = instance.getTransactions(); Transaction transaction = transactions.get(database); if (transaction == null) { DataSource dataSource = instance.getDataSource(database); if (dataSource == null) { ensureConfigured(database); // throws! } transaction = new Transaction(dataSource, database); transactions.put(database, transaction); } return transaction; } /** * Commits the thread local transaction for the given database name if it * has been opened. * * @param database the name of the database. * @return the closed transaction or null for no active transaction. * @throws SQLException if a database access error occur */ public static Transaction commit(String database) throws SQLException { database = context(database).effectiveName(); HashMap<String, Transaction> transactions = instance.getTransactions(); Transaction transaction = transactions.get(database); if (transaction != null) { transaction.commit(); } else { ensureConfigured(database); } return transaction; } /** * Closes the thread local transaction for the given database name if it has * been opened. This method is idempotent when called from the same thread. * * @param database the name of the database. * @return the closed transaction or null for no active transaction. */ public static Transaction close(String database) { database = context(database).effectiveName(); HashMap<String, Transaction> transactions = instance.getTransactions(); Transaction transaction = transactions.get(database); if (transaction != null) { transaction.close(); } else { ensureConfigured(database); } return transaction; } /** * Closes and destroys all transactions for the current thread. */ public static void close() { HashMap<String, Transaction> map = instance.getTransactions(); for (Transaction transaction : map.values()) { transaction.destroy(); } map.clear(); instance.transactions.remove(); } public static void destroy() { if (configurations != null) { for (Configuration configuration : configurations.values()) { configuration.destroy(); } } } private static Map<String, Configuration> configurations; private static void ensureConfigured() { if (configured) { return; } synchronized (Database.class) { if (configured) { return; } configure(); configured = true; } } /* * jorm.properties * --------------- * database.moria.dataSource=org.apache.tomcat.jdbc.pool.DataSource * database.moria.dataSource.driverClassName=org.postgresql.Driver * database.moria.dataSource.url=jdbc:postgresql://localhost:5432/moria * database.moria.dataSource.username=gandalf * database.moria.dataSource.password=mellon * * database.lothlorien.dataSource=org.apache.tomcat.jdbc.pool.DataSource * database.lothlorien.dataSource.driverClassName=org.postgresql.Driver * database.lothlorien.dataSource.url=jdbc:postgresql://localhost:5432/lothlorien * database.lothlorien.dataSource.username=galadriel * database.lothlorien.dataSource.password=nenya * * database.moria@development.dataSource.url=jdbc:postgresql://sjhdb05b.jajja.local:5432/moria_development * database.moria@development.dataSource.username=dev * database.moria@development.dataSource.password=$43CR37 * * database.moria@production.dataSource.url=jdbc:postgresql://sjhdb05b.jajja.local:5432/moria_production * database.moria@production.dataSource.username=prod * database.moria@production.dataSource.password=$43CR37:P455 * * database.context= * database.moria.context=production */ private static void configure() { try { configurations = new HashMap<String, Configuration>(); Enumeration<URL> urls = Thread.currentThread().getContextClassLoader().getResources("jorm.properties"); URL local = null; while (urls.hasMoreElements()) { URL url = urls.nextElement(); if (url.getProtocol().equals("jar")) { configure(url); } else { local = url; } } if (local != null) { configure(local); } for (Entry<String, Configuration> entry : configurations.entrySet()) { String database = entry.getKey(); Configuration configuration = entry.getValue(); int index = database.indexOf(Context.CONTEXT_SEPARATOR); if (index > 0) { Configuration base = configurations.get(database.substring(0, index)); if (base != null) { configuration.inherit(base); } } configuration.init(); configure(database, configuration.dataSource); Database.get().log.debug("Configured " + configuration); } } catch (IOException ex) { Database.get().log.warn("Failed to find resource 'jorm.properties': " + ex.getMessage(), ex); configurations = null; } } private static void configure(URL url) { Database.get().log.debug("Found jorm configuration @ " + url.toString()); Properties properties = new Properties(); try { InputStream is = url.openStream(); properties.load(is); is.close(); } catch (IOException ex) { Database.get().log.error("Failed to open jorm.properties: " + ex.getMessage(), ex); return; } for (Entry<Object, Object> property : properties.entrySet()) { String[] parts = ((String) property.getKey()).split("\\."); boolean isMalformed = false; if (parts[0].equals("database") && parts.length > 1) { String database = parts[1]; Configuration configuration = configurations.get(database); if (configuration == null) { configuration = new Configuration(database); configurations.put(database, configuration); } String value = (String) property.getValue(); switch (parts.length) { case 2: if (parts[1].equals("context")) { Database.get().globalDefaultContext = value; } else { isMalformed = true; } break; case 3: if (parts[2].equals("destroyMethod")) { configuration.destroyMethodName = value; } else if (parts[2].equals("dataSource")) { configuration.dataSourceClassName = value; } else if (parts[2].equals("defaultContext")) { if (database.indexOf(Context.CONTEXT_SEPARATOR) != -1) { isMalformed = true; } else { Database.get().defaultContext.put(database, value); } } else { isMalformed = true; } break; case 4: if (parts[2].equals("dataSource")) { configuration.dataSourceProperties.put(parts[3], value); } else { isMalformed = true; } break; default: isMalformed = true; } } else { isMalformed = true; } if (isMalformed) { Database.get().log.warn(String.format("Malformed jorm property: %s", property.toString())); } } } public static class Configuration { private String database; private String dataSourceClassName; private String destroyMethodName; private Map<String, String> dataSourceProperties = new HashMap<String, String>(); private DataSource dataSource; private Method destroyMethod; public void inherit(Configuration base) { if (dataSourceClassName == null) { dataSourceClassName = base.dataSourceClassName; } if (destroyMethodName == null) { destroyMethodName = base.destroyMethodName; } for (String key : base.dataSourceProperties.keySet()) { if (!dataSourceProperties.containsKey(key)) { dataSourceProperties.put(key, base.dataSourceProperties.get(key)); } } } public Configuration(String database) { this.database = database; } private void init() { try { Class<?> type = Class.forName(dataSourceClassName); if (destroyMethodName != null) { try { destroyMethod = type.getMethod(destroyMethodName); } catch (NoSuchMethodException e) { throw new IllegalArgumentException("The destroy method does not exist!", e); } catch (SecurityException e) { throw new IllegalArgumentException("The destroy method is not accessible!", e); } } dataSource = (DataSource) type.newInstance(); for (Method method : dataSource.getClass().getMethods()) { String methodName = method.getName(); Class<?>[] parameterTypes = method.getParameterTypes(); if (methodName.startsWith("set") && methodName.length() > 3 && parameterTypes.length == 1) { String name = method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4); // setValue -> value String property = dataSourceProperties.get(name); if (property != null) { boolean isAccessible = method.isAccessible(); method.setAccessible(true); try { method.invoke(dataSource, parse(method.getParameterTypes()[0], property)); } catch (Exception e) { get().log.warn( "Failed to invoke " + dataSource.getClass().getName() + "#" + method.getName() + "() in configuration of '" + database + "'", e); } finally { method.setAccessible(isAccessible); } } } } } catch (InstantiationException e) { throw new IllegalArgumentException( "The data source implementation " + dataSourceClassName + " has no default constructor!", e); } catch (IllegalAccessException e) { throw new IllegalArgumentException( "The data source implementation " + dataSourceClassName + " has no public constructor!", e); } catch (ClassNotFoundException e) { throw new IllegalArgumentException( "The data source implementation " + dataSourceClassName + " does not exist!", e); } catch (ClassCastException e) { throw new IllegalArgumentException( "The data source implementation " + dataSourceClassName + " is not a data source!", e); } } public void destroy() { if (destroyMethod != null) { try { destroyMethod.invoke(dataSource); } catch (Exception e) { get().log.error("Failed to invoke destroy method for " + dataSource.getClass(), e); } } } @SuppressWarnings("unchecked") private static <T extends Object> T parse(Class<T> type, String property) { Object object; if (type.isAssignableFrom(String.class)) { object = property; } else if (type.isAssignableFrom(boolean.class) || type.isAssignableFrom(Boolean.class)) { object = Boolean.parseBoolean(property); } else if (type.isAssignableFrom(int.class) || type.isAssignableFrom(Integer.class)) { object = Integer.parseInt(property); } else if (type.isAssignableFrom(long.class) || type.isAssignableFrom(Long.class)) { object = Long.parseLong(property); } else { object = null; } return (T) object; } @Override public String toString() { return "{ database => " + database + ", dataSourceClassName => " + dataSourceClassName + ", dataSourceProperties => " + dataSourceProperties + " }"; } } public static class Context implements Closeable { public static final char CONTEXT_SEPARATOR = '@'; private Context prev; private Context next; private String database; private String name; private boolean isClosed = false; // Root context private Context(String database) { this.database = database; } private Context(Context previous, String name) { this.database = previous.database; this.name = name; this.prev = previous; previous.next = this; } public String database() { return database; } public String name() { return name; } public String effectiveName() { String effectiveName = database; String name = this.name; if (name == null) { name = defaultContext(database); } if (name == null) { name = globalDefaultContext(); } if (name == null) { name = ""; } if (!name.isEmpty()) { effectiveName += CONTEXT_SEPARATOR + name; } return effectiveName; } @Override public void close() { if (prev == null) { throw new IllegalStateException("Attempt to close root context"); } if (next != null || isClosed) { throw new IllegalStateException("Context closed in wrong order"); } contextStack(database).put(database, prev); prev.next = null; prev = null; isClosed = true; } @Override public String toString() { return String.format("%s { database=%s, name=%s, prev=%s, next=%s }", getClass(), database, name, prev != null ? "yes" : "no", next != null ? "yes" : "no"); } } // Get default global context (not thread safe) public static String globalDefaultContext() { return get().globalDefaultContext; } // Set default global context (not thread safe) public static String globalDefaultContext(String name) { if (name == null) { name = ""; } String previous = globalDefaultContext(); get().globalDefaultContext = name; return previous; } // Get default per database context (not thread safe) public static String defaultContext(String database) { return get().defaultContext.get(database); } // Set default per database context (not thread safe) public static String defaultContext(String database, String name) { return get().defaultContext.put(database, name); } private static HashMap<String, Context> contextStack(String database) { ensureConfigured(database); HashMap<String, Context> map = get().contextStack.get(); if (map == null) { map = new HashMap<String, Context>(); get().contextStack.set(map); } return map; } // Get active thread-local context public static Context context(String database) { HashMap<String, Context> map = contextStack(database); Context activeContext = map.get(database); if (activeContext == null) { activeContext = new Context(database); map.put(database, activeContext); } return activeContext; } // Push active thread-local context public static Context context(String database, String name) { HashMap<String, Context> map = contextStack(database); Context newContext = new Context(context(database), name); map.put(database, newContext); return newContext; } }