JAMES-2096 Introduce Cassandra webadmin migration endpoints
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/f495121d Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/f495121d Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/f495121d Branch: refs/heads/master Commit: f495121dff2ac1c81215898cfff5ea9d25d6ad4b Parents: 4f13da5 Author: Antoine Duprat <[email protected]> Authored: Tue Jul 11 15:32:12 2017 +0200 Committer: benwa <[email protected]> Committed: Tue Jul 25 17:51:01 2017 +0700 ---------------------------------------------------------------------- server/pom.xml | 6 + .../webadmin/webadmin-cassandra/pom.xml | 251 ++++++++++++++++++ .../webadmin/dto/CassandraVersionRequest.java | 40 +++ .../webadmin/dto/CassandraVersionResponse.java | 35 +++ .../routes/CassandraMigrationRoutes.java | 105 ++++++++ .../service/CassandraMigrationService.java | 98 +++++++ .../webadmin/service/MigrationException.java | 26 ++ .../james/webadmin/dto/VersionRequestTest.java | 67 +++++ .../routes/CassandraMigrationRoutesTest.java | 202 ++++++++++++++ .../service/CassandraMigrationServiceTest.java | 263 +++++++++++++++++++ 10 files changed, 1093 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/pom.xml ---------------------------------------------------------------------- diff --git a/server/pom.xml b/server/pom.xml index 4e52dae..2f90850 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -102,6 +102,7 @@ <module>protocols/protocols-managesieve</module> <module>protocols/protocols-pop3</module> <module>protocols/protocols-smtp</module> + <module>protocols/webadmin/webadmin-cassandra</module> <module>protocols/webadmin/webadmin-core</module> <module>protocols/webadmin/webadmin-data</module> <module>protocols/webadmin/webadmin-mailbox</module> @@ -679,6 +680,11 @@ </dependency> <dependency> <groupId>org.apache.james</groupId> + <artifactId>james-server-webadmin-cassandra</artifactId> + <version>${project.version}</version> + </dependency> + <dependency> + <groupId>org.apache.james</groupId> <artifactId>james-server-webadmin-core</artifactId> <version>${project.version}</version> </dependency> http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/protocols/webadmin/webadmin-cassandra/pom.xml ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-cassandra/pom.xml b/server/protocols/webadmin/webadmin-cassandra/pom.xml new file mode 100644 index 0000000..0d68ced --- /dev/null +++ b/server/protocols/webadmin/webadmin-cassandra/pom.xml @@ -0,0 +1,251 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <artifactId>james-server</artifactId> + <groupId>org.apache.james</groupId> + <version>3.1.0-SNAPSHOT</version> + <relativePath>../../../pom.xml</relativePath> + </parent> + + <artifactId>james-server-webadmin-cassandra</artifactId> + <packaging>jar</packaging> + + <name>Apache James :: Server :: Web Admin :: Cassandra</name> + + <profiles> + <profile> + <id>noTest</id> + <activation> + <os> + <family>windows</family> + </os> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-surefire-plugin</artifactId> + <configuration> + <skipTests>true</skipTests> + </configuration> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>disable-build-for-older-jdk</id> + <activation> + <jdk>(,1.8)</jdk> + </activation> + <build> + <plugins> + <plugin> + <artifactId>maven-jar-plugin</artifactId> + <executions> + <execution> + <id>default-jar</id> + <phase>none</phase> + </execution> + <execution> + <id>jar</id> + <phase>none</phase> + </execution> + <execution> + <id>test-jar</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-compiler-plugin</artifactId> + <executions> + <execution> + <id>default-compile</id> + <phase>none</phase> + </execution> + <execution> + <id>default-testCompile</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-surefire-plugin</artifactId> + <executions> + <execution> + <id>default-test</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-source-plugin</artifactId> + <executions> + <execution> + <id>attach-sources</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-install-plugin</artifactId> + <executions> + <execution> + <id>default-install</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-resources-plugin</artifactId> + <executions> + <execution> + <id>default-resources</id> + <phase>none</phase> + </execution> + <execution> + <id>default-testResources</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + <plugin> + <artifactId>maven-site-plugin</artifactId> + <executions> + <execution> + <id>attach-descriptor</id> + <phase>none</phase> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>build-for-jdk-8</id> + <activation> + <jdk>[1.8,)</jdk> + </activation> + <dependencies> + <dependency> + <groupId>org.apache.james</groupId> + <artifactId>apache-james-mailbox-cassandra</artifactId> + </dependency> + <dependency> + <groupId>org.apache.james</groupId> + <artifactId>james-server-webadmin-core</artifactId> + </dependency> + <dependency> + <groupId>org.apache.james</groupId> + <artifactId>metrics-logger</artifactId> + <scope>test</scope> + </dependency> + + <dependency> + <groupId>com.jayway.restassured</groupId> + <artifactId>rest-assured</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>javax.inject</groupId> + <artifactId>javax.inject</artifactId> + </dependency> + <dependency> + <groupId>junit</groupId> + <artifactId>junit</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <version>${assertj-3.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + </dependencies> + <build> + <plugins> + <plugin> + <artifactId>maven-assembly-plugin</artifactId> + <configuration> + <archive> + <manifest> + <mainClass>fully.qualified.MainClass</mainClass> + </manifest> + </archive> + <descriptorRefs> + <descriptorRef>jar-with-dependencies</descriptorRef> + </descriptorRefs> + </configuration> + </plugin> + <plugin> + <groupId>org.apache.maven.plugins</groupId> + <artifactId>maven-compiler-plugin</artifactId> + <configuration> + <source>1.8</source> + <target>1.8</target> + </configuration> + </plugin> + </plugins> + </build> + </profile> + <profile> + <id>animal-sniffer-java-8</id> + <activation> + <jdk>[1.8,)</jdk> + </activation> + <build> + <plugins> + <plugin> + <groupId>org.codehaus.mojo</groupId> + <artifactId>animal-sniffer-maven-plugin</artifactId> + <configuration> + <signature> + <groupId>org.codehaus.mojo.signature</groupId> + <artifactId>java18</artifactId> + <version>1.0</version> + </signature> + </configuration> + <executions> + <execution> + <id>check_java_8</id> + <phase>test</phase> + <goals> + <goal>check</goal> + </goals> + </execution> + </executions> + </plugin> + </plugins> + </build> + </profile> + </profiles> +</project> http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/dto/CassandraVersionRequest.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/dto/CassandraVersionRequest.java b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/dto/CassandraVersionRequest.java new file mode 100644 index 0000000..5eaef46 --- /dev/null +++ b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/dto/CassandraVersionRequest.java @@ -0,0 +1,40 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.dto; + +import com.google.common.base.Preconditions; + +public class CassandraVersionRequest { + public static CassandraVersionRequest parse(String version) { + Preconditions.checkNotNull(version, "Version is mandatory"); + return new CassandraVersionRequest(Integer.valueOf(version)); + } + + private final int value; + + private CassandraVersionRequest(int value) { + Preconditions.checkArgument(value >= 0); + this.value = value; + } + + public int getValue() { + return value; + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/dto/CassandraVersionResponse.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/dto/CassandraVersionResponse.java b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/dto/CassandraVersionResponse.java new file mode 100644 index 0000000..34e98a8 --- /dev/null +++ b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/dto/CassandraVersionResponse.java @@ -0,0 +1,35 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.dto; + +import java.util.Optional; + +public class CassandraVersionResponse { + + private final Optional<Integer> version; + + public CassandraVersionResponse(Optional<Integer> version) { + this.version = version; + } + + public Optional<Integer> getversion() { + return version; + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/routes/CassandraMigrationRoutes.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/routes/CassandraMigrationRoutes.java b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/routes/CassandraMigrationRoutes.java new file mode 100644 index 0000000..c9564e7 --- /dev/null +++ b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/routes/CassandraMigrationRoutes.java @@ -0,0 +1,105 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.routes; + +import javax.inject.Inject; + +import org.apache.james.webadmin.Constants; +import org.apache.james.webadmin.Routes; +import org.apache.james.webadmin.dto.CassandraVersionRequest; +import org.apache.james.webadmin.service.CassandraMigrationService; +import org.apache.james.webadmin.service.MigrationException; +import org.apache.james.webadmin.utils.JsonTransformer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import spark.Service; + +public class CassandraMigrationRoutes implements Routes { + + + private static final Logger LOGGER = LoggerFactory.getLogger(CassandraMigrationRoutes.class); + + public static final String VERSION_BASE = "/cassandra/version"; + private static final String VERSION_BASE_LATEST = VERSION_BASE + "/latest"; + private static final String VERSION_UPGRADE_BASE = VERSION_BASE + "/upgrade"; + private static final String VERSION_UPGRADE_TO_LATEST_BASE = VERSION_UPGRADE_BASE + "/latest"; + private static final int NO_CONTENT = 204; + private static final int INVALID_VERSION = 400; + private static final int MIGRATION_CAN_NOT_BE_PERFORMED = 410; + private static final int INTERNAL_ERROR = 500; + + private final CassandraMigrationService cassandraMigrationService; + private final JsonTransformer jsonTransformer; + + @Inject + public CassandraMigrationRoutes(CassandraMigrationService cassandraMigrationService, JsonTransformer jsonTransformer) { + this.cassandraMigrationService = cassandraMigrationService; + this.jsonTransformer = jsonTransformer; + } + + @Override + public void define(Service service) { + service.get(VERSION_BASE, + (request, response) -> cassandraMigrationService.getCurrentVersion(), + jsonTransformer); + + service.get(VERSION_BASE_LATEST, + (request, response) -> cassandraMigrationService.getLatestVersion(), + jsonTransformer); + + service.post(VERSION_UPGRADE_BASE, (request, response) -> { + LOGGER.debug("Cassandra upgrade launched"); + try { + CassandraVersionRequest cassandraVersionRequest = CassandraVersionRequest.parse(request.body()); + cassandraMigrationService.upgradeToVersion(cassandraVersionRequest.getValue()); + response.status(NO_CONTENT); + } catch (NullPointerException | IllegalArgumentException e) { + LOGGER.info("Invalid request for version upgrade"); + response.status(INVALID_VERSION); + } catch (IllegalStateException e) { + LOGGER.info("The migration requested can not be performed.", e); + response.status(MIGRATION_CAN_NOT_BE_PERFORMED); + response.body(e.getMessage()); + } catch (MigrationException e) { + LOGGER.error("An error lead to partial migration process", e); + response.status(INTERNAL_ERROR); + response.body(e.getMessage()); + } + return Constants.EMPTY_BODY; + }); + + service.post(VERSION_UPGRADE_TO_LATEST_BASE, (request, response) -> { + try { + cassandraMigrationService.upgradeToLastVersion(); + } catch (IllegalStateException e) { + LOGGER.info("The migration requested can not be performed.", e); + response.status(MIGRATION_CAN_NOT_BE_PERFORMED); + response.body(e.getMessage()); + } catch (MigrationException e) { + LOGGER.error("An error lead to partial migration process", e); + response.status(INTERNAL_ERROR); + response.body(e.getMessage()); + } + + return Constants.EMPTY_BODY; + }); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/service/CassandraMigrationService.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/service/CassandraMigrationService.java b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/service/CassandraMigrationService.java new file mode 100644 index 0000000..b18770f --- /dev/null +++ b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/service/CassandraMigrationService.java @@ -0,0 +1,98 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.service; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.IntStream; +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.commons.lang.NotImplementedException; +import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDAO; +import org.apache.james.mailbox.cassandra.mail.migration.Migration; +import org.apache.james.webadmin.dto.CassandraVersionResponse; + +import com.google.common.base.Preconditions; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class CassandraMigrationService { + private static final int FIRST_VERSION = 1; + public static final String LATEST_VERSION = "latestVersion"; + private final CassandraSchemaVersionDAO schemaVersionDAO; + private final int latestVersion; + private final Map<Integer, Migration> allMigrationClazz; + private final Logger LOG = LoggerFactory.getLogger(CassandraMigrationService.class); + + @Inject + public CassandraMigrationService(CassandraSchemaVersionDAO schemaVersionDAO, Map<Integer, Migration> allMigrationClazz, @Named(LATEST_VERSION) int latestVersion) { + Preconditions.checkArgument(latestVersion >= 0, "The latest version must be positive"); + this.schemaVersionDAO = schemaVersionDAO; + this.latestVersion = latestVersion; + this.allMigrationClazz = allMigrationClazz; + } + + public CassandraVersionResponse getCurrentVersion() { + return new CassandraVersionResponse(schemaVersionDAO.getCurrentSchemaVersion().join()); + } + + public CassandraVersionResponse getLatestVersion() { + return new CassandraVersionResponse(Optional.of(latestVersion)); + } + + public synchronized void upgradeToVersion(int newVersion) { + int currentVersion = schemaVersionDAO.getCurrentSchemaVersion().join().orElse(FIRST_VERSION); + if (currentVersion >= newVersion) { + throw new IllegalStateException("Current version is already up to date"); + } + + IntStream.range(currentVersion, newVersion) + .boxed() + .forEach(this::doMigration); + } + + public void upgradeToLastVersion() { + upgradeToVersion(latestVersion); + } + + private void doMigration(Integer version) { + if (allMigrationClazz.containsKey(version)) { + LOG.info("Migrating to version {} ", version + 1); + boolean migrationSuccess = allMigrationClazz.get(version).run(); + if (migrationSuccess) { + schemaVersionDAO.updateVersion(version + 1); + LOG.info("Migrating to version {} done", version + 1); + } else { + String message = String.format("Migrating to version %d partially done. " + + "Please check logs for cause of failure and re-run this migration.", + version + 1); + LOG.warn(message); + throw new MigrationException(message); + } + } else { + String message = String.format("Can not migrate to %d. No migration class registered.", version + 1); + LOG.error(message); + throw new NotImplementedException(message); + } + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/service/MigrationException.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/service/MigrationException.java b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/service/MigrationException.java new file mode 100644 index 0000000..7b9d74f --- /dev/null +++ b/server/protocols/webadmin/webadmin-cassandra/src/main/java/org/apache/james/webadmin/service/MigrationException.java @@ -0,0 +1,26 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.service; + +public class MigrationException extends RuntimeException { + public MigrationException(String message) { + super(message); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/dto/VersionRequestTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/dto/VersionRequestTest.java b/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/dto/VersionRequestTest.java new file mode 100644 index 0000000..48ce134 --- /dev/null +++ b/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/dto/VersionRequestTest.java @@ -0,0 +1,67 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.dto; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +public class VersionRequestTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test + public void parseShouldThrowWhenNullVersion() throws Exception { + expectedException.expect(NullPointerException.class); + + CassandraVersionRequest.parse(null); + } + + @Test + public void parseShouldThrowWhenNonIntegerVersion() throws Exception { + expectedException.expect(IllegalArgumentException.class); + + CassandraVersionRequest.parse("NoInt"); + } + + @Test + public void parseShouldThrowWhenNegativeVersion() throws Exception { + expectedException.expect(IllegalArgumentException.class); + + CassandraVersionRequest.parse("-1"); + } + + @Test + public void parseShouldAcceptZeroVersion() throws Exception { + CassandraVersionRequest cassandraVersionRequest = CassandraVersionRequest.parse("0"); + + assertThat(cassandraVersionRequest.getValue()).isEqualTo(0); + } + + @Test + public void parseShouldParseTheVersionValue() throws Exception { + CassandraVersionRequest cassandraVersionRequest = CassandraVersionRequest.parse("1"); + + assertThat(cassandraVersionRequest.getValue()).isEqualTo(1); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/routes/CassandraMigrationRoutesTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/routes/CassandraMigrationRoutesTest.java b/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/routes/CassandraMigrationRoutesTest.java new file mode 100644 index 0000000..5944044 --- /dev/null +++ b/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/routes/CassandraMigrationRoutesTest.java @@ -0,0 +1,202 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.routes; + +import static com.jayway.restassured.RestAssured.given; +import static com.jayway.restassured.RestAssured.when; +import static com.jayway.restassured.config.EncoderConfig.encoderConfig; +import static com.jayway.restassured.config.RestAssuredConfig.newConfig; +import static org.apache.james.webadmin.WebAdminServer.NO_CONFIGURATION; +import static org.hamcrest.CoreMatchers.is; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; + +import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDAO; +import org.apache.james.mailbox.cassandra.mail.migration.Migration; +import org.apache.james.metrics.logger.DefaultMetricFactory; +import org.apache.james.webadmin.WebAdminServer; +import org.apache.james.webadmin.service.CassandraMigrationService; +import org.apache.james.webadmin.utils.JsonTransformer; + +import com.google.common.base.Charsets; +import com.google.common.collect.ImmutableMap; +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.builder.RequestSpecBuilder; +import com.jayway.restassured.http.ContentType; + +import org.junit.After; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Test; + +public class CassandraMigrationRoutesTest { + + public static final boolean MIGRATED = true; + private static final Integer LATEST_VERSION = 3; + private static final Integer CURRENT_VERSION = 2; + private static final Integer OLDER_VERSION = 1; + private WebAdminServer webAdminServer; + private CassandraSchemaVersionDAO schemaVersionDAO; + private Migration successfulMigration; + + private void createServer() throws Exception { + successfulMigration = mock(Migration.class); + when(successfulMigration.run()).thenReturn(MIGRATED); + Map<Integer, Migration> allMigrationClazz = ImmutableMap.<Integer, Migration>builder() + .put(OLDER_VERSION, successfulMigration) + .put(CURRENT_VERSION, successfulMigration) + .put(LATEST_VERSION, successfulMigration) + .build(); + schemaVersionDAO = mock(CassandraSchemaVersionDAO.class); + + webAdminServer = new WebAdminServer( + new DefaultMetricFactory(), + new CassandraMigrationRoutes(new CassandraMigrationService(schemaVersionDAO, allMigrationClazz, LATEST_VERSION), new JsonTransformer())); + webAdminServer.configure(NO_CONFIGURATION); + webAdminServer.await(); + + RestAssured.requestSpecification = new RequestSpecBuilder() + .setContentType(ContentType.JSON) + .setAccept(ContentType.JSON) + .setBasePath(CassandraMigrationRoutes.VERSION_BASE) + .setPort(webAdminServer.getPort().toInt()) + .setConfig(newConfig().encoderConfig(encoderConfig().defaultContentCharset(Charsets.UTF_8))) + .build(); + } + + @Before + public void setUp() throws Exception { + createServer(); + } + + @After + public void tearDown() { + webAdminServer.destroy(); + } + + @Test + public void getShouldReturnTheCurrentVersion() throws Exception { + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(CURRENT_VERSION))); + + when() + .get() + .then() + .statusCode(200) + .body(is("{\"version\":2}")); + } + + @Test + public void getShouldReturnTheLatestVersionWhenSetUpTheLatestVersion() throws Exception { + when() + .get("/latest") + .then() + .statusCode(200) + .body(is("{\"version\":" + LATEST_VERSION + "}")); + } + + @Ignore + @Test + public void postShouldReturnConflictWhenMigrationOnRunning() throws Exception { + when() + .post("/upgrade") + .then() + .statusCode(409); + } + + @Test + public void postShouldReturnErrorCodeWhenInvalidVersion() throws Exception { + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(OLDER_VERSION))); + + given() + .body(String.valueOf("NonInt")) + .with() + .post("/upgrade") + .then() + .statusCode(400); + + verifyNoMoreInteractions(schemaVersionDAO); + } + + @Test + public void postShouldDoMigrationToNewVersion() throws Exception { + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(OLDER_VERSION))); + + given() + .body(String.valueOf(CURRENT_VERSION)) + .with() + .post("/upgrade") + .then() + .statusCode(204); + + verify(schemaVersionDAO, times(1)).getCurrentSchemaVersion(); + verify(schemaVersionDAO, times(1)).updateVersion(eq(CURRENT_VERSION)); + verifyNoMoreInteractions(schemaVersionDAO); + } + + @Test + public void postShouldNotDoMigrationWhenCurrentVersionIsNewerThan() throws Exception { + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(CURRENT_VERSION))); + + given() + .body(String.valueOf(OLDER_VERSION)) + .with() + .post("/upgrade") + .then() + .statusCode(410); + + verify(schemaVersionDAO, times(1)).getCurrentSchemaVersion(); + verifyNoMoreInteractions(schemaVersionDAO); + } + + @Test + public void postShouldDoMigrationToLatestVersion() throws Exception { + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(OLDER_VERSION))); + + when() + .post("/upgrade/latest") + .then() + .statusCode(200); + + verify(schemaVersionDAO, times(1)).getCurrentSchemaVersion(); + verify(schemaVersionDAO, times(1)).updateVersion(eq(CURRENT_VERSION)); + verify(schemaVersionDAO, times(1)).updateVersion(eq(LATEST_VERSION)); + verifyNoMoreInteractions(schemaVersionDAO); + } + + @Test + public void postShouldNotDoMigrationToLatestVersionWhenItIsUpToDate() throws Exception { + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(LATEST_VERSION))); + + when() + .post("/upgrade/latest") + .then() + .statusCode(410); + + verify(schemaVersionDAO, times(1)).getCurrentSchemaVersion(); + verifyNoMoreInteractions(schemaVersionDAO); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/f495121d/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/service/CassandraMigrationServiceTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/service/CassandraMigrationServiceTest.java b/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/service/CassandraMigrationServiceTest.java new file mode 100644 index 0000000..7134e70 --- /dev/null +++ b/server/protocols/webadmin/webadmin-cassandra/src/test/java/org/apache/james/webadmin/service/CassandraMigrationServiceTest.java @@ -0,0 +1,263 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.webadmin.service; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.commons.lang.NotImplementedException; +import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDAO; +import org.apache.james.mailbox.cassandra.mail.migration.Migration; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import com.datastax.driver.core.Session; +import com.google.common.base.Throwables; +import com.google.common.collect.ImmutableMap; + +public class CassandraMigrationServiceTest { + private static final int LATEST_VERSION = 3; + private static final int CURRENT_VERSION = 2; + private static final int OLDER_VERSION = 1; + private static final boolean MIGRATED = true; + private static final boolean MIGRATION_FAILED = false; + private CassandraMigrationService testee; + private CassandraSchemaVersionDAO schemaVersionDAO; + private ExecutorService executorService; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + private Migration successfulMigration; + + @Before + public void setUp() throws Exception { + schemaVersionDAO = mock(CassandraSchemaVersionDAO.class); + successfulMigration = mock(Migration.class); + when(successfulMigration.run()).thenReturn(MIGRATED); + Map<Integer, Migration> allMigrationClazz = ImmutableMap.<Integer, Migration>builder() + .put(OLDER_VERSION, successfulMigration) + .put(CURRENT_VERSION, successfulMigration) + .put(LATEST_VERSION, successfulMigration) + .build(); + testee = new CassandraMigrationService(schemaVersionDAO, allMigrationClazz, LATEST_VERSION); + executorService = Executors.newFixedThreadPool(2); + } + + @After + public void tearDown() { + executorService.shutdownNow(); + } + + @Test + public void getCurrentVersionShouldReturnCurrentVersion() throws Exception { + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(CURRENT_VERSION))); + + assertThat(testee.getCurrentVersion().getversion().get()).isEqualTo(CURRENT_VERSION); + } + + @Test + public void getLatestVersionShouldReturnTheLatestVersion() throws Exception { + assertThat(testee.getLatestVersion().getversion().get()).isEqualTo(LATEST_VERSION); + } + + @Test + public void upgradeToVersionShouldThrowWhenCurrentVersionIsUpToDate() throws Exception { + expectedException.expect(IllegalStateException.class); + + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(CURRENT_VERSION))); + + testee.upgradeToVersion(OLDER_VERSION); + } + + @Test + public void upgradeToVersionShouldUpdateToVersion() throws Exception { + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(OLDER_VERSION))); + + testee.upgradeToVersion(CURRENT_VERSION); + + verify(schemaVersionDAO, times(1)).updateVersion(eq(CURRENT_VERSION)); + } + + @Test + public void upgradeToLastVersionShouldThrowWhenVersionIsUpToDate() throws Exception { + expectedException.expect(IllegalStateException.class); + + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(LATEST_VERSION))); + + testee.upgradeToLastVersion(); + } + + @Test + public void upgradeToLastVersionShouldUpdateToLatestVersion() throws Exception { + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(OLDER_VERSION))); + + testee.upgradeToLastVersion(); + + verify(schemaVersionDAO, times(1)).updateVersion(eq(LATEST_VERSION)); + } + + @Test + public void upgradeToVersionShouldThrowOnMissingVersion() throws Exception { + Map<Integer, Migration> allMigrationClazz = ImmutableMap.<Integer, Migration>builder() + .put(OLDER_VERSION, successfulMigration) + .put(LATEST_VERSION, successfulMigration) + .build(); + testee = new CassandraMigrationService(schemaVersionDAO, allMigrationClazz, LATEST_VERSION); + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(OLDER_VERSION))); + + expectedException.expect(NotImplementedException.class); + + testee.upgradeToVersion(LATEST_VERSION); + } + + @Test + public void upgradeToVersionShouldUpdateIntermediarySuccessfulMigrationsInCaseOfError() throws Exception { + try { + Map<Integer, Migration> allMigrationClazz = ImmutableMap.<Integer, Migration>builder() + .put(OLDER_VERSION, successfulMigration) + .put(LATEST_VERSION, successfulMigration) + .build(); + testee = new CassandraMigrationService(schemaVersionDAO, allMigrationClazz, LATEST_VERSION); + when(schemaVersionDAO.getCurrentSchemaVersion()).thenReturn(CompletableFuture.completedFuture(Optional.of(OLDER_VERSION))); + + expectedException.expect(RuntimeException.class); + + testee.upgradeToVersion(LATEST_VERSION); + } finally { + verify(schemaVersionDAO).updateVersion(CURRENT_VERSION); + } + } + + @Test + public void concurrentMigrationsShouldFail() throws Exception { + // Given a stateful migration service + Migration wait1SecondMigration = mock(Migration.class); + doAnswer(invocation -> { + Thread.sleep(1000); + return MIGRATED; + }).when(wait1SecondMigration).run(); + Map<Integer, Migration> allMigrationClazz = ImmutableMap.<Integer, Migration>builder() + .put(OLDER_VERSION, wait1SecondMigration) + .put(CURRENT_VERSION, wait1SecondMigration) + .put(LATEST_VERSION, wait1SecondMigration) + .build(); + testee = new CassandraMigrationService(new InMemorySchemaDAO(OLDER_VERSION), allMigrationClazz, LATEST_VERSION); + + // When I perform a concurrent migration + AtomicInteger encounteredExceptionCount = new AtomicInteger(0); + executorService.submit(() -> testee.upgradeToVersion(LATEST_VERSION)); + executorService.submit(() -> { + try { + Thread.sleep(500); + } catch (InterruptedException e) { + throw Throwables.propagate(e); + } + + try { + testee.upgradeToVersion(LATEST_VERSION); + } catch (IllegalStateException e) { + encounteredExceptionCount.incrementAndGet(); + } + }); + executorService.awaitTermination(10, TimeUnit.SECONDS); + + // Then the second migration fails + assertThat(encounteredExceptionCount.get()).isEqualTo(1); + } + + @Test + public void partialMigrationShouldThrow() throws Exception { + Migration migration1 = mock(Migration.class); + when(migration1.run()).thenReturn(MIGRATION_FAILED); + Migration migration2 = successfulMigration; + + Map<Integer, Migration> allMigrationClazz = ImmutableMap.<Integer, Migration>builder() + .put(OLDER_VERSION, migration1) + .put(CURRENT_VERSION, migration2) + .build(); + testee = new CassandraMigrationService(new InMemorySchemaDAO(OLDER_VERSION), allMigrationClazz, LATEST_VERSION); + + expectedException.expect(MigrationException.class); + + testee.upgradeToVersion(LATEST_VERSION); + } + + @Test + public void partialMigrationShouldAbortMigrations() throws Exception { + Migration migration1 = mock(Migration.class); + when(migration1.run()).thenReturn(MIGRATION_FAILED); + Migration migration2 = mock(Migration.class); + when(migration2.run()).thenReturn(MIGRATED); + + Map<Integer, Migration> allMigrationClazz = ImmutableMap.<Integer, Migration>builder() + .put(OLDER_VERSION, migration1) + .put(CURRENT_VERSION, migration2) + .build(); + testee = new CassandraMigrationService(new InMemorySchemaDAO(OLDER_VERSION), allMigrationClazz, LATEST_VERSION); + + expectedException.expect(MigrationException.class); + + try { + testee.upgradeToVersion(LATEST_VERSION); + } finally { + verify(migration1, times(1)).run(); + verifyNoMoreInteractions(migration1); + verifyZeroInteractions(migration2); + } + } + + public static class InMemorySchemaDAO extends CassandraSchemaVersionDAO { + private int currentVersion; + + public InMemorySchemaDAO(int currentVersion) { + super(mock(Session.class), null); + this.currentVersion = currentVersion; + } + + @Override + public CompletableFuture<Optional<Integer>> getCurrentSchemaVersion() { + return CompletableFuture.completedFuture(Optional.of(currentVersion)); + } + + @Override + public CompletableFuture<Void> updateVersion(int newVersion) { + currentVersion = newVersion; + return CompletableFuture.completedFuture(null); + } + } +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
