/* * GeoTools - The Open Source Java GIS Toolkit * http://geotools.org * * (C) 2004-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; import java.io.InputStream; import java.io.IOException; import java.net.URL; import java.text.ParseException; import java.util.Collections; import java.util.Map; import java.util.HashMap; import java.util.Properties; import java.util.Set; import org.opengis.metadata.citation.Citation; import org.opengis.referencing.FactoryException; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.NoSuchAuthorityCodeException; import org.opengis.referencing.cs.CSAuthorityFactory; import org.opengis.referencing.crs.CRSAuthorityFactory; import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.datum.DatumAuthorityFactory; import org.opengis.util.InternationalString; import org.opengis.util.GenericName; import org.geotools.factory.Hints; import org.geotools.referencing.wkt.Symbols; import org.geotools.referencing.NamedIdentifier; import org.geotools.metadata.iso.citation.Citations; import org.geotools.util.DerivedSet; import org.geotools.util.NameFactory; import org.geotools.util.SimpleInternationalString; import org.geotools.resources.i18n.Errors; import org.geotools.resources.i18n.ErrorKeys; /** * Default implementation for a coordinate reference system authority factory * backed by a property file. This gives some of the benificts of using the * {@linkplain org.geotools.referencing.factory.epsg.DirectEpsgFactory EPSG database backed * authority factory} (for example), in a portable property file. * <p> * This factory doesn't cache any result. Any call to a {@code createFoo} method will trig a new * WKT parsing. For caching, this factory should be wrapped in some buffered factory like * {@link BufferedAuthorityFactory}. * * @since 2.1 * * @source $URL$ * @version $Id$ * @author Jody Garnett * @author Rueben Schulz * @author Martin Desruisseaux */ public class PropertyAuthorityFactory extends DirectAuthorityFactory implements CRSAuthorityFactory, CSAuthorityFactory, DatumAuthorityFactory { /** * The authority for this factory. */ private final Citation authority; /** * Same as {@link #authority}, but may contains more than one elements in some particular * cases. */ private final Citation[] authorities; /** * The properties object for our properties file. Keys are the authority * code for a coordinate reference system and the associated value is a * WKT string for the CRS. * <p> * It is technically possible to add or remove elements after they have been * loaded by the constructor. However if such modification are made, then we * should update {@link Hints#FORCE_LONGITUDE_FIRST_AXIS_ORDER} accordingly. * It may be an issue since hints are supposed to be immutable after factory * construction. For now, this class do not allow addition of elements. */ private final Properties definitions = new Properties(); /** * An unmodifiable view of the authority keys. This view is always up to date * even if entries are added or removed in the {@linkplain #definitions} map. */ @SuppressWarnings("unchecked") private final Set<String> codes = Collections.unmodifiableSet((Set) definitions.keySet()); /** * Views of {@link #codes} for different types. Views will be constructed only when first * needed. View are always up to date even if entries are added or removed in the * {@linkplain #definitions} map. */ private transient Map<Class<? extends IdentifiedObject>, Set<String>> filteredCodes; /** * A WKT parser. */ private transient Parser parser; /** * Creates a factory for the specified authority from the specified file. * * @param factories The underlying factories used for objects creation. * @param authority The organization or party responsible for definition and maintenance of * the database. * @param definitions URL to the definition file. * @throws IOException if the definitions can't be read. */ public PropertyAuthorityFactory(final ReferencingFactoryContainer factories, final Citation authority, final URL definitions) throws IOException { this(factories, new Citation[] {authority}, definitions); } /** * Creates a factory for the specified authorities from the specified file. More than * one authority may be specified when the CRS to create should have more than one * {@linkplain CoordinateReferenceSystem#getIdentifiers identifier}, each with the same * code but different namespace. For example a * {@linkplain org.geotools.referencing.factory.epsg.EsriExtension factory for CRS defined * by ESRI} uses the {@code "ESRI"} namespace, but also the {@code "EPSG"} namespace * because those CRS are used as extension of the EPSG database. Concequently, the same * CRS can be identified as {@code "ESRI:53001"} and {@code "EPSG:53001"}, where * {@code "53001"} is a unused code in the official EPSG database. * * @param factories The underlying factories used for objects creation. * @param authorities The organizations or party responsible for definition * and maintenance of the database. * @param definitions URL to the definition file. * @throws IOException if the definitions can't be read. * * @since 2.4 */ public PropertyAuthorityFactory(final ReferencingFactoryContainer factories, final Citation[] authorities, final URL definitions) throws IOException { super(factories, MINIMUM_PRIORITY + 10); // The following hints have no effect on this class behaviour, // but tell to the user what this factory do about axis order. // TODO: Following line should not be commented-out. // See http://jira.codehaus.org/browse/GEOT-1699 // hints.put(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER, Boolean.FALSE); hints.put(Hints.FORCE_STANDARD_AXIS_DIRECTIONS, Boolean.FALSE); hints.put(Hints.FORCE_STANDARD_AXIS_UNITS, Boolean.FALSE); ensureNonNull("authorities", authorities); if (authorities.length == 0) { throw new IllegalArgumentException(Errors.format(ErrorKeys.EMPTY_ARRAY)); } this.authorities = authorities.clone(); authority = authorities[0]; ensureNonNull("authority", authority); final InputStream in = definitions.openStream(); this.definitions.load(in); in.close(); /* * If the WKT do not contains any AXIS[...] element, then every CRS will be created with * the default (longitude,latitude) axis order. In such case this factory is insensitive * to the FORCE_LONGITUDE_FIRST_AXIS_ORDER hint (i.e. every CRS to be created by this * instance are invariant under the above-cited hint value) and we can remove it from * the hint map. Removing this hint allow the CRS.decode(..., true) convenience method * to find this factory (GEOT-1175). */ final Symbols s = Symbols.DEFAULT; for (final Object wkt : this.definitions.values()) { if (s.containsAxis((String) wkt)) { LOGGER.warning("Axis elements found in a wkt definition, the force longitude " + "first axis order hint might not be respected:\n" + wkt); return; } } hints.remove(Hints.FORCE_LONGITUDE_FIRST_AXIS_ORDER); } /** * Returns the organization or party responsible for definition and maintenance of the * database. */ public Citation getAuthority() { return authority; } /** * Returns the set of authority codes of the given type. The type * argument specify the base class. For example if this factory is * an instance of CRSAuthorityFactory, then: * <p> * <ul> * <li>{@code CoordinateReferenceSystem.class} asks for all authority codes accepted by * {@link #createGeographicCRS createGeographicCRS}, * {@link #createProjectedCRS createProjectedCRS}, * {@link #createVerticalCRS createVerticalCRS}, * {@link #createTemporalCRS createTemporalCRS} * and their friends.</li> * <li>{@code ProjectedCRS.class} asks only for authority codes accepted by * {@link #createProjectedCRS createProjectedCRS}.</li> * </ul> * * The default implementaiton filters the set of codes based on the * {@code "PROJCS"} and {@code "GEOGCS"} at the start of the WKT strings. * * @param type The spatial reference objects type (may be {@code Object.class}). * @return The set of authority codes for spatial reference objects of the given type. * If this factory doesn't contains any object of the given type, then this method * returns an empty set. * @throws FactoryException if access to the underlying database failed. */ public Set<String> getAuthorityCodes(final Class<? extends IdentifiedObject> type) throws FactoryException { if (type==null || type.isAssignableFrom(IdentifiedObject.class)) { return codes; } if (filteredCodes == null) { filteredCodes = new HashMap<Class<? extends IdentifiedObject>, Set<String>>(); } synchronized (filteredCodes) { Set<String> filtered = filteredCodes.get(type); if (filtered == null) { @SuppressWarnings("unchecked") final Map<String,String> map = (Map) definitions; filtered = new Codes(map, type); filteredCodes.put(type, filtered); } return filtered; } } /** * The set of codes for a specific type of CRS. This set filter the codes set in the * enclosing {@link PropertyAuthorityFactory} in order to keep only the codes for the * specified type. Filtering is performed on the fly. Consequently, this set is cheap * if the user just want to check for the existence of a particular code. */ private static final class Codes extends DerivedSet<String, String> { /** * For cross-version compatibility. */ private static final long serialVersionUID = 2681905294171687900L; /** * The spatial reference objects type. */ private final Class<? extends IdentifiedObject> type; /** * The reference to {@link PropertyAuthorityFactory#definitions}. */ private final Map<String,String> definitions; /** * Constructs a set of codes for the specified type. */ public Codes(final Map<String,String> definitions, final Class<? extends IdentifiedObject> type) { super(definitions.keySet(), String.class); this.definitions = definitions; this.type = type; } /** * Returns the code if the associated key is of the expected type, or {@code null} * otherwise. */ protected String baseToDerived(final String key) { final String wkt = definitions.get(key); final int length = wkt.length(); int i=0; while (i<length && Character.isJavaIdentifierPart(wkt.charAt(i))) i++; Class<?> candidate = Parser.getClassOf(wkt.substring(0,i)); if (candidate == null) { candidate = IdentifiedObject.class; } return type.isAssignableFrom(candidate) ? key : null; } /** * Transforms a value in this set to a value in the base set. */ protected String derivedToBase(final String element) { return element; } } /** * Returns the Well Know Text from a code. * * @param code Value allocated by authority. * @return The Well Know Text (WKT) for the specified code. * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found. */ public String getWKT(final String code) throws NoSuchAuthorityCodeException { ensureNonNull("code", code); final String wkt = definitions.getProperty(trimAuthority(code)); if (wkt == null) { throw noSuchAuthorityCode(IdentifiedObject.class, code); } return wkt.trim(); } /** * Gets a description of the object corresponding to a code. * * @param code Value allocated by authority. * @return A description of the object, or {@code null} if the object * corresponding to the specified {@code code} has no description. * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found. * @throws FactoryException if the query failed for some other reason. */ public InternationalString getDescriptionText(final String code) throws NoSuchAuthorityCodeException, FactoryException { final String wkt = getWKT(code); int start = wkt.indexOf('"'); if (start >= 0) { final int end = wkt.indexOf('"', ++start); if (end >= 0) { return new SimpleInternationalString(wkt.substring(start, end).trim()); } } return null; } /** * Returns the parser. */ private Parser getParser() { if (parser == null) { parser = new Parser(); } return parser; } /** * Returns an arbitrary object from a code. If the object type is know at compile time, it is * recommended to invoke the most precise method instead of this one. * * @param code Value allocated by authority. * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found. * @throws FactoryException if the object creation failed for some other reason. */ @Override public IdentifiedObject createObject(final String code) throws NoSuchAuthorityCodeException, FactoryException { final String wkt = getWKT(code); final Parser parser = getParser(); try { synchronized (parser) { parser.code = code; return (IdentifiedObject) parser.parseObject(wkt); } } catch (ParseException exception) { throw new FactoryException(exception); } } /** * Returns a coordinate reference system from a code. If the object type is know at compile * time, it is recommended to invoke the most precise method instead of this one. * * @param code Value allocated by authority. * @throws NoSuchAuthorityCodeException if the specified {@code code} was not found. * @throws FactoryException if the object creation failed for some other reason. */ @Override public CoordinateReferenceSystem createCoordinateReferenceSystem(final String code) throws NoSuchAuthorityCodeException, FactoryException { final String wkt = getWKT(code); final Parser parser = getParser(); try { synchronized (parser) { parser.code = code; // parseCoordinateReferenceSystem provides a slightly faster path than parseObject. return parser.parseCoordinateReferenceSystem(wkt); } } catch (ParseException exception) { throw new FactoryException(exception); } } /** * Trims the authority scope, if present. If more than one authority were given at * {@linkplain #PropertyAuthorityFactory(ReferencingFactoryContainer, Citation[], URL) * construction time}, then any of them may appears as the scope in the supplied code. * * @param code The code to trim. * @return The code without the authority scope. */ @Override protected String trimAuthority(String code) { code = code.trim(); final GenericName name = NameFactory.create(code); final GenericName scope = name.scope().name(); if (scope == null) { return code; } final String candidate = scope.toString(); for (int i=0; i<authorities.length; i++) { if (Citations.identifierMatches(authorities[i], candidate)) { return name.tip().toString().trim(); } } return code; } /** * The WKT parser for this authority factory. This parser add automatically the authority * code if it was not explicitly specified in the WKT. */ private final class Parser extends org.geotools.referencing.wkt.Parser { /** * For cross-version compatibility. */ private static final long serialVersionUID = -5910561042299146066L; /** * The authority code for the WKT to be parsed. */ String code; /** * Creates the parser. */ public Parser() { super(Symbols.DEFAULT, factories); } /** * Add the authority code to the specified properties, if not already present. */ @Override protected Map<String,Object> alterProperties(Map<String,Object> properties) { Object candidate = properties.get(IdentifiedObject.IDENTIFIERS_KEY); if (candidate == null && code != null) { properties = new HashMap<String,Object>(properties); code = trimAuthority(code); final Object identifiers; if (authorities.length <= 1) { identifiers = new NamedIdentifier(authority, code); } else { final NamedIdentifier[] ids = new NamedIdentifier[authorities.length]; for (int i=0; i<ids.length; i++) { ids[i] = new NamedIdentifier(authorities[i], code); } identifiers = ids; } properties.put(IdentifiedObject.IDENTIFIERS_KEY, identifiers); } return super.alterProperties(properties); } } }