Repository: james-project Updated Branches: refs/heads/master 02f8d6f73 -> 45d311c55
JAMES-2155 Implements a JPA Sieve Repository Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/d5d5c9cb Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/d5d5c9cb Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/d5d5c9cb Branch: refs/heads/master Commit: d5d5c9cb86dce4edda511b2a43a9e753ca777256 Parents: 02f8d6f Author: Sebastian Górecki <[email protected]> Authored: Wed Sep 5 22:53:19 2018 +0200 Committer: Benoit Tellier <[email protected]> Committed: Mon Oct 15 13:15:22 2018 +0700 ---------------------------------------------------------------------- .../james/backends/jpa/TransactionRunner.java | 18 + server/container/guice/guice-common/pom.xml | 4 + .../modules/data/SieveJPARepositoryModules.java | 37 ++ .../org/apache/james/JPAJamesServerMain.java | 8 +- server/data/data-jpa/pom.xml | 8 +- .../james/sieve/jpa/JPASieveRepository.java | 344 +++++++++++++++++++ .../james/sieve/jpa/model/JPASieveQuota.java | 97 ++++++ .../james/sieve/jpa/model/JPASieveScript.java | 200 +++++++++++ .../src/main/resources/META-INF/persistence.xml | 2 + .../james/sieve/jpa/JpaSieveRepositoryTest.java | 50 +++ .../lib/AbstractSieveRepositoryTest.java | 7 + 11 files changed, 769 insertions(+), 6 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/backends-common/jpa/src/main/java/org/apache/james/backends/jpa/TransactionRunner.java ---------------------------------------------------------------------- diff --git a/backends-common/jpa/src/main/java/org/apache/james/backends/jpa/TransactionRunner.java b/backends-common/jpa/src/main/java/org/apache/james/backends/jpa/TransactionRunner.java index dae6f2e..dd1e151 100644 --- a/backends-common/jpa/src/main/java/org/apache/james/backends/jpa/TransactionRunner.java +++ b/backends-common/jpa/src/main/java/org/apache/james/backends/jpa/TransactionRunner.java @@ -76,4 +76,22 @@ public class TransactionRunner { } } + public <T> void runAndHandleException(Consumer<EntityManager> runnable, + Function<PersistenceException, T> errorHandler) { + EntityManager entityManager = entityManagerFactory.createEntityManager(); + EntityTransaction transaction = entityManager.getTransaction(); + try { + transaction.begin(); + runnable.accept(entityManager); + transaction.commit(); + } catch (PersistenceException e) { + LOGGER.warn("Could not execute transaction", e); + if (transaction.isActive()) { + transaction.rollback(); + } + errorHandler.apply(e); + } finally { + entityManager.close(); + } + } } http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/container/guice/guice-common/pom.xml ---------------------------------------------------------------------- diff --git a/server/container/guice/guice-common/pom.xml b/server/container/guice/guice-common/pom.xml index b7bdd99..0559b85 100644 --- a/server/container/guice/guice-common/pom.xml +++ b/server/container/guice/guice-common/pom.xml @@ -51,6 +51,10 @@ </dependency> <dependency> <groupId>${james.groupId}</groupId> + <artifactId>james-server-data-jpa</artifactId> + </dependency> + <dependency> + <groupId>${james.groupId}</groupId> <artifactId>james-server-data-memory</artifactId> </dependency> <dependency> http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/container/guice/guice-common/src/main/java/org/apache/james/modules/data/SieveJPARepositoryModules.java ---------------------------------------------------------------------- diff --git a/server/container/guice/guice-common/src/main/java/org/apache/james/modules/data/SieveJPARepositoryModules.java b/server/container/guice/guice-common/src/main/java/org/apache/james/modules/data/SieveJPARepositoryModules.java new file mode 100644 index 0000000..2d984e3 --- /dev/null +++ b/server/container/guice/guice-common/src/main/java/org/apache/james/modules/data/SieveJPARepositoryModules.java @@ -0,0 +1,37 @@ +/**************************************************************** + * 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.modules.data; + +import org.apache.james.sieve.jpa.JPASieveRepository; +import org.apache.james.sieverepository.api.SieveQuotaRepository; +import org.apache.james.sieverepository.api.SieveRepository; + +import com.google.inject.AbstractModule; +import com.google.inject.Scopes; + +public class SieveJPARepositoryModules extends AbstractModule { + @Override + protected void configure() { + bind(JPASieveRepository.class).in(Scopes.SINGLETON); + + bind(SieveRepository.class).to(JPASieveRepository.class); + bind(SieveQuotaRepository.class).to(JPASieveRepository.class); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/container/guice/jpa-guice/src/main/java/org/apache/james/JPAJamesServerMain.java ---------------------------------------------------------------------- diff --git a/server/container/guice/jpa-guice/src/main/java/org/apache/james/JPAJamesServerMain.java b/server/container/guice/jpa-guice/src/main/java/org/apache/james/JPAJamesServerMain.java index 61eba68..6a5ba46 100644 --- a/server/container/guice/jpa-guice/src/main/java/org/apache/james/JPAJamesServerMain.java +++ b/server/container/guice/jpa-guice/src/main/java/org/apache/james/JPAJamesServerMain.java @@ -22,7 +22,7 @@ package org.apache.james; import org.apache.james.modules.MailboxModule; import org.apache.james.modules.activemq.ActiveMQQueueModule; import org.apache.james.modules.data.JPADataModule; -import org.apache.james.modules.data.SieveFileRepositoryModule; +import org.apache.james.modules.data.SieveJPARepositoryModules; import org.apache.james.modules.mailbox.DefaultEventModule; import org.apache.james.modules.mailbox.JPAMailboxModule; import org.apache.james.modules.mailbox.LuceneSearchMailboxModule; @@ -69,7 +69,7 @@ public class JPAJamesServerMain { new ProtocolHandlerModule(), new SMTPServerModule(), WEBADMIN); - + public static final Module JPA_SERVER_MODULE = Modules.combine( new ActiveMQQueueModule(), new DefaultProcessorsConfigurationProviderModule(), @@ -79,7 +79,7 @@ public class JPAJamesServerMain { new MailboxModule(), new NoJwtModule(), new RawPostDequeueDecoratorModule(), - new SieveFileRepositoryModule(), + new SieveJPARepositoryModules(), new DefaultEventModule(), new SpamAssassinListenerModule()); @@ -90,7 +90,7 @@ public class JPAJamesServerMain { GuiceJamesServer server = GuiceJamesServer.forConfiguration(configuration) .combineWith(JPA_SERVER_MODULE, PROTOCOLS, - new JMXServerModule(), + new JMXServerModule(), new LuceneSearchMailboxModule()); server.start(); } http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/data/data-jpa/pom.xml ---------------------------------------------------------------------- diff --git a/server/data/data-jpa/pom.xml b/server/data/data-jpa/pom.xml index cb0cd25..5d1c3ad 100644 --- a/server/data/data-jpa/pom.xml +++ b/server/data/data-jpa/pom.xml @@ -152,7 +152,9 @@ <artifactId>openjpa-maven-plugin</artifactId> <version>${apache.openjpa.version}</version> <configuration> - <includes>org/apache/james/user/jpa/model/JPAUser.class, + <includes>org/apache/james/sieve/jpa/model/JPASieveQuota.class, + org/apache/james/sieve/jpa/model/JPASieveScript.class, + org/apache/james/user/jpa/model/JPAUser.class, org/apache/james/rrt/jpa/model/JPARecipientRewrite.class, org/apache/james/domainlist/jpa/model/JPADomain.class, org/apache/james/mailrepository/jpa/JPAUrl.class</includes> @@ -165,7 +167,9 @@ </property> <property> <name>metaDataFactory</name> - <value>jpa(Types=org.apache.james.user.jpa.model.JPAUser; + <value>jpa(Types=org.apache.james.sieve.jpa.model.JPASieveQuota; + org.apache.james.sieve.jpa.model.JPASieveScript; + org.apache.james.user.jpa.model.JPAUser; org.apache.james.rrt.jpa.model.JPARecipientRewrite; org.apache.james.domainlist.jpa.model.JPADomain; org.apache.james.mailrepository.jpa.JPAUrl)</value> http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/JPASieveRepository.java ---------------------------------------------------------------------- diff --git a/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/JPASieveRepository.java b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/JPASieveRepository.java new file mode 100644 index 0000000..31c634a --- /dev/null +++ b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/JPASieveRepository.java @@ -0,0 +1,344 @@ +/**************************************************************** + * 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.sieve.jpa; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.time.ZonedDateTime; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; + +import javax.inject.Inject; +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; +import javax.persistence.EntityTransaction; +import javax.persistence.NoResultException; +import javax.persistence.PersistenceException; + +import org.apache.commons.io.IOUtils; +import org.apache.james.backends.jpa.TransactionRunner; +import org.apache.james.core.User; +import org.apache.james.core.quota.QuotaSize; +import org.apache.james.sieve.jpa.model.JPASieveQuota; +import org.apache.james.sieve.jpa.model.JPASieveScript; +import org.apache.james.sieverepository.api.ScriptContent; +import org.apache.james.sieverepository.api.ScriptName; +import org.apache.james.sieverepository.api.ScriptSummary; +import org.apache.james.sieverepository.api.SieveRepository; +import org.apache.james.sieverepository.api.exception.DuplicateException; +import org.apache.james.sieverepository.api.exception.IsActiveException; +import org.apache.james.sieverepository.api.exception.QuotaExceededException; +import org.apache.james.sieverepository.api.exception.QuotaNotFoundException; +import org.apache.james.sieverepository.api.exception.ScriptNotFoundException; +import org.apache.james.sieverepository.api.exception.StorageException; +import org.apache.james.util.OptionalUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.fge.lambdas.Throwing; +import com.google.common.collect.ImmutableList; + +public class JPASieveRepository implements SieveRepository { + + private static final Logger LOGGER = LoggerFactory.getLogger(JPASieveRepository.class); + private static final String DEFAULT_SIEVE_QUOTA_USERNAME = "default.quota"; + + private final TransactionRunner transactionRunner; + + @Inject + public JPASieveRepository(EntityManagerFactory entityManagerFactory) { + this.transactionRunner = new TransactionRunner(entityManagerFactory); + } + + @Override + public void haveSpace(User user, ScriptName name, long size) throws QuotaExceededException, StorageException { + long usedSpace = findAllSieveScriptsForUser(user).stream() + .filter(sieveScript -> !sieveScript.getScriptName().equals(name.getValue())) + .mapToLong(JPASieveScript::getScriptSize) + .sum(); + + QuotaSize quota = limitToUser(user); + if (overQuotaAfterModification(usedSpace, size, quota)) { + throw new QuotaExceededException(); + } + } + + private QuotaSize limitToUser(User user) throws StorageException { + return OptionalUtils.orSuppliers( + Throwing.supplier(() -> findQuotaForUser(user.asString())).sneakyThrow(), + Throwing.supplier(() -> findQuotaForUser(DEFAULT_SIEVE_QUOTA_USERNAME)).sneakyThrow()) + .map(JPASieveQuota::toQuotaSize) + .orElse(QuotaSize.unlimited()); + } + + private boolean overQuotaAfterModification(long usedSpace, long size, QuotaSize quota) { + return QuotaSize.size(usedSpace) + .add(size) + .isGreaterThan(quota); + } + + @Override + public void putScript(User user, ScriptName name, ScriptContent content) throws StorageException, QuotaExceededException { + transactionRunner.runAndHandleException(Throwing.<EntityManager>consumer(entityManager -> { + try { + haveSpace(user, name, content.length()); + JPASieveScript jpaSieveScript = JPASieveScript.builder() + .username(user.asString()) + .scriptName(name.getValue()) + .scriptContent(content) + .build(); + entityManager.persist(jpaSieveScript); + } catch (QuotaExceededException | StorageException e) { + rollbackTransactionIfActive(entityManager.getTransaction()); + throw e; + } + }).sneakyThrow(), throwStorageException("Unable to put script for user " + user.asString())); + } + + @Override + public List<ScriptSummary> listScripts(User user) throws StorageException { + return findAllSieveScriptsForUser(user).stream() + .map(JPASieveScript::toSummary) + .collect(ImmutableList.toImmutableList()); + } + + private List<JPASieveScript> findAllSieveScriptsForUser(User user) throws StorageException { + return transactionRunner.runAndRetrieveResult(entityManager -> { + List<JPASieveScript> sieveScripts = entityManager.createNamedQuery("findAllByUsername", JPASieveScript.class) + .setParameter("username", user.asString()).getResultList(); + return Optional.ofNullable(sieveScripts).orElse(ImmutableList.of()); + }, throwStorageException("Unable to list scripts for user " + user.asString())); + } + + @Override + public ZonedDateTime getActivationDateForActiveScript(User user) throws StorageException, ScriptNotFoundException { + Optional<JPASieveScript> script = findActiveSieveScript(user); + JPASieveScript activeSieveScript = script.orElseThrow(() -> new ScriptNotFoundException("Unable to find active script for user " + user.asString())); + return activeSieveScript.getActivationDateTime().toZonedDateTime(); + } + + @Override + public InputStream getActive(User user) throws ScriptNotFoundException, StorageException { + Optional<JPASieveScript> script = findActiveSieveScript(user); + JPASieveScript activeSieveScript = script.orElseThrow(() -> new ScriptNotFoundException("Unable to find active script for user " + user.asString())); + return IOUtils.toInputStream(activeSieveScript.getScriptContent(), StandardCharsets.UTF_8); + } + + private Optional<JPASieveScript> findActiveSieveScript(User user) throws StorageException { + return transactionRunner.runAndRetrieveResult( + Throwing.<EntityManager, Optional<JPASieveScript>>function(entityManager -> findActiveSieveScript(user, entityManager)).sneakyThrow(), + throwStorageException("Unable to find active script for user " + user.asString())); + } + + private Optional<JPASieveScript> findActiveSieveScript(User user, EntityManager entityManager) throws StorageException { + try { + JPASieveScript activeSieveScript = entityManager.createNamedQuery("findActiveByUsername", JPASieveScript.class) + .setParameter("username", user.asString()).getSingleResult(); + return Optional.ofNullable(activeSieveScript); + } catch (NoResultException e) { + LOGGER.debug("Sieve script not found for user {}", user.asString()); + return Optional.empty(); + } + } + + @Override + public void setActive(User user, ScriptName name) throws ScriptNotFoundException, StorageException { + transactionRunner.runAndHandleException(Throwing.<EntityManager>consumer(entityManager -> { + try { + if (SieveRepository.NO_SCRIPT_NAME.equals(name)) { + switchOffActiveScript(user, entityManager); + } else { + setActiveScript(user, name, entityManager); + } + } catch (StorageException | ScriptNotFoundException e) { + rollbackTransactionIfActive(entityManager.getTransaction()); + throw e; + } + }).sneakyThrow(), throwStorageException("Unable to set active script " + name.getValue() + " for user " + user.asString())); + } + + private void switchOffActiveScript(User user, EntityManager entityManager) throws StorageException { + Optional<JPASieveScript> activeSieveScript = findActiveSieveScript(user, entityManager); + activeSieveScript.ifPresent(JPASieveScript::deactivate); + } + + private void setActiveScript(User user, ScriptName name, EntityManager entityManager) throws StorageException, ScriptNotFoundException { + JPASieveScript sieveScript = findSieveScript(user, name, entityManager) + .orElseThrow(() -> new ScriptNotFoundException("Unable to find script " + name.getValue() + " for user " + user.asString())); + findActiveSieveScript(user, entityManager).ifPresent(JPASieveScript::deactivate); + sieveScript.activate(); + } + + @Override + public InputStream getScript(User user, ScriptName name) throws ScriptNotFoundException, StorageException { + Optional<JPASieveScript> script = findSieveScript(user, name); + JPASieveScript sieveScript = script.orElseThrow(() -> new ScriptNotFoundException("Unable to find script " + name.getValue() + " for user " + user.asString())); + return IOUtils.toInputStream(sieveScript.getScriptContent(), StandardCharsets.UTF_8); + } + + private Optional<JPASieveScript> findSieveScript(User user, ScriptName scriptName) throws StorageException { + return transactionRunner.runAndRetrieveResult(entityManager -> findSieveScript(user, scriptName, entityManager), + throwStorageException("Unable to find script " + scriptName.getValue() + " for user " + user.asString())); + } + + private Optional<JPASieveScript> findSieveScript(User user, ScriptName scriptName, EntityManager entityManager) { + try { + JPASieveScript sieveScript = entityManager.createNamedQuery("findSieveScript", JPASieveScript.class) + .setParameter("username", user.asString()) + .setParameter("scriptName", scriptName.getValue()).getSingleResult(); + return Optional.ofNullable(sieveScript); + } catch (NoResultException e) { + LOGGER.debug("Sieve script not found for user {}", user.asString()); + return Optional.empty(); + } + } + + @Override + public void deleteScript(User user, ScriptName name) throws ScriptNotFoundException, IsActiveException, StorageException { + transactionRunner.runAndHandleException(Throwing.<EntityManager>consumer(entityManager -> { + Optional<JPASieveScript> sieveScript = findSieveScript(user, name, entityManager); + if (!sieveScript.isPresent()) { + rollbackTransactionIfActive(entityManager.getTransaction()); + throw new ScriptNotFoundException("Unable to find script " + name.getValue() + " for user " + user.asString()); + } + JPASieveScript sieveScriptToRemove = sieveScript.get(); + if (sieveScriptToRemove.isActive()) { + rollbackTransactionIfActive(entityManager.getTransaction()); + throw new IsActiveException("Unable to delete active script " + name.getValue() + " for user " + user.asString()); + } + entityManager.remove(sieveScriptToRemove); + }).sneakyThrow(), throwStorageException("Unable to delete script " + name.getValue() + " for user " + user.asString())); + } + + @Override + public void renameScript(User user, ScriptName oldName, ScriptName newName) throws ScriptNotFoundException, DuplicateException, StorageException { + transactionRunner.runAndHandleException(Throwing.<EntityManager>consumer(entityManager -> { + Optional<JPASieveScript> sieveScript = findSieveScript(user, oldName, entityManager); + if (!sieveScript.isPresent()) { + rollbackTransactionIfActive(entityManager.getTransaction()); + throw new ScriptNotFoundException("Unable to find script " + oldName.getValue() + " for user " + user.asString()); + } + + Optional<JPASieveScript> duplicatedSieveScript = findSieveScript(user, newName, entityManager); + if (duplicatedSieveScript.isPresent()) { + rollbackTransactionIfActive(entityManager.getTransaction()); + throw new DuplicateException("Unable to rename script. Duplicate found " + newName.getValue() + " for user " + user.asString()); + } + + JPASieveScript sieveScriptToRename = sieveScript.get(); + sieveScriptToRename.renameTo(newName); + }).sneakyThrow(), throwStorageException("Unable to rename script " + oldName.getValue() + " for user " + user.asString())); + } + + private void rollbackTransactionIfActive(EntityTransaction transaction) { + if (transaction.isActive()) { + transaction.rollback(); + } + } + + @Override + public boolean hasDefaultQuota() throws StorageException { + Optional<JPASieveQuota> defaultQuota = findQuotaForUser(DEFAULT_SIEVE_QUOTA_USERNAME); + return defaultQuota.isPresent(); + } + + @Override + public QuotaSize getDefaultQuota() throws QuotaNotFoundException, StorageException { + JPASieveQuota jpaSieveQuota = findQuotaForUser(DEFAULT_SIEVE_QUOTA_USERNAME) + .orElseThrow(() -> new QuotaNotFoundException("Unable to find quota for default user")); + return QuotaSize.size(jpaSieveQuota.getSize()); + } + + @Override + public void setDefaultQuota(QuotaSize quota) throws StorageException { + setQuotaForUser(DEFAULT_SIEVE_QUOTA_USERNAME, quota); + } + + @Override + public void removeQuota() throws QuotaNotFoundException, StorageException { + removeQuotaForUser(DEFAULT_SIEVE_QUOTA_USERNAME); + } + + @Override + public boolean hasQuota(User user) throws StorageException { + Optional<JPASieveQuota> quotaForUser = findQuotaForUser(user.asString()); + return quotaForUser.isPresent(); + } + + @Override + public QuotaSize getQuota(User user) throws QuotaNotFoundException, StorageException { + JPASieveQuota jpaSieveQuota = findQuotaForUser(user.asString()) + .orElseThrow(() -> new QuotaNotFoundException("Unable to find quota for user " + user.asString())); + return QuotaSize.size(jpaSieveQuota.getSize()); + } + + @Override + public void setQuota(User user, QuotaSize quota) throws StorageException { + setQuotaForUser(user.asString(), quota); + } + + @Override + public void removeQuota(User user) throws QuotaNotFoundException, StorageException { + removeQuotaForUser(user.asString()); + } + + private Optional<JPASieveQuota> findQuotaForUser(String username) throws StorageException { + return transactionRunner.runAndRetrieveResult(entityManager -> findQuotaForUser(username, entityManager), + throwStorageException("Unable to find quota for user " + username)); + } + + private <T> Function<PersistenceException, T> throwStorageException(String message) { + return Throwing.<PersistenceException, T>function(e -> { + throw new StorageException(message, e); + }).sneakyThrow(); + } + + private Optional<JPASieveQuota> findQuotaForUser(String username, EntityManager entityManager) { + try { + JPASieveQuota sieveQuota = entityManager.createNamedQuery("findByUsername", JPASieveQuota.class) + .setParameter("username", username).getSingleResult(); + return Optional.of(sieveQuota); + } catch (NoResultException e) { + return Optional.empty(); + } + } + + private void setQuotaForUser(String username, QuotaSize quota) throws StorageException { + transactionRunner.runAndHandleException(Throwing.consumer(entityManager -> { + Optional<JPASieveQuota> sieveQuota = findQuotaForUser(username, entityManager); + if (sieveQuota.isPresent()) { + JPASieveQuota jpaSieveQuota = sieveQuota.get(); + jpaSieveQuota.setSize(quota); + entityManager.merge(jpaSieveQuota); + } else { + JPASieveQuota jpaSieveQuota = new JPASieveQuota(username, quota.asLong()); + entityManager.persist(jpaSieveQuota); + } + }), throwStorageException("Unable to set quota for user " + username)); + } + + private void removeQuotaForUser(String username) throws StorageException { + transactionRunner.runAndHandleException(Throwing.consumer(entityManager -> { + Optional<JPASieveQuota> quotaForUser = findQuotaForUser(username, entityManager); + quotaForUser.ifPresent(entityManager::remove); + }), throwStorageException("Unable to remove quota for user " + username)); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveQuota.java ---------------------------------------------------------------------- diff --git a/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveQuota.java b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveQuota.java new file mode 100644 index 0000000..9c5e097 --- /dev/null +++ b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveQuota.java @@ -0,0 +1,97 @@ +/**************************************************************** + * 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.sieve.jpa.model; + +import java.util.Objects; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +import org.apache.james.core.quota.QuotaSize; + +import com.google.common.base.MoreObjects; + +@Entity(name = "JamesSieveQuota") +@Table(name = "JAMES_SIEVE_QUOTA") +@NamedQueries({ + @NamedQuery(name = "findByUsername", query = "SELECT sieveQuota FROM JamesSieveQuota sieveQuota WHERE sieveQuota.username=:username") +}) +public class JPASieveQuota { + + @Id + @Column(name = "USER_NAME", nullable = false, length = 100) + private String username; + + @Column(name = "SIZE", nullable = false) + private long size; + + /** + * @deprecated enhancement only + */ + @Deprecated + protected JPASieveQuota() { + } + + public JPASieveQuota(String username, long size) { + this.username = username; + this.size = size; + } + + public long getSize() { + return size; + } + + public void setSize(QuotaSize quotaSize) { + this.size = quotaSize.asLong(); + } + + public QuotaSize toQuotaSize() { + return QuotaSize.size(size); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JPASieveQuota that = (JPASieveQuota) o; + return Objects.equals(username, that.username); + } + + @Override + public int hashCode() { + return Objects.hash(username); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("username", username) + .add("size", size) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveScript.java ---------------------------------------------------------------------- diff --git a/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveScript.java b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveScript.java new file mode 100644 index 0000000..3f846ec --- /dev/null +++ b/server/data/data-jpa/src/main/java/org/apache/james/sieve/jpa/model/JPASieveScript.java @@ -0,0 +1,200 @@ +/**************************************************************** + * 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.sieve.jpa.model; + +import java.time.OffsetDateTime; +import java.util.Objects; +import java.util.UUID; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.NamedQueries; +import javax.persistence.NamedQuery; +import javax.persistence.Table; + +import org.apache.commons.lang.StringUtils; +import org.apache.james.sieverepository.api.ScriptContent; +import org.apache.james.sieverepository.api.ScriptName; +import org.apache.james.sieverepository.api.ScriptSummary; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; + +@Entity(name = "JamesSieveScript") +@Table(name = "JAMES_SIEVE_SCRIPT") +@NamedQueries({ + @NamedQuery(name = "findAllByUsername", query = "SELECT sieveScript FROM JamesSieveScript sieveScript WHERE sieveScript.username=:username"), + @NamedQuery(name = "findActiveByUsername", query = "SELECT sieveScript FROM JamesSieveScript sieveScript WHERE sieveScript.username=:username AND sieveScript.isActive=true"), + @NamedQuery(name = "findSieveScript", query = "SELECT sieveScript FROM JamesSieveScript sieveScript WHERE sieveScript.username=:username AND sieveScript.scriptName=:scriptName") +}) +public class JPASieveScript { + + public static Builder builder() { + return new Builder(); + } + + public static ScriptSummary toSummary(JPASieveScript script) { + return new ScriptSummary(new ScriptName(script.getScriptName()), script.isActive()); + } + + public static class Builder { + + private String username; + private String scriptName; + private String scriptContent; + private long scriptSize; + private boolean isActive; + private OffsetDateTime activationDateTime; + + public Builder username(String username) { + Preconditions.checkNotNull(username); + this.username = username; + return this; + } + + public Builder scriptName(String scriptName) { + Preconditions.checkNotNull(scriptName); + this.scriptName = scriptName; + return this; + } + + public Builder scriptContent(ScriptContent scriptContent) { + Preconditions.checkNotNull(scriptContent); + this.scriptContent = scriptContent.getValue(); + this.scriptSize = scriptContent.length(); + return this; + } + + public Builder isActive(boolean isActive) { + this.isActive = isActive; + return this; + } + + public JPASieveScript build() { + Preconditions.checkState(StringUtils.isNotBlank(username), "'username' is mandatory"); + Preconditions.checkState(StringUtils.isNotBlank(scriptName), "'scriptName' is mandatory"); + this.activationDateTime = isActive ? OffsetDateTime.now() : null; + return new JPASieveScript(username, scriptName, scriptContent, scriptSize, isActive, activationDateTime); + } + } + + @Id + private String uuid = UUID.randomUUID().toString(); + + @Column(name = "USER_NAME", nullable = false, length = 100) + private String username; + + @Column(name = "SCRIPT_NAME", nullable = false, length = 255) + private String scriptName; + + @Column(name = "SCRIPT_CONTENT", nullable = false, length = 1024) + private String scriptContent; + + @Column(name = "SCRIPT_SIZE", nullable = false) + private long scriptSize; + + @Column(name = "IS_ACTIVE", nullable = false) + private boolean isActive; + + @Column(name = "ACTIVATION_DATE_TIME") + private OffsetDateTime activationDateTime; + + /** + * @deprecated enhancement only + */ + @Deprecated + protected JPASieveScript() { + } + + private JPASieveScript(String username, String scriptName, String scriptContent, long scriptSize, boolean isActive, OffsetDateTime activationDateTime) { + this.username = username; + this.scriptName = scriptName; + this.scriptContent = scriptContent; + this.scriptSize = scriptSize; + this.isActive = isActive; + this.activationDateTime = activationDateTime; + } + + public String getUsername() { + return username; + } + + public String getScriptName() { + return scriptName; + } + + public String getScriptContent() { + return scriptContent; + } + + public long getScriptSize() { + return scriptSize; + } + + public boolean isActive() { + return isActive; + } + + public OffsetDateTime getActivationDateTime() { + return activationDateTime; + } + + public void activate() { + this.isActive = true; + this.activationDateTime = OffsetDateTime.now(); + } + + public void deactivate() { + this.isActive = false; + this.activationDateTime = null; + } + + public void renameTo(ScriptName newName) { + this.scriptName = newName.getValue(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + JPASieveScript that = (JPASieveScript) o; + return Objects.equals(uuid, that.uuid); + } + + @Override + public int hashCode() { + return Objects.hash(uuid); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("uuid", uuid) + .add("username", username) + .add("scriptName", scriptName) + .add("isActive", isActive) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/data/data-jpa/src/main/resources/META-INF/persistence.xml ---------------------------------------------------------------------- diff --git a/server/data/data-jpa/src/main/resources/META-INF/persistence.xml b/server/data/data-jpa/src/main/resources/META-INF/persistence.xml index 0e43be9..1927b87 100644 --- a/server/data/data-jpa/src/main/resources/META-INF/persistence.xml +++ b/server/data/data-jpa/src/main/resources/META-INF/persistence.xml @@ -30,6 +30,8 @@ <class>org.apache.james.user.jpa.model.JPAUser</class> <class>org.apache.james.rrt.jpa.model.JPARecipientRewrite</class> <class>org.apache.james.mailrepository.jpa.JPAUrl</class> + <class>org.apache.james.sieve.jpa.model.JPASieveQuota</class> + <class>org.apache.james.sieve.jpa.model.JPASieveScript</class> <exclude-unlisted-classes>true</exclude-unlisted-classes> <properties> <property name="openjpa.jdbc.SynchronizeMappings" value="buildSchema(ForeignKeys=true)"/> http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/data/data-jpa/src/test/java/org/apache/james/sieve/jpa/JpaSieveRepositoryTest.java ---------------------------------------------------------------------- diff --git a/server/data/data-jpa/src/test/java/org/apache/james/sieve/jpa/JpaSieveRepositoryTest.java b/server/data/data-jpa/src/test/java/org/apache/james/sieve/jpa/JpaSieveRepositoryTest.java new file mode 100644 index 0000000..660515c --- /dev/null +++ b/server/data/data-jpa/src/test/java/org/apache/james/sieve/jpa/JpaSieveRepositoryTest.java @@ -0,0 +1,50 @@ +/**************************************************************** + * 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.sieve.jpa; + +import org.apache.james.backends.jpa.JpaTestCluster; +import org.apache.james.sieve.jpa.model.JPASieveQuota; +import org.apache.james.sieve.jpa.model.JPASieveScript; +import org.apache.james.sieverepository.api.SieveRepository; +import org.apache.james.sieverepository.lib.AbstractSieveRepositoryTest; + +import org.junit.After; +import org.junit.Before; + +public class JpaSieveRepositoryTest extends AbstractSieveRepositoryTest { + + private static final JpaTestCluster JPA_TEST_CLUSTER = JpaTestCluster.create(JPASieveScript.class, JPASieveQuota.class); + + @Override + @Before + public void setUp() throws Exception { + super.setUp(); + } + + @After + public void tearDown() throws Exception { + JPA_TEST_CLUSTER.clear("JAMES_SIEVE_SCRIPT", "JAMES_SIEVE_QUOTA"); + } + + @Override + protected SieveRepository createSieveRepository() throws Exception { + return new JPASieveRepository(JPA_TEST_CLUSTER.getEntityManagerFactory()); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/d5d5c9cb/server/data/data-library/src/test/java/org/apache/james/sieverepository/lib/AbstractSieveRepositoryTest.java ---------------------------------------------------------------------- diff --git a/server/data/data-library/src/test/java/org/apache/james/sieverepository/lib/AbstractSieveRepositoryTest.java b/server/data/data-library/src/test/java/org/apache/james/sieverepository/lib/AbstractSieveRepositoryTest.java index 266d8b8..eaf9746 100644 --- a/server/data/data-library/src/test/java/org/apache/james/sieverepository/lib/AbstractSieveRepositoryTest.java +++ b/server/data/data-library/src/test/java/org/apache/james/sieverepository/lib/AbstractSieveRepositoryTest.java @@ -345,6 +345,13 @@ public abstract class AbstractSieveRepositoryTest { assertThat(sieveRepository.getQuota(USER)).isEqualTo(DEFAULT_QUOTA); } + @Test + public void setQuotaShouldOverrideExistingQuota() throws Exception { + sieveRepository.setQuota(USER, USER_QUOTA); + sieveRepository.setQuota(USER, QuotaSize.size(USER_QUOTA.asLong() - 1)); + assertThat(sieveRepository.getQuota(USER)).isEqualTo(QuotaSize.size(USER_QUOTA.asLong() - 1)); + } + protected ScriptContent getScriptContent(InputStream inputStream) throws IOException { return new ScriptContent(IOUtils.toString(inputStream, StandardCharsets.UTF_8)); } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
