Hi, I implemented several improvments for the Cassandra mailbox.
It includes :
- A CassandraGroupMembershipResolver : a distributed Group Membership
resolver using Cassandra as a backend.
- ACL support :
- I overloaded the simplemailbox to store and retrie acl in cassandra
- I stock those ACL in cassandra in JSon format, so there is an ACL
to JSOn converter using Jackson.
- A distributed quota management system based on Cassandra. It have
the same architecture than what is done in the store for quota
management, including :
- A CassandraPerUserQuotaManager : fix quota policies by user
- A CassandraFixedQuotaManager : fix global quota policies
- These are based on a CassandraListeningQuotaManager, so thats its
updated for all received e-mail.
- All these classes uses a CassandraQuotaStorage class to work with
Cassandra, so that storage operations are centralised in a class.
You will find these improvments in the attached patch.
Sincerly yours,
Benoit Tellier
diff -uNr james-mailbox/README.md james-mailbox-new/README.md --- james-mailbox/README.md 2014-12-17 16:04:47.596753249 +0100 +++ james-mailbox-new/README.md 2014-12-17 17:09:55.650255631 +0100 @@ -13,6 +13,7 @@ ~~~ |-- api -- Mailbox API +|-- cassandra -- Mailbox implementation over Cassandra |-- hbase -- Mailbox implementation over HBase |-- jcr -- Mailbox implementation over Java Content Repository (JCR) |-- jpa -- Database Mailbox implementation using Java Persistence API @@ -61,6 +62,15 @@ * distributed SMTP/IMAP access * other +Mailbox Cassandra +================= + +Uses Apache Cassandra (http://cassandra.apache.org/) for storing email messages. Provides a scalable email storage. To have a fully +distributed email server you will also need, among others: + +* distributed SMTP/IMAP access +* other + Zookeeper Sequence Provider ========================== diff -uNr james-mailbox/cassandra/pom.xml james-mailbox-new/cassandra/pom.xml --- james-mailbox/cassandra/pom.xml 2014-12-16 17:49:32.729928977 +0100 +++ james-mailbox-new/cassandra/pom.xml 2014-12-17 17:14:11.796933378 +0100 @@ -70,6 +70,11 @@ <version>${cassandra-driver-core.version}</version> </dependency> <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + <version>2.3.3</version> + </dependency> + <dependency> <groupId>org.cassandraunit</groupId> <artifactId>cassandra-unit</artifactId> <version>${cassandra-unit.version}</version> diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraGroupMembershipResolver.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraGroupMembershipResolver.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraGroupMembershipResolver.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraGroupMembershipResolver.java 2014-12-17 17:09:55.650255631 +0100 @@ -0,0 +1,78 @@ +/**************************************************************** + * 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; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Session; +import org.apache.james.mailbox.acl.GroupMembershipResolver; +import org.apache.james.mailbox.cassandra.table.CassandraGroupMembershipTable; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.*; + + +/** + * A group membership resolver with data stored in Cassandra + */ +public class CassandraGroupMembershipResolver implements GroupMembershipResolver { + + Session session; + + public CassandraGroupMembershipResolver(Session session) { + this.session = session; + } + + public void addMembership(String group, String user) { + session.execute( + insertInto(CassandraGroupMembershipTable.TABLE) + .value(CassandraGroupMembershipTable.GROUP, group) + .value(CassandraGroupMembershipTable.USER, user) + .ifNotExists() + ); + } + + public void removeMembership(String group, String user) { + session.execute( + delete() + .from(CassandraGroupMembershipTable.TABLE) + .where( + eq(CassandraGroupMembershipTable.GROUP, group) + ) + .and( + eq(CassandraGroupMembershipTable.USER, user) + ) + ); + } + + @Override + public boolean isMember(String user, String group) { + ResultSet resultSet = session.execute( + select(CassandraGroupMembershipTable.USER) + .from(CassandraGroupMembershipTable.TABLE) + .where( + eq(CassandraGroupMembershipTable.USER, user) + ) + .and( + eq(CassandraGroupMembershipTable.GROUP, group) + ) + ); + return !resultSet.isExhausted(); + } + + +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxManager.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxManager.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxManager.java 2014-12-16 17:49:32.063262337 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxManager.java 2014-12-17 17:14:11.796933378 +0100 @@ -25,13 +25,13 @@ import org.apache.james.mailbox.MailboxSession; import org.apache.james.mailbox.acl.SimpleGroupMembershipResolver; import org.apache.james.mailbox.acl.UnionMailboxACLResolver; +import org.apache.james.mailbox.cassandra.mail.model.CassandraMailbox; import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.model.MailboxPath; import org.apache.james.mailbox.store.Authenticator; import org.apache.james.mailbox.store.StoreMailboxManager; import org.apache.james.mailbox.store.StoreMessageManager; import org.apache.james.mailbox.store.mail.model.Mailbox; -import org.apache.james.mailbox.store.mail.model.impl.SimpleMailbox; /** * Cassandra implementation of {@link StoreMailboxManager} @@ -46,7 +46,8 @@ @Override protected Mailbox<UUID> doCreateMailbox(MailboxPath mailboxPath, MailboxSession session) throws MailboxException { - return new SimpleMailbox<UUID>(mailboxPath, randomUidValidity()); + CassandraMailboxSessionMapperFactory cassandraMailboxSessionMapperFactory = (CassandraMailboxSessionMapperFactory)getMapperFactory(); + return new CassandraMailbox<UUID>(mailboxPath, randomUidValidity(), cassandraMailboxSessionMapperFactory.getSession()); } @Override diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java 2014-12-16 17:49:32.073262337 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java 2014-12-17 17:14:11.796933378 +0100 @@ -71,4 +71,8 @@ public UidProvider<UUID> getUidProvider() { return uidProvider; } + + Session getSession() { + return session; + } } diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMessageManager.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMessageManager.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMessageManager.java 2014-12-16 17:49:32.079929004 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMessageManager.java 2014-12-17 17:14:11.796933378 +0100 @@ -27,12 +27,18 @@ import org.apache.james.mailbox.MailboxSession; import org.apache.james.mailbox.acl.SimpleGroupMembershipResolver; import org.apache.james.mailbox.acl.UnionMailboxACLResolver; +import org.apache.james.mailbox.cassandra.mail.model.CassandraMailbox; import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.exception.UnsupportedRightException; +import org.apache.james.mailbox.model.MailboxACL; +import org.apache.james.mailbox.model.SimpleMailboxACL; import org.apache.james.mailbox.store.MailboxEventDispatcher; import org.apache.james.mailbox.store.MailboxSessionMapperFactory; import org.apache.james.mailbox.store.StoreMessageManager; import org.apache.james.mailbox.store.mail.model.Mailbox; import org.apache.james.mailbox.store.search.MessageSearchIndex; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Cassandra implementation of {@link StoreMessageManager} @@ -40,9 +46,18 @@ */ public class CassandraMessageManager extends StoreMessageManager<UUID> { - public CassandraMessageManager(MailboxSessionMapperFactory<UUID> mapperFactory, MessageSearchIndex<UUID> index, MailboxEventDispatcher<UUID> dispatcher, MailboxPathLocker locker, Mailbox<UUID> mailbox) throws MailboxException { + private static final int ACL_MAX_RETRY = 100000; + + private static final Logger LOG = LoggerFactory.getLogger(CassandraMessageManager.class); + private int aclMaxRetry; + + public CassandraMessageManager(MailboxSessionMapperFactory<UUID> mapperFactory, MessageSearchIndex<UUID> index, MailboxEventDispatcher<UUID> dispatcher, MailboxPathLocker locker, Mailbox<UUID> mailbox, int aclMaxRetry) throws MailboxException { super(mapperFactory, index, dispatcher, locker, mailbox, new UnionMailboxACLResolver(), new SimpleGroupMembershipResolver()); + this.aclMaxRetry = aclMaxRetry; + } + public CassandraMessageManager(MailboxSessionMapperFactory<UUID> mapperFactory, MessageSearchIndex<UUID> index, MailboxEventDispatcher<UUID> dispatcher, MailboxPathLocker locker, Mailbox<UUID> mailbox) throws MailboxException { + this(mapperFactory, index, dispatcher, locker, mailbox, ACL_MAX_RETRY); } /** @@ -54,4 +69,54 @@ flags.add(Flags.Flag.USER); return flags; } + + /** + * @throws UnsupportedRightException + * @see MessageManager#setRights(MailboxACL.MailboxACLEntryKey, MailboxACL.EditMode, MailboxACL.MailboxACLRights) (String, MailboxACL.EditMode, MailboxACL.MailboxACLRights) + */ + @Override + public void setRights(MailboxACL.MailboxACLEntryKey mailboxACLEntryKey, MailboxACL.EditMode editMode, MailboxACL.MailboxACLRights mailboxAclRights) throws UnsupportedRightException { + boolean succeeded = false; + CassandraMailbox<UUID> cassandraMailbox; + try { + cassandraMailbox = (CassandraMailbox<UUID>) getMailboxEntity(); + } catch(MailboxException me) { + LOG.error("Can not retrieve mailbox while setting ACLs"); + return; + } + int tries = 0; + while(!succeeded && tries < aclMaxRetry) { + tries++; + succeeded = tryToUpdateACL(mailboxACLEntryKey, editMode, mailboxAclRights, cassandraMailbox); + } + if(!succeeded) { + LOG.error("Can not apply lightweight transaction for setting ACL rights on " + cassandraMailbox.getUser() + + ":" + cassandraMailbox.getNamespace() + ":" + cassandraMailbox.getName() + " after " + tries + " tries."); + } + } + + private boolean tryToUpdateACL(MailboxACL.MailboxACLEntryKey mailboxACLEntryKey, MailboxACL.EditMode editMode, MailboxACL.MailboxACLRights mailboxAclRights, CassandraMailbox<UUID> cassandraMailbox) throws UnsupportedRightException { + boolean succeeded; + CassandraMailbox<UUID>.ACLWithVersion aclWithVersion = cassandraMailbox.getACLWithVersion(); + MailboxACL acl = aclWithVersion.acl; + if (acl == null ) { + acl = SimpleMailboxACL.EMPTY; + } + acl = manageMailboxACL(mailboxACLEntryKey, editMode, mailboxAclRights, acl); + succeeded = cassandraMailbox.setACLWithVersion(acl, aclWithVersion.version); + return succeeded; + } + + private MailboxACL manageMailboxACL(MailboxACL.MailboxACLEntryKey mailboxACLEntryKey, MailboxACL.EditMode editMode, MailboxACL.MailboxACLRights mailboxAclRights, MailboxACL acl) throws UnsupportedRightException { + switch (editMode) { + case ADD: + return acl.union(mailboxACLEntryKey, mailboxAclRights); + case REMOVE: + return acl.except(mailboxACLEntryKey, mailboxAclRights); + case REPLACE: + return acl.replace(mailboxACLEntryKey, mailboxAclRights); + default: + throw new IllegalStateException("Unexpected " + MailboxACL.EditMode.class.getName() + "." + editMode); + } + } } diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraSession.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraSession.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraSession.java 2014-12-17 16:04:47.600086583 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraSession.java 2014-12-17 17:14:11.803600045 +0100 @@ -31,7 +31,7 @@ /** * A Cassandra session with the default keyspace - * + * */ public class CassandraSession implements Session { private final static String DEFAULT_CLUSTER_IP = "localhost"; @@ -52,13 +52,17 @@ private void initDatabase(Cluster cluster, String keyspace, int replicationFactor) { session = cluster.connect(); session.execute("CREATE KEYSPACE IF NOT EXISTS " + keyspace + " WITH replication " + "= {'class':'SimpleStrategy', 'replication_factor':" + replicationFactor + "};"); - session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".mailbox (" + "id uuid PRIMARY KEY," + "name text, namespace text," + "uidvalidity bigint," + "user text," + "path text" + ");"); + session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".mailbox (" + "id uuid PRIMARY KEY," + "name text, namespace text," + "uidvalidity bigint," + "user text," + "path text," + "acl text," + "acl_version bigint," + ");"); session.execute("CREATE INDEX IF NOT EXISTS ON " + keyspace + ".mailbox(path);"); session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".messageCounter (" + "mailboxId UUID PRIMARY KEY," + "nextUid bigint," + ");"); session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".mailboxCounters (" + "mailboxId UUID PRIMARY KEY," + "count counter," + "unseen counter," + "nextModSeq counter" + ");"); session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".message (" + "mailboxId UUID," + "uid bigint," + "internalDate timestamp," + "bodyStartOctet int," + "content blob," + "modSeq bigint," + "mediaType text," + "subType text," + "fullContentOctets int," + "bodyOctets int," + "textualLineCount bigint," + "bodyContent blob," + "headerContent blob," + "flagAnswered boolean," + "flagDeleted boolean," + "flagDraft boolean," + "flagRecent boolean," + "flagSeen boolean," + "flagFlagged boolean," + "flagUser boolean," + "PRIMARY KEY (mailboxId, uid)" + ");"); session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".subscription (" + "user text," + "mailbox text," + "PRIMARY KEY (mailbox, user)" + ");"); + session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".quota (" + "user text PRIMARY KEY," + "size_quota counter," + "count_quota counter" + ");"); + session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".quota_max (" + "user text PRIMARY KEY," + "size_quota_max bigint," + "count_quota_max bigint" + ");"); + session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".default_max_quota_table (" + "id int PRIMARY KEY," + "default_max_size_quota bigint," + "default_max_count_quota bigint" + ");"); + session.execute("CREATE TABLE IF NOT EXISTS " + keyspace + ".group_membership ("+"user text,"+"group text,"+"PRIMARY KEY (user, group)"+");"); session.close(); } diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java 2014-12-17 16:04:47.600086583 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java 2014-12-17 17:14:11.800266711 +0100 @@ -22,7 +22,6 @@ 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.mailbox.cassandra.table.CassandraMailboxTable.FIELDS; import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.ID; import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.NAME; @@ -31,16 +30,18 @@ import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.TABLE_NAME; import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.UIDVALIDITY; import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.USER; +import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.ACL; +import static org.apache.james.mailbox.cassandra.table.CassandraMailboxTable.ACL_VERSION; import java.util.List; import java.util.UUID; +import org.apache.james.mailbox.cassandra.mail.model.CassandraMailbox; import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.exception.MailboxNotFoundException; import org.apache.james.mailbox.model.MailboxPath; import org.apache.james.mailbox.store.mail.MailboxMapper; import org.apache.james.mailbox.store.mail.model.Mailbox; -import org.apache.james.mailbox.store.mail.model.impl.SimpleMailbox; import com.datastax.driver.core.ResultSet; import com.datastax.driver.core.Row; @@ -49,6 +50,8 @@ import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList.Builder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Data access management for mailbox. @@ -76,8 +79,8 @@ } } - private SimpleMailbox<UUID> mailbox(Row row) { - SimpleMailbox<UUID> mailbox = new SimpleMailbox<UUID>(new MailboxPath(row.getString(NAMESPACE), row.getString(USER), row.getString(NAME)), row.getLong(UIDVALIDITY)); + private CassandraMailbox<UUID> mailbox(Row row) { + CassandraMailbox<UUID> mailbox = new CassandraMailbox<UUID>(new MailboxPath(row.getString(NAMESPACE), row.getString(USER), row.getString(NAME)), row.getLong(UIDVALIDITY), session); mailbox.setMailboxId(row.getUUID(ID)); return mailbox; } @@ -97,16 +100,16 @@ @Override public void save(Mailbox<UUID> mailbox) throws MailboxException { - Preconditions.checkArgument(mailbox instanceof SimpleMailbox<?>); - SimpleMailbox<UUID> simpleMailbox = (SimpleMailbox<UUID>) mailbox; - if (simpleMailbox.getMailboxId() == null) { - simpleMailbox.setMailboxId(UUID.randomUUID()); + Preconditions.checkArgument(mailbox instanceof CassandraMailbox); + CassandraMailbox<UUID> cassandraMailbox = (CassandraMailbox<UUID>) mailbox; + if (cassandraMailbox.getMailboxId() == null) { + cassandraMailbox.setMailboxId(UUID.randomUUID()); } - upsertMailbox(simpleMailbox); + upsertMailbox(cassandraMailbox); } - private void upsertMailbox(SimpleMailbox<UUID> mailbox) { - session.execute(insertInto(TABLE_NAME).value(ID, mailbox.getMailboxId()).value(NAME, mailbox.getName()).value(NAMESPACE, mailbox.getNamespace()).value(UIDVALIDITY, mailbox.getUidValidity()).value(USER, mailbox.getUser()).value(PATH, path(mailbox).toString())); + private void upsertMailbox(CassandraMailbox mailbox) { + session.execute(insertInto(TABLE_NAME).value(ID, mailbox.getMailboxId()).value(NAME, mailbox.getName()).value(NAMESPACE, mailbox.getNamespace()).value(UIDVALIDITY, mailbox.getUidValidity()).value(USER, mailbox.getUser()).value(PATH, path(mailbox).toString()).value(ACL, "{}").value(ACL_VERSION, 0)); } private MailboxPath path(Mailbox<?> mailbox) { diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/model/CassandraMailbox.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/model/CassandraMailbox.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/model/CassandraMailbox.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/model/CassandraMailbox.java 2014-12-17 17:14:11.800266711 +0100 @@ -0,0 +1,179 @@ +/**************************************************************** + * 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.model; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Row; +import com.datastax.driver.core.Session; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonMappingException; +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.model.SimpleMailboxACL; +import org.apache.james.mailbox.store.mail.model.impl.SimpleMailbox; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.select; +import static com.datastax.driver.core.querybuilder.QueryBuilder.eq; +import static com.datastax.driver.core.querybuilder.QueryBuilder.update; +import static com.datastax.driver.core.querybuilder.QueryBuilder.set; + + +/** + * Mailbox implementation for Cassandra. It overrides SimpleMailbox class, adding ACL support + * ( stored in Cassandra ) and expose specific methods to allow check-and-set ACL management + * in CassandraMessageManager + */ +public class CassandraMailbox<Id> extends SimpleMailbox<Id> { + + private Session session; + + private static final Logger LOG = LoggerFactory.getLogger(CassandraMailbox.class); + + public CassandraMailbox(MailboxPath path, long uidValidity, Session session) { + super(path, uidValidity); + this.session = session; + } + + @Override + public MailboxACL getACL() { + // Get the ACL string from Cassandra + ResultSet resultSet = session.execute( + select(CassandraMailboxTable.ACL) + .from(CassandraMailboxTable.TABLE_NAME) + .where( + eq(CassandraMailboxTable.ID, getMailboxId()) + ) + ); + if(resultSet.isExhausted()) { + // No mailbox. Should not happen : return null + return null; + } + String aclJson = resultSet.one().getString(CassandraMailboxTable.ACL); + return convertIntoMailboxACL(aclJson); + } + + private MailboxACL convertIntoMailboxACL(String aclJson) { + MailboxACL result = SimpleMailboxACL.EMPTY; + try { + result = SimpleMailboxACLJsonConverter.toACL(aclJson); + } catch (JsonParseException parseException) { + LOG.error(aclJson + " provided as ACL is malformed for mailbox " + this.getMailboxId()); + } catch ( JsonMappingException mappingException) { + LOG.error(aclJson + " provided as ACL does not correspond to an ACL for mailbox " + this.getMailboxId()); + } catch( IOException ioException) { + LOG.error("IOException while parsing JSON for mailbox " + this.getMailboxId()); + } + return result; + } + + public class ACLWithVersion { + public long version; + public MailboxACL acl; + } + + /** + * Get the ACL and the version number. + * + * @return The ACL of this mailbox with its version number + */ + public ACLWithVersion getACLWithVersion() { + ACLWithVersion result = new ACLWithVersion(); + ResultSet resultSet = session.execute( + select(CassandraMailboxTable.FIELDS) + .from(CassandraMailboxTable.TABLE_NAME) + .where( + eq(CassandraMailboxTable.ID, getMailboxId()) + ) + ); + if(resultSet.isExhausted()) { + result.version = 0; + result.acl = SimpleMailboxACL.EMPTY; + return result; + } + Row row = resultSet.one(); + String aclJson = row.getString(CassandraMailboxTable.ACL); + result.version = row.getLong(CassandraMailboxTable.ACL_VERSION); + result.acl = convertIntoMailboxACL(aclJson); + return result; + } + + /** + * Not concurrency safe AT ALL ! Do not use ! + * @param acl The mailbox ACL to set + */ + @Override + public void setACL(MailboxACL acl) { + String jsonACL; + try { + jsonACL = SimpleMailboxACLJsonConverter.toJson(acl); + } catch(JsonProcessingException processingException) { + LOG.error("Can not set acl for mailbox " + this.getMailboxId()); + return; + } + session.execute( + update(CassandraMailboxTable.TABLE_NAME) + .with( + set(CassandraMailboxTable.ACL, jsonACL) + ) + .where(eq(CassandraMailboxTable.ID, getMailboxId())) + ); + } + + /** + * Set ACL implementation for Cassandra using test and set CQL instruction + * + * @param acl Mailbox ACLs to set + * @return True if it succeeded, False if the version of the ACL is not the one specified in ACL version + */ + public boolean setACLWithVersion(MailboxACL acl, long version ) { + long newVersionNumber = version + 1; + String jsonACL; + try { + jsonACL = SimpleMailboxACLJsonConverter.toJson(acl); + } catch(JsonProcessingException processingException) { + LOG.error("Can not set acl for mailbox " + this.getMailboxId()); + return true; + } + ResultSet resultSet = session.execute( + update(CassandraMailboxTable.TABLE_NAME) + .with( + set(CassandraMailboxTable.ACL, jsonACL) + ) + .and( + set(CassandraMailboxTable.ACL_VERSION, newVersionNumber) + ) + .where( + eq(CassandraMailboxTable.ID, getMailboxId()) + ) + .onlyIf( + eq(CassandraMailboxTable.ACL_VERSION, version) + ) + + ); + return resultSet.one().getBool("[applied]"); + } + +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/model/SimpleMailboxACLJsonConverter.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/model/SimpleMailboxACLJsonConverter.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/model/SimpleMailboxACLJsonConverter.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/model/SimpleMailboxACLJsonConverter.java 2014-12-17 17:14:11.800266711 +0100 @@ -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.mailbox.cassandra.mail.model; + +import com.fasterxml.jackson.core.JsonParseException; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonMappingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.apache.james.mailbox.exception.UnsupportedRightException; +import org.apache.james.mailbox.model.MailboxACL; +import org.apache.james.mailbox.model.SimpleMailboxACL; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Convert the ACL in Json and convert Json back to ACL + */ +public class SimpleMailboxACLJsonConverter { + + private static final Logger LOG = LoggerFactory.getLogger(SimpleMailboxACLJsonConverter.class); + + /** + * Convert an ACL to JSON in order to store it in Cassandra + * + * @param acl ACL you want to Jsonify + * @return A Json representing your ACL + */ + public static String toJson(MailboxACL acl) throws JsonProcessingException { + Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> entries = acl.getEntries(); + Set<Map.Entry<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights>> set = entries.entrySet(); + Map<String,Integer> convertedEntries = new HashMap<String, Integer>(); + for(Map.Entry<MailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> entry : set) { + SimpleMailboxACL.Rfc4314Rights rights; + try{ + rights = (SimpleMailboxACL.Rfc4314Rights) entry.getValue(); + } catch(ClassCastException classCastException) { + LOG.warn("Cast Exception : you need to provide a Rfc4314Rights object when instantiating ACLs"); + break; + } + convertedEntries.put(entry.getKey().serialize(), rights.getValue()); + } + ObjectMapper mapper = new ObjectMapper(); + return mapper.writeValueAsString(convertedEntries); + } + + /** + * Get back your ACL from a JSON string + * + * A malformed Json is equivalent to an empty ACL + * + * @param jsonACLString Json string representing your ACL + * @return The ACL represented by your Json + */ + public static MailboxACL toACL(String jsonACLString) throws JsonParseException, JsonMappingException, IOException { + ObjectMapper mapper = new ObjectMapper(); + Map<String,Integer> convertedEntries = mapper.readValue(jsonACLString, + new TypeReference<Map<String, Integer>>() { } ); + Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> entries = new HashMap<MailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights>(); + Set<Map.Entry<String, Integer>> convertedEntrySet = convertedEntries.entrySet(); + for(Map.Entry<String, Integer> entry : convertedEntrySet) { + SimpleMailboxACL.Rfc4314Rights rights; + try { + rights = new SimpleMailboxACL.Rfc4314Rights(entry.getValue()); + } catch(UnsupportedRightException rightException) { + LOG.warn("Integer provided as right is out of range, causing an unsupported right exception"); + break; + } + MailboxACL.MailboxACLEntryKey key = new SimpleMailboxACL.SimpleMailboxACLEntryKey(entry.getKey()); + entries.put(key, rights); + } + return new SimpleMailboxACL(entries); + } + +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraFixedQuotaManager.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraFixedQuotaManager.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraFixedQuotaManager.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraFixedQuotaManager.java 2014-12-17 17:09:55.653588965 +0100 @@ -0,0 +1,96 @@ +/**************************************************************** + * 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.quota; + +import com.datastax.driver.core.Session; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.store.StoreMailboxManager; + +/** + * Allow you to create a FixedQuotaManager where all users have the same limit. + */ +public class CassandraFixedQuotaManager extends CassandraListeningQuotaManager { + + + public CassandraFixedQuotaManager(StoreMailboxManager<?> manager, Session session) throws MailboxException { + super(manager, session); + } + + /** + * Return the maximum storage which is allowed for the given {@link org.apache.james.mailbox.MailboxSession} (in fact the user which the session is bound to) + * + * The returned valued must be in <strong>bytes</strong> + * + * @param session + * @return maxBytes + * @throws MailboxException + */ + protected long getMaxStorage(MailboxSession session) throws MailboxException { + return quotaStorage.getDefaultMaxStorage(); + } + + + /** + * Return the maximum message count which is allowed for the given {@link org.apache.james.mailbox.MailboxSession} (in fact the user which the session is bound to) + * + * @param session + * @return maximum of allowed message count + * @throws MailboxException + */ + protected long getMaxMessage(MailboxSession session) throws MailboxException { + return quotaStorage.getDefaultMaxMessageCount(); + } + + /** + * Proxy method allowing you to set the maximum storage quota for a given user + * + * @param user This user + * @param maxStorageQuota The new storage quota ( in bytes ) for this user + */ + public void setUserMaxStorage(String user, long maxStorageQuota) { + + } + + /** + * Proxy method allowing you to set the maximum message count allowed for this user + * @param user This user + * @param maxMessageCount The new message count allowed for this user. + */ + public void setUserMaxMessage(String user, long maxMessageCount) { + + } + + /** + * Proxy method allowing you to set the default maximum storage in bytes. + * @param defaultMaxStorage new default maximum storage + */ + public void setDefaultMaxStorage(long defaultMaxStorage) { + quotaStorage.setDefaultMaxStorage(defaultMaxStorage); + } + + /** + * Proxy method allowing you to set the default maximum message count allowed + * @param defaultMaxMessageCount new default message count + */ + public void setDefaultMaxMessage(long defaultMaxMessageCount) { + quotaStorage.setDefaultMaxMessageCount(defaultMaxMessageCount); + } + +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraListeningQuotaManager.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraListeningQuotaManager.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraListeningQuotaManager.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraListeningQuotaManager.java 2014-12-17 17:09:55.653588965 +0100 @@ -0,0 +1,232 @@ +/**************************************************************** + * 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.quota; + +import com.datastax.driver.core.Session; +import org.apache.james.mailbox.MailboxListener; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.QuotaManager; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.model.MailboxPath; +import org.apache.james.mailbox.model.MessageRange; +import org.apache.james.mailbox.model.Quota; +import org.apache.james.mailbox.store.MailboxSessionMapperFactory; +import org.apache.james.mailbox.store.StoreMailboxManager; +import org.apache.james.mailbox.store.mail.MessageMapper; +import org.apache.james.mailbox.store.mail.model.Mailbox; +import org.apache.james.mailbox.store.mail.model.Message; +import org.apache.james.mailbox.store.quota.QuotaImpl; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; + +/** + * {@link QuotaManager} which will keep track of quota by listing for {@link org.apache.james.mailbox.MailboxListener.Event}'s. + * + * The whole quota is kept in Cassandra after it was lazy-fetched on the first access + * * + */ +public abstract class CassandraListeningQuotaManager implements QuotaManager, MailboxListener { + private MailboxSessionMapperFactory factory; + private boolean calculateWhenUnlimited = false; + protected CassandraQuotaStorage quotaStorage; + + public CassandraListeningQuotaManager(StoreMailboxManager<?> manager, Session session) throws MailboxException { + this.factory = manager.getMapperFactory(); + manager.addGlobalListener(this, null); + this.quotaStorage = new CassandraQuotaStorage(session); + } + + protected MailboxSessionMapperFactory<?> getFactory() { + return factory; + } + + /** + * Allow you to tune default behaviour when facing unlimited quota + * + * @param calculateWhenUnlimited If true, quota are re - calculated when unlimited + */ + public void setCalculateUsedWhenUnlimited(boolean calculateWhenUnlimited) { + this.calculateWhenUnlimited = calculateWhenUnlimited; + } + + /** + * Calculate the current message count for the user owning this MailboxSession + * + * This method is slow ( linear in the number of mailboxes ). It is only called once for each user maximum. + * + * @param session MailboxSession of the user + * @return The number of messages of all mailboxes belonging to this user. + * @throws MailboxException + */ + private long constructMessageCount(MailboxSession session) throws MailboxException { + long mc = 0; + List<Mailbox> mailboxes = factory.getMailboxMapper(session).findMailboxWithPathLike(new MailboxPath(session.getPersonalSpace(), session.getUser().getUserName(), "%")); + for (int i = 0; i < mailboxes.size(); i++) { + mc += factory.getMessageMapper(session).countMessagesInMailbox(mailboxes.get(i)); + } + return mc; + } + + /** + * Calculate the current size of all the mailboxes of the user ( in bytes ). + * + * This method is slow ( linear in the number of mailboxes and in messages contained in these mailboxes). It is only called once for each user maximum. + * + * + * @param session + * @return + * @throws MailboxException + */ + private long calculateMailboxesSize(MailboxSession session) throws MailboxException { + MessageMapper mapper = factory.getMessageMapper(session); + long mSizes = 0; + List<Mailbox> mailboxes = factory.getMailboxMapper(session).findMailboxWithPathLike(new MailboxPath(session.getPersonalSpace(), session.getUser().getUserName(), "%")); + for (int i = 0; i < mailboxes.size(); i++) { + Iterator<Message> messages = mapper.findInMailbox(mailboxes.get(i), MessageRange.all(), MessageMapper.FetchType.Metadata, -1); + + while(messages.hasNext()) { + mSizes += messages.next().getFullContentOctets(); + } + } + return mSizes; + } + + /** + * Return the message quota for the user owning this MailboxSession + * + * @param session MailboxSession of this user + * @return Quota for message count for this user + * @throws MailboxException + */ + @Override + public Quota getMessageQuota(MailboxSession session) throws MailboxException { + long max = getMaxMessage(session); + if (max != Quota.UNLIMITED || calculateWhenUnlimited) { + + String id = session.getUser().getUserName(); + AtomicLong count = quotaStorage.getCount(id); + if (count == null) { + long messageCount = constructMessageCount(session); + count = new AtomicLong(messageCount); + long mailboxesSize = calculateMailboxesSize(session); + quotaStorage.setUserQuotas(session.getUser().getUserName(), messageCount, mailboxesSize); + } + return QuotaImpl.quota(max, count.get()); + } else { + return QuotaImpl.unlimited(); + } + } + + /** + * Return the storage quota for the user owning this MailboxSession + * + * @param session This mailbox session + * @return The storage quota for the user user owning this mailboxSession + * @throws MailboxException + */ + @Override + public Quota getStorageQuota(MailboxSession session) throws MailboxException { + long max = getMaxStorage(session); + if (max != Quota.UNLIMITED || calculateWhenUnlimited) { + MessageMapper mapper = factory.getMessageMapper(session); + String id = session.getUser().getUserName(); + AtomicLong size = quotaStorage.getSize(id); + + if (size == null) { + long messageCount = constructMessageCount(session); + long mailboxesSize = calculateMailboxesSize(session); + size = new AtomicLong(mailboxesSize); + quotaStorage.setUserQuotas(session.getUser().getUserName(), messageCount, mailboxesSize); + } + return QuotaImpl.quota(max, size.get()); + } else { + return QuotaImpl.unlimited(); + } + } + + /** + * Return the maximum storage which is allowed for the given {@link MailboxSession} (in fact the user which the session is bound to) + * + * The returned valued must be in <strong>bytes</strong> + * + * @param session + * @return maxBytes + * @throws MailboxException + */ + protected abstract long getMaxStorage(MailboxSession session) throws MailboxException; + + + /** + * Return the maximum message count which is allowed for the given {@link MailboxSession} (in fact the user which the session is bound to) + * + * @param session + * @return maximum of allowed message count + * @throws MailboxException + */ + protected abstract long getMaxMessage(MailboxSession session) throws MailboxException; + + + @Override + public void event(MailboxListener.Event event) { + String id = event.getSession().getUser().getUserName(); + if (event instanceof MailboxListener.Added) { + MailboxListener.Added added = (MailboxListener.Added) event; + + long s = 0; + long c = 0; + Iterator<Long> uids = added.getUids().iterator();; + while(uids.hasNext()) { + long uid = uids.next(); + s += added.getMetaData(uid).getSize(); + c++; + } + + quotaStorage.addCount(id, c); + quotaStorage.addCount(id, s); + } else if (event instanceof MailboxListener.Expunged) { + MailboxListener.Expunged expunged = (MailboxListener.Expunged) event; + long s = 0; + long c = 0; + Iterator<Long> uids = expunged.getUids().iterator(); + while(uids.hasNext()) { + long uid = uids.next(); + s += expunged.getMetaData(uid).getSize(); + c++; + } + + quotaStorage.addCount(id, -c); + quotaStorage.addCount(id, -s); + } else if (event instanceof MailboxAdded) { + // Trick : creates the row initialized with zero values + quotaStorage.addCount(id, 0); + } + } + + /** + * Get never closed + * + * @return false + */ + public boolean isClosed() { + return false; + } + +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserQuotaManager.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserQuotaManager.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserQuotaManager.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraPerUserQuotaManager.java 2014-12-17 17:09:55.653588965 +0100 @@ -0,0 +1,95 @@ +/**************************************************************** + * 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.quota; + +import com.datastax.driver.core.Session; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.exception.MailboxException; +import org.apache.james.mailbox.store.StoreMailboxManager; + +/** + * Allow you to set a per user quota stored in Cassandra + */ +public class CassandraPerUserQuotaManager extends CassandraListeningQuotaManager { + + public CassandraPerUserQuotaManager(StoreMailboxManager<?> manager, Session session) throws MailboxException { + super(manager, session); + } + + /** + * Return the maximum storage which is allowed for the given {@link MailboxSession} (in fact the user which the session is bound to) + * + * The returned valued must be in <strong>bytes</strong> + * + * @param session + * @return maxBytes + * @throws MailboxException + */ + protected long getMaxStorage(MailboxSession session) throws MailboxException { + return quotaStorage.getMaxMailboxSize(session.getUser().getUserName()); + } + + + /** + * Return the maximum message count which is allowed for the given {@link MailboxSession} (in fact the user which the session is bound to) + * + * @param session + * @return maximum of allowed message count + * @throws MailboxException + */ + protected long getMaxMessage(MailboxSession session) throws MailboxException { + return quotaStorage.getMaxMessageCont(session.getUser().getUserName()); + } + + /** + * Proxy method allowing you to set the maximum storage quota for a given user + * + * @param user This user + * @param maxStorageQuota The new storage quota ( in bytes ) for this user + */ + public void setUserMaxStorage(String user, long maxStorageQuota) { + quotaStorage.setMaxMailboxSize(user, maxStorageQuota); + } + + /** + * Proxy method allowing you to set the maximum message count allowed for this user + * @param user This user + * @param maxMessageCount The new message count allowed for this user. + */ + public void setUserMaxMessage(String user, long maxMessageCount) { + quotaStorage.setMaxMessageCount(user, maxMessageCount); + } + + /** + * Proxy method allowing you to set the default maximum storage in bytes. + * @param defaultMaxStorage new default maximum storage + */ + public void setDefaultMaxStorage(long defaultMaxStorage) { + quotaStorage.setDefaultMaxStorage(defaultMaxStorage); + } + + /** + * Proxy method allowing you to set the default maximum message count allowed + * @param defaultMaxMessageCount new default message count + */ + public void setDefaultMaxMessage(long defaultMaxMessageCount) { + quotaStorage.setDefaultMaxMessageCount(defaultMaxMessageCount); + } + +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaStorage.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaStorage.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaStorage.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaStorage.java 2014-12-17 17:09:55.653588965 +0100 @@ -0,0 +1,264 @@ +/**************************************************************** + * 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.quota; + +import com.datastax.driver.core.ResultSet; +import com.datastax.driver.core.Session; +import org.apache.james.mailbox.cassandra.table.CassandraDefaultMaxQuotaTable; +import org.apache.james.mailbox.cassandra.table.CassandraMaxQuotaTable; +import org.apache.james.mailbox.model.Quota; + +import java.util.concurrent.atomic.AtomicLong; + +import static com.datastax.driver.core.querybuilder.QueryBuilder.*; +import static org.apache.james.mailbox.cassandra.table.CassandraQuotaTable.COUNT_QUOTA; +import static org.apache.james.mailbox.cassandra.table.CassandraQuotaTable.SIZE_QUOTA; +import static org.apache.james.mailbox.cassandra.table.CassandraQuotaTable.TABLE_NAME; +import static org.apache.james.mailbox.cassandra.table.CassandraQuotaTable.USER; + +/** + * This class is responsible for managing quota storage into Cassandra + */ +public class CassandraQuotaStorage { + + private Session session; + + CassandraQuotaStorage(Session session) { + this.session = session; + } + + AtomicLong getCount(String user) { + ResultSet resultSet = session.execute( + select(COUNT_QUOTA) + .from(TABLE_NAME) + .where( + eq(USER, user) + ) + ); + if(resultSet.isExhausted() ) { + return null; + } + return new AtomicLong( resultSet.one().getLong(COUNT_QUOTA) ); + } + + AtomicLong getSize(String user) { + ResultSet resultSet = session.execute( + select(SIZE_QUOTA) + .from(TABLE_NAME) + .where( + eq(USER, user) + ) + ); + if(resultSet.isExhausted() ) { + return null; + } + return new AtomicLong( resultSet.one().getLong(SIZE_QUOTA) ); + } + + AtomicLong setUserQuotas(String user, long messageCount, long mailboxesSize) { + String query = "UPDATE "+TABLE_NAME + +" SET "+COUNT_QUOTA+"="+COUNT_QUOTA+"+"+messageCount+"," + +SIZE_QUOTA+"="+SIZE_QUOTA+"+"+mailboxesSize + +" WHERE "+USER+"='"+user+"';"; + session.execute(query); + return new AtomicLong( messageCount ); + } + + void addCount(String user, long addedCount) { + String query = "UPDATE " + TABLE_NAME + + " SET "+COUNT_QUOTA + "="+ COUNT_QUOTA +"+"+addedCount + + " WHERE " + USER +"='"+user+"'"; + session.execute(query); + } + + void addSize(String user, long addedSize) { + String query = "UPDATE " + TABLE_NAME + + " SET "+SIZE_QUOTA +"="+SIZE_QUOTA+"+"+addedSize + + " WHERE " + USER +"='"+user+"'"; + session.execute(query); + } + + long getMaxMailboxSize(String user) { + ResultSet resultSet = session.execute( + select(CassandraMaxQuotaTable.SIZE_QUOTA_MAX) + .from(CassandraMaxQuotaTable.TABLE_NAME) + .where(eq(CassandraMaxQuotaTable.USER,user)) + ); + if(resultSet.isExhausted()) { + // No specified max value for this user. Return max value instead. + return getDefaultMaxStorage(); + } + long quota_value = resultSet.one().getLong(CassandraMaxQuotaTable.SIZE_QUOTA_MAX); + if( quota_value == Quota.UNKNOWN) { + return getDefaultMaxStorage(); + } + return quota_value; + } + + long getMaxMessageCont(String user) { + ResultSet resultSet = session.execute( + select(CassandraMaxQuotaTable.COUNT_QUOTA_MAX) + .from(CassandraMaxQuotaTable.TABLE_NAME) + .where(eq(CassandraMaxQuotaTable.USER,user)) + ); + if(resultSet.isExhausted()) { + // No specified max value for this user. Return max value instead. + return getDefaultMaxMessageCount(); + } + long quota_value = resultSet.one().getLong(CassandraMaxQuotaTable.COUNT_QUOTA_MAX); + if( quota_value == Quota.UNKNOWN) { + return getDefaultMaxMessageCount(); + } + return quota_value; + } + + void setMaxMailboxSize(String user, long maxMailboxSize) { + createFieldsForUser(user); + session.execute( + update(CassandraMaxQuotaTable.TABLE_NAME) + .with( + set(CassandraMaxQuotaTable.SIZE_QUOTA_MAX, maxMailboxSize) + ) + .where( + eq(CassandraMaxQuotaTable.USER, user) + ) + ); + } + + void setMaxMessageCount(String user, long maxMessageCount) { + createFieldsForUser(user); + session.execute( + update(CassandraMaxQuotaTable.TABLE_NAME) + .with( + set(CassandraMaxQuotaTable.COUNT_QUOTA_MAX, maxMessageCount) + ) + .where( + eq(CassandraMaxQuotaTable.USER, user) + ) + ); + } + + long getDefaultMaxStorage() { + ResultSet resultSet = session.execute( + select(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_SIZE_QUOTA) + .from(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_QUOTA_TABLE) + .where( + eq(CassandraDefaultMaxQuotaTable.ID, 1) + ) + ); + if(resultSet.isExhausted()) { + // No value specified. Set default max quotas to Quota.unlimited + session.execute( + insertInto(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_QUOTA_TABLE) + .value(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_SIZE_QUOTA, Quota.UNLIMITED) + .value(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_COUNT_QUOTA, Quota.UNLIMITED) + .value(CassandraDefaultMaxQuotaTable.ID, 1) + ); + return Quota.UNLIMITED; + } + return resultSet.one().getLong(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_SIZE_QUOTA); + + } + + long getDefaultMaxMessageCount() { + ResultSet resultSet = session.execute( + select(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_COUNT_QUOTA) + .from(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_QUOTA_TABLE) + .where( + eq(CassandraDefaultMaxQuotaTable.ID, 1) + ) + ); + if(resultSet.isExhausted()) { + // No value specified. Set default max quotas to Quota.unlimited + session.execute( + insertInto(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_QUOTA_TABLE) + .value(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_SIZE_QUOTA, Quota.UNLIMITED) + .value(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_COUNT_QUOTA, Quota.UNLIMITED) + .value(CassandraDefaultMaxQuotaTable.ID, 1) + ); + return Quota.UNLIMITED; + } + return resultSet.one().getLong(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_COUNT_QUOTA); + } + + void setDefaultMaxMessageCount(long defaultMaxMessageCount) { + if(defaultRowExist()) { + session.execute( + update(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_QUOTA_TABLE) + .with( + set(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_COUNT_QUOTA, defaultMaxMessageCount) + ) + .where( + eq(CassandraDefaultMaxQuotaTable.ID,1) + ) + ); + } else { + //We need to insert the configuration row. Set Quota.UNLIMITED as storage max default value + session.execute( + insertInto(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_QUOTA_TABLE) + .value(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_SIZE_QUOTA, Quota.UNLIMITED) + .value(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_COUNT_QUOTA, defaultMaxMessageCount) + .value(CassandraDefaultMaxQuotaTable.ID, 1) + ); + } + } + + void setDefaultMaxStorage(long defaultMaxStorage) { + if(defaultRowExist()) { + session.execute( + update(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_QUOTA_TABLE) + .with( + set(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_SIZE_QUOTA, defaultMaxStorage) + ) + .where( + eq(CassandraDefaultMaxQuotaTable.ID,1) + ) + ); + } else { + //We need to insert the configuration row. Set Quota.UNLIMITED as storage max default value + session.execute( + insertInto(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_QUOTA_TABLE) + .value(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_SIZE_QUOTA, defaultMaxStorage) + .value(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_COUNT_QUOTA, Quota.UNLIMITED) + .value(CassandraDefaultMaxQuotaTable.ID, 1) + ); + } + } + + private boolean defaultRowExist() { + // Check if ID 1 exists + ResultSet resultSet = session.execute( + select(CassandraDefaultMaxQuotaTable.ID) + .from(CassandraDefaultMaxQuotaTable.DEFAULT_MAX_QUOTA_TABLE) + .where( + eq(CassandraDefaultMaxQuotaTable.ID,1) + ) + ); + return !resultSet.isExhausted(); + } + + private void createFieldsForUser(String user) { + session.execute(insertInto(CassandraMaxQuotaTable.TABLE_NAME) + .value(CassandraMaxQuotaTable.USER, user) + .value(CassandraMaxQuotaTable.SIZE_QUOTA_MAX, Quota.UNKNOWN ) + .value(CassandraMaxQuotaTable.COUNT_QUOTA_MAX, Quota.UNKNOWN ) + .ifNotExists() + ); + } +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraDefaultMaxQuotaTable.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraDefaultMaxQuotaTable.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraDefaultMaxQuotaTable.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraDefaultMaxQuotaTable.java 2014-12-17 17:09:55.653588965 +0100 @@ -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.mailbox.cassandra.table; + +public interface CassandraDefaultMaxQuotaTable { + String DEFAULT_MAX_QUOTA_TABLE = "default_max_quota_table"; + String ID = "id"; + String DEFAULT_MAX_SIZE_QUOTA = "default_max_size_quota"; + String DEFAULT_MAX_COUNT_QUOTA = "default_max_count_quota"; +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraGroupMembershipTable.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraGroupMembershipTable.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraGroupMembershipTable.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraGroupMembershipTable.java 2014-12-17 17:09:55.653588965 +0100 @@ -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.mailbox.cassandra.table; + +public interface CassandraGroupMembershipTable { + String TABLE = "group_membership"; + String GROUP = "group"; + String USER = "user"; +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraMailboxTable.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraMailboxTable.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraMailboxTable.java 2014-12-17 16:04:47.600086583 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraMailboxTable.java 2014-12-17 17:14:11.800266711 +0100 @@ -27,5 +27,7 @@ String NAMESPACE = "namespace"; String UIDVALIDITY = "uidvalidity"; String NAME = "name"; - String[] FIELDS = { ID, USER, NAMESPACE, UIDVALIDITY, NAME, PATH }; + String ACL = "acl"; + String ACL_VERSION = "acl_version"; + String[] FIELDS = { ID, USER, NAMESPACE, UIDVALIDITY, NAME, PATH, ACL, ACL_VERSION }; } diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraMaxQuotaTable.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraMaxQuotaTable.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraMaxQuotaTable.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraMaxQuotaTable.java 2014-12-17 17:09:55.653588965 +0100 @@ -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.mailbox.cassandra.table; + +public interface CassandraMaxQuotaTable { + String TABLE_NAME = "quota_max"; + String USER = "user"; + String SIZE_QUOTA_MAX = "size_quota_max"; + String COUNT_QUOTA_MAX = "count_quota_max"; +} diff -uNr james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaTable.java james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaTable.java --- james-mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaTable.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/main/java/org/apache/james/mailbox/cassandra/table/CassandraQuotaTable.java 2014-12-17 17:09:55.653588965 +0100 @@ -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.mailbox.cassandra.table; + +public interface CassandraQuotaTable { + String TABLE_NAME = "quota"; + String USER = "user"; + String SIZE_QUOTA = "size_quota"; + String COUNT_QUOTA = "count_quota"; +} diff -uNr james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraClusterSingleton.java james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraClusterSingleton.java --- james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraClusterSingleton.java 2014-12-17 16:04:47.600086583 +0100 +++ james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraClusterSingleton.java 2014-12-17 17:16:16.773605450 +0100 @@ -40,7 +40,7 @@ /** * Builds a MiniCluster instance. - * + * * @return the {@link CassandraClusterSingleton} instance * @throws RuntimeException */ @@ -57,16 +57,17 @@ EmbeddedCassandraServerHelper.startEmbeddedCassandra(); // Let Cassandra initialization before creating // the session. Solve very fast computer tests run. - Thread.sleep(2000); + Thread.sleep(4000); this.session = new CassandraSession(CLUSTER_IP, CLUSTER_PORT_TEST, KEYSPACE_NAME, DEFAULT_REPLICATION_FACTOR); } catch (Exception e) { + e.printStackTrace(); throw new RuntimeException(e); } } /** * Return a configuration for the runnning MiniCluster. - * + * * @return */ public Session getConf() { @@ -75,13 +76,13 @@ /** * Create a specific table. - * + * * @param tableName * the table name */ public void ensureTable(String tableName) { if (tableName.equals("mailbox")) { - session.execute("CREATE TABLE IF NOT EXISTS " + session.getLoggedKeyspace() + ".mailbox (" + "id uuid PRIMARY KEY," + "name text, namespace text," + "uidvalidity bigint," + "user text," + "path text" + ");"); + session.execute("CREATE TABLE IF NOT EXISTS " + session.getLoggedKeyspace() + ".mailbox (" + "id uuid PRIMARY KEY," + "name text, namespace text," + "uidvalidity bigint," + "user text," + "path text," + "acl text," + "acl_version bigint" + ");"); session.execute("CREATE INDEX IF NOT EXISTS ON " + session.getLoggedKeyspace() + ".mailbox(path);"); } else if (tableName.equals("messageCounter")) { @@ -94,6 +95,30 @@ + "PRIMARY KEY (mailboxId, uid)" + ");"); } else if (tableName.equals("subscription")) { session.execute("CREATE TABLE IF NOT EXISTS " + session.getLoggedKeyspace() + ".subscription (" + "user text," + "mailbox text," + "PRIMARY KEY (mailbox, user)" + ");"); + } else if (tableName.equals("quota")) { + session.execute("CREATE TABLE IF NOT EXISTS " + session.getLoggedKeyspace() + ".quota (" + + "user text PRIMARY KEY," + + "size_quota counter," + + "count_quota counter" + + ");"); + } else if (tableName.equals("quota_max")) { + session.execute("CREATE TABLE IF NOT EXISTS " + session.getLoggedKeyspace() + ".quota_max (" + + "user text PRIMARY KEY," + + "size_quota_max bigint," + + "count_quota_max bigint" + + ");"); + } else if(tableName.equals("default_max_quota_table")) { + session.execute("CREATE TABLE IF NOT EXISTS " + session.getLoggedKeyspace() + ".default_max_quota_table (" + + "id int PRIMARY KEY," + + "default_max_size_quota bigint," + + "default_max_count_quota bigint" + + ");"); + } else if(tableName.equals("group_membership")) { + session.execute("CREATE TABLE IF NOT EXISTS " + session.getLoggedKeyspace() + ".group_membership(" + + "user text," + + "group text," + + "PRIMARY KEY (user,group)" + + ");"); } else { throw new NotImplementedException("We don't support the class " + tableName); } @@ -107,11 +132,15 @@ ensureTable("mailboxCounters"); ensureTable("message"); ensureTable("subscription"); + ensureTable("quota"); + ensureTable("quota_max"); + ensureTable("default_max_quota_table"); + ensureTable("group_membership"); } /** * Delete all rows from specified table. - * + * * @param tableName */ public void clearTable(String tableName) { @@ -126,6 +155,11 @@ clearTable("mailboxCounters"); clearTable("message"); clearTable("subscription"); + clearTable("quota"); + clearTable("quota_max"); + clearTable("default_max_quota_table"); + clearTable("group_membership"); + } } diff -uNr james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraGroupMembershipTest.java james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraGroupMembershipTest.java --- james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraGroupMembershipTest.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraGroupMembershipTest.java 2014-12-17 17:09:55.653588965 +0100 @@ -0,0 +1,44 @@ +/**************************************************************** + * 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; + +import org.junit.Test; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + + +/** + * Tests the CassandraGroupMembershipResolver class. + */ +public class CassandraGroupMembershipTest { + @Test + public void testGroupMembership() { + CassandraClusterSingleton CLUSTER = CassandraClusterSingleton.build(); + CLUSTER.ensureAllTables(); + CassandraGroupMembershipResolver groupMembershipResolver = new CassandraGroupMembershipResolver(CLUSTER.getConf()); + assertFalse(groupMembershipResolver.isMember("benwa", "Linagora")); + groupMembershipResolver.addMembership("Linagora", "benwa"); + assertFalse(groupMembershipResolver.isMember("benwa", "Renault")); + assertTrue(groupMembershipResolver.isMember("benwa", "Linagora")); + groupMembershipResolver.removeMembership("Linagora","benwa"); + assertFalse(groupMembershipResolver.isMember("benwa","Linagora")); + CLUSTER.clearAllTables(); + } +} diff -uNr james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapperTest.java james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapperTest.java --- james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapperTest.java 2014-12-16 17:49:32.163262333 +0100 +++ james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapperTest.java 2014-12-17 17:14:11.800266711 +0100 @@ -28,9 +28,11 @@ import java.util.UUID; import org.apache.james.mailbox.cassandra.CassandraClusterSingleton; +import org.apache.james.mailbox.cassandra.mail.model.CassandraMailbox; import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.exception.MailboxNotFoundException; import org.apache.james.mailbox.model.MailboxPath; +import org.apache.james.mailbox.model.SimpleMailboxACL; import org.apache.james.mailbox.store.mail.model.Mailbox; import org.apache.james.mailbox.store.mail.model.impl.SimpleMailbox; import org.junit.Before; @@ -82,6 +84,9 @@ testSave(); testDelete(); testHasChildren(); + testAclInitialisation(); + testSetACLMethod(); + testSetAclWithVersion(); // testDeleteAllMemberships(); // Ignore this test } @@ -119,7 +124,7 @@ if (i % 2 == 0) { newPath.setUser(null); } - addMailbox(new SimpleMailbox<UUID>(newPath, 1234)); + addMailbox(new CassandraMailbox<UUID>(newPath, 1234, CLUSTER.getConf())); } result = mapper.findMailboxWithPathLike(path); assertEquals(end - start + 1, result.size()); @@ -209,6 +214,38 @@ } } + private void testAclInitialisation() { + CassandraMailbox<UUID> mailbox = (CassandraMailbox<UUID>) mailboxList.get(0); + assertEquals(SimpleMailboxACL.EMPTY, mailbox.getACL()); + CassandraMailbox.ACLWithVersion aclWithVersion = mailbox.getACLWithVersion(); + assertEquals(SimpleMailboxACL.EMPTY, aclWithVersion.acl); + assertEquals(aclWithVersion.version, 0); + } + + private void testSetAclWithVersion() { + CassandraMailbox<UUID> mailbox = (CassandraMailbox<UUID>) mailboxList.get(0); + mailbox.setACL(SimpleMailboxACL.EMPTY); + assertEquals(SimpleMailboxACL.EMPTY, mailbox.getACL()); + mailbox.setACLWithVersion(SimpleMailboxACL.OWNER_FULL_ACL, 0); + CassandraMailbox.ACLWithVersion aclWithVersion = mailbox.getACLWithVersion(); + assertEquals(SimpleMailboxACL.OWNER_FULL_ACL, aclWithVersion.acl); + assertEquals(1, aclWithVersion.version); + } + + /** + * Test if a mailbox can store and retrieve its ACLs + */ + private void testSetACLMethod() { + // Find a mailbox + CassandraMailbox<UUID> mailbox = (CassandraMailbox<UUID>) mailboxList.get(0); + // Test a setACL + mailbox.setACL(SimpleMailboxACL.OWNER_FULL_ACL); + assertEquals(SimpleMailboxACL.OWNER_FULL_ACL, mailbox.getACL()); + CassandraMailbox.ACLWithVersion aclWithVersion = mailbox.getACLWithVersion(); + assertEquals(SimpleMailboxACL.OWNER_FULL_ACL, aclWithVersion.acl); + assertEquals(0, aclWithVersion.version); + } + private static void fillMailboxList() { mailboxList = new ArrayList<SimpleMailbox<UUID>>(); pathsList = new ArrayList<MailboxPath>(); @@ -224,14 +261,14 @@ } path = new MailboxPath("namespace" + i, "user" + j, name); pathsList.add(path); - mailboxList.add(new SimpleMailbox<UUID>(path, 13)); + mailboxList.add(new CassandraMailbox<UUID>(path, 13, CLUSTER.getConf())); } } } LOG.info("Created test case with {} mailboxes and {} paths", mailboxList.size(), pathsList.size()); } - private void addMailbox(SimpleMailbox<UUID> mailbox) throws MailboxException { + private void addMailbox(CassandraMailbox<UUID> mailbox) throws MailboxException { mailboxList.add(mailbox); pathsList.add(new MailboxPath(mailbox.getNamespace(), mailbox.getUser(), mailbox.getName())); mapper.save(mailbox); diff -uNr james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraUidAndModSeqProviderTest.java james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraUidAndModSeqProviderTest.java --- james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraUidAndModSeqProviderTest.java 2014-12-16 17:49:32.159929000 +0100 +++ james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraUidAndModSeqProviderTest.java 2014-12-17 17:14:11.800266711 +0100 @@ -25,6 +25,7 @@ import java.util.UUID; import org.apache.james.mailbox.cassandra.CassandraClusterSingleton; +import org.apache.james.mailbox.cassandra.mail.model.CassandraMailbox; import org.apache.james.mailbox.model.MailboxPath; import org.apache.james.mailbox.store.mail.model.impl.SimpleMailbox; import org.junit.Before; @@ -82,7 +83,7 @@ } path = new MailboxPath("namespace" + i, "user" + j, name); pathsList.add(path); - mailboxList.add(new SimpleMailbox<UUID>(path, 13)); + mailboxList.add(new CassandraMailbox<UUID>(path, 13, CLUSTER.getConf())); } } } @@ -97,7 +98,7 @@ public void testLastUid() throws Exception { LOG.info("lastUid"); final MailboxPath path = new MailboxPath("gsoc", "ieugen", "Trash"); - final SimpleMailbox<UUID> newBox = new SimpleMailbox<UUID>(path, 1234); + final SimpleMailbox<UUID> newBox = new CassandraMailbox<UUID>(path, 1234, CLUSTER.getConf()); mapper.save(newBox); mailboxList.add(newBox); pathsList.add(path); @@ -133,7 +134,7 @@ LOG.info("highestModSeq"); LOG.info("lastUid"); MailboxPath path = new MailboxPath("gsoc", "ieugen", "Trash"); - SimpleMailbox<UUID> newBox = new SimpleMailbox<UUID>(path, 1234); + SimpleMailbox<UUID> newBox = new CassandraMailbox<UUID>(path, 1234, CLUSTER.getConf()); mapper.save(newBox); mailboxList.add(newBox); pathsList.add(path); diff -uNr james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/SimpleMailboxACLJsonConverterTest.java james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/SimpleMailboxACLJsonConverterTest.java --- james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/SimpleMailboxACLJsonConverterTest.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/SimpleMailboxACLJsonConverterTest.java 2014-12-17 17:14:11.800266711 +0100 @@ -0,0 +1,127 @@ +/**************************************************************** + * 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 org.apache.james.mailbox.cassandra.mail.model.SimpleMailboxACLJsonConverter; +import org.apache.james.mailbox.model.MailboxACL; +import org.apache.james.mailbox.model.SimpleMailboxACL; +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.Assert.assertEquals; + +/** + * Test the converter of a simple ACL into Json + */ +public class SimpleMailboxACLJsonConverterTest { + + @Test + public void testEmptyACLToJson() throws Exception { + MailboxACL emptyACL = SimpleMailboxACL.EMPTY; + assertEquals("{}", SimpleMailboxACLJsonConverter.toJson(emptyACL)); + } + + @Test + public void testSingleUserEntryToJson() throws Exception { + Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map = new HashMap<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights>(); + addSingleUserEntryToMap(map); + MailboxACL acl = new SimpleMailboxACL(map); + assertEquals("{\"-user\":2040}", SimpleMailboxACLJsonConverter.toJson(acl)); + } + + private void addSingleUserEntryToMap(Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map) { + SimpleMailboxACL.Rfc4314Rights rights = new SimpleMailboxACL.Rfc4314Rights(false, true, true, true, false, true, false, true, true, true, true); + SimpleMailboxACL.MailboxACLEntryKey key = new SimpleMailboxACL.SimpleMailboxACLEntryKey("user", MailboxACL.NameType.user, true); + map.put(key, rights); + } + + @Test + public void testSingleGroupEntryToJson() throws Exception { + Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map = new HashMap<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights>(); + addSingleGroupEntryToMap(map); + MailboxACL acl = new SimpleMailboxACL(map); + assertEquals("{\"-$group\":2032}", SimpleMailboxACLJsonConverter.toJson(acl)); + } + + private void addSingleGroupEntryToMap(Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map) { + SimpleMailboxACL.Rfc4314Rights rights = new SimpleMailboxACL.Rfc4314Rights(false, false, true, true, false, true, false, true, true, true, true); + SimpleMailboxACL.MailboxACLEntryKey key = new SimpleMailboxACL.SimpleMailboxACLEntryKey("group", MailboxACL.NameType.group, true); + map.put(key, rights); + } + + @Test + public void testSingleEntryToJson() throws Exception { + Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map = new HashMap<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights>(); + addSingleSpecialEntryToMap(map); + MailboxACL acl = new SimpleMailboxACL(map); + assertEquals("{\"-special\":1968}", SimpleMailboxACLJsonConverter.toJson(acl)); + } + + private void addSingleSpecialEntryToMap(Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map) { + SimpleMailboxACL.Rfc4314Rights rights = new SimpleMailboxACL.Rfc4314Rights(false, false, true, true, false, true, false, true, false, true, true); + SimpleMailboxACL.MailboxACLEntryKey key = new SimpleMailboxACL.SimpleMailboxACLEntryKey("special", MailboxACL.NameType.special, true); + map.put(key, rights); + } + + @Test + public void testMultipleEntriesToJson() throws Exception { + Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map = new HashMap<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights>(); + addSingleUserEntryToMap(map); + addSingleGroupEntryToMap(map); + MailboxACL acl = new SimpleMailboxACL(map); + assertEquals("{\"-user\":2040,\"-$group\":2032}", SimpleMailboxACLJsonConverter.toJson(acl)); + } + + @Test + public void testEmptyACLFromJson() throws Exception { + String aclString = "{}"; + assertEquals(SimpleMailboxACL.EMPTY, SimpleMailboxACLJsonConverter.toACL(aclString)); + } + + @Test + public void testSingleUserEntryFromJson() throws Exception { + String aclString = "{\"-user\":2040}"; + Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map = new HashMap<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights>(); + addSingleUserEntryToMap(map); + MailboxACL acl = new SimpleMailboxACL(map); + assertEquals(acl, SimpleMailboxACLJsonConverter.toACL(aclString)); + } + + @Test + public void testSingleGroupEntryFromJson() throws Exception { + String aclString = "{\"-$group\":2032}"; + Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map = new HashMap<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights>(); + addSingleGroupEntryToMap(map); + MailboxACL acl = new SimpleMailboxACL(map); + assertEquals(acl, SimpleMailboxACLJsonConverter.toACL(aclString)); + } + + @Test + public void testMultipleEntriesFromJson() throws Exception { + String aclString = "{\"-$group\":2032,\"-user\":2040}"; + Map<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights> map = new HashMap<SimpleMailboxACL.MailboxACLEntryKey, MailboxACL.MailboxACLRights>(); + addSingleUserEntryToMap(map); + addSingleGroupEntryToMap(map); + MailboxACL acl = new SimpleMailboxACL(map); + assertEquals(acl, SimpleMailboxACLJsonConverter.toACL(aclString)); + } +} diff -uNr james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaStorageTest.java james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaStorageTest.java --- james-mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaStorageTest.java 1970-01-01 01:00:00.000000000 +0100 +++ james-mailbox-new/cassandra/src/test/java/org/apache/james/mailbox/cassandra/quota/CassandraQuotaStorageTest.java 2014-12-17 17:09:55.653588965 +0100 @@ -0,0 +1,163 @@ +/**************************************************************** + * 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.quota; + +import org.apache.james.mailbox.cassandra.CassandraClusterSingleton; +import org.apache.james.mailbox.model.Quota; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +import static org.junit.Assert.assertEquals; + +/** + * Run tests for CassandraQuotaStorage. + */ +public class CassandraQuotaStorageTest { + + private static final Logger LOG = LoggerFactory.getLogger(CassandraQuotaStorageTest.class); + public static final CassandraClusterSingleton CLUSTER = CassandraClusterSingleton.build(); + private static CassandraQuotaStorage quotaStorage = new CassandraQuotaStorage(CLUSTER.getConf()); + + @Before + public void setUp() throws Exception { + LOG.info("Set up for tests for Quotas over Cassandra"); + CLUSTER.ensureAllTables(); + CLUSTER.clearAllTables(); + } + + @After + public void cleanUp() { + CLUSTER.clearAllTables(); + } + + @Test + public void testSetUserQuota() { + quotaStorage.setUserQuotas("benwa", 42, 524); + quotaStorage.setUserQuotas("toto", 24, 444); + quotaStorage.setUserQuotas("tata", 56, 965); + + assertEquals(42, quotaStorage.getCount("benwa").get() ); + assertEquals(24, quotaStorage.getCount("toto").get() ); + assertEquals(56, quotaStorage.getCount("tata").get() ); + + assertEquals(524, quotaStorage.getSize("benwa").get() ); + assertEquals(444, quotaStorage.getSize("toto").get() ); + assertEquals(965, quotaStorage.getSize("tata").get() ); + } + + @Test + public void testAddCountQuotaStorage() { + quotaStorage.addCount("toubib",0); + assertEquals(0, quotaStorage.getCount("toubib").get() ); + + quotaStorage.setUserQuotas("benwa", 42, 524); + quotaStorage.setUserQuotas("toto", 24, 444); + quotaStorage.setUserQuotas("tata", 56, 965); + + quotaStorage.addCount("benwa", 36); + quotaStorage.addCount("toto", -12); + quotaStorage.addCount("tata", 0); + + assertEquals(78, quotaStorage.getCount("benwa").get() ); + assertEquals(12, quotaStorage.getCount("toto").get() ); + assertEquals(56, quotaStorage.getCount("tata").get() ); + } + + @Test + public void testAddSizeQuotaStorage() { + quotaStorage.addCount("toubib",0); + assertEquals(0, quotaStorage.getCount("toubib").get() ); + + quotaStorage.setUserQuotas("benwa", 42, 524); + quotaStorage.setUserQuotas("toto", 24, 444); + quotaStorage.setUserQuotas("tata", 56, 965); + + quotaStorage.addSize("benwa", 102); + quotaStorage.addSize("toto", -56); + quotaStorage.addSize("tata", 0); + + assertEquals(626, quotaStorage.getSize("benwa").get() ); + assertEquals(388, quotaStorage.getSize("toto").get() ); + assertEquals(965, quotaStorage.getSize("tata").get() ); + } + + @Test + public void testGetDefaultMaxMessageCount() { + assertEquals(Quota.UNLIMITED, quotaStorage.getDefaultMaxMessageCount() ); + } + + @Test + public void testGetDefaultMaxStorage() { + assertEquals(Quota.UNLIMITED, quotaStorage.getDefaultMaxStorage() ); + } + + @Test + public void testSetDefaultMaxMessageCount() { + quotaStorage.setDefaultMaxMessageCount(42); + assertEquals(42, quotaStorage.getDefaultMaxMessageCount()); + } + + @Test + public void testSetDefaultMaxStorage() { + quotaStorage.setDefaultMaxStorage(421); + assertEquals(421, quotaStorage.getDefaultMaxStorage()); + } + + @Test + public void testGetMaxMessageCont() { + quotaStorage.setDefaultMaxMessageCount(42); + assertEquals(42,quotaStorage.getMaxMessageCont("benwa")); + } + + @Test + public void testSetMaxMessageCont() { + quotaStorage.setMaxMessageCount("benwa",84); + assertEquals(84, quotaStorage.getMaxMessageCont("benwa")); + } + + @Test + public void testGetMaxMailboxSize() { + quotaStorage.setMaxMailboxSize("benwa", 421); + assertEquals(421, quotaStorage.getMaxMailboxSize("benwa")); + } + + @Test + public void testSetMaxMailboxSize() { + quotaStorage.setMaxMailboxSize("benwa", 842); + assertEquals(842,quotaStorage.getMaxMailboxSize("benwa")); + } + + @Test + public void testSetMailboxSizeInteractionOnGetMailboxSizeDefaultBehavior() { + quotaStorage.setMaxMailboxSize("benwa", 1024); + quotaStorage.setDefaultMaxMessageCount(102); + assertEquals(102, quotaStorage.getMaxMessageCont("benwa")); + } + + @Test + public void testSetMessageCountInteractionOnGetMailboxSizeDefaultBehavior() { + quotaStorage.setMaxMessageCount("benwa", 102); + quotaStorage.setDefaultMaxStorage(2048); + assertEquals(2048, quotaStorage.getMaxMailboxSize("benwa")); + } +}
signature.asc
Description: OpenPGP digital signature
