/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2007-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.geotoolkit.referencing.factory.wkt; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.util.Map; import java.util.Set; import org.opengis.util.FactoryException; import org.opengis.metadata.citation.Citation; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.crs.CRSAuthorityFactory; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.geotoolkit.factory.Hints; import org.geotoolkit.io.TableWriter; import org.apache.sis.util.logging.Logging; import org.apache.sis.metadata.iso.DefaultIdentifier; import org.geotoolkit.metadata.Citations; import org.apache.sis.metadata.iso.citation.DefaultCitation; import org.apache.sis.util.iso.DefaultNameSpace; import org.geotoolkit.resources.Vocabulary; /** * An authority factory creating CRS from the {@value #TABLE} table in a spatial * SQL database. This class is called <code>Direct<u>Postgis</u>Factory</code> because * of some assumptions more suitable to PostGIS, like the default {@linkplain #getAuthority() * authority} if none were explicitly defined. But this class should be usable with other OGC * compliant spatial database as well. * <p> * This factory doesn't cache any result. Any call to a {@code createFoo} method will * trig a new WKT parsing. For adding caching service, this factory needs to be wrapped * in a {@link org.geotoolkit.referencing.factory.CachingAuthorityFactory} instance. The * {@link AuthorityFactoryProvider#createFromPostGIS AuthorityFactoryProvider} * convenience class can be used for that purpose. * * @author Martin Desruisseaux (Geomatys) * @version 3.10 * * @since 3.10 (derived from 2.5) * @module */ public class DirectPostgisFactory extends WKTParsingAuthorityFactory implements CRSAuthorityFactory { /** * The standard name of the table containing CRS definitions, which is {@value}. */ public static final String TABLE = "spatial_ref_sys"; /** * The primary key column, which is {@value}. */ public static final String PRIMARY_KEY = "srid"; /** * The standard name ({@value}) of the column containing the authority names. */ public static final String AUTHORITY_COLUMN = "auth_name"; /** * The standard name ({@value}) of the column containing the authority codes. */ public static final String CODE_COLUMN = "auth_srid"; /** * The standard name ({@value}) of the column containing the WKT definitions. */ public static final String WKT_COLUMN = "srtext"; /** * Authorities found in the database. Will be computed only when first needed. * Keys are authority names, and values are whatever the authority codes match * primary keys or not. */ private transient Map<String,Boolean> authorityUsePK; /** * Creates a factory using the given connection. The connection is * {@linkplain Connection#close() closed} when this factory is * {@linkplain #dispose(boolean) disposed}. * <p> * <b>Note:</b> we recommend to avoid keeping the connection open for a long time. An easy * way to get the connection created only when first needed and closed automatically after * a short timeout is to instantiate this {@code DirectPostgisFactory} class only in a * {@link org.geotoolkit.referencing.factory.ThreadedAuthorityFactory}. This approach also * gives concurrency and caching services in bonus. * * @param hints The hints, or {@code null} if none. * @param connection The connection to the database. * @throws SQLException If an error occurred while fetching metadata from the database. */ public DirectPostgisFactory(final Hints hints, final Connection connection) throws SQLException { super(hints, new SpatialRefSysMap(connection)); } /** * Returns a description of the underlying backing store. */ @Override public String getBackingStoreDescription() throws FactoryException { final Citation authority = getAuthority(); final TableWriter table = new TableWriter(null, " "); final Vocabulary resources = Vocabulary.getResources(null); CharSequence cs; if ((cs=authority.getEdition()) != null) { final String identifier = org.apache.sis.metadata.iso.citation.Citations.getIdentifier(authority); table.write(resources.getString(Vocabulary.Keys.VersionOf_1, identifier)); table.write(':'); table.nextColumn(); table.write(cs.toString()); table.nextLine(); } try { String s; final DatabaseMetaData metadata = ((SpatialRefSysMap) definitions).connection.getMetaData(); if ((s=metadata.getDatabaseProductName()) != null) { table.write(resources.getLabel(Vocabulary.Keys.DatabaseEngine)); table.nextColumn(); table.write(s); if ((s = metadata.getDatabaseProductVersion()) != null) { table.write(' '); table.write(resources.getString(Vocabulary.Keys.Version_1, s)); } table.nextLine(); } if ((s = metadata.getURL()) != null) { table.write(resources.getLabel(Vocabulary.Keys.DatabaseUrl)); table.nextColumn(); table.write(s); table.nextLine(); } } catch (SQLException exception) { throw databaseFailure(null, null, exception); } return table.toString(); } /** * Returns the authority which is responsible for the maintenance of the primary keys. * Note that <cite>primary keys</cite> are not necessarily the same than authority codes. * The primary keys are stored in the {@value #PRIMARY_KEY} column, while the authority * codes are defined by the {@value #AUTHORITY_COLUMN} : {@value #CODE_COLUMN} tuples. * <p> * The default implementation returns {@link Citations#POSTGIS} in all cases. * * @return The authority which is responsible for the maintenance of primary keys. * * @see #getPrimaryKey(Class, String) */ @Override public Citation getPrimaryKeyAuthority() { return Citations.POSTGIS; } /** * Returns all authority names declared in the CRS table. If some authorities use the same * codes than the primary key, then those authorities are returned first, ordered by the * most common ones. The last element in the array is the authority returned by * {@link #getPrimaryKeyAuthority()}. * * @return All authorities found in the database. */ @Override final synchronized Citation[] getAuthorities() { Citation[] authorities = this.authorities; if (authorities == null) { final Citation pkAuthority = getPrimaryKeyAuthority(); int count = 0; try { final Set<String> names = getAuthorityNames().keySet(); authorities = new Citation[names.size() + 1]; for (final String name : names) { final Citation authority = (name != null) ? Citations.fromName(name) : pkAuthority; /* * If the authority is not one of the predefined constants (in which case the * class would have been CitationConstant), then add the name to the list of * identifiers. */ if (authority.getClass() == DefaultCitation.class) { ((DefaultCitation) authority).getIdentifiers().add(new DefaultIdentifier(name)); } authorities[count++] = authority; } } catch (FactoryException exception) { Logging.unexpectedException(LOGGER, DirectPostgisFactory.class, "getAuthority", exception); authorities = new Citation[] {getPrimaryKeyAuthority()}; } authorities[count] = pkAuthority; this.authorities = authorities; } return authorities; } /** * Returns the authority names found in the database. Keys are authority names, * and values are whatever the authority codes match primary keys or not. * * @return All authority names found in the database. * @throws FactoryException if an access to the database failed. */ private Map<String,Boolean> getAuthorityNames() throws FactoryException { assert Thread.holdsLock(this); if (authorityUsePK == null) { try { authorityUsePK = ((SpatialRefSysMap) definitions).getAuthorityNames(); } catch (SQLException exception) { throw databaseFailure(null, null, exception); } } return authorityUsePK; } /** * Returns the authority codes defined in the database for the given type. * * @param category The type of objects to search for (typically * <code>{@linkplain CoordinateReferenceSystem}.class</code>). * @return The set of available codes. * @throws FactoryException if an error occurred while querying the database. */ @Override public synchronized Set<String> getAuthorityCodes(final Class<? extends IdentifiedObject> category) throws FactoryException { try { return ((SpatialRefSysMap) definitions).getAuthorityCodes(category); } catch (SQLException exception) { throw databaseFailure(category, null, exception); } } /** * Returns the primary key for the specified authority code. If the supplied code contains an * <cite>authority</cite> part as in {@code "EPSG:4326"}, then this method searches for a row * with the given authority ({@code "EPSG"}) in the {@value #AUTHORITY_COLUMN} column and the * given integer code ({@code 4326}) in the {@value #CODE_COLUMN} column. If such row is found, * then the value of its {@value #PRIMARY_KEY} column is returned. * <p> * If the supplied code does not contain an <cite>authority</cite> part (e.g. {@code "4326"}), * then this method parses the code as an integer. This is consistent with common practice * where the spatial CRS table contains entries from a single authority with primary keys * identical to the authority codes. This is also consistent with the codes returned by the * {@link #getAuthorityCodes(Class)} method. * * @param type The type of the object being created (usually * <code>{@linkplain CoordinateReferenceSystem}.class</code>). * @param code The authority code to convert to primary key value. * @return The primary key for the supplied code. There is no guarantee that this key exists * (this method may or may not query the database). * @throws NoSuchAuthorityCodeException if a code can't be parsed as an integer or can't * be found in the database. * @throws FactoryException if an error occurred while querying the database. * * @see #getPrimaryKeyAuthority() */ @Override public synchronized Integer getPrimaryKey(final Class<? extends IdentifiedObject> type, String code) throws NoSuchAuthorityCodeException, FactoryException { ensureNonNull("code", code); code = code.trim(); final int separator = code.lastIndexOf(DefaultNameSpace.DEFAULT_SEPARATOR); final String authority = (separator >= 0) ? code.substring(0, separator).trim() : ""; final String identifier = code.substring(separator+1).trim(); int srid; try { srid = Integer.parseInt(identifier); } catch (NumberFormatException cause) { NoSuchAuthorityCodeException e = noSuchAuthorityCode(IdentifiedObject.class, code); e.initCause(cause); throw e; } if (authority.isEmpty() || Boolean.TRUE.equals(getAuthorityNames().get(authority))) { return srid; } final Integer c; try { c = ((SpatialRefSysMap) definitions).getPrimaryKey(code, authority, srid); } catch (SQLException exception) { throw databaseFailure(type, code, exception); } if (c == null) { throw noSuchAuthorityCode(type, code); } return c; } /** * Closes the JDBC connection used by this factory. */ @Override protected synchronized void dispose(final boolean shutdown) { try { ((SpatialRefSysMap) definitions).dispose(); } catch (SQLException exception) { Logging.unexpectedException(null, DirectPostgisFactory.class, "dispose", exception); } authority = null; authorities = null; super.dispose(shutdown); } }