001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2005-2006, GeoTools Project Managment Committee (PMC)
005: * (C) 2005, Institut de Recherche pour le Développement
006: *
007: * This library is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU Lesser General Public
009: * License as published by the Free Software Foundation;
010: * version 2.1 of the License.
011: *
012: * This library is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015: * Lesser General Public License for more details.
016: */
017: package org.geotools.referencing.factory.epsg;
018:
019: // J2SE dependencies
020: import java.util.Set;
021: import java.util.AbstractSet;
022: import java.util.AbstractMap;
023: import java.util.Collections;
024: import java.util.LinkedHashSet;
025: import java.util.NoSuchElementException;
026: import java.util.logging.Level;
027: import java.util.logging.Logger;
028: import java.util.logging.LogRecord;
029: import java.io.Serializable;
030: import java.io.ObjectStreamException;
031: import java.sql.Connection;
032: import java.sql.PreparedStatement;
033: import java.sql.ResultSet;
034: import java.sql.SQLException;
035:
036: // OpenGIS dependencies
037: import org.opengis.referencing.operation.Projection;
038:
039: // Geotools dependencies
040: import org.geotools.resources.i18n.Logging;
041: import org.geotools.resources.i18n.LoggingKeys;
042:
043: /**
044: * A set of EPSG authority codes. This set requires a living connection to the EPSG database.
045: * All {@link #iterator} method call creates a new {@link ResultSet} holding the codes. However,
046: * call to {@link #contains} map directly to a SQL call.
047: * <p>
048: * Serialization of this class store a copy of all authority codes. The serialization
049: * do not preserve any connection to the database.
050: *
051: * @since 2.2
052: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/referencing/src/main/java/org/geotools/referencing/factory/epsg/AuthorityCodes.java $
053: * @version $Id: AuthorityCodes.java 27862 2007-11-12 19:51:19Z desruisseaux $
054: * @author Martin Desruisseaux
055: */
056: final class AuthorityCodes extends AbstractSet implements Serializable {
057: /**
058: * For compatibility with different versions.
059: */
060: private static final long serialVersionUID = 7105664579449680562L;
061:
062: /**
063: * The logger name.
064: */
065: private static final String LOGGER = "org.geotools.referencing.factory.epsg";
066:
067: /**
068: * The factory which is the owner of this set. One purpose of this field (even if it were not
069: * used directly by this class) is to avoid garbage collection of the factory as long as this
070: * set is in use. This is required because {@link DirectEpsgFactory#finalize} closes the JDBC
071: * connections.
072: */
073: private final DirectEpsgFactory factory;
074:
075: /**
076: * The type for this code set. This is translated to the most appropriate
077: * interface type even if the user supplied an implementation type.
078: */
079: public final Class type;
080:
081: /**
082: * {@code true} if {@link #type} is assignable to {@link Projection}.
083: */
084: private final boolean isProjection;
085:
086: /**
087: * A view of this set as a map with object's name as values, or {@code null} if none.
088: * Will be created only when first needed.
089: */
090: private transient java.util.Map asMap;
091:
092: /**
093: * The SQL command to use for creating the {@code queryAll} statement.
094: * Used for iteration over all codes.
095: */
096: final String sqlAll;
097:
098: /**
099: * The SQL command to use for creating the {@code querySingle} statement.
100: * Used for fetching the description from a code.
101: */
102: private final String sqlSingle;
103:
104: /**
105: * The statement to use for querying all codes.
106: * Will be created only when first needed.
107: */
108: private transient PreparedStatement queryAll;
109:
110: /**
111: * The statement to use for querying a single code.
112: * Will be created only when first needed.
113: */
114: private transient PreparedStatement querySingle;
115:
116: /**
117: * The connection to the underlying database. This set should never close
118: * this connection. Closing it is {@link DirectEpsgFactory}'s job.
119: */
120: private final Connection connection;
121:
122: /**
123: * The collection's size, or a negative value if not yet computed. The records will be counted
124: * only when first needed. The special value -2 if set by {@link #isEmpty} if the size has not
125: * yet been computed, but we know that the set is not empty.
126: */
127: private int size = -1;
128:
129: /**
130: * Creates a new set of authority codes for the specified type.
131: *
132: * @param connection The connection to the EPSG database.
133: * @param table The table to query.
134: * @param type The type to query.
135: * @param factory The factory originator.
136: */
137: public AuthorityCodes(final Connection connection,
138: final TableInfo table, final Class type,
139: final DirectEpsgFactory factory) {
140: this .factory = factory;
141: this .connection = connection;
142: final StringBuffer buffer = new StringBuffer("SELECT ");
143: buffer.append(table.codeColumn);
144: if (table.nameColumn != null) {
145: buffer.append(", ").append(table.nameColumn);
146: }
147: buffer.append(" FROM ").append(table.table);
148: boolean hasWhere = false;
149: Class tableType = table.type;
150: if (table.typeColumn != null) {
151: for (int i = 0; i < table.subTypes.length; i++) {
152: final Class candidate = table.subTypes[i];
153: if (candidate.isAssignableFrom(type)) {
154: buffer.append(" WHERE (").append(table.typeColumn)
155: .append(" LIKE '").append(
156: table.typeNames[i]).append("%'");
157: hasWhere = true;
158: tableType = candidate;
159: break;
160: }
161: }
162: if (hasWhere) {
163: buffer.append(')');
164: }
165: }
166: this .type = tableType;
167: isProjection = Projection.class.isAssignableFrom(tableType);
168: final int length = buffer.length();
169: buffer.append(" ORDER BY ").append(table.codeColumn);
170: sqlAll = factory.adaptSQL(buffer.toString());
171: buffer.setLength(length);
172: buffer.append(hasWhere ? " AND " : " WHERE ").append(
173: table.codeColumn).append(" = ?");
174: sqlSingle = factory.adaptSQL(buffer.toString());
175: }
176:
177: /**
178: * Returns all codes.
179: */
180: private ResultSet getAll() throws SQLException {
181: assert Thread.holdsLock(this );
182: if (queryAll != null) {
183: try {
184: return queryAll.executeQuery();
185: } catch (SQLException ignore) {
186: /*
187: * Failed to reuse an existing statement. This problem occurs in some occasions
188: * with the JDBC-ODBC bridge in Java 6 (the error message is "Invalid handle").
189: * I'm not sure where the bug come from (didn't noticed it when using HSQL). We
190: * will try again with a new statement created in the code after this 'catch'
191: * clause. Note that we set 'queryAll' to null first in case of failure during
192: * the 'prepareStatement(...)' execution.
193: */
194: queryAll.close();
195: queryAll = null;
196: recoverableException("getAll", ignore);
197: }
198: }
199: queryAll = connection.prepareStatement(sqlAll);
200: return queryAll.executeQuery();
201: }
202:
203: /**
204: * Returns a single code.
205: */
206: private ResultSet getSingle(final Object code) throws SQLException {
207: assert Thread.holdsLock(this );
208: if (querySingle == null) {
209: querySingle = connection.prepareStatement(sqlSingle);
210: }
211: querySingle.setString(1, code.toString());
212: return querySingle.executeQuery();
213: }
214:
215: /**
216: * Returns {@code true} if the code in the specified result set is acceptable.
217: * This method handle projections in a special way.
218: */
219: private boolean isAcceptable(final ResultSet results)
220: throws SQLException {
221: if (!isProjection) {
222: return true;
223: }
224: final String code = results.getString(1);
225: synchronized (factory) {
226: return factory.isProjection(code);
227: }
228: }
229:
230: /**
231: * Returns {@code true} if the code in the specified code is acceptable.
232: * This method handle projections in a special way.
233: */
234: private boolean isAcceptable(final String code) throws SQLException {
235: if (!isProjection) {
236: return true;
237: }
238: synchronized (factory) {
239: return factory.isProjection(code);
240: }
241: }
242:
243: /**
244: * Returns {@code true} if this collection contains no elements.
245: * This method fetch at most one row instead of counting all rows.
246: */
247: public synchronized boolean isEmpty() {
248: if (size != -1) {
249: return size == 0;
250: }
251: boolean empty = true;
252: try {
253: final ResultSet results = getAll();
254: while (results.next()) {
255: if (isAcceptable(results)) {
256: empty = false;
257: break;
258: }
259: }
260: results.close();
261: } catch (SQLException exception) {
262: unexpectedException("isEmpty", exception);
263: }
264: size = empty ? 0 : -2;
265: return empty;
266: }
267:
268: /**
269: * Count the number of elements in the underlying result set.
270: */
271: public synchronized int size() {
272: if (size >= 0) {
273: return size;
274: }
275: int count = 0;
276: try {
277: final ResultSet results = getAll();
278: while (results.next()) {
279: if (isAcceptable(results)) {
280: count++;
281: }
282: }
283: results.close();
284: } catch (SQLException exception) {
285: unexpectedException("size", exception);
286: }
287: size = count; // Stores only on success.
288: return count;
289: }
290:
291: /**
292: * Returns {@code true} if this collection contains the specified element.
293: */
294: public synchronized boolean contains(final Object code) {
295: boolean exists = false;
296: if (code != null)
297: try {
298: final ResultSet results = getSingle(code);
299: while (results.next()) {
300: if (isAcceptable(results)) {
301: exists = true;
302: break;
303: }
304: }
305: results.close();
306: } catch (SQLException exception) {
307: unexpectedException("contains", exception);
308: }
309: return exists;
310: }
311:
312: /**
313: * Returns an iterator over the codes. The iterator is backed by a living {@link ResultSet},
314: * which will be closed as soon as the iterator reach the last element.
315: */
316: public synchronized java.util.Iterator iterator() {
317: try {
318: final Iterator iterator = new Iterator(getAll());
319: /*
320: * Set the statement to null without closing it, in order to force a new statement
321: * creation if getAll() is invoked before the iterator finish its iteration. This
322: * is needed because only one ResultSet is allowed for each Statement.
323: */
324: queryAll = null;
325: return iterator;
326: } catch (SQLException exception) {
327: unexpectedException("iterator", exception);
328: return Collections.EMPTY_SET.iterator();
329: }
330: }
331:
332: /**
333: * Returns a serializable copy of this set. This method is invoked automatically during
334: * serialization. The serialised set of authority code is disconnected from the underlying
335: * database.
336: */
337: protected Object writeReplace() throws ObjectStreamException {
338: return new LinkedHashSet(this );
339: }
340:
341: /**
342: * Closes the underlying statements. Note: this method is also invoked directly
343: * by {@link DirectEpsgFactory#dispose}, which is okay in this particular case since
344: * the implementation of this method can be executed an arbitrary amount of times.
345: */
346: protected synchronized void finalize() throws SQLException {
347: if (querySingle != null) {
348: querySingle.close();
349: querySingle = null;
350: }
351: if (queryAll != null) {
352: queryAll.close();
353: queryAll = null;
354: }
355: }
356:
357: /**
358: * Invoked when an exception occured. This method just log a warning.
359: */
360: private static void unexpectedException(final String method,
361: final SQLException exception) {
362: unexpectedException(AuthorityCodes.class, method, exception);
363: }
364:
365: /**
366: * Invoked when an exception occured. This method just log a warning.
367: */
368: static void unexpectedException(final Class classe,
369: final String method, final SQLException exception) {
370: org.geotools.util.logging.Logging.unexpectedException(LOGGER,
371: classe, method, exception);
372: }
373:
374: /**
375: * Invoked when a recoverable exception occured.
376: */
377: private static void recoverableException(final String method,
378: final SQLException exception) {
379: // Uses the FINE level instead of WARNING because it may be a recoverable error.
380: LogRecord record = Logging.format(Level.FINE,
381: LoggingKeys.UNEXPECTED_EXCEPTION);
382: record.setSourceClassName(AuthorityCodes.class.getName());
383: record.setSourceMethodName(method);
384: record.setThrown(exception);
385: org.geotools.util.logging.Logging.getLogger(LOGGER).log(record);
386: }
387:
388: /**
389: * The iterator over the codes. This inner class must kept a reference toward the enclosing
390: * {@link AuthorityCodes} in order to prevent a call to {@link AuthorityCodes#finalize}
391: * before the iteration is finished.
392: */
393: private final class Iterator implements java.util.Iterator {
394: /** The result set, or {@code null} if there is no more elements. */
395: private ResultSet results;
396:
397: /** The next code. */
398: private transient String next;
399:
400: /** Creates a new iterator for the specified result set. */
401: Iterator(final ResultSet results) throws SQLException {
402: assert Thread.holdsLock(AuthorityCodes.this );
403: this .results = results;
404: toNext();
405: }
406:
407: /** Moves to the next element. */
408: private void toNext() throws SQLException {
409: while (results.next()) {
410: next = results.getString(1);
411: if (isAcceptable(next)) {
412: return;
413: }
414: }
415: finalize();
416: }
417:
418: /** Returns {@code true} if there is more elements. */
419: public boolean hasNext() {
420: return results != null;
421: }
422:
423: /** Returns the next element. */
424: public Object next() {
425: if (results == null) {
426: throw new NoSuchElementException();
427: }
428: final String current = next;
429: try {
430: toNext();
431: } catch (SQLException exception) {
432: results = null;
433: unexpectedException(Iterator.class, "next", exception);
434: }
435: return current;
436: }
437:
438: /** Always throws an exception, since this iterator is read-only. */
439: public void remove() {
440: throw new UnsupportedOperationException();
441: }
442:
443: /** Closes the underlying result set. */
444: protected void finalize() throws SQLException {
445: next = null;
446: if (results != null) {
447: final PreparedStatement owner = (PreparedStatement) results
448: .getStatement();
449: results.close();
450: results = null;
451: synchronized (AuthorityCodes.this ) {
452: /*
453: * We don't need the statement anymore. Gives it back to the enclosing class
454: * in order to avoid creating a new one when AuthorityCodes.getAll() will be
455: * invoked again, or closes the statement if getAll() already created a new
456: * statement anyway.
457: */
458: assert owner != queryAll;
459: if (queryAll == null) {
460: queryAll = owner;
461: } else {
462: owner.close();
463: }
464: }
465: }
466: }
467: }
468:
469: /**
470: * Returns a view of this set as a map with object's name as value, or {@code null} if none.
471: */
472: final java.util.Map asMap() {
473: if (asMap == null) {
474: asMap = new Map();
475: }
476: return asMap;
477: }
478:
479: /**
480: * A view of {@link AuthorityCodes} as a map, with authority codes as key and
481: * object names as values.
482: */
483: private final class Map extends AbstractMap {
484: /**
485: * Returns the number of key-value mappings in this map.
486: */
487: public int size() {
488: return AuthorityCodes.this .size();
489: }
490:
491: /**
492: * Returns {@code true} if this map contains no key-value mappings.
493: */
494: public boolean isEmpty() {
495: return AuthorityCodes.this .isEmpty();
496: }
497:
498: /**
499: * Returns the description to which this map maps the specified EPSG code.
500: */
501: public Object get(final Object code) {
502: String value = null;
503: if (code != null)
504: try {
505: synchronized (AuthorityCodes.this ) {
506: final ResultSet results = getSingle(code);
507: while (results.next()) {
508: if (isAcceptable(results)) {
509: value = results.getString(2);
510: break;
511: }
512: }
513: results.close();
514: }
515: } catch (SQLException exception) {
516: unexpectedException("get", exception);
517: }
518: return value;
519: }
520:
521: /**
522: * Returns {@code true} if this map contains a mapping for the specified EPSG code.
523: */
524: public boolean containsKey(final Object key) {
525: return contains(key);
526: }
527:
528: /**
529: * Returns a set view of the keys contained in this map.
530: */
531: public Set keySet() {
532: return AuthorityCodes.this ;
533: }
534:
535: /**
536: * Returns a set view of the mappings contained in this map.
537: *
538: * @todo Not yet implemented.
539: */
540: public Set entrySet() {
541: throw new UnsupportedOperationException();
542: }
543: }
544: }
|