/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2005-2012, Open Source Geospatial Foundation (OSGeo) * (C) 2007-2012, Geomatys * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License. * * 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 * Lesser General Public License for more details. */ package org.geotoolkit.internal.sql.table; import java.sql.Statement; import java.sql.Connection; import java.sql.SQLException; import java.sql.SQLTimeoutException; import java.sql.SQLNonTransientException; import javax.sql.DataSource; import java.lang.reflect.Constructor; import java.util.Map; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Iterator; import java.util.Locale; import java.util.TimeZone; import java.util.Properties; import java.util.Calendar; import java.util.ConcurrentModificationException; import java.util.GregorianCalendar; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.atomic.AtomicInteger; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.LogRecord; import org.opengis.parameter.ParameterValueGroup; import org.geotoolkit.factory.Hints; import org.apache.sis.util.Localized; import org.apache.sis.util.Classes; import org.geotoolkit.resources.Errors; import org.geotoolkit.resources.Loggings; import org.geotoolkit.resources.Vocabulary; import org.geotoolkit.internal.sql.StatementPool; import org.geotoolkit.internal.sql.DefaultDataSource; import org.geotoolkit.internal.sql.AuthenticatedDataSource; import static org.apache.sis.util.ArgumentChecks.ensureNonNull; /** * Connection to a catalog database through JDBC (<cite>Java Database Connectivity</cite>). * The connection is specified by a {@link DataSource}, which should create pooled connections. * * {@section Concurrency} * This class is thread-safe and concurrent. However it is recommended to access it only from a * limited number of threads (for example from a {@link java.util.concurrent.ThreadPoolExecutor}) * and to recycle those threads, because this class may use a new connection for each thread. * * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.18 * * @since 3.09 (derived from Seagis) * @module */ public class Database implements Localized { /** * The timeout (in minutes) for acquiring a write lock. */ private static final int TIMEOUT = 2; /** * The data source, which is mandatory. It is recommended to provide a data source that * create pooled connections, because connections may be created and closed often. */ private final AuthenticatedDataSource source; /** * The database catalog where the tables are defined, or {@code null} if none. */ final String catalog; /** * The database schema where the tables are defined, or {@code null} if none. * If {@code null}, then the tables will be located using the default mechanism * on the underlying database. On PostgreSQL, the search order is determined by * the {@code "search_path"} database variable. */ final String schema; /** * The timezone to use for reading and writing dates in the database. This timezone can be * set by the {@link ConfigurationKey#TIMEZONE} property and will be used by the calendar * returned by {@link Table#getCalendar()}. */ private final TimeZone timezone; /** * The locale to use for formatting messages, or {@code null} for the system-default. */ private volatile Locale locale; /** * The hints to use for fetching factories, or {@code null} for the default hints. * Shall be considered read-only. */ final Hints hints; /** * The {@code Session} instance used for each thread. Threads are identified by their * {@linkplain Thread#getId() ID}. New values are created by {@link #getLocalCache()} * if no existing values can be used. Values are removed by {@link Session#monitorExit} * after some delay (may be 2 seconds) of inactivity. * * {@note In a previous version, we used a single <code>ThreadLocal</code> instance. * We switched to a map because this allow us to reuse an available session * for a different thread, and because it allows us to know the set of all * active sessions.} */ private final Map<Long,Session> sessions = new LinkedHashMap<>(); /** * Holds thread-local information for SQL statements being executed. * Those information can not be shared between different threads. * <p> * This class opportunistically extends {@code StatementPool}, which duplicates the work of * connection pools provided in modern JDBC driver. But we use {@code StatementPool} anyway * because we want the {@code org.geotoolkit.coverage.sql} package to work reasonably well * in the absence of connection pool, and because the {@code StatementPool} timer provides * a convenient way to let the connections being closed automatically without worrying if * the connection is still in use. The alternative would be to maintain a {@code usageCount} * variable incremented when a {@link Table} uses the connection, and decremented when it is * done - but even this alternative would close the connection just before we get a new one, * for example when creating a {@code LayerEntry} and immediately invoking one of its method * which deferred the database query. * * @author Martin Desruisseaux (Geomatys) * @version 3.12 * * @since 3.09 * @module */ @SuppressWarnings("serial") private static final class Session extends StatementPool<String, LocalCache.Stmt> implements LocalCache { /** * The {@linkplain Thread#getId() ID} of the thread which is using this {@code Session}, * or {@code null} if this {@code Session} is available for reuse by any thread. * <p> * This field can be read or written only in a {@code synchronized(session)} block. */ private Long threadID; /** * The name of the thread that created the connection to the database. * This is used for logging purpose only. */ private String threadName; /** * The number of statement creates, for logging purpose only. */ private int numQueries; /** * A copy of the {@link Database#sessions} references, used for synchronization purpose. * We don't take a reference to the enclosing {@link Database} because we don't need it, * so we give more chances to GC to collect it. */ private final Map<Long,Session> sessions; /** * The calendar to use for reading and writing dates in a database. This calendar * is created when first needed and shall use the {@link Database#timezone}. * * @see Database#getCalendar() */ Calendar calendar; /** * Generators of named identifiers. Will be created only when first needed. The keys are * the column names for which ID are generated. Note that the same instance can be used * for different tables if the column name is the same. * * @since 3.11 */ Map<String,NameGenerator> generators; /** * Creates a new instance for the given data source. * We will cache a maximum of 8 prepared statements. */ Session(final DataSource source, final Map<Long,Session> sessions) { super(8, source); this.sessions = sessions; } /** * Returns a prepared statement for the given SQL query. * See {@link LocalCache} javadoc for usage example. */ @Override public Stmt prepareStatement(final Table table, final String sql) throws SQLException { Stmt value = remove(sql); if (value == null) { final Connection connection = connection(); value = new Stmt(connection.prepareStatement(sql, table.wantsAutoGeneratedKeys() ? Statement.RETURN_GENERATED_KEYS : Statement.NO_GENERATED_KEYS), sql); if (numQueries == 0) { threadName = Thread.currentThread().getName(); final Logger logger = table.getLogger(); if (logger.isLoggable(Level.FINE)) { final Locale locale = table.getLocale(); String url = connection.getMetaData().getURL(); if (url == null) { url = Vocabulary.getResources(locale).getString(Vocabulary.Keys.Unknown); } // "getStatement" is name of the Table method which invoke this method. table.log("getStatement", Loggings.getResources(locale).getLogRecord( Level.FINE, Loggings.Keys.ConnectedDatabaseForThread_2, threadName, url)); } } } numQueries++; return value; } /** * Invoked in a background thread when the user thread exits its outer {@code synchronized} * statement. This method declares that this object is available for reuse. If all JDBC * resources have been closed, we will let the garbage collector collects this object. */ @Override protected void monitorExit(final boolean closed) { super.monitorExit(closed); synchronized (sessions) { if (closed) { final Session old = sessions.remove(threadID); assert old == null || old == this : old; final Logger logger = DefaultDataSource.LOGGER; if (logger.isLoggable(Level.FINE)) { final LogRecord record = Loggings.format(Level.FINE, Loggings.Keys.ClosedDatabaseForThread_2, threadName, numQueries); record.setLoggerName(logger.getName()); record.setSourceClassName(StatementPool.class.getName()); record.setSourceMethodName("run"); logger.log(record); } } threadID = null; // Declare this Session as available for reuse. } } /** * Returns a string representation for debugging purpose. This string appears in the * {@code assert} statements of the enclosing class, especially when checking if the * thread holds the expected monitor. * <p> * This method creates a list of all {@code Session} instances (not just this instance) * managed by the enclosing class. This instance is flagged by a "{@code .this}" prefix * in front of the line. */ @Override public String toString() { final long currentID = Thread.currentThread().getId(); final StringBuilder buffer = new StringBuilder("Sessions: (this.threadID=") .append(currentID).append(')'); synchronized (sessions) { for (final Map.Entry<Long,Session> entry : sessions.entrySet()) { final Session session = entry.getValue(); final Long check = session.threadID; final Long id = entry.getKey(); buffer.append("\n ").append(session == this ? "this." : " ") .append("threadID=").append(id).append(' ') .append(check == null ? "available" : check.equals(id) ? "in use" : "ERROR"); if (id == currentID) { buffer.append(" (current thread)"); } if (Thread.holdsLock(session)) { buffer.append(" (holds lock)"); } } } return buffer.toString(); } } /** * Incremented every time a modification is applied in the configuration of a table. * This is for internal use by {@link Table#fireStateChanged(String)} only. */ final AtomicInteger modificationCount = new AtomicInteger(); /** * Lock for transactions performing write operations. The {@link Connection#commit} or * {@link Connection#rollback} method will be invoked when the lock count reach zero. * * @see #transactionBegin() * @see #transactionEnd(boolean) */ private final ReentrantLock transactionLock = new ReentrantLock(true); /** * Last table created for each category. Every time the {@link #getTable(Class)} method * is invoked, the last returned table is stored in this map so it can be used as a * template for the next table to create. * * {@section Synchronization note} * Every access to this map shall be synchronized on {@code tables}. */ private final Map<Class<? extends Table>, Table> tables = new HashMap<>(); /** * The properties given at construction time, or {@code null} if none. * Can be either an instance of {@link Properties} or {@link ParameterValueGroup}. */ private final Object properties; /** * Creates a new instance using the same configuration than the given instance. * The new instance will have its own, initially empty, cache. * * @param toCopy The existing instance to copy. */ public Database(final Database toCopy) { this.source = toCopy.source; this.catalog = toCopy.catalog; this.schema = toCopy.schema; this.timezone = toCopy.timezone; this.hints = toCopy.hints; this.properties = toCopy.properties; } /** * Creates a new instance using the provided data source and configuration properties. * If a properties map is specified, then the keys enumerated in {@link ConfigurationKey} * will be used. * <p> * If the given properties contains only one entry, and the key for this entry is * {@value org.geotoolkit.internal.sql.table.ConfigurationKey#PARAMETERS}, then the * value will be used as {@link ParameterValueGroup}. * * @param datasource The data source, or {@code null} for creating it from the URL. * @param properties The configuration properties, or {@code null} if none. */ public Database(DataSource datasource, final Properties properties) { Object parameters = properties; if (properties != null && properties.size() == 1) { parameters = properties.get(ConfigurationKey.PARAMETERS); } this.properties = parameters; // Must be set before to ask for properties. if (datasource == null) { datasource = new DefaultDataSource(getProperty(ConfigurationKey.URL)); } ensureNonNull("datasource", datasource); final String username, password, tz; username = getProperty(ConfigurationKey.USER); password = getProperty(ConfigurationKey.PASSWORD); tz = getProperty(ConfigurationKey.TIMEZONE); timezone = !tz.equalsIgnoreCase("local") ? TimeZone.getTimeZone(tz) : TimeZone.getDefault(); catalog = getProperty(ConfigurationKey.CATALOG); schema = getProperty(ConfigurationKey.SCHEMA); source = new AuthenticatedDataSource(datasource, username, password, Boolean.TRUE); hints = null; // May be configurable in a future version. } /** * Returns a property. The key is usually a constant like {@link ConfigurationKey#TIMEZONE}. * * @param key The key for the property to fetch. * @return The property value, or {@code null} if none and there is no default value. * * @see Table#getProperty(ConfigurationKey) */ public final String getProperty(final ConfigurationKey key) { String value = null; if (properties instanceof Properties) { // No need to synchronize since 'Properties' is already synchronized. value = ((Properties) properties).getProperty(key.key); } else if (properties instanceof ParameterValueGroup) { final Object obj = ((ParameterValueGroup) properties).parameter(key.key).getValue(); if (obj != null) { value = obj.toString(); } } if (value == null || (value = value.trim()).isEmpty()) { value = key.defaultValue; } return value; } /** * Returns the logger to use, or {@code null} for a default (implementation-specific) logger. * Subclasses shall override this method is order to specify their application-specific logger. * * @return The logger to use, or {@code null}. * * @since 3.16 */ protected Logger getLogger() { return null; } /** * Returns the locale to use for formatting messages. This is used mostly for error messages * in exceptions, and for warnings during {@linkplain javax.imageio.ImageRead image read} * operations. * * @return The locale for message formatting, or {@code null} for the * {@linkplain Locale#getDefault() system default}. */ @Override public final Locale getLocale() { return locale; } /** * Sets the locale to use for formatting messages. * * @param locale The new locale for message formatting, or {@code null} for the * {@linkplain Locale#getDefault() system default}. */ public final void setLocale(final Locale locale) { this.locale = locale; } /** * Returns the timezone in which the dates in the database are expressed. This information * can be specified through the {@link ConfigurationKey#TIMEZONE} property. It is used in * order to convert the dates from the database timezone to UTC. * * @return The time zone for dates to appear in database records. * * @see Table#getCalendar() */ public final TimeZone getTimeZone() { return (TimeZone) timezone.clone(); } /** * Creates and returns a new calendar using the database timezone. * * {@section Implementation note} * We use {@link Locale#CANADA} because it is close to the format typically used in * databases ({@code "yyyy/mm/dd"}). We don't use {@link Calendar#getInstance()} * because it may be a totally different and incompatible calendar for our purpose. */ final Calendar getCalendar(final LocalCache cache) { final Session s = (Session) cache; assert Thread.holdsLock(s) : s; Calendar calendar = s.calendar; if (calendar == null) { s.calendar = calendar = new GregorianCalendar(timezone, Locale.CANADA); } return calendar; } /** * Returns the data source which has been specified to the constructor. * * @param wrap {@code true} for returning the data source in a wrapper which provide the * username, password and set the connection to read-only mode, or {@code false} * for returning directly the data source given to the constructor. * @return The data source. */ public final DataSource getDataSource(final boolean wrap) { return wrap ? source : source.wrapped; } /** * Returns the {@link LocalCache} instance for the current thread. This is the interface to * use for getting the JDBC connection and prepared statements. See the {@link LocalCache} * javadoc for usage examples. * * @see Table#release() */ final Session getLocalCache() { Session session; final Long threadID = Thread.currentThread().getId(); synchronized (sessions) { /* * WARNING: Do not invoke any method which may synchronize * on 'session' inside this synchronized block. */ session = sessions.get(threadID); if (session != null) { if (session.threadID == null) { session.threadID = threadID; } else { assert threadID.equals(session.threadID) : session; } } else { /* * Search for an existing Session instance which is available for reuse. * If none is found, we will create a new one. */ for (final Iterator<Session> it=sessions.values().iterator(); it.hasNext();) { final Session candidate = it.next(); if (candidate.threadID == null) { session = candidate; it.remove(); break; } } if (session == null) { session = new Session(source, sessions); } if (sessions.put(threadID, session) != null) { throw new ConcurrentModificationException(); // Should never happen. } session.threadID = threadID; } } return session; } /** * Puts the given statement back in the pool. This method is invoked by * {@link LocalCache.Stmt#release()} only. The later is the API to use. * * @throws SQLException If an error occurred while closing the <strong>previous</strong> * statement, if any. It may happen only if the same {@link Table} is queried * recursively in the same thread. */ static void release(final LocalCache cache, final LocalCache.Stmt entry) throws SQLException { final LocalCache.Stmt old = ((Session) cache).put(entry.sql, entry); if (old != null) { old.statement.close(); } } /** * Returns a table of the specified type. * * @param <T> The table class. * @param type The table class. * @return An instance of a table of the specified type. * @throws NoSuchTableException if the specified type is unknown to this database. */ public final <T extends Table> T getTable(final Class<T> type) throws NoSuchTableException { Table table; synchronized (tables) { table = tables.get(type); if (table != null && table.canReuse) { table.canReuse = false; } else { if (table != null) { table = table.clone(); } else try { final Constructor<T> c = type.getConstructor(Database.class); c.setAccessible(true); table = c.newInstance(this); } catch (ReflectiveOperationException exception) { throw new NoSuchTableException(Classes.getShortName(type), exception); } tables.put(type, table); } } return type.cast(table); } /** * Invoked before an arbitrary amount of {@code INSERT}, {@code UPDATE} or {@code DELETE} * SQL statement. This method <strong>must</strong> be invoked in a {@code try} ... {@code * finally} block as below: * * {@preformat java * final LocalCache cache = getLocalCache(); * synchronized (cache) { * boolean success = false; * transactionBegin(cache); * try { * // Do some operation here... * success = true; * } finally { * transactionEnd(cache, success); * } * } * } * * @throws SQLException If the operation failed. */ final void transactionBegin(final LocalCache sp) throws SQLException { boolean success = false; final boolean locked; try { locked = transactionLock.tryLock(TIMEOUT, TimeUnit.MINUTES); } catch (InterruptedException e) { throw new SQLTimeoutException(e); } if (locked) try { if (transactionLock.getHoldCount() == 1) { assert Thread.holdsLock(sp) : sp; // Necessary for blocking the cleaner thread. final Connection connection = sp.connection(); connection.setReadOnly(false); connection.setAutoCommit(false); } success = true; } finally { if (!success) { transactionLock.unlock(); } } else { throw new SQLTimeoutException(Errors.getResources(getLocale()).getString(Errors.Keys.Timeout)); } } /** * Invoked after the {@code INSERT}, {@code UPDATE} or {@code DELETE} * SQL statement finished. * * @param success {@code true} if the operation succeed and should be committed, * or {@code false} if we should rollback. * @throws SQLException If the commit or the rollback failed. */ final void transactionEnd(final LocalCache sp, final boolean success) throws SQLException { ensureOngoingTransaction(); try { if (transactionLock.getHoldCount() == 1) { assert Thread.holdsLock(sp) : sp; // Necessary for blocking the cleaner thread. final Connection connection = sp.connection(); if (success) { connection.commit(); } else { connection.rollback(); } connection.setAutoCommit(true); connection.setReadOnly(true); } } finally { transactionLock.unlock(); } } /** * Ensures that the current thread is allowed to performs a transaction. */ final void ensureOngoingTransaction() throws SQLException { if (!transactionLock.isHeldByCurrentThread()) { throw new SQLNonTransientException(Errors.getResources(getLocale()) .getString(Errors.Keys.ThreadDoesntHoldLock)); } } /** * Returns a {@link NameGenerator} for the given column name. * * @param pk The name of the primary key column. * @return An identifier generator for the given column. * @throws SQLException If an error occurred while creating the generator. */ final NameGenerator getIdentifierGenerator(final LocalCache cache, final String pk) throws SQLException { final Session s = (Session) cache; Map<String, NameGenerator> generators = s.generators; NameGenerator generator; if (generators != null) { generator = generators.get(pk); if (generator != null) { return generator; } generator = new NameGenerator(generators.values().iterator().next(), pk); } else { synchronized (s) { generator = new NameGenerator(s, pk); } s.generators = generators = new HashMap<>(4); } if (generators.put(pk, generator) != null) { throw new AssertionError(pk); } return generator; } /** * Closes all connections and restores the attributes to their initial state. * * @throws SQLException If an error occurred while closing the connection. */ public void reset() throws SQLException { final Session[] s; synchronized (sessions) { s = sessions.values().toArray(new Session[sessions.size()]); sessions.clear(); } /* * The calls to Session.close() must be performed * outside the synchronized (sessions) block. */ for (final Session session : s) { session.close(); } locale = null; } }