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 b367787dae294d519cd588250882e3219405dd9c Author: Rene Cordier <rcord...@linagora.com> AuthorDate: Fri Dec 22 16:13:43 2023 +0700 JAMES-2586 Implement UnseenSearchOverrideTest for Postgresql --- .../mail/dao/PostgresMailboxMessageDAO.java | 40 ++++ .../postgres/search/UnseenSearchOverride.java | 93 +++++++++ .../postgres/search/UnseenSearchOverrideTest.java | 216 +++++++++++++++++++++ 3 files changed, 349 insertions(+) diff --git a/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/mail/dao/PostgresMailboxMessageDAO.java b/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/mail/dao/PostgresMailboxMessageDAO.java index 1c8f032c41..61ff87a764 100644 --- a/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/mail/dao/PostgresMailboxMessageDAO.java +++ b/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/mail/dao/PostgresMailboxMessageDAO.java @@ -118,6 +118,46 @@ public class PostgresMailboxMessageDAO { .map(RECORD_TO_MESSAGE_UID_FUNCTION); } + public Flux<MessageUid> listUnseen(PostgresMailboxId mailboxId) { + return postgresExecutor.executeRows(dslContext -> Flux.from(selectMessageUidByMailboxIdAndExtraConditionQuery(mailboxId, + IS_SEEN.eq(false), Limit.unlimited(), dslContext))) + .map(RECORD_TO_MESSAGE_UID_FUNCTION); + } + + public Flux<MessageUid> listUnseen(PostgresMailboxId mailboxId, MessageRange range) { + switch (range.getType()) { + case ALL: + return listUnseen(mailboxId); + case FROM: + return postgresExecutor.executeRows(dslContext -> Flux.from(dslContext.select(MESSAGE_UID) + .from(TABLE_NAME) + .where(MAILBOX_ID.eq(mailboxId.asUuid())) + .and(IS_SEEN.eq(false)) + .and(MESSAGE_UID.greaterOrEqual(range.getUidFrom().asLong())) + .orderBy(DEFAULT_SORT_ORDER_BY))) + .map(RECORD_TO_MESSAGE_UID_FUNCTION); + case RANGE: + return postgresExecutor.executeRows(dslContext -> Flux.from(dslContext.select(MESSAGE_UID) + .from(TABLE_NAME) + .where(MAILBOX_ID.eq(mailboxId.asUuid())) + .and(IS_SEEN.eq(false)) + .and(MESSAGE_UID.greaterOrEqual(range.getUidFrom().asLong())) + .and(MESSAGE_UID.lessOrEqual(range.getUidTo().asLong())) + .orderBy(DEFAULT_SORT_ORDER_BY))) + .map(RECORD_TO_MESSAGE_UID_FUNCTION); + case ONE: + return postgresExecutor.executeRows(dslContext -> Flux.from(dslContext.select(MESSAGE_UID) + .from(TABLE_NAME) + .where(MAILBOX_ID.eq(mailboxId.asUuid())) + .and(IS_SEEN.eq(false)) + .and(MESSAGE_UID.eq(range.getUidFrom().asLong())) + .orderBy(DEFAULT_SORT_ORDER_BY))) + .map(RECORD_TO_MESSAGE_UID_FUNCTION); + default: + throw new RuntimeException("Unsupported range type " + range.getType()); + } + } + public Flux<MessageUid> findAllRecentMessageUid(PostgresMailboxId mailboxId) { return postgresExecutor.executeRows(dslContext -> Flux.from(selectMessageUidByMailboxIdAndExtraConditionQuery(mailboxId, IS_RECENT.eq(true), Limit.unlimited(), dslContext))) diff --git a/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/search/UnseenSearchOverride.java b/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/search/UnseenSearchOverride.java new file mode 100644 index 0000000000..2bef17a9f9 --- /dev/null +++ b/mailbox/postgres/src/main/java/org/apache/james/mailbox/postgres/search/UnseenSearchOverride.java @@ -0,0 +1,93 @@ +/**************************************************************** + * 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.mailbox.postgres.search; + +import java.util.Optional; + +import javax.inject.Inject; +import javax.mail.Flags; + +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.MessageUid; +import org.apache.james.mailbox.model.Mailbox; +import org.apache.james.mailbox.model.MessageRange; +import org.apache.james.mailbox.model.SearchQuery; +import org.apache.james.mailbox.postgres.PostgresMailboxId; +import org.apache.james.mailbox.postgres.mail.dao.PostgresMailboxMessageDAO; +import org.apache.james.mailbox.store.search.ListeningMessageSearchIndex; + +import com.google.common.collect.ImmutableList; + +import reactor.core.publisher.Flux; + +public class UnseenSearchOverride implements ListeningMessageSearchIndex.SearchOverride { + private final PostgresMailboxMessageDAO dao; + + @Inject + public UnseenSearchOverride(PostgresMailboxMessageDAO dao) { + this.dao = dao; + } + + @Override + public boolean applicable(SearchQuery searchQuery, MailboxSession session) { + return isUnseenWithAll(searchQuery) + || isNotSeenWithAll(searchQuery); + } + + private boolean isUnseenWithAll(SearchQuery searchQuery) { + return searchQuery.getCriteria().contains(SearchQuery.flagIsUnSet(Flags.Flag.SEEN)) + && allMessages(searchQuery) + && searchQuery.getSorts().equals(SearchQuery.DEFAULT_SORTS); + } + + private boolean isNotSeenWithAll(SearchQuery searchQuery) { + return searchQuery.getCriteria().contains(SearchQuery.not(SearchQuery.flagIsSet(Flags.Flag.SEEN))) + && allMessages(searchQuery) + && searchQuery.getSorts().equals(SearchQuery.DEFAULT_SORTS); + } + + private boolean allMessages(SearchQuery searchQuery) { + if (searchQuery.getCriteria().size() == 1) { + // Only the unseen critrion + return true; + } + if (searchQuery.getCriteria().size() == 2) { + return searchQuery.getCriteria().stream() + .anyMatch(criterion -> criterion instanceof SearchQuery.UidCriterion) || + searchQuery.getCriteria().stream() + .anyMatch(criterion -> criterion instanceof SearchQuery.AllCriterion); + } + return false; + } + + @Override + public Flux<MessageUid> search(MailboxSession session, Mailbox mailbox, SearchQuery searchQuery) { + final Optional<SearchQuery.UidCriterion> maybeUidCriterion = searchQuery.getCriteria().stream() + .filter(criterion -> criterion instanceof SearchQuery.UidCriterion) + .map(SearchQuery.UidCriterion.class::cast) + .findFirst(); + + return maybeUidCriterion + .map(uidCriterion -> Flux.fromIterable(ImmutableList.copyOf(uidCriterion.getOperator().getRange())) + .concatMap(range -> dao.listUnseen((PostgresMailboxId) mailbox.getMailboxId(), + MessageRange.range(range.getLowValue(), range.getHighValue())))) + .orElseGet(() -> dao.listUnseen((PostgresMailboxId) mailbox.getMailboxId())); + } +} diff --git a/mailbox/postgres/src/test/java/org/apache/james/mailbox/postgres/search/UnseenSearchOverrideTest.java b/mailbox/postgres/src/test/java/org/apache/james/mailbox/postgres/search/UnseenSearchOverrideTest.java new file mode 100644 index 0000000000..51d8825096 --- /dev/null +++ b/mailbox/postgres/src/test/java/org/apache/james/mailbox/postgres/search/UnseenSearchOverrideTest.java @@ -0,0 +1,216 @@ +/**************************************************************** + * 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.mailbox.postgres.search; + +import static javax.mail.Flags.Flag.SEEN; +import static org.assertj.core.api.Assertions.assertThat; + +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Date; + +import javax.mail.Flags; + +import org.apache.james.backends.postgres.PostgresExtension; +import org.apache.james.core.Username; +import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.MailboxSessionUtil; +import org.apache.james.mailbox.MessageUid; +import org.apache.james.mailbox.ModSeq; +import org.apache.james.mailbox.model.ByteContent; +import org.apache.james.mailbox.model.Mailbox; +import org.apache.james.mailbox.model.MailboxId; +import org.apache.james.mailbox.model.MailboxPath; +import org.apache.james.mailbox.model.SearchQuery; +import org.apache.james.mailbox.model.ThreadId; +import org.apache.james.mailbox.model.UidValidity; +import org.apache.james.mailbox.postgres.PostgresMailboxAggregateModule; +import org.apache.james.mailbox.postgres.PostgresMailboxId; +import org.apache.james.mailbox.postgres.PostgresMessageId; +import org.apache.james.mailbox.postgres.mail.dao.PostgresMailboxMessageDAO; +import org.apache.james.mailbox.postgres.mail.dao.PostgresMessageDAO; +import org.apache.james.mailbox.store.mail.model.MailboxMessage; +import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder; +import org.apache.james.mailbox.store.mail.model.impl.SimpleMailboxMessage; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class UnseenSearchOverrideTest { + private static final MailboxSession MAILBOX_SESSION = MailboxSessionUtil.create(Username.of("benwa")); + private static final Mailbox MAILBOX = new Mailbox(MailboxPath.inbox(MAILBOX_SESSION), UidValidity.of(12), PostgresMailboxId.generate()); + private static final String BLOB_ID = "abc"; + private static final Charset MESSAGE_CHARSET = StandardCharsets.UTF_8; + private static final String MESSAGE_CONTENT = "Simple message content"; + private static final byte[] MESSAGE_CONTENT_BYTES = MESSAGE_CONTENT.getBytes(MESSAGE_CHARSET); + private static final ByteContent CONTENT_STREAM = new ByteContent(MESSAGE_CONTENT_BYTES); + private final static long SIZE = MESSAGE_CONTENT_BYTES.length; + + @RegisterExtension + static PostgresExtension postgresExtension = PostgresExtension.withoutRowLevelSecurity(PostgresMailboxAggregateModule.MODULE); + + private PostgresMailboxMessageDAO postgresMailboxMessageDAO; + private PostgresMessageDAO postgresMessageDAO; + private UnseenSearchOverride testee; + + @BeforeEach + void setUp() { + postgresMessageDAO = new PostgresMessageDAO(postgresExtension.getPostgresExecutor()); + postgresMailboxMessageDAO = new PostgresMailboxMessageDAO(postgresExtension.getPostgresExecutor()); + testee = new UnseenSearchOverride(postgresMailboxMessageDAO); + } + + @Test + void unseenQueryShouldBeApplicable() { + assertThat(testee.applicable( + SearchQuery.builder() + .andCriteria(SearchQuery.flagIsUnSet(SEEN)) + .build(), + MAILBOX_SESSION)) + .isTrue(); + } + + @Test + void notSeenQueryShouldBeApplicable() { + assertThat(testee.applicable( + SearchQuery.builder() + .andCriteria(SearchQuery.not(SearchQuery.flagIsSet(SEEN))) + .build(), + MAILBOX_SESSION)) + .isTrue(); + } + + @Test + void unseenAndAllQueryShouldBeApplicable() { + assertThat(testee.applicable( + SearchQuery.builder() + .andCriteria(SearchQuery.flagIsUnSet(SEEN)) + .andCriteria(SearchQuery.all()) + .build(), + MAILBOX_SESSION)) + .isTrue(); + } + + @Test + void notSeenAndAllQueryShouldBeApplicable() { + assertThat(testee.applicable( + SearchQuery.builder() + .andCriteria(SearchQuery.not(SearchQuery.flagIsSet(SEEN))) + .andCriteria(SearchQuery.all()) + .build(), + MAILBOX_SESSION)) + .isTrue(); + } + + @Test + void unseenAndFromOneQueryShouldBeApplicable() { + assertThat(testee.applicable( + SearchQuery.builder() + .andCriteria(SearchQuery.flagIsUnSet(SEEN)) + .andCriteria(SearchQuery.uid(new SearchQuery.UidRange(MessageUid.MIN_VALUE, MessageUid.MAX_VALUE))) + .build(), + MAILBOX_SESSION)) + .isTrue(); + } + + @Test + void notSeenFromOneQueryShouldBeApplicable() { + assertThat(testee.applicable( + SearchQuery.builder() + .andCriteria(SearchQuery.not(SearchQuery.flagIsSet(SEEN))) + .andCriteria(SearchQuery.uid(new SearchQuery.UidRange(MessageUid.MIN_VALUE, MessageUid.MAX_VALUE))) + .build(), + MAILBOX_SESSION)) + .isTrue(); + } + + @Test + void sizeQueryShouldNotBeApplicable() { + assertThat(testee.applicable( + SearchQuery.builder() + .andCriteria(SearchQuery.sizeEquals(12)) + .build(), + MAILBOX_SESSION)) + .isFalse(); + } + + @Test + void searchShouldReturnEmptyByDefault() { + assertThat(testee.search(MAILBOX_SESSION, MAILBOX, + SearchQuery.builder() + .andCriteria(SearchQuery.flagIsUnSet(SEEN)) + .build()).collectList().block()) + .isEmpty(); + } + + @Test + void searchShouldReturnMailboxEntries() { + MessageUid messageUid = MessageUid.of(1); + insert(messageUid, MAILBOX.getMailboxId(), new Flags()); + MessageUid messageUid2 = MessageUid.of(2); + insert(messageUid2, MAILBOX.getMailboxId(), new Flags()); + MessageUid messageUid3 = MessageUid.of(3); + insert(messageUid3, MAILBOX.getMailboxId(), new Flags(SEEN)); + + assertThat(testee.search(MAILBOX_SESSION, MAILBOX, + SearchQuery.builder() + .andCriteria(SearchQuery.flagIsUnSet(SEEN)) + .build()).collectList().block()) + .containsOnly(messageUid, messageUid2); + } + + @Test + void searchShouldSupportRanges() { + MessageUid messageUid = MessageUid.of(1); + insert(messageUid, MAILBOX.getMailboxId(), new Flags()); + MessageUid messageUid2 = MessageUid.of(2); + insert(messageUid2, MAILBOX.getMailboxId(), new Flags()); + MessageUid messageUid3 = MessageUid.of(3); + insert(messageUid3, MAILBOX.getMailboxId(), new Flags(SEEN)); + MessageUid messageUid4 = MessageUid.of(4); + insert(messageUid4, MAILBOX.getMailboxId(), new Flags()); + + assertThat(testee.search(MAILBOX_SESSION, MAILBOX, + SearchQuery.builder() + .andCriteria(SearchQuery.flagIsUnSet(SEEN)) + .andCriterion(SearchQuery.uid(new SearchQuery.UidRange(MessageUid.of(2), MessageUid.of(4)))) + .build()).collectList().block()) + .containsOnly(messageUid2, messageUid4); + } + + private void insert(MessageUid messageUid, MailboxId mailboxId, Flags flags) { + PostgresMessageId messageId = new PostgresMessageId.Factory().generate(); + MailboxMessage message = SimpleMailboxMessage.builder() + .messageId(messageId) + .threadId(ThreadId.fromBaseMessageId(messageId)) + .uid(messageUid) + .content(CONTENT_STREAM) + .size(SIZE) + .internalDate(new Date()) + .bodyStartOctet(18) + .flags(flags) + .properties(new PropertyBuilder()) + .mailboxId(mailboxId) + .modseq(ModSeq.of(1)) + .build(); + postgresMessageDAO.insert(message, BLOB_ID).block(); + postgresMailboxMessageDAO.insert(message).block(); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org