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


Reply via email to