/* * Geotoolkit.org - An Open Source Java GIS Toolkit * http://www.geotoolkit.org * * (C) 2004-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.net.URL; import java.io.File; import java.io.InputStream; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Map; import java.util.List; import java.util.ArrayList; import java.util.Properties; import java.util.Collection; import java.util.Collections; import java.util.logging.Level; import java.util.logging.LogRecord; import org.opengis.metadata.citation.Citation; 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.geotoolkit.factory.Hints; import org.apache.sis.io.wkt.Symbols; import org.apache.sis.metadata.iso.citation.Citations; import org.geotoolkit.resources.Loggings; import org.apache.sis.util.logging.Logging; import static org.geotoolkit.util.collection.XCollections.addIfNonNull; /** * A CRS Authority Factory that manages object creation using a set of static strings from a * {@linkplain java.util.Properties property file}. This gives some of the benefits of using * the {@linkplain org.geotoolkit.referencing.factory.epsg.DirectEpsgFactory EPSG database} * in a portable property file (which must be provided by the users), or add new authorities. * See {@link org.geotoolkit.referencing.factory.epsg.PropertyEpsgFactory} for a subclass * specialized for the EPSG authority. * <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 {@link org.geotoolkit.referencing.factory.CachingAuthorityFactory}. * The {@link AuthorityFactoryProvider#createFromProperties AuthorityFactoryProvider} * convenience class can be used for that purpose. * * @author Jody Garnett (Refractions) * @author Rueben Schulz (UBC) * @author Martin Desruisseaux (IRD, Geomatys) * @version 3.10 * * @since 3.10 (derived from 2.1) * @module */ public class PropertyAuthorityFactory extends WKTParsingAuthorityFactory implements CRSAuthorityFactory, CSAuthorityFactory, DatumAuthorityFactory { /* * 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 does not allow addition of elements. */ /** * Creates a factory for the specified authorities using the definitions declared in the given * property file. There is usually only one authority, but more can be given when the objects * to create should have more than one {@linkplain CoordinateReferenceSystem#getIdentifiers * identifier}, each with the same code but different namespace. * See {@link WKTParsingAuthorityFactory} for more details. * * @param userHints * An optional set of hints, or {@code null} for the default ones. * @param definitionFile * URL to the definition file. This is typically a value returned by * {@link Class#getResource(String)}. * @param authorities * The organizations or parties responsible for definition and maintenance of the database. * @throws IOException * If the definitions can't be read. * * @since 3.00 */ public PropertyAuthorityFactory(final Hints userHints, final URL definitionFile, final Citation... authorities) throws IOException { this(userHints, authorities); if (definitionFile != null) { load(Collections.singleton(definitionFile)); } } /** * Creates a factory for the specified authorities using the definitions declared in the given * property files. There is usually only one file, but more are allowed. If there is more than * one file, their content will be merged. If the same key appears in more than one file, the * first occurrence is used. This is consistent with the usual rule saying that the first item * in a class-path has precedence. * * @param userHints * An optional set of hints, or {@code null} for the default ones. * @param definitionFiles * URL to the definition file(s). This is typically built from the * values returned by {@link ClassLoader#getResources(String)}. * @param authorities * The organizations or parties responsible for definition and maintenance of the database. * @throws IOException * If the definitions can't be read. * * @since 3.00 */ public PropertyAuthorityFactory(final Hints userHints, final Collection<URL> definitionFiles, final Citation... authorities) throws IOException { this(userHints, authorities); load(definitionFiles); } /** * Creates a factory using the definitions found in properties files loaded from a directory * and resources. The directory and the resources are both optional - one or both of them can * be omitted if the corresponding argument ({@code directoryKey} or {@code resourceLoader}) * is null. If they are non-null, then they are used as below: * * <ol> * <li><p>The file directory is specified as a hint stored under the {@code directoryKey}. * That key is usually {@link Hints#CRS_AUTHORITY_EXTRA_DIRECTORY}, but other keys are * allowed. If a value exists for that key and a file having the given {@code filename} * exists in that directory, then it is loaded.</p></li> * * <li><p>The resource directory (which may be a directory in a JAR file) is specified as * a class given by the {@code resourceLoader} argument. The resources are found in * the same way than {@link Class#getResource(String)} - with {@code filename} as the * string argument - except that more than one URLs will be obtained if possible.</p></li> * </ol> * * If definitions are found for the same keys in both cases, then the definitions found in * step 1 have precedence over the definitions found in case 2. * * @param userHints * An optional set of hints, or {@code null} for the default ones. * @param directoryKey * The key under which a directory may be stored in the hints map, or {@code null} if none. * If non-null, this value is typically {@link Hints#CRS_AUTHORITY_EXTRA_DIRECTORY}. * @param resourceLoader * The class to use for loading resources, or {@code null} if none. If non-null, the * class package determine the directory (potentially in a JAR file) where to look * for the resources, in the way documented at {@link Class#getResource(String)}. * @param filename * The name of the file to look in the directory if {@code directoryKey} is non-null, * or the name of the resources to load if {@code resourceLoader} is non-null. * @param authorities * The organizations or parties responsible for definition and maintenance of the database. * @throws IOException * If the definitions can't be read. * * @since 3.00 */ public PropertyAuthorityFactory(final Hints userHints, final Hints.FileKey directoryKey, final Class<?> resourceLoader, final String filename, final Citation... authorities) throws IOException { this(userHints, authorities); /* * Gets the directory, or null if none. If the user requested us to look for the * directory, then we must save what we found in the super.hints map even if that * directory is null - the fact that the directory was not found is significant. */ Path directory = null; if (directoryKey != null) { if (userHints != null) { Object hint = userHints.get(directoryKey); if (hint instanceof Path) { directory = (Path) hint; } else if(hint instanceof File) { directory = ((File) hint).toPath(); } else if (hint instanceof String) { directory = Paths.get((String) hint); } } } /* * Now build the list of URLs to load. First we look for a real file (not * a URL to a resource) in the directory supplied by the user, if any. */ String path = filename; // Will be used for formatting a "File not found" message if needed. final List<URL> definitionFiles = new ArrayList<>(4); if (directory != null) { try { final Path filePath = directory.resolve(filename); path = filePath.toString(); if (Files.isRegularFile(filePath)) { // May throw a SecurityException. definitionFiles.add(filePath.toUri().toURL()); } } catch (SecurityException exception) { // Considered unexpected because if the user provided excplicitly a // directory, we assume that he was expecting us to read its file. Logging.unexpectedException(LOGGER, PropertyAuthorityFactory.class, "<init>", exception); } } /* * Now search for URL to resources, which may be entries in a JAR file. * The same resources may be present in more than one JAR file. */ if (resourceLoader != null) { List<URL> more = Collections.emptyList(); path = resourceLoader.getName(); path = path.substring(0, path.lastIndexOf('.') + 1).replace('.', '/') + filename; try { final ClassLoader cl = resourceLoader.getClassLoader(); if (cl != null) { more = Collections.list(cl.getResources(path)); // Note: above list may still empty. } } catch (SecurityException exception) { // Not considered "unexpected" because this error is actually common in server // environment. The Class.getResource(...) method is the recommended method, // but has no API returning the enumeration of all occurrence of the file. Logging.recoverableException(LOGGER, PropertyAuthorityFactory.class, "<init>", exception); } definitionFiles.addAll(more); /* * If we have not been able to get the resources from the root (typically because * of security constraints), try to get the resource relative to the class. This * approach usually don't have security constraint. */ if (more.isEmpty()) { addIfNonNull(definitionFiles, resourceLoader.getResource(filename)); } } /* * Now the list of URLs to load is complete. If this list is empty, * logs a message for debugging purpose. */ if (definitionFiles.isEmpty()) { log(false, Loggings.Keys.CantReadFile_1, null, path); } load(definitionFiles); } /** * Creates an initially empty factory. This constructors is reserved for subclasses constructors * only. Subclasses must invoke {@link #load(Collection)} in their constructor in order to * populate the factory before it is used. * * @param userHints * An optional set of hints, or {@code null} for the default ones. * @param authorities * The organizations or parties responsible for definition and maintenance of the database. * * @since 3.00 */ @SuppressWarnings({"unchecked","rawtypes"}) protected PropertyAuthorityFactory(final Hints userHints, final Citation... authorities) { super(userHints, (Map) new Properties(), authorities); } /** * Loads the WKT strings from the given property files. This method should be * invoked from constructors only. Changes after construction are not allowed. * * @param definitionFiles URL to the definition file(s). This is typically built * from the values returned by {@link ClassLoader#getResources(String)}. * @throws IOException If the definition files can't be read. * * @since 3.00 */ protected synchronized void load(final Collection<URL> definitionFiles) throws IOException { ensureNonNull("definitionFiles", definitionFiles); @SuppressWarnings("unchecked") final Properties definitions = (Properties) (Map<?,?>) this.definitions; Properties currentFile = definitions; boolean containsAxis = false; for (final URL url : definitionFiles) { /* * If we have read other files before this one, use a temporary object. * It allows us to remove duplicated keys before they are merged. */ if (!currentFile.isEmpty()) { if (currentFile == definitions) { currentFile = new Properties(); } else { currentFile.clear(); } } try (InputStream in = url.openStream()) { currentFile.load(in); } if (!currentFile.isEmpty()) { // Note: the 'authorities' array length is never 0 (checked by the constructor). final String authority = String.valueOf(Citations.getIdentifier(authorities[0])); log(false, Loggings.Keys.UsingFileAsFactory_2, url, authority); } if (currentFile != definitions) { if (currentFile.keySet().removeAll(definitions.keySet())) { log(true, Loggings.Keys.DuplicatedContentInFile_1, url, null); } definitions.putAll(currentFile); } /* * Checks if the map we just loaded contains axis. We don't do that in the constructor * expecting a Map argument because we don't know if iteration over that map is costly, * neither if the user intend to modify it after construction. */ if (!containsAxis) { final Symbols s = Symbols.getDefault(); for (final Object wkt : definitions.values()) { if (s.containsAxis((String) wkt)) { containsAxis = true; break; } } } } } /** * Logs a message using the given resource key and using the given URL as a value. * * @param warning {@code true} if the message should be logged as a warning. * @param key The key of the internationalized string to fetch. * @param url The URL to put in the message, or {@code null} if none. * @param ext An additional parameter to put in the message, or {@code null} if none. */ private static void log(final boolean warning, final short key, final URL url, final Object ext) { final Level level = warning ? Level.WARNING : Level.CONFIG; final LogRecord record; if (url != null) { String path = url.getPath(); path = path.substring(path.lastIndexOf('/') + 1); if (ext != null) { record = Loggings.format(level, key, path, ext); } else { record = Loggings.format(level, key, path); } } else { record = Loggings.format(level, key, ext); } record.setSourceClassName(PropertyAuthorityFactory.class.getName()); record.setSourceMethodName("load"); record.setLoggerName(LOGGER.getName()); LOGGER.log(record); } /** * Disposes the resources used by this factory. * * @param shutdown {@code false} for normal disposal, or {@code true} if * this method is invoked during the process of a JVM shutdown. */ @Override protected synchronized void dispose(final boolean shutdown) { // The call to definitions.clear() is not performed in the super-class because we // don't want to touch user-supplied collection. It could be backed by a database. definitions.clear(); super.dispose(shutdown); } }