/* * 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.metadata.sql; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Array; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.net.MalformedURLException; import java.net.URL; import java.net.URI; import java.net.URISyntaxException; import java.sql.Connection; import java.sql.SQLException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.SortedSet; import java.util.TreeSet; import org.opengis.metadata.MetaData; import org.opengis.util.CodeList; import org.opengis.util.InternationalString; import org.geotools.util.SimpleInternationalString; /** * A connection to a metadata database. The metadata database can be created * using one of the scripts suggested in GeoAPI, for example * <code><A HREF="http://geoapi.sourceforge.net/snapshot/javadoc/org/opengis/metadata/doc-files/postgre/create.sql">create.sql</A></CODE>. * Then, in order to get for example a telephone number, the following code * may be used. * * <BLOCKQUOTE><PRE> * import org.opengis.metadata.citation.{@linkplain org.opengis.metadata.citation.Telephone Telephone}; * ... * Connection connection = ... * MetadataSource source = new MetadataSource(connection); * Telephone telephone = (Telephone) source.getEntry(Telephone.class, id); * </PRE></BLOCKQUOTE> * * where {@code id} is the primary key value for the desired record in the * {@code CI_Telephone} table. * * @since 2.1 * @source $URL$ * @version $Id$ * @author Touraïvane * @author Olivier Kartotaroeno (Institut de Recherche pour le Développement) * @author Martin Desruisseaux (IRD) */ public class MetadataSource { /** * The package for metadata <strong>interfaces</strong> (not the implementation). */ final String metadataPackage = "org.opengis.metadata."; /** * The connection to the database. */ private final Connection connection; /** * The SQL query to use for fetching the attribute in a specific row. * The first question mark is the table name to search into; the second * one is the primary key of the record to search. */ private final String query = "SELECT * FROM metadata.\"?\" WHERE id=?"; /** * The SQL query to use for fetching a code list element. * The first question mark is the table name to search into; * the second one is the primary key of the element to search. */ private final String codeQuery = "SELECT name FROM metadata.\"?\" WHERE code=?"; /** * The prepared statements created is previous call to {@link #getValue}. * Those statements are encapsulated into {@link MetadataResult} objects. */ private final Map<Class<?>,MetadataResult> statements = new HashMap<Class<?>,MetadataResult>(); /** * The map from GeoAPI names to ISO names. For example the GeoAPI * {@link org.opengis.metadata.citation.Citation} interface maps * to the ISO 19115 {@code CI_Citation} name. */ private final Properties geoApiToIso = new Properties(); /** * Type of collections. */ private final Properties collectionTypes = new Properties(); /** * The class loader to use for proxy creation. */ private final ClassLoader loader; /** * Creates a new metadata source. * * @param connection The connection to the database. */ public MetadataSource(final Connection connection) { this.connection = connection; try { InputStream in = MetaData.class.getResourceAsStream("GeoAPI_to_ISO.properties"); geoApiToIso.load(in); in.close(); in = MetaData.class.getResourceAsStream("CollectionTypes.properties"); // TODO: remove the (!= null) check after the next geoapi update. if (in != null) { collectionTypes.load(in); in.close(); } } catch (IOException exception) { /* * Note: we do not expose the checked IOException because in a future * version (when we will be allowed to use J2SE 1.5), it should * disaspear. This is because a J2SE 1.5 enabled version should * use method's annotations instead. */ throw new MetadataException("Can't read resources.", exception); // TODO: localize } loader = getClass().getClassLoader(); } /** * Returns an implementation of the specified metadata interface filled * with the data referenced by the specified identifier. Alternatively, * this method can also returns a {@link CodeList} element. * * @param type The interface to implement (e.g. * {@link org.opengis.metadata.citation.Citation}), or * the {@link CodeList}. * @param identifier The identifier used in order to locate the record for * the metadata entity to be created. This is usually the primary key * of the record to search for. * @return An implementation of the required interface, or the code list element. * @throws SQLException if a SQL query failed. */ public synchronized Object getEntry(final Class type, final String identifier) throws SQLException { if (CodeList.class.isAssignableFrom(type)) { return getCodeList(type, identifier); } return Proxy.newProxyInstance(loader, new Class[] {type}, new MetadataEntity(identifier, this)); } /** * Returns an attribute from a table. * * @param type The interface class. This is mapped to the table name in the database. * @param method The method invoked. This is mapped to the column name in the database. * @param identifier The primary key of the record to search for. * @return The value of the requested attribute. * @throws SQLException if the SQL query failed. */ final synchronized Object getValue(final Class<?> type, final Method method, final String identifier) throws SQLException { final String className = getClassName(type); MetadataResult result = statements.get(type); if (result == null) { result = new MetadataResult(connection, query, getTableName(className)); statements.put(type, result); } final String columnName = getColumnName(className, method); final Class<?> valueType = method.getReturnType(); /* * Process the ResultSet value according the expected return type. If a collection * is expected, then assumes that the ResultSet contains an array and invokes the * 'getValue' method for each element. */ if (Collection.class.isAssignableFrom(valueType)) { final Collection<Object> collection; if (List.class.isAssignableFrom(valueType)) { collection = new ArrayList<Object>(); } else if (SortedSet.class.isAssignableFrom(valueType)) { collection = new TreeSet<Object>(); } else { collection = new LinkedHashSet<Object>(); } assert valueType.isAssignableFrom(collection.getClass()); final Object elements = result.getArray(identifier, columnName); if (elements != null) { final Class elementType = getElementType(className, method); final boolean isMetadata = isMetadata(elementType); final int length = Array.getLength(elements); for (int i=0; i<length; i++) { collection.add(isMetadata ? getEntry(elementType, Array.get(elements, i).toString()) : convert (elementType, Array.get(elements, i))); } } return collection; } /* * If a GeoAPI interface or a code list is expected, then assumes that the ResultSet * value is a foreigner key. Queries again the database in the foreigner table. */ if (valueType.isInterface() && isMetadata(valueType)) { final String foreigner = result.getString(identifier, columnName); return result.wasNull() ? null : getEntry(valueType, foreigner); } if (CodeList.class.isAssignableFrom(valueType)) { final String foreigner = result.getString(identifier, columnName); return result.wasNull() ? null : getCodeList(valueType, foreigner); } /* * Not a foreigner key. Get the value and transform it to the * espected type, if needed. */ return convert(valueType, result.getObject(identifier, columnName)); } /** * Returns {@code true} if the specified type belong to the metadata package. */ private boolean isMetadata(final Class valueType) { return valueType.getName().startsWith(metadataPackage); } /** * Converts the specified non-metadata value into an object of the expected type. * The expected value is an instance of a class outside the metadata package, for * example {@link String}, {@link InternationalString}, {@link URI}, etc. */ private static Object convert(final Class<?> valueType, final Object value) { if (value!=null && !valueType.isAssignableFrom(value.getClass())) { if (InternationalString.class.isAssignableFrom(valueType)) { return new SimpleInternationalString(value.toString()); } if (URL.class.isAssignableFrom(valueType)) try { return new URL(value.toString()); } catch (MalformedURLException exception) { // TODO: localize and provides more details. throw new MetadataException("Illegal value.", exception); } if (URI.class.isAssignableFrom(valueType)) try { return new URI(value.toString()); } catch (URISyntaxException exception) { // TODO: localize and provides more details. throw new MetadataException("Illegal value.", exception); } } return value; } /** * Returns a code list of the given type. * * @param type The type, as a subclass of {@link CodeList}. * @param identifier The identifier in the code list. This method accepts either The numerical * value of the code to search for (usually the primary key), or the code name. * @return The code list element. * @throws SQLException if a SQL query failed. */ private CodeList getCodeList(final Class<?> type, String identifier) throws SQLException { assert Thread.holdsLock(this); final String className = getClassName(type); int code; // The identifier as an integer. boolean isNumerical; // 'true' if 'code' is valid. try { code = Integer.parseInt(identifier); isNumerical = true; } catch (NumberFormatException exception) { code = 0; isNumerical = false; } /* * Converts the numerical value into the code list name. */ if (isNumerical) { MetadataResult result = statements.get(type); if (result == null) { result = new MetadataResult(connection, codeQuery, getTableName(className)); statements.put(type, result); } identifier = result.getString(identifier); } /* * Search a code list with the same name than the one declared * in the database. We will use name instead of code numerical * value, since the later is more bug prone. */ final CodeList<?>[] values; try { values = (CodeList[]) type.getMethod("values", (Class []) null) .invoke (null, (Object[]) null); } catch (NoSuchMethodException exception) { throw new MetadataException("Can't read code list.", exception); // TODO: localize } catch (IllegalAccessException exception) { throw new MetadataException("Can't read code list.", exception); // TODO: localize } catch (InvocationTargetException exception) { throw new MetadataException("Can't read code list.", exception); // TODO: localize } CodeList<?> candidate; final StringBuilder candidateName = new StringBuilder(className); candidateName.append('.'); final int base = candidateName.length(); if (code>=1 && code<values.length) { candidate = values[code-1]; candidateName.append(candidate.name()); if (identifier.equals(geoApiToIso.getProperty(candidateName.toString()))) { return candidate; } } /* * The previous code was an optimization which checked directly the code list * for the same code than the one used in the database. Most of the time, the * name matches and this loop is never executed. If we reach this point, then * maybe the numerical code are not the same in the database than in the Java * CodeList implementation. Check each code list element by name. */ for (int i=0; i<values.length; i++) { candidate = values[i]; candidateName.setLength(base); candidateName.append(candidate.name()); if (identifier.equals(geoApiToIso.getProperty(candidateName.toString()))) { return candidate; } } // TODO: localize throw new SQLException("Unknow code list: \""+identifier+"\" in table \"" + getTableName(className)+'"'); } /** * Returns the unqualified Java interface name for the specified type. * This is usually the GeoAPI name. */ private static String getClassName(final Class<?> type) { final String className = type.getName(); return className.substring(className.lastIndexOf('.') + 1); } /** * Returns the table name for the specified class. * This is usually the ISO 19115 name. */ private String getTableName(final String className) { final String tableName = geoApiToIso.getProperty(className); return (tableName != null) ? tableName : className; } /** * Returns the column name for the specified method. */ private String getColumnName(final String className, final Method method) { final String methodName = method.getName(); final String columnName = geoApiToIso.getProperty(className+'.'+methodName); return (columnName != null) ? columnName : methodName; } /** * Returns the element type in collection for the specified method. */ private Class getElementType(final String className, final Method method) { final String key = className+'.'+method.getName(); final String typeName = collectionTypes.getProperty(key); Exception cause = null; if (typeName != null) try { return Class.forName(typeName); } catch (ClassNotFoundException exception) { cause = exception; } // TODO: localize. final MetadataException e = new MetadataException("Unknow element type for "+key); if (cause != null) { e.initCause(cause); } throw e; } /** * Close all connections used in this object. */ public synchronized void close() throws SQLException { for (final Iterator it=statements.values().iterator(); it.hasNext();) { ((MetadataResult)it.next()).close(); it.remove(); } connection.close(); } }