This is an automated email from the ASF dual-hosted git repository. desruisseaux pushed a commit to branch geoapi-4.0 in repository https://gitbox.apache.org/repos/asf/sis.git
The following commit(s) were added to refs/heads/geoapi-4.0 by this push: new 70d35b06da Add support for `DataSource` created from JDBC URL in `StorageConnector`. Add documentation and minor cleanup. 70d35b06da is described below commit 70d35b06da4121c6c5d3c6ce03bf2d82f4dcb0d4 Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Wed Aug 28 18:44:35 2024 +0200 Add support for `DataSource` created from JDBC URL in `StorageConnector`. Add documentation and minor cleanup. --- .../org/apache/sis/metadata/sql/privy/Dialect.java | 3 +- .../main/module-info.java | 24 ++- .../main/org/apache/sis/storage/sql/SQLStore.java | 2 +- .../apache/sis/storage/sql/SimpleFeatureStore.java | 4 +- .../sis/storage/sql/feature/CRSEncoding.java | 1 + .../sis/storage/sql/feature/GeometryGetter.java | 4 +- .../sis/storage/sql/feature/SpatialSchema.java | 4 +- .../org/apache/sis/storage/sql/package-info.java | 25 +-- .../org/apache/sis/storage/sql/SQLStoreTest.java | 21 ++- .../org/apache/sis/storage/StorageConnector.java | 74 +++++--- .../main/org/apache/sis/storage/URLDataSource.java | 198 +++++++++++++++++++++ .../org/apache/sis/storage/internal/Resources.java | 5 + .../sis/storage/internal/Resources.properties | 1 + .../sis/storage/internal/Resources_fr.properties | 1 + .../main/org/apache/sis/util/privy/Constants.java | 2 +- 15 files changed, 320 insertions(+), 49 deletions(-) diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java index a942ada0df..c1f2b16716 100644 --- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/privy/Dialect.java @@ -19,6 +19,7 @@ package org.apache.sis.metadata.sql.privy; import java.sql.SQLException; import java.sql.DatabaseMetaData; import org.apache.sis.util.CharSequences; +import org.apache.sis.util.privy.Constants; /** @@ -170,7 +171,7 @@ public enum Dialect { final String url = metadata.getURL(); if (url != null) { int start = url.indexOf(':'); - if (start >= 0 && "jdbc".equalsIgnoreCase((String) CharSequences.trimWhitespaces(url, 0, start))) { + if (start >= 0 && Constants.JDBC.equalsIgnoreCase((String) CharSequences.trimWhitespaces(url, 0, start))) { final int end = url.indexOf(':', ++start); if (end >= 0) { final String protocol = (String) CharSequences.trimWhitespaces(url, start, end); diff --git a/endorsed/src/org.apache.sis.storage.sql/main/module-info.java b/endorsed/src/org.apache.sis.storage.sql/main/module-info.java index 90a561b5a0..aa3eab4fe6 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/module-info.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/module-info.java @@ -16,12 +16,32 @@ */ /** - * SQL databases store. + * Data store for features in a <abbr>SQL</abbr> spatial database. + * This module expects a spatial schema conforms to the conventions described in the + * <a href="https://www.ogc.org/standards/sfs">OGC Simple feature access - Part 2: SQL option</a> + * international standard, also known as <abbr>ISO</abbr> 19125-2. + * + * <h2>Difference with Geopackage</h2> + * Compared to the <abbr>OGC</abbr> Geopackage standard, + * this <abbr>SQL</abbr> module has the following differences: + * + * <ul> + * <li>There is no discovery mechanism (e.g., no {@code "gpkg_contents"} table). + * The tables to use as {@linkplain org.apache.sis.storage.sql.ResourceDefinition resource definitions} + * must be specified explicitly.</li> + * <li>Each feature table can contain an arbitrary number of geometry columns, including zero. + * By contrast, Geopackage requires each feature table to have exactly one geometry column.</li> + * <li>As a consequence of the above, this module makes no distinction between "features" table and "attributes" table.</li> + * <li>This module supports <dfn>complex features</dfn>, i.e. features having associations to other features. + * The associations are discovered automatically by following the foreigner keys.</li> + * <li>Primary keys are optional. If present, they can be of any type (not necessarily integers) and can be composite + * (made of many columns). By contrast, Geopackage mandates primary keys made of exactly one column of integers.</li> + * </ul> * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) * @author Alexis Manin (Geomatys) - * @version 1.4 + * @version 1.5 * @since 1.0 */ module org.apache.sis.storage.sql { diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java index 6cc17a02b7..ca6889ffdb 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SQLStore.java @@ -218,7 +218,7 @@ public abstract class SQLStore extends DataStore implements Aggregate { */ protected SQLStore(final DataStoreProvider provider, final StorageConnector connector) throws DataStoreException { super(provider, connector); - source = connector.getStorageAs(DataSource.class); + source = connector.commit(DataSource.class, "SQL"); geomLibrary = connector.getOption(OptionKey.GEOMETRY_LIBRARY); contentLocale = connector.getOption(OptionKey.LOCALE); customizer = connector.getOption(SchemaModifier.OPTION); diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SimpleFeatureStore.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SimpleFeatureStore.java index fc839f5b60..eb32c98b8e 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SimpleFeatureStore.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/SimpleFeatureStore.java @@ -43,7 +43,9 @@ import org.apache.sis.util.ArgumentChecks; * inferred by foreigner keys will be followed automatically.</li> * </ul> * - * The mapping from table structures to feature types is described in the package Javadoc. + * Despite the {@code SimpleFeatureStore} class name, this class supports <dfn>complex features</dfn>, + * i.e. features having associations to other features. + * The associations are discovered automatically by following the foreigner keys. * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/CRSEncoding.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/CRSEncoding.java index 8b8e91df0b..69ed7be60a 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/CRSEncoding.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/CRSEncoding.java @@ -19,6 +19,7 @@ package org.apache.sis.storage.sql.feature; /** * The encoding of Coordinate Reference Systems in a particular column, in preference order. + * The Geopackage specification said that WKT 2 has precedence over WKT 1. * * <p><b>Note:</b> the distinction between version 1 and 2 of <abbr>WKT</abbr> formats should not have been needed, * because a decent parser should be able to differentiate those two versions automatically based on the fact that diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java index 451ad045b9..fefebec715 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/GeometryGetter.java @@ -126,8 +126,8 @@ final class GeometryGetter<G, V extends G> extends ValueGetter<V> { final int flags = wkb[3]; final boolean bigEndian = (flags & 0b000001) == 0; final int envelopeType = (flags & 0b001110) >> 1; - final boolean isEmpty = (flags & 0b010000) != 0; - final boolean extendedType = (flags & 0b100000) != 0; + // final boolean isEmpty = (flags & 0b010000) != 0; + // final boolean extendedType = (flags & 0b100000) != 0; buffer.order(bigEndian ? ByteOrder.BIG_ENDIAN : ByteOrder.LITTLE_ENDIAN); gpkgSrid = buffer.getInt(Integer.BYTES); // Skip header and envelope. diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java index cd2b2c3204..42b62ef88f 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/feature/SpatialSchema.java @@ -119,8 +119,8 @@ public enum SpatialSchema { /** * Name of the column for CRS definitions in Well-Known Text (<abbr>WKT</abbr>) format. - * Example: {@code "SRTEXT"}, {@code "DEFINITION"}. - * Entries are in no particular order. + * Example: {@code "SRTEXT"}, {@code "DEFINITION"}. Entries are in no particular order. + * The priority order is not defined by this map, but by the {@link CRSEncoding} enumeration. */ final Map<CRSEncoding, String> crsDefinitionColumn; diff --git a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java index 1b38c39225..ec2b149779 100644 --- a/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java +++ b/endorsed/src/org.apache.sis.storage.sql/main/org/apache/sis/storage/sql/package-info.java @@ -25,18 +25,23 @@ * to another feature (with transitive dependencies automatically resolved), and the other columns are represented * by {@link org.opengis.feature.AttributeType}. * - * <p>The storage of spatial features in SQL databases is described by the + * <p>The storage of spatial features in <abbr>SQL</abbr> databases is described by the * <a href="https://www.ogc.org/standards/sfs">OGC Simple feature access - Part 2: SQL option</a> - * international standard, also known as ISO 19125-2. Implementation of geometric types and operations must - * be provided by the database (sometimes through an extension, for example PostGIS on PostgreSQL databases). - * This Java package uses those provided types and operations.</p> + * international standard, also known as ISO 19125-2. + * The implementation of geometric objects and their operations must be provided by the database. + * This is sometimes provided by an extension that needs to be installed explicitly. + * For example, when using PostgreSQL, the PostGIS extension is recommended.</p> + * + * <p>The tables to use as {@linkplain org.apache.sis.storage.sql.ResourceDefinition resource definitions} + * must be specified at construction time. There is no automatic discovery mechanism. Note that discovery + * may be done by other modules. For example, Geopackage module uses the {@code "gpkg_contents"} table.</p> * * <h2>Performance tips</h2> * <p>A subset of features can be obtained by applying filters on the stream returned by * {@link org.apache.sis.storage.FeatureSet#features(boolean)}. * While the filter can be any {@link java.util.function.Predicate}, * performances will be much better if they are instances of {@link org.opengis.filter.Filter} - * because Apache SIS will know how to translate some of them to SQL statements.</p> + * because Apache SIS will know how to translate some of them to <abbr>SQL</abbr> statements.</p> * * <p>In filter expressions like {@code ST_Intersects(A,B)} where the <var>A</var> and <var>B</var> parameters are * two sub-expressions evaluating to geometry values, if one of those expressions is a literal, then that literal @@ -44,13 +49,9 @@ * Coordinate Reference System of <var>A</var>. If <var>B</var> is a literal, Apache SIS can do this transformation * only once before to start the filtering process instead of every time that the filter needs to be evaluated.</p> * - * <h2>Limitations</h2> - * <ul> - * <li>Current implementation does not scan the {@code "GEOMETRY_COLUMNS"} (from Simple Feature Access) - * or {@code "gpkg_content"} (from GeoPackage) tables for a default list of feature tables.</li> - * <li>If a parent feature contains association to other features, those other features are created - * at the same time as the parent feature (no lazy instantiation yet).</li> - * </ul> + * <p><b>Limitation:</b> if a parent feature contains association to other features (defined by foreigner keys), + * those other features are created at the same time as the parent feature. There is no lazy instantiation yet. + * Performances should be okay if each parent feature references only a small amount of children.</p> * * @author Johann Sorel (Geomatys) * @author Martin Desruisseaux (Geomatys) diff --git a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java index 6b38bac882..ff57652574 100644 --- a/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java +++ b/endorsed/src/org.apache.sis.storage.sql/test/org/apache/sis/storage/sql/SQLStoreTest.java @@ -122,6 +122,15 @@ public final class SQLStoreTest extends TestOnAllDatabases { }; } + /** + * Returns the storage connector to use for connecting to the test database. + * A new instance shall be created for each test, because each instance can + * be used only once. + */ + private static StorageConnector connector(final TestDatabase database) { + return new StorageConnector(database.source); + } + /** * Runs all tests on a single database software. A temporary schema is created at the beginning of this method * and deleted after all tests finished. The schema is created and populated by the {@code Features.sql} script. @@ -139,19 +148,19 @@ public final class SQLStoreTest extends TestOnAllDatabases { } scripts.add(resource("Features.sql")); database.executeSQL(scripts); - final StorageConnector connector = new StorageConnector(database.source); final ResourceDefinition table = ResourceDefinition.table(null, noschema ? null : SCHEMA, "Cities"); - testTableQuery(connector, table); + testTableQuery(connector(database), table); /* * Verify using SQL statements instead of tables. */ - verifyFetchCityTableAsQuery(connector); - verifyNestedSQLQuery(connector); - verifyLimitOffsetAndColumnSelectionFromQuery(connector); - verifyDistinctQuery(connector); + verifyFetchCityTableAsQuery(connector(database)); + verifyNestedSQLQuery(connector(database)); + verifyLimitOffsetAndColumnSelectionFromQuery(connector(database)); + verifyDistinctQuery(connector(database)); /* * Test on the table again, but with cyclic associations enabled. */ + final StorageConnector connector = connector(database); connector.setOption(SchemaModifier.OPTION, new SchemaModifier() { @Override public boolean isCyclicAssociationAllowed(TableReference dependency) { return true; diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java index c74d312130..d563aca6bb 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/StorageConnector.java @@ -47,6 +47,7 @@ import javax.imageio.ImageIO; import javax.imageio.stream.ImageInputStream; import javax.imageio.stream.ImageOutputStream; import java.sql.Connection; +import java.sql.DriverManager; import java.sql.SQLException; import javax.sql.DataSource; import org.apache.sis.util.Debug; @@ -57,6 +58,7 @@ import org.apache.sis.util.UnconvertibleObjectException; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.logging.Logging; import org.apache.sis.util.privy.Strings; +import org.apache.sis.util.privy.Constants; import org.apache.sis.util.collection.TreeTable; import org.apache.sis.util.collection.TableColumn; import org.apache.sis.util.collection.DefaultTreeTable; @@ -231,6 +233,7 @@ public class StorageConnector implements Serializable { add(InputStream.class, StorageConnector::createInputStream); add(OutputStream.class, StorageConnector::createOutputStream); add(Reader.class, StorageConnector::createReader); + add(DataSource.class, StorageConnector::createDataSource); add(Connection.class, StorageConnector::createConnection); add(ChannelDataInput.class, StorageConnector::createChannelDataInput); // Undocumented case (SIS internal) add(ChannelDataOutput.class, StorageConnector::createChannelDataOutput); // Undocumented case (SIS internal) @@ -756,7 +759,7 @@ public class StorageConnector implements Serializable { * <li>If the {@linkplain #getStorage() storage} object is an instance of the {@link Path}, * {@link File}, {@link URL}, {@link URI} or {@link CharSequence} types, * returns the string representation of their path.</li> - * <li>Otherwise this method returns {@code null}.</li> + * <li>Otherwise, this method returns {@code null}.</li> * </ul> * </li> * <li>{@link Path}, {@link URI}, {@link URL}, {@link File}: @@ -764,14 +767,14 @@ public class StorageConnector implements Serializable { * <li>If the {@linkplain #getStorage() storage} object is an instance of the {@link Path}, * {@link File}, {@link URL}, {@link URI} or {@link CharSequence} types and * that type can be converted to the requested type, returned the conversion result.</li> - * <li>Otherwise this method returns {@code null}.</li> + * <li>Otherwise, this method returns {@code null}.</li> * </ul> * </li> * <li>{@link ByteBuffer}: * <ul> * <li>If the {@linkplain #getStorage() storage} object can be obtained as described in bullet 2 of the * {@code DataInput} section below, then this method returns the associated byte buffer.</li> - * <li>Otherwise this method returns {@code null}.</li> + * <li>Otherwise, this method returns {@code null}.</li> * </ul> * </li> * <li>{@link DataInput}: @@ -779,16 +782,16 @@ public class StorageConnector implements Serializable { * <li>If the {@linkplain #getStorage() storage} object is already an instance of {@code DataInput} * (including the {@link ImageInputStream} and {@link ImageOutputStream} types), * then it is returned unchanged.</li> - * <li>Otherwise if the input is an instance of {@link ByteBuffer}, then an {@link ImageInputStream} + * <li>Otherwise, if the input is an instance of {@link ByteBuffer}, then an {@link ImageInputStream} * backed by a read-only view of that buffer is created when first needed and returned. * The properties (position, mark, limit) of the original buffer are unmodified.</li> - * <li>Otherwise if the input is an instance of {@link Path}, {@link File}, + * <li>Otherwise, if the input is an instance of {@link Path}, {@link File}, * {@link URI}, {@link URL}, {@link CharSequence}, {@link InputStream} or * {@link ReadableByteChannel}, then an {@link ImageInputStream} backed by a * {@link ByteBuffer} is created when first needed and returned.</li> - * <li>Otherwise if {@link ImageIO#createImageInputStream(Object)} returns a non-null value, + * <li>Otherwise, if {@link ImageIO#createImageInputStream(Object)} returns a non-null value, * then this value is cached and returned.</li> - * <li>Otherwise this method returns {@code null}.</li> + * <li>Otherwise, this method returns {@code null}.</li> * </ul> * </li> * <li>{@link ImageInputStream}: @@ -801,9 +804,9 @@ public class StorageConnector implements Serializable { * <ul> * <li>If the {@linkplain #getStorage() storage} object is already an instance of {@link InputStream}, * then it is returned unchanged.</li> - * <li>Otherwise if the above {@code ImageInputStream} can be created, + * <li>Otherwise, if the above {@code ImageInputStream} can be created, * returns a wrapper around that stream.</li> - * <li>Otherwise this method returns {@code null}.</li> + * <li>Otherwise, this method returns {@code null}.</li> * </ul> * </li> * <li>{@link Reader}: @@ -811,23 +814,23 @@ public class StorageConnector implements Serializable { * <li>If the {@linkplain #getStorage() storage} object is already an instance of {@link Reader}, * then it is returned unchanged.</li> * - * <li>Otherwise if the above {@code InputStream} can be created, returns an {@link InputStreamReader} + * <li>Otherwise, if the above {@code InputStream} can be created, returns an {@link InputStreamReader} * using the encoding specified by {@link OptionKey#ENCODING} if any, or using the system default * encoding otherwise.</li> - * <li>Otherwise this method returns {@code null}.</li> + * <li>Otherwise, this method returns {@code null}.</li> * </ul> * </li> * <li>{@link DataOutput}: * <ul> * <li>If the {@linkplain #getStorage() storage} object is already an instance of {@code DataOutput} * (including the {@link ImageOutputStream} type), then it is returned unchanged.</li> - * <li>Otherwise if the output is an instance of {@link Path}, {@link File}, + * <li>Otherwise, if the output is an instance of {@link Path}, {@link File}, * {@link URI}, {@link URL}, {@link CharSequence}, {@link OutputStream} or * {@link WritableByteChannel}, then an {@link ImageInputStream} backed by a * {@link ByteBuffer} is created when first needed and returned.</li> - * <li>Otherwise if {@link ImageIO#createImageOutputStream(Object)} returns a non-null value, + * <li>Otherwise, if {@link ImageIO#createImageOutputStream(Object)} returns a non-null value, * then this value is cached and returned.</li> - * <li>Otherwise this method returns {@code null}.</li> + * <li>Otherwise, this method returns {@code null}.</li> * </ul> * </li> * <li>{@link ImageOutputStream}: @@ -842,20 +845,30 @@ public class StorageConnector implements Serializable { * <li>Otherwise this method returns {@code null}.</li> * </ul> * </li> + * <li>{@link DataSource}: + * <ul> + * <li>If the {@linkplain #getStorage() storage} object is already an instance of {@link DataSource}, + * then it is returned unchanged.</li> + * <li>Otherwise, if the storage is convertible to an {@link URI} and the {@linkplain URI#getScheme() + * URI scheme} is "jdbc" (ignoring case), then a data source delegating to {@link DriverManager} + * is created when first needed and returned.</li> + * <li>Otherwise, this method returns {@code null}.</li> + * </ul> + * </li> * <li>{@link Connection}: * <ul> * <li>If the {@linkplain #getStorage() storage} object is already an instance of {@link Connection}, * then it is returned unchanged.</li> - * <li>Otherwise if the storage is an instance of {@link DataSource}, then a connection is obtained + * <li>Otherwise, if the storage is convertible to a {@link DataSource}, then a connection is obtained * when first needed and returned.</li> - * <li>Otherwise this method returns {@code null}.</li> + * <li>Otherwise, this method returns {@code null}.</li> * </ul> * </li> * <li>Any other types: * <ul> * <li>If the storage given at construction time is already an instance of the requested type, * returns it <i>as-is</i>.</li> - * <li>Otherwise this method throws {@link IllegalArgumentException}.</li> + * <li>Otherwise, this method throws {@link IllegalArgumentException}.</li> * </ul> * </li> * </ul> @@ -1405,17 +1418,36 @@ public class StorageConnector implements Serializable { return in; } + /** + * Creates a database source if possible. + * + * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per + * {@code StorageConnector} instance.</p> + * + * @return input/output, or {@code null} if none. + */ + private DataSource createDataSource() throws DataStoreException { + final URI uri = getStorageAs(URI.class); + if (uri != null && Constants.JDBC.equalsIgnoreCase(uri.getScheme())) { + final var source = new URLDataSource(uri); + addView(DataSource.class, source, null, (byte) 0); + return source; + } + return null; + } + /** * Creates a database connection if possible. * * <p>This method is one of the {@link #OPENERS} methods and should be invoked at most once per * {@code StorageConnector} instance.</p> * - * @return input, or {@code null} if none. + * @return input/output, or {@code null} if none. */ - private Connection createConnection() throws SQLException { - if (storage instanceof DataSource) { - final Connection c = ((DataSource) storage).getConnection(); + private Connection createConnection() throws SQLException, DataStoreException { + final DataSource source = getStorageAs(DataSource.class); + if (source != null) { + final Connection c = source.getConnection(); addView(Connection.class, c, null, (byte) 0); return c; } diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/URLDataSource.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/URLDataSource.java new file mode 100644 index 0000000000..a4857e14bd --- /dev/null +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/URLDataSource.java @@ -0,0 +1,198 @@ +/* + * Geotoolkit.org - An Open Source Java GIS Toolkit + * http://www.geotoolkit.org + * + * (C) 2009-2012, Open Source Geospatial Foundation (OSGeo) + * (C) 2009-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.apache.sis.storage; + +import java.net.URI; +import java.util.Set; +import java.util.HashSet; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.logging.LogRecord; +import java.io.PrintWriter; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.DatabaseMetaData; +import javax.sql.DataSource; +import org.apache.sis.util.Classes; +import org.apache.sis.util.logging.Logging; +import org.apache.sis.storage.internal.Resources; +import static org.apache.sis.storage.base.StoreUtilities.LOGGER; + + +/** + * A data source which gets the connections from a {@link DriverManager}. + * + * @author Martin Desruisseaux (Geomatys) + * @author Guilhem Legal (Geomatys) + */ +final class URLDataSource implements DataSource { + /** + * The driver names of the connection returned by {@code URLDataSource}. + * This is used for logging purposes only. + */ + private static final Set<String> DRIVERS = new HashSet<>(); + + /** + * The URL to use for connecting to the database. + * This field can not be {@code null}. + */ + private final String url; + + /** + * Creates a data source for the given URL. + * The value of {@link URI#getScheme()} should be {@code "jdbc"}, ignoring case. + * + * @param url The URL to use for connecting to the database. + */ + public URLDataSource(final URI url) { + this.url = url.toString(); + } + + /** + * Logs the driver version if this is the first time we get a connection for that driver. + */ + private static Connection log(final Connection connection) throws SQLException { + if (LOGGER.isLoggable(Level.CONFIG)) { + final DatabaseMetaData metadata = connection.getMetaData(); + final String name = metadata.getDriverName(); + final boolean log; + synchronized (DRIVERS) { + log = DRIVERS.add(name); + } + if (log) { + final LogRecord record = Resources.forLocale(null) + .getLogRecord(Level.CONFIG, Resources.Keys.UseJdbcDriverVersion_3, name, + metadata.getDriverMajorVersion(), metadata.getDriverMinorVersion()); + Logging.completeAndLog(LOGGER, StorageConnector.class, "getStorageAs", record); + } + } + return connection; + } + + /** + * Delegates to {@link DriverManager}. + * + * @throws SQLException If the connection can not be established. + */ + @Override + public Connection getConnection() throws SQLException { + return log(DriverManager.getConnection(url)); + } + + /** + * Delegates to {@link DriverManager}. + * + * @throws SQLException If the connection can not be established. + */ + @Override + public Connection getConnection(String username, String password) throws SQLException { + return log(DriverManager.getConnection(url, username, password)); + } + + /** + * Delegates to {@link DriverManager}. + */ + @Override + public PrintWriter getLogWriter() { + return DriverManager.getLogWriter(); + } + + /** + * Delegates to {@link DriverManager}. It is better to avoid + * calling this method since it has a system-wide effect. + */ + @Override + public void setLogWriter(final PrintWriter out) { + DriverManager.setLogWriter(out); + } + + /** + * Delegates to {@link DriverManager}. + */ + @Override + public int getLoginTimeout() { + return DriverManager.getLoginTimeout(); + } + + /** + * Delegates to {@link DriverManager}. It is better to avoid + * calling this method since it has a system-wide effect. + */ + @Override + public void setLoginTimeout(final int seconds) { + DriverManager.setLoginTimeout(seconds); + } + + /** + * Returns (@code false} in all cases, since this class is not a wrapper (omitting {@code DriverManager}). + */ + @Override + public boolean isWrapperFor(Class<?> iface) { + return false; + } + + /** + * Throws an exception in all cases, since this class is not a wrapper (omitting {@code DriverManager}). + * + * @param <T> Ignored. + */ + @Override + public <T> T unwrap(final Class<T> iface) throws SQLException { + throw new SQLException(); + } + + /** + * Compares this data source with the given object for equality. + * + * @param other The object to compare with this data source. + * @return {@code true} if both objects are equal. + */ + @Override + public boolean equals(final Object other) { + return (other instanceof URLDataSource) && url.equals(((URLDataSource) other).url); + } + + /** + * Returns a hash code value for this data source. + * + * @return A hash code value for this data source. + */ + @Override + public int hashCode() { + return url.hashCode() ^ 335483867; + } + + /** + * Returns a string representation of this data source. + */ + @Override + public String toString() { + return Classes.getShortClassName(this) + "[\"" + url + "\"]"; + } + + /** + * Returns the parent logger for this data source. + * + * @return the parent Logger for this data source + */ + @Override + public Logger getParentLogger() { + return LOGGER; + } +} diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java index 39f32775a0..efa18420f6 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.java @@ -458,6 +458,11 @@ public class Resources extends IndexedResourceBundle { */ public static final short UnknownFormatFor_1 = 14; + /** + * Using {0} JDBC driver version {1}.{2}. + */ + public static final short UseJdbcDriverVersion_3 = 82; + /** * Used only if this information is not encoded with the data. */ diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties index 4e433e2b72..b0eb6f1e38 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources.properties @@ -100,3 +100,4 @@ UnexpectedNumberOfCoordinates_4 = The \u201c{0}\u201d feature at {1} has a {3} UnfilteredData = Unfiltered data. UnknownFormatFor_1 = Format of \u201c{0}\u201d is not recognized. UsedOnlyIfNotEncoded = Used only if this information is not encoded with the data. +UseJdbcDriverVersion_3 = Using {0} JDBC driver version {1}.{2}. diff --git a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties index 6b742719e3..6b64a03e76 100644 --- a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties +++ b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/internal/Resources_fr.properties @@ -105,3 +105,4 @@ UnexpectedNumberOfCoordinates_4 = L\u2019entit\u00e9 nomm\u00e9e \u00ab\u202f{ UnfilteredData = Donn\u00e9es non-filtr\u00e9es. UnknownFormatFor_1 = Le format de \u00ab\u202f{0}\u202f\u00bb n\u2019est pas reconnu. UsedOnlyIfNotEncoded = Utilis\u00e9 seulement si cette information n\u2019est pas encod\u00e9e avec les donn\u00e9es. +UseJdbcDriverVersion_3 = Utilise le pilote JDBC \u00ab\u202f{0}\u202f\u00bb version {1}.{2}. diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java index 10179de258..de6ecf2cf6 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/privy/Constants.java @@ -100,7 +100,7 @@ public final class Constants extends Static { /** * The {@value} protocol. */ - public static final String HTTP = "http", HTTPS = "https"; + public static final String HTTP = "http", HTTPS = "https", JDBC = "jdbc"; /** * The {@value} code space.