--- /dev/null
+/*
+ * 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();
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+ }
+}
@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;
return;
}
- running.getAndSet(true);
+ running.set(true);
executorService.execute(new Runnable() {
@Override
public void run() {
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;
add(
DefaultServerUpgradeStatus.class,
DatabaseMigrator.class,
+ DatabaseCharsetChecker.class,
+ VerifyDatabaseCharsetAtStartup.class,
// depends on Ruby
PlatformRubyBridge.class,
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;
@Override
protected void configureLevel() {
add(
- CollationChecker.class,
+ VerifyDatabaseCharsetAfterMigration.class,
PersistentSettings.class,
ServerMetadataPersister.class,
DefaultHttpDownloader.class,
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) {
* 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 {
/**
* 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();
}
}
};
- 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() {
/**
* 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() {
+++ /dev/null
-/*
- * 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);
- }
- }
- }
-
-}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+
+}
--- /dev/null
+/*
+ * 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)));
+ }
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+ }
+ }
+}
--- /dev/null
+/*
+ * 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));
+ }
+ }
+}
--- /dev/null
+/*
+ * 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)));
+ }
+ }
+}
--- /dev/null
+/*
+ * 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;
+
+++ /dev/null
-/*
- * 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);
- }
-}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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");
+
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+/*
+ * 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");
+ }
+ }
+}