001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2005-2006, GeoTools Project Managment Committee (PMC)
005: *
006: * This library is free software; you can redistribute it and/or
007: * modify it under the terms of the GNU Lesser General Public
008: * License as published by the Free Software Foundation;
009: * version 2.1 of the License.
010: *
011: * This library is distributed in the hope that it will be useful,
012: * but WITHOUT ANY WARRANTY; without even the implied warranty of
013: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
014: * Lesser General Public License for more details.
015: */
016: package org.geotools.referencing.factory.epsg;
017:
018: // J2SE dependencies
019: import java.io.File;
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.io.OutputStream;
023: import java.io.BufferedReader;
024: import java.io.FileInputStream;
025: import java.io.FileOutputStream;
026: import java.io.InputStreamReader;
027: import java.sql.ResultSet;
028: import java.sql.Statement;
029: import java.sql.Connection;
030: import java.sql.SQLException;
031: import javax.sql.DataSource;
032: import java.util.Properties;
033: import java.util.logging.Level;
034: import java.util.logging.Logger;
035:
036: // Geotools dependencies
037: import org.geotools.util.Version;
038: import org.geotools.factory.Hints;
039: import org.geotools.resources.i18n.Errors;
040: import org.geotools.resources.i18n.ErrorKeys;
041: import org.geotools.resources.i18n.Logging;
042: import org.geotools.resources.i18n.LoggingKeys;
043: import org.geotools.referencing.factory.AbstractAuthorityFactory;
044:
045: // HSQL dependencies
046: import org.hsqldb.jdbc.jdbcDataSource;
047:
048: /**
049: * Connection to the EPSG database in HSQL database engine format using JDBC. The EPSG
050: * database can be downloaded from <A HREF="http://www.epsg.org">http://www.epsg.org</A>.
051: * The SQL scripts (modified for the HSQL syntax as <A HREF="doc-files/HSQL.html">explained
052: * here</A>) are bundled into this plugin. The database version is given in the
053: * {@linkplain org.opengis.metadata.citation.Citation#getEdition edition attribute}
054: * of the {@linkplain org.opengis.referencing.AuthorityFactory#getAuthority authority}.
055: * The HSQL database is read only.
056: * <P>
057: * <H3>Implementation note</H3>
058: * The SQL scripts are executed the first time a connection is required. The database
059: * is then created as cached tables ({@code HSQL.properties} and {@code HSQL.data} files)
060: * in a temporary directory. Future connections to the EPSG database while reuse the cached
061: * tables, if available. Otherwise, the scripts will be executed again in order to recreate
062: * them.
063: * <p>
064: * If the EPSG database should be created in a different directory (or already exists in that
065: * directory), it may be specified as a {@linkplain System#getProperty(String) system property}
066: * nammed {@value #DIRECTORY_KEY}.
067: *
068: * @since 2.4
069: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/plugin/epsg-hsql/src/main/java/org/geotools/referencing/factory/epsg/FactoryOnHSQL.java $
070: * @version $Id: FactoryOnHSQL.java 27863 2007-11-12 20:34:34Z desruisseaux $
071: * @author Martin Desruisseaux
072: * @author Didier Richard
073: *
074: * @deprecated Will be renamed as {@code ThreadedHsqlEpsgFactory} in Geotools 2.5.
075: */
076: public class FactoryOnHSQL extends DefaultFactory {
077: /**
078: * Current version of EPSG-HSQL plugin. This is usually the same version number than the
079: * one in the EPSG database bundled in this plugin. However this field may contains
080: * additional minor version number if there is some changes related to the EPSG-HSQL
081: * plugin rather then the EPSG database itself (for example additional database index).
082: *
083: * @since 2.4
084: */
085: public static final Version VERSION = new Version("6.12.0");
086:
087: /**
088: * The key for fetching the database directory from {@linkplain System#getProperty(String)
089: * system properties}.
090: */
091: public static final String DIRECTORY_KEY = "EPSG-HSQL.directory";
092:
093: /**
094: * The name of the SQL file to read in order to create the cached database.
095: */
096: private static final String SQL_FILE = "EPSG.sql";
097:
098: /**
099: * The database name.
100: */
101: public static final String DATABASE_NAME = "EPSG";
102:
103: /**
104: * The prefix to put in front of URL to the database.
105: */
106: private static final String PREFIX = "jdbc:hsqldb:file:";
107:
108: /**
109: * The logger name.
110: */
111: private static final String LOGGER = "org.geotools.referencing.factory.epsg";
112:
113: /**
114: * Creates a new instance of this factory. If the {@value #DIRECTORY_KEY}
115: * {@linkplain System#getProperty(String) system property} is defined and contains
116: * the name of a directory with a valid {@linkplain File#getParent parent}, then the
117: * {@value #DATABASE_NAME} database will be saved in that directory. Otherwise, a
118: * temporary directory will be used.
119: */
120: public FactoryOnHSQL() {
121: this (null);
122: }
123:
124: /**
125: * Creates a new instance of this data source using the specified hints. The priority
126: * is set to a lower value than the {@linkplain FactoryOnAccess}'s one in order to give
127: * precedence to the Access-backed database, if presents. Priorities are set that way
128: * because:
129: * <ul>
130: * <li>The MS-Access format is the primary EPSG database format.</li>
131: * <li>If a user downloads the MS-Access database himself, he probably wants to use it.</li>
132: * </ul>
133: */
134: public FactoryOnHSQL(final Hints hints) {
135: super (hints, PRIORITY + 1);
136: }
137:
138: /**
139: * Returns the default directory for the EPSG database. If the {@value #DIRECTORY_KEY}
140: * {@linkplain System#getProperty(String) system property} is defined and contains the
141: * name of a directory with a valid {@linkplain File#getParent parent}, then the
142: * {@value #DATABASE_NAME} database will be saved in that directory. Otherwise,
143: * a temporary directory will be used.
144: */
145: private static File getDirectory() {
146: try {
147: final String property = System.getProperty(DIRECTORY_KEY);
148: if (property != null) {
149: final File directory = new File(property);
150: /*
151: * Creates the directory if needed (mkdir), but NOT the parent directories (mkdirs)
152: * because a missing parent directory may be a symptom of an installation problem.
153: * For example if 'directory' is a subdirectory in the temporary directory (~/tmp/),
154: * this temporary directory should already exists. If it doesn't, an administrator
155: * should probably looks at this problem.
156: */
157: if (directory.isDirectory() || directory.mkdir()) {
158: return directory;
159: }
160: }
161: } catch (SecurityException e) {
162: /*
163: * Can't fetch the base directory from system properties.
164: * Fallback on the default temporary directory.
165: */
166: }
167: return getTemporaryDirectory();
168: }
169:
170: /**
171: * Returns the directory to uses in the temporary directory folder.
172: */
173: private static File getTemporaryDirectory() {
174: File directory = new File(System.getProperty("java.io.tmpdir",
175: "."), "Geotools");
176: if (directory.isDirectory() || directory.mkdir()) {
177: directory = new File(directory, "Databases/HSQL");
178: if (directory.isDirectory() || directory.mkdirs()) {
179: return directory;
180: }
181: }
182: return null;
183: }
184:
185: /**
186: * Extract the directory from the specified data source, or {@code null} if this
187: * information is not available.
188: */
189: private static File getDirectory(final DataSource source) {
190: if (source instanceof jdbcDataSource) {
191: String path = ((jdbcDataSource) source).getDatabase();
192: if (path != null
193: && PREFIX.regionMatches(true, 0, path, 0, PREFIX
194: .length())) {
195: path = path.substring(PREFIX.length());
196: return new File(path).getParentFile();
197: }
198: }
199: return null;
200: }
201:
202: /**
203: * Returns a data source for the HSQL database.
204: */
205: protected DataSource createDataSource() throws SQLException {
206: DataSource candidate = super .createDataSource();
207: if (candidate instanceof jdbcDataSource) {
208: return candidate;
209: }
210: final jdbcDataSource source = new jdbcDataSource();
211: File directory = getDirectory();
212: if (directory != null) {
213: /*
214: * Constructs the full path to the HSQL database. Note: we do not use
215: * File.toURI() because HSQL doesn't seem to expect an encoded URL
216: * (e.g. "%20" instead of spaces).
217: */
218: final StringBuffer url = new StringBuffer(PREFIX);
219: final String path = directory.getAbsolutePath().replace(
220: File.separatorChar, '/');
221: if (path.length() == 0 || path.charAt(0) != '/') {
222: url.append('/');
223: }
224: url.append(path);
225: if (url.charAt(url.length() - 1) != '/') {
226: url.append('/');
227: }
228: url.append(DATABASE_NAME);
229: source.setDatabase(url.toString());
230: assert directory.equals(getDirectory(source)) : url;
231: }
232: /*
233: * If the temporary directory do not exists or can't be created, lets the 'database'
234: * attribute unset. If the user do not set it explicitly (through JNDI or by overrding
235: * this method), an exception will be thrown when 'createBackingStore()' will be invoked.
236: */
237: source.setUser("SA"); // System administrator. No password.
238: return source;
239: }
240:
241: /**
242: * Returns {@code true} if the database contains data. This method returns {@code false}
243: * if an empty EPSG database has been automatically created by HSQL and not yet populated.
244: */
245: private static boolean dataExists(final Connection connection)
246: throws SQLException {
247: final ResultSet tables = connection.getMetaData().getTables(
248: null, null, "EPSG_%", new String[] { "TABLE" });
249: final boolean exists = tables.next();
250: tables.close();
251: return exists;
252: }
253:
254: /**
255: * Compares the {@code "epsg.version"} property in the specified file with the expected
256: * {@link #VERSION}. If the version found in the property file is equals or higher than
257: * the expected one, then this method do nothing. Otherwise or if no version information
258: * is found in the property file, then this method clean the temporary directory
259: * containing the cached database.
260: */
261: private static void deleteIfOutdated(final File directory,
262: final File propertyFile) {
263: if (directory == null
264: || !directory.equals(getTemporaryDirectory())) {
265: /*
266: * Never touch to the directory if it is not in the temporary directory.
267: * It may be a user file!
268: */
269: return;
270: }
271: if (propertyFile.isFile())
272: try {
273: final InputStream propertyIn = new FileInputStream(
274: propertyFile);
275: final Properties properties = new Properties();
276: properties.load(propertyIn);
277: propertyIn.close();
278: final String version = properties
279: .getProperty("epsg.version");
280: if (version != null) {
281: if (new Version(version).compareTo(VERSION) >= 0) {
282: return;
283: }
284: }
285: } catch (IOException exception) {
286: /*
287: * Failure to read the property file. This is just a warning, not an error, because
288: * we will attempt to rebuild the whole database. Note: "createBackingStore" is the
289: * public method that invoked this method, so we use it for the logging message.
290: */
291: org.geotools.util.logging.Logging.unexpectedException(
292: LOGGER, FactoryOnHSQL.class,
293: "createBackingStore", exception);
294: }
295: delete(directory);
296: }
297:
298: /**
299: * Deletes the specified directory and all sub-directories. Used for
300: * cleaning the temporary directory containing the cached database only.
301: */
302: private static void delete(final File directory) {
303: if (directory != null) {
304: final File[] files = directory.listFiles();
305: if (files != null) {
306: for (int i = 0; i < files.length; i++) {
307: delete(files[i]);
308: }
309: }
310: directory.delete();
311: }
312: }
313:
314: /**
315: * Returns the backing-store factory for HSQL syntax. If the cached tables are not available,
316: * they will be created now from the SQL scripts bundled in this plugin.
317: *
318: * @param hints A map of hints, including the low-level factories to use for CRS creation.
319: * @return The EPSG factory using HSQL syntax.
320: * @throws SQLException if connection to the database failed.
321: */
322: protected AbstractAuthorityFactory createBackingStore(
323: final Hints hints) throws SQLException {
324: final DataSource source = getDataSource();
325: final File directory = getDirectory(source);
326: final File propertyFile = new File(directory, DATABASE_NAME
327: + ".properties");
328: deleteIfOutdated(directory, propertyFile);
329: Connection connection = source.getConnection();
330: if (!dataExists(connection)) {
331: /*
332: * HSQL has created automatically an empty database. We need to populate it.
333: * Executes the SQL scripts bundled in the JAR. In theory, each line contains
334: * a full SQL statement. For this plugin however, we have compressed "INSERT
335: * INTO" statements using Compactor class in this package.
336: */
337: org.geotools.util.logging.Logging
338: .getLogger(LOGGER)
339: .log(
340: Logging
341: .format(
342: Level.INFO,
343: LoggingKeys.CREATING_CACHED_EPSG_DATABASE_$1,
344: VERSION));
345: final Statement statement = connection.createStatement();
346: try {
347: final BufferedReader in = new BufferedReader(
348: new InputStreamReader(FactoryOnHSQL.class
349: .getResourceAsStream(SQL_FILE),
350: "ISO-8859-1"));
351: StringBuffer insertStatement = null;
352: String line;
353: while ((line = in.readLine()) != null) {
354: line = line.trim();
355: final int length = line.length();
356: if (length != 0) {
357: if (line.startsWith("INSERT INTO")) {
358: /*
359: * We are about to insert many rows into a single table.
360: * The row values appear in next lines; the current line
361: * should stop right after the VALUES keyword.
362: */
363: insertStatement = new StringBuffer(line);
364: continue;
365: }
366: if (insertStatement != null) {
367: /*
368: * We are about to insert a row. Prepend the "INSERT INTO"
369: * statement and check if we will have more rows to insert
370: * after this one.
371: */
372: final int values = insertStatement.length();
373: insertStatement.append(line);
374: final boolean hasMore = (line
375: .charAt(length - 1) == ',');
376: if (hasMore) {
377: insertStatement
378: .setLength(insertStatement
379: .length() - 1);
380: }
381: line = insertStatement.toString();
382: insertStatement.setLength(values);
383: if (!hasMore) {
384: insertStatement = null;
385: }
386: }
387: statement.execute(line);
388: }
389: }
390: in.close();
391: /*
392: * The database has been fully created. Now, make it read-only.
393: */
394: if (directory != null) {
395: final InputStream propertyIn = new FileInputStream(
396: propertyFile);
397: final Properties properties = new Properties();
398: properties.load(propertyIn);
399: propertyIn.close();
400: properties.put("epsg.version", VERSION.toString());
401: properties.put("readonly", "true");
402: final OutputStream out = new FileOutputStream(
403: propertyFile);
404: properties.store(out, "EPSG database on HSQL");
405: out.close();
406:
407: final File backup = new File(directory,
408: DATABASE_NAME + ".backup");
409: if (backup.exists()) {
410: backup.delete();
411: }
412: }
413: } catch (IOException exception) {
414: statement.close();
415: SQLException e = new SQLException(Errors.format(
416: ErrorKeys.CANT_READ_$1, SQL_FILE));
417: e.initCause(exception);
418: throw e;
419: }
420: statement.close();
421: connection.close();
422: connection = source.getConnection();
423: assert dataExists(connection);
424: }
425: return new FactoryUsingHSQL(hints, connection);
426: }
427: }
|