JAMES-1925 Introduce CassandraMailboxDAO and its tests
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/6cee1367 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/6cee1367 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/6cee1367 Branch: refs/heads/master Commit: 6cee1367fc90eea000fd77013e91891feaf3d5c3 Parents: 3c2dc46 Author: Benoit Tellier <[email protected]> Authored: Tue Feb 14 11:23:00 2017 +0700 Committer: Antoine Duprat <[email protected]> Committed: Wed Feb 15 13:12:38 2017 +0100 ---------------------------------------------------------------------- .../cassandra/mail/CassandraMailboxDAO.java | 186 +++++++++++++++++++ .../mail/utils/MailboxBaseTupleUtil.java | 40 ++++ .../cassandra/mail/CassandraMailboxDAOTest.java | 169 +++++++++++++++++ 3 files changed, 395 insertions(+) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/6cee1367/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxDAO.java ---------------------------------------------------------------------- diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxDAO.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxDAO.java new file mode 100644 index 0000000..8b5868a --- /dev/null +++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxDAO.java @@ -0,0 +1,186 @@ +/**************************************************************** + * 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.mailbox.cassandra.mail; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.bindMarker; +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 com.datastax.driver.core.querybuilder.QueryBuilder.set; +import static com.datastax.driver.core.querybuilder.QueryBuilder.update; +import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.FIELDS; +import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.ID; +import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.MAILBOX_BASE; +import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.NAME; +import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.PATH; +import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.TABLE_NAME; +import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.UIDVALIDITY; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Stream; + +import org.apache.james.backends.cassandra.init.CassandraTypesProvider; +import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor; +import org.apache.james.backends.cassandra.utils.CassandraUtils; +import org.apache.james.mailbox.cassandra.CassandraId; +import org.apache.james.mailbox.cassandra.mail.utils.MailboxBaseTupleUtil; +import org.apache.james.mailbox.cassandra.table.CassandraMailboxTable; +import org.apache.james.mailbox.model.MailboxACL; +import org.apache.james.mailbox.model.MailboxPath; +import org.apache.james.mailbox.store.mail.model.Mailbox; +import org.apache.james.mailbox.store.mail.model.impl.SimpleMailbox; +import org.apache.james.util.CompletableFutureUtil; + +import com.datastax.driver.core.PreparedStatement; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; +import com.datastax.driver.core.querybuilder.QueryBuilder; + +public class CassandraMailboxDAO { + + private final CassandraAsyncExecutor executor; + private final MailboxBaseTupleUtil mailboxBaseTupleUtil; + private final Session session; + private final int maxAclRetry; + private final PreparedStatement readStatement; + private final PreparedStatement listStatement; + private final PreparedStatement deleteStatement; + private final PreparedStatement insertStatement; + private final PreparedStatement updateStatement; + + public CassandraMailboxDAO(Session session, CassandraTypesProvider typesProvider, int maxAclRetry) { + this.executor = new CassandraAsyncExecutor(session); + this.mailboxBaseTupleUtil = new MailboxBaseTupleUtil(typesProvider); + this.session = session; + this.insertStatement = prepareInsert(session); + this.updateStatement = prepareUpdate(session); + this.deleteStatement = prepareDelete(session); + this.listStatement = prepareList(session); + this.readStatement = prepareRead(session); + this.maxAclRetry = maxAclRetry; + } + + private PreparedStatement prepareInsert(Session session) { + return session.prepare(insertInto(TABLE_NAME) + .value(ID, bindMarker(ID)) + .value(NAME, bindMarker(NAME)) + .value(UIDVALIDITY, bindMarker(UIDVALIDITY)) + .value(MAILBOX_BASE, bindMarker(MAILBOX_BASE)) + .value(PATH, bindMarker(PATH))); + } + + private PreparedStatement prepareUpdate(Session session) { + return session.prepare(update(TABLE_NAME) + .with(set(MAILBOX_BASE, bindMarker(MAILBOX_BASE))) + .and(set(NAME, bindMarker(NAME))) + .and(set(PATH, bindMarker(PATH))) + .where(eq(ID, bindMarker(ID)))); + } + + private PreparedStatement prepareDelete(Session session) { + return session.prepare(QueryBuilder.delete() + .from(TABLE_NAME) + .where(eq(ID, bindMarker(ID)))); + } + + private PreparedStatement prepareList(Session session) { + return session.prepare(select(FIELDS).from(TABLE_NAME)); + } + + private PreparedStatement prepareRead(Session session) { + return session.prepare(select(FIELDS).from(TABLE_NAME) + .where(eq(ID, bindMarker(ID)))); + } + + public CompletableFuture<Void> save(Mailbox mailbox) { + CassandraId cassandraId = (CassandraId) mailbox.getMailboxId(); + return executor.executeVoid(insertStatement.bind() + .setUUID(ID, cassandraId.asUuid()) + .setString(NAME, mailbox.getName()) + .setLong(UIDVALIDITY, mailbox.getUidValidity()) + .setUDTValue(MAILBOX_BASE, mailboxBaseTupleUtil.createMailboxBaseUDT(mailbox.getNamespace(), mailbox.getUser())) + .setString(PATH, mailbox.generateAssociatedPath().asString())); + } + + public CompletableFuture<Void> updatePath(CassandraId mailboxId, MailboxPath mailboxPath) { + return executor.executeVoid(updateStatement.bind() + .setUUID(ID, mailboxId.asUuid()) + .setString(PATH, mailboxPath.asString()) + .setString(NAME, mailboxPath.getName()) + .setUDTValue(MAILBOX_BASE, mailboxBaseTupleUtil.createMailboxBaseUDT(mailboxPath.getNamespace(), mailboxPath.getUser()))); + } + + public CompletableFuture<Void> delete(CassandraId mailboxId) { + return executor.executeVoid(deleteStatement.bind() + .setUUID(ID, mailboxId.asUuid())); + } + + public CompletableFuture<Optional<SimpleMailbox>> retrieveMailbox(CassandraId mailboxId) { + return mailbox(mailboxId, + executor.executeSingleRow(readStatement.bind() + .setUUID(ID, mailboxId.asUuid()))); + } + + private CompletableFuture<Optional<SimpleMailbox>> mailbox(CassandraId cassandraId, CompletableFuture<Optional<Row>> rowFuture) { + CompletableFuture<MailboxACL> aclCompletableFuture = new CassandraACLMapper(cassandraId, session, maxAclRetry).getACL(); + return rowFuture.thenApply(rowOptional -> rowOptional.map(this::mailboxFromRow)) + .thenApply(mailboxOptional -> { + mailboxOptional.ifPresent(mailbox -> mailbox.setMailboxId(cassandraId)); + return mailboxOptional; + }) + .thenCompose(mailboxOptional -> aclCompletableFuture.thenApply(acl -> { + mailboxOptional.ifPresent(mailbox -> mailbox.setACL(acl)); + return mailboxOptional; + })); + } + + private SimpleMailbox mailboxFromRow(Row row) { + return new SimpleMailbox( + new MailboxPath( + row.getUDTValue(MAILBOX_BASE).getString(CassandraMailboxTable.MailboxBase.NAMESPACE), + row.getUDTValue(MAILBOX_BASE).getString(CassandraMailboxTable.MailboxBase.USER), + row.getString(NAME)), + row.getLong(UIDVALIDITY)); + } + + public CompletableFuture<Stream<SimpleMailbox>> retrieveAllMailboxes() { + return executor.execute(listStatement.bind()) + .thenApply(CassandraUtils::convertToStream) + .thenApply(stream -> stream.map(this::toMailboxWithId)) + .thenCompose(stream -> CompletableFutureUtil.allOf(stream.map(this::toMailboxWithAclFuture))); + } + + private SimpleMailbox toMailboxWithId(Row row) { + SimpleMailbox mailbox = mailboxFromRow(row); + mailbox.setMailboxId(CassandraId.of(row.getUUID(ID))); + return mailbox; + } + + private CompletableFuture<SimpleMailbox> toMailboxWithAclFuture(SimpleMailbox mailbox) { + CassandraId cassandraId = (CassandraId) mailbox.getMailboxId(); + return new CassandraACLMapper(cassandraId, session, maxAclRetry).getACL() + .thenApply(acl -> { + mailbox.setACL(acl); + return mailbox; + }); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/6cee1367/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/utils/MailboxBaseTupleUtil.java ---------------------------------------------------------------------- diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/utils/MailboxBaseTupleUtil.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/utils/MailboxBaseTupleUtil.java new file mode 100644 index 0000000..3e65457 --- /dev/null +++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/utils/MailboxBaseTupleUtil.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.mailbox.cassandra.mail.utils; + +import org.apache.james.backends.cassandra.init.CassandraTypesProvider; +import org.apache.james.mailbox.cassandra.table.CassandraMailboxTable; + +import com.datastax.driver.core.UDTValue; + +public class MailboxBaseTupleUtil { + private final CassandraTypesProvider typesProvider; + + public MailboxBaseTupleUtil(CassandraTypesProvider typesProvider) { + this.typesProvider = typesProvider; + } + + public UDTValue createMailboxBaseUDT(String namespace, String user) { + return typesProvider.getDefinedUserType(CassandraMailboxTable.MAILBOX_BASE) + .newValue() + .setString(CassandraMailboxTable.MailboxBase.NAMESPACE, namespace) + .setString(CassandraMailboxTable.MailboxBase.USER, user); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/6cee1367/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxDAOTest.java ---------------------------------------------------------------------- diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxDAOTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxDAOTest.java new file mode 100644 index 0000000..6cc79e9 --- /dev/null +++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxDAOTest.java @@ -0,0 +1,169 @@ +/**************************************************************** + * 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.mailbox.cassandra.mail; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; +import java.util.Optional; + +import org.apache.james.backends.cassandra.CassandraCluster; +import org.apache.james.backends.cassandra.init.CassandraModuleComposite; +import org.apache.james.mailbox.cassandra.CassandraId; +import org.apache.james.mailbox.cassandra.modules.CassandraAclModule; +import org.apache.james.mailbox.cassandra.modules.CassandraMailboxModule; +import org.apache.james.mailbox.model.MailboxConstants; +import org.apache.james.mailbox.model.MailboxPath; +import org.apache.james.mailbox.store.mail.model.impl.SimpleMailbox; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.github.steveash.guavate.Guavate; + +public class CassandraMailboxDAOTest { + + public static final int MAX_ACL_RETRY = 10; + public static final int UID_VALIDITY_1 = 145; + public static final int UID_VALIDITY_2 = 147; + public static final MailboxPath NEW_MAILBOX_PATH = new MailboxPath(MailboxConstants.USER_NAMESPACE, "user", "xyz"); + public static CassandraId CASSANDRA_ID_1 = CassandraId.timeBased(); + public static CassandraId CASSANDRA_ID_2 = CassandraId.timeBased(); + private CassandraCluster cassandra; + private CassandraMailboxDAO testee; + private SimpleMailbox mailbox1; + private SimpleMailbox mailbox2; + + @Before + public void setUp() { + cassandra = CassandraCluster.create(new CassandraModuleComposite(new CassandraMailboxModule(), new CassandraAclModule())); + cassandra.ensureAllTables(); + + testee = new CassandraMailboxDAO(cassandra.getConf(), cassandra.getTypesProvider(), MAX_ACL_RETRY); + mailbox1 = new SimpleMailbox(new MailboxPath(MailboxConstants.USER_NAMESPACE, "user", "abcd"), + UID_VALIDITY_1, + CASSANDRA_ID_1); + mailbox2 = new SimpleMailbox(new MailboxPath(MailboxConstants.USER_NAMESPACE, "user", "defg"), + UID_VALIDITY_2, + CASSANDRA_ID_2); + } + + @After + public void tearDown() { + cassandra.clearAllTables(); + } + + @Test + public void retrieveMailboxShouldReturnEmptyWhenNone() { + Optional<SimpleMailbox> mailboxOptional = testee.retrieveMailbox(CASSANDRA_ID_1).join(); + assertThat(mailboxOptional.isPresent()).isFalse(); + } + + @Test + public void saveShouldAddAMailbox() { + testee.save(mailbox1).join(); + + Optional<SimpleMailbox> readMailbox = testee.retrieveMailbox(CASSANDRA_ID_1) + .join(); + assertThat(readMailbox.isPresent()).isTrue(); + assertThat(readMailbox.get()).isEqualToComparingFieldByField(mailbox1); + } + + @Test + public void saveShouldOverride() { + testee.save(mailbox1).join(); + + mailbox2.setMailboxId(CASSANDRA_ID_1); + testee.save(mailbox2).join(); + + + Optional<SimpleMailbox> readMailbox = testee.retrieveMailbox(CASSANDRA_ID_1) + .join(); + assertThat(readMailbox.isPresent()).isTrue(); + assertThat(readMailbox.get()).isEqualToComparingFieldByField(mailbox2); + } + + @Test + public void retrieveAllMailboxesShouldBeEmptyByDefault() { + List<SimpleMailbox> mailboxes = testee.retrieveAllMailboxes() + .join() + .collect(Guavate.toImmutableList()); + + assertThat(mailboxes).isEmpty(); + } + + @Test + public void retrieveAllMailboxesShouldReturnSingleMailbox() { + testee.save(mailbox1).join(); + + List<SimpleMailbox> mailboxes = testee.retrieveAllMailboxes() + .join() + .collect(Guavate.toImmutableList()); + + assertThat(mailboxes).containsOnly(mailbox1); + } + + @Test + public void retrieveAllMailboxesShouldReturnMultiMailboxes() { + testee.save(mailbox1).join(); + testee.save(mailbox2).join(); + + List<SimpleMailbox> mailboxes = testee.retrieveAllMailboxes() + .join() + .collect(Guavate.toImmutableList()); + + assertThat(mailboxes).containsOnly(mailbox1, mailbox2); + } + + @Test + public void deleteShouldNotFailWhenMailboxIsAbsent() { + testee.delete(CASSANDRA_ID_1).join(); + } + + @Test + public void deleteShouldRemoveExistingMailbox() { + testee.save(mailbox1).join(); + + testee.delete(CASSANDRA_ID_1).join(); + + Optional<SimpleMailbox> mailboxOptional = testee.retrieveMailbox(CASSANDRA_ID_1).join(); + assertThat(mailboxOptional.isPresent()).isFalse(); + } + + @Test + public void updateShouldNotFailWhenMailboxIsAbsent() { + testee.updatePath(CASSANDRA_ID_1, NEW_MAILBOX_PATH).join(); + } + + @Test + public void updateShouldChangeMailboxPath() { + testee.save(mailbox1).join(); + + testee.updatePath(CASSANDRA_ID_1, NEW_MAILBOX_PATH).join(); + + mailbox1.setNamespace(NEW_MAILBOX_PATH.getNamespace()); + mailbox1.setUser(NEW_MAILBOX_PATH.getUser()); + mailbox1.setName(NEW_MAILBOX_PATH.getName()); + Optional<SimpleMailbox> readMailbox = testee.retrieveMailbox(CASSANDRA_ID_1) + .join(); + assertThat(readMailbox.isPresent()).isTrue(); + assertThat(readMailbox.get()).isEqualToComparingFieldByField(mailbox1); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
