sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1722651 - in /sis/branches/JDK8/core: sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/ sis-utility/src/main/java/org/apache/sis/util/ sis-utility/src/main/java/org/apache/sis/util/resources/ sis-utility/src/test/java/o...
Date Sat, 02 Jan 2016 18:22:43 GMT
Author: desruisseaux
Date: Sat Jan  2 18:22:43 2016
New Revision: 1722651

URL: http://svn.apache.org/viewvc?rev=1722651&view=rev
Log:
Initial port of the code in charge of adapting SQL statements from the MS-Access syntax (the original distribution format of EPSG dataset) to the syntax used in the Data Description Language (DDL) scripts provided by EPSG.

Added:
    sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLAdapter.java   (with props)
Modified:
    sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
    sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
    sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGFactory.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.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/test/java/org/apache/sis/util/CharSequencesTest.java

Modified: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/AuthorityCodes.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/AuthorityCodes.java?rev=1722651&r1=1722650&r2=1722651&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/AuthorityCodes.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/AuthorityCodes.java [UTF-8] Sat Jan  2 18:22:43 2016
@@ -160,7 +160,7 @@ final class AuthorityCodes extends Abstr
         }
         final int conditionStart = buffer.length();
         buffer.append(" ORDER BY ").append(table.codeColumn);
-        sql[ALL] = factory.adaptSQL(buffer.toString());
+        sql[ALL] = factory.adapter.adaptSQL(buffer.toString());
         /*
          * Build the SQL query for fetching the name of a single object for a given code.
          * This query will also be used for testing object existence. It is of the form:
@@ -172,7 +172,7 @@ final class AuthorityCodes extends Abstr
             buffer.replace(columnNameStart, columnNameEnd, table.nameColumn);
         }
         buffer.append(hasWhere ? " AND " : " WHERE ").append(table.codeColumn).append(" = ?");
-        sql[ONE] = factory.adaptSQL(buffer.toString());
+        sql[ONE] = factory.adapter.adaptSQL(buffer.toString());
         /*
          * Other information opportunistically computed from above search.
          */

Modified: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java?rev=1722651&r1=1722650&r2=1722651&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/EPSGDataAccess.java [UTF-8] Sat Jan  2 18:22:43 2016
@@ -151,13 +151,6 @@ public class EPSGDataAccess extends Geod
         CSAuthorityFactory, DatumAuthorityFactory, CoordinateOperationAuthorityFactory, Localized, AutoCloseable
 {
     /**
-     * The prefix in table names. The SQL scripts are provided by EPSG with this prefix in front of all table names.
-     * SIS rather uses a modified version of those SQL scripts which creates the tables in an "EPSG" database schema.
-     * But we still need to check for existence of this prefix in case someone used the original SQL scripts.
-     */
-    private static final String TABLE_PREFIX = "epsg_";
-
-    /**
      * The namespace of EPSG names and codes. This namespace is needed by all {@code createFoo(String)} methods.
      * The {@code EPSGDataAccess} constructor relies on the {@link #nameFactory} caching mechanism for giving us
      * the same {@code NameSpace} instance than the one used by previous {@code EPSGDataAccess} instances, if any.
@@ -282,6 +275,12 @@ public class EPSGDataAccess extends Geod
     protected final Connection connection;
 
     /**
+     * The adapter from the SQL statements using MS-Access syntax
+     * to SQL statements using the syntax of the actual database.
+     */
+    protected final SQLAdapter adapter;
+
+    /**
      * Creates a factory using the given connection. The connection will be {@linkplain Connection#close() closed}
      * when this factory will be {@linkplain #close() closed}.
      *
@@ -292,37 +291,20 @@ public class EPSGDataAccess extends Geod
      *
      * @param parent      The {@code EPSGFactory} which is creating this Data Access Object (DAO).
      * @param connection  The connection to the underlying EPSG database.
+     * @param adapter     The adapter from the SQL statements using MS-Access syntax
+     *                    to SQL statements using the syntax of the actual database.
      */
-    protected EPSGDataAccess(final EPSGFactory parent, final Connection connection) {
+    protected EPSGDataAccess(final EPSGFactory parent, final Connection connection, final SQLAdapter adapter) {
         super(parent);
         ArgumentChecks.ensureNonNull("connection", connection);
+        ArgumentChecks.ensureNonNull("adapter", adapter);
         this.parent     = parent;
         this.connection = connection;
+        this.adapter    = adapter;
         this.namespace  = nameFactory.createNameSpace(nameFactory.createLocalName(null, Constants.EPSG), null);
     }
 
     /**
-     * Invoked when a new {@link PreparedStatement} is about to be created from a SQL string.
-     * Since the <a href="http://www.epsg.org">EPSG database</a> is available primarily in MS-Access format,
-     * SQL statements are formatted using a syntax specific to this particular database software
-     * (for example "{@code SELECT * FROM [Coordinate Reference System]}").
-     * When a subclass targets another database vendor, it must overrides this method in order to adapt the SQL syntax.
-     *
-     * <div class="note"><b>Example</b>
-     * a subclass connecting to a <cite>PostgreSQL</cite> database would replace the watching braces
-     * ({@code '['} and {@code ']'}) by the quote character ({@code '"'}).</div>
-     *
-     * The default implementation returns the given statement unchanged.
-     *
-     * @param  statement The statement in MS-Access syntax.
-     * @return The SQL statement adapted to the syntax of the target database.
-     * @throws SQLException if an error occurred while adapting the SQL statement.
-     */
-    protected String adaptSQL(final String statement) throws SQLException {
-        return statement;
-    }
-
-    /**
      * Returns the locale used by this factory for producing error messages.
      * This locale does not change the way data are read from the EPSG database.
      *
@@ -354,8 +336,7 @@ public class EPSGDataAccess extends Geod
     @Override
     public synchronized Citation getAuthority() {
         /*
-         * We do not cache this citation because the caching service is already provided by ConcurrentAuthorityFactory
-         * and we overridden the trimAuthority(…) and noSuchAuthorityCode(…) methods that invoked this getAuthority().
+         * We do not cache this citation because the caching service is already provided by ConcurrentAuthorityFactory.
          */
         final DefaultCitation c = new DefaultCitation("EPSG Geodetic Parameter Dataset");
         c.setIdentifiers(Collections.singleton(new ImmutableIdentifier(null, null, Constants.EPSG)));
@@ -365,8 +346,8 @@ public class EPSGDataAccess extends Geod
              * instead then UTC because the date is for information purpose only, and the local timezone is
              * more likely to be shown nicely (without artificial hours) to the user.
              */
-            final String query = adaptSQL("SELECT VERSION_NUMBER, VERSION_DATE FROM [Version History]" +
-                                          " ORDER BY VERSION_DATE DESC, VERSION_HISTORY_CODE DESC");
+            final String query = adapter.adaptSQL("SELECT VERSION_NUMBER, VERSION_DATE FROM [Version History]" +
+                                                 " ORDER BY VERSION_DATE DESC, VERSION_HISTORY_CODE DESC");
             String version = null;
             try (Statement statement = connection.createStatement();
                  ResultSet result = statement.executeQuery(query))
@@ -530,10 +511,9 @@ addURIs:    for (int i=0; ; i++) {
      */
     @Override
     public InternationalString getDescriptionText(final String code) throws NoSuchAuthorityCodeException, FactoryException {
-        final String primaryKey = trimAuthority(code);
         try {
             for (final TableInfo table : TableInfo.EPSG) {
-                final String text = getCodeMap(table.type).get(primaryKey);
+                final String text = getCodeMap(table.type).get(code);
                 if (text != null) {
                     return (table.nameColumn != null) ? new SimpleInternationalString(text) : null;
                 }
@@ -600,9 +580,6 @@ addURIs:    for (int i=0; ; i++) {
     /**
      * Converts EPSG codes or EPSG names to the numerical identifiers (the primary keys).
      *
-     * <p>Note that this method includes a call to {@link #trimAuthority(String)},
-     * so there is no need to call it before or after this method.</p>
-     *
      * <div class="note"><b>Note:</b>
      * this method could be seen as the converse of above {@link #getDescriptionText(String)} method.</div>
      *
@@ -619,8 +596,7 @@ addURIs:    for (int i=0; ; i++) {
         final int[] primaryKeys = new int[codes.length];
         for (int i=0; i<codes.length; i++) {
             final String code = codes[i];
-            final String identifier = trimAuthority(code);
-            if (codeColumn != null && nameColumn != null && !isPrimaryKey(identifier)) {
+            if (codeColumn != null && nameColumn != null && !isPrimaryKey(code)) {
                 /*
                  * The given string is not a numerical code. Search the value in the database.
                  * If a prepared statement is already available, reuse it providing that it was
@@ -639,10 +615,10 @@ addURIs:    for (int i=0; ; i++) {
                 if (statement == null) {
                     final String query = "SELECT " + codeColumn + " FROM " + table +
                                          " WHERE " + nameColumn + " = ?";
-                    statement = connection.prepareStatement(adaptSQL(query));
+                    statement = connection.prepareStatement(adapter.adaptSQL(query));
                     statements.put(KEY, statement);
                 }
-                statement.setString(1, identifier);
+                statement.setString(1, code);
                 Integer resolved = null;
                 try (ResultSet result = statement.executeQuery()) {
                     while (result.next()) {
@@ -659,10 +635,10 @@ addURIs:    for (int i=0; ; i++) {
              * if we the above code did not found a match in the name column.
              */
             try {
-                primaryKeys[i] = Integer.parseInt(identifier);
+                primaryKeys[i] = Integer.parseInt(code);
             } catch (NumberFormatException e) {
                 final NoSuchIdentifierException ne = new NoSuchIdentifierException(error().getString(
-                        Errors.Keys.IllegalIdentifierForCodespace_2, Constants.EPSG, identifier), code);
+                        Errors.Keys.IllegalIdentifierForCodespace_2, Constants.EPSG, code), code);
                 ne.initCause(e);
                 throw ne;
             }
@@ -676,9 +652,6 @@ addURIs:    for (int i=0; ; i++) {
      * {@linkplain #isPrimaryKey primary key}, then this method assumes that the code is the object name
      * and will search for its primary key value.
      *
-     * <p>Note that this method includes a call to {@link #trimAuthority(String)},
-     * so there is no need to call it before or after this method.</p>
-     *
      * @param  table       The table where the code should appears.
      * @param  codeColumn  The column name for the codes, or {@code null} if none.
      * @param  nameColumn  The column name for the names, or {@code null} if none.
@@ -713,7 +686,7 @@ addURIs:    for (int i=0; ; i++) {
         assert Thread.holdsLock(this);
         PreparedStatement stmt = statements.get(table);
         if (stmt == null) {
-            stmt = connection.prepareStatement(adaptSQL(sql));
+            stmt = connection.prepareStatement(adapter.adaptSQL(sql));
             statements.put(table, stmt);
         }
         // Partial check that the statement is for the right SQL query.
@@ -931,8 +904,8 @@ addURIs:    for (int i=0; ; i++) {
         if (name == null) {
             return false;
         }
-        if (name.startsWith(TABLE_PREFIX)) {
-            name = name.substring(TABLE_PREFIX.length());
+        if (name.startsWith(SQLAdapter.TABLE_PREFIX)) {
+            name = name.substring(SQLAdapter.TABLE_PREFIX.length());
         }
         return CharSequences.isAcronymForWords(name, expected);
     }
@@ -1109,13 +1082,12 @@ addURIs:    for (int i=0; ; i++) {
             throws NoSuchAuthorityCodeException, FactoryException
     {
         ArgumentChecks.ensureNonNull("code", code);
-        final String  epsg         = trimAuthority(code);
-        final boolean isPrimaryKey = isPrimaryKey(epsg);
+        final boolean isPrimaryKey = isPrimaryKey(code);
         final StringBuilder query  = new StringBuilder("SELECT ");
         final int queryStart       = query.length();
         int found = -1;
         try {
-            final int pk = isPrimaryKey ? toPrimaryKeys(null, null, null, epsg)[0] : 0;
+            final int pk = isPrimaryKey ? toPrimaryKeys(null, null, null, code)[0] : 0;
             for (int i = 0; i < TableInfo.EPSG.length; i++) {
                 final TableInfo table = TableInfo.EPSG[i];
                 final String column = isPrimaryKey ? table.codeColumn : table.nameColumn;
@@ -1125,7 +1097,7 @@ addURIs:    for (int i=0; ; i++) {
                 query.setLength(queryStart);
                 query.append(table.codeColumn).append(" FROM ").append(table.table)
                         .append(" WHERE ").append(column).append(" = ?");
-                try (PreparedStatement stmt = connection.prepareStatement(adaptSQL(query.toString()))) {
+                try (PreparedStatement stmt = connection.prepareStatement(adapter.adaptSQL(query.toString()))) {
                     /*
                      * Check if at least one record is found for the code or the name.
                      * Ensure that there is not two values for the same code or name.
@@ -1133,7 +1105,7 @@ addURIs:    for (int i=0; ; i++) {
                     if (isPrimaryKey) {
                         stmt.setInt(1, pk);
                     } else {
-                        stmt.setString(1, epsg);
+                        stmt.setString(1, code);
                     }
                     Integer present = null;
                     try (ResultSet result = stmt.executeQuery()) {
@@ -2924,7 +2896,7 @@ addURIs:    for (int i=0; ; i++) {
             }
             final Set<String> result = new LinkedHashSet<>();
             try {
-                final String sql = adaptSQL(buffer.toString());
+                final String sql = adapter.adaptSQL(buffer.toString());
                 try (Statement s = connection.createStatement();
                      ResultSet r = s.executeQuery(sql))
                 {
@@ -3087,7 +3059,7 @@ addURIs:    for (int i=0; ; i++) {
      */
     private NoSuchAuthorityCodeException noSuchAuthorityCode(final Class<?> type, final String code) {
         return new NoSuchAuthorityCodeException(error().getString(Errors.Keys.NoSuchAuthorityCode_3,
-                Constants.EPSG, type, code), Constants.EPSG, trimAuthority(code), code);
+                Constants.EPSG, type, code), Constants.EPSG, code, code);
     }
 
     /**

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=1722651&r1=1722650&r2=1722651&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] Sat Jan  2 18:22:43 2016
@@ -94,6 +94,12 @@ public class EPSGFactory extends Concurr
     protected final MathTransformFactory mtFactory;
 
     /**
+     * The adapter from the SQL statements using MS-Access syntax to SQL statements using the syntax
+     * of the actual database. If null, will be created when first needed.
+     */
+    private volatile SQLAdapter adapter;
+
+    /**
      * The locale for producing error messages. This is usually the default locale.
      *
      * @see #getLocale()
@@ -130,6 +136,8 @@ 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 adapter       The adapter from the SQL statements using MS-Access syntax to SQL statements
+     *                      using the syntax of the actual database, or {@code null} for the default adapter.
      */
     public EPSGFactory(final DataSource                 dataSource,
                        final NameFactory                nameFactory,
@@ -137,7 +145,8 @@ public class EPSGFactory extends Concurr
                        final CSFactory                  csFactory,
                        final CRSFactory                 crsFactory,
                        final CoordinateOperationFactory copFactory,
-                       final MathTransformFactory       mtFactory)
+                       final MathTransformFactory       mtFactory,
+                       final SQLAdapter                 adapter)
     {
         super(nameFactory);
         ArgumentChecks.ensureNonNull("dataSource",   dataSource);
@@ -152,6 +161,7 @@ public class EPSGFactory extends Concurr
         this.crsFactory   = crsFactory;
         this.copFactory   = copFactory;
         this.mtFactory    = mtFactory;
+        this.adapter      = adapter;
         this.locale       = Locale.getDefault(Locale.Category.DISPLAY);
     }
 
@@ -179,21 +189,33 @@ public class EPSGFactory extends Concurr
     }
 
     /**
-     * Invoked by {@code ConcurrentAuthorityFactory} when a new worker is required.
-     * This method gets a new connection from the {@link #dataSource} and delegates
-     * the worker creation to {@link #createBackingStore(Connection)}.
+     * 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 creates a new {@link EPSGDataAccess} instance.
+     * Subclasses can override this method if they want to return a custom instance.</p>
      *
      * @return The backing store to use in {@code createFoo(String)} methods.
      * @throws FactoryException if the constructor failed to connect to the EPSG database.
      *         This exception usually has a {@link SQLException} as its cause.
      */
     @Override
-    protected final GeodeticAuthorityFactory createBackingStore() throws FactoryException {
+    protected GeodeticAuthorityFactory createBackingStore() throws FactoryException {
         Connection c = null;
         try {
             c = dataSource.getConnection();
-            final EPSGDataAccess factory = createBackingStore(c);
-            return factory;
+            SQLAdapter a = adapter;
+            if (a == null) {
+                synchronized (this) {
+                    a = adapter;
+                    if (a == null) {
+                        adapter = a = new SQLAdapter(c.getMetaData());
+                    }
+                }
+            }
+            return new EPSGDataAccess(this, c, a);
         } catch (Exception e) {
             if (c != null) try {
                 c.close();
@@ -203,20 +225,4 @@ public class EPSGFactory extends Concurr
             throw new UnavailableFactoryException(e.getLocalizedMessage(), e);
         }
     }
-
-    /**
-     * 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 creates a new {@link EPSGDataAccess} instance.
-     * Subclasses can override this method if they want to return a custom instance.</p>
-     *
-     * @param  connection A connection to the EPSG database.
-     * @throws SQLException if {@code EPSGDataAccess} detected a problem with the database.
-     * @return The backing store to use in {@code createFoo(String)} methods.
-     */
-    protected EPSGDataAccess createBackingStore(final Connection connection) throws SQLException {
-        return new EPSGDataAccess(this, connection);
-    }
 }

Added: sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLAdapter.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLAdapter.java?rev=1722651&view=auto
==============================================================================
--- sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLAdapter.java (added)
+++ sis/branches/JDK8/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/sql/SQLAdapter.java [UTF-8] Sat Jan  2 18:22:43 2016
@@ -0,0 +1,284 @@
+/*
+ * 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.Map;
+import java.util.HashMap;
+import java.util.Collections;
+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;
+import org.apache.sis.internal.util.Constants;
+
+
+/**
+ * Converts the SQL statements from MS-Access dialect to standard SQL. The {@link #adaptSQL(String)} method
+ * is invoked when a new {@link java.sql.PreparedStatement} is about to be created from a SQL string.
+ * Since the <a href="http://www.epsg.org">EPSG dataset</a> is available primarily in MS-Access format,
+ * the original SQL statements are formatted using a syntax specific to that particular database software.
+ * If the actual EPSG dataset to query is hosted on another database product, then the SQL query needs to be
+ * adapted to the target database syntax before to be executed.
+ *
+ * <div class="note"><b>Example</b>
+ * SQL statements for an EPSG dataset hosted on the <cite>PostgreSQL</cite> database need to have their brackets
+ * ({@code '['} and {@code ']'}) replaced by the quote character ({@code '"'}) before to be sent to the database
+ * driver. Furthermore table names may be different. So the following MS-Access query:
+ *
+ * <ul>
+ *   <li>{@code SELECT * FROM [Coordinate Reference System]}</li>
+ * </ul>
+ *
+ * needs to be converted to one of the following possibilities for a PostgreSQL database
+ * (the reason for those multiple choices will be discussed later):
+ *
+ * <ul>
+ *   <li>{@code SELECT * FROM "Coordinate Reference System"}</li>
+ *   <li>{@code SELECT * FROM epsg_coordinatereferencesystem} (in the default schema)</li>
+ *   <li>{@code SELECT * FROM epsg.coordinatereferencesystem} (in the {@code "epsg"} schema)</li>
+ *   <li>{@code SELECT * FROM "EPSG"."Coordinate Reference System"}</li>
+ * </ul></div>
+ *
+ * In addition to the MS-Access format, EPSG also provides the dataset as <cite>Data Description Language</cite> (DDL)
+ * scripts for PostgreSQL, MySQL and Oracle databases. But the table names and some column names in those scripts differ
+ * from the ones used in the MS-Access database. The following table summarizes the name changes:
+ *
+ * <table class="sis">
+ *   <caption>Table and column names</caption>
+ *   <tr><th>Element</th><th>Name in MS-Access database</th>                    <th>Name in DDL scripts</th></tr>
+ *   <tr><td>Table</td>  <td>{@code Alias}</td>                                 <td>{@code epsg_alias}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Area}</td>                                  <td>{@code epsg_area}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate Axis}</td>                       <td>{@code epsg_coordinateaxis}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate Axis Name}</td>                  <td>{@code epsg_coordinateaxisname}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate_Operation}</td>                  <td>{@code epsg_coordoperation}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate_Operation Method}</td>           <td>{@code epsg_coordoperationmethod}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate_Operation Parameter}</td>        <td>{@code epsg_coordoperationparam}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate_Operation Parameter Usage}</td>  <td>{@code epsg_coordoperationparamusage}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate_Operation Parameter Value}</td>  <td>{@code epsg_coordoperationparamvalue}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate_Operation Path}</td>             <td>{@code epsg_coordoperationpath}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate Reference System}</td>           <td>{@code epsg_coordinatereferencesystem}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Coordinate System}</td>                     <td>{@code epsg_coordinatesystem}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Datum}</td>                                 <td>{@code epsg_datum}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Ellipsoid}</td>                             <td>{@code epsg_ellipsoid}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Naming System}</td>                         <td>{@code epsg_namingsystem}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Prime Meridian}</td>                        <td>{@code epsg_primemeridian}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Supersession}</td>                          <td>{@code epsg_supersession}</td></tr>
+ *   <tr><td>Table</td>  <td>{@code Unit of Measure}</td>                       <td>{@code epsg_unitofmeasure}</td></tr>
+ *   <tr><td>Column</td> <td>{@code ORDER}</td>                                 <td>{@code coord_axis_order}</td></tr>
+ * </table>
+ *
+ * This class auto-detects the schema where the EPSG database seems to be located and whether the table names
+ * are the ones used by EPSG in the MS-Access version or the PostgreSQL, MySQL or Oracle version of the database.
+ * Consequently it is legal to use the MS-Access table names, which are more readable, in a PostgreSQL database.
+ *
+ * <div class="section">Thread safety</div>
+ * All {@code SQLAdapter} instances given to the {@link EPSGFactory} constructor
+ * <strong>shall</strong> be immutable and thread-safe.
+ *
+ * @author  Rueben Schulz (UBC)
+ * @author  Martin Desruisseaux (IRD)
+ * @author  Didier Richard (IGN)
+ * @author  John Grange
+ * @since   0.7
+ * @version 0.7
+ * @module
+ *
+ * @see EPSGDataAccess#adaptSQL(String)
+ */
+public class SQLAdapter {
+    /**
+     * Table names used as "sentinel value" for detecting the presence of an EPSG database.
+     * This array lists different possible names for the same table. The first entry must be
+     * the MS-Access name. Other names may be in any order. They will be tried in reverse order.
+     */
+    private static final String[] SENTINAL = {
+        "Coordinate Reference System",
+        "coordinatereferencesystem",
+        "epsg_coordinatereferencesystem"
+    };
+
+    /**
+     * Index of the {@link #SENTINAL} element which is in mixed case. No other element should be in mixed case.
+     */
+    private static final int MIXED_CASE = 0;
+
+    /**
+     * The prefix in table names. The SQL scripts are provided by EPSG with this prefix in front of all table names.
+     * SIS rather uses a modified version of those SQL scripts which creates the tables in an "EPSG" database schema.
+     * But we still need to check for existence of this prefix in case someone used the original SQL scripts.
+     */
+    static final String TABLE_PREFIX = "epsg_";
+
+    /**
+     * 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}.
+     */
+    private final String schema;
+
+    /**
+     * Mapping from words used in the MS-Access database to words used in the ANSI versions of EPSG databases.
+     * A word may be a table or a column name, or a part of it. A table name may consist in many words separated
+     * by spaces.
+     *
+     * <p>The keys are the names in the MS-Access database, and the values are the names in the SQL scripts.
+     * By convention, all column names in keys are in upper-case while table names are in mixed-case characters.</p>
+     */
+    private final Map<String,String> accessToAnsi;
+
+    /**
+     * {@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>
+     */
+    private boolean quoteTableNames;
+
+    /**
+     * The characters used for quoting identifiers, or a whitespace if none.
+     * This information is provided by {@link DatabaseMetaData#getIdentifierQuoteString()}.
+     */
+    private final String quote;
+
+    /**
+     * Creates a new adapter for the database described by the given metadata.
+     * This constructor:
+     *
+     * <ul>
+     *   <li>gets the characters to use for quoting identifiers,</li>
+     *   <li>finds the schema where are located the EPSG tables,</li>
+     *   <li>detects if the table names are the ones used in MS-Access database or in the DDL scripts.</li>
+     * </ul>
+     *
+     * <div class="note"><b>API design note:</b>
+     * this constructor is for sub-classing only. Otherwise, instances of {@code SQLAdapter} should not need to be
+     * created explicitely since instantiations are performed automatically by {@link EPSGFactory} when first needed.</div>
+     *
+     * @param  md Information about the database.
+     * @throws SQLException if an error occurred while querying the database metadata.
+     */
+    protected SQLAdapter(final DatabaseMetaData md) throws SQLException {
+        ArgumentChecks.ensureNonNull("md", md);
+        quote = md.getIdentifierQuoteString();
+        schema = findSchema(md);
+        if (quoteTableNames) {
+            /*
+             * MS-Access database uses a column named "ORDER" in the "Coordinate Axis" table.
+             * This column has been renamed "coord_axis_order" in DLL scripts.
+             * We need to check which name our current database uses.
+             */
+            try (ResultSet result = md.getColumns(null, schema, "Coordinate Axis", "ORDER")) {
+                if (result.next()) {
+                    accessToAnsi = Collections.emptyMap();
+                } else {
+                    accessToAnsi = Collections.singletonMap("ORDER", "coord_axis_order");
+                }
+            }
+        } else {
+            accessToAnsi = new HashMap<>(4);
+            accessToAnsi.put("ORDER",                "coord_axis_order");     // A column, not a table.
+            accessToAnsi.put("Coordinate_Operation", "coordoperation");
+            accessToAnsi.put("Parameter",            "param");
+        }
+    };
+
+    /**
+     * Returns the schema where the EPSG tables seems to be located.
+     */
+    private String findSchema(final DatabaseMetaData md) throws SQLException {
+        final boolean toUpperCase = md.storesUpperCaseIdentifiers();
+        for (int i = SENTINAL.length; --i >= 0;) {
+            String table = SENTINAL[i];
+            if (toUpperCase && i != MIXED_CASE) {
+                table = table.toUpperCase(Locale.US);
+            }
+            try (ResultSet result = md.getTables(null, null, table, null)) {
+                if (result.next()) {
+                    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;
+                }
+            }
+        }
+        throw new SQLDataException(Errors.format(Errors.Keys.TableNotFound_1, SENTINAL[MIXED_CASE]));
+    }
+
+    /**
+     * Adapts the given SQL statement from the original MS-Access syntax to the syntax of the target database.
+     * Table and column names may also be replaced.
+     *
+     * @param  sql The statement in MS-Access syntax.
+     * @return The SQL statement adapted to the syntax of the target database.
+     * @throws SQLException if an error occurred while adapting the SQL statement.
+     */
+    public String adaptSQL(final String sql) throws SQLException {
+        if (schema == null && accessToAnsi.isEmpty() && quote.trim().isEmpty()) {
+            return sql;
+        }
+        final StringBuilder ansi = new StringBuilder(sql.length() + 16);
+        int start, end = 0;
+        while ((start = sql.indexOf('[', end)) >= 0) {
+            /*
+             * Append every characters since the end of the last processed table/column name,
+             * or since the beginning of the SQL statement if we are in the first iteration.
+             * Then find the end of the new table/column name to process in this iteration.
+             */
+            ansi.append(sql, end, start);
+            if ((end = sql.indexOf(']', ++start)) < 0) {
+                throw new IllegalArgumentException(Errors.format(
+                        Errors.Keys.MissingCharacterInElement_2, sql.substring(start), ']'));
+            }
+            /*
+             * The name can be a table name or a column name, but only table names will be quoted.
+             * EPSG seems to write all column names in upper-case (MS-Access) or lower-case (ANSI),
+             * so we will let the database driver selects the case of its choice for column names.
+             */
+            final String name = sql.substring(start, end++);
+            if (CharSequences.isUpperCase(name)) {
+                ansi.append(accessToAnsi.getOrDefault(name, name));
+            } else {
+                if (schema != null) {
+                    ansi.append(quote).append(schema).append(quote).append('.');
+                }
+                if (quoteTableNames) {
+                    ansi.append(quote);
+                }
+                if (schema == null) {
+                    ansi.append(TABLE_PREFIX);
+                }
+                if (quoteTableNames) {
+                    ansi.append(accessToAnsi.getOrDefault(name, name)).append(quote);
+                } else {
+                    for (final String word : name.split("\\s")) {
+                        ansi.append(accessToAnsi.getOrDefault(word, word));
+                    }
+                }
+            }
+        }
+        return ansi.append(sql, end, sql.length()).toString();
+    }
+}

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

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

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java?rev=1722651&r1=1722650&r2=1722651&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/util/CharSequences.java [UTF-8] Sat Jan  2 18:22:43 2016
@@ -73,7 +73,7 @@ import static java.lang.Character.*;
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.3
- * @version 0.4
+ * @version 0.7
  * @module
  *
  * @see StringBuilders
@@ -396,7 +396,7 @@ search:     for (; fromIndex <= toIndex;
             }
             char head = (char) toSearch;
             char tail = (char) 0;
-            if (head != toSearch) { // Outside BMP plane?
+            if (head != toSearch) {                     // Outside BMP plane?
                 head = highSurrogate(toSearch);
                 tail = lowSurrogate (toSearch);
                 toIndex--;
@@ -1323,12 +1323,13 @@ searchWordBreak:    while (true) {
     }
 
     /**
-     * Creates an acronym from the given text. If every characters in the given text are upper
-     * case, then the text is returned unchanged on the assumption that it is already an acronym.
-     * Otherwise this method returns a string containing the first character of each word, where
-     * the words are separated by the camel case convention, the {@code '_'} character, or any
-     * character which is not a {@linkplain Character#isUnicodeIdentifierPart(int) Unicode
-     * identifier part} (including spaces).
+     * Creates an acronym from the given text. This method returns a string containing the first character of each word,
+     * where the words are separated by the camel case convention, the {@code '_'} character, or any character which is
+     * not a {@linkplain Character#isUnicodeIdentifierPart(int) Unicode identifier part} (including spaces).
+     *
+     * <p>An exception to the above rule happens if the given text is a Unicode identifier without the {@code '_'}
+     * character, and every characters are upper case. In such case the text is returned unchanged on the assumption
+     * that it is already an acronym.</p>
      *
      * <p><b>Examples:</b> given {@code "northEast"}, this method returns {@code "NE"}.
      * Given {@code "Open Geospatial Consortium"}, this method returns {@code "OGC"}.</p>
@@ -1338,9 +1339,9 @@ searchWordBreak:    while (true) {
      */
     public static CharSequence camelCaseToAcronym(CharSequence text) {
         text = trimWhitespaces(text);
-        if (text != null && !isUpperCase(text, 0, text.length())) {
+        if (text != null && !isAcronym(text)) {
             final int length = text.length();
-            final StringBuilder buffer = new StringBuilder(8); // Acronyms are usually short.
+            final StringBuilder buffer = new StringBuilder(8);              // Acronyms are usually short.
             boolean wantChar = true;
             for (int i=0; i<length;) {
                 final int c = codePointAt(text, i);
@@ -1367,7 +1368,7 @@ searchWordBreak:    while (true) {
                  * first one is upper-case as well. This is for handling the identifiers which
                  * are compliant to Java-Beans convention (e.g. "northEast").
                  */
-                if (isUpperCase(buffer, 1, acrlg)) {
+                if (isUpperCase(buffer, 1, acrlg, true)) {
                     final int c = buffer.codePointAt(0);
                     final int up = toUpperCase(c);
                     if (c != up) {
@@ -1494,6 +1495,16 @@ cmp:    while (ia < lga) {
     }
 
     /**
+     * Returns {@code true} if the given text is presumed to be an acronym. Acronyms are presumed
+     * to be valid Unicode identifiers in all upper-case letters and without the {@code '_'} character.
+     *
+     * @see #camelCaseToAcronym(CharSequence)
+     */
+    private static boolean isAcronym(final CharSequence text) {
+        return isUpperCase(text) && indexOf(text, '_', 0, text.length()) < 0 && isUnicodeIdentifier(text);
+    }
+
+    /**
      * Returns {@code true} if the given identifier is a legal Unicode identifier.
      * This method returns {@code true} if the identifier length is greater than zero,
      * the first character is a {@linkplain Character#isUnicodeIdentifierStart(int)
@@ -1556,27 +1567,43 @@ cmp:    while (ia < lga) {
     }
 
     /**
-     * Returns {@code true} if every characters in the given sequence are
-     * {@linkplain Character#isUpperCase(int) upper-case} letters.
-     *
-     * <div class="note"><b>Note:</b>
-     * The behavior of this method regarding digits and punctuation is unspecified
-     * and may change in future versions.</div>
+     * Returns {@code true} if the given text is non-null, contains at least one upper-case character and
+     * no lower-case character. Space and punctuation are ignored.
      *
-     * @param  text The character sequence to test.
-     * @return {@code true} if every character are upper-case.
+     * @param  text The character sequence to test (may be {@code null}).
+     * @return {@code true} if non-null, contains at least one upper-case character and no lower-case character.
      *
      * @see String#toUpperCase()
+     *
+     * @since 0.7
      */
-    static boolean isUpperCase(final CharSequence text, int lower, final int upper) {
+    public static boolean isUpperCase(final CharSequence text) {
+        return isUpperCase(text, 0, length(text), false);
+    }
+
+    /**
+     * Returns {@code true} if the given sub-sequence is non-null, contains at least one upper-case character and
+     * no lower-case character. Space and punctuation are ignored.
+     *
+     * @param  text  The character sequence to test.
+     * @param  lower Index of the first character to check, inclusive.
+     * @param  upper Index of the last character to check, exclusive.
+     * @param  hasUpperCase {@code true} if this method should behave as if the given text already had
+     *         at least one upper-case character (not necessarily in the portion given by the indices).
+     * @return {@code true} if contains at least one upper-case character and no lower-case character.
+     */
+    private static boolean isUpperCase(final CharSequence text, int lower, final int upper, boolean hasUpperCase) {
         while (lower < upper) {
             final int c = codePointAt(text, lower);
-            if (!Character.isUpperCase(c)) {
+            if (Character.isLowerCase(c)) {
                 return false;
             }
+            if (!hasUpperCase) {
+                hasUpperCase = Character.isUpperCase(c);
+            }
             lower += charCount(c);
         }
-        return true;
+        return hasUpperCase;
     }
 
     /**

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=1722651&r1=1722650&r2=1722651&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] Sat Jan  2 18:22:43 2016
@@ -911,6 +911,11 @@ public final class Errors extends Indexe
         public static final short StreamIsForwardOnly_1 = 103;
 
         /**
+         * Table “{0}” has not been found.
+         */
+        public static final short TableNotFound_1 = 216;
+
+        /**
          * Expected at least {0} argument{0,choice,1#|2#s}, but got {1}.
          */
         public static final short TooFewArguments_2 = 104;

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=1722651&r1=1722650&r2=1722651&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] Sat Jan  2 18:22:43 2016
@@ -193,6 +193,7 @@ RequireDecimalSeparator           = A de
 SingularMatrix                    = Matrix is singular.
 StalledThread_1                   = Thread \u201c{0}\u201d seems stalled.
 StreamIsForwardOnly_1             = Can not move backward in the \u201c{0}\u201d stream.
+TableNotFound_1                   = Table \u201c{0}\u201d has not been found.
 TooFewArguments_2                 = Expected at least {0} argument{0,choice,1#|2#s}, but got {1}.
 TooFewOccurrences_2               = Too few occurrences of \u201c{1}\u201d. Expected at least {0} of them.
 TooManyArguments_2                = Expected at most {0} argument{0,choice,1#|2#s}, but got {1}.

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=1722651&r1=1722650&r2=1722651&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] Sat Jan  2 18:22:43 2016
@@ -189,6 +189,7 @@ RequireDecimalSeparator           = Un s
 SingularMatrix                    = La matrice est singuli\u00e8re.
 StalledThread_1                   = La t\u00e2che \u00ab\u202f{0}\u202f\u00bb semble bloqu\u00e9e.
 StreamIsForwardOnly_1             = Ne peut pas reculer dans le flux de donn\u00e9es \u00ab\u202f{0}\u202f\u00bb.
+TableNotFound_1                   = La table \u00ab\u202f{0}\u202f\u00bb n\u2019a pas \u00e9t\u00e9 trouv\u00e9e.
 TooFewArguments_2                 = Au moins {0} argument{0,choice,1# \u00e9tait attendu|2#s \u00e9taient attendus}, mais seulement {1} {1,choice,1#a \u00e9t\u00e9 sp\u00e9cifi\u00e9|2#ont \u00e9t\u00e9 sp\u00e9cifi\u00e9s}.
 TooFewOccurrences_2               = Trop peu d\u2019occurrences de \u00ab\u202f{1}\u202f\u00bb. Il en faut au moins {0}.
 TooManyArguments_2                = Au plus {0} argument{0,choice,1# \u00e9tait attendu|2#s \u00e9taient attendus}, mais {1} {1,choice,1#a \u00e9t\u00e9 sp\u00e9cifi\u00e9|2#ont \u00e9t\u00e9 sp\u00e9cifi\u00e9s}.

Modified: sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/util/CharSequencesTest.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/util/CharSequencesTest.java?rev=1722651&r1=1722650&r2=1722651&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/util/CharSequencesTest.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/util/CharSequencesTest.java [UTF-8] Sat Jan  2 18:22:43 2016
@@ -34,7 +34,7 @@ import static org.apache.sis.util.CharSe
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Johann Sorel (Geomatys)
  * @since   0.3
- * @version 0.6
+ * @version 0.7
  * @module
  */
 @DependsOn({
@@ -315,6 +315,7 @@ public final strictfp class CharSequence
         assertEquals("OGC", camelCaseToAcronym("OGC").toString());
         assertEquals("OGC", camelCaseToAcronym("Open Geospatial Consortium").toString());
         assertEquals("E",   camelCaseToAcronym("East").toString());
+        assertEquals("E",   camelCaseToAcronym("east").toString());
         assertEquals("NE",  camelCaseToAcronym("North-East").toString());
         assertEquals("NE",  camelCaseToAcronym("NORTH_EAST").toString());
         assertEquals("NE",  camelCaseToAcronym("northEast").toString());
@@ -409,13 +410,19 @@ public final strictfp class CharSequence
     }
 
     /**
-     * Tests the {@link CharSequences#isUpperCase(CharSequence, int, int)} method.
+     * Tests the {@link CharSequences#isUpperCase(CharSequence)} method.
      */
     @Test
     public void testIsUpperCase() {
-        assertTrue ("ABC", isUpperCase("ABC", 0, 3));
-        assertFalse("AbC", isUpperCase("AbC", 0, 3));
-        assertFalse("A2C", isUpperCase("A2C", 0, 3));
+        assertFalse("null",  isUpperCase(null));
+        assertFalse("empty", isUpperCase(""));
+        assertTrue ("ABC",   isUpperCase("ABC"));
+        assertFalse("AbC",   isUpperCase("AbC"));
+        assertTrue ("A2C",   isUpperCase("A2C"));
+        assertFalse("A2c",   isUpperCase("A2c"));
+        assertTrue ("A.C",   isUpperCase("A.C"));
+        assertTrue ("A C",   isUpperCase("A C"));
+        assertFalse(".2-",   isUpperCase(".2-"));
     }
 
     /**



Mime
View raw message