sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1770500 - in /sis/branches/JDK8/core/sis-metadata/src: main/java/org/apache/sis/internal/metadata/sql/ main/java/org/apache/sis/metadata/sql/ test/java/org/apache/sis/internal/metadata/sql/ test/java/org/apache/sis/test/suite/
Date Sat, 19 Nov 2016 15:39:50 GMT
Author: desruisseaux
Date: Sat Nov 19 15:39:49 2016
New Revision: 1770500

URL: http://svn.apache.org/viewvc?rev=1770500&view=rev
Log:
Continue implementation of org.apache.sis.metadata.sql.

Added:
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/SQLBuilder.java   (with props)
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/TypeMapper.java   (with props)
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/CacheKey.java   (with props)
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/ResultPool.java   (with props)
    sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/sql/TypeMapperTest.java   (with props)
Modified:
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataResult.java
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataSource.java
    sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java

Added: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/SQLBuilder.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/SQLBuilder.java?rev=1770500&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/SQLBuilder.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/SQLBuilder.java [UTF-8] Sat Nov 19 15:39:49 2016
@@ -0,0 +1,304 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.metadata.sql;
+
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+import java.util.StringTokenizer;
+
+
+/**
+ * Utility methods for building SQL statements.
+ * This class is for internal purpose only and may change or be removed in any future SIS version.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+public class SQLBuilder {
+    /**
+     * The database dialect. This is used for a few database-dependent syntax.
+     */
+    private final Dialect dialect;
+
+    /**
+     * The characters used for quoting identifiers, or an empty string if none.
+     */
+    private final String quote;
+
+    /**
+     * The string that can be used to escape wildcard characters.
+     * This is the value returned by {@link DatabaseMetaData#getSearchStringEscape()}.
+     */
+    private final String escape;
+
+    /**
+     * The buffer where the SQL query is to be created.
+     */
+    private final StringBuilder buffer = new StringBuilder();
+
+    /**
+     * Creates a new {@code SQLBuilder} initialized from the given database metadata.
+     *
+     * @param  metadata  the database metadata.
+     * @throws SQLException if an error occurred while fetching the database metadata.
+     */
+    public SQLBuilder(final DatabaseMetaData metadata) throws SQLException {
+        dialect = Dialect.guess(metadata);
+        quote   = metadata.getIdentifierQuoteString();
+        escape  = metadata.getSearchStringEscape();
+    }
+
+    /**
+     * Creates a new {@code SQLBuilder} initialized to the same metadata than the given builder.
+     *
+     * @param metadata  the builder from which to copy metadata.
+     */
+    public SQLBuilder(final SQLBuilder metadata) {
+        dialect = metadata.dialect;
+        quote   = metadata.quote;
+        escape  = metadata.escape;
+    }
+
+    /**
+     * Returns {@code true} if the builder is currently empty.
+     *
+     * @return {@code true} if the builder is empty.
+     */
+    public final boolean isEmpty() {
+        return buffer.length() == 0;
+    }
+
+    /**
+     * Clears this builder and make it ready for creating a new SQL statement.
+     *
+     * @return this builder, for method call chaining.
+     */
+    public final SQLBuilder clear() {
+        buffer.setLength(0);
+        return this;
+    }
+
+    /**
+     * Appends the given integer.
+     *
+     * @param  n  the integer to append.
+     * @return this builder, for method call chaining.
+     */
+    public final SQLBuilder append(final int n) {
+        buffer.append(n);
+        return this;
+    }
+
+    /**
+     * Appends the given character.
+     *
+     * @param  c  the character to append.
+     * @return this builder, for method call chaining.
+     */
+    public final SQLBuilder append(final char c) {
+        buffer.append(c);
+        return this;
+    }
+
+    /**
+     * Appends the given text verbatim.
+     * The text should be SQL keywords like {@code "SELECT * FROM"}.
+     *
+     * @param  keyword  the keyword to append verbatim.
+     * @return this builder, for method call chaining.
+     */
+    public final SQLBuilder append(final String keyword) {
+        buffer.append(keyword);
+        return this;
+    }
+
+    /**
+     * Appends an identifier between quote characters.
+     *
+     * @param  identifier  the identifier to append.
+     * @return this builder, for method call chaining.
+     */
+    public final SQLBuilder appendIdentifier(final String identifier) {
+        buffer.append(quote).append(identifier).append(quote);
+        return this;
+    }
+
+    /**
+     * Appends an identifier for an element in the given schema.
+     * The identifier will be written between the quote characters.
+     * The schema will be written only if non-null.
+     *
+     * @param  schema      the schema, or {@code null} if none.
+     * @param  identifier  the identifier to append.
+     * @return this builder, for method call chaining.
+     */
+    public final SQLBuilder appendIdentifier(final String schema, final String identifier) {
+        if (schema != null) {
+            appendIdentifier(schema).append('.');
+        }
+        return appendIdentifier(identifier);
+    }
+
+    /**
+     * Appends a value in a {@code SELECT} statement.
+     * The {@code "="} string will be inserted before the value.
+     *
+     * @param  value  the value to append, or {@code null}.
+     * @return this builder, for method call chaining.
+     */
+    public final SQLBuilder appendCondition(final Object value) {
+        if (value == null) {
+            buffer.append("IS NULL");
+            return this;
+        }
+        buffer.append('=');
+        return appendValue(value);
+    }
+
+    /**
+     * Appends a value in an {@code INSERT} statement.
+     *
+     * @param  value  the value to append, or {@code null}.
+     * @return this builder, for method call chaining.
+     */
+    public final SQLBuilder appendValue(final Object value) {
+        if (value == null) {
+            buffer.append("NULL");
+        } else if (value instanceof Boolean) {
+            buffer.append((Boolean) value ? "TRUE" : "FALSE");
+        } else if (value instanceof Number) {
+            buffer.append(value);
+        } else {
+            buffer.append('\'').append(doubleQuotes(value)).append('\'');
+        }
+        return this;
+    }
+
+    /**
+     * Appends a string as an escaped {@code LIKE} argument.
+     * This method does not put any {@code '} character, and does not accept null argument.
+     *
+     * <p>This method does not double the simple quotes of the given string on intend, because
+     * it may be used in a {@code PreparedStatement}. If the simple quotes need to be doubled,
+     * then {@link #doubleQuotes(Object)} should be invoked explicitly.</p>
+     *
+     * @param  value  the value to append.
+     * @return this builder, for method call chaining.
+     */
+    public final SQLBuilder appendEscaped(final String value) {
+        final StringTokenizer tokens = new StringTokenizer(value, "_%", true);
+        while (tokens.hasMoreTokens()) {
+            buffer.append(tokens.nextToken());
+            if (!tokens.hasMoreTokens()) {
+                break;
+            }
+            buffer.append(escape).append(tokens.nextToken());
+        }
+        return this;
+    }
+
+    /**
+     * Returns a SQL statement for adding a column in a table.
+     * The returned statement is of the form:
+     *
+     * {@preformat sql
+     *   ALTER TABLE "schema"."table" ADD COLUMN "column" type
+     * }
+     *
+     * where {@code type} is some SQL keyword like {@code INTEGER} or {@code VARCHAR}
+     * depending on the {@code type} argument.
+     *
+     * @param  schema     the schema for the table.
+     * @param  table      the table to alter with the new column.
+     * @param  column     the column to add.
+     * @param  type       the column type, or {@code null} for {@code VARCHAR}.
+     * @param  maxLength  the maximal length (used for {@code VARCHAR} only).
+     * @return a SQL statement for creating the column.
+     */
+    public final String createColumn(final String schema, final String table,
+            final String column, final Class<?> type, final int maxLength)
+    {
+        clear().append("ALTER TABLE ").appendIdentifier(schema, table)
+               .append(" ADD COLUMN ").appendIdentifier(column).append(' ');
+        final String sqlType = TypeMapper.keywordFor(type);
+        if (sqlType != null) {
+            append(sqlType);
+        } else {
+            append("VARCHAR(").append(maxLength).append(')');
+        }
+        return toString();
+    }
+
+    /**
+     * Returns a SQL statement for creating a foreigner key constraint.
+     * The returned statement is of the form:
+     *
+     * {@preformat sql
+     *   ALTER TABLE "schema"."table" ADD CONSTRAINT "table_column_fkey" FOREIGN KEY("column")
+     *   REFERENCES "schema"."target" (primaryKey) ON UPDATE CASCADE ON DELETE RESTRICT
+     * }
+     *
+     * Note that the primary key is <strong>not</strong> quoted on intend.
+     * If quoted are desired, then they must be added explicitly before to call this method.
+     *
+     * @param  schema      the schema for both tables.
+     * @param  table       the table to alter with the new constraint.
+     * @param  column      the column to alter with the new constraint.
+     * @param  target      the table to reference.
+     * @param  primaryKey  the primary key in the target table.
+     * @param  cascade     {@code true} if updates in primary key should be cascaded.
+     *                     this apply to updates only; delete is always restricted.
+     * @return a SQL statement for creating the foreigner key constraint.
+     */
+    public final String createForeignKey(final String schema, final String table, final String column,
+            final String target, final String primaryKey, boolean cascade)
+    {
+        if (dialect == Dialect.DERBY) {
+            // Derby does not support "ON UPDATE CASCADE". It must be RESTRICT.
+            cascade = false;
+        }
+        buffer.setLength(0);
+        final String name = buffer.append(table).append('_').append(column).append("_fkey").toString();
+        return clear().append("ALTER TABLE ").appendIdentifier(schema, table).append(" ADD CONSTRAINT ")
+                .appendIdentifier(name).append(" FOREIGN KEY(").appendIdentifier(column).append(") REFERENCES ")
+                .appendIdentifier(schema, target).append(" (").append(primaryKey)
+                .append(") ON UPDATE ").append(cascade ? "CASCADE" : "RESTRICT")
+                .append(" ON DELETE RESTRICT").toString();
+    }
+
+    /**
+     * Returns the string representation of the given value with simple quote doubled.
+     *
+     * @param  value  the object for which to double the quotes in the string representation.
+     * @return a string representation of the given object with simple quotes doubled.
+     */
+    public static String doubleQuotes(final Object value) {
+        return value.toString().replace("'", "''");
+    }
+
+    /**
+     * Returns the SQL statement.
+     *
+     * @return the SQL statement.
+     */
+    @Override
+    public final String toString() {
+        return buffer.toString();
+    }
+}

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/SQLBuilder.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/SQLBuilder.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Added: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/TypeMapper.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/TypeMapper.java?rev=1770500&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/TypeMapper.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/TypeMapper.java [UTF-8] Sat Nov 19 15:39:49 2016
@@ -0,0 +1,110 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.metadata.sql;
+
+import java.util.Date;
+import java.sql.Types;
+
+
+/**
+ * Maps a few basic Java types to JDBC types.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+public final class TypeMapper {
+    /**
+     * A list of Java classes to be mapped to SQL types. We do not want to map every SQL types,
+     * but only the ones which is of interest for the Apache SIS metadata implementation.
+     * The types will be tested in the order they are declared, so the last declarations are fallbacks.
+     *
+     * <p>The types declared here matches both the JavaDB and PostgreSQL mapping.</p>
+     */
+    private static final TypeMapper[] TYPES = {
+        // Note: JavaDB does not supports BOOLEAN as of Derby 10.5.
+        // See http://issues.apache.org/jira/browse/DERBY-499
+        new TypeMapper(Boolean.class, Types.BOOLEAN,   "BOOLEAN"),
+        new TypeMapper(Date   .class, Types.TIMESTAMP, "TIMESTAMP"),
+        new TypeMapper(Double .class, Types.DOUBLE,    "DOUBLE PRECISION"),
+        new TypeMapper(Float  .class, Types.REAL  ,    "REAL"),
+        new TypeMapper(Long   .class, Types.BIGINT,    "BIGINT"),
+        new TypeMapper(Integer.class, Types.INTEGER,   "INTEGER"),
+        new TypeMapper(Short  .class, Types.SMALLINT,  "SMALLINT"),
+        new TypeMapper(Byte   .class, Types.TINYINT,   "SMALLINT"),     // JavaDB does not support TINYINT.
+        new TypeMapper(Number .class, Types.DECIMAL,   "DECIMAL")       // Implemented by BigDecimal.
+    };
+
+    /**
+     * The Java class.
+     */
+    private final Class<?> classe;
+
+    /**
+     * A constant from the SQL {@link Types} enumeration.
+     */
+    private final int type;
+
+    /**
+     * The SQL keyword for that type.
+     */
+    private final String keyword;
+
+    /**
+     * For internal use only.
+     */
+    private TypeMapper(final Class<?> classe, final int type, final String keyword) {
+        this.classe  = classe;
+        this.type    = type;
+        this.keyword = keyword;
+    }
+
+    /**
+     * Returns the SQL keyword for storing an element of the given type, or {@code null} if unknown.
+     * This method does not handle the text type, so {@link String} are treated as "unknown" as well.
+     * We do that way because the caller will need to specify a value in {@code VARCHAR(n)} statement.
+     *
+     * @param  classe  the class for which to get the SQL keyword in a {@code CREATE TABLE} statement.
+     * @return the SQL keyword, or {@code null} if unknown.
+     */
+    public static String keywordFor(final Class<?> classe) {
+        if (classe != null) {
+            for (final TypeMapper type : TYPES) {
+                if (type.classe.isAssignableFrom(classe)) {
+                    return type.keyword;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the Java class for the given SQL type, or {@code null} if none.
+     *
+     * @param  type  one of the {@link Types} constants.
+     * @return the Java class, or {@code null} if none.
+     */
+    public static Class<?> toJavaType(final int type) {
+        for (final TypeMapper t : TYPES) {
+            if (t.type == type) {
+                return t.classe;
+            }
+        }
+        return null;
+    }
+}

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/TypeMapper.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/TypeMapper.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Added: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/CacheKey.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/CacheKey.java?rev=1770500&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/CacheKey.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/CacheKey.java [UTF-8] Sat Nov 19 15:39:49 2016
@@ -0,0 +1,70 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.metadata.sql;
+
+import java.util.Objects;
+
+
+/**
+ * The key for an entry in the {@link MetadataSource} cache.
+ *
+ * @author  Touraïvane (IRD)
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+final class CacheKey {
+    /**
+     * The metadata interface to be implemented.
+     */
+    private final Class<?> type;
+
+    /**
+     * The primary key for the entry in the table.
+     */
+    private final String identifier;
+
+    /**
+     * Creates a new key.
+     */
+    CacheKey(final Class<?> type, final String identifier) {
+        this.type = type;
+        this.identifier = identifier;
+    }
+
+    /**
+     * Compares the given object with this key for equality.
+     */
+    @Override
+    public boolean equals(final Object other) {
+        if (other instanceof CacheKey) {
+            final CacheKey that = (CacheKey) other;
+            return Objects.equals(this.type,       that.type) &&
+                   Objects.equals(this.identifier, that.identifier);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a hash code for this key.
+     */
+    @Override
+    public int hashCode() {
+        return type.hashCode() ^ identifier.hashCode();
+    }
+}

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/CacheKey.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/CacheKey.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Modified: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataResult.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataResult.java?rev=1770500&r1=1770499&r2=1770500&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataResult.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataResult.java [UTF-8] Sat Nov 19 15:39:49 2016
@@ -36,43 +36,46 @@ import org.apache.sis.internal.system.Lo
  * <div class="section"><b>Synchronization</b>:
  * This class is <strong>not</strong> thread-safe. Callers must perform their own synchronization in such a way
  * that only one query is executed on the same connection (JDBC connections can not be assumed thread-safe).
- * The synchronization block shall be the {@link Tables} which contain this entry.</div>
+ * The synchronization block shall be the {@link ResultPool} which contain this entry.</div>
  *
  * <div class="section"><b>Closing</b>:
- * This class does not implement {@link java.lang.AutoCloseable} because it is typically closed by a different
- * thread than the one that created the {@code MetadataResult} instance. This object is closed by a background
- * thread of {@link Tables}.</div>
+ * While this class implements {@link java.lang.AutoCloseable}, it should not be used in a try-finally block.
+ * This is because {@code MetadataResult} is typically closed by a different thread than the one that created
+ * the {@code MetadataResult} instance. This object is closed by a background thread of {@link Tables}.</div>
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @since   0.8
  * @version 0.8
  * @module
  */
-final class MetadataResult {
+final class MetadataResult implements AutoCloseable {
     /**
-     * The implemented interface, used for formatting error messages.
+     * The interface for which the prepared statement has been created.
      */
-    private final Class<?> type;
+    final Class<?> type;
 
     /**
-     * The statement associated with this entry.
-     * This is the statement given to the constructor.
+     * The identifier (usually the primary key) for current results. If the record to fetch does not
+     * have the same identifier, then the {@link #results} will need to be closed and reconstructed.
      */
-    private final PreparedStatement statement;
+    private String identifier;
 
     /**
-     * The results, or {@code null} if not yet determined.
+     * The statement associated with this entry. The SQL query depends on the {@link #type},
+     * which can not be changed, and the {@link #identifier}, which can be changed at any time.
+     * The first parameter of the statement shall be the identifier.
      */
-    private ResultSet results;
+    private final PreparedStatement statement;
 
     /**
-     * The identifier (usually the primary key) for current results. If the record to fetch does not
-     * have the same identifier, then the {@link #results} will need to be closed and reconstructed.
+     * The results of last call to {@link PreparedStatement#executeQuery()},
+     * or {@code null} if not yet determined.
      */
-    private String identifier;
+    private ResultSet results;
 
     /**
-     * The expiration time of this result. This is read and updated by {@link Tables} only.
+     * The expiration time of this result, in nanoseconds as given by {@link System#nanoTime()}.
+     * This is read and updated by {@link ResultPool} only.
      */
     long expireTime;
 
@@ -105,7 +108,7 @@ final class MetadataResult {
      * @throws SQLException if an SQL operation failed.
      * @throws MetadataStoreException if no record has been found for the given key.
      */
-    Object getValue(final String id, final String attribute) throws SQLException, MetadataStoreException {
+    final Object getValue(final String id, final String attribute) throws SQLException, MetadataStoreException {
         if (!id.equals(identifier)) {
             closeResultSet();
         }
@@ -147,37 +150,18 @@ final class MetadataResult {
      * Closes the statement and free all resources.
      * After this method has been invoked, this object can not be used anymore.
      *
-     * <p>This method is usually not invoked by the method or thread that created the
-     * {@code StatementEntry} instance. It is invoked by {@link Tables#close()} instead.</p>
+     * <p>This method is not invoked by the method or thread that created this {@code MetadataResult} instance.
+     * This method is invoked by {@link ResultPool#close()} instead.</p>
      *
      * @throws SQLException if an error occurred while closing the statement.
      */
-    void close() throws SQLException {
+    @Override
+    public void close() throws SQLException {
         closeResultSet();
         statement.close();
     }
 
     /**
-     * Closes the statement and free all resources. In case of failure while closing JDBC objects,
-     * the message is logged but the process continue since we are not supposed to use the statement anymore.
-     * This method is invoked from other methods that can not throws an SQL exception.
-     */
-    void closeQuietly() {
-        try {
-            close();
-        } catch (Exception e) {
-            /*
-             * Catch Exception rather than SQLException because this method is invoked from semi-critical code
-             * which need to never fail, otherwise some memory leak could occur. Pretend that the message come
-             * from PreparedStatement.close(), which is the closest we can get to a public API.
-             */
-            final LogRecord record = new LogRecord(Level.WARNING, e.toString());
-            record.setThrown(e);
-            warning(PreparedStatement.class, "close", record);
-        }
-    }
-
-    /**
      * Reports a warning.
      *
      * @param source  the class to report as the warning emitter.

Modified: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataSource.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataSource.java?rev=1770500&r1=1770499&r2=1770500&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataSource.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/MetadataSource.java [UTF-8] Sat Nov 19 15:39:49 2016
@@ -20,17 +20,24 @@ import java.util.Set;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.Collections;
+import java.util.Iterator;
+import java.lang.reflect.Method;
 import java.sql.SQLException;
 import javax.sql.DataSource;
+import org.opengis.annotation.UML;
 import org.opengis.metadata.distribution.Format;
 import org.apache.sis.metadata.MetadataStandard;
+import org.apache.sis.metadata.KeyNamePolicy;
+import org.apache.sis.metadata.ValueExistencePolicy;
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
 import org.apache.sis.metadata.iso.distribution.DefaultFormat;
 import org.apache.sis.internal.system.Modules;
 import org.apache.sis.internal.system.SystemListener;
 import org.apache.sis.internal.metadata.sql.Initializer;
+import org.apache.sis.util.collection.WeakValueHashMap;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.ObjectConverter;
 import org.apache.sis.util.iso.Types;
 
 
@@ -87,6 +94,30 @@ public class MetadataSource {
     private final Map<String, Set<String>> tables;
 
     /**
+     * The prepared statements created in previous calls to {@link #getValue(Class, Method, String)} method.
+     * Those statements are encapsulated into {@link MetadataResult} objects.
+     * This object is also the lock on which every SQL query must be guarded.
+     * We use this object because SQL queries will typically involve usage of this map.
+     */
+    private final ResultPool statements;
+
+    /**
+     * The previously created objects.
+     * Used in order to share existing instances for the same interface and primary key.
+     */
+    private final WeakValueHashMap<CacheKey,Object> cache;
+
+    /**
+     * The last converter used.
+     */
+    private transient volatile ObjectConverter<?,?> lastConverter;
+
+    /**
+     * The class loader to use for proxy creation.
+     */
+    private final ClassLoader loader;
+
+    /**
      * The default instance, created when first needed and cleared when the classpath change.
      */
     private static volatile MetadataSource instance;
@@ -145,7 +176,84 @@ public class MetadataSource {
         this.standard = standard;
         this.schema   = schema;
         this.tables   = new HashMap<>();
-        // TODO
+        statements    = new ResultPool(dataSource, this);
+        cache         = new WeakValueHashMap<>(CacheKey.class);
+        loader        = getClass().getClassLoader();
+    }
+
+    /**
+     * Creates a new metadata source with the same configuration than the given source.
+     * The two sources will share the same data source but will use their own {@linkplain Connection connection}.
+     * This constructor is useful when concurrency is desired.
+     *
+     * @param  source  the source from which to copy the configuration.
+     */
+    public MetadataSource(final MetadataSource source) {
+        ArgumentChecks.ensureNonNull("source", source);
+        standard   = source.standard;
+        schema     = source.schema;
+        loader     = source.loader;
+        tables     = new HashMap<>();
+        statements = new ResultPool(source.statements);
+        cache      = new WeakValueHashMap<>(CacheKey.class);
+    }
+
+    /**
+     * If the given value is a collection, returns the first element in that collection
+     * or {@code null} if empty.
+     *
+     * @param  value  the value to inspect (can be {@code null}).
+     * @return the given value, or its first element if the value is a collection,
+     *         or {@code null} if the given value is null or an empty collection.
+     */
+    private static Object extractFromCollection(Object value) {
+        while (value instanceof Iterable<?>) {
+            final Iterator<?> it = ((Iterable<?>) value).iterator();
+            if (!it.hasNext()) {
+                return null;
+            }
+            if (value == (value = it.next())) break;
+        }
+        return value;
+    }
+
+    /**
+     * Returns the table name for the specified class.
+     * This is usually the ISO 19115 name.
+     */
+    private static String getTableName(final Class<?> type) {
+        final UML annotation = type.getAnnotation(UML.class);
+        if (annotation == null) {
+            return type.getSimpleName();
+        }
+        final String name = annotation.identifier();
+        return name.substring(name.lastIndexOf('.') + 1);
+    }
+
+    /**
+     * Returns the column name for the specified method.
+     */
+    private static String getColumnName(final Method method) {
+        final UML annotation = method.getAnnotation(UML.class);
+        if (annotation == null) {
+            return method.getName();
+        }
+        final String name = annotation.identifier();
+        return name.substring(name.lastIndexOf('.') + 1);
+    }
+
+    /**
+     * Returns a view of the given metadata as a map. This method returns always a map using UML identifier
+     * and containing all entries including the null ones because the {@code MetadataSource} implementation
+     * assumes so.
+     *
+     * @param  metadata  the metadata object to view as a map.
+     * @return a map view over the metadata object.
+     * @throws ClassCastException if the metadata object does not implement a metadata interface
+     *         of the expected package.
+     */
+    final Map<String,Object> asMap(final Object metadata) throws ClassCastException {
+        return standard.asValueMap(metadata, KeyNamePolicy.UML_IDENTIFIER, ValueExistencePolicy.ALL);
     }
 
     /**

Added: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/ResultPool.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/ResultPool.java?rev=1770500&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/ResultPool.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/ResultPool.java [UTF-8] Sat Nov 19 15:39:49 2016
@@ -0,0 +1,334 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.metadata.sql;
+
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.PreparedStatement;
+import javax.sql.DataSource;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.logging.WarningListeners;
+import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.internal.system.DelayedExecutor;
+import org.apache.sis.internal.system.DelayedRunnable;
+import org.apache.sis.internal.metadata.sql.Initializer;
+import org.apache.sis.internal.metadata.sql.SQLBuilder;
+
+
+/**
+ * A pool of prepared statements with a maximal capacity. Oldest statements are automatically
+ * closed and removed from the map when the number of statements exceed the maximal capacity.
+ * Inactive statements are also closed after some timeout.
+ *
+ * <div class="note"><b>Note:</b>
+ * this class duplicates the work done by statement pools in modern JDBC drivers. Nevertheless
+ * it still useful in our case since we retain some additional JDBC resources together with the
+ * {@link PreparedStatement}, for example the {@link ResultSet} created from that statement.</div>
+ *
+ * Every access to this pool <strong>must</strong> be synchronized on {@code this}.
+ * Synchronization is caller's responsibility; this class is not thread safe alone.
+ * Synchronization will be verified if assertions are enabled.
+ *
+ * <div class="note"><b>Rational:</b>
+ * synchronization must be performed by the caller because we typically need synchronized block
+ * wider than the {@code get} and {@code put} scope. Execution of a prepared statement may also
+ * need to be done inside the synchronized block, because a single JDBC connection can not be
+ * assumed thread-safe.</div>
+ *
+ * <p>Usage example:</p>
+ * {@preformat java
+ *     ResultPool pool = …;
+ *     Class<?>   type = …;
+ *     synchronized (pool) {
+ *         // Get an entry, or create a new one if no entry is available.
+ *         MetadataResult statement = pool.take(type, preferredIndex);
+ *         if (statement == null) {
+ *             statement = new MetadataResult(someStatement);
+ *         }
+ *         // Use the statement and give it back to the pool once we are done.
+ *         // We do not put it back in case of SQLException.
+ *         Object value = statement.getValue(…);
+ *         preferredIndex = pool.recycle(statement, preferredIndex);
+ *     }
+ * }
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+final class ResultPool {
+    /**
+     * The timeout before to close a prepared statement, in nanoseconds. This is set to 2 seconds,
+     * which is a bit short but should be okay if the {@link DataSource} creates pooled connections.
+     * In case there is no connection pool, then the mechanism defined in this package will hopefully
+     * keeps the performance at a reasonable level.
+     */
+    private static final long TIMEOUT = 2000_000000;
+
+    /**
+     * An extra delay to add to the {@link #TIMEOUT} in order to increase the chances to
+     * close many statements at once.
+     */
+    private static final int EXTRA_DELAY = 500_000000;
+
+    /**
+     * The data source object for fetching the connection to the database.
+     */
+    private final DataSource dataSource;
+
+    /**
+     * The connection to the database. This is automatically closed and set to {@code null}
+     * after all statements have been closed.
+     *
+     * @see #connection()
+     */
+    private Connection connection;
+
+    /**
+     * JDBC statements available for reuse. The array length is the maximal number of statements that can
+     * be cached; it should be a reasonably small length. This array may contain null element anywhere.
+     */
+    private final MetadataResult[] cache;
+
+    /**
+     * Whether at least one {@link CloseTask} is scheduled for execution.
+     */
+    private boolean isCloseScheduled;
+
+    /**
+     * Where to report the warnings. This is not necessarily a logger, since users can register listeners.
+     */
+    private final WarningListeners<MetadataSource> listeners;
+
+    /**
+     * A helper class used for constructing SQL statements.
+     * This helper is created when first needed, then kept until the connection is closed.
+     */
+    private SQLBuilder helper;
+
+    /**
+     * Creates a new pool for the given data source.
+     *
+     * @param dataSource  the source of connections to the database.
+     * @param owner       the object that will contain this {@code ResultPool}.
+     */
+    ResultPool(final DataSource dataSource, final MetadataSource owner) {
+        this.dataSource = dataSource;
+        this.listeners  = new WarningListeners<>(owner);
+        this.cache      = new MetadataResult[10];
+    }
+
+    /**
+     * Creates a new pool with the same configuration than the given pool.
+     * The new pool will use its own connection - it will not be shared.
+     *
+     * @param source  the pool from which to copy the configuration.
+     */
+    ResultPool(final ResultPool source) {
+        dataSource = source.dataSource;
+        listeners  = source.listeners;
+        cache      = new MetadataResult[source.cache.length];
+    }
+
+    /**
+     * Returns the connection to the database, creating a new one if needed. This method shall
+     * be invoked inside a synchronized block wider than just the scope of this method in order
+     * to ensure that the connection is used by only one thread at time. This is also necessary
+     * for preventing the background thread to close the connection too early.
+     *
+     * @return the connection to the database.
+     * @throws SQLException if an error occurred while fetching the connection.
+     */
+    final Connection connection() throws SQLException {
+        assert Thread.holdsLock(this);
+        Connection c = connection;
+        if (c == null) {
+            connection = c = dataSource.getConnection();
+            Logging.log(MetadataSource.class, "lookup", Initializer.connected(connection.getMetaData()));
+        }
+        return c;
+    }
+
+    /**
+     * Returns a helper class for building SQL statements.
+     */
+    final SQLBuilder helper() throws SQLException {
+        assert Thread.holdsLock(this);
+        if (helper == null) {
+            helper = new SQLBuilder(connection().getMetaData());
+        }
+        return helper;
+    }
+
+    /**
+     * Returns a statement that can be reused for the given interface, or {@code null} if none.
+     *
+     * @param  type            the interface for which to reuse a prepared statement.
+     * @param  preferredIndex  index in the cache array where to search first. This is only a hint for increasing
+     *         the chances to find quickly a {@code MetadataResult} instance for the right type and identifier.
+     */
+    final MetadataResult take(final Class<?> type, final int preferredIndex) {
+        assert Thread.holdsLock(this);
+        if (preferredIndex >= 0 && preferredIndex < cache.length) {
+            final MetadataResult statement = cache[preferredIndex];
+            if (statement != null && statement.type == type) {
+                cache[preferredIndex] = null;
+                return statement;
+            }
+        }
+        for (int i=0; i < cache.length; i++) {
+            final MetadataResult statement = cache[i];
+            if (statement != null && statement.type == type) {
+                cache[i] = null;
+                return statement;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Flags the given {@code MetadataResult} as available for reuse.
+     *
+     * @param  statement       the prepared statement to cache.
+     * @param  preferredIndex  index in the cache array to use if the corresponding slot is available.
+     * @return index in the cache array where the result has been actually stored, or -1 if none.
+     */
+    final int recycle(final MetadataResult statement, int preferredIndex) {
+        assert Thread.holdsLock(this);
+        if (preferredIndex < 0 || preferredIndex >= cache.length || cache[preferredIndex] != null) {
+            preferredIndex = 0;
+            while (cache[preferredIndex] != null) {
+                if (++preferredIndex >= cache.length) {
+                    return -1;
+                }
+            }
+        }
+        cache[preferredIndex] = statement;
+        statement.expireTime = System.nanoTime() + TIMEOUT;
+        if (!isCloseScheduled) {
+            DelayedExecutor.schedule(new CloseTask(System.nanoTime() + (TIMEOUT + EXTRA_DELAY)));
+            isCloseScheduled = true;
+        }
+        return preferredIndex;
+    }
+
+    /**
+     * A task to be executed later for closing all expired {@link MetadataResult}.
+     * A result is expired if {@link MetadataResult#expireTime} is later than {@link System#nanoTime()}.
+     */
+    private final class CloseTask extends DelayedRunnable {
+        /**
+         * Creates a new task to be executed later.
+         *
+         * @param timestamp  time of execution of this task, in nanoseconds relative to {@link System#nanoTime()}.
+         */
+        CloseTask(final long timestamp) {
+            super(timestamp);
+        }
+
+        /**
+         * Invoked in a background thread for closing all expired {@link MetadataResult} instances.
+         */
+        @Override public void run() {
+            closeExpired();
+        }
+    }
+
+    /**
+     * Executed in a background thread for closing statements after their expiration time.
+     * This task will be given to the executor every time the first statement is recycled.
+     */
+    final synchronized void closeExpired() {
+        isCloseScheduled = false;
+        long delay = 0;
+        final long currentTime = System.nanoTime();
+        for (int i=0; i < cache.length; i++) {
+            final MetadataResult statement = cache[i];
+            if (statement != null) {
+                /*
+                 * Note: we really need to compute t1 - t0 and compare the delays.
+                 * Do not simplify the equations in a way that result in comparisons
+                 * like t1 > t0. See System.nanoTime() javadoc for more information.
+                 */
+                final long wait = statement.expireTime - currentTime;
+                if (wait > delay) {
+                    delay = wait;
+                } else {
+                    cache[i] = null;
+                    closeQuietly(statement);
+                }
+            }
+        }
+        if (delay > 0) {
+            // Some statements can not be disposed yet.
+            DelayedExecutor.schedule(new CloseTask(currentTime + delay + EXTRA_DELAY));
+            isCloseScheduled = true;
+        } else {
+            // No more prepared statements.
+            final Connection c = this.connection;
+            connection = null;
+            helper = null;
+            closeQuietly(c);
+        }
+    }
+
+    /**
+     * Closes the given resource without throwing exception. In case of failure while closing the resource,
+     * the message is logged but the process continue since we are not supposed to use the resource anymore.
+     * This method is invoked from methods that can not throw a SQL exception.
+     */
+    private void closeQuietly(final AutoCloseable resource) {
+        if (resource != null) try {
+            resource.close();
+        } catch (Exception e) {
+            /*
+             * Catch Exception rather than SQLException because this method is invoked from semi-critical code
+             * which need to never fail, otherwise some memory leak could occur. Pretend that the message come
+             * from ResultPool.closeExpired(), which is the closest we can get to a public API.
+             */
+            final LogRecord record = new LogRecord(Level.WARNING, e.toString());
+            record.setSourceClassName(ResultPool.class.getCanonicalName());
+            record.setSourceMethodName("closeExpired");
+            record.setLoggerName(Loggers.SQL);
+            record.setThrown(e);
+            listeners.warning(record);
+        }
+    }
+
+    /**
+     * Closes all statements and removes them from the pool.
+     *
+     * @throws SQLException if an error occurred while closing the statements.
+     */
+    final synchronized void close() throws SQLException {
+        for (int i=0; i < cache.length; i++) {
+            final MetadataResult statement = cache[i];
+            if (statement != null) {
+                statement.close();
+                cache[i] = null;
+            }
+        }
+        if (connection != null) {
+            connection.close();
+            connection = null;
+        }
+        helper = null;
+    }
+}

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/ResultPool.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/ResultPool.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Added: sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/sql/TypeMapperTest.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/sql/TypeMapperTest.java?rev=1770500&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/sql/TypeMapperTest.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/sql/TypeMapperTest.java [UTF-8] Sat Nov 19 15:39:49 2016
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.metadata.sql;
+
+import java.sql.Types;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+
+/**
+ * Tests {@link TypeMapper}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.8
+ * @version 0.8
+ * @module
+ */
+public final strictfp class TypeMapperTest extends TestCase {
+    /**
+     * Tests {@link TypeMapper#toJavaType(int)}.
+     */
+    @Test
+    public void testToJavaType() {
+        assertEquals(Integer.class, TypeMapper.toJavaType(Types.INTEGER));
+        assertEquals(Boolean.class, TypeMapper.toJavaType(Types.BOOLEAN));
+        assertNull  (               TypeMapper.toJavaType(Types.LONGVARCHAR));
+    }
+}

Propchange: sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/sql/TypeMapperTest.java
------------------------------------------------------------------------------
    svn:eol-style = native

Propchange: sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/internal/metadata/sql/TypeMapperTest.java
------------------------------------------------------------------------------
    svn:mime-type = text/plain;charset=UTF-8

Modified: sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java?rev=1770500&r1=1770499&r2=1770500&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java [UTF-8] Sat Nov 19 15:39:49 2016
@@ -26,7 +26,7 @@ import org.junit.BeforeClass;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.3
- * @version 0.7
+ * @version 0.8
  * @module
  */
 @Suite.SuiteClasses({
@@ -101,7 +101,8 @@ import org.junit.BeforeClass;
     org.apache.sis.io.wkt.FormatterTest.class,
     org.apache.sis.io.wkt.ElementTest.class,
 
-    org.apache.sis.internal.metadata.sql.SQLUtilitiesTest.class
+    org.apache.sis.internal.metadata.sql.SQLUtilitiesTest.class,
+    org.apache.sis.internal.metadata.sql.TypeMapperTest.class
 })
 public final strictfp class MetadataTestSuite extends TestSuite {
     /**



Mime
View raw message