]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-7549 SONAR-6171 verifies UTF8 charset and case-sensitive collation
authorSimon Brandhof <simon.brandhof@sonarsource.com>
Thu, 14 Apr 2016 07:39:13 +0000 (09:39 +0200)
committerSimon Brandhof <simon.brandhof@sonarsource.com>
Mon, 25 Apr 2016 13:24:03 +0000 (15:24 +0200)
25 files changed:
server/sonar-server/src/main/java/org/sonar/server/db/VerifyDatabaseCharsetAfterMigration.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/db/VerifyDatabaseCharsetAtStartup.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/db/migrations/PlatformDatabaseMigration.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel2.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevelSafeMode.java
server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationAsynchronousTest.java
server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationConcurrentAccessTest.java
server/sonar-server/src/test/java/org/sonar/server/db/migrations/PlatformDatabaseMigrationTest.java
sonar-db/src/main/java/org/sonar/db/CollationChecker.java [deleted file]
sonar-db/src/main/java/org/sonar/db/charset/CharsetHandler.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/charset/DatabaseCharsetChecker.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/charset/MssqlCharsetHandler.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/charset/MysqlCharsetHandler.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/charset/OracleCharsetHandler.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/charset/PostgresCharsetHandler.java [new file with mode: 0644]
sonar-db/src/main/java/org/sonar/db/charset/package-info.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/CollationCheckerTest.java [deleted file]
sonar-db/src/test/java/org/sonar/db/charset/DatabaseCharsetCheckerTest.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/charset/MssqlCharsetHandlerTest.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/charset/MysqlCharsetHandlerTest.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/charset/MysqlCollationEditorTest.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/charset/OracleCharsetHandlerTest.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/charset/PostgresCharsetHandlerTest.java [new file with mode: 0644]
sonar-db/src/test/java/org/sonar/db/charset/SelectExecutorTest.java [new file with mode: 0644]

diff --git a/server/sonar-server/src/main/java/org/sonar/server/db/VerifyDatabaseCharsetAfterMigration.java b/server/sonar-server/src/main/java/org/sonar/server/db/VerifyDatabaseCharsetAfterMigration.java
new file mode 100644 (file)
index 0000000..6d8f25c
--- /dev/null
@@ -0,0 +1,37 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.db;
+
+import org.sonar.api.platform.ServerUpgradeStatus;
+import org.sonar.db.charset.DatabaseCharsetChecker;
+
+public class VerifyDatabaseCharsetAfterMigration extends VerifyDatabaseCharsetAtStartup {
+
+  public VerifyDatabaseCharsetAfterMigration(ServerUpgradeStatus upgradeStatus, DatabaseCharsetChecker charsetChecker) {
+    super(upgradeStatus, charsetChecker);
+  }
+
+  @Override
+  public void start() {
+    if (getUpgradeStatus().isFreshInstall() || getUpgradeStatus().isUpgraded()) {
+      check();
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/db/VerifyDatabaseCharsetAtStartup.java b/server/sonar-server/src/main/java/org/sonar/server/db/VerifyDatabaseCharsetAtStartup.java
new file mode 100644 (file)
index 0000000..aadd9ec
--- /dev/null
@@ -0,0 +1,54 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.db;
+
+import org.picocontainer.Startable;
+import org.sonar.api.platform.ServerUpgradeStatus;
+import org.sonar.db.charset.DatabaseCharsetChecker;
+
+public class VerifyDatabaseCharsetAtStartup implements Startable {
+
+  private final ServerUpgradeStatus upgradeStatus;
+  private final DatabaseCharsetChecker charsetChecker;
+
+  public VerifyDatabaseCharsetAtStartup(ServerUpgradeStatus upgradeStatus, DatabaseCharsetChecker charsetChecker) {
+    this.upgradeStatus = upgradeStatus;
+    this.charsetChecker = charsetChecker;
+  }
+
+  @Override
+  public void start() {
+    check();
+  }
+
+  @Override
+  public void stop() {
+    // do nothing
+  }
+
+  protected void check() {
+    boolean enforceUtf8 = upgradeStatus.isFreshInstall();
+    charsetChecker.check(enforceUtf8);
+  }
+
+  protected ServerUpgradeStatus getUpgradeStatus() {
+    return upgradeStatus;
+  }
+}
index a003031e6d6ace6b737a9e141b94a01232828817..d4420506492966f388796b618b5b7b59bbf44216 100644 (file)
@@ -63,7 +63,8 @@ public class PlatformDatabaseMigration implements DatabaseMigration {
   @Nullable
   private Throwable failureError;
 
-  public PlatformDatabaseMigration(RubyBridge rubyBridge, PlatformDatabaseMigrationExecutorService executorService, Platform platform) {
+  public PlatformDatabaseMigration(RubyBridge rubyBridge,
+    PlatformDatabaseMigrationExecutorService executorService, Platform platform) {
     this.rubyBridge = rubyBridge;
     this.executorService = executorService;
     this.platform = platform;
@@ -93,7 +94,7 @@ public class PlatformDatabaseMigration implements DatabaseMigration {
       return;
     }
 
-    running.getAndSet(true);
+    running.set(true);
     executorService.execute(new Runnable() {
       @Override
       public void run() {
index f1ddb825890070c81fc8e6a8538713a5e0b7a5ad..6006d20c9f0f246a723a3a74e72cfc17744e3d80 100644 (file)
@@ -24,6 +24,8 @@ import org.sonar.core.i18n.DefaultI18n;
 import org.sonar.core.i18n.RuleI18nManager;
 import org.sonar.core.platform.PluginClassloaderFactory;
 import org.sonar.core.platform.PluginLoader;
+import org.sonar.db.charset.DatabaseCharsetChecker;
+import org.sonar.server.db.VerifyDatabaseCharsetAtStartup;
 import org.sonar.server.db.migrations.DatabaseMigrator;
 import org.sonar.server.db.migrations.PlatformDatabaseMigration;
 import org.sonar.server.db.migrations.PlatformDatabaseMigrationExecutorServiceImpl;
@@ -46,6 +48,8 @@ public class PlatformLevel2 extends PlatformLevel {
     add(
       DefaultServerUpgradeStatus.class,
       DatabaseMigrator.class,
+      DatabaseCharsetChecker.class,
+      VerifyDatabaseCharsetAtStartup.class,
 
       // depends on Ruby
       PlatformRubyBridge.class,
index 6b614a41c9dc753fc46d82cb92e953055bfe43b8..14c29a30859054484671fa626fafff8fb2eaa4f2 100644 (file)
@@ -21,7 +21,7 @@ package org.sonar.server.platform.platformlevel;
 
 import org.sonar.api.utils.UriReader;
 import org.sonar.core.util.DefaultHttpDownloader;
-import org.sonar.db.CollationChecker;
+import org.sonar.server.db.VerifyDatabaseCharsetAfterMigration;
 import org.sonar.server.platform.PersistentSettings;
 import org.sonar.server.platform.ServerIdGenerator;
 import org.sonar.server.startup.ServerMetadataPersister;
@@ -34,7 +34,7 @@ public class PlatformLevel3 extends PlatformLevel {
   @Override
   protected void configureLevel() {
     add(
-      CollationChecker.class,
+      VerifyDatabaseCharsetAfterMigration.class,
       PersistentSettings.class,
       ServerMetadataPersister.class,
       DefaultHttpDownloader.class,
index 218e9c6e8b47d7f66c7de3962f726a736a9b97b2..1fb52f66f9ee121d7872076dd2e80df6b3c96731 100644 (file)
@@ -23,8 +23,8 @@ import org.sonar.server.platform.ws.DbMigrationStatusAction;
 import org.sonar.server.platform.ws.MigrateDbAction;
 import org.sonar.server.platform.ws.StatusAction;
 import org.sonar.server.platform.ws.SystemWs;
-import org.sonar.server.ws.WebServicesWs;
 import org.sonar.server.ws.WebServiceEngine;
+import org.sonar.server.ws.WebServicesWs;
 
 public class PlatformLevelSafeMode extends PlatformLevel {
   public PlatformLevelSafeMode(PlatformLevel parent) {
index 5cec2731897b31a3d80e3c6e9b5f2f2b895a5510..4c5e407ac2755f060b3df070055faa1ef69c82bd 100644 (file)
@@ -33,15 +33,15 @@ public class PlatformDatabaseMigrationAsynchronousTest {
    * Implementation of execute wraps specified Runnable to add a delay of 200 ms before passing it
    * to a SingleThread executor to execute asynchronously.
    */
-  private PlatformDatabaseMigrationExecutorService executorService = new PlatformDatabaseMigrationExecutorServiceAdaptor() {
+  PlatformDatabaseMigrationExecutorService executorService = new PlatformDatabaseMigrationExecutorServiceAdaptor() {
     @Override
     public void execute(final Runnable command) {
       taskSuppliedForAsyncProcess = true;
     }
   };
-  private RubyBridge rubyBridge = mock(RubyBridge.class);
-  private Platform platform = mock(Platform.class);
-  private PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService, platform);
+  RubyBridge rubyBridge = mock(RubyBridge.class);
+  Platform platform = mock(Platform.class);
+  PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService, platform);
 
   @Test
   public void testName() throws Exception {
index f524b6ac58cfcfd942f5ad90854bfd6439919538..d11dfffa2f717a93692beb03a7fae435555fa1f0 100644 (file)
@@ -61,7 +61,7 @@ public class PlatformDatabaseMigrationConcurrentAccessTest {
   /**
    * Implementation of RubyDatabaseMigration which trigger method increments a thread-safe counter and add a delay of 200ms
    */
-  private RubyDatabaseMigration rubyDatabaseMigration = new RubyDatabaseMigration() {
+  RubyDatabaseMigration rubyDatabaseMigration = new RubyDatabaseMigration() {
     @Override
     public void trigger() {
       triggerCount.incrementAndGet();
@@ -72,10 +72,10 @@ public class PlatformDatabaseMigrationConcurrentAccessTest {
       }
     }
   };
-  private RubyBridge rubyBridge = mock(RubyBridge.class);
-  private Platform platform = mock(Platform.class);
-  private RubyRailsRoutes railsRoutes = mock(RubyRailsRoutes.class);
-  private PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService, platform);
+  RubyBridge rubyBridge = mock(RubyBridge.class);
+  Platform platform = mock(Platform.class);
+  RubyRailsRoutes railsRoutes = mock(RubyRailsRoutes.class);
+  PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService, platform);
 
   @After
   public void tearDown() {
index 969b27f4c6f298fc616d66a58cac421a0b261ce7..c4251d4b504bdea7db8f02c833b76f7475438143 100644 (file)
@@ -44,19 +44,19 @@ public class PlatformDatabaseMigrationTest {
   /**
    * Implementation of execute runs Runnable synchronously.
    */
-  private PlatformDatabaseMigrationExecutorService executorService = new PlatformDatabaseMigrationExecutorServiceAdaptor() {
+  PlatformDatabaseMigrationExecutorService executorService = new PlatformDatabaseMigrationExecutorServiceAdaptor() {
     @Override
     public void execute(Runnable command) {
       command.run();
     }
   };
-  private RubyBridge rubyBridge = mock(RubyBridge.class);
-  private RubyDatabaseMigration rubyDatabaseMigration = mock(RubyDatabaseMigration.class);
-  private RubyRailsRoutes rubyRailsRoutes = mock(RubyRailsRoutes.class);
-  private Platform platform = mock(Platform.class);
-  private InOrder inOrder = inOrder(rubyDatabaseMigration, rubyBridge, rubyRailsRoutes, platform);
+  RubyBridge rubyBridge = mock(RubyBridge.class);
+  RubyDatabaseMigration rubyDatabaseMigration = mock(RubyDatabaseMigration.class);
+  RubyRailsRoutes rubyRailsRoutes = mock(RubyRailsRoutes.class);
+  Platform platform = mock(Platform.class);
+  InOrder inOrder = inOrder(rubyDatabaseMigration, rubyBridge, rubyRailsRoutes, platform);
 
-  private PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService, platform);
+  PlatformDatabaseMigration underTest = new PlatformDatabaseMigration(rubyBridge, executorService, platform);
 
   @Test
   public void status_is_NONE_when_component_is_created() {
diff --git a/sonar-db/src/main/java/org/sonar/db/CollationChecker.java b/sonar-db/src/main/java/org/sonar/db/CollationChecker.java
deleted file mode 100644 (file)
index b09d923..0000000
+++ /dev/null
@@ -1,241 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.db;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
-import javax.annotation.CheckForNull;
-import org.apache.commons.lang.StringUtils;
-import org.picocontainer.Startable;
-import org.sonar.api.utils.MessageException;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.db.dialect.H2;
-import org.sonar.db.dialect.MsSql;
-import org.sonar.db.dialect.MySql;
-import org.sonar.db.dialect.Oracle;
-import org.sonar.db.dialect.PostgreSql;
-
-import static java.lang.String.format;
-import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
-import static org.apache.commons.lang.StringUtils.endsWithIgnoreCase;
-
-/**
- * SONAR-6171
- * Check that database has UTF8 character set and case-sensitive collation.
- * As obviously tables must be checked after being created, this component
- * must not be executed at the same time as {@link DatabaseChecker}.
- */
-public class CollationChecker implements Startable {
-
-  private static final String UTF8 = "utf8";
-  private final Database db;
-  private final StatementExecutor statementExecutor;
-
-  public CollationChecker(Database db) {
-    this(db, new StatementExecutor());
-  }
-
-  @VisibleForTesting
-  CollationChecker(Database db, StatementExecutor statementExecutor) {
-    this.db = db;
-    this.statementExecutor = statementExecutor;
-  }
-
-  @Override
-  public void start() {
-    try {
-      Loggers.get(getClass()).info("Verify database collation");
-      check();
-    } catch (SQLException e) {
-      throw new IllegalStateException(e);
-    }
-  }
-
-  @Override
-  public void stop() {
-    // nothing to do
-  }
-
-  private void check() throws SQLException {
-    try (Connection connection = db.getDataSource().getConnection()) {
-      switch (db.getDialect().getId()) {
-        case H2.ID:
-          // nothing to check
-          break;
-        case Oracle.ID:
-          checkOracle(connection);
-          break;
-        case PostgreSql.ID:
-          checkPostgreSql(connection);
-          break;
-        case MySql.ID:
-          checkMySql(connection);
-          break;
-        case MsSql.ID:
-          checkMsSql(connection);
-          break;
-        default:
-          throw new IllegalArgumentException("Database not supported: " + db.getDialect().getId());
-      }
-    }
-  }
-
-  /**
-   * Oracle does not allow to override character set on tables. Only global charset is verified.
-   */
-  private void checkOracle(Connection connection) throws SQLException {
-    String charset = selectSingleCell(connection, "select value from nls_database_parameters where parameter='NLS_CHARACTERSET'");
-    String sort = selectSingleCell(connection, "select value from nls_database_parameters where parameter='NLS_SORT'");
-    if (!containsIgnoreCase(charset, UTF8) || !"BINARY".equalsIgnoreCase(sort)) {
-      throw MessageException.of(format("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is %s and NLS_SORT is %s.", charset, sort));
-    }
-  }
-
-  /**
-   * PostgreSQL does not support case-insensitive collations. Only character set must be verified.
-   */
-  private void checkPostgreSql(Connection connection) throws SQLException {
-    // Character set is defined globally and can be overridden on each column.
-    // This request returns all VARCHAR columns. Collation may be empty.
-    // Examples:
-    // issues | key | ''
-    // projects | name | utf8
-    List<String[]> rows = select(connection, "select table_name, column_name, collation_name " +
-      "from information_schema.columns " +
-      "where table_schema='public' " +
-      "and udt_name='varchar' " +
-      "order by table_name, column_name", 3);
-    boolean mustCheckGlobalCollation = false;
-    List<String> errors = new ArrayList<>();
-    for (String[] row : rows) {
-      if (StringUtils.isBlank(row[2])) {
-        mustCheckGlobalCollation = true;
-      } else if (!containsIgnoreCase(row[2], UTF8)) {
-        errors.add(format("%s.%s", row[0], row[1]));
-      }
-    }
-
-    if (mustCheckGlobalCollation) {
-      String charset = selectSingleCell(connection, "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()");
-      if (!containsIgnoreCase(charset, UTF8)) {
-        throw MessageException.of(format("Database charset is %s. It must be UTF8.", charset));
-      }
-    }
-    if (!errors.isEmpty()) {
-      throw MessageException.of(format("Database columns [%s] must have UTF8 charset.", Joiner.on(", ").join(errors)));
-    }
-  }
-
-  /**
-   * Check VARCHAR columns
-   */
-  private void checkMySql(Connection connection) throws SQLException {
-    // All VARCHAR columns are returned. No need to check database general collation.
-    // Example of row:
-    // issues | kee | utf8 | utf8_bin
-    List<String[]> rows = select(connection,
-      "SELECT table_name, column_name, character_set_name, collation_name " +
-        "FROM INFORMATION_SCHEMA.columns " +
-        "WHERE table_schema=database() and character_set_name is not null and collation_name is not null", 4 /* columns */);
-    List<String> errors = new ArrayList<>();
-    for (String[] row : rows) {
-      if (!containsIgnoreCase(row[2], UTF8) || endsWithIgnoreCase(row[3], "_ci")) {
-        errors.add(format("%s.%s", row[0], row[1]));
-      }
-    }
-    if (!errors.isEmpty()) {
-      throw MessageException.of(format("UTF8 charset and case-sensitive collation are required for database columns [%s]", Joiner.on(", ").join(errors)));
-    }
-  }
-
-  private void checkMsSql(Connection connection) throws SQLException {
-    // All VARCHAR columns are returned. No need to check database general collation.
-    // Example of row:
-    // issues | kee | Latin1_General_CS_AS
-    List<String[]> rows = select(connection,
-      "SELECT table_name, column_name, collation_name " +
-        "FROM [INFORMATION_SCHEMA].[COLUMNS] " +
-        "WHERE collation_name is not null " +
-        "ORDER BY table_name,column_name", 3 /* columns */);
-    List<String> errors = new ArrayList<>();
-    for (String[] row : rows) {
-      if (!endsWithIgnoreCase(row[2], "_CS_AS")) {
-        errors.add(row[0] + "." + row[1]);
-      }
-    }
-    if (!errors.isEmpty()) {
-      throw MessageException.of(format("Case-sensitive and accent-sensitive charset (CS_AS) is required for database columns [%s]", Joiner.on(", ").join(errors)));
-    }
-  }
-
-  @CheckForNull
-  private String selectSingleCell(Connection connection, String sql) throws SQLException {
-    String[] cols = selectSingleRow(connection, sql, 1);
-    return cols == null ? null : cols[0];
-  }
-
-  @CheckForNull
-  private String[] selectSingleRow(Connection connection, String sql, int columns) throws SQLException {
-    List<String[]> rows = select(connection, sql, columns);
-    if (rows.isEmpty()) {
-      return null;
-    }
-    if (rows.size() == 1) {
-      return rows.get(0);
-    }
-    throw new IllegalStateException("Expecting only one result for [" + sql + "]");
-  }
-
-  private List<String[]> select(Connection connection, String sql, int columns) throws SQLException {
-    return statementExecutor.executeQuery(connection, sql, columns);
-  }
-
-  @VisibleForTesting
-  static class StatementExecutor {
-    List<String[]> executeQuery(Connection connection, String sql, int columns) throws SQLException {
-      Statement stmt = null;
-      ResultSet rs = null;
-      try {
-        stmt = connection.createStatement();
-        rs = stmt.executeQuery(sql);
-        List<String[]> result = new ArrayList<>();
-        while (rs.next()) {
-          String[] row = new String[columns];
-          for (int i = 0; i < columns; i++) {
-            row[i] = DatabaseUtils.getString(rs, i + 1);
-          }
-          result.add(row);
-        }
-        return result;
-
-      } finally {
-        DatabaseUtils.closeQuietly(rs);
-        DatabaseUtils.closeQuietly(stmt);
-      }
-    }
-  }
-
-}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/CharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/CharsetHandler.java
new file mode 100644 (file)
index 0000000..a3aceb0
--- /dev/null
@@ -0,0 +1,90 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.List;
+import javax.annotation.CheckForNull;
+import org.sonar.db.DatabaseUtils;
+
+abstract class CharsetHandler {
+
+  protected static final String UTF8 = "utf8";
+
+  private final SelectExecutor selectExecutor;
+
+  protected CharsetHandler(SelectExecutor selectExecutor) {
+    this.selectExecutor = selectExecutor;
+  }
+
+  abstract void handle(Connection connection, boolean enforceUtf8) throws SQLException;
+
+  @CheckForNull
+  protected final String selectSingleCell(Connection connection, String sql) throws SQLException {
+    String[] cols = selectSingleRow(connection, sql, 1);
+    return cols == null ? null : cols[0];
+  }
+
+  @CheckForNull
+  protected final String[] selectSingleRow(Connection connection, String sql, int columns) throws SQLException {
+    List<String[]> rows = select(connection, sql, columns);
+    if (rows.isEmpty()) {
+      return null;
+    }
+    if (rows.size() == 1) {
+      return rows.get(0);
+    }
+    throw new IllegalStateException("Expecting only one result for [" + sql + "]");
+  }
+
+  protected final List<String[]> select(Connection connection, String sql, int columns) throws SQLException {
+    return selectExecutor.executeQuery(connection, sql, columns);
+  }
+
+  @VisibleForTesting
+  static class SelectExecutor {
+    List<String[]> executeQuery(Connection connection, String sql, int columns) throws SQLException {
+      Statement stmt = null;
+      ResultSet rs = null;
+      try {
+        stmt = connection.createStatement();
+        rs = stmt.executeQuery(sql);
+        List<String[]> result = new ArrayList<>();
+        while (rs.next()) {
+          String[] row = new String[columns];
+          for (int i = 0; i < columns; i++) {
+            row[i] = DatabaseUtils.getString(rs, i + 1);
+          }
+          result.add(row);
+        }
+        return result;
+
+      } finally {
+        DatabaseUtils.closeQuietly(rs);
+        DatabaseUtils.closeQuietly(stmt);
+      }
+    }
+  }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/DatabaseCharsetChecker.java b/sonar-db/src/main/java/org/sonar/db/charset/DatabaseCharsetChecker.java
new file mode 100644 (file)
index 0000000..de54bca
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.db.Database;
+import org.sonar.db.dialect.H2;
+import org.sonar.db.dialect.MsSql;
+import org.sonar.db.dialect.MySql;
+import org.sonar.db.dialect.Oracle;
+import org.sonar.db.dialect.PostgreSql;
+
+/**
+ * On fresh installations, checks that all db columns are UTF8. On all installations on MySQL or MSSQL,
+ * whatever fresh or upgrade, fixes case-insensitive columns by converting them to
+ * case-sensitive.
+ *
+ * See SONAR-6171 and SONAR-7549
+ */
+public class DatabaseCharsetChecker {
+
+  private final Database db;
+  private final CharsetHandler.SelectExecutor selectExecutor;
+
+  public DatabaseCharsetChecker(Database db) {
+    this(db, new CharsetHandler.SelectExecutor());
+  }
+
+  @VisibleForTesting
+  DatabaseCharsetChecker(Database db, CharsetHandler.SelectExecutor selectExecutor) {
+    this.db = db;
+    this.selectExecutor = selectExecutor;
+  }
+
+  public void check(boolean enforceUtf8) {
+    try {
+      try (Connection connection = db.getDataSource().getConnection()) {
+        switch (db.getDialect().getId()) {
+          case H2.ID:
+            // nothing to check
+            break;
+          case Oracle.ID:
+            new OracleCharsetHandler(selectExecutor).handle(connection, enforceUtf8);
+            break;
+          case PostgreSql.ID:
+            new PostgresCharsetHandler(selectExecutor).handle(connection, enforceUtf8);
+            break;
+          case MySql.ID:
+            new MysqlCharsetHandler(selectExecutor).handle(connection, enforceUtf8);
+            break;
+          case MsSql.ID:
+            new MssqlCharsetHandler(selectExecutor).handle(connection, enforceUtf8);
+            break;
+          default:
+            throw new IllegalArgumentException("Database not supported: " + db.getDialect().getId());
+        }
+      }
+    } catch (SQLException e) {
+      throw new IllegalStateException(e);
+    }
+  }
+
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/MssqlCharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/MssqlCharsetHandler.java
new file mode 100644 (file)
index 0000000..a844e73
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import com.google.common.base.Joiner;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.endsWithIgnoreCase;
+
+class MssqlCharsetHandler extends CharsetHandler {
+
+  protected MssqlCharsetHandler(SelectExecutor selectExecutor) {
+    super(selectExecutor);
+  }
+
+  @Override
+  void handle(Connection connection, boolean enforceUtf8) throws SQLException {
+    Loggers.get(getClass()).info("Verify that database collation is case-sensitive and accent-sensitive");
+    checkCollation(connection);
+  }
+
+  private void checkCollation(Connection connection) throws SQLException {
+    // All VARCHAR columns are returned. No need to check database general collation.
+    // Example of row:
+    // issues | kee | Latin1_General_CS_AS
+    List<String[]> rows = select(connection,
+      "SELECT table_name, column_name, collation_name " +
+        "FROM [INFORMATION_SCHEMA].[COLUMNS] " +
+        "WHERE collation_name is not null " +
+        "ORDER BY table_name,column_name", 3 /* columns */);
+    List<String> errors = new ArrayList<>();
+    for (String[] row : rows) {
+      if (!endsWithIgnoreCase(row[2], "_CS_AS")) {
+        errors.add(row[0] + "." + row[1]);
+      }
+    }
+    if (!errors.isEmpty()) {
+      throw MessageException.of(format("Case-sensitive and accent-sensitive collation (CS_AS) is required for database columns [%s]", Joiner.on(", ").join(errors)));
+    }
+  }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/MysqlCharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/MysqlCharsetHandler.java
new file mode 100644 (file)
index 0000000..7fda519
--- /dev/null
@@ -0,0 +1,138 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
+import static org.apache.commons.lang.StringUtils.endsWithIgnoreCase;
+import static org.sonar.db.DatabaseUtils.closeQuietly;
+
+class MysqlCharsetHandler extends CharsetHandler {
+
+  private final CollationEditor collationEditor;
+
+  protected MysqlCharsetHandler(SelectExecutor selectExecutor) {
+    this(selectExecutor, new CollationEditor());
+  }
+
+  @VisibleForTesting
+  MysqlCharsetHandler(SelectExecutor selectExecutor, CollationEditor editor) {
+    super(selectExecutor);
+    this.collationEditor = editor;
+  }
+
+  @Override
+  void handle(Connection connection, boolean enforceUtf8) throws SQLException {
+    String message = "Verify that database collation is case-sensitive";
+    if (enforceUtf8) {
+      message = "Verify that database collation is UTF8";
+    }
+    Loggers.get(getClass()).info(message);
+    checkCollation(connection, enforceUtf8);
+  }
+
+  private void checkCollation(Connection connection, boolean enforceUtf8) throws SQLException {
+    // All VARCHAR columns are returned. No need to check database general collation.
+    // Example of row:
+    // issues | kee | utf8 | utf8_bin
+    List<String[]> rows = select(connection,
+      "SELECT table_name, column_name, character_set_name, collation_name " +
+        "FROM INFORMATION_SCHEMA.columns " +
+        "WHERE table_schema=database() and character_set_name is not null and collation_name is not null", 4 /* columns */);
+    List<String> utf8Errors = new ArrayList<>();
+    for (String[] row : rows) {
+      String table = row[0];
+      String column = row[1];
+      String charset = row[2];
+      String collation = row[3];
+      if (enforceUtf8 && !containsIgnoreCase(charset, UTF8)) {
+        utf8Errors.add(format("%s.%s", table, column));
+      } else if (endsWithIgnoreCase(collation, "_ci")) {
+        repairCaseInsensitiveColumn(connection, table, column, collation);
+      }
+    }
+    if (!utf8Errors.isEmpty()) {
+      throw MessageException.of(format("UTF8 case-sensitive collation is required for database columns [%s]", Joiner.on(", ").join(utf8Errors)));
+    }
+  }
+
+  private void repairCaseInsensitiveColumn(Connection connection, String table, String column, String ciCollation)
+    throws SQLException {
+    String csCollation = toCaseSensitive(ciCollation);
+    Loggers.get(getClass()).info("Changing collation of column [{}.{}] from {} to {}", table, column, ciCollation, csCollation);
+    collationEditor.alter(connection, table, column, csCollation);
+  }
+
+  @VisibleForTesting
+  static String toCaseSensitive(String caseInsensitiveCollation) {
+    // example: big5_chinese_ci becomes big5_bin
+    return StringUtils.substringBefore(caseInsensitiveCollation, "_") + "_bin";
+  }
+
+  @VisibleForTesting
+  static class CollationEditor {
+    void alter(Connection connection, String table, String column, String csCollation) throws SQLException {
+      String charset;
+      String dataType;
+      boolean isNullable;
+      int length;
+      PreparedStatement stmt = null;
+      ResultSet rs = null;
+      try {
+        stmt = connection.prepareStatement("SELECT character_set_name, data_type, is_nullable, character_maximum_length " +
+          "FROM INFORMATION_SCHEMA.columns " +
+          "WHERE table_schema=database() and table_name=? and column_name=?");
+        stmt.setString(1, table);
+        stmt.setString(2, column);
+        rs = stmt.executeQuery();
+        rs.next();
+        charset = rs.getString(1);
+        dataType = rs.getString(2);
+        isNullable = rs.getBoolean(3);
+        length = rs.getInt(4);
+      } finally {
+        closeQuietly(stmt);
+        closeQuietly(rs);
+      }
+
+      try {
+        String nullability = isNullable ? "NULL" : "NOT NULL";
+        String alter = format("ALTER TABLE %s MODIFY %s %s(%d) CHARACTER SET '%s' COLLATE '%s' %s",
+          table, column, dataType, length, charset, csCollation, nullability);
+        stmt = connection.prepareStatement(alter);
+        stmt.executeUpdate();
+      } finally {
+        closeQuietly(stmt);
+      }
+    }
+  }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/OracleCharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/OracleCharsetHandler.java
new file mode 100644 (file)
index 0000000..225ae15
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
+
+class OracleCharsetHandler extends CharsetHandler {
+
+  protected OracleCharsetHandler(SelectExecutor selectExecutor) {
+    super(selectExecutor);
+  }
+
+  @Override
+  public void handle(Connection connection, boolean enforceUtf8) throws SQLException {
+    // Oracle does not allow to override character set on tables. Only global charset is verified.
+    if (enforceUtf8) {
+      Loggers.get(getClass()).info("Verify that database charset is UTF8");
+      checkUtf8(connection);
+    }
+  }
+
+  private void checkUtf8(Connection connection) throws SQLException {
+    String charset = selectSingleCell(connection, "select value from nls_database_parameters where parameter='NLS_CHARACTERSET'");
+    String sort = selectSingleCell(connection, "select value from nls_database_parameters where parameter='NLS_SORT'");
+    if (!containsIgnoreCase(charset, UTF8) || !"BINARY".equalsIgnoreCase(sort)) {
+      throw MessageException.of(format("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is %s and NLS_SORT is %s.", charset, sort));
+    }
+  }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/PostgresCharsetHandler.java b/sonar-db/src/main/java/org/sonar/db/charset/PostgresCharsetHandler.java
new file mode 100644 (file)
index 0000000..f7917fb
--- /dev/null
@@ -0,0 +1,80 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import com.google.common.base.Joiner;
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.List;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.utils.MessageException;
+import org.sonar.api.utils.log.Loggers;
+
+import static java.lang.String.format;
+import static org.apache.commons.lang.StringUtils.containsIgnoreCase;
+
+class PostgresCharsetHandler extends CharsetHandler {
+
+  protected PostgresCharsetHandler(SelectExecutor selectExecutor) {
+    super(selectExecutor);
+  }
+
+  @Override
+  void handle(Connection connection, boolean enforceUtf8) throws SQLException {
+    // PostgreSQL does not support case-insensitive collations. Only charset must be verified.
+    if (enforceUtf8) {
+      Loggers.get(getClass()).info("Verify that database collation supports UTF8");
+      checkUtf8(connection);
+    }
+  }
+
+  private void checkUtf8(Connection connection) throws SQLException {
+    // Character set is defined globally and can be overridden on each column.
+    // This request returns all VARCHAR columns. Collation may be empty.
+    // Examples:
+    // issues | key | ''
+    // projects | name | utf8
+    List<String[]> rows = select(connection, "select table_name, column_name, collation_name " +
+      "from information_schema.columns " +
+      "where table_schema='public' " +
+      "and udt_name='varchar' " +
+      "order by table_name, column_name", 3);
+    boolean mustCheckGlobalCollation = false;
+    List<String> errors = new ArrayList<>();
+    for (String[] row : rows) {
+      if (StringUtils.isBlank(row[2])) {
+        mustCheckGlobalCollation = true;
+      } else if (!containsIgnoreCase(row[2], UTF8)) {
+        errors.add(format("%s.%s", row[0], row[1]));
+      }
+    }
+
+    if (mustCheckGlobalCollation) {
+      String charset = selectSingleCell(connection, "SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()");
+      if (!containsIgnoreCase(charset, UTF8)) {
+        throw MessageException.of(format("Database collation is %s. It must support UTF8.", charset));
+      }
+    }
+    if (!errors.isEmpty()) {
+      throw MessageException.of(format("Database columns [%s] must support UTF8 collation.", Joiner.on(", ").join(errors)));
+    }
+  }
+}
diff --git a/sonar-db/src/main/java/org/sonar/db/charset/package-info.java b/sonar-db/src/main/java/org/sonar/db/charset/package-info.java
new file mode 100644 (file)
index 0000000..25a4faf
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.db.charset;
+
+import javax.annotation.ParametersAreNonnullByDefault;
+
diff --git a/sonar-db/src/test/java/org/sonar/db/CollationCheckerTest.java b/sonar-db/src/test/java/org/sonar/db/CollationCheckerTest.java
deleted file mode 100644 (file)
index d653ab3..0000000
+++ /dev/null
@@ -1,232 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2016 SonarSource SA
- * mailto:contact AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
- */
-package org.sonar.db;
-
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.mockito.Mockito;
-import org.sonar.api.utils.MessageException;
-import org.sonar.db.dialect.MsSql;
-import org.sonar.db.dialect.MySql;
-import org.sonar.db.dialect.Oracle;
-import org.sonar.db.dialect.PostgreSql;
-
-import static java.util.Arrays.asList;
-import static java.util.Collections.singletonList;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.anyInt;
-import static org.mockito.Matchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-
-public class CollationCheckerTest {
-
-  private static final String TABLE_ISSUES = "issues";
-  private static final String TABLE_PROJECTS = "projects";
-  private static final String COLUMN_KEE = "kee";
-  private static final String COLUMN_NAME = "name";
-
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
-
-  Database db = mock(Database.class, Mockito.RETURNS_MOCKS);
-  CollationChecker.StatementExecutor statementExecutor = mock(CollationChecker.StatementExecutor.class);
-  CollationChecker underTest = new CollationChecker(db, statementExecutor);
-
-  @Test
-  public void valid_oracle() throws Exception {
-    when(db.getDialect()).thenReturn(new Oracle());
-    answerSql(
-      singletonList(new String[] {"UTF8"}), singletonList(new String[] {"BINARY"}));
-
-    underTest.start();
-  }
-
-  @Test
-  public void support_oracle_al32utf8() throws Exception {
-    when(db.getDialect()).thenReturn(new Oracle());
-    answerSql(
-      singletonList(new String[] {"AL32UTF8"}), singletonList(new String[] {"BINARY"}));
-
-    underTest.start();
-  }
-
-  @Test
-  public void fail_if_oracle_is_not_utf8() throws Exception {
-    when(db.getDialect()).thenReturn(new Oracle());
-    answerSql(
-      singletonList(new String[] {"LATIN"}), singletonList(new String[] {"BINARY"}));
-
-    expectedException.expect(MessageException.class);
-    expectedException.expectMessage("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is LATIN and NLS_SORT is BINARY.");
-
-    underTest.start();
-  }
-
-  @Test
-  public void fail_if_oracle_is_not_case_sensitive() throws Exception {
-    when(db.getDialect()).thenReturn(new Oracle());
-    answerSql(
-      singletonList(new String[] {"UTF8"}), singletonList(new String[] {"LINGUISTIC"}));
-
-    expectedException.expect(MessageException.class);
-    expectedException.expectMessage("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is UTF8 and NLS_SORT is LINGUISTIC.");
-
-    underTest.start();
-  }
-
-  @Test
-  public void fail_if_can_not_get_oracle_charset() throws Exception {
-    when(db.getDialect()).thenReturn(new Oracle());
-    answerSql(Collections.<String[]>emptyList(), Collections.<String[]>emptyList());
-
-    expectedException.expect(MessageException.class);
-
-    underTest.start();
-  }
-
-  @Test
-  public void valid_postgresql() throws Exception {
-    when(db.getDialect()).thenReturn(new PostgreSql());
-    answerSql(asList(
-      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
-      new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8"}));
-
-    underTest.start();
-  }
-
-  @Test
-  public void fail_if_postgresql_has_non_utf8_column() throws Exception {
-    when(db.getDialect()).thenReturn(new PostgreSql());
-    answerSql(asList(
-      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
-      new String[] {TABLE_PROJECTS, COLUMN_KEE, "latin"},
-      new String[] {TABLE_PROJECTS, COLUMN_NAME, "latin"}));
-
-    expectedException.expect(MessageException.class);
-    expectedException.expectMessage("Database columns [projects.kee, projects.name] must have UTF8 charset.");
-
-    underTest.start();
-  }
-
-  @Test
-  public void fail_if_postgresql_has_non_utf8_db() throws Exception {
-    when(db.getDialect()).thenReturn(new PostgreSql());
-    answerSql(
-      // first request to get columns
-      asList(
-        new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
-        new String[] {TABLE_PROJECTS, COLUMN_NAME, "" /* unset -> uses db collation */}),
-
-      // second request to get db collation
-      Arrays.<String[]>asList(new String[] {"latin"}));
-
-    expectedException.expect(MessageException.class);
-    expectedException.expectMessage("Database charset is latin. It must be UTF8.");
-
-    underTest.start();
-  }
-
-  @Test
-  public void valid_postgresql_if_utf8_db() throws Exception {
-    when(db.getDialect()).thenReturn(new PostgreSql());
-    answerSql(
-      // first request to get columns
-      asList(
-        new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
-        new String[] {TABLE_PROJECTS, COLUMN_NAME, "" /* unset -> uses db collation */}),
-
-      // second request to get db collation
-      Arrays.<String[]>asList(new String[] {"utf8"}));
-
-    // no error
-    underTest.start();
-  }
-
-  @Test
-  public void valid_mysql() throws Exception {
-    when(db.getDialect()).thenReturn(new MySql());
-    answerSql(asList(
-      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin"},
-      new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8", "utf8_bin"}));
-
-    underTest.start();
-  }
-
-  @Test
-  public void fail_if_mysql_is_not_utf8_charset() throws Exception {
-    when(db.getDialect()).thenReturn(new MySql());
-    answerSql(asList(
-      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin"},
-      new String[] {TABLE_PROJECTS, COLUMN_KEE, "latin1", "utf8_bin"},
-      new String[] {TABLE_PROJECTS, COLUMN_NAME, "latin1", "utf8_bin"}));
-
-    expectedException.expect(MessageException.class);
-    expectedException.expectMessage("UTF8 charset and case-sensitive collation are required for database columns [projects.kee, projects.name]");
-
-    underTest.start();
-  }
-
-  @Test
-  public void fail_if_mysql_is_not_case_sensitive() throws Exception {
-    when(db.getDialect()).thenReturn(new MySql());
-    answerSql(asList(
-      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin"},
-      new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8", "latin1_swedish_ci"}));
-
-    expectedException.expect(MessageException.class);
-
-    underTest.start();
-  }
-
-  @Test
-  public void valid_mssql() throws Exception {
-    when(db.getDialect()).thenReturn(new MsSql());
-    answerSql(asList(
-      new String[] {TABLE_ISSUES, COLUMN_KEE, "Latin1_General_CS_AS"},
-      new String[] {TABLE_PROJECTS, COLUMN_NAME, "Latin1_General_CS_AS"}));
-
-    underTest.start();
-  }
-
-  @Test
-  public void fail_if_mssql_is_not_case_sensitive() throws Exception {
-    when(db.getDialect()).thenReturn(new MsSql());
-    answerSql(asList(
-      new String[] {TABLE_ISSUES, COLUMN_KEE, "Latin1_General_CS_AS"},
-      new String[] {TABLE_PROJECTS, COLUMN_KEE, "Latin1_General_CI_AI"},
-      new String[] {TABLE_PROJECTS, COLUMN_NAME, "Latin1_General_CI_AI"}));
-
-    expectedException.expect(MessageException.class);
-    expectedException.expectMessage("Case-sensitive and accent-sensitive charset (CS_AS) is required for database columns [projects.kee, projects.name]");
-
-    underTest.start();
-  }
-
-  private void answerSql(List<String[]> firstRequest, List<String[]>... otherRequests) throws SQLException {
-    when(statementExecutor.executeQuery(any(Connection.class), anyString(), anyInt())).thenReturn(firstRequest, otherRequests);
-  }
-}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/DatabaseCharsetCheckerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/DatabaseCharsetCheckerTest.java
new file mode 100644 (file)
index 0000000..54b166d
--- /dev/null
@@ -0,0 +1,40 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.sonar.db.Database;
+import org.sonar.db.dialect.H2;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class DatabaseCharsetCheckerTest {
+
+  Database db = mock(Database.class, Mockito.RETURNS_MOCKS);
+  DatabaseCharsetChecker underTest = new DatabaseCharsetChecker(db);
+
+  @Test
+  public void does_nothing_if_h2() throws Exception {
+    when(db.getDialect()).thenReturn(new H2());
+    underTest.check(true);
+  }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/MssqlCharsetHandlerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/MssqlCharsetHandlerTest.java
new file mode 100644 (file)
index 0000000..5f9b8c6
--- /dev/null
@@ -0,0 +1,75 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.MessageException;
+
+import static java.util.Arrays.asList;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class MssqlCharsetHandlerTest {
+
+  private static final String TABLE_ISSUES = "issues";
+  private static final String TABLE_PROJECTS = "projects";
+  private static final String COLUMN_KEE = "kee";
+  private static final String COLUMN_NAME = "name";
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  CharsetHandler.SelectExecutor selectExecutor = mock(CharsetHandler.SelectExecutor.class);
+  MssqlCharsetHandler underTest = new MssqlCharsetHandler(selectExecutor);
+
+  @Test
+  public void checks_case_sensibility() throws Exception {
+    answerSql(asList(
+      new String[] {TABLE_ISSUES, COLUMN_KEE, "Latin1_General_CS_AS"},
+      new String[] {TABLE_PROJECTS, COLUMN_NAME, "Latin1_General_CS_AS"}));
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void fails_if_case_insensitive() throws Exception {
+    answerSql(asList(
+      new String[] {TABLE_ISSUES, COLUMN_KEE, "Latin1_General_CS_AS"},
+      new String[] {TABLE_PROJECTS, COLUMN_KEE, "Latin1_General_CI_AI"},
+      new String[] {TABLE_PROJECTS, COLUMN_NAME, "Latin1_General_CI_AI"}));
+
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage("Case-sensitive and accent-sensitive collation (CS_AS) is required for database columns [projects.kee, projects.name]");
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  private void answerSql(List<String[]> firstRequest, List<String[]>... otherRequests) throws SQLException {
+    when(selectExecutor.executeQuery(any(Connection.class), anyString(), anyInt())).thenReturn(firstRequest, otherRequests);
+  }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/MysqlCharsetHandlerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/MysqlCharsetHandlerTest.java
new file mode 100644 (file)
index 0000000..3727419
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.MessageException;
+
+import static java.util.Arrays.asList;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class MysqlCharsetHandlerTest {
+
+  private static final String TABLE_ISSUES = "issues";
+  private static final String TABLE_PROJECTS = "projects";
+  private static final String COLUMN_KEE = "kee";
+  private static final String COLUMN_NAME = "name";
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  CharsetHandler.SelectExecutor selectExecutor = mock(CharsetHandler.SelectExecutor.class);
+  MysqlCharsetHandler.CollationEditor collationEditor = mock(MysqlCharsetHandler.CollationEditor.class);
+  MysqlCharsetHandler underTest = new MysqlCharsetHandler(selectExecutor, collationEditor);
+
+  @Test
+  public void checks_utf8() throws Exception {
+    answerSql(asList(
+      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin"},
+      new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8", "utf8_bin"}));
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void fails_if_not_utf8() throws Exception {
+    answerSql(asList(
+      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin"},
+      new String[] {TABLE_PROJECTS, COLUMN_KEE, "latin1", "utf8_bin"},
+      new String[] {TABLE_PROJECTS, COLUMN_NAME, "latin1", "utf8_bin"}));
+
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage("UTF8 case-sensitive collation is required for database columns [projects.kee, projects.name]");
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void repairs_case_insensitive_column() throws Exception {
+    answerSql(asList(
+      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8", "utf8_bin"},
+      new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8", "latin1_swedish_ci"}));
+
+    Connection connection = mock(Connection.class);
+    underTest.handle(connection, true);
+
+    verify(collationEditor).alter(connection, TABLE_PROJECTS, COLUMN_NAME, "latin1_bin");
+  }
+
+  @Test
+  public void tests_toCaseSensitive() {
+    assertThat(MysqlCharsetHandler.toCaseSensitive("big5_chinese_ci")).isEqualTo("big5_bin");
+  }
+
+  private void answerSql(List<String[]> firstRequest, List<String[]>... otherRequests) throws SQLException {
+    when(selectExecutor.executeQuery(any(Connection.class), anyString(), anyInt())).thenReturn(firstRequest, otherRequests);
+  }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/MysqlCollationEditorTest.java b/sonar-db/src/test/java/org/sonar/db/charset/MysqlCollationEditorTest.java
new file mode 100644 (file)
index 0000000..a5bf4a1
--- /dev/null
@@ -0,0 +1,30 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import org.junit.Rule;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbTester;
+
+public class MysqlCollationEditorTest {
+  @Rule
+  public DbTester db = DbTester.createForSchema(System2.INSTANCE, MysqlCollationEditorTest.class, "schema.sql");
+
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/OracleCharsetHandlerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/OracleCharsetHandlerTest.java
new file mode 100644 (file)
index 0000000..acda3df
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Collections;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.MessageException;
+
+import static java.util.Collections.singletonList;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class OracleCharsetHandlerTest {
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  CharsetHandler.SelectExecutor selectExecutor = mock(CharsetHandler.SelectExecutor.class);
+  OracleCharsetHandler underTest = new OracleCharsetHandler(selectExecutor);
+
+  @Test
+  public void checks_utf8() throws Exception {
+    answerSql(
+      singletonList(new String[] {"UTF8"}), singletonList(new String[] {"BINARY"}));
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void supports_al32utf8() throws Exception {
+    answerSql(
+      singletonList(new String[] {"AL32UTF8"}), singletonList(new String[] {"BINARY"}));
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void fails_if_charset_is_not_utf8() throws Exception {
+    answerSql(
+      singletonList(new String[] {"LATIN"}), singletonList(new String[] {"BINARY"}));
+
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is LATIN and NLS_SORT is BINARY.");
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void fails_if_not_case_sensitive() throws Exception {
+    answerSql(
+      singletonList(new String[] {"UTF8"}), singletonList(new String[] {"LINGUISTIC"}));
+
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage("Oracle must be have UTF8 charset and BINARY sort. NLS_CHARACTERSET is UTF8 and NLS_SORT is LINGUISTIC.");
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void fails_if_can_not_get_charset() throws Exception {
+    answerSql(Collections.<String[]>emptyList(), Collections.<String[]>emptyList());
+
+    expectedException.expect(MessageException.class);
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void does_nothing_if_utf8_must_not_verified() throws Exception {
+    underTest.handle(mock(Connection.class), false);
+  }
+
+  private void answerSql(List<String[]> firstRequest, List<String[]>... otherRequests) throws SQLException {
+    when(selectExecutor.executeQuery(any(Connection.class), anyString(), anyInt())).thenReturn(firstRequest, otherRequests);
+  }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/PostgresCharsetHandlerTest.java b/sonar-db/src/test/java/org/sonar/db/charset/PostgresCharsetHandlerTest.java
new file mode 100644 (file)
index 0000000..57ba731
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.Arrays;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.MessageException;
+
+import static java.util.Arrays.asList;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class PostgresCharsetHandlerTest {
+
+  private static final String TABLE_ISSUES = "issues";
+  private static final String TABLE_PROJECTS = "projects";
+  private static final String COLUMN_KEE = "kee";
+  private static final String COLUMN_NAME = "name";
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  CharsetHandler.SelectExecutor selectExecutor = mock(CharsetHandler.SelectExecutor.class);
+  PostgresCharsetHandler underTest = new PostgresCharsetHandler(selectExecutor);
+
+  @Test
+  public void checks_that_column_is_utf8() throws Exception {
+    answerSql(asList(
+      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
+      new String[] {TABLE_PROJECTS, COLUMN_NAME, "utf8"}));
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void checks_that_db_is_utf8_if_column_collation_is_not_defined() throws Exception {
+    answerSql(
+      // first request to get columns
+      asList(
+        new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
+        new String[] {TABLE_PROJECTS, COLUMN_NAME, "" /* unset -> uses db collation */}),
+
+      // second request to get db collation
+      Arrays.<String[]>asList(new String[] {"utf8"}));
+
+    // no error
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void fails_if_non_utf8_column() throws Exception {
+    answerSql(asList(
+      new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
+      new String[] {TABLE_PROJECTS, COLUMN_KEE, "latin"},
+      new String[] {TABLE_PROJECTS, COLUMN_NAME, "latin"}));
+
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage("Database columns [projects.kee, projects.name] must support UTF8 collation.");
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void fails_if_non_utf8_db() throws Exception {
+    answerSql(
+      // first request to get columns
+      asList(
+        new String[] {TABLE_ISSUES, COLUMN_KEE, "utf8"},
+        new String[] {TABLE_PROJECTS, COLUMN_NAME, "" /* unset -> uses db collation */}),
+
+      // second request to get db collation
+      Arrays.<String[]>asList(new String[] {"latin"}));
+
+    expectedException.expect(MessageException.class);
+    expectedException.expectMessage("Database collation is latin. It must support UTF8.");
+
+    underTest.handle(mock(Connection.class), true);
+  }
+
+  @Test
+  public void does_nothing_if_utf8_must_not_verified() throws Exception {
+    underTest.handle(mock(Connection.class), false);
+  }
+
+  private void answerSql(List<String[]> firstRequest, List<String[]>... otherRequests) throws SQLException {
+    when(selectExecutor.executeQuery(any(Connection.class), anyString(), anyInt())).thenReturn(firstRequest, otherRequests);
+  }
+}
diff --git a/sonar-db/src/test/java/org/sonar/db/charset/SelectExecutorTest.java b/sonar-db/src/test/java/org/sonar/db/charset/SelectExecutorTest.java
new file mode 100644 (file)
index 0000000..bfa0a48
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.db.charset;
+
+import java.sql.Connection;
+import java.util.List;
+import org.junit.Rule;
+import org.junit.Test;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.user.UserDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class SelectExecutorTest {
+
+  @Rule
+  public DbTester dbTester = DbTester.create(System2.INSTANCE);
+
+  CharsetHandler.SelectExecutor underTest = new CharsetHandler.SelectExecutor();
+
+  @Test
+  public void testExecuteQuery() throws Exception {
+    DbSession session = dbTester.getSession();
+    dbTester.getDbClient().userDao().insert(session, new UserDto().setLogin("him").setName("Him"));
+    dbTester.getDbClient().userDao().insert(session, new UserDto().setLogin("her").setName("Her"));
+    session.commit();
+
+    try (Connection connection = dbTester.openConnection()) {
+      List<String[]> rows = underTest.executeQuery(connection, "select login, name from users order by login", 2);
+      assertThat(rows).hasSize(2);
+      assertThat(rows.get(0)[0]).isEqualTo("her");
+      assertThat(rows.get(0)[1]).isEqualTo("Her");
+      assertThat(rows.get(1)[0]).isEqualTo("him");
+      assertThat(rows.get(1)[1]).isEqualTo("Him");
+    }
+  }
+}