This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 2225bd5dda05b8a6023cf174b09a4174a4fa598d Author: Benoit Tellier <btell...@linagora.com> AuthorDate: Wed Jul 10 10:36:27 2019 +0200 JAMES-2810 Implement StorageInformation DAO This allow a quick retrieval of storage information for a given user message and will be used as a building block of the CassandraDeletedMessageMetadataVault --- .../deleted-messages-vault-cassandra/pom.xml | 74 ++++++++++++++ .../metadata/DeletedMessageMetadataModule.java | 53 ++++++++++ .../vault/metadata/StorageInformationDAO.java | 102 +++++++++++++++++++ .../vault/metadata/StorageInformationDAOTest.java | 113 +++++++++++++++++++++ 4 files changed, 342 insertions(+) diff --git a/mailbox/plugin/deleted-messages-vault-cassandra/pom.xml b/mailbox/plugin/deleted-messages-vault-cassandra/pom.xml new file mode 100644 index 0000000..5f2e5cd --- /dev/null +++ b/mailbox/plugin/deleted-messages-vault-cassandra/pom.xml @@ -0,0 +1,74 @@ +<?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/xsd/maven-4.0.0.xsd"> + <modelVersion>4.0.0</modelVersion> + <parent> + <artifactId>apache-james-mailbox</artifactId> + <groupId>org.apache.james</groupId> + <version>3.4.0-SNAPSHOT</version> + <relativePath>../../pom.xml</relativePath> + </parent> + + <artifactId>apache-james-mailbox-deleted-messages-vault-cassandra</artifactId> + <name>Apache James :: Mailbox :: Plugin :: Deleted Messages Vault :: Cassandra</name> + <description>Apache James Mailbox Deleted Messages Vault metadata on top of Cassandra</description> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>apache-james-backends-cassandra</artifactId> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>apache-james-backends-cassandra</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>apache-james-mailbox-api</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>apache-james-mailbox-deleted-messages-vault</artifactId> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + +</project> \ No newline at end of file diff --git a/mailbox/plugin/deleted-messages-vault-cassandra/src/main/java/org/apache/james/vault/metadata/DeletedMessageMetadataModule.java b/mailbox/plugin/deleted-messages-vault-cassandra/src/main/java/org/apache/james/vault/metadata/DeletedMessageMetadataModule.java new file mode 100644 index 0000000..6fff8a9 --- /dev/null +++ b/mailbox/plugin/deleted-messages-vault-cassandra/src/main/java/org/apache/james/vault/metadata/DeletedMessageMetadataModule.java @@ -0,0 +1,53 @@ +/**************************************************************** + * 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.vault.metadata; + +import static com.datastax.driver.core.DataType.text; + +import org.apache.james.backends.cassandra.components.CassandraModule; + +import com.datastax.driver.core.schemabuilder.SchemaBuilder; + +public interface DeletedMessageMetadataModule { + + interface StorageInformationTable { + String TABLE = "storageInformation"; + + String OWNER = "owner"; + String MESSAGE_ID = "messageId"; + String BUCKET_NAME = "bucketName"; + String BLOB_ID = "blobId"; + } + + CassandraModule MODULE = CassandraModule + .builder() + + .table(StorageInformationTable.TABLE) + .comment("Holds storage information for deleted messages in the BlobStore based DeletedMessages vault") + .options(options -> options + .caching(SchemaBuilder.KeyCaching.ALL, SchemaBuilder.noRows())) + .statement(statement -> statement + .addPartitionKey(StorageInformationTable.OWNER, text()) + .addPartitionKey(StorageInformationTable.MESSAGE_ID, text()) + .addColumn(StorageInformationTable.BUCKET_NAME, text()) + .addColumn(StorageInformationTable.BLOB_ID, text())) + + .build(); +} diff --git a/mailbox/plugin/deleted-messages-vault-cassandra/src/main/java/org/apache/james/vault/metadata/StorageInformationDAO.java b/mailbox/plugin/deleted-messages-vault-cassandra/src/main/java/org/apache/james/vault/metadata/StorageInformationDAO.java new file mode 100644 index 0000000..e434a8b --- /dev/null +++ b/mailbox/plugin/deleted-messages-vault-cassandra/src/main/java/org/apache/james/vault/metadata/StorageInformationDAO.java @@ -0,0 +1,102 @@ +/**************************************************************** + * 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.vault.metadata; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.bindMarker; +import static com.datastax.driver.core.querybuilder.QueryBuilder.delete; +import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static com.datastax.driver.core.querybuilder.QueryBuilder.insertInto; +import static com.datastax.driver.core.querybuilder.QueryBuilder.select; +import static org.apache.james.vault.metadata.DeletedMessageMetadataModule.StorageInformationTable.BLOB_ID; +import static org.apache.james.vault.metadata.DeletedMessageMetadataModule.StorageInformationTable.BUCKET_NAME; +import static org.apache.james.vault.metadata.DeletedMessageMetadataModule.StorageInformationTable.MESSAGE_ID; +import static org.apache.james.vault.metadata.DeletedMessageMetadataModule.StorageInformationTable.OWNER; +import static org.apache.james.vault.metadata.DeletedMessageMetadataModule.StorageInformationTable.TABLE; + +import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor; +import org.apache.james.blob.api.BlobId; +import org.apache.james.blob.api.BucketName; +import org.apache.james.core.User; +import org.apache.james.mailbox.model.MessageId; + +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Session; + +import reactor.core.publisher.Mono; + +class StorageInformationDAO { + private final CassandraAsyncExecutor cassandraAsyncExecutor; + private final PreparedStatement addStatement; + private final PreparedStatement removeStatement; + private final PreparedStatement readStatement; + private final BlobId.Factory blobIdFactory; + + StorageInformationDAO(Session session, BlobId.Factory blobIdFactory) { + this.cassandraAsyncExecutor = new CassandraAsyncExecutor(session); + this.addStatement = prepareAdd(session); + this.removeStatement = prepareRemove(session); + this.readStatement = prepareRead(session); + this.blobIdFactory = blobIdFactory; + } + + private PreparedStatement prepareRead(Session session) { + return session.prepare(select(BUCKET_NAME, BLOB_ID) + .from(TABLE) + .where(eq(OWNER, bindMarker(OWNER))) + .and(eq(MESSAGE_ID, bindMarker(MESSAGE_ID)))); + } + + private PreparedStatement prepareRemove(Session session) { + return session.prepare(delete().from(TABLE) + .where(eq(OWNER, bindMarker(OWNER))) + .and(eq(MESSAGE_ID, bindMarker(MESSAGE_ID)))); + } + + private PreparedStatement prepareAdd(Session session) { + return session.prepare(insertInto(TABLE) + .value(OWNER, bindMarker(OWNER)) + .value(MESSAGE_ID, bindMarker(MESSAGE_ID)) + .value(BUCKET_NAME, bindMarker(BUCKET_NAME)) + .value(BLOB_ID, bindMarker(BLOB_ID))); + } + + Mono<Void> referenceStorageInformation(User user, MessageId messageId, StorageInformation storageInformation) { + return cassandraAsyncExecutor.executeVoid(addStatement.bind() + .setString(OWNER, user.asString()) + .setString(MESSAGE_ID, messageId.serialize()) + .setString(BUCKET_NAME, storageInformation.getBucketName().asString()) + .setString(BLOB_ID, storageInformation.getBlobId().asString())); + } + + Mono<Void> deleteStorageInformation(User user, MessageId messageId) { + return cassandraAsyncExecutor.executeVoid(removeStatement.bind() + .setString(OWNER, user.asString()) + .setString(MESSAGE_ID, messageId.serialize())); + } + + Mono<StorageInformation> retrieveStorageInformation(User user, MessageId messageId) { + return cassandraAsyncExecutor.executeSingleRow(readStatement.bind() + .setString(OWNER, user.asString()) + .setString(MESSAGE_ID, messageId.serialize())) + .map(row -> StorageInformation.builder() + .bucketName(BucketName.of(row.getString(BUCKET_NAME))) + .blobId(blobIdFactory.from(row.getString(BLOB_ID)))); + } +} diff --git a/mailbox/plugin/deleted-messages-vault-cassandra/src/test/java/org/apache/james/vault/metadata/StorageInformationDAOTest.java b/mailbox/plugin/deleted-messages-vault-cassandra/src/test/java/org/apache/james/vault/metadata/StorageInformationDAOTest.java new file mode 100644 index 0000000..3a80502 --- /dev/null +++ b/mailbox/plugin/deleted-messages-vault-cassandra/src/test/java/org/apache/james/vault/metadata/StorageInformationDAOTest.java @@ -0,0 +1,113 @@ +/**************************************************************** + * 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.vault.metadata; + +import static org.apache.james.vault.metadata.DeletedMessageMetadataModule.MODULE; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; + +import java.util.Optional; + +import org.apache.james.backends.cassandra.CassandraCluster; +import org.apache.james.backends.cassandra.CassandraClusterExtension; +import org.apache.james.blob.api.BlobId; +import org.apache.james.blob.api.BucketName; +import org.apache.james.blob.api.HashBlobId; +import org.apache.james.core.User; +import org.apache.james.mailbox.model.TestMessageId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +class StorageInformationDAOTest { + private static final BucketName BUCKET_NAME = BucketName.of("deletedMessages-2019-06-01"); + private static final BucketName BUCKET_NAME_2 = BucketName.of("deletedMessages-2019-07-01"); + private static final HashBlobId.Factory BLOB_ID_FACTORY = new HashBlobId.Factory(); + private static final User OWNER = User.fromUsername("owner"); + private static final TestMessageId MESSAGE_ID = TestMessageId.of(36); + private static final BlobId BLOB_ID = new HashBlobId.Factory().from("05dcb33b-8382-4744-923a-bc593ad84d23"); + private static final BlobId BLOB_ID_2 = new HashBlobId.Factory().from("05dcb33b-8382-4744-923a-bc593ad84d24"); + private static final StorageInformation STORAGE_INFORMATION = StorageInformation.builder().bucketName(BUCKET_NAME).blobId(BLOB_ID); + private static final StorageInformation STORAGE_INFORMATION_2 = StorageInformation.builder().bucketName(BUCKET_NAME_2).blobId(BLOB_ID_2); + + @RegisterExtension + static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(MODULE); + + private StorageInformationDAO testee; + + @BeforeEach + void setUp(CassandraCluster cassandra) { + testee = new StorageInformationDAO(cassandra.getConf(), BLOB_ID_FACTORY); + } + + @Test + void retrieveStorageInformationShouldReturnEmptyWhenNone() { + Optional<StorageInformation> storageInformation = testee.retrieveStorageInformation(OWNER, MESSAGE_ID).blockOptional(); + + assertThat(storageInformation).isEmpty(); + } + + @Test + void retrieveStorageInformationShouldReturnAddedValue() { + testee.referenceStorageInformation(OWNER, MESSAGE_ID, STORAGE_INFORMATION).block(); + + Optional<StorageInformation> storageInformation = testee.retrieveStorageInformation(OWNER, MESSAGE_ID).blockOptional(); + assertThat(storageInformation).contains(STORAGE_INFORMATION); + } + + @Test + void retrieveStorageInformationShouldReturnLatestAddedValue() { + testee.referenceStorageInformation(OWNER, MESSAGE_ID, STORAGE_INFORMATION).block(); + + testee.referenceStorageInformation(OWNER, MESSAGE_ID, STORAGE_INFORMATION_2).block(); + + Optional<StorageInformation> storageInformation = testee.retrieveStorageInformation(OWNER, MESSAGE_ID).blockOptional(); + assertThat(storageInformation).contains(STORAGE_INFORMATION_2); + } + + @Test + void retrieveStorageInformationShouldReturnEmptyWhenDeleted() { + testee.referenceStorageInformation(OWNER, MESSAGE_ID, STORAGE_INFORMATION).block(); + + testee.deleteStorageInformation(OWNER, MESSAGE_ID).block(); + + Optional<StorageInformation> storageInformation = testee.retrieveStorageInformation(OWNER, MESSAGE_ID).blockOptional(); + assertThat(storageInformation).isEmpty(); + } + + + @Test + void referenceStorageInformationShouldBeAllowedAfterADelete() { + testee.referenceStorageInformation(OWNER, MESSAGE_ID, STORAGE_INFORMATION).block(); + + testee.deleteStorageInformation(OWNER, MESSAGE_ID).block(); + + testee.referenceStorageInformation(OWNER, MESSAGE_ID, STORAGE_INFORMATION).block(); + + Optional<StorageInformation> storageInformation = testee.retrieveStorageInformation(OWNER, MESSAGE_ID).blockOptional(); + assertThat(storageInformation).contains(STORAGE_INFORMATION); + } + + @Test + void deleteStorageInformationShouldNotThrowWhenNone() { + assertThatCode(() -> testee.deleteStorageInformation(OWNER, MESSAGE_ID).block()) + .doesNotThrowAnyException(); + } +} \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org