001: /*
002: * GeoTools - OpenSource mapping toolkit
003: * http://geotools.org
004: * (C) 2004-2006, GeoTools Project Managment Committee (PMC)
005: * (C) 2004, 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.metadata.sql;
018:
019: // J2SE dependencies
020: import java.io.IOException;
021: import java.io.InputStream;
022: import java.lang.reflect.Array;
023: import java.lang.reflect.InvocationTargetException;
024: import java.lang.reflect.Method;
025: import java.lang.reflect.Proxy;
026: import java.net.MalformedURLException;
027: import java.net.URL;
028: import java.net.URI;
029: import java.net.URISyntaxException;
030: import java.sql.Connection;
031: import java.sql.SQLException;
032: import java.util.ArrayList;
033: import java.util.Collection;
034: import java.util.HashMap;
035: import java.util.Iterator;
036: import java.util.LinkedHashSet;
037: import java.util.List;
038: import java.util.Map;
039: import java.util.Properties;
040: import java.util.SortedSet;
041: import java.util.TreeSet;
042:
043: // OpenGIS dependencies
044: import org.opengis.metadata.MetaData;
045: import org.opengis.util.CodeList;
046: import org.opengis.util.InternationalString;
047:
048: // Geotools dependencies
049: import org.geotools.util.SimpleInternationalString;
050:
051: /**
052: * A connection to a metadata database. The metadata database can be created
053: * using one of the scripts suggested in GeoAPI, for example
054: * <code><A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/metadata/doc-files/postgre/create.sql">create.sql</A></CODE>.
055: * Then, in order to get for example a telephone number, the following code
056: * may be used.
057: *
058: * <BLOCKQUOTE><PRE>
059: * import org.opengis.metadata.citation.{@linkplain org.opengis.metadata.citation.Telephone Telephone};
060: * ...
061: * Connection connection = ...
062: * MetadataSource source = new MetadataSource(connection);
063: * Telephone telephone = (Telephone) source.getEntry(Telephone.class, id);
064: * </PRE></BLOCKQUOTE>
065: *
066: * where {@code id} is the primary key value for the desired record in the
067: * {@code CI_Telephone} table.
068: *
069: * @author Touraïvane
070: * @author Olivier Kartotaroeno
071: * @author Martin Desruisseaux
072: *
073: * @since 2.1
074: * @source $URL: http://svn.geotools.org/geotools/tags/2.4.1/modules/library/metadata/src/main/java/org/geotools/metadata/sql/MetadataSource.java $
075: */
076: public class MetadataSource {
077: /**
078: * The package for metadata <strong>interfaces</strong> (not the implementation).
079: */
080: final String metadataPackage = "org.opengis.metadata.";
081:
082: /**
083: * The connection to the database.
084: */
085: private final Connection connection;
086:
087: /**
088: * The SQL query to use for fetching the attribute in a specific row.
089: * The first question mark is the table name to search into; the second
090: * one is the primary key of the record to search.
091: */
092: private final String query = "SELECT * FROM metadata.\"?\" WHERE id=?";
093:
094: /**
095: * The SQL query to use for fetching a code list element.
096: * The first question mark is the table name to search into;
097: * the second one is the primary key of the element to search.
098: */
099: private final String codeQuery = "SELECT name FROM metadata.\"?\" WHERE code=?";
100:
101: /**
102: * The prepared statements created is previous call to {@link #getValue}.
103: * Those statements are encapsulated into {@link MetadataResult} objects.
104: */
105: private final Map statements = new HashMap();
106:
107: /**
108: * The map from GeoAPI names to ISO names. For example the GeoAPI
109: * {@link org.opengis.metadata.citation.Citation} interface maps
110: * to the ISO 19115 {@code CI_Citation} name.
111: */
112: private final Properties geoApiToIso = new Properties();
113:
114: /**
115: * Type of collections.
116: */
117: private final Properties collectionTypes = new Properties();
118:
119: /**
120: * The class loader to use for proxy creation.
121: */
122: private final ClassLoader loader;
123:
124: /**
125: * Creates a new metadata source.
126: *
127: * @param connection The connection to the database.
128: */
129: public MetadataSource(final Connection connection) {
130: this .connection = connection;
131: try {
132: InputStream in = MetaData.class
133: .getResourceAsStream("GeoAPI_to_ISO.properties");
134: geoApiToIso.load(in);
135: in.close();
136: in = MetaData.class
137: .getResourceAsStream("CollectionTypes.properties");
138: // TODO: remove the (!= null) check after the next geoapi update.
139: if (in != null) {
140: collectionTypes.load(in);
141: in.close();
142: }
143: } catch (IOException exception) {
144: /*
145: * Note: we do not expose the checked IOException because in a future
146: * version (when we will be allowed to use J2SE 1.5), it should
147: * disaspear. This is because a J2SE 1.5 enabled version should
148: * use method's annotations instead.
149: */
150: throw new MetadataException("Can't read resources.",
151: exception); // TODO: localize
152: }
153: loader = getClass().getClassLoader();
154: }
155:
156: /**
157: * Returns an implementation of the specified metadata interface filled
158: * with the data referenced by the specified identifier. Alternatively,
159: * this method can also returns a {@link CodeList} element.
160: *
161: * @param type The interface to implement (e.g.
162: * {@link org.opengis.metadata.citation.Citation}), or
163: * the {@link CodeList}.
164: * @param identifier The identifier used in order to locate the record for
165: * the metadata entity to be created. This is usually the primary key
166: * of the record to search for.
167: * @return An implementation of the required interface, or the code list element.
168: * @throws SQLException if a SQL query failed.
169: */
170: public synchronized Object getEntry(final Class type,
171: final String identifier) throws SQLException {
172: if (CodeList.class.isAssignableFrom(type)) {
173: return getCodeList(type, identifier);
174: }
175: return Proxy.newProxyInstance(loader, new Class[] { type },
176: new MetadataEntity(identifier, this ));
177: }
178:
179: /**
180: * Returns an attribute from a table.
181: *
182: * @param type The interface class. This is mapped to the table name in the database.
183: * @param method The method invoked. This is mapped to the column name in the database.
184: * @param identifier The primary key of the record to search for.
185: * @return The value of the requested attribute.
186: * @throws SQLException if the SQL query failed.
187: */
188: final synchronized Object getValue(final Class type,
189: final Method method, final String identifier)
190: throws SQLException {
191: final String className = getClassName(type);
192: MetadataResult result = (MetadataResult) statements.get(type);
193: if (result == null) {
194: result = new MetadataResult(connection, query,
195: getTableName(className));
196: statements.put(type, result);
197: }
198: final String columnName = getColumnName(className, method);
199: final Class valueType = method.getReturnType();
200: /*
201: * Process the ResultSet value according the expected return type. If a collection
202: * is expected, then assumes that the ResultSet contains an array and invokes the
203: * 'getValue' method for each element.
204: */
205: if (Collection.class.isAssignableFrom(valueType)) {
206: final Collection collection;
207: if (List.class.isAssignableFrom(valueType)) {
208: collection = new ArrayList();
209: } else if (SortedSet.class.isAssignableFrom(valueType)) {
210: collection = new TreeSet();
211: } else {
212: collection = new LinkedHashSet();
213: }
214: assert valueType.isAssignableFrom(collection.getClass());
215: final Object elements = result.getArray(identifier,
216: columnName);
217: if (elements != null) {
218: final Class elementType = getElementType(className,
219: method);
220: final boolean isMetadata = isMetadata(elementType);
221: final int length = Array.getLength(elements);
222: for (int i = 0; i < length; i++) {
223: collection.add(isMetadata ? getEntry(elementType,
224: Array.get(elements, i).toString())
225: : convert(elementType, Array.get(elements,
226: i)));
227: }
228: }
229: return collection;
230: }
231: /*
232: * If a GeoAPI interface or a code list is expected, then assumes that the ResultSet
233: * value is a foreigner key. Queries again the database in the foreigner table.
234: */
235: if (valueType.isInterface() && isMetadata(valueType)) {
236: final String foreigner = result.getString(identifier,
237: columnName);
238: return result.wasNull() ? null : getEntry(valueType,
239: foreigner);
240: }
241: if (CodeList.class.isAssignableFrom(valueType)) {
242: final String foreigner = result.getString(identifier,
243: columnName);
244: return result.wasNull() ? null : getCodeList(valueType,
245: foreigner);
246: }
247: /*
248: * Not a foreigner key. Get the value and transform it to the
249: * espected type, if needed.
250: */
251: return convert(valueType, result.getObject(identifier,
252: columnName));
253: }
254:
255: /**
256: * Returns {@code true} if the specified type belong to the metadata package.
257: */
258: private boolean isMetadata(final Class valueType) {
259: return valueType.getName().startsWith(metadataPackage);
260: }
261:
262: /**
263: * Converts the specified non-metadata value into an object of the expected type.
264: * The expected value is an instance of a class outside the metadata package, for
265: * example {@link String}, {@link InternationalString}, {@link URI}, etc.
266: */
267: private static Object convert(final Class valueType,
268: final Object value) {
269: if (value != null
270: && !valueType.isAssignableFrom(value.getClass())) {
271: if (InternationalString.class.isAssignableFrom(valueType)) {
272: return new SimpleInternationalString(value.toString());
273: }
274: if (URL.class.isAssignableFrom(valueType))
275: try {
276: return new URL(value.toString());
277: } catch (MalformedURLException exception) {
278: // TODO: localize and provides more details.
279: throw new MetadataException("Illegal value.",
280: exception);
281: }
282: if (URI.class.isAssignableFrom(valueType))
283: try {
284: return new URI(value.toString());
285: } catch (URISyntaxException exception) {
286: // TODO: localize and provides more details.
287: throw new MetadataException("Illegal value.",
288: exception);
289: }
290: }
291: return value;
292: }
293:
294: /**
295: * Returns a code list of the given type.
296: *
297: * @param type The type, as a subclass of {@link CodeList}.
298: * @param identifier The identifier in the code list. This method accepts either The numerical
299: * value of the code to search for (usually the primary key), or the code name.
300: * @return The code list element.
301: * @throws SQLException if a SQL query failed.
302: */
303: private CodeList getCodeList(final Class type, String identifier)
304: throws SQLException {
305: assert Thread.holdsLock(this );
306: final String className = getClassName(type);
307: int code; // The identifier as an integer.
308: boolean isNumerical; // 'true' if 'code' is valid.
309: try {
310: code = Integer.parseInt(identifier);
311: isNumerical = true;
312: } catch (NumberFormatException exception) {
313: code = 0;
314: isNumerical = false;
315: }
316: /*
317: * Converts the numerical value into the code list name.
318: */
319: if (isNumerical) {
320: MetadataResult result = (MetadataResult) statements
321: .get(type);
322: if (result == null) {
323: result = new MetadataResult(connection, codeQuery,
324: getTableName(className));
325: statements.put(type, result);
326: }
327: identifier = result.getString(identifier);
328: }
329: /*
330: * Search a code list with the same name than the one declared
331: * in the database. We will use name instead of code numerical
332: * value, since the later is more bug prone.
333: */
334: final CodeList[] values;
335: try {
336: values = (CodeList[]) type.getMethod("values",
337: (Class[]) null).invoke(null, (Object[]) null);
338: } catch (NoSuchMethodException exception) {
339: throw new MetadataException("Can't read code list.",
340: exception); // TODO: localize
341: } catch (IllegalAccessException exception) {
342: throw new MetadataException("Can't read code list.",
343: exception); // TODO: localize
344: } catch (InvocationTargetException exception) {
345: throw new MetadataException("Can't read code list.",
346: exception); // TODO: localize
347: }
348: CodeList candidate;
349: final StringBuffer candidateName = new StringBuffer(className);
350: candidateName.append('.');
351: final int base = candidateName.length();
352: if (code >= 1 && code < values.length) {
353: candidate = values[code - 1];
354: candidateName.append(candidate.name());
355: if (identifier.equals(geoApiToIso.getProperty(candidateName
356: .toString()))) {
357: return candidate;
358: }
359: }
360: /*
361: * The previous code was an optimization which checked directly the code list
362: * for the same code than the one used in the database. Most of the time, the
363: * name matches and this loop is never executed. If we reach this point, then
364: * maybe the numerical code are not the same in the database than in the Java
365: * CodeList implementation. Check each code list element by name.
366: */
367: for (int i = 0; i < values.length; i++) {
368: candidate = values[i];
369: candidateName.setLength(base);
370: candidateName.append(candidate.name());
371: if (identifier.equals(geoApiToIso.getProperty(candidateName
372: .toString()))) {
373: return candidate;
374: }
375: }
376: // TODO: localize
377: throw new SQLException("Unknow code list: \"" + identifier
378: + "\" in table \"" + getTableName(className) + '"');
379: }
380:
381: /**
382: * Returns the unqualified Java interface name for the specified type.
383: * This is usually the GeoAPI name.
384: */
385: private static String getClassName(final Class type) {
386: final String className = type.getName();
387: return className.substring(className.lastIndexOf('.') + 1);
388: }
389:
390: /**
391: * Returns the table name for the specified class.
392: * This is usually the ISO 19115 name.
393: */
394: private String getTableName(final String className) {
395: final String tableName = geoApiToIso.getProperty(className);
396: return (tableName != null) ? tableName : className;
397: }
398:
399: /**
400: * Returns the column name for the specified method.
401: */
402: private String getColumnName(final String className,
403: final Method method) {
404: final String methodName = method.getName();
405: final String columnName = geoApiToIso.getProperty(className
406: + '.' + methodName);
407: return (columnName != null) ? columnName : methodName;
408: }
409:
410: /**
411: * Returns the element type in collection for the specified method.
412: */
413: private Class getElementType(final String className,
414: final Method method) {
415: final String key = className + '.' + method.getName();
416: final String typeName = collectionTypes.getProperty(key);
417: Exception cause = null;
418: if (typeName != null)
419: try {
420: return Class.forName(typeName);
421: } catch (ClassNotFoundException exception) {
422: cause = exception;
423: }
424: // TODO: localize.
425: final MetadataException e = new MetadataException(
426: "Unknow element type for " + key);
427: if (cause != null) {
428: e.initCause(cause);
429: }
430: throw e;
431: }
432:
433: /**
434: * Close all connections used in this object.
435: */
436: public synchronized void close() throws SQLException {
437: for (final Iterator it = statements.values().iterator(); it
438: .hasNext();) {
439: ((MetadataResult) it.next()).close();
440: it.remove();
441: }
442: connection.close();
443: }
444: }
|