/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2001-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.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.SQLException; import javax.sql.DataSource; import java.util.Iterator; import java.util.Comparator; import java.util.Collections; import java.util.jar.Attributes.Name; import java.util.logging.Level; import java.util.logging.LogRecord; import javax.imageio.spi.ServiceRegistry; import javax.naming.InitialContext; import javax.naming.NameNotFoundException; import javax.naming.NamingException; import javax.naming.NoInitialContextException; import org.opengis.metadata.citation.Citation; import org.opengis.referencing.FactoryException; import org.opengis.referencing.cs.CSAuthorityFactory; import org.opengis.referencing.crs.CRSAuthorityFactory; import org.opengis.referencing.datum.DatumAuthorityFactory; import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory; import org.geotools.factory.GeoTools; import org.geotools.factory.Hints; import org.geotools.factory.FactoryRegistry; import org.geotools.metadata.iso.citation.Citations; import org.geotools.referencing.ReferencingFactoryFinder; import org.geotools.referencing.factory.AbstractAuthorityFactory; import org.geotools.referencing.factory.DeferredAuthorityFactory; import org.geotools.referencing.factory.FactoryNotFoundException; import org.geotools.referencing.factory.ReferencingFactoryContainer; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; import org.geotools.resources.i18n.Loggings; import org.geotools.resources.i18n.LoggingKeys; import org.geotools.resources.i18n.Vocabulary; import org.geotools.resources.i18n.VocabularyKeys; /** * Base class for EPSG factories to be registered in {@link ReferencingFactoryFinder}. * Various subclasses are defined for different database backends: Access, PostgreSQL, * HSQL, <cite>etc.</cite>. * <p> * This class has the following responsibilities: * <ul> * <li>aquire a DataSource (using JNDI or otherwise)</li> * <li>specify a worker class that will talk to the database in * the event of a cache miss. The class will be specific to the delect of SQL * used by the database hosting the EPSG tables.</li> * </ul> * Please note we are working with <b>the same</b> tables as defined by EPSG. The only * thing that changes is the database used to host these tables. * <p> * Subclasses should override the following methods: * <ul> * <li>{@linkplain #createDataSource} used to aquire a DataSource</li> * <li>{@link #createBackingStore} instance capable to speak that database syntax</li> * </ul> * <p> * Users should not creates instance of this class directly. They should invoke one of * <code>{@linkplain ReferencingFactoryFinder}.getFooAuthorityFactory("EPSG")</code> methods instead. * * @since 2.4 * * @source $URL$ * @version $Id$ * @author Martin Desruisseaux (IRD) */ public class ThreadedEpsgFactory extends DeferredAuthorityFactory implements CRSAuthorityFactory, CSAuthorityFactory, DatumAuthorityFactory, CoordinateOperationAuthorityFactory { /** * The default JDBC {@linkplain DataSource data source} name in JNDI. * This is the name used if no other name were specified through the * {@link Hints#EPSG_DATA_SOURCE EPSG_DATA_SOURCE} hint. * * @see #createDataSource */ public static final String DATASOURCE_NAME = "java:comp/env/jdbc/EPSG"; /** * {@code true} if automatic registration of {@link #datasourceName} is allowed. * Set to {@code false} for now because the registration has not been correctly * tested in JEE environment. * * @todo Consider removing completly the code related to JNDI binding. In such * case, this field and the {@link #registerInto} field would be removed. */ private static final boolean ALLOW_REGISTRATION = false; /** * The default priority level for this factory. */ static final int PRIORITY = MAXIMUM_PRIORITY - 10; /** * The factories to be given to the backing store. */ private final ReferencingFactoryContainer factories; /** * The context where to register {@link #datasource}, or {@code null} if it should * not be registered. This is used only as a way to pass "hiden" return value between * {@link #createDataSource()} and {@link #createBackingStore()}. */ private transient InitialContext registerInto; /** * The data source name. If it was not specified by the {@link Hints#EPSG_DATA_SOURCE * EPSG_DATA_SOURCE} hint, then this is the {@value #DATASOURCE_NAME} value. */ private String datasourceName; /** * The data source, or {@code null} if the connection has not yet been etablished. */ protected DataSource datasource; /** * Whether the DataSource is created along with the backing store, or it's a stable, long lived one */ protected boolean dynamicDataSource = true; /** * Constructs an authority factory using the default set of factories. */ public ThreadedEpsgFactory() { this(null); } /** * Constructs an authority factory with the default priority. */ public ThreadedEpsgFactory(final Hints userHints) { this(userHints, PRIORITY); } /** * Constructs an authority factory using a set of factories created from the specified hints. * This constructor recognizes the {@link Hints#CRS_FACTORY CRS}, {@link Hints#CS_FACTORY CS}, * {@link Hints#DATUM_FACTORY DATUM} and {@link Hints#MATH_TRANSFORM_FACTORY MATH_TRANSFORM} * {@code FACTORY} hints, in addition of {@link Hints#EPSG_DATA_SOURCE EPSG_DATA_SOURCE}. * * @param userHints An optional set of hints, or {@code null} if none. * @param priority The priority for this factory, as a number between * {@link #MINIMUM_PRIORITY MINIMUM_PRIORITY} and * {@link #MAXIMUM_PRIORITY MAXIMUM_PRIORITY} inclusive. */ public ThreadedEpsgFactory(final Hints userHints, final int priority) { super(userHints, priority); Object hint = (userHints == null) ? null : userHints.get(Hints.EPSG_DATA_SOURCE); if (hint == null) { datasourceName = DATASOURCE_NAME; //datasourceName = GeoTools.fixName(DATASOURCE_NAME); hints.put(Hints.EPSG_DATA_SOURCE, datasourceName); } else if (hint instanceof String) { datasourceName = (String) hint; //datasourceName = GeoTools.fixName(datasourceName); hints.put(Hints.EPSG_DATA_SOURCE, datasourceName); } else if (hint instanceof Name) { Name name = (Name) hint; hints.put(Hints.EPSG_DATA_SOURCE, name); datasourceName = name.toString(); //datasourceName = GeoTools.fixName(name.toString()); } else if (hint instanceof DataSource) { datasource = (DataSource) hint; hints.put(Hints.EPSG_DATA_SOURCE, datasource); datasourceName = DATASOURCE_NAME; dynamicDataSource = false; } factories = ReferencingFactoryContainer.instance(userHints); long timeout = 30 * 60 * 1000; String defaultTimeout = System.getProperty("org.geotools.epsg.factory.timeout", String.valueOf(30 * 60 * 1000)); try { timeout = Long.valueOf(defaultTimeout); } catch(NumberFormatException e) { LOGGER.log(Level.WARNING, "Invalid value for org.geotools.epsg.factory.timeout, " + "using the default (30 minutes) instead"); } // in case of negative timeout, we don't release the data source and backing store if(timeout > 0) { LOGGER.log(Level.FINE, "Setting the EPSG factory " + getClass().getName() + " to a " + timeout + "ms timeout"); setTimeout(timeout); // Close the connection after 1 second of inactivity. } else { LOGGER.log(Level.FINE, "The EPSG factory " + getClass().getName() + " will not timeout"); } } /** * Returns the authority for this EPSG database. * This authority will contains the database version in the {@linkplain Citation#getEdition * edition} attribute, together with the {@linkplain Citation#getEditionDate edition date}. */ @Override public Citation getAuthority() { final Citation authority = super.getAuthority(); return (authority!=null) ? authority : Citations.EPSG; } /** * Returns the data source for the EPSG database. If no data source has been previously * {@linkplain #setDataSource set}, then this method invokes {@link #createDataSource}. * <strong>Note:</strong> invoking this method may force immediate connection to the EPSG * database. * * @return The data source. * @throws SQLException if the connection to the EPSG database failed. * * @see #setDataSource * @see #createDataSource */ public final synchronized DataSource getDataSource() throws SQLException { if (datasource == null) { // Force the creation of the underlying backing store. It will invokes // (indirectly) createBackingStore, which will fetch the DataSource. if (!super.isAvailable()) { // Connection failed, but the exception is not available. datasource = null; throw new SQLException(Errors.format(ErrorKeys.NO_DATA_SOURCE)); } } return datasource; } /** * Set the data source for the EPSG database. If an other EPSG database was already in use, * it will be disconnected. Users should not invoke this method on the factory returned by * {@link ReferencingFactoryFinder}, since it could have a system-wide effect. * * @param datasource The new datasource. * @throws SQLException if an error occured. */ public synchronized void setDataSource(final DataSource datasource) throws SQLException { if (datasource != this.datasource) { try { dispose(); } catch (FactoryException exception) { final Throwable cause = exception.getCause(); if (cause instanceof SQLException) { throw (SQLException) cause; } if (cause instanceof RuntimeException) { throw (RuntimeException) cause; } // Not really an SQL exception, but we should not reach this point anyway. final SQLException e = new SQLException(exception.getLocalizedMessage()); e.initCause(exception); // TODO: inline when we will be allowed to target Java 6. throw e; } this.datasource = datasource; } } /** * Setup a data source for a connection to the EPSG database. This method is invoked by * {@link #getDataSource()} when no data source has been {@linkplain #setDataSource * explicitly set}. The default implementation searchs for a {@link DataSource} instance * binded to the {@link Hints#EPSG_DATA_SOURCE} name * (<code>{@value #DATASOURCE_NAME}</code> by default) using <cite>Java Naming and * Directory Interfaces</cite> (JNDI). If no data source were found, then this method * returns {@code null}. * <p> * Subclasses override this method in order to initialize a default data source when none were * found with JNDI. For example {@code plugin/epsg-access} defines a default data source using * the JDBC-ODBC bridge, which expects an "{@code EPSG}" database registered as an ODBC data * source (see the {@linkplain org.geotools.referencing.factory.epsg package javadoc} for * installation instructions). Example for a PostgreSQL data source: * * <blockquote><pre> * protected DataSource createDataSource() throws SQLException { * DataSource candidate = super.createDataSource(); * if (candidate instanceof Jdbc3SimpleDataSource) { * return candidate; * } * Jdbc3SimpleDataSource ds = new Jdbc3SimpleDataSource(); * ds.setServerName("localhost"); * ds.setDatabaseName("EPSG"); * ds.setUser("postgre"); * return ds; * } * </pre></blockquote> * * @return The EPSG data source, or {@code null} if none where found. * @throws SQLException if an error occured while creating the data source. */ protected DataSource createDataSource() throws SQLException { InitialContext context = null; DataSource source = null; try { context = GeoTools.getInitialContext(new Hints(hints)); source = (DataSource) context.lookup(datasourceName); } catch (IllegalArgumentException exception) { // Fall back on 'return null' below. } catch (NoInitialContextException exception) { // Fall back on 'return null' below. } catch (NamingException exception) { registerInto = context; // Fall back on 'return null' below. } return source; } /** * Creates the backing store for the specified data source. This method usually returns * a new instance of {@link AccessDialectEpsgFactory} or {@link AnsiDialectEpsgFactory}. * Subclasses may override this method in order to returns an instance tuned for the * SQL syntax of the underlying database. Example for a PostgreSQL data source: * * <blockquote><pre> * protected AbstractAuthorityFactory createBackingStore(Hints hints) throws SQLException { * return new AnsiDialectEpsgFactory(hints, getDataSource().getConnection()); * } * </pre></blockquote> * * @param hints A map of hints, including the low-level factories to use for CRS creation. * This argument should be given unchanged to {@code DirectEpsgFactory} constructor. * @return The {@linkplain DirectEpsgFactory EPSG factory} using SQL queries appropriate * for this data source. * @throws SQLException if connection to the database failed. */ protected AbstractAuthorityFactory createBackingStore(final Hints hints) throws SQLException { final DataSource source = getDataSource(); final Connection connection = source.getConnection(); final String quote = connection.getMetaData().getIdentifierQuoteString(); if (quote.equals("\"")) { /* * PostgreSQL quotes the indentifiers with "..." while MS-Access quotes the * identifiers with [...], so we use the identifier quote string metadata as * a way to distinguish the two cases. However I'm not sure that it is a robust * criterion. Subclasses should always override as a safety. */ return new FactoryUsingAnsiSQL(hints, connection); } return new FactoryUsingSQL(hints, connection); } /** * Gets the EPSG factory implementation connected to the database. This method is invoked * automatically by {@link #createBackingStore()}. * * @return The connection to the EPSG database. * @throws FactoryException if no data source were found. * @throws SQLException if this method failed to etablish a connection. * * @todo Inline this method into {@link #createBackingStore()} after we removed the * deprecated code. */ private AbstractAuthorityFactory createBackingStore0() throws FactoryException, SQLException { assert Thread.holdsLock(this); final Hints sourceHints = new Hints(hints); sourceHints.putAll(factories.getImplementationHints()); if (datasource != null) { return createBackingStore(sourceHints); } /* * Try to gets the DataSource from JNDI. In case of success, it will be tried * for a connection before any DataSource declared in META-INF/services/. */ DataSource source; final InitialContext context; try { source = createDataSource(); context = registerInto; } finally { registerInto = null; } if (source == null) { throw new FactoryNotFoundException(Errors.format(ErrorKeys.NO_DATA_SOURCE)); } final AbstractAuthorityFactory factory; try { datasource = source; factory = createBackingStore(sourceHints); } finally { datasource = null; } /* * We now have a working connection. If a naming directory is running but didn't contains * the "jdbc/EPSG" entry, add it now. In such case, a message is prepared and logged. */ LogRecord record; if (ALLOW_REGISTRATION && context != null) { try { context.bind(datasourceName, source); record = Loggings.format(Level.FINE, LoggingKeys.CREATED_DATASOURCE_ENTRY_$1, datasourceName); } catch (NamingException exception) { record = Loggings.format(Level.WARNING, LoggingKeys.CANT_BIND_DATASOURCE_$1, datasourceName); record.setThrown(exception); } log(record); } this.datasource = source; // Stores the data source only after success. return factory; } /** * Creates the backing store authority factory. * * @return The backing store to uses in {@code createXXX(...)} methods. * @throws FactoryException if the constructor failed to connect to the EPSG database. * This exception usually has a {@link SQLException} as its cause. */ protected AbstractAuthorityFactory createBackingStore() throws FactoryException { final AbstractAuthorityFactory factory; String product = '<' + Vocabulary.format(VocabularyKeys.UNKNOWN) + '>'; String url = product; try { factory = createBackingStore0(); if (factory instanceof DirectEpsgFactory) { final DatabaseMetaData info = ((DirectEpsgFactory) factory).getConnection().getMetaData(); product = info.getDatabaseProductName(); url = info.getURL(); } } catch (SQLException exception) { throw new FactoryException(Errors.format(ErrorKeys.CANT_CONNECT_DATABASE_$1, "EPSG"), exception); } log(Loggings.format(Level.CONFIG, LoggingKeys.CONNECTED_EPSG_DATABASE_$2, url, product)); if (factory instanceof DirectEpsgFactory) { ((DirectEpsgFactory) factory).buffered = this; } return factory; } /** * For internal use by {@link #createFactory()} and {@link #createBackingStore()} only. */ private static void log(final LogRecord record) { record.setSourceClassName(ThreadedEpsgFactory.class.getName()); record.setSourceMethodName("createBackingStore"); // The public caller. record.setLoggerName(LOGGER.getName()); LOGGER.log(record); } /** * Returns {@code true} if the backing store can be disposed now. This method is invoked * automatically after the amount of time specified by {@link #setTimeout} if the factory * were not used during that time. * * @param backingStore The backing store in process of being disposed. */ @Override protected boolean canDisposeBackingStore(final AbstractAuthorityFactory backingStore) { if (backingStore instanceof DirectEpsgFactory) { return ((DirectEpsgFactory) backingStore).canDispose(); } return super.canDisposeBackingStore(backingStore); } @Override protected void disposeBackingStore() { super.disposeBackingStore(); if(dynamicDataSource) { datasource = null; } } @Override public synchronized void dispose() throws FactoryException { super.dispose(); datasource = null; } }