sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1728021 - in /sis/branches/JDK8/core: sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/ sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/ sis-referencing/src/test/java/org/apache/sis/referencing/factory/s...
Date Mon, 01 Feb 2016 23:12:08 GMT
Author: desruisseaux
Date: Mon Feb  1 23:12:08 2016
New Revision: 1728021

URL: http://svn.apache.org/viewvc?rev=1728021&view=rev
Log:
Initial port of the EPSG installer (needs tests).

Added:
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Dialect.java   (with props)
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/ScriptRunner.java   (with props)
    sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGInstaller.java   (with props)
Modified:
    sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGFactory.java
    sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLTranslator.java
    sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.properties
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages_fr.properties

Added: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Dialect.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Dialect.java?rev=1728021&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Dialect.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Dialect.java [UTF-8] Mon Feb  1 23:12:08 2016
@@ -0,0 +1,100 @@
+/*
+ * 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.SQLException;
+import java.sql.DatabaseMetaData;
+import org.apache.sis.util.CharSequences;
+
+
+/**
+ * The SQL dialect used by a connection. This class defines also a few driver-specific operations
+ * that can not (to our knowledge) be inferred from the {@link DatabaseMetaData}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.7
+ * @version 0.7
+ * @module
+ */
+enum Dialect {
+    /**
+     * The database is presumed to use ANSI SQL syntax.
+     */
+    ANSI(null),
+
+    /**
+     * The database uses Derby syntax. This is ANSI, with some constraints that PostgreSQL does not have
+     * (for example column with {@code UNIQUE} constraint must explicitly be specified as {@code NOT NULL}).
+     */
+    DERBY("derby"),
+
+    /**
+     * The database uses HSQL syntax. This is ANSI, but does not allow {@code INSERT} statements inserting many lines.
+     * It also have a {@code SHUTDOWN} command which is specific to HSQLDB.
+     */
+    HSQL("hsqldb"),
+
+    /**
+     * The database uses PostgreSQL syntax. This is ANSI, but provided an a separated
+     * enumeration value because it allows a few additional commands like {@code VACUUM}.
+     */
+    POSTGRESQL("postgresql"),
+
+    /**
+     * The database uses Oracle syntax. This is ANSI, but without {@code "AS"} keyword.
+     */
+    ORACLE("oracle");
+
+    /**
+     * The protocol in JDBC URL, or {@code null} if unknown.
+     * This is the part after {@code "jdbc:"} and before the next {@code ':'}.
+     */
+    private final String protocol;
+
+    /**
+     * Creates a new enumeration value for a SQL dialect for the given protocol.
+     */
+    private Dialect(final String protocol) {
+        this.protocol = protocol;
+    }
+
+    /**
+     * Returns the presumed SQL dialect.
+     *
+     * @param  metadata The database metadata.
+     * @return The presumed SQL dialect.
+     * @throws SQLException if an error occurred while querying the metadata.
+     */
+    public static Dialect guess(final DatabaseMetaData metadata) throws SQLException {
+        final String url = metadata.getURL();
+        if (url != null) {
+            int start = url.indexOf(':');
+            if (start >= 0 && "jdbc".equalsIgnoreCase((String) CharSequences.trimWhitespaces(url, 0, start))) {
+                final int end = url.indexOf(':', ++start);
+                if (end >= 0) {
+                    final String protocol = (String) CharSequences.trimWhitespaces(url, start, end);
+                    for (final Dialect candidate : values()) {
+                        if (protocol.equalsIgnoreCase(candidate.protocol)) {
+                            return candidate;
+                        }
+                    }
+                }
+            }
+        }
+        return ANSI;
+    }
+}

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

Propchange: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/Dialect.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/ScriptRunner.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/ScriptRunner.java?rev=1728021&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/ScriptRunner.java (added)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/sql/ScriptRunner.java [UTF-8] Mon Feb  1 23:12:08 2016
@@ -0,0 +1,623 @@
+/*
+ * 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.Map;
+import java.util.HashMap;
+import java.util.Locale;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.io.StringReader;
+import java.sql.Statement;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.sql.DatabaseMetaData;
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.CharSequences;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * Run SQL scripts. The script is expected to use a standardized syntax, where the {@value #QUOTE} character
+ * is used for quoting text, the {@value #IDENTIFIER_QUOTE} character is used for quoting identifier and the
+ * {@value #END_OF_STATEMENT} character is used at the end for every SQL statement. Those characters will be
+ * replaced on-the-fly by the characters actually used by the database engine.
+ *
+ * <p><strong>This class is not intended for executing arbitrary SQL scripts.</strong>
+ * This class is for executing known scripts bundled with Apache SIS or in an extension
+ * (for example the scripts for creating the EPSG database). We do not try to support SQL
+ * functionalities other than what we need for those scripts.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.7
+ * @version 0.7
+ * @module
+ */
+public class ScriptRunner implements AutoCloseable {
+    /**
+     * The database user having read (not write) permissions.
+     *
+     * @see #isGrantOnSchemaSupported
+     * @see #isGrantOnTableSupported
+     */
+    protected static final String PUBLIC = "PUBLIC";
+
+    /**
+     * The sequence for SQL comments. Leading lines starting by those characters will be ignored.
+     */
+    private static final String COMMENT = "--";
+
+    /**
+     * The quote character expected to be found in the SQL script.
+     * This character shall not be a whitespace or a Unicode identifier part.
+     */
+    private static final char QUOTE = '\'';
+
+    /**
+     * The quote character for identifiers expected to be found in the SQL script.
+     * This character shall not be a whitespace or a Unicode identifier part.
+     */
+    private static final char IDENTIFIER_QUOTE = '"';
+
+    /**
+     * The character at the end of statements.
+     * This character shall not be a whitespace or a Unicode identifier part.
+     */
+    private static final char END_OF_STATEMENT = ';';
+
+    /**
+     * The characters for escaping a portion of the SQL script. This is used by PostgreSQL
+     * for the definition of triggers. Those characters should appear at the beginning of
+     * a line (ignoring whitespaces), because the text before it will not be parsed.
+     *
+     * <p>This string shall not begin with a whitespace or
+     * {@linkplain Character#isUnicodeIdentifierPart(int) Unicode identifier part}.</p>
+     */
+    private static final String ESCAPE = "$BODY$";
+
+    /**
+     * The character encoding of SQL scripts. Typical values are {@code "UTF-8"} or {@code "ISO-8859-1"}.
+     * For SQL scripts provided by the EPSG, the encoding shall be {@code "ISO-8859-1"}.
+     */
+    private final String encoding;
+
+    /**
+     * The presumed dialect spoken by the database.
+     */
+    private final Dialect dialect;
+
+    /**
+     * A mapping of words to replace. The replacements are performed only for occurrences outside identifiers or texts.
+     * See {@link #replace(String, String)} for more explanation.
+     *
+     * @see #replace(String, String)
+     */
+    private final Map<String,String> replacements = new HashMap<>();
+
+    /**
+     * A sentinel value for the {@linkplain #replace replacements} map meaning that {@code ScriptRunner}
+     * needs to look also at the word after the word associated to {@code MORE_WORDS}.
+     *
+     * @see #replace(String, String)
+     */
+    protected static final String MORE_WORDS = "…";
+
+    /**
+     * The quote character for identifiers actually used in the database,
+     * as determined by {@link DatabaseMetaData#getIdentifierQuoteString()}.
+     */
+    protected final String identifierQuote;
+
+    /**
+     * {@code true} if the database supports enums.
+     *
+     * <p>Notes per database product:</p>
+     * <ul>
+     *   <li><b>PostgreSQL:</b> while enumeration were introduced in PostgreSQL 8.3,
+     *       we require PostgreSQL 8.4 because we need the {@code CAST … WITH INOUT} feature.</li>
+     *   <li><b>Other databases:</b> assumed not supported.</li>
+     * </ul>
+     */
+    protected final boolean isEnumTypeSupported;
+
+    /**
+     * {@code true} if the database supports schema.
+     */
+    protected final boolean isSchemaSupported;
+
+    /**
+     * {@code true} if the database supports {@code "GRANT USAGE ON SCHEMA"} statements.
+     * Read-only permissions are typically granted to {@link #PUBLIC}.
+     */
+    protected final boolean isGrantOnSchemaSupported;
+
+    /**
+     * {@code true} if the database supports {@code "GRANT SELECT ON TABLE"} statements.
+     * Read-only permissions are typically granted to {@link #PUBLIC}.
+     */
+    protected final boolean isGrantOnTableSupported;
+
+    /**
+     * {@code true} if the following instruction shall be executed
+     * (assuming that the PostgreSQL {@code "plpgsql"} language is desired):
+     *
+     * {@code sql
+     *   CREATE TRUSTED PROCEDURAL LANGUAGE 'plpgsql'
+     *     HANDLER plpgsql_call_handler
+     *     VALIDATOR plpgsql_validator;
+     * }
+     *
+     * <p>Notes per database product:</p>
+     * <ul>
+     *   <li><b>PostgreSQL:</b> {@code true} only for database prior to version 9.
+     *       Starting at version 9, the language is installed by default.</li>
+     *   <li><b>Other databases:</b> {@code false} because not supported.</li>
+     * </ul>
+     */
+    protected final boolean isCreateLanguageRequired;
+
+    /**
+     * The maximum number of rows allowed per {@code "INSERT"} statement.
+     * This is 1 if the database does not support multi-rows insertion.
+     * For other database, this is set to an arbitrary "reasonable" value since attempts to insert
+     * too many rows with a single statement on Derby database cause a {@link StackOverflowError}.
+     */
+    private final int maxRowsPerInsert;
+
+    /**
+     * The statement created from a connection to the database.
+     */
+    private final Statement statement;
+
+    /**
+     * Name of the SQL script under execution, or {@code null} if unknown.
+     * This is used only for error reporting.
+     */
+    @Debug
+    private String currentFile;
+
+    /**
+     * The line number of the SQL statement being executed. The first line in a file is numbered 1.
+     * This is used only for error reporting.
+     */
+    @Debug
+    private int currentLine;
+
+    /**
+     * The SQL statement being executed.
+     * This is used only for error reporting.
+     */
+    @Debug
+    private String currentSQL;
+
+    /**
+     * Creates a new runner which will execute the statements using the given connection.
+     *
+     * <p>Some {@code maxRowsPerInsert} parameter values of interest:</p>
+     * <ul>
+     *   <li>A value of 0 means to create only the schemas without inserting any data in them.</li>
+     *   <li>A value of 1 means to use one separated {@code INSERT INTO} statement for each row, which may be slow.</li>
+     *   <li>A value of 100 is a value which have been found empirically as giving good results.</li>
+     *   <li>A value of {@link Integer#MAX_VALUE} means to not perform any attempt to limit the number of rows in an
+     *       {@code INSERT INTO} statement. Note that this causes {@link StackOverflowError} in some JDBC driver.</li>
+     * </ul>
+     *
+     * @param connection        The connection to the database.
+     * @param encoding          The encoding of SQL scripts. Typical values are {@code "UTF-8"} or {@code "ISO-8859-1"}.
+     *                          For SQL scripts provided by the EPSG authority, the encoding shall be {@code "ISO-8859-1"}.
+     * @param maxRowsPerInsert  Maximum number of rows per {@code "INSERT INTO"} statement.
+     * @throws SQLException if an error occurred while creating a SQL statement.
+     */
+    protected ScriptRunner(final Connection connection, final String encoding, int maxRowsPerInsert) throws SQLException {
+        ArgumentChecks.ensureNonNull("connection", connection);
+        ArgumentChecks.ensureNonNull("encoding", encoding);
+        ArgumentChecks.ensurePositive("maxRowsPerInsert", maxRowsPerInsert);
+        final DatabaseMetaData metadata = connection.getMetaData();
+        this.encoding          = encoding;
+        this.dialect           = Dialect.guess(metadata);
+        this.identifierQuote   = metadata.getIdentifierQuoteString();
+        this.isSchemaSupported = metadata.supportsSchemasInTableDefinitions() &&
+                                 metadata.supportsSchemasInDataManipulation();
+        switch (dialect) {
+            default: {
+                isEnumTypeSupported      = false;
+                isGrantOnSchemaSupported = false;
+                isGrantOnTableSupported  = false;
+                isCreateLanguageRequired = false;
+                break;
+            }
+            case POSTGRESQL: {
+                final int version = metadata.getDatabaseMajorVersion();
+                isEnumTypeSupported      = (version == 8) ? metadata.getDatabaseMinorVersion() >= 4 : version >= 8;
+                isGrantOnSchemaSupported = true;
+                isGrantOnTableSupported  = true;
+                isCreateLanguageRequired = (version < 9);
+                break;
+            }
+            case HSQL: {
+                isEnumTypeSupported      = false;
+                isGrantOnSchemaSupported = false;
+                isGrantOnTableSupported  = false;
+                isCreateLanguageRequired = false;
+                if (maxRowsPerInsert != 0) {
+                    maxRowsPerInsert = 1;
+                }
+                /*
+                 * HSQLDB does not seem to support the {@code UNIQUE} keyword in {@code CREATE TABLE} statements.
+                 * In addition, we must declare explicitly that we want the tables to be cached on disk. Finally,
+                 * HSQL expects "CHR" to be spelled "CHAR".
+                 */
+                replace("UNIQUE", "");
+                replace("CHR", "CHAR");
+                replace("CREATE", MORE_WORDS);
+                replace("CREATE TABLE", "CREATE CACHED TABLE");
+                break;
+            }
+        }
+        this.maxRowsPerInsert = maxRowsPerInsert;
+        statement = connection.createStatement();
+    }
+
+    /**
+     * Returns the connection to the database.
+     *
+     * @return The connection.
+     * @throws SQLException if the connection can not be obtained.
+     */
+    protected final Connection getConnection() throws SQLException {
+        return statement.getConnection();
+    }
+
+    /**
+     * Declares that a word in the SQL script needs to be replaced by the given word.
+     * The replacement is performed only for occurrences outside identifiers or texts.
+     *
+     * <div class="note"><b>Example</b>
+     * this is used for mapping the table names in the EPSG scripts to table names as they were in the MS-Access
+     * flavor of EPSG database. It may also contains the mapping between SQL keywords used in the SQL scripts to
+     * SQL keywords understood by the database (for example Derby does not support the {@code TEXT} data type,
+     * which need to be replaced by {@code VARCHAR}).</div>
+     *
+     * If a text to replace contains two or more words, then this map needs to contain an entry for the first word
+     * associated to the {@link #MORE_WORDS} value. For example if one needs to replace the {@code "CREATE TABLE"}
+     * words, then in addition to the {@code "CREATE TABLE"} entry this {@code replacements} map shall also contain
+     * a {@code "CREATE"} entry associated with the {@link #MORE_WORDS} value.
+     *
+     * @param inScript The word in the script which need to be replaced.
+     * @param replacement The word to use instead.
+     */
+    protected final void replace(final String inScript, final String replacement) {
+        if (replacements.put(inScript, replacement) != null) {
+            throw new IllegalArgumentException(inScript);
+        }
+    }
+
+    /**
+     * Runs the given SQL script.
+     * Lines are read and grouped up to the terminal {@value #END_OF_STATEMENT} character, then sent to the database.
+     *
+     * @param  statement The SQL statements to execute.
+     * @return The number of rows added or modified as a result of the statement execution.
+     * @throws IOException if an error occurred while reading the input (should never happen).
+     * @throws SQLException if an error occurred while executing a SQL statement.
+     */
+    public final int run(final String statement) throws IOException, SQLException {
+        return run(new LineNumberReader(new StringReader(statement)));
+    }
+
+    /**
+     * Runs the SQL script from the given input stream, which will be closed.
+     * Lines are read and grouped up to the terminal {@value #END_OF_STATEMENT} character, then sent to the database.
+     *
+     * @param  filename Name of the SQL script being executed. This is used only for error reporting.
+     * @param  in The stream to read. <strong>This stream will be closed</strong> at the end.
+     * @return The number of rows added or modified as a result of the script execution.
+     * @throws IOException if an error occurred while reading the input.
+     * @throws SQLException if an error occurred while executing a SQL statement.
+     */
+    public final int run(final String filename, final InputStream in) throws IOException, SQLException {
+        currentFile = filename;
+        final int count;
+        try (LineNumberReader reader = new LineNumberReader(new InputStreamReader(in, encoding))) {
+            count = run(reader);
+        }
+        currentFile = null;
+        return count;
+    }
+
+    /**
+     * Run the script from the given reader. Lines are read and grouped up to the
+     * terminal {@value #END_OF_STATEMENT} character, then sent to the database.
+     *
+     * @param  in The stream to read. It is caller's responsibility to close this reader.
+     * @return The number of rows added or modified as a result of the script execution.
+     * @throws IOException if an error occurred while reading the input.
+     * @throws SQLException if an error occurred while executing a SQL statement.
+     */
+    private int run(final LineNumberReader in) throws IOException, SQLException {
+        int     statementCount     = 0;
+        boolean isInsideText       = false;
+        boolean isInsideIdentifier = false;
+        final StringBuilder buffer = new StringBuilder();
+        String line;
+        while ((line = in.readLine()) != null) {
+            /*
+             * Ignore empty lines and comment lines, but only if they appear at the begining of the SQL statement.
+             */
+            if (buffer.length() == 0) {
+                final int s = CharSequences.skipLeadingWhitespaces(line, 0, line.length());
+                if (s >= line.length() || line.regionMatches(s, COMMENT, 0, COMMENT.length())) {
+                    continue;
+                }
+                currentLine = in.getLineNumber();
+            } else {
+                buffer.append('\n');
+            }
+            /*
+             * If we find the "$BODY$" string, copy verbatism (without any attempt to parse the lines) until
+             * the next occurrence of "$BODY$".  This simple algorithm does not allow more than one block of
+             * "$BODY$ ... $BODY$" on the same statement and presumes that the text before "$BODY$" contains
+             * nothing that need to be parsed.
+             */
+            int pos = line.indexOf(ESCAPE);
+            if (pos >= 0) {
+                pos += ESCAPE.length();
+                while ((pos = line.indexOf(ESCAPE, pos)) < 0) {
+                    buffer.append(line).append('\n');
+                    line = in.readLine();
+                    if (line == null) {
+                        throw new EOFException();
+                    }
+                    pos = 0;
+                }
+                pos += ESCAPE.length();
+                buffer.append(line, 0, pos);
+                line = line.substring(pos);
+            }
+            /*
+             * Copy the current line in the buffer. Then, the loop will search for words or characters to replace
+             * (for example replacements of IDENTIFIER_QUOTE character by the database-specific quote character).
+             * Replacements (if any) will be performed in-place in the buffer. Concequently the buffer length may
+             * vary during the loop execution.
+             */
+            pos = buffer.length();
+            int length = buffer.append(line).length();
+parseLine:  while (pos < length) {
+                int c = buffer.codePointAt(pos);
+                int n = Character.charCount(c);
+                if (!isInsideText && !isInsideIdentifier) {
+                    int start = pos;
+                    while (Character.isUnicodeIdentifierStart(c)) {
+                        /*
+                         * 'start' is the position of the first character of a Unicode identifier. Following loop
+                         * sets 'pos' to the end (exclusive) of that Unicode identifier. Variable 'c' will be set
+                         * to the character after the Unicode identifier, provided that we have not reached EOL.
+                         */
+                        while ((pos += n) < length) {
+                            c = buffer.codePointAt(pos);
+                            n = Character.charCount(c);
+                            if (!Character.isUnicodeIdentifierPart(c)) break;
+                        }
+                        /*
+                         * Perform in-place replacement if the Unicode identifier is one of the keys in the listed
+                         * in the 'replacements' map. This operation may change the buffer length.  The 'pos' must
+                         * be updated if needed for staying the position after the Unicode identifier.
+                         */
+                        final String word = buffer.substring(start, pos);
+                        final String replace = replacements.get(word);
+                        boolean moreWords = false;
+                        if (replace != null) {
+                            moreWords = replace.equals(MORE_WORDS);
+                            if (!moreWords) {
+                                length = buffer.replace(start, pos, replace).length();
+                                pos = start + replace.length();
+                            }
+                        }
+                        /*
+                         * Skip whitespaces and set the 'c' variable to the next character, which may be either
+                         * another Unicode start (to be processed by the enclosing loop) or another character
+                         * (to be processed by the switch statement after the enclosing loop).
+                         */
+                        if (pos >= length) break parseLine;
+                        while (Character.isWhitespace(c)) {
+                            if ((pos += n) >= length) break parseLine;
+                            c = buffer.codePointAt(pos);
+                            n = Character.charCount(c);
+                        }
+                        if (!moreWords) {
+                            start = pos;
+                        }
+                    }
+                }
+                switch (c) {
+                    /*
+                     * Found a character for an identifier like "Coordinate Operations".
+                     * Check if we have found the opening or the closing character. Then
+                     * replace the standard quote character by the database-specific one.
+                     */
+                    case IDENTIFIER_QUOTE: {
+                        if (!isInsideText) {
+                            isInsideIdentifier = !isInsideIdentifier;
+                            length = buffer.replace(pos, pos + n, identifierQuote).length();
+                            n = identifierQuote.length();
+                        }
+                        break;
+                    }
+                    /*
+                     * Found a character for a text like 'This is a text'. Check if we have
+                     * found the opening or closing character, ignoring the '' escape sequence.
+                     */
+                    case QUOTE: {
+                        if (!isInsideIdentifier) {
+                            if (!isInsideText) {
+                                isInsideText = true;
+                            } else if ((pos += n) >= length || buffer.codePointAt(pos) == QUOTE) {
+                                isInsideText = false;
+                                continue;   // Because we already skipped the ' character.
+                            } // else found a double ' character, which means to escape it.
+                        }
+                        break;
+                    }
+                    /*
+                     * Found the end of statement. Remove that character if it is the last non-white character,
+                     * since SQL statement in JDBC are not expected to contain it.
+                     */
+                    case END_OF_STATEMENT: {
+                        if (!isInsideText && !isInsideIdentifier) {
+                            if (CharSequences.skipLeadingWhitespaces(buffer, pos + n, length) >= length) {
+                                buffer.setLength(pos);
+                            }
+                            statementCount += execute(buffer);
+                            buffer.setLength(0);
+                            break parseLine;
+                        }
+                        break;
+                    }
+                }
+                pos += n;
+            }
+        }
+        line = buffer.toString().trim();
+        if (!line.isEmpty() && !line.startsWith(COMMENT)) {
+            throw new EOFException(Errors.format(Errors.Keys.UnexpectedEndOfString_1, line));
+        }
+        return statementCount;
+    }
+
+    /**
+     * Executes the given SQL statement.
+     * This method performs the following choices:
+     *
+     * <ul>
+     *   <li>If the {@code maxRowsPerInsert} argument given at construction time was zero,
+     *       then this method skips {@code "INSERT INTO"} statements but executes all other.</li>
+     *   <li>Otherwise this method executes the given statement with the following modification:
+     *       if the statement is an {@code "INSERT INTO"} with many values, then this method may break
+     *       that statement into many {@code "INSERT INTO"} where each statements does not have move
+     *       than {@code maxRowsPerInsert} rows.</li>
+     * </ul>
+     *
+     * Subclasses that override this method can freely edit the {@link StringBuilder} content before
+     * to invoke this method.
+     *
+     * @param  sql The SQL statement to execute.
+     * @return The number of rows added or modified as a result of the statement execution.
+     * @throws SQLException if an error occurred while executing the SQL statement.
+     * @throws IOException if an I/O operation was required and failed.
+     */
+    protected int execute(final StringBuilder sql) throws SQLException, IOException {
+        String subSQL = currentSQL = CharSequences.trimWhitespaces(sql).toString();
+        int count = 0;
+        /*
+         * The scripts usually do not contain any SELECT statement. One exception is the creation
+         * of geometry columns in a PostGIS database, which use "SELECT AddGeometryColumn(…)".
+         */
+        if (subSQL.startsWith("SELECT ")) {
+            statement.executeQuery(subSQL).close();
+        } else {
+            if (maxRowsPerInsert != Integer.MAX_VALUE && subSQL.startsWith("INSERT INTO")) {
+                if (maxRowsPerInsert == 0) {
+                    subSQL = null;              // Skip completely the "INSERT INTO" statement.
+                } else {
+                    int endOfLine = subSQL.indexOf('\n', 11);                    // 11 is the length of "INSERT INTO".
+                    if (subSQL.regionMatches(endOfLine - 6, "VALUES", 0, 6)) {   //  6 is the length of "VALUES".
+                        /*
+                         * The following code is very specific to the syntax of the scripts generated by SIS.
+                         * This code fetches the "INSERT INTO" part, which is expected to be on its own line.
+                         * We will left this part of the buffer unchanged and write only after the offset.
+                         */
+                        sql.setLength(0);   // Rewrite from the beginning in case we trimmed whitespaces.
+                        final int startOfValues = sql.append(subSQL, 0, endOfLine).append(' ').length();
+                        int nrows = maxRowsPerInsert;
+                        int begin = endOfLine + 1;
+                        while ((endOfLine = subSQL.indexOf('\n', ++endOfLine)) >= 0) {
+                            if (--nrows == 0) {    // Extract lines until we have reached the 'maxRowsPerInsert' amount.
+                                int end = endOfLine;
+                                if (subSQL.charAt(end - 1) == ',') {
+                                    end--;
+                                }
+                                count += statement.executeUpdate(sql.append(subSQL, begin, end).toString());
+                                sql.setLength(startOfValues);       // Prepare for next INSERT INTO statement.
+                                nrows = maxRowsPerInsert;
+                                begin = endOfLine + 1;
+                            }
+                        }
+                        // The remaining of the statement to be executed.
+                        int end = CharSequences.skipTrailingWhitespaces(sql, begin, sql.length());
+                        subSQL = (end > begin) ? sql.append(subSQL, begin, end).toString() : null;
+                    }
+                }
+            }
+            if (subSQL != null) {
+                count += statement.executeUpdate(subSQL);
+            }
+        }
+        currentSQL = null;      // Clear on success only.
+        return count;
+    }
+
+    /**
+     * Closes the statement used by this runner. Note that this method does not close the connection
+     * given to the constructor; this connection still needs to be closed explicitly by the caller.
+     *
+     * @throws SQLException If an error occurred while closing the statement.
+     */
+    @Override
+    public void close() throws SQLException {
+        statement.close();
+    }
+
+    /**
+     * Returns the current position (current file and current line in that file). The returned string may also contain
+     * the SQL statement under execution. The main purpose of this method is to provide information about the position
+     * where an exception occurred.
+     *
+     * @param locale The locale for the message to return.
+     * @return A string representation of the current position, or {@code null} if unknown.
+     */
+    public String status(final Locale locale) {
+        String position = null;
+        if (currentFile != null) {
+            position = Errors.getResources(locale).getString(Errors.Keys.ErrorInFileAtLine_2, currentFile, currentLine);
+        }
+        if (currentSQL != null) {
+            final StringBuilder buffer = new StringBuilder();
+            if (position != null) {
+                buffer.append(position).append('\n');
+            }
+            position = buffer.append("SQL: ").append(currentSQL).toString();
+        }
+        return position;
+    }
+
+    /**
+     * Returns a string representation of this runner for debugging purpose. Current implementation returns the
+     * current position in the script being executed, and the SQL statement. This method may be invoked after a
+     * {@link SQLException} occurred in order to determine the line in the SQL script that caused the error.
+     *
+     * @return The current position in the script being executed.
+     */
+    @Debug
+    @Override
+    public String toString() {
+        return status(null);
+    }
+}

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

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

Modified: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGFactory.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGFactory.java?rev=1728021&r1=1728020&r2=1728021&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGFactory.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGFactory.java [UTF-8] Mon Feb  1 23:12:08 2016
@@ -18,11 +18,14 @@ package org.apache.sis.referencing.facto
 
 import java.util.Collections;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 import java.sql.Connection;
+import java.sql.DatabaseMetaData;
 import java.sql.SQLException;
 import javax.sql.DataSource;
-import java.util.concurrent.TimeUnit;
+import java.io.IOException;
 import org.opengis.util.NameFactory;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.crs.CRSFactory;
@@ -41,6 +44,10 @@ import org.apache.sis.referencing.factor
 import org.apache.sis.referencing.factory.UnavailableFactoryException;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Localized;
+import org.apache.sis.util.ObjectConverters;
+
+// Branch-dependent imports
+import java.nio.file.Path;
 
 
 /**
@@ -129,7 +136,7 @@ public class EPSGFactory extends Concurr
      * @throws FactoryException if the data source can not be obtained.
      */
     public EPSGFactory() throws FactoryException {
-        this(null, null, null, null, null, null, null, null);
+        this(null, null, null, null, null, null, null);
     }
 
     /**
@@ -146,8 +153,6 @@ public class EPSGFactory extends Concurr
      * @param crsFactory    The factory to use for creating {@link CoordinateReferenceSystem} instances.
      * @param copFactory    The factory to use for creating {@link CoordinateOperation} instances.
      * @param mtFactory     The factory to use for creating {@link MathTransform} instances.
-     * @param translator    The translator from the SQL statements using MS-Access dialect to SQL statements
-     *                      using the dialect of the actual database.
      * @throws FactoryException if an error occurred while creating the EPSG factory.
      */
     public EPSGFactory(final DataSource                 dataSource,
@@ -156,8 +161,7 @@ public class EPSGFactory extends Concurr
                        final CSFactory                  csFactory,
                        final CRSFactory                 crsFactory,
                        final CoordinateOperationFactory copFactory,
-                       final MathTransformFactory       mtFactory,
-                       final SQLTranslator              translator)
+                       final MathTransformFactory       mtFactory)
             throws FactoryException
     {
         super(EPSGDataAccess.class);
@@ -177,7 +181,6 @@ public class EPSGFactory extends Concurr
         this.crsFactory   = factory(CRSFactory.class, crsFactory);
         this.copFactory   = factory(CoordinateOperationFactory.class, copFactory);
         this.mtFactory    = factory(MathTransformFactory.class, mtFactory);
-        this.translator   = translator;
         this.locale       = Locale.getDefault(Locale.Category.DISPLAY);
         super.setTimeout(10, TimeUnit.SECONDS);
     }
@@ -226,13 +229,91 @@ public class EPSGFactory extends Concurr
     }
 
     /**
+     * Creates the EPSG schema in the database and populates the tables with geodetic definitions.
+     * This method is invoked automatically when {@link #newDataAccess()} detects that the EPSG dataset is not installed.
+     * Users can also invoke this method explicitely if they wish to install the dataset with custom properties.
+     *
+     * <p>The {@code properties} map is optional.
+     * If non-null, the following properties are recognized (all other properties are ignored):</p>
+     *
+     * <ul class="verbose">
+     *   <li><b>{@code schema}:</b>
+     *     a {@link String} giving the name of the database schema where to create the tables.
+     *     That schema shall not exist prior this method call as it will be created by this {@code install(…)} method.
+     *     If no schema is specified or if the schema is null, then the tables will be created without schema.
+     *     If the database does not {@linkplain DatabaseMetaData#supportsSchemasInTableDefinitions() support
+     *     schema in table definitions} or in {@linkplain DatabaseMetaData#supportsSchemasInDataManipulation()
+     *     data manipulation}, then this property is ignored.</li>
+     *
+     *   <li><b>{@code scriptDirectory}:</b>
+     *     a {@link java.nio.file.Path}, {@link java.io.File} or {@link java.net.URL} to a directory containing
+     *     the SQL scripts to execute. If non-null, that directory shall contain at least files matching the
+     *     {@code *Tables*.sql}, {@code *Data*.sql} and {@code *FKeys*.sql} patterns (those files are provided by EPSG).
+     *     Files matching the {@code *Patches*.sql}, {@code *Indexes*.sql} and {@code *Grant*.sql} patterns
+     *     (provided by Apache SIS) are optional but recommended.
+     *     If no directory is specified, then this method will search for resources provided by the
+     *     {@code geotk-epsg.jar} bundle.</li>
+     * </ul>
+     *
+     * <p><b>Legal constraint:</b>
+     * the EPSG dataset can not be distributed with Apache SIS at this time for licensing reasons.
+     * Users need to either install the dataset manually (for example with the help of this method),
+     * or add on the classpath a non-Apache bundle like {@code geotk-epsg.jar}.
+     * See <a href="https://issues.apache.org/jira/browse/LEGAL-183">LEGAL-183</a> for more information.</p>
+     *
+     * @param  connection Connection to the database where to create the EPSG schema.
+     * @param  properties Properties controlling the schema name and location of SQL scripts, or {@code null} if none.
+     * @throws IOException if the SQL script can not be found or an I/O error occurred while reading them.
+     * @throws SQLException if an error occurred while writing to the database.
+     */
+    public synchronized void install(final Connection connection, Map<?,?> properties) throws IOException, SQLException {
+        ArgumentChecks.ensureNonNull("connection", connection);
+        if (properties == null) {
+            properties = Collections.emptyMap();
+        }
+        final String schema = ObjectConverters.convert(properties.get("schema"), String.class);
+        final Path scriptDirectory = ObjectConverters.convert(properties.get("scriptDirectory"), Path.class);
+        try (EPSGInstaller installer = new EPSGInstaller(connection)) {
+            final boolean ac = connection.getAutoCommit();
+            if (ac) {
+                connection.setAutoCommit(false);
+            }
+            boolean success = false;
+            try {
+                if (schema != null) {
+                    installer.setSchema(schema);
+                }
+                installer.run(scriptDirectory);
+                success = true;
+            } finally {
+                if (ac) {
+                    if (success) {
+                        connection.commit();
+                    } else {
+                        connection.rollback();
+                    }
+                    connection.setAutoCommit(true);
+                }
+                if (!success) {
+                    installer.logFailure(locale);
+                }
+            }
+        }
+    }
+
+    /**
      * Creates the factory which will perform the actual geodetic object creation work.
      * This method is invoked automatically when a new worker is required, either because the previous
      * one has been disposed after its timeout or because a new one is required for concurrency.
      *
-     * <p>The default implementation gets a new connection from the {@link #dataSource} and delegates to
-     * {@link #newDataAccess(Connection, SQLTranslator)}, which provides an easier overriding point
-     * for subclasses wanting to return a custom {@link EPSGDataAccess} instance.</p>
+     * <p>The default implementation performs the following steps:</p>
+     * <ol>
+     *   <li>Gets a new connection from the {@link #dataSource}.</li>
+     *   <li>If this method is invoked for the first time, verifies if the EPSG tables exists.
+     *       If the tables are not found, invokes {@link #install(Connection, Map)}.</li>
+     *   <li>Delegates to {@link #newDataAccess(Connection, SQLTranslator)}, which provides an easier
+     *       overriding point for subclasses wanting to return a custom {@link EPSGDataAccess} instance.</li>
+     * </ol>
      *
      * @return Data Access Object (DAO) to use in {@code createFoo(String)} methods.
      * @throws FactoryException if the constructor failed to connect to the EPSG database.
@@ -248,12 +329,23 @@ public class EPSGFactory extends Concurr
                 synchronized (this) {
                     tr = translator;
                     if (tr == null) {
-                        translator = tr = new SQLTranslator(connection.getMetaData());
+                        tr = new SQLTranslator(connection.getMetaData());
+                        try {
+                            if (!tr.isSchemaFound()) {
+                                install(connection, Collections.singletonMap("schema", Constants.EPSG));
+                                tr.setSchemaFound(connection.getMetaData());   // Set only on success.
+                            }
+                        } finally {
+                            translator = tr;        // Set only after installation in order to block other threads.
+                        }
                     }
                 }
             }
-            return newDataAccess(connection, tr);
-        } catch (Exception e) {
+            if (tr.isSchemaFound()) {
+                return newDataAccess(connection, tr);
+            }
+            connection.close();
+        } catch (Exception e) {                     // Really want to catch all exceptions here.
             if (connection != null) try {
                 connection.close();
             } catch (SQLException e2) {
@@ -261,6 +353,7 @@ public class EPSGFactory extends Concurr
             }
             throw new UnavailableFactoryException(e.getLocalizedMessage(), e);
         }
+        throw new UnavailableFactoryException(SQLTranslator.schemaNotFound(locale));
     }
 
     /**

Added: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGInstaller.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGInstaller.java?rev=1728021&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGInstaller.java (added)
+++ sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGInstaller.java [UTF-8] Mon Feb  1 23:12:08 2016
@@ -0,0 +1,245 @@
+/*
+ * 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.referencing.factory.sql;
+
+import java.util.Locale;
+import java.io.IOException;
+import java.sql.Connection;
+import java.sql.DatabaseMetaData;
+import java.sql.SQLException;
+import java.util.StringTokenizer;
+import java.util.concurrent.TimeUnit;
+import java.util.logging.Level;
+import java.util.logging.LogRecord;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import org.apache.sis.util.StringBuilders;
+import org.apache.sis.internal.metadata.sql.ScriptRunner;
+import org.apache.sis.internal.system.Loggers;
+import org.apache.sis.internal.util.Constants;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.resources.Messages;
+import org.apache.sis.util.logging.Logging;
+import org.apache.sis.util.logging.PerformanceLevel;
+
+// Branch-specific imports
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+
+/**
+ * Runs the SQL scripts for creating an EPSG database.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @since   0.7
+ * @version 0.7
+ * @module
+ */
+final class EPSGInstaller extends ScriptRunner {
+    /**
+     * The embedded SQL scripts to execute for creating the EPSG database, in that order.
+     * The {@code ".sql"} suffix is omitted. The {@code "Grant"} script must be last.
+     */
+    private static final String[] SCRIPTS = {
+        "Tables", "Data", "Patches", "FKeys", "Indexes", "Grant"         // "Grant" must be last.
+    };
+
+    /**
+     * The pattern for an {@code "UPDATE … SET … REPLACE"} instruction.
+     * Example:
+     *
+     * {@preformat sql
+     *     UPDATE epsg_datum
+     *     SET datum_name = replace(datum_name, CHAR(182), CHAR(10));
+     * }
+     */
+    private static final String REPLACE_STATEMENT =
+            "\\s*UPDATE\\s+[\\w\\.\" ]+\\s+SET\\s+(\\w+)\\s*=\\s*replace\\s*\\(\\s*\\1\\W+.*";
+
+    /**
+     * {@code true} if the Pilcrow character (¶ - decimal code 182) should be replaced by Line Feed
+     * (LF - decimal code 10). This is a possible workaround when the database does not support the
+     * {@code REPLACE(column, CHAR(182), CHAR(10))} SQL statement, but accepts LF.
+     */
+    private final boolean replacePilcrow;
+
+    /**
+     * Non-null if there is SQL statements to skip. This is the case of {@code UPDATE … SET x = REPLACE(x, …)}
+     * functions, since Derby does not supports the {@code REPLACE} function.
+     */
+    private final Matcher statementToSkip;
+
+    /**
+     * Creates a new runner which will execute the statements using the given connection.
+     * The encoding is {@code "ISO-8859-1"}, which is the encoding used for the files provided by EPSG.
+     *
+     * @param connection The connection to the database.
+     * @throws SQLException if an error occurred while executing a SQL statement.
+     */
+    public EPSGInstaller(final Connection connection) throws SQLException {
+        super(connection, "ISO-8859-1", 100);
+        boolean isReplaceSupported = false;
+        final DatabaseMetaData metadata = connection.getMetaData();
+        final String functions = metadata.getStringFunctions();
+        for (final StringTokenizer tk = new StringTokenizer(functions, ","); tk.hasMoreTokens();) {
+            final String token = tk.nextToken().trim();
+            if (token.equalsIgnoreCase("REPLACE")) {
+                isReplaceSupported = true;
+                break;
+            }
+        }
+        if (isReplaceSupported) {
+            statementToSkip = null;
+        } else {
+            statementToSkip = Pattern.compile(REPLACE_STATEMENT, Pattern.CASE_INSENSITIVE).matcher("");
+        }
+        replacePilcrow = false;         // Never supported for now.
+    }
+
+    /**
+     * Creates immediately a schema of the given name in the database and remember that the
+     * {@code "epsg_"} prefix in table names will need to be replaced by path to that schema.
+     *
+     * <p>This method should be invoked only once. It does nothing if the database does not supports schema.</p>
+     *
+     * @param schema The schema (usually {@code "epsg"}).
+     * @throws SQLException if the schema can not be created.
+     * @throws IOException if an I/O operation was required and failed.
+     */
+    public void setSchema(final String schema) throws SQLException, IOException {
+        if (isSchemaSupported) {
+            /*
+             * Creates the schema on the database. We do that before to setup the 'toSchema' map, while the map still null.
+             * Note that we do not quote the schema name, which is a somewhat arbitrary choice.
+             */
+            execute(new StringBuilder("CREATE SCHEMA ").append(schema));
+            if (isGrantOnSchemaSupported) {
+                execute(new StringBuilder("GRANT USAGE ON SCHEMA ").append(schema).append(" TO ").append(PUBLIC));
+            }
+            /*
+             * Mapping from the table names used in the SQL scripts to the original names used in the MS-Access database.
+             * We use those original names because they are easier to read than the names in SQL scripts.
+             */
+            replace(SQLTranslator.TABLE_PREFIX + "coordinateaxis",             "Coordinate Axis");
+            replace(SQLTranslator.TABLE_PREFIX + "coordinateaxisname",         "Coordinate Axis Name");
+            replace(SQLTranslator.TABLE_PREFIX + "coordoperation",             "Coordinate_Operation");
+            replace(SQLTranslator.TABLE_PREFIX + "coordoperationmethod",       "Coordinate_Operation Method");
+            replace(SQLTranslator.TABLE_PREFIX + "coordoperationparam",        "Coordinate_Operation Parameter");
+            replace(SQLTranslator.TABLE_PREFIX + "coordoperationparamusage",   "Coordinate_Operation Parameter Usage");
+            replace(SQLTranslator.TABLE_PREFIX + "coordoperationparamvalue",   "Coordinate_Operation Parameter Value");
+            replace(SQLTranslator.TABLE_PREFIX + "coordoperationpath",         "Coordinate_Operation Path");
+            replace(SQLTranslator.TABLE_PREFIX + "coordinatereferencesystem",  "Coordinate Reference System");
+            replace(SQLTranslator.TABLE_PREFIX + "coordinatesystem",           "Coordinate System");
+            replace(SQLTranslator.TABLE_PREFIX + "namingsystem",               "Naming System");
+            replace(SQLTranslator.TABLE_PREFIX + "primemeridian",              "Prime Meridian");
+            replace(SQLTranslator.TABLE_PREFIX + "unitofmeasure",              "Unit of Measure");
+            replace(SQLTranslator.TABLE_PREFIX + "versionhistory",             "Version History");
+        }
+    }
+
+    /**
+     * Modifies the SQL statement before to execute it, or omit unsupported statements.
+     *
+     * @throws SQLException if an error occurred while executing the SQL statement.
+     * @throws IOException if an I/O operation was required and failed.
+     */
+    @Override
+    protected int execute(final StringBuilder sql) throws SQLException, IOException {
+        if (statementToSkip != null && statementToSkip.reset(sql).matches()) {
+            return 0;
+        }
+        if (replacePilcrow) {
+            StringBuilders.replace(sql, "¶", "\n");
+        }
+        return super.execute(sql);
+    }
+
+    /**
+     * Processes to the creation of the EPSG database using the files in the given directory.
+     * The given directory should contain at least files similar to the following ones
+     * (files without {@code ".sql"} extension are ignored):
+     *
+     * <ul>
+     *   <li>{@code EPSG_v8_18.mdb_Tables_PostgreSQL.sql}</li>
+     *   <li>{@code EPSG_v6_18.mdb_Data_PostgreSQL.sql}</li>
+     *   <li>{@code EPSG_v6_18.mdb_FKeys_PostgreSQL.sql}</li>
+     *   <li>Optional but recommended: {@code EPSG_v6_18.mdb_Indexes_PostgreSQL.sql}.</li>
+     * </ul>
+     *
+     * The suffix may be different (for example {@code "_MySQL.sql"} instead of {@code "_PostgreSQL.sql"})
+     * and the version number may be different.
+     *
+     * <p>If the given directory is {@code null}, then the scripts will be read from the resources.
+     * If no resources is found, then an exception will be thrown.
+     *
+     * @throws IOException if an error occurred while reading an input.
+     * @throws SQLException if an error occurred while executing a SQL statement.
+     */
+    public void run(final Path scriptDirectory) throws SQLException, IOException {
+        long time = System.nanoTime();
+        if (scriptDirectory == null) {
+            log(Messages.getResources(null).getLogRecord(Level.INFO, Messages.Keys.CreatingSchema_2,
+                    Constants.EPSG, getConnection().getMetaData().getURL()));
+        }
+        int numScripts = SCRIPTS.length;
+        if (!isGrantOnTableSupported) {
+            numScripts--;
+        }
+        int numRows = 0;
+        for (int i=0; i<numScripts; i++) {
+            final String script = SCRIPTS[i] + ".sql";
+            final InputStream in;
+            if (scriptDirectory != null) {
+                in = Files.newInputStream(scriptDirectory.resolve(script));
+            } else {
+                in = EPSGInstaller.class.getResourceAsStream(script);
+                if (in == null) {
+                    throw new FileNotFoundException(Errors.format(Errors.Keys.FileNotFound_1, script));
+                }
+            }
+            numRows += run(script, in);
+            // The stream will be closed by the run method.
+        }
+        time = System.nanoTime() - time;
+        log(Messages.getResources(null).getLogRecord(
+                PerformanceLevel.forDuration(time, TimeUnit.NANOSECONDS),
+                Messages.Keys.InsertDuration_2, numRows, time / 1E9f));
+    }
+
+    /**
+     * Logs a message reporting the failure to create EPSG database.
+     */
+    final void logFailure(final Locale locale) {
+        String message = Messages.getResources(locale).getString(Messages.Keys.CanNotCreateSchema_1, Constants.EPSG);
+        String status = status(locale);
+        if (status != null) {
+            message = message + ' ' + status;
+        }
+        log(new LogRecord(Level.WARNING, message));
+    }
+
+    /**
+     * Logs the given record. This method pretend that the record has been logged by
+     * {@code EPSGFactory.install(…)} because it is the public API using this class.
+     */
+    private static void log(final LogRecord record) {
+        record.setLoggerName(Loggers.CRS_FACTORY);
+        Logging.log(EPSGFactory.class, "install", record);
+    }
+}

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

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

Modified: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLTranslator.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLTranslator.java?rev=1728021&r1=1728020&r2=1728021&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLTranslator.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLTranslator.java [UTF-8] Mon Feb  1 23:12:08 2016
@@ -23,7 +23,6 @@ import java.util.Locale;
 import java.sql.DatabaseMetaData;
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.sql.SQLDataException;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.resources.Errors;
@@ -131,8 +130,11 @@ public class SQLTranslator implements Fu
     /**
      * The name of the schema where the tables are located, or {@code null} if none.
      * In the later case, table names are prefixed by {@value #TABLE_PREFIX}.
+     *
+     * <p><b>Consider this field as final.</b> This field is non-final only for construction convenience,
+     * or for updating after the {@link EPSGInstaller} class created the database.</p>
      */
-    private final String schema;
+    private String schema;
 
     /**
      * Mapping from words used in the MS-Access database to words used in the ANSI versions of EPSG databases.
@@ -148,7 +150,8 @@ public class SQLTranslator implements Fu
      * {@code true} if this class needs to quote table names. This quoting should be done only if the database
      * uses the MS-Access table names, even if we are targeting a PostgreSQL, MySQL or Oracle database.
      *
-     * <p><b>Consider this field as final.</b> This field is non-final only for construction convenience.</p>
+     * <p><b>Consider this field as final.</b> This field is non-final only for construction convenience,
+     * or for updating after the {@link EPSGInstaller} class created the database.</p>
      */
     private boolean quoteTableNames;
 
@@ -159,6 +162,11 @@ public class SQLTranslator implements Fu
     private final String quote;
 
     /**
+     * {@code true} if the tables exist. If {@code false}, then {@link EPSGInstaller} needs to be run.
+     */
+    private boolean isSchemaFound;
+
+    /**
      * Creates a new adapter for the database described by the given metadata.
      * This constructor:
      *
@@ -178,7 +186,12 @@ public class SQLTranslator implements Fu
     protected SQLTranslator(final DatabaseMetaData md) throws SQLException {
         ArgumentChecks.ensureNonNull("md", md);
         quote = md.getIdentifierQuoteString();
-        schema = findSchema(md);
+        findSchema(md);
+        /*
+         * Initialize the 'accessToAnsi' map. This map translates the table and column names used in the SQL
+         * statements into the names used by the database. Two conventions are understood: the names used in
+         * the MS-Access database or the names used in the SQL scripts. Both of them are distributed by EPSG.
+         */
         if (quoteTableNames) {
             /*
              * MS-Access database uses a column named "ORDER" in the "Coordinate Axis" table.
@@ -201,9 +214,11 @@ public class SQLTranslator implements Fu
     };
 
     /**
-     * Returns the schema where the EPSG tables seems to be located.
+     * Finds the schema where the EPSG tables seem to be located. If there is more than one schema containing the
+     * tables, give precedence to the schema named "EPSG" if one is found. If there is no schema named "EPSG",
+     * take an arbitrary schema. It may be null if the tables are not located in a schema.
      */
-    private String findSchema(final DatabaseMetaData md) throws SQLException {
+    private void findSchema(final DatabaseMetaData md) throws SQLException {
         final boolean toUpperCase = md.storesUpperCaseIdentifiers();
         for (int i = SENTINEL.length; --i >= 0;) {
             String table = SENTINEL[i];
@@ -212,20 +227,38 @@ public class SQLTranslator implements Fu
             }
             try (ResultSet result = md.getTables(null, null, table, null)) {
                 if (result.next()) {
+                    isSchemaFound = true;
                     quoteTableNames = (i == MIXED_CASE);
-                    /*
-                     * If there is more than one schema containing the tables, give precedence to the schema
-                     * named "EPSG" if one is found. If there is no "EPSG" schema, take an arbitrary schema.
-                     */
-                    String schema;
                     do {
                         schema = result.getString("TABLE_SCHEM");
                     } while (!Constants.EPSG.equalsIgnoreCase(schema) && result.next());
-                    return schema;
+                    break;
                 }
             }
         }
-        throw new SQLDataException(Errors.format(Errors.Keys.TableNotFound_1, SENTINEL[MIXED_CASE]));
+    }
+
+    /**
+     * Returns {@code true} if the constructor has found the EPSG schema.
+     */
+    final boolean isSchemaFound() {
+        return isSchemaFound;
+    }
+
+    /**
+     * Declares that the EPSG schema should exists now.
+     * This method is invoked after {@link EPSGInstaller} execution.
+     */
+    final void setSchemaFound(final DatabaseMetaData md) throws SQLException {
+        findSchema(md);
+        isSchemaFound = true;
+    }
+
+    /**
+     * Returns the error message for the exception to throw if the EPSG schema is not found and we can not create it.
+     */
+    static String schemaNotFound(final Locale locale) {
+        return Errors.getResources(locale).getString(Errors.Keys.TableNotFound_1, SENTINEL[MIXED_CASE]);
     }
 
     /**

Modified: sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java?rev=1728021&r1=1728020&r2=1728021&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-referencing/src/test/java/org/apache/sis/referencing/factory/sql/EPSGFactoryTest.java [UTF-8] Mon Feb  1 23:12:08 2016
@@ -103,7 +103,7 @@ public final strictfp class EPSGFactoryT
     public static void createFactory() throws FactoryException {
         final GeodeticObjectFactory f = new GeodeticObjectFactory();
         try {
-            factory = new EPSGFactory(null, null, f, f, f, null, null, null);
+            factory = new EPSGFactory(null, null, f, f, f, null, null);
         } catch (UnavailableFactoryException e) {
             Logging.getLogger(Loggers.CRS_FACTORY).warning(e.toString());
             // Leave INSTANCE to null. This will have the effect of skipping tests.

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java?rev=1728021&r1=1728020&r2=1728021&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java [UTF-8] Mon Feb  1 23:12:08 2016
@@ -276,6 +276,11 @@ public final class Errors extends Indexe
         public static final short EmptyProperty_1 = 23;
 
         /**
+         * An error occurred in file “{0}” at Line {1}.
+         */
+        public static final short ErrorInFileAtLine_2 = 216;
+
+        /**
          * Error in “{0}”: {1}
          */
         public static final short ErrorIn_2 = 190;

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties?rev=1728021&r1=1728020&r2=1728021&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties [ISO-8859-1] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties [ISO-8859-1] Mon Feb  1 23:12:08 2016
@@ -67,6 +67,7 @@ EmptyDictionary                   = The
 EmptyEnvelope2D                   = Envelope must be at least two-dimensional and non-empty.
 EmptyProperty_1                   = Property named \u201c{0}\u201d shall not be empty.
 ErrorIn_2                         = Error in \u201c{0}\u201d: {1}
+ErrorInFileAtLine_2               = An error occurred in file \u201c{0}\u201d at Line {1}.
 ExcessiveArgumentSize_3           = Argument \u2018{0}\u2019 shall not contain more than {1} elements. A number of {2} is excessive.
 ExcessiveListSize_2               = A size of {1} elements is excessive for the \u201c{0}\u201d list.
 ExcessiveNumberOfDimensions_1     = For this algorithm, {0} is an excessive number of dimensions.

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties?rev=1728021&r1=1728020&r2=1728021&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties [ISO-8859-1] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties [ISO-8859-1] Mon Feb  1 23:12:08 2016
@@ -64,6 +64,7 @@ EmptyDictionary                   = Le d
 EmptyEnvelope2D                   = L\u2019enveloppe doit avoir au moins deux dimensions et ne pas \u00eatre vide.
 EmptyProperty_1                   = La propri\u00e9t\u00e9 nomm\u00e9e \u00ab\u202f{0}\u202f\u00bb ne doit pas \u00eatre vide.
 ErrorIn_2                         = Erreur dans \u00ab\u202f{0}\u202f\u00bb\u2008: {1}
+ErrorInFileAtLine_2               = Une erreur est survenue dans le fichier \u00ab\u202f{0}\u202f\u00bb \u00e0 la ligne {1}.
 ExcessiveArgumentSize_3           = L\u2019argument \u2018{0}\u2019 ne peut pas contenir plus de {1} \u00e9l\u00e9ments. Un nombre de {2} est excessif.
 ExcessiveListSize_2               = Une taille de {1} \u00e9l\u00e9ments est excessive pour la liste \u00ab\u202f{0}\u202f\u00bb.
 ExcessiveNumberOfDimensions_1     = Pour cet algorithme, {0} est un trop grand nombre de dimensions.

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.java?rev=1728021&r1=1728020&r2=1728021&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.java [UTF-8] Mon Feb  1 23:12:08 2016
@@ -67,6 +67,11 @@ public final class Messages extends Inde
         public static final short AmbiguousEllipsoid_1 = 30;
 
         /**
+         * Can not create the {0} schema in database.
+         */
+        public static final short CanNotCreateSchema_1 = 41;
+
+        /**
          * Can not instantiate the object of type ‘{0}’ identified by “{1}”. Reason is:{2}
          */
         public static final short CanNotInstantiateForIdentifier_3 = 35;
@@ -113,6 +118,11 @@ public final class Messages extends Inde
         public static final short CreatedNamedObject_2 = 16;
 
         /**
+         * Creating {0} schema in the “{1}” database.
+         */
+        public static final short CreatingSchema_2 = 39;
+
+        /**
          * {0} dataset version {1} on “{2}” version {3}.
          */
         public static final short DataBase_4 = 28;
@@ -193,6 +203,11 @@ public final class Messages extends Inde
         public static final short IncompleteParsing_1 = 14;
 
         /**
+         * Inserted {0} records in {1} seconds.
+         */
+        public static final short InsertDuration_2 = 40;
+
+        /**
          * No object associated to the “{0}” JNDI name.
          */
         public static final short JNDINotSpecified_1 = 32;

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.properties?rev=1728021&r1=1728020&r2=1728021&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.properties [ISO-8859-1] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages.properties [ISO-8859-1] Mon Feb  1 23:12:08 2016
@@ -16,6 +16,7 @@
 #
 AlreadyRegistered_2              = {0} \u201c{1}\u201d is already registered. The second instance will be ignored.
 AmbiguousEllipsoid_1             = Ambiguity between inverse flattening and semi minor axis length for \u201c{0}\u201d. Using inverse flattening.
+CanNotCreateSchema_1             = Can not create the {0} schema in database.
 # In following message, the first characters of parameter {2} should be a line separator ("\r", "\n" or "\r\n").
 CanNotInstantiateForIdentifier_3 = Can not instantiate the object of type \u2018{0}\u2019 identified by \u201c{1}\u201d. Reason is:{2}
 ChangedContainerCapacity_2       = Changed the container capacity from {0} to {1} elements.
@@ -26,6 +27,7 @@ CreatedNamedObject_2             = Creat
 CreatedIdentifiedObject_3        = Created an instance of \u2018{0}\u2019 named \u201c{1}\u201d with the \u201c{2}\u201d identifier.
 CreateDuration_2                 = Created an instance of \u2018{0}\u2019 in {1} seconds.
 CreateDurationFromIdentifier_3   = Created an instance of \u2018{0}\u2019 from the \u201c{1}\u201d identifier in {2} seconds.
+CreatingSchema_2                 = Creating {0} schema in the \u201c{1}\u201d database.
 DataBase_4                       = {0} dataset version {1} on \u201c{2}\u201d version {3}.
 DataDirectory_2                  = Environment variable {0} specifies the \u201c{1}\u201d data directory.
 DataDirectoryDoesNotExist_2      = The {0} environment variable is defined, but the given \u201c{1}\u201d value is not an existing directory.
@@ -41,6 +43,7 @@ IgnoredPropertiesAfterFirst_1    = Ignor
 IgnoredPropertyAssociatedTo_1    = Ignored property associated to \u2018{0}\u2019.
 IgnoredServiceProvider_3         = More than one service provider of type \u2018{0}\u2019 are declared for \u201c{1}\u201d. Only the first provider (an instance of \u2018{2}\u2019) will be used.
 IncompleteParsing_1              = Parsing of \u201c{0}\u201d done, but some elements were ignored.
+InsertDuration_2                 = Inserted {0} records in {1} seconds.
 JNDINotSpecified_1               = No object associated to the \u201c{0}\u201d JNDI name.
 LoadingDatumShiftFile_1          = Loading datum shift file \u201c{0}\u201d.
 LocalesDiscarded                 = Text were discarded for some locales.

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages_fr.properties
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages_fr.properties?rev=1728021&r1=1728020&r2=1728021&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages_fr.properties [ISO-8859-1] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/resources/Messages_fr.properties [ISO-8859-1] Mon Feb  1 23:12:08 2016
@@ -23,6 +23,7 @@
 #
 AlreadyRegistered_2              = Le {0} \u00ab\u202f{1}\u202f\u00bb est d\u00e9j\u00e0 inscrit dans le registre. La seconde instance sera ignor\u00e9e.
 AmbiguousEllipsoid_1             = Ambigu\u00eft\u00e9 entre l\u2019aplatissement et la longueur du semi-axe mineur pour \u00ab\u202f{0}\u202f\u00bb. Utilise l\u2019aplatissement.
+CanNotCreateSchema_1             = Ne peut pas cr\u00e9er le sch\u00e9ma {0} dans la base de donn\u00e9es.
 # In following message, the first characters of parameter {2} should be a line separator ("\r", "\n" or "\r\n").
 CanNotInstantiateForIdentifier_3 = Ne peut pas cr\u00e9er l\u2019objet de type \u2018{0}\u2019 identifi\u00e9 par \u00ab\u202f{1}\u202f\u00bb. La raison est\u00a0:{2}
 ChangedContainerCapacity_2       = Changement de la capacit\u00e9 du conteneur de {0} vers {1} \u00e9l\u00e9ments.
@@ -33,6 +34,7 @@ CreatedNamedObject_2             = Cr\u0
 CreatedIdentifiedObject_3        = Cr\u00e9ation d\u2019une instance de \u2018{0}\u2019 nomm\u00e9e \u00ab\u202f{1}\u202f\u00bb avec l\u2019identifiant \u00ab\u202f{2}\u202f\u00bb.
 CreateDuration_2                 = Cr\u00e9ation d\u2019une instance de \u2018{0}\u2019 en {1} secondes.
 CreateDurationFromIdentifier_3   = Cr\u00e9ation d\u2019une instance de \u2018{0}\u2019 \u00e0 partir de l\u2019identifiant \u00ab\u202f{1}\u202f\u00bb en {2} secondes.
+CreatingSchema_2                 = Cr\u00e9ation du sch\u00e9ma {0} dans la base de donn\u00e9es \u00ab\u202f{1}\u202f\u00bb.
 DataBase_4                       = Base de donn\u00e9es {0} version {1} sur \u00ab\u202f{2}\u202f\u00bb version {3}.
 DataDirectory_2                  = La variable environnementale {0} sp\u00e9cifie le r\u00e9pertoire de donn\u00e9es \u00ab\u202f{1}\u202f\u00bb.
 DataDirectoryDoesNotExist_2      = La variable environnementale {0} est bien d\u00e9finie, mais sa valeur \u00ab\u202f{1}\u202f\u00bb n\u2019est pas un r\u00e9pertoire existant.
@@ -48,6 +50,7 @@ IgnoredPropertiesAfterFirst_1    = Des p
 IgnoredPropertyAssociatedTo_1    = Une propri\u00e9t\u00e9 associ\u00e9e \u00e0 \u2018{0}\u2019 a \u00e9t\u00e9 ignor\u00e9e.
 IgnoredServiceProvider_3         = Plusieurs fournisseurs de service de type \u2018{0}\u2019 sont d\u00e9clar\u00e9s pour \u00ab\u202f{1}\u202f\u00bb. Seul le premier fournisseur (une instance de \u2018{2}\u2019) sera utilis\u00e9.
 IncompleteParsing_1              = La lecture de \u00ab\u202f{0}\u202f\u00bb a \u00e9t\u00e9 faite, mais en ignorant certains \u00e9l\u00e9ments.
+InsertDuration_2                 = {0} enregistrements ont \u00e9t\u00e9 ajout\u00e9s en {1} secondes.
 JNDINotSpecified_1               = Aucun objet n\u2019est associ\u00e9 au nom JNDI \u00ab\u202f{0}\u202f\u00bb.
 LoadingDatumShiftFile_1          = Chargement du fichier de changement de r\u00e9f\u00e9rentiel \u00ab\u202f{0}\u202f\u00bb.
 LocalesDiscarded                 = Des textes ont \u00e9t\u00e9 ignor\u00e9s pour certaines langues.



Mime
View raw message