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