/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2005-2008, Open Source Geospatial Foundation (OSGeo)
*
* 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.geotools.referencing.factory.epsg;
import java.util.Set;
import java.util.AbstractSet;
import java.util.AbstractMap;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.NoSuchElementException;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;
import java.io.Serializable;
import java.io.ObjectStreamException;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.opengis.referencing.operation.Projection;
import org.geotools.util.logging.Logging;
import org.geotools.resources.i18n.Loggings;
import org.geotools.resources.i18n.LoggingKeys;
/**
* A set of EPSG authority codes. This set requires a living connection to the EPSG database.
* All {@link #iterator} method call creates a new {@link ResultSet} holding the codes. However,
* call to {@link #contains} map directly to a SQL call.
* <p>
* Serialization of this class store a copy of all authority codes. The serialization
* do not preserve any connection to the database.
*
* @since 2.2
* @source $URL$
* @version $Id$
* @author Martin Desruisseaux (IRD)
*/
final class AuthorityCodes extends AbstractSet<String> implements Serializable {
/**
* For compatibility with different versions.
*/
private static final long serialVersionUID = 7105664579449680562L;
/**
* The factory which is the owner of this set. One purpose of this field (even if it were not
* used directly by this class) is to avoid garbage collection of the factory as long as this
* set is in use. This is required because {@link DirectEpsgFactory#finalize} closes the JDBC
* connections.
*/
private final DirectEpsgFactory factory;
/**
* The type for this code set. This is translated to the most appropriate
* interface type even if the user supplied an implementation type.
*/
public final Class<?> type;
/**
* {@code true} if {@link #type} is assignable to {@link Projection}.
*/
private final boolean isProjection;
/**
* A view of this set as a map with object's name as values, or {@code null} if none.
* Will be created only when first needed.
*/
private transient java.util.Map<String,String> asMap;
/**
* The SQL command to use for creating the {@code queryAll} statement.
* Used for iteration over all codes.
*/
final String sqlAll;
/**
* The SQL command to use for creating the {@code querySingle} statement.
* Used for fetching the description from a code.
*/
private final String sqlSingle;
/**
* The statement to use for querying all codes.
* Will be created only when first needed.
*/
private transient PreparedStatement queryAll;
/**
* The statement to use for querying a single code.
* Will be created only when first needed.
*/
private transient PreparedStatement querySingle;
/**
* The connection to the underlying database.
*/
private Connection connection;
/**
* The collection's size, or a negative value if not yet computed. The records will be counted
* only when first needed. The special value -2 if set by {@link #isEmpty} if the size has not
* yet been computed, but we know that the set is not empty.
*/
private int size = -1;
/**
* Creates a new set of authority codes for the specified type.
*
* @param connection The provider of connection to the EPSG database.
* @param table The table to query.
* @param type The type to query.
* @param factory The factory originator.
*/
public AuthorityCodes(final TableInfo table,
final Class<?> type, final DirectEpsgFactory factory)
{
this.factory = factory;
final StringBuilder buffer = new StringBuilder("SELECT ");
buffer.append(table.codeColumn);
if (table.nameColumn != null) {
buffer.append(", ").append(table.nameColumn);
}
buffer.append(" FROM ").append(table.table);
boolean hasWhere = false;
Class<?> tableType = table.type;
if (table.typeColumn != null) {
for (int i=0; i<table.subTypes.length; i++) {
final Class<?> candidate = table.subTypes[i];
if (candidate.isAssignableFrom(type)) {
buffer.append(" WHERE (").append(table.typeColumn)
.append(" LIKE '").append(table.typeNames[i]).append("%'");
hasWhere = true;
tableType = candidate;
break;
}
}
if (hasWhere) {
buffer.append(')');
}
}
this.type = tableType;
isProjection = Projection.class.isAssignableFrom(tableType);
final int length = buffer.length();
buffer.append(" ORDER BY ").append(table.codeColumn);
sqlAll = factory.adaptSQL(buffer.toString());
buffer.setLength(length);
buffer.append(hasWhere ? " AND " : " WHERE ").append(table.codeColumn).append(" = ?");
sqlSingle = factory.adaptSQL(buffer.toString());
}
/**
* Returns all codes.
*/
private ResultSet getAll() throws SQLException {
assert Thread.holdsLock(this);
if (queryAll != null && factory.isConnectionValid(queryAll.getConnection())) {
try {
return queryAll.executeQuery();
} catch (SQLException ignore) {
/*
* Failed to reuse an existing statement. This problem occurs in some occasions
* with the JDBC-ODBC bridge in Java 6 (the error message is "Invalid handle").
* I'm not sure where the bug come from (didn't noticed it when using HSQL). We
* will try again with a new statement created in the code after this 'catch'
* clause. Note that we set 'queryAll' to null first in case of failure during
* the 'prepareStatement(...)' execution.
*/
queryAll.close();
queryAll = null;
recoverableException("getAll", ignore);
}
}
queryAll = factory.getConnection().prepareStatement(sqlAll);
return queryAll.executeQuery();
}
/**
* Returns a single code.
*/
private ResultSet getSingle(final Object code) throws SQLException {
assert Thread.holdsLock(this);
if (querySingle == null || !factory.isConnectionValid(querySingle.getConnection())) {
querySingle = factory.getConnection().prepareStatement(sqlSingle);
}
querySingle.setString(1, code.toString());
return querySingle.executeQuery();
}
/**
* Returns {@code true} if the code in the specified result set is acceptable.
* This method handle projections in a special way.
*/
private boolean isAcceptable(final ResultSet results) throws SQLException {
if (!isProjection) {
return true;
}
final String code = results.getString(1);
synchronized (factory) {
return factory.isProjection(code);
}
}
/**
* Returns {@code true} if the code in the specified code is acceptable.
* This method handle projections in a special way.
*/
private boolean isAcceptable(final String code) throws SQLException {
if (!isProjection) {
return true;
}
synchronized (factory) {
return factory.isProjection(code);
}
}
/**
* Returns {@code true} if this collection contains no elements.
* This method fetch at most one row instead of counting all rows.
*/
@Override
public synchronized boolean isEmpty() {
if (size != -1) {
return size == 0;
}
boolean empty = true;
try {
final ResultSet results = getAll();
while (results.next()) {
if (isAcceptable(results)) {
empty = false;
break;
}
}
results.close();
} catch (SQLException exception) {
unexpectedException("isEmpty", exception);
}
size = empty ? 0 : -2;
return empty;
}
/**
* Count the number of elements in the underlying result set.
*/
public synchronized int size() {
if (size >= 0) {
return size;
}
int count = 0;
try {
final ResultSet results = getAll();
while (results.next()) {
if (isAcceptable(results)) {
count++;
}
}
results.close();
} catch (SQLException exception) {
unexpectedException("size", exception);
}
size = count; // Stores only on success.
return count;
}
/**
* Returns {@code true} if this collection contains the specified element.
*/
@Override
public synchronized boolean contains(final Object code) {
boolean exists = false;
if (code != null) try {
final ResultSet results = getSingle(code);
while (results.next()) {
if (isAcceptable(results)) {
exists = true;
break;
}
}
results.close();
} catch (SQLException exception) {
unexpectedException("contains", exception);
}
return exists;
}
/**
* Returns an iterator over the codes. The iterator is backed by a living {@link ResultSet},
* which will be closed as soon as the iterator reach the last element.
*/
@Override
public synchronized java.util.Iterator<String> iterator() {
try {
final Iterator iterator = new Iterator(getAll());
/*
* Set the statement to null without closing it, in order to force a new statement
* creation if getAll() is invoked before the iterator finish its iteration. This
* is needed because only one ResultSet is allowed for each Statement.
*/
queryAll = null;
return iterator;
} catch (SQLException exception) {
unexpectedException("iterator", exception);
final Set<String> empty = Collections.emptySet();
return empty.iterator();
}
}
/**
* Returns a serializable copy of this set. This method is invoked automatically during
* serialization. The serialised set of authority code is disconnected from the underlying
* database.
*/
protected Object writeReplace() throws ObjectStreamException {
return new LinkedHashSet<String>(this);
}
/**
* Closes the underlying statements. Note: this method is also invoked directly
* by {@link DirectEpsgFactory#dispose}, which is okay in this particular case since
* the implementation of this method can be executed an arbitrary amount of times.
*/
@Override
protected synchronized void finalize() throws SQLException {
if (querySingle != null) {
querySingle.close();
querySingle = null;
}
if (queryAll != null) {
queryAll.close();
queryAll = null;
}
}
/**
* Invoked when an exception occured. This method just log a warning.
*/
private static void unexpectedException(final String method,
final SQLException exception)
{
unexpectedException(AuthorityCodes.class, method, exception);
}
/**
* Invoked when an exception occured. This method just log a warning.
*/
static void unexpectedException(final Class<?> classe,
final String method,
final SQLException exception)
{
Logging.unexpectedException(classe, method, exception);
}
/**
* Invoked when a recoverable exception occured.
*/
private static void recoverableException(final String method, final SQLException exception) {
// Uses the FINE level instead of WARNING because it may be a recoverable error.
LogRecord record = Loggings.format(Level.FINE, LoggingKeys.UNEXPECTED_EXCEPTION);
record.setSourceClassName(AuthorityCodes.class.getName());
record.setSourceMethodName(method);
record.setThrown(exception);
final Logger logger = Logging.getLogger(AuthorityCodes.class);
record.setLoggerName(logger.getName());
logger.log(record);
}
/**
* The iterator over the codes. This inner class must kept a reference toward the enclosing
* {@link AuthorityCodes} in order to prevent a call to {@link AuthorityCodes#finalize}
* before the iteration is finished.
*/
private final class Iterator implements java.util.Iterator<String> {
/** The result set, or {@code null} if there is no more elements. */
private ResultSet results;
/** The next code. */
private transient String next;
/** Creates a new iterator for the specified result set. */
Iterator(final ResultSet results) throws SQLException {
assert Thread.holdsLock(AuthorityCodes.this);
this.results = results;
toNext();
}
/** Moves to the next element. */
private void toNext() throws SQLException {
while (results.next()) {
next = results.getString(1);
if (isAcceptable(next)) {
return;
}
}
finalize();
}
/** Returns {@code true} if there is more elements. */
public boolean hasNext() {
return results != null;
}
/** Returns the next element. */
public String next() {
if (results == null) {
throw new NoSuchElementException();
}
final String current = next;
try {
toNext();
} catch (SQLException exception) {
results = null;
unexpectedException(Iterator.class, "next", exception);
}
return current;
}
/** Always throws an exception, since this iterator is read-only. */
public void remove() {
throw new UnsupportedOperationException();
}
/** Closes the underlying result set. */
@Override
protected void finalize() throws SQLException {
next = null;
if (results != null) {
final PreparedStatement owner = (PreparedStatement) results.getStatement();
results.close();
results = null;
synchronized (AuthorityCodes.this) {
/*
* We don't need the statement anymore. Gives it back to the enclosing class
* in order to avoid creating a new one when AuthorityCodes.getAll() will be
* invoked again, or closes the statement if getAll() already created a new
* statement anyway.
*/
assert owner != queryAll;
if (queryAll == null) {
queryAll = owner;
} else {
owner.close();
}
}
}
}
}
/**
* Returns a view of this set as a map with object's name as value, or {@code null} if none.
*/
final java.util.Map<String,String> asMap() {
if (asMap == null) {
asMap = new Map();
}
return asMap;
}
/**
* A view of {@link AuthorityCodes} as a map, with authority codes as key and
* object names as values.
*/
private final class Map extends AbstractMap<String,String> {
/**
* Returns the number of key-value mappings in this map.
*/
@Override
public int size() {
return AuthorityCodes.this.size();
}
/**
* Returns {@code true} if this map contains no key-value mappings.
*/
@Override
public boolean isEmpty() {
return AuthorityCodes.this.isEmpty();
}
/**
* Returns the description to which this map maps the specified EPSG code.
*/
@Override
public String get(final Object code) {
String value = null;
if (code != null) try {
synchronized (AuthorityCodes.this) {
final ResultSet results = getSingle(code);
while (results.next()) {
if (isAcceptable(results)) {
value = results.getString(2);
break;
}
}
results.close();
}
} catch (SQLException exception) {
unexpectedException("get", exception);
}
return value;
}
/**
* Returns {@code true} if this map contains a mapping for the specified EPSG code.
*/
@Override
public boolean containsKey(final Object key) {
return contains(key);
}
/**
* Returns a set view of the keys contained in this map.
*/
@Override
public Set<String> keySet() {
return AuthorityCodes.this;
}
/**
* Returns a set view of the mappings contained in this map.
*
* @todo Not yet implemented.
*/
public Set<java.util.Map.Entry<String,String>> entrySet() {
throw new UnsupportedOperationException();
}
}
}