]> source.dussan.org Git - sonarqube.git/commitdiff
LICENSE-96 implement support for staging and new server id format
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 20 Jun 2018 15:15:44 +0000 (17:15 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 5 Jul 2018 18:21:54 +0000 (20:21 +0200)
20 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java [deleted file]
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java
server/sonar-server/src/main/java/org/sonar/server/platform/serverid/JdbcUrlSanitizer.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdChecksum.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/serverid/package-info.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java [deleted file]
server/sonar-server/src/test/java/org/sonar/server/platform/serverid/JdbcUrlSanitizerTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdChecksumTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java [new file with mode: 0644]
sonar-core/src/main/java/org/sonar/core/platform/ServerId.java [new file with mode: 0644]
sonar-core/src/test/java/org/sonar/core/platform/ServerIdTest.java [new file with mode: 0644]

index 0d4bb52919524324772c8938ef5fc2d1198529e6..2b1519862ba1cd3ea9cb5af8cba7a380cbc5ede2 100644 (file)
@@ -132,7 +132,6 @@ import org.sonar.server.permission.ws.template.DefaultTemplatesResolverImpl;
 import org.sonar.server.platform.DatabaseServerCompatibility;
 import org.sonar.server.platform.DefaultServerUpgradeStatus;
 import org.sonar.server.platform.ServerFileSystemImpl;
-import org.sonar.server.platform.ServerIdManager;
 import org.sonar.server.platform.ServerImpl;
 import org.sonar.server.platform.ServerLifecycleNotifier;
 import org.sonar.server.log.ServerLogging;
@@ -145,6 +144,7 @@ import org.sonar.server.platform.db.migration.version.DatabaseVersion;
 import org.sonar.server.platform.monitoring.DbSection;
 import org.sonar.server.platform.monitoring.OfficialDistribution;
 import org.sonar.server.platform.monitoring.cluster.ProcessInfoProvider;
+import org.sonar.server.platform.serverid.ServerIdModule;
 import org.sonar.server.plugins.InstalledPluginReferentialFactory;
 import org.sonar.server.plugins.ServerExtensionInstaller;
 import org.sonar.server.property.InternalPropertiesImpl;
@@ -360,7 +360,7 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
   private static void populateLevel3(ComponentContainer container) {
     container.add(
       new StartupMetadataProvider(),
-      ServerIdManager.class,
+      ServerIdModule.class,
       UriReader.class,
       ServerImpl.class,
       DefaultOrganizationProviderImpl.class,
index d86e30af7fcac2aab4c8bbe6e50cfd0045051c18..ddeaafc1cddf1631c34568f323da26892bbdac2b 100644 (file)
@@ -22,8 +22,11 @@ package org.sonar.ce.container;
 import java.io.File;
 import java.io.IOException;
 import java.util.Date;
+import java.util.Locale;
 import java.util.Properties;
 import java.util.stream.Collectors;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.lang.StringUtils;
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
@@ -40,7 +43,6 @@ import org.sonar.db.property.PropertyDto;
 import org.sonar.process.ProcessId;
 import org.sonar.process.ProcessProperties;
 import org.sonar.process.Props;
-import org.sonar.server.platform.ServerIdChecksum;
 import org.sonar.server.property.InternalProperties;
 
 import static java.lang.String.valueOf;
@@ -84,7 +86,7 @@ public class ComputeEngineContainerImplTest {
     // required persisted properties
     insertProperty(CoreProperties.SERVER_ID, "a_server_id");
     insertProperty(CoreProperties.SERVER_STARTTIME, DateUtils.formatDateTime(new Date()));
-    insertInternalProperty(InternalProperties.SERVER_ID_CHECKSUM, ServerIdChecksum.of("a_server_id", db.getUrl()));
+    insertInternalProperty(InternalProperties.SERVER_ID_CHECKSUM, DigestUtils.sha256Hex("a_server_id|" + cleanJdbcUrl()));
 
     underTest
       .start(new Props(properties));
@@ -110,6 +112,7 @@ public class ComputeEngineContainerImplTest {
       assertThat(picoContainer.getParent().getComponentAdapters()).hasSize(
         CONTAINER_ITSELF
           + 7 // level 3
+          + 4 // content of ServerIdModule
       );
       assertThat(picoContainer.getParent().getParent().getComponentAdapters()).hasSize(
         CONTAINER_ITSELF
@@ -140,6 +143,10 @@ public class ComputeEngineContainerImplTest {
     assertThat(picoContainer.getLifecycleState().isDisposed()).isTrue();
   }
 
+  private String cleanJdbcUrl() {
+    return StringUtils.lowerCase(StringUtils.substringBefore(db.getUrl(), "?"), Locale.ENGLISH);
+  }
+
   private Properties getProperties() throws IOException {
     Properties properties = ProcessProperties.defaults();
     File homeDir = tempFolder.newFolder();
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java
deleted file mode 100644 (file)
index 1e4e7e7..0000000
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.platform;
-
-import com.google.common.annotations.VisibleForTesting;
-import java.util.Arrays;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Objects;
-import java.util.Optional;
-import org.apache.commons.codec.digest.DigestUtils;
-import org.apache.commons.lang.StringUtils;
-import org.sonar.api.utils.KeyValueFormat;
-
-public class ServerIdChecksum {
-
-  private static final String SQLSERVER_PREFIX = "jdbc:sqlserver://";
-
-  private ServerIdChecksum() {
-    // only static methods
-  }
-
-  public static String of(String serverId, String jdbcUrl) {
-    return DigestUtils.sha256Hex(serverId + "|" + sanitizeJdbcUrl(jdbcUrl));
-  }
-
-  @VisibleForTesting
-  static String sanitizeJdbcUrl(String jdbcUrl) {
-    String result;
-    if (jdbcUrl.startsWith(SQLSERVER_PREFIX)) {
-      result = sanitizeSqlServerUrl(jdbcUrl);
-    } else {
-      // remove query parameters, they don't aim to reference the schema
-      result = StringUtils.substringBefore(jdbcUrl, "?");
-    }
-    return StringUtils.lowerCase(result, Locale.ENGLISH);
-  }
-
-  /**
-   * Deal with this strange URL format:
-   * https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url
-   * https://docs.microsoft.com/en-us/sql/connect/jdbc/setting-the-connection-properties
-   */
-  private static String sanitizeSqlServerUrl(String jdbcUrl) {
-    StringBuilder result = new StringBuilder();
-    result.append(SQLSERVER_PREFIX);
-
-    String host;
-    if (jdbcUrl.contains(";")) {
-      host = StringUtils.substringBetween(jdbcUrl, SQLSERVER_PREFIX, ";");
-    } else {
-      host = StringUtils.substringAfter(jdbcUrl, SQLSERVER_PREFIX);
-    }
-
-    String queryString = StringUtils.substringAfter(jdbcUrl, ";");
-    Map<String, String> parameters = KeyValueFormat.parse(queryString);
-    Optional<String> server = firstValue(parameters, "serverName", "servername", "server");
-    if (server.isPresent()) {
-      result.append(server.get());
-    } else {
-      result.append(StringUtils.substringBefore(host, ":"));
-    }
-
-    Optional<String> port = firstValue(parameters, "portNumber", "port");
-    if (port.isPresent()) {
-      result.append(':').append(port.get());
-    } else if (host.contains(":")) {
-      result.append(':').append(StringUtils.substringAfter(host, ":"));
-    }
-
-    Optional<String> database = firstValue(parameters, "databaseName", "database");
-    database.ifPresent(s -> result.append('/').append(s));
-    return result.toString();
-  }
-
-  private static Optional<String> firstValue(Map<String, String> map, String... keys) {
-    return Arrays.stream(keys)
-      .map(map::get)
-      .filter(Objects::nonNull)
-      .findFirst();
-  }
-}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java b/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java
deleted file mode 100644 (file)
index 11bd09b..0000000
+++ /dev/null
@@ -1,154 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.platform;
-
-import java.text.ParseException;
-import java.text.SimpleDateFormat;
-import java.util.Optional;
-import org.picocontainer.Startable;
-import org.sonar.api.CoreProperties;
-import org.sonar.api.SonarQubeSide;
-import org.sonar.api.SonarRuntime;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.utils.log.Logger;
-import org.sonar.api.utils.log.Loggers;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.property.PropertyDto;
-import org.sonar.server.property.InternalProperties;
-
-import static com.google.common.base.Preconditions.checkState;
-import static org.apache.commons.lang.StringUtils.isEmpty;
-import static org.apache.commons.lang.StringUtils.isNotEmpty;
-import static org.sonar.api.CoreProperties.SERVER_ID;
-import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
-import static org.sonar.server.property.InternalProperties.SERVER_ID_CHECKSUM;
-
-public class ServerIdManager implements Startable {
-  private static final Logger LOGGER = Loggers.get(ServerIdManager.class);
-
-  private final Configuration config;
-  private final DbClient dbClient;
-  private final SonarRuntime runtime;
-  private final WebServer webServer;
-  private final UuidFactory uuidFactory;
-
-  public ServerIdManager(Configuration config, DbClient dbClient, SonarRuntime runtime, WebServer webServer, UuidFactory uuidFactory) {
-    this.config = config;
-    this.dbClient = dbClient;
-    this.runtime = runtime;
-    this.webServer = webServer;
-    this.uuidFactory = uuidFactory;
-  }
-
-  @Override
-  public void start() {
-    try (DbSession dbSession = dbClient.openSession(false)) {
-      if (runtime.getSonarQubeSide() == SonarQubeSide.SERVER && webServer.isStartupLeader()) {
-        if (needsToBeDropped(dbSession)) {
-          dbClient.propertiesDao().deleteGlobalProperty(SERVER_ID, dbSession);
-        }
-        persistServerIdIfMissing(dbSession);
-        dbSession.commit();
-      } else {
-        ensureServerIdIsValid(dbSession);
-      }
-    }
-  }
-
-  private boolean needsToBeDropped(DbSession dbSession) {
-    PropertyDto dto = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID);
-    if (dto == null) {
-      // does not exist, no need to drop
-      return false;
-    }
-
-    if (isEmpty(dto.getValue())) {
-      return true;
-    }
-
-    if (isDate(dto.getValue())) {
-      LOGGER.info("Server ID is changed to new format.");
-      return true;
-    }
-
-    Optional<String> checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM);
-    if (checksum.isPresent()) {
-      String expectedChecksum = computeChecksum(dto.getValue());
-      if (!expectedChecksum.equals(checksum.get())) {
-        LOGGER.warn("Server ID is reset because it is not valid anymore. Database URL probably changed. The new server ID affects SonarSource licensed products.");
-        return true;
-      }
-    }
-
-    // Existing server ID must be kept when upgrading to 6.7+. In that case the checksum does
-    // not exist.
-
-    return false;
-  }
-
-  private void persistServerIdIfMissing(DbSession dbSession) {
-    String serverId;
-    PropertyDto idDto = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID);
-    if (idDto == null) {
-      serverId = uuidFactory.create();
-      dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(SERVER_ID).setValue(serverId));
-    } else {
-      serverId = idDto.getValue();
-    }
-
-    // checksum must be generated when it does not exist (upgrading to 6.7 or greater)
-    // or when server ID changed.
-    dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, computeChecksum(serverId));
-  }
-
-  /**
-   * Checks whether the specified value is a date according to the old format of the {@link CoreProperties#SERVER_ID}.
-   */
-  private static boolean isDate(String value) {
-    try {
-      new SimpleDateFormat("yyyyMMddHHmmss").parse(value);
-      return true;
-    } catch (ParseException e) {
-      return false;
-    }
-  }
-
-  private String computeChecksum(String serverId) {
-    String jdbcUrl = config.get(JDBC_URL.getKey()).orElseThrow(() -> new IllegalStateException("Missing JDBC URL"));
-    return ServerIdChecksum.of(serverId, jdbcUrl);
-  }
-
-  private void ensureServerIdIsValid(DbSession dbSession) {
-    PropertyDto id = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID);
-    checkState(id != null, "Property %s is missing in database", SERVER_ID);
-    checkState(isNotEmpty(id.getValue()), "Property %s is empty in database", SERVER_ID);
-
-    Optional<String> checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM);
-    checkState(checksum.isPresent(), "Internal property %s is missing in database", SERVER_ID_CHECKSUM);
-    checkState(checksum.get().equals(computeChecksum(id.getValue())), "Server ID is invalid");
-  }
-
-  @Override
-  public void stop() {
-    // nothing to do
-  }
-}
index 27e8550d0a3e442fa3e0f75a8982c11c78f2e7ea..30376168e21cd150b5274002d0e84c961f91e2b9 100644 (file)
@@ -26,11 +26,11 @@ import org.sonar.core.util.DefaultHttpDownloader;
 import org.sonar.server.async.AsyncExecutionModule;
 import org.sonar.server.organization.DefaultOrganizationProviderImpl;
 import org.sonar.server.organization.OrganizationFlagsImpl;
-import org.sonar.server.platform.ServerIdManager;
 import org.sonar.server.platform.ServerImpl;
 import org.sonar.server.platform.StartupMetadataPersister;
 import org.sonar.server.platform.WebCoreExtensionsInstaller;
 import org.sonar.server.platform.db.migration.NoopDatabaseMigrationImpl;
+import org.sonar.server.platform.serverid.ServerIdModule;
 import org.sonar.server.setting.DatabaseSettingLoader;
 import org.sonar.server.setting.DatabaseSettingsEnabler;
 
@@ -47,7 +47,7 @@ public class PlatformLevel3 extends PlatformLevel {
     addIfStartupLeader(StartupMetadataPersister.class);
     add(
       NoopDatabaseMigrationImpl.class,
-      ServerIdManager.class,
+      ServerIdModule.class,
       ServerImpl.class,
       DatabaseSettingLoader.class,
       DatabaseSettingsEnabler.class,
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/JdbcUrlSanitizer.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/JdbcUrlSanitizer.java
new file mode 100644 (file)
index 0000000..05c3ba8
--- /dev/null
@@ -0,0 +1,87 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.utils.KeyValueFormat;
+
+public class JdbcUrlSanitizer {
+  private static final String SQLSERVER_PREFIX = "jdbc:sqlserver://";
+
+  public String sanitize(String jdbcUrl) {
+    String result;
+    if (jdbcUrl.startsWith(SQLSERVER_PREFIX)) {
+      result = sanitizeSqlServerUrl(jdbcUrl);
+    } else {
+      // remove query parameters, they don't aim to reference the schema
+      result = StringUtils.substringBefore(jdbcUrl, "?");
+    }
+    return StringUtils.lowerCase(result, Locale.ENGLISH);
+  }
+
+  /**
+   * Deal with this strange URL format:
+   * https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url
+   * https://docs.microsoft.com/en-us/sql/connect/jdbc/setting-the-connection-properties
+   */
+  private static String sanitizeSqlServerUrl(String jdbcUrl) {
+    StringBuilder result = new StringBuilder();
+    result.append(SQLSERVER_PREFIX);
+
+    String host;
+    if (jdbcUrl.contains(";")) {
+      host = StringUtils.substringBetween(jdbcUrl, SQLSERVER_PREFIX, ";");
+    } else {
+      host = StringUtils.substringAfter(jdbcUrl, SQLSERVER_PREFIX);
+    }
+
+    String queryString = StringUtils.substringAfter(jdbcUrl, ";");
+    Map<String, String> parameters = KeyValueFormat.parse(queryString);
+    Optional<String> server = firstValue(parameters, "serverName", "servername", "server");
+    if (server.isPresent()) {
+      result.append(server.get());
+    } else {
+      result.append(StringUtils.substringBefore(host, ":"));
+    }
+
+    Optional<String> port = firstValue(parameters, "portNumber", "port");
+    if (port.isPresent()) {
+      result.append(':').append(port.get());
+    } else if (host.contains(":")) {
+      result.append(':').append(StringUtils.substringAfter(host, ":"));
+    }
+
+    Optional<String> database = firstValue(parameters, "databaseName", "database");
+    database.ifPresent(s -> result.append('/').append(s));
+    return result.toString();
+  }
+
+  private static Optional<String> firstValue(Map<String, String> map, String... keys) {
+    return Arrays.stream(keys)
+      .map(map::get)
+      .filter(Objects::nonNull)
+      .findFirst();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdChecksum.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdChecksum.java
new file mode 100644 (file)
index 0000000..57cd9e3
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import org.apache.commons.codec.digest.DigestUtils;
+import org.sonar.api.config.Configuration;
+
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+
+public class ServerIdChecksum {
+
+  private final Configuration config;
+  private final JdbcUrlSanitizer jdbcUrlSanitizer;
+
+  public ServerIdChecksum(Configuration config, JdbcUrlSanitizer jdbcUrlSanitizer) {
+    this.config = config;
+    this.jdbcUrlSanitizer = jdbcUrlSanitizer;
+  }
+
+  public String computeFor(String serverId) {
+    String jdbcUrl = config.get(JDBC_URL.getKey()).orElseThrow(() -> new IllegalStateException("Missing JDBC URL"));
+    return DigestUtils.sha256Hex(serverId + "|" + jdbcUrlSanitizer.sanitize(jdbcUrl));
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java
new file mode 100644 (file)
index 0000000..ab04418
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import org.sonar.core.platform.ServerId;
+
+public interface ServerIdFactory {
+  /**
+   * Create a new ServerId from scratch.
+   */
+  ServerId create();
+
+  /**
+   * Create a new ServerId from the current serverId.
+   */
+  ServerId create(ServerId currentServerId);
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java
new file mode 100644 (file)
index 0000000..a0898d9
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import com.google.common.annotations.VisibleForTesting;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.zip.CRC32;
+import org.apache.commons.lang.StringUtils;
+import org.sonar.api.config.Configuration;
+import org.sonar.core.platform.ServerId;
+import org.sonar.core.util.UuidFactory;
+
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+
+public class ServerIdFactoryImpl implements ServerIdFactory {
+
+  private final Configuration config;
+  private final UuidFactory uuidFactory;
+  private final JdbcUrlSanitizer jdbcUrlSanitizer;
+
+  public ServerIdFactoryImpl(Configuration config, UuidFactory uuidFactory, JdbcUrlSanitizer jdbcUrlSanitizer) {
+    this.config = config;
+    this.uuidFactory = uuidFactory;
+    this.jdbcUrlSanitizer = jdbcUrlSanitizer;
+  }
+
+  @Override
+  public ServerId create() {
+    return ServerId.of(computeDatabaseId(), uuidFactory.create());
+  }
+
+  @Override
+  public ServerId create(ServerId currentServerId) {
+    return ServerId.of(computeDatabaseId(), currentServerId.getDatasetId());
+  }
+
+  private String computeDatabaseId() {
+    String jdbcUrl = config.get(JDBC_URL.getKey()).orElseThrow(() -> new IllegalStateException("Missing JDBC URL"));
+    return crc32Hex(jdbcUrlSanitizer.sanitize(jdbcUrl));
+  }
+
+  @VisibleForTesting
+  static String crc32Hex(String str) {
+    CRC32 crc32 = new CRC32();
+    crc32.update(str.getBytes(StandardCharsets.UTF_8));
+    long hash = crc32.getValue();
+    String s = Long.toHexString(hash).toUpperCase(Locale.ENGLISH);
+    return StringUtils.leftPad(s, ServerId.DATABASE_ID_LENGTH, "0");
+  }
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java
new file mode 100644 (file)
index 0000000..7d2d3bb
--- /dev/null
@@ -0,0 +1,163 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import java.util.Optional;
+import org.picocontainer.Startable;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.SonarRuntime;
+import org.sonar.api.utils.log.Logger;
+import org.sonar.api.utils.log.Loggers;
+import org.sonar.core.platform.ServerId;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.platform.WebServer;
+import org.sonar.server.property.InternalProperties;
+
+import static com.google.common.base.Preconditions.checkState;
+import static org.apache.commons.lang.StringUtils.isEmpty;
+import static org.apache.commons.lang.StringUtils.isNotEmpty;
+import static org.sonar.api.CoreProperties.SERVER_ID;
+import static org.sonar.core.platform.ServerId.Format.DEPRECATED;
+import static org.sonar.core.platform.ServerId.Format.NO_DATABASE_ID;
+import static org.sonar.server.property.InternalProperties.SERVER_ID_CHECKSUM;
+
+public class ServerIdManager implements Startable {
+  private static final Logger LOGGER = Loggers.get(ServerIdManager.class);
+
+  private final ServerIdChecksum serverIdChecksum;
+  private final ServerIdFactory serverIdFactory;
+  private final DbClient dbClient;
+  private final SonarRuntime runtime;
+  private final WebServer webServer;
+
+  public ServerIdManager(ServerIdChecksum serverIdChecksum, ServerIdFactory serverIdFactory, DbClient dbClient, SonarRuntime runtime, WebServer webServer) {
+    this.serverIdChecksum = serverIdChecksum;
+    this.serverIdFactory = serverIdFactory;
+    this.dbClient = dbClient;
+    this.runtime = runtime;
+    this.webServer = webServer;
+  }
+
+  @Override
+  public void start() {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      if (runtime.getSonarQubeSide() == SonarQubeSide.SERVER && webServer.isStartupLeader()) {
+        Optional<String> checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM);
+
+        ServerId serverId = readCurrentServerId(dbSession)
+          .map(currentServerId -> keepOrReplaceCurrentServerId(dbSession, currentServerId, checksum))
+          .orElseGet(() -> createFirstServerId(dbSession));
+        updateChecksum(dbSession, serverId);
+
+        dbSession.commit();
+      } else {
+        ensureServerIdIsValid(dbSession);
+      }
+    }
+  }
+
+  private ServerId keepOrReplaceCurrentServerId(DbSession dbSession, ServerId currentServerId, Optional<String> checksum) {
+    if (keepServerId(currentServerId, checksum)) {
+      return currentServerId;
+    }
+
+    ServerId serverId = replaceCurrentServerId(currentServerId);
+    persistServerId(dbSession, serverId);
+    return serverId;
+  }
+
+  private boolean keepServerId(ServerId serverId, Optional<String> checksum) {
+    ServerId.Format format = serverId.getFormat();
+    if (format == DEPRECATED || format == NO_DATABASE_ID) {
+      LOGGER.info("Server ID is changed to new format.");
+      return false;
+    }
+
+    if (checksum.isPresent()) {
+      String expectedChecksum = serverIdChecksum.computeFor(serverId.toString());
+      if (!expectedChecksum.equals(checksum.get())) {
+        LOGGER.warn("Server ID is reset because it is not valid anymore. Database URL probably changed. The new server ID affects SonarSource licensed products.");
+        return false;
+      }
+    }
+
+    // Existing server ID must be kept when upgrading to 6.7+. In that case the checksum does not exist.
+    return true;
+  }
+
+  private ServerId replaceCurrentServerId(ServerId currentServerId) {
+    if (currentServerId.getFormat() == DEPRECATED) {
+      return serverIdFactory.create();
+    }
+    return serverIdFactory.create(currentServerId);
+  }
+
+  private ServerId createFirstServerId(DbSession dbSession) {
+    ServerId serverId = serverIdFactory.create();
+    persistServerId(dbSession, serverId);
+    return serverId;
+  }
+
+  private Optional<ServerId> readCurrentServerId(DbSession dbSession) {
+    PropertyDto dto = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID);
+    if (dto == null) {
+      return Optional.empty();
+    }
+
+    String value = dto.getValue();
+    if (isEmpty(value)) {
+      return Optional.empty();
+    }
+
+    return Optional.of(ServerId.parse(value));
+  }
+
+  private void updateChecksum(DbSession dbSession, ServerId serverId) {
+    // checksum must be generated when it does not exist (upgrading to 6.7 or greater)
+    // or when server ID changed.
+    String checksum = serverIdChecksum.computeFor(serverId.toString());
+    persistChecksum(dbSession, checksum);
+  }
+
+  private void persistServerId(DbSession dbSession, ServerId serverId) {
+    dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(SERVER_ID).setValue(serverId.toString()));
+  }
+
+  private void persistChecksum(DbSession dbSession, String checksump) {
+    dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, checksump);
+  }
+
+  private void ensureServerIdIsValid(DbSession dbSession) {
+    PropertyDto id = dbClient.propertiesDao().selectGlobalProperty(dbSession, SERVER_ID);
+    checkState(id != null, "Property %s is missing in database", SERVER_ID);
+    checkState(isNotEmpty(id.getValue()), "Property %s is empty in database", SERVER_ID);
+
+    Optional<String> checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM);
+    checkState(checksum.isPresent(), "Internal property %s is missing in database", SERVER_ID_CHECKSUM);
+    checkState(checksum.get().equals(serverIdChecksum.computeFor(id.getValue())), "Server ID is invalid");
+  }
+
+  @Override
+  public void stop() {
+    // nothing to do
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java
new file mode 100644 (file)
index 0000000..3ef2117
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import org.sonar.core.platform.Module;
+
+public class ServerIdModule extends Module {
+  @Override
+  protected void configureModule() {
+    add(
+      ServerIdFactoryImpl.class,
+      JdbcUrlSanitizer.class,
+      ServerIdChecksum.class,
+      ServerIdManager.class
+
+    );
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/package-info.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/package-info.java
new file mode 100644 (file)
index 0000000..767e13e
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.server.platform.serverid;
+
+import javax.annotation.ParametersAreNonnullByDefault;
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java
deleted file mode 100644 (file)
index 7bfae03..0000000
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.platform;
-
-import org.junit.Test;
-
-import static org.assertj.core.api.Assertions.assertThat;
-
-public class ServerIdChecksumTest {
-
-  @Test
-  public void test_checksum() {
-    assertThat(ServerIdChecksum.of("id1", "url1"))
-      .isNotEmpty()
-      .isEqualTo(ServerIdChecksum.of("id1", "url1"))
-      .isNotEqualTo(ServerIdChecksum.of("id1", "url2"))
-      .isNotEqualTo(ServerIdChecksum.of("id2", "url1"))
-      .isNotEqualTo(ServerIdChecksum.of("id2", "url2"));
-  }
-
-  @Test
-  public void sanitize_h2_url() {
-    verifyJdbcUrl("jdbc:h2:tcp://dbserv:8084/~/sample", "jdbc:h2:tcp://dbserv:8084/~/sample");
-    verifyJdbcUrl("jdbc:h2:tcp://localhost/mem:test", "jdbc:h2:tcp://localhost/mem:test");
-    verifyJdbcUrl("jdbc:h2:tcp://localhost/mem:TEST", "jdbc:h2:tcp://localhost/mem:test");
-  }
-
-  @Test
-  public void sanitize_mysql_url() {
-    verifyJdbcUrl("jdbc:mysql://127.0.0.1:3306/sonarqube?useUnicode=true&characterEncoding=utf8", "jdbc:mysql://127.0.0.1:3306/sonarqube");
-    verifyJdbcUrl("jdbc:mysql://127.0.0.1:3306/sonarqube", "jdbc:mysql://127.0.0.1:3306/sonarqube");
-    verifyJdbcUrl("jdbc:mysql://127.0.0.1:3306/SONARQUBE", "jdbc:mysql://127.0.0.1:3306/sonarqube");
-  }
-
-  @Test
-  public void sanitize_oracle_url() {
-    verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@localhost:1521/XE", "sonar.jdbc.url=jdbc:oracle:thin:@localhost:1521/xe");
-    verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE", "sonar.jdbc.url=jdbc:oracle:thin:@localhost/xe");
-    verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE?foo", "sonar.jdbc.url=jdbc:oracle:thin:@localhost/xe");
-    verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@LOCALHOST/XE?foo", "sonar.jdbc.url=jdbc:oracle:thin:@localhost/xe");
-  }
-
-  @Test
-  public void sanitize_sqlserver_url() {
-    // see examples listed at https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url
-    verifyJdbcUrl("jdbc:sqlserver://localhost;user=MyUserName;password=*****;", "jdbc:sqlserver://localhost");
-    verifyJdbcUrl("jdbc:sqlserver://;servername=server_name;integratedSecurity=true;authenticationScheme=JavaKerberos", "jdbc:sqlserver://server_name");
-    verifyJdbcUrl("jdbc:sqlserver://localhost;integratedSecurity=true;", "jdbc:sqlserver://localhost");
-    verifyJdbcUrl("jdbc:sqlserver://localhost;databaseName=AdventureWorks;integratedSecurity=true;", "jdbc:sqlserver://localhost/adventureworks");
-    verifyJdbcUrl("jdbc:sqlserver://localhost:1433;databaseName=AdventureWorks;integratedSecurity=true;", "jdbc:sqlserver://localhost:1433/adventureworks");
-    verifyJdbcUrl("jdbc:sqlserver://localhost;databaseName=AdventureWorks;integratedSecurity=true;applicationName=MyApp;", "jdbc:sqlserver://localhost/adventureworks");
-    verifyJdbcUrl("jdbc:sqlserver://localhost;instanceName=instance1;integratedSecurity=true;", "jdbc:sqlserver://localhost");
-    verifyJdbcUrl("jdbc:sqlserver://;serverName=3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\\\instance1;integratedSecurity=true;", "jdbc:sqlserver://3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\\\instance1");
-
-    // test parameter aliases
-    verifyJdbcUrl("jdbc:sqlserver://;server=server_name", "jdbc:sqlserver://server_name");
-    verifyJdbcUrl("jdbc:sqlserver://;server=server_name;portNumber=1234", "jdbc:sqlserver://server_name:1234");
-    verifyJdbcUrl("jdbc:sqlserver://;server=server_name;port=1234", "jdbc:sqlserver://server_name:1234");
-
-    // case-insensitive
-    verifyJdbcUrl("jdbc:sqlserver://LOCALHOST;user=MyUserName;password=*****;", "jdbc:sqlserver://localhost");
-
-  }
-
-  @Test
-  public void sanitize_postgres_url() {
-    verifyJdbcUrl("jdbc:postgresql://localhost/sonar", "jdbc:postgresql://localhost/sonar");
-    verifyJdbcUrl("jdbc:postgresql://localhost:1234/sonar", "jdbc:postgresql://localhost:1234/sonar");
-    verifyJdbcUrl("jdbc:postgresql://localhost:1234/sonar?foo", "jdbc:postgresql://localhost:1234/sonar");
-
-    // case-insensitive
-    verifyJdbcUrl("jdbc:postgresql://localhost:1234/SONAR?foo", "jdbc:postgresql://localhost:1234/sonar");
-  }
-
-  private static void verifyJdbcUrl(String url, String expectedResult) {
-    assertThat(ServerIdChecksum.sanitizeJdbcUrl(url)).isEqualTo(expectedResult);
-  }
-}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java
deleted file mode 100644 (file)
index 412d801..0000000
+++ /dev/null
@@ -1,258 +0,0 @@
-/*
- * SonarQube
- * Copyright (C) 2009-2018 SonarSource SA
- * mailto:info 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.platform;
-
-import org.junit.After;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.sonar.api.CoreProperties;
-import org.sonar.api.SonarQubeSide;
-import org.sonar.api.config.Configuration;
-import org.sonar.api.config.internal.MapSettings;
-import org.sonar.api.internal.SonarRuntimeImpl;
-import org.sonar.api.utils.System2;
-import org.sonar.api.utils.Version;
-import org.sonar.core.util.UuidFactory;
-import org.sonar.db.DbClient;
-import org.sonar.db.DbSession;
-import org.sonar.db.DbTester;
-import org.sonar.db.property.PropertyDto;
-import org.sonar.server.property.InternalProperties;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.when;
-import static org.sonar.api.SonarQubeSide.COMPUTE_ENGINE;
-import static org.sonar.api.SonarQubeSide.SERVER;
-
-public class ServerIdManagerTest {
-
-  private static final String A_SERVER_ID = "uuid";
-  private static final String A_JDBC_URL = "jdbc:postgres:foo";
-  private static final String A_VALID_CHECKSUM = ServerIdChecksum.of(A_SERVER_ID, A_JDBC_URL);
-
-  @Rule
-  public final DbTester dbTester = DbTester.create(System2.INSTANCE);
-  @Rule
-  public ExpectedException expectedException = ExpectedException.none();
-
-  private Configuration config = new MapSettings().setProperty("sonar.jdbc.url", A_JDBC_URL).asConfig();
-  private DbClient dbClient = dbTester.getDbClient();
-  private DbSession dbSession = dbTester.getSession();
-  private WebServer webServer = mock(WebServer.class);
-  private UuidFactory uuidFactory = mock(UuidFactory.class);
-  private ServerIdManager underTest;
-
-  @After
-  public void tearDown() {
-    if (underTest != null) {
-      underTest.stop();
-    }
-  }
-
-  @Test
-  public void web_leader_persists_new_server_id_if_missing() {
-    when(uuidFactory.create()).thenReturn(A_SERVER_ID);
-    when(webServer.isStartupLeader()).thenReturn(true);
-
-    test(SERVER);
-
-    verifyDb(A_SERVER_ID, A_VALID_CHECKSUM);
-  }
-
-  @Test
-  public void web_leader_persists_new_server_id_if_format_is_old_date() {
-    insertServerId("20161123150657");
-    when(uuidFactory.create()).thenReturn(A_SERVER_ID);
-    when(webServer.isStartupLeader()).thenReturn(true);
-
-    test(SERVER);
-
-    verifyDb(A_SERVER_ID, A_VALID_CHECKSUM);
-  }
-
-  @Test
-  public void web_leader_persists_new_server_id_if_value_is_empty() {
-    insertServerId("");
-    when(uuidFactory.create()).thenReturn(A_SERVER_ID);
-    when(webServer.isStartupLeader()).thenReturn(true);
-
-    test(SERVER);
-
-    verifyDb(A_SERVER_ID, A_VALID_CHECKSUM);
-  }
-
-  @Test
-  public void web_leader_keeps_existing_server_id_if_valid() {
-    insertServerId(A_SERVER_ID);
-    insertChecksum(A_VALID_CHECKSUM);
-    when(webServer.isStartupLeader()).thenReturn(true);
-
-    test(SERVER);
-
-    verifyDb(A_SERVER_ID, A_VALID_CHECKSUM);
-  }
-
-  @Test
-  public void web_leader_resets_server_id_if_invalid() {
-    insertServerId("foo");
-    insertChecksum("invalid");
-    when(uuidFactory.create()).thenReturn(A_SERVER_ID);
-    when(webServer.isStartupLeader()).thenReturn(true);
-
-    test(SERVER);
-
-    verifyDb(A_SERVER_ID, A_VALID_CHECKSUM);
-  }
-
-  @Test
-  public void web_leader_generates_missing_checksum() {
-    insertServerId(A_SERVER_ID);
-    when(webServer.isStartupLeader()).thenReturn(true);
-
-    test(SERVER);
-
-    verifyDb(A_SERVER_ID, A_VALID_CHECKSUM);
-  }
-
-  @Test
-  public void web_follower_does_not_fail_if_server_id_is_valid() {
-    insertServerId(A_SERVER_ID);
-    insertChecksum(A_VALID_CHECKSUM);
-    when(webServer.isStartupLeader()).thenReturn(false);
-
-    test(SERVER);
-
-    // no changes
-    verifyDb(A_SERVER_ID, A_VALID_CHECKSUM);
-  }
-
-  @Test
-  public void web_follower_fails_if_server_id_is_missing() {
-    when(webServer.isStartupLeader()).thenReturn(false);
-
-    expectMissingServerIdException();
-
-    test(SERVER);
-  }
-
-  @Test
-  public void web_follower_fails_if_server_id_is_empty() {
-    insertServerId("");
-    when(webServer.isStartupLeader()).thenReturn(false);
-
-    expectEmptyServerIdException();
-
-    test(SERVER);
-  }
-
-  @Test
-  public void web_follower_fails_if_server_id_is_invalid() {
-    insertServerId(A_SERVER_ID);
-    insertChecksum("boom");
-    when(webServer.isStartupLeader()).thenReturn(false);
-
-    expectInvalidServerIdException();
-
-    test(SERVER);
-
-    // no changes
-    verifyDb(A_SERVER_ID, "boom");
-  }
-
-  @Test
-  public void compute_engine_does_not_fail_if_server_id_is_valid() {
-    insertServerId(A_SERVER_ID);
-    insertChecksum(A_VALID_CHECKSUM);
-
-    test(COMPUTE_ENGINE);
-
-    // no changes
-    verifyDb(A_SERVER_ID, A_VALID_CHECKSUM);
-  }
-
-  @Test
-  public void compute_engine_fails_if_server_id_is_missing() {
-    expectMissingServerIdException();
-
-    test(COMPUTE_ENGINE);
-  }
-
-  @Test
-  public void compute_engine_fails_if_server_id_is_empty() {
-    insertServerId("");
-
-    expectEmptyServerIdException();
-
-    test(COMPUTE_ENGINE);
-  }
-
-  @Test
-  public void compute_engine_fails_if_server_id_is_invalid() {
-    insertServerId(A_SERVER_ID);
-    insertChecksum("boom");
-
-    expectInvalidServerIdException();
-
-    test(COMPUTE_ENGINE);
-
-    // no changes
-    verifyDb(A_SERVER_ID, "boom");
-  }
-
-  private void expectEmptyServerIdException() {
-    expectedException.expect(IllegalStateException.class);
-    expectedException.expectMessage("Property sonar.core.id is empty in database");
-  }
-
-  private void expectMissingServerIdException() {
-    expectedException.expect(IllegalStateException.class);
-    expectedException.expectMessage("Property sonar.core.id is missing in database");
-  }
-
-  private void expectInvalidServerIdException() {
-    expectedException.expect(IllegalStateException.class);
-    expectedException.expectMessage("Server ID is invalid");
-  }
-
-  private void verifyDb(String expectedServerId, String expectedChecksum) {
-    assertThat(dbClient.propertiesDao().selectGlobalProperty(dbSession, CoreProperties.SERVER_ID))
-      .extracting(PropertyDto::getValue)
-      .containsExactly(expectedServerId);
-    assertThat(dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.SERVER_ID_CHECKSUM))
-      .hasValue(expectedChecksum);
-  }
-
-  private void insertServerId(String value) {
-    dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(CoreProperties.SERVER_ID).setValue(value));
-    dbSession.commit();
-  }
-
-  private void insertChecksum(String value) {
-    dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, value);
-    dbSession.commit();
-  }
-
-  private void test(SonarQubeSide side) {
-    underTest = new ServerIdManager(config, dbClient, SonarRuntimeImpl.forSonarQube(Version.create(6, 7), side), webServer, uuidFactory);
-    underTest.start();
-  }
-}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/JdbcUrlSanitizerTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/JdbcUrlSanitizerTest.java
new file mode 100644 (file)
index 0000000..6c0f642
--- /dev/null
@@ -0,0 +1,88 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import org.junit.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class JdbcUrlSanitizerTest {
+  private JdbcUrlSanitizer underTest = new JdbcUrlSanitizer();
+
+  @Test
+  public void sanitize_h2_url() {
+    verifyJdbcUrl("jdbc:h2:tcp://dbserv:8084/~/sample", "jdbc:h2:tcp://dbserv:8084/~/sample");
+    verifyJdbcUrl("jdbc:h2:tcp://localhost/mem:test", "jdbc:h2:tcp://localhost/mem:test");
+    verifyJdbcUrl("jdbc:h2:tcp://localhost/mem:TEST", "jdbc:h2:tcp://localhost/mem:test");
+  }
+
+  @Test
+  public void sanitize_mysql_url() {
+    verifyJdbcUrl("jdbc:mysql://127.0.0.1:3306/sonarqube?useUnicode=true&characterEncoding=utf8", "jdbc:mysql://127.0.0.1:3306/sonarqube");
+    verifyJdbcUrl("jdbc:mysql://127.0.0.1:3306/sonarqube", "jdbc:mysql://127.0.0.1:3306/sonarqube");
+    verifyJdbcUrl("jdbc:mysql://127.0.0.1:3306/SONARQUBE", "jdbc:mysql://127.0.0.1:3306/sonarqube");
+  }
+
+  @Test
+  public void sanitize_oracle_url() {
+    verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@localhost:1521/XE", "sonar.jdbc.url=jdbc:oracle:thin:@localhost:1521/xe");
+    verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE", "sonar.jdbc.url=jdbc:oracle:thin:@localhost/xe");
+    verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@localhost/XE?foo", "sonar.jdbc.url=jdbc:oracle:thin:@localhost/xe");
+    verifyJdbcUrl("sonar.jdbc.url=jdbc:oracle:thin:@LOCALHOST/XE?foo", "sonar.jdbc.url=jdbc:oracle:thin:@localhost/xe");
+  }
+
+  @Test
+  public void sanitize_sqlserver_url() {
+    // see examples listed at https://docs.microsoft.com/en-us/sql/connect/jdbc/building-the-connection-url
+    verifyJdbcUrl("jdbc:sqlserver://localhost;user=MyUserName;password=*****;", "jdbc:sqlserver://localhost");
+    verifyJdbcUrl("jdbc:sqlserver://;servername=server_name;integratedSecurity=true;authenticationScheme=JavaKerberos", "jdbc:sqlserver://server_name");
+    verifyJdbcUrl("jdbc:sqlserver://localhost;integratedSecurity=true;", "jdbc:sqlserver://localhost");
+    verifyJdbcUrl("jdbc:sqlserver://localhost;databaseName=AdventureWorks;integratedSecurity=true;", "jdbc:sqlserver://localhost/adventureworks");
+    verifyJdbcUrl("jdbc:sqlserver://localhost:1433;databaseName=AdventureWorks;integratedSecurity=true;", "jdbc:sqlserver://localhost:1433/adventureworks");
+    verifyJdbcUrl("jdbc:sqlserver://localhost;databaseName=AdventureWorks;integratedSecurity=true;applicationName=MyApp;", "jdbc:sqlserver://localhost/adventureworks");
+    verifyJdbcUrl("jdbc:sqlserver://localhost;instanceName=instance1;integratedSecurity=true;", "jdbc:sqlserver://localhost");
+    verifyJdbcUrl("jdbc:sqlserver://;serverName=3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\\\instance1;integratedSecurity=true;", "jdbc:sqlserver://3ffe:8311:eeee:f70f:0:5eae:10.203.31.9\\\\instance1");
+
+    // test parameter aliases
+    verifyJdbcUrl("jdbc:sqlserver://;server=server_name", "jdbc:sqlserver://server_name");
+    verifyJdbcUrl("jdbc:sqlserver://;server=server_name;portNumber=1234", "jdbc:sqlserver://server_name:1234");
+    verifyJdbcUrl("jdbc:sqlserver://;server=server_name;port=1234", "jdbc:sqlserver://server_name:1234");
+
+    // case-insensitive
+    verifyJdbcUrl("jdbc:sqlserver://LOCALHOST;user=MyUserName;password=*****;", "jdbc:sqlserver://localhost");
+
+  }
+
+  @Test
+  public void sanitize_postgres_url() {
+    verifyJdbcUrl("jdbc:postgresql://localhost/sonar", "jdbc:postgresql://localhost/sonar");
+    verifyJdbcUrl("jdbc:postgresql://localhost:1234/sonar", "jdbc:postgresql://localhost:1234/sonar");
+    verifyJdbcUrl("jdbc:postgresql://localhost:1234/sonar?foo", "jdbc:postgresql://localhost:1234/sonar");
+
+    // case-insensitive
+    verifyJdbcUrl("jdbc:postgresql://localhost:1234/SONAR?foo", "jdbc:postgresql://localhost:1234/sonar");
+  }
+
+  private void verifyJdbcUrl(String url, String expectedResult) {
+    assertThat(underTest.sanitize(url)).isEqualTo(expectedResult);
+  }
+
+
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdChecksumTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdChecksumTest.java
new file mode 100644 (file)
index 0000000..b97cd8a
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.config.internal.MapSettings;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+
+public class ServerIdChecksumTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void compute_throws_ISE_if_jdbcUrl_property_is_not_set() {
+    ServerIdChecksum underTest = new ServerIdChecksum(new MapSettings().asConfig(), null /*doesn't matter*/);
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Missing JDBC URL");
+
+    underTest.computeFor("foo");
+  }
+
+  @Test
+  public void test_checksum() {
+    assertThat(computeFor("id1", "url1"))
+      .isNotEmpty()
+      .isEqualTo(computeFor("id1", "url1"))
+      .isNotEqualTo(computeFor("id1", "url2"))
+      .isNotEqualTo(computeFor("id2", "url1"))
+      .isNotEqualTo(computeFor("id2", "url2"));
+  }
+
+  private String computeFor(String serverId, String jdbcUrl) {
+    MapSettings settings = new MapSettings();
+    JdbcUrlSanitizer jdbcUrlSanitizer = mock(JdbcUrlSanitizer.class);
+    when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn("_" + jdbcUrl);
+    ServerIdChecksum underTest = new ServerIdChecksum(settings.asConfig(), jdbcUrlSanitizer);
+    settings.setProperty(JDBC_URL.getKey(), jdbcUrl);
+    return underTest.computeFor(serverId);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java
new file mode 100644 (file)
index 0000000..fdeee07
--- /dev/null
@@ -0,0 +1,119 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.api.config.Configuration;
+import org.sonar.api.config.internal.MapSettings;
+import org.sonar.core.platform.ServerId;
+import org.sonar.core.util.UuidFactory;
+import org.sonar.core.util.Uuids;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH;
+import static org.sonar.process.ProcessProperties.Property.JDBC_URL;
+import static org.sonar.server.platform.serverid.ServerIdFactoryImpl.crc32Hex;
+
+@RunWith(DataProviderRunner.class)
+public class ServerIdFactoryImplTest {
+  private static final ServerId A_SERVERID = ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH));
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private MapSettings settings = new MapSettings();
+  private Configuration config = settings.asConfig();
+  private UuidFactory uuidFactory = mock(UuidFactory.class);
+  private JdbcUrlSanitizer jdbcUrlSanitizer = mock(JdbcUrlSanitizer.class);
+  private ServerIdFactoryImpl underTest = new ServerIdFactoryImpl(config, uuidFactory, jdbcUrlSanitizer);
+
+  @Test
+  public void create_from_scratch_fails_with_ISE_if_JDBC_property_not_set() {
+    expectMissingJdbcUrlISE();
+
+    underTest.create();
+  }
+
+  @Test
+  public void create_from_scratch_creates_ServerId_from_JDBC_URL_and_new_uuid() {
+    String jdbcUrl = "jdbc";
+    String uuid = Uuids.create();
+    String sanitizedJdbcUrl = "sanitized_jdbc";
+    settings.setProperty(JDBC_URL.getKey(), jdbcUrl);
+    when(uuidFactory.create()).thenReturn(uuid);
+    when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn(sanitizedJdbcUrl);
+
+    ServerId serverId = underTest.create();
+
+    assertThat(serverId.getDatabaseId().get()).isEqualTo(crc32Hex(sanitizedJdbcUrl));
+    assertThat(serverId.getDatasetId()).isEqualTo(uuid);
+  }
+
+  @Test
+  public void create_from_ServerId_fails_with_ISE_if_JDBC_property_not_set() {
+    expectMissingJdbcUrlISE();
+
+    underTest.create(A_SERVERID);
+  }
+
+  @Test
+  @UseDataProvider("anyFormatServerId")
+  public void create_from_ServerId_creates_ServerId_from_JDBC_URL_and_serverId_datasetId(ServerId currentServerId) {
+    String jdbcUrl = "jdbc";
+    String sanitizedJdbcUrl = "sanitized_jdbc";
+    settings.setProperty(JDBC_URL.getKey(), jdbcUrl);
+    when(uuidFactory.create()).thenThrow(new IllegalStateException("UuidFactory.create() should not be called"));
+    when(jdbcUrlSanitizer.sanitize(jdbcUrl)).thenReturn(sanitizedJdbcUrl);
+
+    ServerId serverId = underTest.create(currentServerId);
+
+    assertThat(serverId.getDatabaseId().get()).isEqualTo(crc32Hex(sanitizedJdbcUrl));
+    assertThat(serverId.getDatasetId()).isEqualTo(currentServerId.getDatasetId());
+  }
+
+  @DataProvider
+  public static Object[][] anyFormatServerId() {
+    return new Object[][] {
+      {ServerId.parse(new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()))},
+      {ServerId.parse(randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH))},
+      {ServerId.parse(randomAlphabetic(UUID_DATASET_ID_LENGTH))},
+      {ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH))},
+      {ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH))}
+    };
+  }
+
+  private void expectMissingJdbcUrlISE() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Missing JDBC URL");
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java
new file mode 100644 (file)
index 0000000..88bd240
--- /dev/null
@@ -0,0 +1,361 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.platform.serverid;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import org.junit.After;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.api.CoreProperties;
+import org.sonar.api.SonarQubeSide;
+import org.sonar.api.internal.SonarRuntimeImpl;
+import org.sonar.api.utils.System2;
+import org.sonar.api.utils.Version;
+import org.sonar.core.platform.ServerId;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.property.PropertyDto;
+import org.sonar.server.platform.WebServer;
+import org.sonar.server.property.InternalProperties;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import static org.sonar.api.SonarQubeSide.COMPUTE_ENGINE;
+import static org.sonar.api.SonarQubeSide.SERVER;
+import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH;
+
+@RunWith(DataProviderRunner.class)
+public class ServerIdManagerTest {
+
+  private static final ServerId OLD_FORMAT_SERVER_ID = ServerId.parse("20161123150657");
+  private static final ServerId NO_DATABASE_ID_SERVER_ID = ServerId.parse(randomAlphanumeric(UUID_DATASET_ID_LENGTH));
+  private static final ServerId WITH_DATABASE_ID_SERVER_ID = ServerId.of(randomAlphanumeric(DATABASE_ID_LENGTH), randomAlphanumeric(NOT_UUID_DATASET_ID_LENGTH));
+  private static final String CHECKSUM_1 = randomAlphanumeric(12);
+
+  @Rule
+  public final DbTester dbTester = DbTester.create(System2.INSTANCE);
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private ServerIdChecksum serverIdChecksum = mock(ServerIdChecksum.class);
+  private ServerIdFactory serverIdFactory = mock(ServerIdFactory.class);
+  private DbClient dbClient = dbTester.getDbClient();
+  private DbSession dbSession = dbTester.getSession();
+  private WebServer webServer = mock(WebServer.class);
+  private ServerIdManager underTest;
+
+  @After
+  public void tearDown() {
+    if (underTest != null) {
+      underTest.stop();
+    }
+  }
+
+  @Test
+  public void web_leader_persists_new_server_id_if_missing() {
+    mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID);
+    mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(true);
+
+    test(SERVER);
+
+    verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    verifyCreateNewServerIdFromScratch();
+  }
+
+  @Test
+  public void web_leader_persists_new_server_id_if_format_is_old_date() {
+    insertServerId(OLD_FORMAT_SERVER_ID);
+    mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID);
+    mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(true);
+
+    test(SERVER);
+
+    verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    verifyCreateNewServerIdFromScratch();
+  }
+
+  @Test
+  public void web_leader_persists_new_server_id_if_value_is_empty() {
+    insertServerId("");
+    mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID);
+    mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(true);
+
+    test(SERVER);
+
+    verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    verifyCreateNewServerIdFromScratch();
+  }
+
+  @Test
+  public void web_leader_keeps_existing_server_id_if_valid() {
+    insertServerId(WITH_DATABASE_ID_SERVER_ID);
+    insertChecksum(CHECKSUM_1);
+    mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(true);
+
+    test(SERVER);
+
+    verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+  }
+
+  @Test
+  public void web_leader_creates_server_id_from_scratch_if_checksum_fails_for_serverId_in_deprecated_format() {
+    ServerId currentServerId = OLD_FORMAT_SERVER_ID;
+    insertServerId(currentServerId);
+    insertChecksum("invalid");
+    mockChecksumOf(currentServerId, "valid");
+    mockCreateNewServerId(WITH_DATABASE_ID_SERVER_ID);
+    mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(true);
+
+    test(SERVER);
+
+    verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    verifyCreateNewServerIdFromScratch();
+  }
+
+  @Test
+  public void web_leader_creates_server_id_from_current_serverId_without_databaseId_if_checksum_fails() {
+    ServerId currentServerId = ServerId.parse(randomAlphanumeric(UUID_DATASET_ID_LENGTH));
+    insertServerId(currentServerId);
+    insertChecksum("does_not_match_WITH_DATABASE_ID_SERVER_ID");
+    mockChecksumOf(currentServerId, "matches_WITH_DATABASE_ID_SERVER_ID");
+    mockCreateNewServerIdFrom(currentServerId, WITH_DATABASE_ID_SERVER_ID);
+    mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(true);
+
+    test(SERVER);
+
+    verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    verifyCreateNewServerIdFrom(currentServerId);
+  }
+
+  @Test
+  public void web_leader_creates_server_id_from_current_serverId_with_databaseId_if_checksum_fails() {
+    ServerId currentServerId = ServerId.of(randomAlphanumeric(DATABASE_ID_LENGTH), randomAlphanumeric(UUID_DATASET_ID_LENGTH));
+    insertServerId(currentServerId);
+    insertChecksum("does_not_match_WITH_DATABASE_ID_SERVER_ID");
+    mockChecksumOf(currentServerId, "matches_WITH_DATABASE_ID_SERVER_ID");
+    mockCreateNewServerIdFrom(currentServerId, WITH_DATABASE_ID_SERVER_ID);
+    mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(true);
+
+    test(SERVER);
+
+    verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    verifyCreateNewServerIdFrom(currentServerId);
+  }
+
+  @Test
+  public void web_leader_generates_missing_checksum_for_current_serverId_with_databaseId() {
+    insertServerId(WITH_DATABASE_ID_SERVER_ID);
+    mockChecksumOf(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(true);
+
+    test(SERVER);
+
+    verifyDb(WITH_DATABASE_ID_SERVER_ID, CHECKSUM_1);
+  }
+
+  @Test
+  @UseDataProvider("allFormatsOfServerId")
+  public void web_follower_does_not_fail_if_server_id_matches_checksum(ServerId serverId) {
+    insertServerId(serverId);
+    insertChecksum(CHECKSUM_1);
+    mockChecksumOf(serverId, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(false);
+
+    test(SERVER);
+
+    // no changes
+    verifyDb(serverId, CHECKSUM_1);
+  }
+
+  @Test
+  public void web_follower_fails_if_server_id_is_missing() {
+    when(webServer.isStartupLeader()).thenReturn(false);
+
+    expectMissingServerIdException();
+
+    test(SERVER);
+  }
+
+  @Test
+  public void web_follower_fails_if_server_id_is_empty() {
+    insertServerId("");
+    when(webServer.isStartupLeader()).thenReturn(false);
+
+    expectEmptyServerIdException();
+
+    test(SERVER);
+  }
+
+  @Test
+  @UseDataProvider("allFormatsOfServerId")
+  public void web_follower_fails_if_checksum_does_not_match(ServerId serverId) {
+    String dbChecksum = "boom";
+    insertServerId(serverId);
+    insertChecksum(dbChecksum);
+    mockChecksumOf(serverId, CHECKSUM_1);
+    when(webServer.isStartupLeader()).thenReturn(false);
+
+    try {
+      test(SERVER);
+      fail("An ISE should have been raised");
+    }
+    catch (IllegalStateException e) {
+      assertThat(e.getMessage()).isEqualTo("Server ID is invalid");
+      // no changes
+      verifyDb(serverId, dbChecksum);
+    }
+  }
+
+  @Test
+  @UseDataProvider("allFormatsOfServerId")
+  public void compute_engine_does_not_fail_if_server_id_is_valid(ServerId serverId) {
+    insertServerId(serverId);
+    insertChecksum(CHECKSUM_1);
+    mockChecksumOf(serverId, CHECKSUM_1);
+
+    test(COMPUTE_ENGINE);
+
+    // no changes
+    verifyDb(serverId, CHECKSUM_1);
+  }
+
+  @Test
+  public void compute_engine_fails_if_server_id_is_missing() {
+    expectMissingServerIdException();
+
+    test(COMPUTE_ENGINE);
+  }
+
+  @Test
+  public void compute_engine_fails_if_server_id_is_empty() {
+    insertServerId("");
+
+    expectEmptyServerIdException();
+
+    test(COMPUTE_ENGINE);
+  }
+
+  @Test
+  @UseDataProvider("allFormatsOfServerId")
+  public void compute_engine_fails_if_server_id_is_invalid(ServerId serverId) {
+    String dbChecksum = "boom";
+    insertServerId(serverId);
+    insertChecksum(dbChecksum);
+    mockChecksumOf(serverId, CHECKSUM_1);
+
+    try {
+      test(SERVER);
+      fail("An ISE should have been raised");
+    }
+    catch (IllegalStateException e) {
+      assertThat(e.getMessage()).isEqualTo("Server ID is invalid");
+      // no changes
+      verifyDb(serverId, dbChecksum);
+    }
+  }
+
+  @DataProvider
+  public static Object[][] allFormatsOfServerId() {
+    return new Object[][] {
+        {OLD_FORMAT_SERVER_ID},
+        {NO_DATABASE_ID_SERVER_ID},
+        {WITH_DATABASE_ID_SERVER_ID}
+    };
+  }
+
+  private void expectEmptyServerIdException() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Property sonar.core.id is empty in database");
+  }
+
+  private void expectMissingServerIdException() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Property sonar.core.id is missing in database");
+  }
+
+  private void verifyDb(ServerId expectedServerId, String expectedChecksum) {
+    assertThat(dbClient.propertiesDao().selectGlobalProperty(dbSession, CoreProperties.SERVER_ID))
+      .extracting(PropertyDto::getValue)
+      .containsExactly(expectedServerId.toString());
+    assertThat(dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.SERVER_ID_CHECKSUM))
+      .hasValue(expectedChecksum);
+  }
+
+  private void mockCreateNewServerId(ServerId newServerId) {
+    when(serverIdFactory.create()).thenReturn(newServerId);
+    when(serverIdFactory.create(any())).thenThrow(new IllegalStateException("new ServerId should not be created from current server id"));
+  }
+
+  private void mockCreateNewServerIdFrom(ServerId currentServerId, ServerId newServerId) {
+    when(serverIdFactory.create()).thenThrow(new IllegalStateException("new ServerId should be created from current server id"));
+    when(serverIdFactory.create(eq(currentServerId))).thenReturn(newServerId);
+  }
+
+  private void verifyCreateNewServerIdFromScratch() {
+    verify(serverIdFactory).create();
+  }
+
+  private void verifyCreateNewServerIdFrom(ServerId currentServerId) {
+    verify(serverIdFactory).create(currentServerId);
+  }
+
+  private void mockChecksumOf(ServerId serverId, String checksum1) {
+    when(serverIdChecksum.computeFor(serverId.toString())).thenReturn(checksum1);
+  }
+
+  private void insertServerId(ServerId serverId) {
+    insertServerId(serverId.toString());
+  }
+
+  private void insertServerId(String serverId) {
+    dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(CoreProperties.SERVER_ID).setValue(serverId.toString()));
+    dbSession.commit();
+  }
+
+  private void insertChecksum(String value) {
+    dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, value);
+    dbSession.commit();
+  }
+
+  private void test(SonarQubeSide side) {
+    underTest = new ServerIdManager(serverIdChecksum, serverIdFactory, dbClient, SonarRuntimeImpl.forSonarQube(Version.create(6, 7), side), webServer);
+    underTest.start();
+  }
+}
diff --git a/sonar-core/src/main/java/org/sonar/core/platform/ServerId.java b/sonar-core/src/main/java/org/sonar/core/platform/ServerId.java
new file mode 100644 (file)
index 0000000..b30ee81
--- /dev/null
@@ -0,0 +1,164 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.core.platform;
+
+import com.google.common.collect.ImmutableSet;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import javax.annotation.Nullable;
+import javax.annotation.concurrent.Immutable;
+import org.sonar.api.CoreProperties;
+import org.sonar.core.util.UuidFactory;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static org.sonar.core.platform.ServerId.Format.DEPRECATED;
+import static org.sonar.core.platform.ServerId.Format.NO_DATABASE_ID;
+import static org.sonar.core.platform.ServerId.Format.WITH_DATABASE_ID;
+
+@Immutable
+public final class ServerId {
+
+  public static final char SPLIT_CHARACTER = '-';
+  public static final int DATABASE_ID_LENGTH = 8;
+  public static final int DEPRECATED_SERVER_ID_LENGTH = 14;
+  public static final int NOT_UUID_DATASET_ID_LENGTH = 15;
+  public static final int UUID_DATASET_ID_LENGTH = 20;
+  private static final Set<Integer> ALLOWED_LENGTHS = ImmutableSet.of(
+    DEPRECATED_SERVER_ID_LENGTH,
+    NOT_UUID_DATASET_ID_LENGTH,
+    NOT_UUID_DATASET_ID_LENGTH + 1 + DATABASE_ID_LENGTH,
+    UUID_DATASET_ID_LENGTH,
+    UUID_DATASET_ID_LENGTH + 1 + DATABASE_ID_LENGTH);
+
+  public enum Format {
+    /* server id format before 6.1 (see SONAR-6992) */
+    DEPRECATED,
+    /* server id format before 6.7.5 and 7.3 (see LICENSE-96) */
+    NO_DATABASE_ID,
+    WITH_DATABASE_ID
+  }
+
+  private final String databaseId;
+  private final String datasetId;
+  private final Format format;
+
+  private ServerId(@Nullable String databaseId, String datasetId) {
+    this.databaseId = databaseId;
+    this.datasetId = datasetId;
+    this.format = computeFormat(databaseId, datasetId);
+  }
+
+  public Optional<String> getDatabaseId() {
+    return Optional.ofNullable(databaseId);
+  }
+
+  public String getDatasetId() {
+    return datasetId;
+  }
+
+  public Format getFormat() {
+    return format;
+  }
+
+  private static Format computeFormat(@Nullable String databaseId, String datasetId) {
+    if (databaseId != null) {
+      return WITH_DATABASE_ID;
+    }
+    if (isDate(datasetId)) {
+      return DEPRECATED;
+    }
+    return NO_DATABASE_ID;
+  }
+
+  public static ServerId parse(String serverId) {
+    String trimmed = serverId.trim();
+
+    int length = trimmed.length();
+    checkArgument(length > 0, "serverId can't be empty");
+    checkArgument(ALLOWED_LENGTHS.contains(length), "serverId does not have a supported length");
+    if (length == DEPRECATED_SERVER_ID_LENGTH || length == UUID_DATASET_ID_LENGTH || length == NOT_UUID_DATASET_ID_LENGTH) {
+      return new ServerId(null, trimmed);
+    }
+
+    int splitCharIndex = trimmed.indexOf(SPLIT_CHARACTER);
+    if (splitCharIndex == -1) {
+      return new ServerId(null, trimmed);
+    }
+    checkArgument(splitCharIndex == DATABASE_ID_LENGTH, "Unrecognized serverId format. Parts have wrong length");
+    return of(trimmed.substring(0, splitCharIndex), trimmed.substring(splitCharIndex + 1));
+  }
+
+  public static ServerId of(@Nullable String databaseId, String datasetId) {
+    if (databaseId != null) {
+      int databaseIdLength = databaseId.length();
+      checkArgument(databaseIdLength == DATABASE_ID_LENGTH, "Illegal databaseId length (%s)", databaseIdLength);
+    }
+    int datasetIdLength = datasetId.length();
+    checkArgument(datasetIdLength == DEPRECATED_SERVER_ID_LENGTH
+      || datasetIdLength == NOT_UUID_DATASET_ID_LENGTH
+      || datasetIdLength == UUID_DATASET_ID_LENGTH, "Illegal datasetId length (%s)", datasetIdLength);
+    return new ServerId(databaseId, datasetId);
+  }
+
+  public static ServerId create(UuidFactory uuidFactory) {
+    return new ServerId(null, uuidFactory.create());
+  }
+
+  /**
+   * Checks whether the specified value is a date according to the old format of the {@link CoreProperties#SERVER_ID}.
+   */
+  private static boolean isDate(String value) {
+    try {
+      new SimpleDateFormat("yyyyMMddHHmmss").parse(value);
+      return true;
+    } catch (ParseException e) {
+      return false;
+    }
+  }
+
+  @Override
+  public String toString() {
+    if (databaseId == null) {
+      return datasetId;
+    }
+    return databaseId + SPLIT_CHARACTER + datasetId;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) {
+      return true;
+    }
+    if (o == null || getClass() != o.getClass()) {
+      return false;
+    }
+    ServerId serverId = (ServerId) o;
+    return Objects.equals(databaseId, serverId.databaseId) &&
+      Objects.equals(datasetId, serverId.datasetId);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(databaseId, datasetId);
+  }
+}
diff --git a/sonar-core/src/test/java/org/sonar/core/platform/ServerIdTest.java b/sonar-core/src/test/java/org/sonar/core/platform/ServerIdTest.java
new file mode 100644 (file)
index 0000000..f6f15d2
--- /dev/null
@@ -0,0 +1,309 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 SonarSource SA
+ * mailto:info 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.core.platform;
+
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Random;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+import org.sonar.core.util.UuidFactoryImpl;
+
+import static org.apache.commons.lang.RandomStringUtils.randomAlphabetic;
+import static org.apache.commons.lang.StringUtils.repeat;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.sonar.core.platform.ServerId.DATABASE_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.DEPRECATED_SERVER_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.NOT_UUID_DATASET_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.SPLIT_CHARACTER;
+import static org.sonar.core.platform.ServerId.UUID_DATASET_ID_LENGTH;
+import static org.sonar.core.platform.ServerId.Format.DEPRECATED;
+import static org.sonar.core.platform.ServerId.Format.NO_DATABASE_ID;
+import static org.sonar.core.platform.ServerId.Format.WITH_DATABASE_ID;
+
+@RunWith(DataProviderRunner.class)
+public class ServerIdTest {
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  @Test
+  public void parse_throws_NPE_if_argument_is_null() {
+    expectedException.expect(NullPointerException.class);
+
+    ServerId.parse(null);
+  }
+
+  @Test
+  @UseDataProvider("emptyAfterTrim")
+  public void parse_throws_IAE_if_parameter_is_empty_after_trim(String serverId) {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("serverId can't be empty");
+
+    ServerId.parse(serverId);
+  }
+
+  @DataProvider
+  public static Object[][] emptyAfterTrim() {
+    return new Object[][] {
+      {""},
+      {" "},
+      {"    "}
+    };
+  }
+
+  @Test
+  @UseDataProvider("wrongFormatWithDatabaseId")
+  public void parse_throws_IAE_if_split_char_is_at_wrong_position(String emptyDatabaseId) {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Unrecognized serverId format. Parts have wrong length");
+
+    ServerId.parse(emptyDatabaseId);
+  }
+
+  @DataProvider
+  public static Object[][] wrongFormatWithDatabaseId() {
+    String onlySplitChar = repeat(SPLIT_CHARACTER + "", DATABASE_ID_LENGTH);
+    String startWithSplitChar = SPLIT_CHARACTER + randomAlphabetic(DATABASE_ID_LENGTH - 1);
+
+    Stream<String> databaseIds = Stream.of(
+      UuidFactoryImpl.INSTANCE.create(),
+      randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH),
+      randomAlphabetic(UUID_DATASET_ID_LENGTH),
+      repeat(SPLIT_CHARACTER + "", NOT_UUID_DATASET_ID_LENGTH),
+      repeat(SPLIT_CHARACTER + "", UUID_DATASET_ID_LENGTH));
+
+    return databaseIds
+      .flatMap(datasetId -> Stream.of(
+        startWithSplitChar + SPLIT_CHARACTER + datasetId,
+        onlySplitChar + SPLIT_CHARACTER + datasetId,
+        startWithSplitChar + randomAlphabetic(1) + datasetId,
+        onlySplitChar + randomAlphabetic(1) + datasetId))
+      .flatMap(serverId -> Stream.of(
+        serverId,
+        " " + serverId,
+        "    " + serverId))
+      .map(t -> new Object[] {t})
+      .toArray(Object[][]::new);
+  }
+
+  @Test
+  public void parse_parses_deprecated_format_serverId() {
+    String deprecated = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
+
+    ServerId serverId = ServerId.parse(deprecated);
+
+    assertThat(serverId.getFormat()).isEqualTo(DEPRECATED);
+    assertThat(serverId.getDatasetId()).isEqualTo(deprecated);
+    assertThat(serverId.getDatabaseId()).isEmpty();
+    assertThat(serverId.toString()).isEqualTo(deprecated);
+  }
+
+  @Test
+  @UseDataProvider("validOldFormatServerIds")
+  public void parse_parses_no_databaseId_format_serverId(String noDatabaseId) {
+    ServerId serverId = ServerId.parse(noDatabaseId);
+
+    assertThat(serverId.getFormat()).isEqualTo(NO_DATABASE_ID);
+    assertThat(serverId.getDatasetId()).isEqualTo(noDatabaseId);
+    assertThat(serverId.getDatabaseId()).isEmpty();
+    assertThat(serverId.toString()).isEqualTo(noDatabaseId);
+  }
+
+  @DataProvider
+  public static Object[][] validOldFormatServerIds() {
+    return new Object[][] {
+      {UuidFactoryImpl.INSTANCE.create()},
+      {randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH)},
+      {repeat(SPLIT_CHARACTER + "", NOT_UUID_DATASET_ID_LENGTH)},
+      {randomAlphabetic(UUID_DATASET_ID_LENGTH)},
+      {repeat(SPLIT_CHARACTER + "", UUID_DATASET_ID_LENGTH)}
+    };
+  }
+
+  @Test
+  @UseDataProvider("validServerIdWithDatabaseId")
+  public void parse_parses_serverId_with_database_id(String databaseId, String datasetId) {
+    String rawServerId = databaseId + SPLIT_CHARACTER + datasetId;
+
+    ServerId serverId = ServerId.parse(rawServerId);
+
+    assertThat(serverId.getFormat()).isEqualTo(WITH_DATABASE_ID);
+    assertThat(serverId.getDatasetId()).isEqualTo(datasetId);
+    assertThat(serverId.getDatabaseId()).contains(databaseId);
+    assertThat(serverId.toString()).isEqualTo(rawServerId);
+  }
+
+  @DataProvider
+  public static Object[][] validServerIdWithDatabaseId() {
+    return new Object[][] {
+      {randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(NOT_UUID_DATASET_ID_LENGTH)},
+      {randomAlphabetic(DATABASE_ID_LENGTH), randomAlphabetic(UUID_DATASET_ID_LENGTH)},
+      {randomAlphabetic(DATABASE_ID_LENGTH), repeat(SPLIT_CHARACTER + "", NOT_UUID_DATASET_ID_LENGTH)},
+      {randomAlphabetic(DATABASE_ID_LENGTH), repeat(SPLIT_CHARACTER + "", UUID_DATASET_ID_LENGTH)},
+      {randomAlphabetic(DATABASE_ID_LENGTH), UuidFactoryImpl.INSTANCE.create()},
+    };
+  }
+
+  @Test
+  public void parse_does_not_support_deprecated_server_id_with_database_id() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("serverId does not have a supported length");
+
+    ServerId.parse(randomAlphabetic(DATABASE_ID_LENGTH) + SPLIT_CHARACTER + randomAlphabetic(DEPRECATED_SERVER_ID_LENGTH));
+  }
+
+  @Test
+  public void of_throws_NPE_if_datasetId_is_null() {
+    expectedException.expect(NullPointerException.class);
+
+    ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), null);
+  }
+
+  @Test
+  public void of_throws_IAE_if_datasetId_is_empty() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Illegal datasetId length (0)");
+
+    ServerId.of(randomAlphabetic(DATABASE_ID_LENGTH), "");
+  }
+
+  @Test
+  public void of_throws_IAE_if_databaseId_is_empty() {
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Illegal databaseId length (0)");
+
+    ServerId.of("", randomAlphabetic(UUID_DATASET_ID_LENGTH));
+  }
+
+  @Test
+  @UseDataProvider("datasetIdSupportedLengths")
+  public void of_accepts_null_databaseId(int datasetIdLength) {
+    String datasetId = randomAlphabetic(datasetIdLength);
+    ServerId serverId = ServerId.of(null, datasetId);
+
+    assertThat(serverId.getDatabaseId()).isEmpty();
+    assertThat(serverId.getDatasetId()).isEqualTo(datasetId);
+  }
+
+  @Test
+  @UseDataProvider("illegalDatabaseIdLengths")
+  public void of_throws_IAE_if_databaseId_length_is_not_8(int illegalDatabaseIdLengths) {
+    String databaseId = randomAlphabetic(illegalDatabaseIdLengths);
+    String datasetId = randomAlphabetic(UUID_DATASET_ID_LENGTH);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Illegal databaseId length (" + illegalDatabaseIdLengths + ")");
+
+    ServerId.of(databaseId, datasetId);
+  }
+
+  @DataProvider
+  public static Object[][] illegalDatabaseIdLengths() {
+    return IntStream.range(1, 8 + new Random().nextInt(5))
+      .filter(i -> i != DATABASE_ID_LENGTH)
+      .mapToObj(i -> new Object[] {i})
+      .toArray(Object[][]::new);
+  }
+
+  @Test
+  @UseDataProvider("illegalDatasetIdLengths")
+  public void of_throws_IAE_if_datasetId_length_is_not_8(int illegalDatasetIdLengths) {
+    String datasetId = randomAlphabetic(illegalDatasetIdLengths);
+    String databaseId = randomAlphabetic(DATABASE_ID_LENGTH);
+
+    expectedException.expect(IllegalArgumentException.class);
+    expectedException.expectMessage("Illegal datasetId length (" + illegalDatasetIdLengths + ")");
+
+    ServerId.of(databaseId, datasetId);
+  }
+
+  @DataProvider
+  public static Object[][] illegalDatasetIdLengths() {
+    return IntStream.range(1, UUID_DATASET_ID_LENGTH + new Random().nextInt(5))
+      .filter(i -> i != UUID_DATASET_ID_LENGTH)
+      .filter(i -> i != NOT_UUID_DATASET_ID_LENGTH)
+      .filter(i -> i != DEPRECATED_SERVER_ID_LENGTH)
+      .mapToObj(i -> new Object[] {i})
+      .toArray(Object[][]::new);
+  }
+
+  @Test
+  @UseDataProvider("datasetIdSupportedLengths")
+  public void equals_is_based_on_databaseId_and_datasetId(int datasetIdLength) {
+    String databaseId = randomAlphabetic(DATABASE_ID_LENGTH - 1) + 'a';
+    String otherDatabaseId = randomAlphabetic(DATABASE_ID_LENGTH - 1) + 'b';
+    String datasetId = randomAlphabetic(datasetIdLength - 1) + 'a';
+    String otherDatasetId = randomAlphabetic(datasetIdLength - 1) + 'b';
+
+    ServerId newServerId = ServerId.of(databaseId, datasetId);
+    assertThat(newServerId).isEqualTo(newServerId);
+    assertThat(newServerId).isEqualTo(ServerId.of(databaseId, datasetId));
+    assertThat(newServerId).isNotEqualTo(new Object());
+    assertThat(newServerId).isNotEqualTo(null);
+    assertThat(newServerId).isNotEqualTo(ServerId.of(otherDatabaseId, datasetId));
+    assertThat(newServerId).isNotEqualTo(ServerId.of(databaseId, otherDatasetId));
+    assertThat(newServerId).isNotEqualTo(ServerId.of(otherDatabaseId, otherDatasetId));
+
+    ServerId oldServerId = ServerId.parse(datasetId);
+    assertThat(oldServerId).isEqualTo(oldServerId);
+    assertThat(oldServerId).isEqualTo(ServerId.parse(datasetId));
+    assertThat(oldServerId).isNotEqualTo(ServerId.parse(otherDatasetId));
+    assertThat(oldServerId).isNotEqualTo(ServerId.of(databaseId, datasetId));
+  }
+
+  @Test
+  @UseDataProvider("datasetIdSupportedLengths")
+  public void hashcode_is_based_on_databaseId_and_datasetId(int datasetIdLength) {
+    String databaseId = randomAlphabetic(DATABASE_ID_LENGTH - 1) + 'a';
+    String otherDatabaseId = randomAlphabetic(DATABASE_ID_LENGTH - 1) + 'b';
+    String datasetId = randomAlphabetic(datasetIdLength - 1) + 'a';
+    String otherDatasetId = randomAlphabetic(datasetIdLength - 1) + 'b';
+
+    ServerId newServerId = ServerId.of(databaseId, datasetId);
+    assertThat(newServerId.hashCode()).isEqualTo(newServerId.hashCode());
+    assertThat(newServerId.hashCode()).isEqualTo(ServerId.of(databaseId, datasetId).hashCode());
+    assertThat(newServerId.hashCode()).isNotEqualTo(new Object().hashCode());
+    assertThat(newServerId.hashCode()).isNotEqualTo(null);
+    assertThat(newServerId.hashCode()).isNotEqualTo(ServerId.of(otherDatabaseId, datasetId).hashCode());
+    assertThat(newServerId.hashCode()).isNotEqualTo(ServerId.of(databaseId, otherDatasetId).hashCode());
+    assertThat(newServerId.hashCode()).isNotEqualTo(ServerId.of(otherDatabaseId, otherDatasetId).hashCode());
+
+    ServerId oldServerId = ServerId.parse(datasetId);
+    assertThat(oldServerId.hashCode()).isEqualTo(oldServerId.hashCode());
+    assertThat(oldServerId.hashCode()).isEqualTo(ServerId.parse(datasetId).hashCode());
+    assertThat(oldServerId.hashCode()).isNotEqualTo(ServerId.parse(otherDatasetId).hashCode());
+    assertThat(oldServerId.hashCode()).isNotEqualTo(ServerId.of(databaseId, datasetId).hashCode());
+  }
+
+  @DataProvider
+  public static Object[][] datasetIdSupportedLengths() {
+    return new Object[][] {
+      {ServerId.NOT_UUID_DATASET_ID_LENGTH},
+      {UUID_DATASET_ID_LENGTH},
+    };
+  }
+}