This is an automated email from the ASF dual-hosted git repository. btellier pushed a commit to branch postgresql in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 4e56c426df82e2adad36a0bbc019b44e02ccf260 Author: Quan Tran <hqt...@linagora.com> AuthorDate: Mon Dec 11 17:45:11 2023 +0700 JAMES-2586 Implement PostgresSieveScriptDAO + PostgresSieveRepository --- .../backends/postgres/utils/PostgresExecutor.java | 8 + .../lib/SieveRepositoryContract.java | 11 + .../james/sieve/postgres/PostgresSieveModule.java | 65 +++++ .../sieve/postgres/PostgresSieveRepository.java | 279 ++++++++------------- .../sieve/postgres/PostgresSieveScriptDAO.java | 152 +++++++++++ .../sieve/postgres/model/PostgresSieveScript.java | 148 +++++++++++ .../postgres/PostgresSieveRepositoryTest.java | 18 +- 7 files changed, 493 insertions(+), 188 deletions(-) diff --git a/backends-common/postgres/src/main/java/org/apache/james/backends/postgres/utils/PostgresExecutor.java b/backends-common/postgres/src/main/java/org/apache/james/backends/postgres/utils/PostgresExecutor.java index 686145dbeb..dceb4ac06f 100644 --- a/backends-common/postgres/src/main/java/org/apache/james/backends/postgres/utils/PostgresExecutor.java +++ b/backends-common/postgres/src/main/java/org/apache/james/backends/postgres/utils/PostgresExecutor.java @@ -111,6 +111,14 @@ public class PostgresExecutor { .map(Record1::value1); } + public Mono<Long> executeReturnAffectedRowsCount(Function<DSLContext, Mono<Integer>> queryFunction) { + return dslContext() + .flatMap(queryFunction) + .cast(Long.class) + .retryWhen(Retry.backoff(MAX_RETRY_ATTEMPTS, MIN_BACKOFF) + .filter(preparedStatementConflictException())); + } + public Mono<Connection> connection() { return connection; } diff --git a/server/data/data-library/src/test/java/org/apache/james/sieverepository/lib/SieveRepositoryContract.java b/server/data/data-library/src/test/java/org/apache/james/sieverepository/lib/SieveRepositoryContract.java index 97f5841423..91531749a6 100644 --- a/server/data/data-library/src/test/java/org/apache/james/sieverepository/lib/SieveRepositoryContract.java +++ b/server/data/data-library/src/test/java/org/apache/james/sieverepository/lib/SieveRepositoryContract.java @@ -185,6 +185,17 @@ public interface SieveRepositoryContract { .isInstanceOf(ScriptNotFoundException.class); } + @Test + default void setActiveScriptOnNonExistingScriptShouldNotDeactivateTheCurrentActiveScript() throws Exception { + sieveRepository().putScript(USERNAME, SCRIPT_NAME, SCRIPT_CONTENT); + sieveRepository().setActive(USERNAME, SCRIPT_NAME); + + assertThatThrownBy(() -> sieveRepository().setActive(USERNAME, OTHER_SCRIPT_NAME)) + .isInstanceOf(ScriptNotFoundException.class); + + assertThat(getScriptContent(sieveRepository().getActive(USERNAME))).isEqualTo(SCRIPT_CONTENT); + } + @Test default void setActiveScriptShouldWork() throws Exception { sieveRepository().putScript(USERNAME, SCRIPT_NAME, SCRIPT_CONTENT); diff --git a/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveModule.java b/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveModule.java new file mode 100644 index 0000000000..b6780f9e63 --- /dev/null +++ b/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveModule.java @@ -0,0 +1,65 @@ +/**************************************************************** + * 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.postgres; + +import java.time.OffsetDateTime; + +import org.apache.james.backends.postgres.PostgresIndex; +import org.apache.james.backends.postgres.PostgresModule; +import org.apache.james.backends.postgres.PostgresTable; +import org.jooq.Field; +import org.jooq.Record; +import org.jooq.Table; +import org.jooq.impl.DSL; +import org.jooq.impl.SQLDataType; + +public interface PostgresSieveModule { + interface PostgresSieveScriptTable { + Table<Record> TABLE_NAME = DSL.table("sieve_scripts"); + + Field<String> USERNAME = DSL.field("username", SQLDataType.VARCHAR(255).notNull()); + Field<String> SCRIPT_NAME = DSL.field("script_name", SQLDataType.VARCHAR.notNull()); + Field<Long> SCRIPT_SIZE = DSL.field("script_size", SQLDataType.BIGINT.notNull()); + Field<String> SCRIPT_CONTENT = DSL.field("script_content", SQLDataType.VARCHAR.notNull()); + Field<Boolean> IS_ACTIVE = DSL.field("is_active", SQLDataType.BOOLEAN.notNull()); + Field<OffsetDateTime> ACTIVATION_DATE_TIME = DSL.field("activation_date_time", SQLDataType.OFFSETDATETIME); + + PostgresTable TABLE = PostgresTable.name(TABLE_NAME.getName()) + .createTableStep(((dsl, tableName) -> dsl.createTableIfNotExists(tableName) + .column(USERNAME) + .column(SCRIPT_NAME) + .column(SCRIPT_SIZE) + .column(SCRIPT_CONTENT) + .column(IS_ACTIVE) + .column(ACTIVATION_DATE_TIME) + .primaryKey(USERNAME, SCRIPT_NAME))) + .disableRowLevelSecurity(); + + PostgresIndex MAXIMUM_ONE_ACTIVE_SCRIPT_PER_USER_UNIQUE_INDEX = PostgresIndex.name("maximum_one_active_script_per_user") + .createIndexStep(((dsl, indexName) -> dsl.createUniqueIndexIfNotExists(indexName) + .on(TABLE_NAME, USERNAME) + .where(IS_ACTIVE))); + } + + PostgresModule MODULE = PostgresModule.builder() + .addTable(PostgresSieveScriptTable.TABLE) + .addIndex(PostgresSieveScriptTable.MAXIMUM_ONE_ACTIVE_SCRIPT_PER_USER_UNIQUE_INDEX) + .build(); +} diff --git a/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveRepository.java b/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveRepository.java index 544ef43999..662915c523 100644 --- a/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveRepository.java +++ b/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveRepository.java @@ -19,27 +19,21 @@ package org.apache.james.sieve.postgres; +import static org.apache.james.backends.postgres.utils.PostgresUtils.UNIQUE_CONSTRAINT_VIOLATION_PREDICATE; + 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.Consumer; -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.Username; import org.apache.james.core.quota.QuotaSizeLimit; import org.apache.james.core.quota.QuotaSizeUsage; -import org.apache.james.sieve.postgres.model.JPASieveScript; +import org.apache.james.sieve.postgres.model.PostgresSieveScript; import org.apache.james.sieverepository.api.ScriptContent; import org.apache.james.sieverepository.api.ScriptName; import org.apache.james.sieverepository.api.ScriptSummary; @@ -49,217 +43,166 @@ 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.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.github.fge.lambdas.Throwing; -import com.google.common.collect.ImmutableList; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; public class PostgresSieveRepository implements SieveRepository { - - private static final Logger LOGGER = LoggerFactory.getLogger(PostgresSieveRepository.class); - - private final TransactionRunner transactionRunner; private final PostgresSieveQuotaDAO postgresSieveQuotaDAO; + private final PostgresSieveScriptDAO postgresSieveScriptDAO; @Inject - public PostgresSieveRepository(EntityManagerFactory entityManagerFactory, PostgresSieveQuotaDAO postgresSieveQuotaDAO) { - this.transactionRunner = new TransactionRunner(entityManagerFactory); + public PostgresSieveRepository(PostgresSieveQuotaDAO postgresSieveQuotaDAO, + PostgresSieveScriptDAO postgresSieveScriptDAO) { this.postgresSieveQuotaDAO = postgresSieveQuotaDAO; + this.postgresSieveScriptDAO = postgresSieveScriptDAO; } @Override - public void haveSpace(Username username, ScriptName name, long size) throws QuotaExceededException, StorageException { - long usedSpace = findAllSieveScriptsForUser(username).stream() - .filter(sieveScript -> !sieveScript.getScriptName().equals(name.getValue())) - .mapToLong(JPASieveScript::getScriptSize) - .sum(); - - QuotaSizeLimit quota = limitToUser(username); - if (overQuotaAfterModification(usedSpace, size, quota)) { - throw new QuotaExceededException(); + public void haveSpace(Username username, ScriptName name, long size) throws QuotaExceededException { + long sizeDifference = spaceThatWillBeUsedByNewScript(username, name, size).block(); + throwOnOverQuota(username, sizeDifference); + } + + @Override + public void putScript(Username username, ScriptName name, ScriptContent content) throws QuotaExceededException { + long sizeDifference = spaceThatWillBeUsedByNewScript(username, name, content.length()).block(); + throwOnOverQuota(username, sizeDifference); + postgresSieveScriptDAO.upsertScript(PostgresSieveScript.builder() + .username(username.asString()) + .scriptName(name.getValue()) + .scriptContent(content.getValue()) + .scriptSize(content.length()) + .isActive(false) + .build()) + .flatMap(upsertedScripts -> { + if (upsertedScripts > 0) { + return updateSpaceUsed(username, sizeDifference); + } + return Mono.empty(); + }) + .block(); + } + + private Mono<Void> updateSpaceUsed(Username username, long spaceToUse) { + if (spaceToUse == 0) { + return Mono.empty(); } + return postgresSieveQuotaDAO.updateSpaceUsed(username, spaceToUse); } - private QuotaSizeLimit limitToUser(Username username) { - return postgresSieveQuotaDAO.getQuota(username) - .filter(Optional::isPresent) - .switchIfEmpty(postgresSieveQuotaDAO.getGlobalQuota()) - .block() - .orElse(QuotaSizeLimit.unlimited()); + private Mono<Long> spaceThatWillBeUsedByNewScript(Username username, ScriptName name, long scriptSize) { + return postgresSieveScriptDAO.getScriptSize(username, name) + .defaultIfEmpty(0L) + .map(sizeOfStoredScript -> scriptSize - sizeOfStoredScript); } - private boolean overQuotaAfterModification(long usedSpace, long size, QuotaSizeLimit quota) { - return QuotaSizeUsage.size(usedSpace) - .add(size) - .exceedLimit(quota); + private void throwOnOverQuota(Username username, Long sizeDifference) throws QuotaExceededException { + long spaceUsed = postgresSieveQuotaDAO.spaceUsedBy(username).block(); + QuotaSizeLimit limit = limitToUser(username).block(); + + if (QuotaSizeUsage.size(spaceUsed) + .add(sizeDifference) + .exceedLimit(limit)) { + throw new QuotaExceededException(); + } } - @Override - public void putScript(Username username, ScriptName name, ScriptContent content) { - transactionRunner.runAndHandleException(Throwing.<EntityManager>consumer(entityManager -> { - try { - haveSpace(username, name, content.length()); - JPASieveScript jpaSieveScript = JPASieveScript.builder() - .username(username.asString()) - .scriptName(name.getValue()) - .scriptContent(content) - .build(); - entityManager.persist(jpaSieveScript); - } catch (QuotaExceededException | StorageException e) { - rollbackTransactionIfActive(entityManager.getTransaction()); - throw e; - } - }).sneakyThrow(), throwStorageExceptionConsumer("Unable to put script for user " + username.asString())); + private Mono<QuotaSizeLimit> limitToUser(Username username) { + return postgresSieveQuotaDAO.getQuota(username) + .filter(Optional::isPresent) + .switchIfEmpty(postgresSieveQuotaDAO.getGlobalQuota()) + .map(optional -> optional.orElse(QuotaSizeLimit.unlimited())); } @Override public List<ScriptSummary> listScripts(Username username) { - return findAllSieveScriptsForUser(username).stream() - .map(JPASieveScript::toSummary) - .collect(ImmutableList.toImmutableList()); + return listScriptsReactive(username) + .collectList() + .block(); } @Override public Flux<ScriptSummary> listScriptsReactive(Username username) { - return Mono.fromCallable(() -> listScripts(username)).flatMapMany(Flux::fromIterable); - } - - private List<JPASieveScript> findAllSieveScriptsForUser(Username username) { - return transactionRunner.runAndRetrieveResult(entityManager -> { - List<JPASieveScript> sieveScripts = entityManager.createNamedQuery("findAllByUsername", JPASieveScript.class) - .setParameter("username", username.asString()).getResultList(); - return Optional.ofNullable(sieveScripts).orElse(ImmutableList.of()); - }, throwStorageException("Unable to list scripts for user " + username.asString())); + return postgresSieveScriptDAO.getScripts(username) + .map(PostgresSieveScript::toScriptSummary); } @Override public ZonedDateTime getActivationDateForActiveScript(Username username) throws ScriptNotFoundException { - Optional<JPASieveScript> script = findActiveSieveScript(username); - JPASieveScript activeSieveScript = script.orElseThrow(() -> new ScriptNotFoundException("Unable to find active script for user " + username.asString())); - return activeSieveScript.getActivationDateTime().toZonedDateTime(); + return postgresSieveScriptDAO.getActiveScript(username) + .blockOptional() + .orElseThrow(ScriptNotFoundException::new) + .getActivationDateTime() + .toZonedDateTime(); } @Override public InputStream getActive(Username username) throws ScriptNotFoundException { - Optional<JPASieveScript> script = findActiveSieveScript(username); - JPASieveScript activeSieveScript = script.orElseThrow(() -> new ScriptNotFoundException("Unable to find active script for user " + username.asString())); - return IOUtils.toInputStream(activeSieveScript.getScriptContent(), StandardCharsets.UTF_8); - } - - private Optional<JPASieveScript> findActiveSieveScript(Username username) { - return transactionRunner.runAndRetrieveResult( - Throwing.<EntityManager, Optional<JPASieveScript>>function(entityManager -> findActiveSieveScript(username, entityManager)).sneakyThrow(), - throwStorageException("Unable to find active script for user " + username.asString())); + return IOUtils.toInputStream(postgresSieveScriptDAO.getActiveScript(username) + .blockOptional() + .orElseThrow(ScriptNotFoundException::new) + .getScriptContent(), StandardCharsets.UTF_8); } - private Optional<JPASieveScript> findActiveSieveScript(Username username, EntityManager entityManager) { - try { - JPASieveScript activeSieveScript = entityManager.createNamedQuery("findActiveByUsername", JPASieveScript.class) - .setParameter("username", username.asString()).getSingleResult(); - return Optional.ofNullable(activeSieveScript); - } catch (NoResultException e) { - LOGGER.debug("Sieve script not found for user {}", username.asString()); - return Optional.empty(); + @Override + public void setActive(Username username, ScriptName name) throws ScriptNotFoundException { + if (SieveRepository.NO_SCRIPT_NAME.equals(name)) { + switchOffCurrentActiveScript(username); + } else { + throwOnScriptNonExistence(username, name); + switchOffCurrentActiveScript(username); + activateScript(username, name); } } - @Override - public void setActive(Username username, ScriptName name) { - transactionRunner.runAndHandleException(Throwing.<EntityManager>consumer(entityManager -> { - try { - if (SieveRepository.NO_SCRIPT_NAME.equals(name)) { - switchOffActiveScript(username, entityManager); - } else { - setActiveScript(username, name, entityManager); - } - } catch (StorageException | ScriptNotFoundException e) { - rollbackTransactionIfActive(entityManager.getTransaction()); - throw e; - } - }).sneakyThrow(), throwStorageExceptionConsumer("Unable to set active script " + name.getValue() + " for user " + username.asString())); + private void throwOnScriptNonExistence(Username username, ScriptName name) throws ScriptNotFoundException { + if (!postgresSieveScriptDAO.scriptExists(username, name).block()) { + throw new ScriptNotFoundException(); + } } - private void switchOffActiveScript(Username username, EntityManager entityManager) throws StorageException { - Optional<JPASieveScript> activeSieveScript = findActiveSieveScript(username, entityManager); - activeSieveScript.ifPresent(JPASieveScript::deactivate); + private void switchOffCurrentActiveScript(Username username) { + postgresSieveScriptDAO.deactivateCurrentActiveScript(username).block(); } - private void setActiveScript(Username username, ScriptName name, EntityManager entityManager) throws StorageException, ScriptNotFoundException { - JPASieveScript sieveScript = findSieveScript(username, name, entityManager) - .orElseThrow(() -> new ScriptNotFoundException("Unable to find script " + name.getValue() + " for user " + username.asString())); - findActiveSieveScript(username, entityManager).ifPresent(JPASieveScript::deactivate); - sieveScript.activate(); + private void activateScript(Username username, ScriptName scriptName) { + postgresSieveScriptDAO.activateScript(username, scriptName).block(); } @Override public InputStream getScript(Username username, ScriptName name) throws ScriptNotFoundException { - Optional<JPASieveScript> script = findSieveScript(username, name); - JPASieveScript sieveScript = script.orElseThrow(() -> new ScriptNotFoundException("Unable to find script " + name.getValue() + " for user " + username.asString())); - return IOUtils.toInputStream(sieveScript.getScriptContent(), StandardCharsets.UTF_8); + return IOUtils.toInputStream(postgresSieveScriptDAO.getScript(username, name) + .blockOptional() + .orElseThrow(ScriptNotFoundException::new) + .getScriptContent(), StandardCharsets.UTF_8); } - private Optional<JPASieveScript> findSieveScript(Username username, ScriptName scriptName) { - return transactionRunner.runAndRetrieveResult(entityManager -> findSieveScript(username, scriptName, entityManager), - throwStorageException("Unable to find script " + scriptName.getValue() + " for user " + username.asString())); - } + @Override + public void deleteScript(Username username, ScriptName name) throws ScriptNotFoundException, IsActiveException { + boolean isActive = postgresSieveScriptDAO.getIsActive(username, name) + .blockOptional() + .orElseThrow(ScriptNotFoundException::new); - private Optional<JPASieveScript> findSieveScript(Username username, ScriptName scriptName, EntityManager entityManager) { - try { - JPASieveScript sieveScript = entityManager.createNamedQuery("findSieveScript", JPASieveScript.class) - .setParameter("username", username.asString()) - .setParameter("scriptName", scriptName.getValue()).getSingleResult(); - return Optional.ofNullable(sieveScript); - } catch (NoResultException e) { - LOGGER.debug("Sieve script not found for user {}", username.asString()); - return Optional.empty(); + if (isActive) { + throw new IsActiveException(); } - } - @Override - public void deleteScript(Username username, ScriptName name) { - transactionRunner.runAndHandleException(Throwing.<EntityManager>consumer(entityManager -> { - Optional<JPASieveScript> sieveScript = findSieveScript(username, name, entityManager); - if (!sieveScript.isPresent()) { - rollbackTransactionIfActive(entityManager.getTransaction()); - throw new ScriptNotFoundException("Unable to find script " + name.getValue() + " for user " + username.asString()); - } - JPASieveScript sieveScriptToRemove = sieveScript.get(); - if (sieveScriptToRemove.isActive()) { - rollbackTransactionIfActive(entityManager.getTransaction()); - throw new IsActiveException("Unable to delete active script " + name.getValue() + " for user " + username.asString()); - } - entityManager.remove(sieveScriptToRemove); - }).sneakyThrow(), throwStorageExceptionConsumer("Unable to delete script " + name.getValue() + " for user " + username.asString())); + postgresSieveScriptDAO.deleteScript(username, name).block(); } @Override - public void renameScript(Username username, ScriptName oldName, ScriptName newName) { - transactionRunner.runAndHandleException(Throwing.<EntityManager>consumer(entityManager -> { - Optional<JPASieveScript> sieveScript = findSieveScript(username, oldName, entityManager); - if (!sieveScript.isPresent()) { - rollbackTransactionIfActive(entityManager.getTransaction()); - throw new ScriptNotFoundException("Unable to find script " + oldName.getValue() + " for user " + username.asString()); + public void renameScript(Username username, ScriptName oldName, ScriptName newName) throws DuplicateException, ScriptNotFoundException { + try { + long renamedScripts = postgresSieveScriptDAO.renameScript(username, oldName, newName).block(); + if (renamedScripts == 0) { + throw new ScriptNotFoundException(); } - - Optional<JPASieveScript> duplicatedSieveScript = findSieveScript(username, newName, entityManager); - if (duplicatedSieveScript.isPresent()) { - rollbackTransactionIfActive(entityManager.getTransaction()); - throw new DuplicateException("Unable to rename script. Duplicate found " + newName.getValue() + " for user " + username.asString()); + } catch (Exception e) { + if (UNIQUE_CONSTRAINT_VIOLATION_PREDICATE.test(e)) { + throw new DuplicateException(); } - - JPASieveScript sieveScriptToRename = sieveScript.get(); - sieveScriptToRename.renameTo(newName); - }).sneakyThrow(), throwStorageExceptionConsumer("Unable to rename script " + oldName.getValue() + " for user " + username.asString())); - } - - private void rollbackTransactionIfActive(EntityTransaction transaction) { - if (transaction.isActive()) { - transaction.rollback(); + throw e; } } @@ -316,18 +259,6 @@ public class PostgresSieveRepository implements SieveRepository { postgresSieveQuotaDAO.removeQuota(username).block(); } - private <T> Function<PersistenceException, T> throwStorageException(String message) { - return Throwing.<PersistenceException, T>function(e -> { - throw new StorageException(message, e); - }).sneakyThrow(); - } - - private Consumer<PersistenceException> throwStorageExceptionConsumer(String message) { - return Throwing.<PersistenceException>consumer(e -> { - throw new StorageException(message, e); - }).sneakyThrow(); - } - @Override public Mono<Void> resetSpaceUsedReactive(Username username, long spaceUsed) { return Mono.error(new UnsupportedOperationException()); diff --git a/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveScriptDAO.java b/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveScriptDAO.java new file mode 100644 index 0000000000..b87778db9f --- /dev/null +++ b/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/PostgresSieveScriptDAO.java @@ -0,0 +1,152 @@ +/**************************************************************** + * 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.postgres; + +import static org.apache.james.backends.postgres.utils.PostgresExecutor.DEFAULT_INJECT; +import static org.apache.james.sieve.postgres.PostgresSieveModule.PostgresSieveScriptTable.ACTIVATION_DATE_TIME; +import static org.apache.james.sieve.postgres.PostgresSieveModule.PostgresSieveScriptTable.IS_ACTIVE; +import static org.apache.james.sieve.postgres.PostgresSieveModule.PostgresSieveScriptTable.SCRIPT_CONTENT; +import static org.apache.james.sieve.postgres.PostgresSieveModule.PostgresSieveScriptTable.SCRIPT_NAME; +import static org.apache.james.sieve.postgres.PostgresSieveModule.PostgresSieveScriptTable.SCRIPT_SIZE; +import static org.apache.james.sieve.postgres.PostgresSieveModule.PostgresSieveScriptTable.TABLE_NAME; +import static org.apache.james.sieve.postgres.PostgresSieveModule.PostgresSieveScriptTable.USERNAME; + +import java.time.OffsetDateTime; +import java.util.function.Function; + +import javax.inject.Inject; +import javax.inject.Named; + +import org.apache.james.backends.postgres.utils.PostgresExecutor; +import org.apache.james.core.Username; +import org.apache.james.sieve.postgres.model.PostgresSieveScript; +import org.apache.james.sieverepository.api.ScriptName; +import org.jooq.Record; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +public class PostgresSieveScriptDAO { + private final PostgresExecutor postgresExecutor; + + @Inject + public PostgresSieveScriptDAO(@Named(DEFAULT_INJECT) PostgresExecutor postgresExecutor) { + this.postgresExecutor = postgresExecutor; + } + + public Mono<Long> upsertScript(PostgresSieveScript sieveScript) { + return postgresExecutor.executeReturnAffectedRowsCount(dslContext -> Mono.from(dslContext.insertInto(TABLE_NAME) + .set(USERNAME, sieveScript.getUsername()) + .set(SCRIPT_NAME, sieveScript.getScriptName()) + .set(SCRIPT_SIZE, sieveScript.getScriptSize()) + .set(SCRIPT_CONTENT, sieveScript.getScriptContent()) + .set(IS_ACTIVE, sieveScript.isActive()) + .set(ACTIVATION_DATE_TIME, sieveScript.getActivationDateTime()) + .onConflict(USERNAME, SCRIPT_NAME) + .doUpdate() + .set(SCRIPT_SIZE, sieveScript.getScriptSize()) + .set(SCRIPT_CONTENT, sieveScript.getScriptContent()) + .set(IS_ACTIVE, sieveScript.isActive()) + .set(ACTIVATION_DATE_TIME, sieveScript.getActivationDateTime()))); + } + + public Mono<PostgresSieveScript> getScript(Username username, ScriptName scriptName) { + return postgresExecutor.executeRow(dslContext -> Mono.from(dslContext.selectFrom(TABLE_NAME) + .where(USERNAME.eq(username.asString()), + SCRIPT_NAME.eq(scriptName.getValue())))) + .map(recordToPostgresSieveScript()); + } + + public Mono<Long> getScriptSize(Username username, ScriptName scriptName) { + return postgresExecutor.executeRow(dslContext -> Mono.from(dslContext.select(SCRIPT_SIZE) + .from(TABLE_NAME) + .where(USERNAME.eq(username.asString()), + SCRIPT_NAME.eq(scriptName.getValue())))) + .map(record -> record.get(SCRIPT_SIZE)); + } + + public Mono<Boolean> getIsActive(Username username, ScriptName scriptName) { + return postgresExecutor.executeRow(dslContext -> Mono.from(dslContext.select(IS_ACTIVE) + .from(TABLE_NAME) + .where(USERNAME.eq(username.asString()), + SCRIPT_NAME.eq(scriptName.getValue())))) + .map(record -> record.get(IS_ACTIVE)); + } + + public Mono<Boolean> scriptExists(Username username, ScriptName scriptName) { + return postgresExecutor.executeCount(dslContext -> Mono.from(dslContext.selectCount() + .from(TABLE_NAME) + .where(USERNAME.eq(username.asString()), + SCRIPT_NAME.eq(scriptName.getValue())))) + .map(count -> count > 0); + } + + public Flux<PostgresSieveScript> getScripts(Username username) { + return postgresExecutor.executeRows(dslContext -> Flux.from(dslContext.selectFrom(TABLE_NAME) + .where(USERNAME.eq(username.asString())))) + .map(recordToPostgresSieveScript()); + } + + public Mono<PostgresSieveScript> getActiveScript(Username username) { + return postgresExecutor.executeRow(dslContext -> Mono.from(dslContext.selectFrom(TABLE_NAME) + .where(USERNAME.eq(username.asString()), + IS_ACTIVE.eq(true)))) + .map(recordToPostgresSieveScript()); + } + + public Mono<Void> activateScript(Username username, ScriptName scriptName) { + return postgresExecutor.executeVoid(dslContext -> Mono.from(dslContext.update(TABLE_NAME) + .set(IS_ACTIVE, true) + .set(ACTIVATION_DATE_TIME, OffsetDateTime.now()) + .where(USERNAME.eq(username.asString()), + SCRIPT_NAME.eq(scriptName.getValue())))); + } + + public Mono<Void> deactivateCurrentActiveScript(Username username) { + return postgresExecutor.executeVoid(dslContext -> Mono.from(dslContext.update(TABLE_NAME) + .set(IS_ACTIVE, false) + .where(USERNAME.eq(username.asString()), + IS_ACTIVE.eq(true)))); + } + + public Mono<Long> renameScript(Username username, ScriptName oldName, ScriptName newName) { + return postgresExecutor.executeReturnAffectedRowsCount(dslContext -> Mono.from(dslContext.update(TABLE_NAME) + .set(SCRIPT_NAME, newName.getValue()) + .where(USERNAME.eq(username.asString()), + SCRIPT_NAME.eq(oldName.getValue())))); + } + + public Mono<Void> deleteScript(Username username, ScriptName scriptName) { + return postgresExecutor.executeVoid(dslContext -> Mono.from(dslContext.deleteFrom(TABLE_NAME) + .where(USERNAME.eq(username.asString()), + SCRIPT_NAME.eq(scriptName.getValue())))); + } + + private Function<Record, PostgresSieveScript> recordToPostgresSieveScript() { + return record -> PostgresSieveScript.builder() + .username(record.get(USERNAME)) + .scriptName(record.get(SCRIPT_NAME)) + .scriptContent(record.get(SCRIPT_CONTENT)) + .scriptSize(record.get(SCRIPT_SIZE)) + .isActive(record.get(IS_ACTIVE)) + .activationDateTime(record.get(ACTIVATION_DATE_TIME)) + .build(); + } +} diff --git a/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/model/PostgresSieveScript.java b/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/model/PostgresSieveScript.java new file mode 100644 index 0000000000..d5831d5464 --- /dev/null +++ b/server/data/data-postgres/src/main/java/org/apache/james/sieve/postgres/model/PostgresSieveScript.java @@ -0,0 +1,148 @@ +/**************************************************************** + * 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.postgres.model; + +import java.time.OffsetDateTime; +import java.util.Objects; + +import org.apache.commons.lang3.StringUtils; +import org.apache.james.sieverepository.api.ScriptName; +import org.apache.james.sieverepository.api.ScriptSummary; + +import com.google.common.base.Preconditions; + +public class PostgresSieveScript { + + public static Builder builder() { + return new Builder(); + } + + 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(String scriptContent) { + this.scriptContent = scriptContent; + return this; + } + + public Builder scriptSize(long scriptSize) { + this.scriptSize = scriptSize; + return this; + } + + public Builder isActive(boolean isActive) { + this.isActive = isActive; + return this; + } + + public Builder activationDateTime(OffsetDateTime offsetDateTime) { + this.activationDateTime = offsetDateTime; + return this; + } + + public PostgresSieveScript build() { + Preconditions.checkState(StringUtils.isNotBlank(username), "'username' is mandatory"); + Preconditions.checkState(StringUtils.isNotBlank(scriptName), "'scriptName' is mandatory"); + + return new PostgresSieveScript(username, scriptName, scriptContent, scriptSize, isActive, activationDateTime); + } + } + + private final String username; + private final String scriptName; + private final String scriptContent; + private final long scriptSize; + private final boolean isActive; + private final OffsetDateTime activationDateTime; + + private PostgresSieveScript(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 ScriptSummary toScriptSummary() { + return new ScriptSummary(new ScriptName(scriptName), isActive, scriptSize); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof PostgresSieveScript) { + PostgresSieveScript that = (PostgresSieveScript) o; + + return Objects.equals(this.scriptSize, that.scriptSize) + && Objects.equals(this.isActive, that.isActive) + && Objects.equals(this.username, that.username) + && Objects.equals(this.scriptName, that.scriptName) + && Objects.equals(this.scriptContent, that.scriptContent) + && Objects.equals(this.activationDateTime, that.activationDateTime); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(username, scriptName); + } +} diff --git a/server/data/data-postgres/src/test/java/org/apache/james/sieve/postgres/PostgresSieveRepositoryTest.java b/server/data/data-postgres/src/test/java/org/apache/james/sieve/postgres/PostgresSieveRepositoryTest.java index b31b1e173a..d67c71069e 100644 --- a/server/data/data-postgres/src/test/java/org/apache/james/sieve/postgres/PostgresSieveRepositoryTest.java +++ b/server/data/data-postgres/src/test/java/org/apache/james/sieve/postgres/PostgresSieveRepositoryTest.java @@ -19,37 +19,27 @@ package org.apache.james.sieve.postgres; -import org.apache.james.backends.jpa.JpaTestCluster; import org.apache.james.backends.postgres.PostgresExtension; import org.apache.james.backends.postgres.PostgresModule; import org.apache.james.backends.postgres.quota.PostgresQuotaCurrentValueDAO; import org.apache.james.backends.postgres.quota.PostgresQuotaLimitDAO; import org.apache.james.backends.postgres.quota.PostgresQuotaModule; -import org.apache.james.sieve.postgres.model.JPASieveScript; import org.apache.james.sieverepository.api.SieveRepository; import org.apache.james.sieverepository.lib.SieveRepositoryContract; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.RegisterExtension; class PostgresSieveRepositoryTest implements SieveRepositoryContract { @RegisterExtension - static PostgresExtension postgresExtension = PostgresExtension.withoutRowLevelSecurity(PostgresModule.aggregateModules(PostgresQuotaModule.MODULE)); - - final JpaTestCluster JPA_TEST_CLUSTER = JpaTestCluster.create(JPASieveScript.class); + static PostgresExtension postgresExtension = PostgresExtension.withoutRowLevelSecurity(PostgresModule.aggregateModules(PostgresQuotaModule.MODULE, + PostgresSieveModule.MODULE)); SieveRepository sieveRepository; @BeforeEach void setUp() { - sieveRepository = new PostgresSieveRepository(JPA_TEST_CLUSTER.getEntityManagerFactory(), - new PostgresSieveQuotaDAO(new PostgresQuotaCurrentValueDAO(postgresExtension.getPostgresExecutor()), - new PostgresQuotaLimitDAO(postgresExtension.getPostgresExecutor()))); - } - - @AfterEach - void tearDown() { - JPA_TEST_CLUSTER.clear("JAMES_SIEVE_SCRIPT"); + sieveRepository = new PostgresSieveRepository(new PostgresSieveQuotaDAO(new PostgresQuotaCurrentValueDAO(postgresExtension.getPostgresExecutor()), new PostgresQuotaLimitDAO(postgresExtension.getPostgresExecutor())), + new PostgresSieveScriptDAO(postgresExtension.getPostgresExecutor())); } @Override --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org