/*
* 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.SQLException;
import java.sql.SQLDataException;
import java.sql.PreparedStatement;
import java.util.List;
import java.util.Locale;
import java.util.Calendar;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import java.util.concurrent.TimeUnit;
import org.apache.sis.util.Localized;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.logging.PerformanceLevel;
import org.apache.sis.util.resources.IndexedResourceBundle;
import org.geotoolkit.resources.Vocabulary;
import org.geotoolkit.resources.Errors;
import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
/**
* Base class for a table in a coverage {@linkplain Database database}. This class is not
* thread-safe. For usage in multi-threads environment, the copy constructor shall be used
* for creating a different instance of the {@code Table} subclass for each thread.
*
* {@section Synchronization}
* In principle, {@code Table} implementations don't need to be synchronized since they are
* not aimed to be used concurrently by different threads. If concurrent usage is desired,
* then each thread shall have its own instance. Nevertheless a few synchronization are still
* needed:
*
* <ul>
* <li><p>JDBC commands shall be executed inside a {@code synchronized(getLocalCache())} block.
* This is necessary in order to avoid the disposer background thread to close the
* JDBC statement while it still in use.</p></li>
* </ul>
*
* @author Martin Desruisseaux (IRD, Geomatys)
* @version 3.16
*
* @since 3.09 (derived from Seagis)
* @module
*/
public abstract class Table implements Localized {
/**
* The query executed by this table.
*
* @see #getStatement(QueryType)
*/
protected final Query query;
/**
* The query type for the current {@linkplain #statement}.
*/
private QueryType type;
/**
* Incremented every time the {@link Table} configuration changed. This is compared
* with {@link LocalCache.Stmt#stamp} in order to determine if the statement needs
* to be set.
*/
private int modificationCount;
/**
* {@cdoe true} if this table is available for reuse.
*
* @see #release()
*/
transient volatile boolean canReuse;
/**
* Creates a new table using the specified query. The query given in argument should be some
* subclass with {@code addFooColumn(...)} and {@code addParameter(...)} methods invoked in
* its constructor.
*
* @param query The query to use for this table.
*/
protected Table(final Query query) {
ensureNonNull("query", query);
this.query = query;
}
/**
* Creates a new table connected to the same {@linkplain #getDatabase database} and using
* the same {@linkplain #query} than the specified table. Subclass constructors should
* copy <strong>only the final fields</strong>, not the fields that may be modified by
* setter methods. This is because mutable fields may not be correctly published in a
* multi-thread context. In addition, subclass constructors shall not modify any shared
* object like {@link Query}.
*
* @param table The table to use as a template.
*/
protected Table(final Table table) {
query = table.query;
}
/**
* Returns a copy of this table. Subclasses should invoke their copy-constructor here.
*/
@Override
protected abstract Table clone();
/**
* Returns the database that contains this table. This is the database
* specified at construction time.
*
* @return The database (never {@code null}).
* @throws IllegalStateException If this table is not connected to a database.
*/
public final Database getDatabase() throws IllegalStateException {
final Database database = query.database;
if (database == null) {
throw new IllegalStateException(errors().getString(Errors.Keys.NoDataSource));
}
return database;
}
/**
* Returns a property for the given key. This method tries to get the property
* from the {@linkplain #getDatabase() database} if available, or return the
* {@linkplain ConfigurationKey#defaultValue default value} otherwise.
*
* @param key The property key, usually one of {@link Database} constants.
* @return The property value, or {@code null} if none.
*/
protected final String getProperty(final ConfigurationKey key) {
final Database database = query.database;
return (database != null) ? database.getProperty(key) : key.defaultValue;
}
/**
* Returns the lock to use for any call to a {@code getStatement(...)} method. This lock is
* only a lock for preventing the connection to be closed before the query is finished. It
* is <strong>not</strong> for providing thread-safety to {@code Table} instances - since the
* lock is thread-local, it is useless for that purpose.
*
* @return The lock to use in a {@code synchronized} statement.
*/
protected final LocalCache getLocalCache() {
return getDatabase().getLocalCache();
}
/**
* Returns {@code true} if the prepared statement to be created by
* {@link #getStatement(String)} should be able to return auto-generated keys.
* <p>
* The default implementation returns {@code false} in every cases. Tables with primary keys
* (like {@link SingletonTable}) may return a different value during {@code INSERT} statements
* if they don't supply the primary key values themselves.
*/
boolean wantsAutoGeneratedKeys() {
return false;
}
/**
* Returns a prepared statement for the given SQL query. If the specified {@code query} is the
* same one than last time this method has been invoked, then this method returns the same
* {@link PreparedStatement} instance (if it still available).
* <p>
* If a new statement is created, or if the table has {@linkplain #fireStateChanged
* changed its state} since the last call, then this method invokes {@link #configure}.
* <p>
* This method must be invoked in a synchronized block as documented in
* {@link #getStatement(LocalCache, QueryType)}. This is verified only if
* assertions are enabled.
*
* @param query The SQL query to prepare.
* @return The prepared statement.
* @throws SQLException if a SQL error occurred while configuring the statement.
*/
private LocalCache.Stmt getStatement(final LocalCache cache, final String query) throws SQLException {
assert Thread.holdsLock(cache);
final LocalCache.Stmt ce = cache.prepareStatement(this, query);
ce.startTime = System.nanoTime();
if (modificationCount != ce.stamp) {
final QueryType type = this.type;
configure(cache, type, ce.statement);
ce.stamp = modificationCount;
}
return ce;
}
/**
* Returns a prepared statement for the given query type.
* <p>
* This method must be invoked in a synchronized block as below. Note that the lock
* will not prevent concurrent usage of this table. This is just a lock for preventing
* the JDBC resources to be reclaimed while they are still in use.
*
* {@preformat java
* final LocalCache lc = getLocalCache();
* synchronized(lc) {
* final LocalCache.Stmt ce = getStatement(lc, type);
* final PreparedStatement statement = ce.statement;
* final ResultSet results = statement.executeQuery();
* while (results.next()) {
* // ...
* }
* results.close();
* release(lc, ce);
* }
* }
*
* @param lc The value returned by {@link #getLocalCache()}.
* @param type The query type.
* @return The prepared statement.
* @throws SQLException if a SQL error occurred while configuring the statement.
*/
protected final LocalCache.Stmt getStatement(final LocalCache lc, final QueryType type)
throws SQLException
{
final String sql;
switch (type) {
default: sql = query.select(lc, type); break;
case INSERT: sql = query.insert(lc, type); break;
case DELETE: sql = query.delete(lc, type); break;
case DELETE_ALL: sql = query.delete(lc, type); break;
}
this.type = type;
return getStatement(lc, sql);
}
/**
* Same as {@link #getStatement(LocalCache, QueryType)}, but with a column parameter which
* affect the SQL statement to be created. This apply only to queries performing aggregation,
* like {@link QueryType#COUNT}.
*
* @param lc The value returned by {@link #getLocalCache()}.
* @param type The query type.
* @param column The column on which to perform aggregation.
* @return The prepared statement.
* @throws SQLException if a SQL error occurred while configuring the statement.
*/
protected final LocalCache.Stmt getStatement(final LocalCache lc, final QueryType type, final Column column)
throws SQLException
{
final String sql;
switch (type) {
case COUNT: sql = query.count(lc, type, column); break;
default: throw new IllegalArgumentException(errors().getString(
Errors.Keys.IllegalArgument_2, "type", type));
}
this.type = type;
fireStateChanged(); // Because the column may be different on every call.
return getStatement(lc, sql);
}
/**
* Releases the given statement. This method shall be invoked after a
* {@link #getStatement(QueryType)} method call when the statement is
* not needed anymore.
*
* @param lc The value returned by {@link #getLocalCache()}.
* @param statement The statement to release.
* @throws SQLException If an error occurred while releasing the statement.
*/
protected final void release(final LocalCache lc, final LocalCache.Stmt statement) throws SQLException {
assert Thread.holdsLock(lc);
Database.release(lc, statement);
final Logger logger = getLogger();
final long duration = System.nanoTime() - statement.startTime;
final Level level = PerformanceLevel.forDuration(duration, TimeUnit.NANOSECONDS);
if (logger.isLoggable(level)) {
final Locale locale = getLocale();
final LogRecord record = new LogRecord(level, String.format(locale, "(%s: %.4f s) %s",
Vocabulary.getResources(locale).getString(Vocabulary.Keys.Duration),
duration / 1E9, statement));
record.setSourceClassName(getClass().getName());
record.setSourceMethodName(type.method);
record.setLoggerName(logger.getName());
logger.log(record);
}
}
/**
* Invoked before an arbitrary amount of {@code INSERT}, {@code UPDATE} or {@code DELETE}
* SQL statements. This method <strong>must</strong> be invoked in a
* {@code try} ... {@code finally} block as below:
*
* {@preformat java
* final LocalCache lc = getLocalCache();
* synchronized(lc) {
* boolean success = false;
* transactionBegin(lc);
* try {
* // Do some operation here...
* success = true; // Must be the very last line in the try block.
* } finally {
* transactionEnd(lc, success);
* }
* }
* }
*
* @param lc The {@link #getLocalCache()} value.
* @throws SQLException if the operation failed.
*/
protected final void transactionBegin(final LocalCache lc) throws SQLException {
getDatabase().transactionBegin(lc);
}
/**
* Invoked after the {@code INSERT}, {@code UPDATE} or {@code DELETE}
* SQL statements finished.
*
* @param lc The {@link #getLocalCache()} value.
* @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.
*/
protected final void transactionEnd(final LocalCache lc, final boolean success) throws SQLException {
getDatabase().transactionEnd(lc, success);
}
/**
* Invoked automatically by {@link #getStatement(String)} for a newly created statement, or
* for an existing statement when this table {@linkplain #fireStateChanged changed its state}.
* Subclasses should override this method if they need to set SQL parameters according the
* table state. The default implementation does nothing.
*
* @param lc The {@link #getLocalCache()} value.
* @param type The query type.
* @param statement The statement to configure (never {@code null}).
* @throws SQLException if a SQL error occurred while configuring the statement.
*/
protected void configure(LocalCache lc, QueryType type, PreparedStatement statement) throws SQLException {
}
/**
* Returns the column at the specified index, or {@code null} if none.
*
* @param index The column index (number starts at 1).
* @return The column, or {@code null} if none.
*/
final Column getColumn(final int index) {
if (index >= 1) {
final List<Column> columns = query.getColumns(getQueryType());
if (columns != null && index <= columns.size()) {
return columns.get(index - 1);
}
}
return null;
}
/**
* Returns the current query type. This is {@code null} if {@link #getStatement(QueryType)}
* has never been invoked. But once at least one query has been initiated, it should never
* be null.
*/
final QueryType getQueryType() {
return type;
}
/**
* Delegates to <code>column.{@linkplain Column#indexOf(QueryType) indexOf}(type)</code>,
* except that an exception is thrown if the specified column is not applicable to the
* current query type. The {@code type} value is the argument given to the last call to
* {@link #getStatement(QueryType)}.
*
* @param column The column.
* @return The column index (starting with 1).
* @throws SQLException if the specified column is not applicable.
*/
protected final int indexOf(final Column column) throws SQLException {
final QueryType type = getQueryType();
final int index = column.indexOf(type);
if (index > 0) {
return index;
}
final IndexedResourceBundle errors = errors();
throw new SQLDataException(
errors.getString(Errors.Keys.UnsupportedOperation_1, type) + ". " +
errors.getString(Errors.Keys.CantReadDatabaseTable_2, column.table, column.name));
}
/**
* Delegates to <code>parameter.{@linkplain Parameter#indexOf indexOf}(type)</code>,
* except that an exception is thrown if the specified parameter is not applicable to the
* current query type. The {@code type} value is the argument given to the last call to
* {@link #getStatement(QueryType)}.
*
* @param parameter The parameter.
* @return The parameter index (starting with 1).
* @throws SQLException if the specified parameter is not applicable.
*/
protected final int indexOf(final Parameter parameter) throws SQLException {
final QueryType type = getQueryType();
final int index = parameter.indexOf(type);
if (index > 0) {
return index;
}
throw new SQLDataException(errors().getString(Errors.Keys.UnsupportedOperation_1, type));
}
/**
* Returns a calendar using the {@linkplain Database#getTimeZone() database time zone}.
* This calendar should be used for fetching dates from the database as in the example
* below:
*
* {@preformat java
* Calendar calendar = getCalendar();
* Timestamp startTime = resultSet.getTimestamp(1, calendar);
* Timestamp endTime = resultSet.getTimestamp(2, calendar);
* }
*
* This calendar should be used for storing dates as well.
*
* @param lc The value returned by {@link #getLocalCache()}.
* @return The calendar for date calculation in this table.
*/
protected final Calendar getCalendar(final LocalCache lc) {
final Database database = getDatabase();
final Calendar calendar = database.getCalendar(lc);
assert calendar.getTimeZone().equals(database.getTimeZone());
return calendar;
}
/**
* Notifies that the state of this table changed. Subclasses should invoke this method every
* time some {@code setXXX(...)} method has been invoked on this {@code Table} object.
*
* @param property The name of the property that changed, or {@code null} if unknown.
*/
protected void fireStateChanged(final String property) {
fireStateChanged();
}
/**
* Implementation of {@link #fireStateChanged(String)} invoked directly when the state that
* changed is not a property stored by this {@code Table} instance.
*/
private void fireStateChanged() {
final Database database = query.database;
do {
if (database != null) {
modificationCount = database.modificationCount.incrementAndGet();
} else {
modificationCount++;
}
} while (modificationCount == LocalCache.Stmt.UNINITIALIZED); // Paranoiac check.
}
/**
* Notifies that a recoverable error occurred.
*
* @param method The method name in which the error occurred.
* @param exception The error.
*/
final void unexpectedException(final String method, final Throwable exception) {
Logging.unexpectedException(getLogger(), getClass(), method, exception);
}
/**
* Returns the resources to use for formatting error messages.
*
* @return The {@link Errors} resource bundle.
*/
protected final IndexedResourceBundle errors() {
return Errors.getResources(getLocale());
}
/**
* Sets the {@linkplain LogRecord#setLoggerName logger name},
* {@linkplain LogRecord#setSourceClassName source class name} and
* {@linkplain LogRecord#setSourceMethodName source method name} in the given record,
* and {@linkplain Logger#log(LogRecord) logs} it.
*
* @param methodName The name of the caller method.
* @param record The record to log.
*/
protected final void log(final String methodName, final LogRecord record) {
final Logger logger = getLogger();
record.setLoggerName(logger.getName());
record.setSourceClassName(getClass().getName());
record.setSourceMethodName(methodName);
logger.log(record);
}
/**
* Returns the logger to use. The default implementation looks for the package name
* of the implementing class.
*
* @return The logger to use (never {@code null}).
*
* @since 3.16
*/
public final Logger getLogger() {
final Database database = query.database;
if (database != null) {
final Logger logger = database.getLogger();
if (logger != null) {
return logger;
}
}
return Logging.getLogger("org.geotoolkit.sql");
}
/**
* 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() {
final Database database = query.database;
return (database != null) ? database.getLocale() : null;
}
/**
* Marks this table as available for reuse. This method can be invoked after a call
* to {@link Database#getTable(Class)} when the caller is sure that he will not use
* this table anymore. Example:
*
* {@preformat java
* Table table = Database.getTable(...);
* // use the table
* table.release();
* }
*
* In case of doubt, do not invoke this method.
*/
public final void release() {
canReuse = true;
}
}