From 45f6d410d36e999607e306dcf4374d739d58677b Mon Sep 17 00:00:00 2001 From: =?utf8?q?S=C3=A9bastien=20Lesaint?= Date: Wed, 20 Jun 2018 17:15:44 +0200 Subject: [PATCH] LICENSE-96 implement support for staging and new server id format --- .../container/ComputeEngineContainerImpl.java | 4 +- .../ComputeEngineContainerImplTest.java | 11 +- .../platformlevel/PlatformLevel3.java | 4 +- .../JdbcUrlSanitizer.java} | 18 +- .../platform/serverid/ServerIdChecksum.java | 42 ++ .../platform/serverid/ServerIdFactory.java | 34 ++ .../serverid/ServerIdFactoryImpl.java | 69 ++++ .../{ => serverid}/ServerIdManager.java | 119 +++--- .../platform/serverid/ServerIdModule.java | 35 ++ .../platform/serverid/package-info.java | 23 ++ .../server/platform/ServerIdManagerTest.java | 258 ------------- .../JdbcUrlSanitizerTest.java} | 21 +- .../serverid/ServerIdChecksumTest.java | 64 ++++ .../serverid/ServerIdFactoryImplTest.java | 119 ++++++ .../serverid/ServerIdManagerTest.java | 361 ++++++++++++++++++ .../org/sonar/core/platform/ServerId.java | 164 ++++++++ .../org/sonar/core/platform/ServerIdTest.java | 309 +++++++++++++++ 17 files changed, 1307 insertions(+), 348 deletions(-) rename server/sonar-server/src/main/java/org/sonar/server/platform/{ServerIdChecksum.java => serverid/JdbcUrlSanitizer.java} (87%) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdChecksum.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java rename server/sonar-server/src/main/java/org/sonar/server/platform/{ => serverid}/ServerIdManager.java (54%) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java create mode 100644 server/sonar-server/src/main/java/org/sonar/server/platform/serverid/package-info.java delete mode 100644 server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java rename server/sonar-server/src/test/java/org/sonar/server/platform/{ServerIdChecksumTest.java => serverid/JdbcUrlSanitizerTest.java} (88%) create mode 100644 server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdChecksumTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java create mode 100644 sonar-core/src/main/java/org/sonar/core/platform/ServerId.java create mode 100644 sonar-core/src/test/java/org/sonar/core/platform/ServerIdTest.java diff --git a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java index 0d4bb529195..2b1519862ba 100644 --- a/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java +++ b/server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java @@ -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, diff --git a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java index d86e30af7fc..ddeaafc1cdd 100644 --- a/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java +++ b/server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java @@ -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/platformlevel/PlatformLevel3.java b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java index 27e8550d0a3..30376168e21 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java @@ -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/ServerIdChecksum.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/JdbcUrlSanitizer.java similarity index 87% rename from server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java rename to server/sonar-server/src/main/java/org/sonar/server/platform/serverid/JdbcUrlSanitizer.java index 1e4e7e7c283..05c3ba816b8 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdChecksum.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/JdbcUrlSanitizer.java @@ -17,32 +17,20 @@ * 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; +package org.sonar.server.platform.serverid; -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 { - +public class JdbcUrlSanitizer { 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) { + public String sanitize(String jdbcUrl) { String result; if (jdbcUrl.startsWith(SQLSERVER_PREFIX)) { result = sanitizeSqlServerUrl(jdbcUrl); 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 index 00000000000..57cd9e30cf9 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdChecksum.java @@ -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 index 00000000000..ab044181397 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactory.java @@ -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 index 00000000000..a0898d9c74b --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdFactoryImpl.java @@ -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/ServerIdManager.java b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java similarity index 54% rename from server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java rename to server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java index 11bd09b77dc..7d2d3bb2caa 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/platform/ServerIdManager.java +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdManager.java @@ -17,56 +17,57 @@ * 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; +package org.sonar.server.platform.serverid; -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.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.process.ProcessProperties.Property.JDBC_URL; +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 Configuration config; + private final ServerIdChecksum serverIdChecksum; + private final ServerIdFactory serverIdFactory; 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; + 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; - 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); + Optional 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); @@ -74,67 +75,75 @@ public class ServerIdManager implements Startable { } } - 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; + private ServerId keepOrReplaceCurrentServerId(DbSession dbSession, ServerId currentServerId, Optional checksum) { + if (keepServerId(currentServerId, checksum)) { + return currentServerId; } - if (isEmpty(dto.getValue())) { - return true; - } + ServerId serverId = replaceCurrentServerId(currentServerId); + persistServerId(dbSession, serverId); + return serverId; + } - if (isDate(dto.getValue())) { + private boolean keepServerId(ServerId serverId, Optional checksum) { + ServerId.Format format = serverId.getFormat(); + if (format == DEPRECATED || format == NO_DATABASE_ID) { LOGGER.info("Server ID is changed to new format."); - return true; + return false; } - Optional checksum = dbClient.internalPropertiesDao().selectByKey(dbSession, SERVER_ID_CHECKSUM); if (checksum.isPresent()) { - String expectedChecksum = computeChecksum(dto.getValue()); + 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 true; + return false; } } - // Existing server ID must be kept when upgrading to 6.7+. In that case the checksum does - // not exist. + // Existing server ID must be kept when upgrading to 6.7+. In that case the checksum does not exist. + return true; + } - return false; + private ServerId replaceCurrentServerId(ServerId currentServerId) { + if (currentServerId.getFormat() == DEPRECATED) { + return serverIdFactory.create(); + } + return serverIdFactory.create(currentServerId); } - 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(); + private ServerId createFirstServerId(DbSession dbSession) { + ServerId serverId = serverIdFactory.create(); + persistServerId(dbSession, serverId); + return serverId; + } + + private Optional 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. - dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, computeChecksum(serverId)); + String checksum = serverIdChecksum.computeFor(serverId.toString()); + persistChecksum(dbSession, checksum); } - /** - * 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 void persistServerId(DbSession dbSession, ServerId serverId) { + dbClient.propertiesDao().saveProperty(dbSession, new PropertyDto().setKey(SERVER_ID).setValue(serverId.toString())); } - 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 persistChecksum(DbSession dbSession, String checksump) { + dbClient.internalPropertiesDao().save(dbSession, InternalProperties.SERVER_ID_CHECKSUM, checksump); } private void ensureServerIdIsValid(DbSession dbSession) { @@ -144,7 +153,7 @@ public class ServerIdManager implements Startable { Optional 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"); + checkState(checksum.get().equals(serverIdChecksum.computeFor(id.getValue())), "Server ID is invalid"); } @Override 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 index 00000000000..3ef21171216 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/ServerIdModule.java @@ -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 index 00000000000..767e13e7270 --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/platform/serverid/package-info.java @@ -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/ServerIdManagerTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java deleted file mode 100644 index 412d8019121..00000000000 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdManagerTest.java +++ /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/ServerIdChecksumTest.java b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/JdbcUrlSanitizerTest.java similarity index 88% rename from server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java rename to server/sonar-server/src/test/java/org/sonar/server/platform/serverid/JdbcUrlSanitizerTest.java index 7bfae035f90..6c0f642276f 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/platform/ServerIdChecksumTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/JdbcUrlSanitizerTest.java @@ -17,23 +17,14 @@ * 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; +package org.sonar.server.platform.serverid; 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")); - } +public class JdbcUrlSanitizerTest { + private JdbcUrlSanitizer underTest = new JdbcUrlSanitizer(); @Test public void sanitize_h2_url() { @@ -89,7 +80,9 @@ public class ServerIdChecksumTest { 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); + 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 index 00000000000..b97cd8aa2af --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdChecksumTest.java @@ -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 index 00000000000..fdeee077e9b --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdFactoryImplTest.java @@ -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 index 00000000000..88bd2409d6e --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/platform/serverid/ServerIdManagerTest.java @@ -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 index 00000000000..b30ee8130e8 --- /dev/null +++ b/sonar-core/src/main/java/org/sonar/core/platform/ServerId.java @@ -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 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 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 index 00000000000..f6f15d22bab --- /dev/null +++ b/sonar-core/src/test/java/org/sonar/core/platform/ServerIdTest.java @@ -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 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}, + }; + } +} -- 2.39.5