blob: fb79be5d5a9dbb3191116a3b532d61c5ed07eea5 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.sis.referencing.factory.sql;
import java.util.Collections;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import javax.sql.DataSource;
import java.io.IOException;
import java.io.FileNotFoundException;
import org.opengis.util.NameFactory;
import org.opengis.util.FactoryException;
import org.opengis.referencing.crs.CRSFactory;
import org.opengis.referencing.crs.CRSAuthorityFactory;
import org.opengis.referencing.cs.CSFactory;
import org.opengis.referencing.cs.CSAuthorityFactory;
import org.opengis.referencing.datum.DatumFactory;
import org.opengis.referencing.datum.DatumAuthorityFactory;
import org.opengis.referencing.operation.CoordinateOperationFactory;
import org.opengis.referencing.operation.CoordinateOperationAuthorityFactory;
import org.opengis.referencing.operation.MathTransformFactory;
import org.apache.sis.internal.metadata.sql.Initializer;
import org.apache.sis.internal.referencing.DeferredCoordinateOperation;
import org.apache.sis.internal.referencing.Resources;
import org.apache.sis.internal.system.DefaultFactories;
import org.apache.sis.internal.util.Constants;
import org.apache.sis.referencing.factory.ConcurrentAuthorityFactory;
import org.apache.sis.referencing.factory.UnavailableFactoryException;
import org.apache.sis.util.resources.Messages;
import org.apache.sis.util.logging.Logging;
import org.apache.sis.util.ArgumentChecks;
import org.apache.sis.util.Classes;
import org.apache.sis.util.Exceptions;
import org.apache.sis.util.Localized;
/**
* A geodetic object factory backed by the EPSG database. This class creates JDBC connections to the EPSG database
* when first needed using the {@link DataSource} specified at construction time. The geodetic objects are cached
* for reuse and the idle connections are closed after a timeout.
*
* <p>If no data source has been specified to the constructor, then {@code EPSGFactory} searches for a
* default data source in JNDI, or in the directory given by the {@code SIS_DATA} environment variable,
* or in the directory given by the {@code "derby​.system​.home"} property, in that order.
* See the {@linkplain org.apache.sis.referencing.factory.sql package documentation} for more information.</p>
*
* <div class="section">EPSG dataset installation</div>
* This class tries to automatically detect the schema that contains the EPSG tables
* (see {@link SQLTranslator} for examples of tables to look for). If the tables are not found,
* then the {@link #install(Connection)} method will be invoked for creating the EPSG schema.
* The {@code install(…)} method can perform its work only if the definition files are reachable
* on the classpath, or if the directory containing the files have been specified.
*
* <div class="section">Data Access Object (DAO)</div>
* If there is no cached object for a given code, then {@code EPSGFactory} creates an {@link EPSGDataAccess} instance
* for performing the actual creation work. Developers who need to customize the geodetic object creation can override
* the {@link #newDataAccess(Connection, SQLTranslator)} method in order to return their own {@link EPSGDataAccess}
* subclass.
*
* @author Martin Desruisseaux (Geomatys)
* @version 0.8
*
* @see EPSGDataAccess
* @see SQLTranslator
* @see <a href="http://sis.apache.org/tables/CoordinateReferenceSystems.html">List of authority codes</a>
*
* @since 0.7
* @module
*/
public class EPSGFactory extends ConcurrentAuthorityFactory<EPSGDataAccess> implements CRSAuthorityFactory,
CSAuthorityFactory, DatumAuthorityFactory, CoordinateOperationAuthorityFactory, Localized
{
/**
* The namespace of EPSG codes.
*
* @see #getCodeSpaces()
*/
private static final Set<String> CODESPACES = Collections.singleton(Constants.EPSG);
/**
* The factory to use for creating {@link Connection}s to the EPSG database.
*/
protected final DataSource dataSource;
/**
* The factory to use for creating {@link org.opengis.util.GenericName} instances.
*/
protected final NameFactory nameFactory;
/**
* The factory to use for creating {@link org.opengis.referencing.datum.Datum} instances
* from the properties read in the database.
*/
protected final DatumFactory datumFactory;
/**
* The factory to use for creating {@link org.opengis.referencing.cs.CoordinateSystem} instances
* from the properties read in the database.
*/
protected final CSFactory csFactory;
/**
* The factory to use for creating {@link org.opengis.referencing.crs.CoordinateReferenceSystem} instances
* from the properties read in the database.
*/
protected final CRSFactory crsFactory;
/**
* The factory to use for creating {@link org.opengis.referencing.operation.CoordinateOperation} instances
* from the properties read in the database.
*/
protected final CoordinateOperationFactory copFactory;
/**
* The factory to use for creating {@link org.opengis.referencing.operation.MathTransform} instances.
* The math transforms are created as part of {@link org.opengis.referencing.operation.CoordinateOperation}
* creation process.
*/
protected final MathTransformFactory mtFactory;
/**
* The name of the catalog that contains the EPSG tables, or {@code null} or an empty string.
* <ul>
* <li>The {@code ""} value retrieves the EPSG schema without a catalog.</li>
* <li>The {@code null} value means that the catalog name should not be used to narrow the search.</li>
* </ul>
*/
private final String catalog;
/**
* The name of the schema that contains the EPSG tables, or {@code null} or an empty string.
* <ul>
* <li>The {@code ""} value retrieves the EPSG tables without a schema.
* In such case, table names are prefixed by {@value SQLTranslator#TABLE_PREFIX}.</li>
* <li>The {@code null} value means that the schema name should not be used to narrow the search.
* In such case, {@link SQLTranslator} will tries to automatically detect the schema.</li>
* </ul>
*/
private final String schema;
/**
* A provider of SQL scripts to use if {@code EPSGFactory} needs to create the database,
* or {@code null} for the default mechanism.
*/
private final InstallationScriptProvider scriptProvider;
/**
* The translator from the SQL statements using MS-Access dialect to SQL statements using the dialect
* of the actual database. If null, will be created when first needed.
*/
private volatile SQLTranslator translator;
/**
* The locale for producing error messages. This is usually the default locale.
*
* @see #getLocale()
*/
private final Locale locale;
/**
* Creates a factory using the given configuration. The properties recognized by this constructor
* are listed in the table below. Any property not listed below will be ignored by this constructor.
* All properties are optional and can {@code null} or omitted, in which case default values are used.
* Those default values are implementation-specific and may change in any future SIS version.
*
* <table class="sis">
* <caption>Recognized properties</caption>
* <tr>
* <th>Key</th>
* <th>Value class</th>
* <th>Description</th>
* </tr><tr>
* <td>{@code dataSource}</td>
* <td>{@link DataSource}</td>
* <td>The factory to use for creating {@link Connection}s to the EPSG database.</td>
* </tr><tr>
* <td>{@code nameFactory}</td>
* <td>{@link NameFactory}</td>
* <td>The factory to use for creating {@link org.opengis.util.GenericName} instances.</td>
* </tr><tr>
* <td>{@code datumFactory}</td>
* <td>{@link DatumAuthorityFactory}</td>
* <td>The factory to use for creating {@link org.opengis.referencing.datum.Datum} instances.</td>
* </tr><tr>
* <td>{@code csFactory}</td>
* <td>{@link CSAuthorityFactory}</td>
* <td>The factory to use for creating {@link org.opengis.referencing.cs.CoordinateSystem} instances.</td>
* </tr><tr>
* <td>{@code crsFactory}</td>
* <td>{@link CRSAuthorityFactory}</td>
* <td>The factory to use for creating {@link org.opengis.referencing.crs.CoordinateReferenceSystem} instances.</td>
* </tr><tr>
* <td>{@code copFactory}</td>
* <td>{@link CoordinateOperationAuthorityFactory}</td>
* <td>The factory to use for creating {@link org.opengis.referencing.operation.CoordinateOperation} instances.</td>
* </tr><tr>
* <td>{@code mtFactory}</td>
* <td>{@link MathTransformFactory}</td>
* <td>The factory to use for creating {@link org.opengis.referencing.operation.MathTransform} instances.</td>
* </tr><tr>
* <td>{@code catalog}</td>
* <td>{@link String}</td>
* <td>The database catalog that contains the EPSG schema (see {@linkplain #install install}).</td>
* </tr><tr>
* <td>{@code schema}</td>
* <td>{@link String}</td>
* <td>The database schema that contains the EPSG tables (see {@linkplain #install install}).</td>
* </tr><tr>
* <td>{@code scriptProvider}</td>
* <td>{@link InstallationScriptProvider}</td>
* <td>A provider of SQL scripts to use if {@code EPSGFactory} needs to create the database.</td>
* </tr><tr>
* <td>{@code locale}</td>
* <td>{@link Locale}</td>
* <td>The locale for producing error messages on a <cite>best effort</cite> basis.</td>
* </tr>
* </table>
*
* <p>Default values</p>
* <ul>
* <li>If no {@code dataSource} is specified, this constructor defaults to the search algorithm described
* in the {@linkplain org.apache.sis.referencing.factory.sql package documentation}.</li>
* <li>If no {@code catalog} or {@code schema} is specified, {@link SQLTranslator} will try to auto-detect
* the schema that contains the EPSG tables.</li>
* <li>If no {@code locale} is specified, this constructor defaults to the
* {@linkplain Locale#getDefault(Locale.Category) display locale}.</li>
* </ul>
*
* @param properties the data source, authority factories and other configuration properties,
* or {@code null} for the default values.
* @throws ClassCastException if a property value is not of the expected class.
* @throws IllegalArgumentException if a property value is invalid.
* @throws FactoryException if an error occurred while creating the EPSG factory.
*/
public EPSGFactory(Map<String,?> properties) throws FactoryException {
super(EPSGDataAccess.class);
if (properties == null) {
properties = Collections.emptyMap();
}
DataSource ds = (DataSource) properties.get("dataSource");
Locale locale = (Locale) properties.get("locale");
schema = (String) properties.get("schema");
catalog = (String) properties.get("catalog");
scriptProvider = (InstallationScriptProvider) properties.get("scriptProvider");
if (locale == null) {
locale = Locale.getDefault(Locale.Category.DISPLAY);
}
this.locale = locale;
if (ds == null) try {
ds = Initializer.getDataSource();
if (ds == null) {
throw new UnavailableFactoryException(String.valueOf(Initializer.unspecified(locale, false)));
}
} catch (Exception e) {
throw new UnavailableFactoryException(canNotUse(e), e);
}
dataSource = ds;
nameFactory = factory(NameFactory.class, "nameFactory", properties);
datumFactory = factory(DatumFactory.class, "datumFactory", properties);
csFactory = factory(CSFactory.class, "csFactory", properties);
crsFactory = factory(CRSFactory.class, "crsFactory", properties);
copFactory = factory(CoordinateOperationFactory.class, "copFactory", properties);
mtFactory = factory(MathTransformFactory.class, "mtFactory", properties);
super.setTimeout(10, TimeUnit.SECONDS);
}
/**
* Returns the factory for the given key if it exists, or the default factory instance otherwise.
*/
private static <F> F factory(final Class<F> type, final String key, final Map<String,?> properties) {
final F factory = type.cast(properties.get(key));
return (factory != null) ? factory : DefaultFactories.forBuildin(type);
}
/**
* Returns the message to put in an {@link UnavailableFactoryException} having the given exception as its cause.
*/
private String canNotUse(final Exception e) {
String message = Exceptions.getLocalizedMessage(e, locale);
if (message == null) {
message = Classes.getShortClassName(e);
}
return canNotUse(message);
}
/**
* Returns the message to put in an {@link UnavailableFactoryException} having the given cause.
*/
private String canNotUse(final String cause) {
return Resources.forLocale(locale).getString(Resources.Keys.CanNotUseGeodeticParameters_2, Constants.EPSG, cause);
}
/**
* Returns the namespace of EPSG codes.
*
* @return the {@code "EPSG"} string in a singleton map.
*/
@Override
public Set<String> getCodeSpaces() {
return CODESPACES;
}
/**
* Returns the locale used by this factory for producing error messages.
* This locale does not change the way data are read from the EPSG database.
*
* @return the locale for error messages.
*/
@Override
public Locale getLocale() {
return locale;
}
/**
* Creates the EPSG schema in the database and populates the tables with geodetic definitions.
* This method is invoked automatically when {@link #newDataAccess()} detects that the EPSG dataset is not installed.
* Users can also invoke this method explicitly if they wish to force the dataset installation.
*
* <p>This method uses the following properties from the map specified at
* {@linkplain #EPSGFactory(Map) construction time}:</p>
*
* <ul class="verbose">
* <li><b>{@code catalog}:</b><br>
* a {@link String} giving the name of the database catalog where to create the EPSG schema.
* If non-null, that catalog shall exist prior this method call (this method does not create any catalog).
* If no catalog is specified or if the catalog is an empty string,
* then the EPSG schema will be created without catalog.
* If the database does not {@linkplain DatabaseMetaData#supportsCatalogsInTableDefinitions() support
* catalogs in table definitions} or in {@linkplain DatabaseMetaData#supportsCatalogsInDataManipulation()
* data manipulation}, then this property is ignored.</li>
*
* <li><b>{@code schema}:</b><br>
* a {@link String} giving the name of the database schema where to create the EPSG tables.
* That schema shall <strong>not</strong> exist prior this method call;
* the schema will be created by this {@code install(…)} method.
* If the schema is an empty string, then the tables will be created without schema.
* If no schema is specified, then the default schema is {@code "EPSG"}.
* If the database does not {@linkplain DatabaseMetaData#supportsSchemasInTableDefinitions() support
* schemas in table definitions} or in {@linkplain DatabaseMetaData#supportsSchemasInDataManipulation()
* data manipulation}, then this property is ignored.</li>
*
* <li><b>{@code scriptProvider}:</b><br>
* an {@link InstallationScriptProvider} giving the SQL scripts to execute for creating the EPSG database.
* If no provider is specified, then this method will search on the classpath (with {@link java.util.ServiceLoader})
* for user-provided implementations of {@code InstallationScriptProvider}.
* If no user-specified provider is found, then this method will search for
* {@code "EPSG_*Tables.sql"}, {@code "EPSG_*Data.sql"} and {@code "EPSG_*FKeys.sql"} files in the
* {@code $SIS_DATA/Databases/ExternalSources} directory where {@code *} stands for any characters
* provided that there is no ambiguity.</li>
* </ul>
*
* <p><b>Legal constraint:</b>
* the EPSG dataset can not be distributed with Apache SIS at this time for licensing reasons.
* Users need to either install the dataset manually (for example with the help of this method),
* or add on the classpath to a separated bundle like {@code org.apache.sis:non-free:sis-epsg.jar}.
* See <a href="http://sis.apache.org/epsg.html">How to use EPSG geodetic dataset</a> for more information.</p>
*
* @param connection connection to the database where to create the EPSG schema.
* @throws UnavailableFactoryException if installation failed. The exception will have a
* {@link FileNotFoundException} cause if a SQL script has not been found
* (typically because a required resource is not on the classpath), an
* {@link IOException} if an I/O error occurred while reading a SQL script, or a
* {@link SQLException} if an error occurred while writing to the database.
*
* @see InstallationScriptProvider
*/
public synchronized void install(final Connection connection) throws UnavailableFactoryException {
ArgumentChecks.ensureNonNull("connection", connection);
String message = null;
Exception failure = null;
try (EPSGInstaller installer = new EPSGInstaller(connection)) {
final boolean ac = connection.getAutoCommit();
if (ac) {
connection.setAutoCommit(false);
}
try {
boolean success = false;
try {
if (!"".equals(schema)) { // Schema may be null.
installer.setSchema(schema != null ? schema : Constants.EPSG);
if (catalog != null && !catalog.isEmpty()) {
installer.prependNamespace(catalog);
}
}
installer.run(scriptProvider, locale);
success = true;
} finally {
if (ac) {
if (success) {
connection.commit();
} else {
connection.rollback();
}
connection.setAutoCommit(true);
}
}
} catch (IOException | SQLException e) {
message = installer.failure(locale, e);
failure = e;
}
} catch (SQLException e) {
message = Messages.getResources(locale).getString(Messages.Keys.CanNotCreateSchema_1, Constants.EPSG);
failure = e;
}
if (failure != null) {
/*
* Derby sometime wraps SQLException into another SQLException. For making the stack strace a
* little bit simpler, keep only the root cause provided that the exception type is compatible.
*/
UnavailableFactoryException exception = new UnavailableFactoryException(message, Exceptions.unwrap(failure));
exception.setUnavailableFactory(this);
throw exception;
}
}
/**
* Creates the factory which will perform the actual geodetic object creation work.
* This method is invoked automatically when a new worker is required, either because the previous
* one has been disposed after its timeout or because a new one is required for concurrency.
*
* <p>The default implementation performs the following steps:</p>
* <ol>
* <li>Gets a new connection from the {@link #dataSource}.</li>
* <li>If this method is invoked for the first time, verifies if the EPSG tables exists.
* If the tables are not found, invokes {@link #install(Connection)}.</li>
* <li>Delegates to {@link #newDataAccess(Connection, SQLTranslator)}, which provides an easier
* overriding point for subclasses wanting to return a custom {@link EPSGDataAccess} instance.</li>
* </ol>
*
* @return Data Access Object (DAO) to use in {@code createFoo(String)} methods.
* @throws FactoryException if the constructor failed to connect to the EPSG database.
* This exception usually has a {@link SQLException} as its cause.
*/
@Override
protected EPSGDataAccess newDataAccess() throws FactoryException {
UnavailableFactoryException exception;
Connection connection = null;
try {
connection = dataSource.getConnection();
Logging.log(EPSGFactory.class, "newDataAccess", Initializer.connected(connection.getMetaData()));
SQLTranslator tr = translator;
if (tr == null) {
synchronized (this) {
tr = translator;
if (tr == null) {
tr = new SQLTranslator(connection.getMetaData(), catalog, schema);
try {
if (!tr.isTableFound()) {
install(connection);
tr.setup(connection.getMetaData()); // Set only on success.
}
} finally {
translator = tr; // Set only after installation in order to block other threads.
}
}
}
}
if (tr.isTableFound()) {
return newDataAccess(connection, tr);
} else {
connection.close();
exception = new UnavailableFactoryException(canNotUse(SQLTranslator.tableNotFound(locale)));
}
} catch (Exception e) { // Really want to catch all exceptions here.
if (connection != null) try {
connection.close();
} catch (SQLException e2) {
e.addSuppressed(e2);
}
if (e instanceof FactoryException) {
throw (FactoryException) e;
}
/*
* Derby sometime wraps SQLException into another SQLException. For making the stack strace a
* little bit simpler, keep only the root cause provided that the exception type is compatible.
*/
exception = new UnavailableFactoryException(canNotUse(e), Exceptions.unwrap(e));
}
exception.setUnavailableFactory(this);
throw exception;
}
/**
* Creates the factory which will perform the actual geodetic object creation from a given connection.
* This method is a convenience hook easier to override than {@link #newDataAccess()} for subclasses
* wanting to return instances of their own {@link EPSGDataAccess} subclass.
* The default implementation is simply:
*
* {@preformat java
* return new EPSGDataAccess(this, connection, translator);
* }
*
* Subclasses can override this method with a similar code but with {@code new EPSGDataAccess(…)} replaced
* by {@code new MyDataAccessSubclass(…)}.
*
* @param connection a connection to the EPSG database.
* @param translator the translator from the SQL statements using MS-Access dialect to SQL statements
* using the dialect of the actual database.
* @return Data Access Object (DAO) to use in {@code createFoo(String)} methods.
* @throws SQLException if a problem with the database has been detected.
*
* @see EPSGDataAccess#EPSGDataAccess(EPSGFactory, Connection, SQLTranslator)
*/
protected EPSGDataAccess newDataAccess(Connection connection, SQLTranslator translator) throws SQLException {
return new EPSGDataAccess(this, connection, translator);
}
/**
* Returns {@code true} if the given Data Access Object (DAO) can be closed. This method is invoked automatically
* after the {@linkplain #getTimeout timeout} if the given DAO has been idle during all that time. The default
* implementation always returns {@code false} if a set returned by {@link EPSGDataAccess#getAuthorityCodes(Class)}
* is still in use.
*
* @param factory the Data Access Object which is about to be closed.
* @return {@code true} if the given Data Access Object can be closed.
*/
@Override
protected boolean canClose(final EPSGDataAccess factory) {
return factory.canClose();
}
/**
* Returns whether the given object can be cached.
* This method is invoked after {@link EPSGDataAccess} created a new object not previously in the cache.
*
* @return whether the given object should be cached.
*
* @since 0.8
*/
@Override
protected boolean isCacheable(String code, Object object) {
return !(object instanceof DeferredCoordinateOperation);
}
}