You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2024/02/07 02:25:48 UTC

(james-project) 02/05: JAMES-2586 Implement PostgresVacationRepository

This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch postgresql
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit e9f1a69063b9d40ca473b125c7a92c26e10408c4
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Mon Feb 5 15:21:24 2024 +0700

    JAMES-2586 Implement PostgresVacationRepository
---
 .../backends/postgres/utils/PostgresExecutor.java  |   6 +
 .../vacation/postgres/PostgresVacationModule.java  |  64 +++++++++
 .../postgres/PostgresVacationRepository.java       |  64 +++++++++
 .../postgres/PostgresVacationResponseDAO.java      | 159 +++++++++++++++++++++
 .../postgres/PostgresVacationRepositoryTest.java   |  44 ++++++
 5 files changed, 337 insertions(+)

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 88ccd47746..889c815115 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
@@ -115,6 +115,12 @@ public class PostgresExecutor {
                 .filter(preparedStatementConflictException()));
     }
 
+    public Mono<Optional<Record>> executeSingleRowOptional(Function<DSLContext, Publisher<Record>> queryFunction) {
+        return executeRow(queryFunction)
+            .map(Optional::ofNullable)
+            .switchIfEmpty(Mono.just(Optional.empty()));
+    }
+
     public Mono<Integer> executeCount(Function<DSLContext, Mono<Record1<Integer>>> queryFunction) {
         return dslContext()
             .flatMap(queryFunction)
diff --git a/server/data/data-postgres/src/main/java/org/apache/james/vacation/postgres/PostgresVacationModule.java b/server/data/data-postgres/src/main/java/org/apache/james/vacation/postgres/PostgresVacationModule.java
new file mode 100644
index 0000000000..c1deec31fd
--- /dev/null
+++ b/server/data/data-postgres/src/main/java/org/apache/james/vacation/postgres/PostgresVacationModule.java
@@ -0,0 +1,64 @@
+/****************************************************************
+ * 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.vacation.postgres;
+
+import static org.apache.james.vacation.postgres.PostgresVacationModule.PostgresVacationResponseTable.TABLE;
+
+import java.time.LocalDateTime;
+
+import org.apache.james.backends.postgres.PostgresCommons;
+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 PostgresVacationModule {
+    interface PostgresVacationResponseTable {
+        Table<Record> TABLE_NAME = DSL.table("vacation_response");
+
+        Field<String> ACCOUNT_ID = DSL.field("account_id", SQLDataType.VARCHAR.notNull());
+        Field<Boolean> IS_ENABLED = DSL.field("is_enabled", SQLDataType.BOOLEAN);
+        Field<LocalDateTime> FROM_DATE = DSL.field("from_date", PostgresCommons.DataTypes.TIMESTAMP);
+        Field<LocalDateTime> TO_DATE = DSL.field("to_date", PostgresCommons.DataTypes.TIMESTAMP);
+        Field<String> TEXT = DSL.field("text", SQLDataType.VARCHAR);
+        Field<String> SUBJECT = DSL.field("subject", SQLDataType.VARCHAR);
+        Field<String> HTML = DSL.field("html", SQLDataType.VARCHAR);
+
+        PostgresTable TABLE = PostgresTable.name(TABLE_NAME.getName())
+            .createTableStep(((dsl, tableName) -> dsl.createTableIfNotExists(tableName)
+                .column(ACCOUNT_ID)
+                .column(IS_ENABLED)
+                .column(FROM_DATE)
+                .column(TO_DATE)
+                .column(TEXT)
+                .column(SUBJECT)
+                .column(HTML)
+                .constraint(DSL.primaryKey(ACCOUNT_ID))))
+            .supportsRowLevelSecurity()
+            .build();
+    }
+
+    PostgresModule MODULE = PostgresModule.builder()
+        .addTable(TABLE)
+        .build();
+}
diff --git a/server/data/data-postgres/src/main/java/org/apache/james/vacation/postgres/PostgresVacationRepository.java b/server/data/data-postgres/src/main/java/org/apache/james/vacation/postgres/PostgresVacationRepository.java
new file mode 100644
index 0000000000..6f859c7d97
--- /dev/null
+++ b/server/data/data-postgres/src/main/java/org/apache/james/vacation/postgres/PostgresVacationRepository.java
@@ -0,0 +1,64 @@
+/****************************************************************
+ * 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.vacation.postgres;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import org.apache.james.backends.postgres.utils.PostgresExecutor;
+import org.apache.james.core.Username;
+import org.apache.james.vacation.api.AccountId;
+import org.apache.james.vacation.api.Vacation;
+import org.apache.james.vacation.api.VacationPatch;
+import org.apache.james.vacation.api.VacationRepository;
+
+import com.google.common.base.Preconditions;
+
+import reactor.core.publisher.Mono;
+
+public class PostgresVacationRepository implements VacationRepository {
+    private final PostgresExecutor.Factory executorFactory;
+
+    @Inject
+    @Singleton
+    public PostgresVacationRepository(PostgresExecutor.Factory executorFactory) {
+        this.executorFactory = executorFactory;
+    }
+
+    @Override
+    public Mono<Void> modifyVacation(AccountId accountId, VacationPatch vacationPatch) {
+        Preconditions.checkNotNull(accountId);
+        Preconditions.checkNotNull(vacationPatch);
+        if (vacationPatch.isIdentity()) {
+            return Mono.empty();
+        } else {
+            return vacationResponseDao(accountId).modifyVacation(accountId, vacationPatch);
+        }
+    }
+
+    @Override
+    public Mono<Vacation> retrieveVacation(AccountId accountId) {
+        return vacationResponseDao(accountId).retrieveVacation(accountId).map(optional -> optional.orElse(DEFAULT_VACATION));
+    }
+
+    private PostgresVacationResponseDAO vacationResponseDao(AccountId accountId) {
+        return new PostgresVacationResponseDAO(executorFactory.create(Username.of(accountId.getIdentifier()).getDomainPart()));
+    }
+}
diff --git a/server/data/data-postgres/src/main/java/org/apache/james/vacation/postgres/PostgresVacationResponseDAO.java b/server/data/data-postgres/src/main/java/org/apache/james/vacation/postgres/PostgresVacationResponseDAO.java
new file mode 100644
index 0000000000..8131156da2
--- /dev/null
+++ b/server/data/data-postgres/src/main/java/org/apache/james/vacation/postgres/PostgresVacationResponseDAO.java
@@ -0,0 +1,159 @@
+/****************************************************************
+ * 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.vacation.postgres;
+
+import static org.apache.james.backends.postgres.PostgresCommons.LOCAL_DATE_TIME_ZONED_DATE_TIME_FUNCTION;
+import static org.apache.james.vacation.postgres.PostgresVacationModule.PostgresVacationResponseTable.ACCOUNT_ID;
+import static org.apache.james.vacation.postgres.PostgresVacationModule.PostgresVacationResponseTable.FROM_DATE;
+import static org.apache.james.vacation.postgres.PostgresVacationModule.PostgresVacationResponseTable.HTML;
+import static org.apache.james.vacation.postgres.PostgresVacationModule.PostgresVacationResponseTable.IS_ENABLED;
+import static org.apache.james.vacation.postgres.PostgresVacationModule.PostgresVacationResponseTable.SUBJECT;
+import static org.apache.james.vacation.postgres.PostgresVacationModule.PostgresVacationResponseTable.TABLE_NAME;
+import static org.apache.james.vacation.postgres.PostgresVacationModule.PostgresVacationResponseTable.TEXT;
+import static org.apache.james.vacation.postgres.PostgresVacationModule.PostgresVacationResponseTable.TO_DATE;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Optional;
+import java.util.function.BiFunction;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import org.apache.james.backends.postgres.utils.PostgresExecutor;
+import org.apache.james.util.ValuePatch;
+import org.apache.james.vacation.api.AccountId;
+import org.apache.james.vacation.api.Vacation;
+import org.apache.james.vacation.api.VacationPatch;
+import org.jooq.DSLContext;
+import org.jooq.Field;
+import org.jooq.InsertOnDuplicateSetMoreStep;
+import org.jooq.InsertOnDuplicateSetStep;
+import org.jooq.InsertSetMoreStep;
+import org.jooq.Record;
+
+import reactor.core.publisher.Mono;
+
+public class PostgresVacationResponseDAO {
+    private final PostgresExecutor postgresExecutor;
+
+    public PostgresVacationResponseDAO(PostgresExecutor postgresExecutor) {
+        this.postgresExecutor = postgresExecutor;
+    }
+
+    public Mono<Void> modifyVacation(AccountId accountId, VacationPatch vacationPatch) {
+        return postgresExecutor.executeVoid(dsl -> {
+            if (vacationPatch.isIdentity()) {
+                return Mono.from(insertVacationQuery(accountId, vacationPatch, dsl)
+                    .onConflictDoNothing());
+            } else {
+                return Mono.from(withUpdateOnConflict(vacationPatch, insertVacationQuery(accountId, vacationPatch, dsl)));
+            }
+        });
+    }
+
+    private InsertSetMoreStep<Record> insertVacationQuery(AccountId accountId, VacationPatch vacationPatch, DSLContext dsl) {
+        InsertSetMoreStep<Record> baseInsert = dsl.insertInto(TABLE_NAME)
+            .set(ACCOUNT_ID, accountId.getIdentifier());
+
+        return Stream.of(
+                applyInsertForField(IS_ENABLED, VacationPatch::getIsEnabled),
+                applyInsertForField(SUBJECT, VacationPatch::getSubject),
+                applyInsertForField(HTML, VacationPatch::getHtmlBody),
+                applyInsertForField(TEXT, VacationPatch::getTextBody),
+                applyInsertForFieldZonedDateTime(FROM_DATE, VacationPatch::getFromDate),
+                applyInsertForFieldZonedDateTime(TO_DATE, VacationPatch::getToDate))
+            .reduce((vacation, insert) -> insert,
+                (a, b) -> (vacation, insert) -> b.apply(vacation, a.apply(vacation, insert)))
+            .apply(vacationPatch, baseInsert);
+    }
+
+    private InsertOnDuplicateSetMoreStep<Record> withUpdateOnConflict(VacationPatch vacationPatch, InsertSetMoreStep<Record> insertVacation) {
+        InsertOnDuplicateSetStep<Record> baseUpdateIfConflict = insertVacation.onConflict(ACCOUNT_ID)
+            .doUpdate();
+
+        return (InsertOnDuplicateSetMoreStep<Record>) Stream.of(
+                applyUpdateOnConflictForField(IS_ENABLED, VacationPatch::getIsEnabled),
+                applyUpdateOnConflictForField(SUBJECT, VacationPatch::getSubject),
+                applyUpdateOnConflictForField(HTML, VacationPatch::getHtmlBody),
+                applyUpdateOnConflictForField(TEXT, VacationPatch::getTextBody),
+                applyUpdateOnConflictForFieldZonedDateTime(FROM_DATE, VacationPatch::getFromDate),
+                applyUpdateOnConflictForFieldZonedDateTime(TO_DATE, VacationPatch::getToDate))
+            .reduce((vacation, updateOnConflict) -> updateOnConflict,
+                (a, b) -> (vacation, updateOnConflict) -> b.apply(vacation, a.apply(vacation, updateOnConflict)))
+            .apply(vacationPatch, baseUpdateIfConflict);
+    }
+
+    private <F, V> BiFunction<VacationPatch, InsertSetMoreStep<Record>, InsertSetMoreStep<Record>> applyInsertForField(Field<F> field, Function<VacationPatch, ValuePatch<V>> getter) {
+        return (vacation, insert) ->
+            getter.apply(vacation)
+                .mapNotKeptToOptional(optionalValue -> applyInsertForField(field, optionalValue, insert))
+                .orElse(insert);
+    }
+
+    private <F> BiFunction<VacationPatch, InsertSetMoreStep<Record>, InsertSetMoreStep<Record>> applyInsertForFieldZonedDateTime(Field<F> field, Function<VacationPatch, ValuePatch<ZonedDateTime>> getter) {
+        return (vacation, insert) ->
+            getter.apply(vacation)
+                .mapNotKeptToOptional(optionalValue -> applyInsertForField(field,
+                    optionalValue.map(zonedDateTime -> zonedDateTime.withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime()),
+                    insert))
+                .orElse(insert);
+    }
+
+    private <T> InsertSetMoreStep<Record> applyInsertForField(Field field, Optional<T> value, InsertSetMoreStep<Record> insert) {
+        return insert.set(field, value.orElse(null));
+    }
+
+    private <F, V> BiFunction<VacationPatch, InsertOnDuplicateSetStep<Record>, InsertOnDuplicateSetStep<Record>> applyUpdateOnConflictForField(Field<F> field, Function<VacationPatch, ValuePatch<V>> getter) {
+        return (vacation, update) ->
+            getter.apply(vacation)
+                .mapNotKeptToOptional(optionalValue -> applyUpdateOnConflictForField(field, optionalValue, update))
+                .orElse(update);
+    }
+
+    private <F> BiFunction<VacationPatch, InsertOnDuplicateSetStep<Record>, InsertOnDuplicateSetStep<Record>> applyUpdateOnConflictForFieldZonedDateTime(Field<F> field, Function<VacationPatch, ValuePatch<ZonedDateTime>> getter) {
+        return (vacation, update) ->
+            getter.apply(vacation)
+                .mapNotKeptToOptional(optionalValue -> applyUpdateOnConflictForField(field,
+                    optionalValue.map(zonedDateTime -> zonedDateTime.withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime()),
+                    update))
+                .orElse(update);
+    }
+
+    private <T> InsertOnDuplicateSetStep<Record> applyUpdateOnConflictForField(Field field, Optional<T> value, InsertOnDuplicateSetStep<Record> updateOnConflict) {
+        return updateOnConflict.set(field, value.orElse(null));
+    }
+
+    public Mono<Optional<Vacation>> retrieveVacation(AccountId accountId) {
+        return postgresExecutor.executeSingleRowOptional(dsl -> dsl.selectFrom(TABLE_NAME)
+                .where(ACCOUNT_ID.eq(accountId.getIdentifier())))
+            .map(recordOptional -> recordOptional.map(record -> Vacation.builder()
+                .enabled(Optional.ofNullable(record.get(IS_ENABLED))
+                    .orElse(false))
+                .fromDate(Optional.ofNullable(record.get(FROM_DATE, LocalDateTime.class))
+                    .map(LOCAL_DATE_TIME_ZONED_DATE_TIME_FUNCTION))
+                .toDate(Optional.ofNullable(record.get(TO_DATE, LocalDateTime.class))
+                    .map(LOCAL_DATE_TIME_ZONED_DATE_TIME_FUNCTION))
+                .subject(Optional.ofNullable(record.get(SUBJECT)))
+                .textBody(Optional.ofNullable(record.get(TEXT)))
+                .htmlBody(Optional.ofNullable(record.get(HTML)))
+                .build()));
+    }
+}
\ No newline at end of file
diff --git a/server/data/data-postgres/src/test/java/org/apache/james/vacation/postgres/PostgresVacationRepositoryTest.java b/server/data/data-postgres/src/test/java/org/apache/james/vacation/postgres/PostgresVacationRepositoryTest.java
new file mode 100644
index 0000000000..81488b8677
--- /dev/null
+++ b/server/data/data-postgres/src/test/java/org/apache/james/vacation/postgres/PostgresVacationRepositoryTest.java
@@ -0,0 +1,44 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.vacation.postgres;
+
+import org.apache.james.backends.postgres.PostgresExtension;
+import org.apache.james.backends.postgres.PostgresModule;
+import org.apache.james.vacation.api.VacationRepository;
+import org.apache.james.vacation.api.VacationRepositoryContract;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class PostgresVacationRepositoryTest implements VacationRepositoryContract {
+    @RegisterExtension
+    static PostgresExtension postgresExtension = PostgresExtension.withRowLevelSecurity(PostgresModule.aggregateModules(PostgresVacationModule.MODULE));
+
+    VacationRepository vacationRepository;
+
+    @BeforeEach
+    void setUp() {
+        vacationRepository = new PostgresVacationRepository(postgresExtension.getExecutorFactory());
+    }
+
+    @Override
+    public VacationRepository vacationRepository() {
+        return vacationRepository;
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org