JAMES-2110 GetMessageList should allow filtering by keywords
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/a6f1c105 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/a6f1c105 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/a6f1c105 Branch: refs/heads/master Commit: a6f1c1059d20dd179e9f5feb2e3374c38869cfed Parents: 92365a2 Author: quynhn <[email protected]> Authored: Tue Aug 15 16:45:26 2017 +0700 Committer: Raphael Ouazana <[email protected]> Committed: Thu Aug 24 15:47:27 2017 +0200 ---------------------------------------------------------------------- .../james/jmap/model/FilterCondition.java | 48 ++++++++++-- .../james/jmap/utils/FilterToSearchQuery.java | 27 ++++++- .../james/jmap/model/FilterConditionTest.java | 77 +++++++++++++++++++- .../jmap/utils/FilterToSearchQueryTest.java | 52 +++++++++++++ 4 files changed, 196 insertions(+), 8 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/a6f1c105/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java index 63663c5..334667d 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/FilterCondition.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.MoreObjects; import com.google.common.base.MoreObjects.ToStringHelper; +import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; @JsonDeserialize(builder = FilterCondition.Builder.class) @@ -60,10 +61,14 @@ public class FilterCondition implements Filter { private String subject; private String body; private Header header; + private Optional<String> hasKeyword; + private Optional<String> notKeyword; private Builder() { inMailboxes = Optional.empty(); notInMailboxes = Optional.empty(); + hasKeyword = Optional.empty(); + notKeyword = Optional.empty(); } public Builder inMailboxes(String... inMailboxes) { @@ -88,6 +93,18 @@ public class FilterCondition implements Filter { return this; } + @JsonDeserialize + public Builder hasKeyword(Optional<String> hasKeyword) { + this.hasKeyword = hasKeyword; + return this; + } + + @JsonDeserialize + public Builder notKeyword(Optional<String> notKeyword) { + this.notKeyword = notKeyword; + return this; + } + public Builder before(ZonedDateTime before) { this.before = before; return this; @@ -174,9 +191,12 @@ public class FilterCondition implements Filter { } public FilterCondition build() { + Preconditions.checkArgument(!hasKeyword.isPresent() || (new Keyword(hasKeyword.get()) != null), "hasKeyword is not valid"); + Preconditions.checkArgument(!notKeyword.isPresent() || (new Keyword(notKeyword.get()) != null), "notKeyword is not valid"); return new FilterCondition(inMailboxes, notInMailboxes, Optional.ofNullable(before), Optional.ofNullable(after), Optional.ofNullable(minSize), Optional.ofNullable(maxSize), Optional.ofNullable(isFlagged), Optional.ofNullable(isUnread), Optional.ofNullable(isAnswered), Optional.ofNullable(isDraft), Optional.ofNullable(hasAttachment), - Optional.ofNullable(text), Optional.ofNullable(from), Optional.ofNullable(to), Optional.ofNullable(cc), Optional.ofNullable(bcc), Optional.ofNullable(subject), Optional.ofNullable(body), Optional.ofNullable(header)); + Optional.ofNullable(text), Optional.ofNullable(from), Optional.ofNullable(to), Optional.ofNullable(cc), Optional.ofNullable(bcc), Optional.ofNullable(subject), + Optional.ofNullable(body), Optional.ofNullable(header), hasKeyword, notKeyword); } } @@ -199,10 +219,13 @@ public class FilterCondition implements Filter { private final Optional<String> subject; private final Optional<String> body; private final Optional<Header> header; + private final Optional<String> hasKeyword; + private final Optional<String> notKeyword; @VisibleForTesting FilterCondition(Optional<List<String>> inMailboxes, Optional<List<String>> notInMailboxes, Optional<ZonedDateTime> before, Optional<ZonedDateTime> after, Optional<Integer> minSize, Optional<Integer> maxSize, - Optional<Boolean> isFlagged, Optional<Boolean> isUnread, Optional<Boolean> isAnswered, Optional<Boolean> isDraft, Optional<Boolean> hasAttachment, - Optional<String> text, Optional<String> from, Optional<String> to, Optional<String> cc, Optional<String> bcc, Optional<String> subject, Optional<String> body, Optional<Header> header) { + Optional<Boolean> isFlagged, Optional<Boolean> isUnread, Optional<Boolean> isAnswered, Optional<Boolean> isDraft, Optional<Boolean> hasAttachment, + Optional<String> text, Optional<String> from, Optional<String> to, Optional<String> cc, Optional<String> bcc, Optional<String> subject, + Optional<String> body, Optional<Header> header, Optional<String> hasKeyword, Optional<String> notKeyword) { this.inMailboxes = inMailboxes; this.notInMailboxes = notInMailboxes; @@ -223,6 +246,8 @@ public class FilterCondition implements Filter { this.subject = subject; this.body = body; this.header = header; + this.hasKeyword = hasKeyword; + this.notKeyword = notKeyword; } public Optional<List<String>> getInMailboxes() { @@ -301,6 +326,14 @@ public class FilterCondition implements Filter { return header; } + public Optional<String> getHasKeyword() { + return hasKeyword; + } + + public Optional<String> getNotKeyword() { + return notKeyword; + } + @Override public final boolean equals(Object obj) { if (obj instanceof FilterCondition) { @@ -323,14 +356,17 @@ public class FilterCondition implements Filter { && Objects.equals(this.bcc, other.bcc) && Objects.equals(this.subject, other.subject) && Objects.equals(this.body, other.body) - && Objects.equals(this.header, other.header); + && Objects.equals(this.header, other.header) + && Objects.equals(this.hasKeyword, other.hasKeyword) + && Objects.equals(this.notKeyword, other.notKeyword); } return false; } @Override public final int hashCode() { - return Objects.hash(inMailboxes, notInMailboxes, before, after, minSize, maxSize, isFlagged, isUnread, isAnswered, isDraft, hasAttachment, text, from, to, cc, bcc, subject, body, header); + return Objects.hash(inMailboxes, notInMailboxes, before, after, minSize, maxSize, isFlagged, isUnread, isAnswered, isDraft, hasAttachment, + text, from, to, cc, bcc, subject, body, header, hasKeyword, notKeyword); } @Override @@ -355,6 +391,8 @@ public class FilterCondition implements Filter { subject.ifPresent(x -> helper.add("subject", x)); body.ifPresent(x -> helper.add("body", x)); header.ifPresent(x -> helper.add("header", x)); + hasKeyword.ifPresent(x -> helper.add("hasKeyword", x)); + notKeyword.ifPresent(x -> helper.add("notKeyword", x)); return helper.toString(); } http://git-wip-us.apache.org/repos/asf/james-project/blob/a6f1c105/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java index 95799e4..f619357 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/utils/FilterToSearchQuery.java @@ -20,12 +20,13 @@ package org.apache.james.jmap.utils; import java.util.Date; - +import java.util.Optional; import javax.mail.Flags.Flag; import org.apache.james.jmap.model.Filter; import org.apache.james.jmap.model.FilterCondition; import org.apache.james.jmap.model.FilterOperator; +import org.apache.james.jmap.model.Keyword; import org.apache.james.mailbox.model.SearchQuery; import org.apache.james.mailbox.model.SearchQuery.AddressType; import org.apache.james.mailbox.model.SearchQuery.Criterion; @@ -75,9 +76,33 @@ public class FilterToSearchQuery { filter.getMaxSize().ifPresent(maxSize -> searchQuery.andCriteria(SearchQuery.sizeLessThan(maxSize))); filter.getMinSize().ifPresent(minSize -> searchQuery.andCriteria(SearchQuery.sizeGreaterThan(minSize))); filter.getHasAttachment().ifPresent(hasAttachment -> searchQuery.andCriteria(SearchQuery.hasAttachment(hasAttachment))); + filter.getHasKeyword().ifPresent(hasKeyword -> { + keywordQuery(hasKeyword, true).ifPresent(hasKeywordCriterion + -> searchQuery.andCriteria(hasKeywordCriterion)); + }); + filter.getNotKeyword().ifPresent(notKeyword -> { + keywordQuery(notKeyword, false).ifPresent(notKeywordCriterion + -> searchQuery.andCriteria(notKeywordCriterion)); + }); + return searchQuery; } + private Optional<Criterion> keywordQuery(String stringKeyword, boolean isSet) { + Keyword keyword = new Keyword(stringKeyword); + if (keyword.isExposedImapKeyword()) { + return Optional.of(getFlagCriterion(keyword, isSet)); + } + + return Optional.empty(); + } + + private Criterion getFlagCriterion(Keyword keyword, boolean isSet) { + return keyword.asSystemFlag() + .map(flag -> SearchQuery.flagSet(flag, isSet)) + .orElse(SearchQuery.flagSet(keyword.getFlagName(), isSet)); + } + private Criterion convertOperator(FilterOperator filter) { switch (filter.getOperator()) { case AND: http://git-wip-us.apache.org/repos/asf/james-project/blob/a6f1c105/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java index 5a518f0..5efaecc 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/FilterConditionTest.java @@ -23,15 +23,22 @@ import static org.assertj.core.api.Assertions.assertThat; import java.time.ZonedDateTime; import java.util.Optional; +import java.util.Set; +import org.junit.Rule; import org.junit.Test; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.rules.ExpectedException; public class FilterConditionTest { + @Rule + public ExpectedException expectedException = ExpectedException.none(); + @Test public void buildShouldWorkWhenNoInMailboxes() { FilterCondition filterCondition = FilterCondition.builder().build(); @@ -89,9 +96,13 @@ public class FilterConditionTest { String subject = "subject"; String body = "body"; Header header = Header.from(ImmutableList.of("name", "value")); - FilterCondition expectedFilterCondition = new FilterCondition(Optional.of(ImmutableList.of("1")), Optional.of(ImmutableList.of("2")), Optional.of(before), Optional.of(after), Optional.of(minSize), Optional.of(maxSize), + Optional<String> hasKeyword = Optional.of("$Draft"); + Optional<String> notKeyword = Optional.of("$Flagged"); + + FilterCondition expectedFilterCondition = new FilterCondition(Optional.of(ImmutableList.of("1")), Optional.of(ImmutableList.of("2")), Optional.of(before), Optional.of(after), Optional.of(minSize), Optional.of(maxSize), Optional.of(isFlagged), Optional.of(isUnread), Optional.of(isAnswered), Optional.of(isDraft), Optional.of(hasAttachment), Optional.of(text), Optional.of(from), - Optional.of(to), Optional.of(cc), Optional.of(bcc), Optional.of(subject), Optional.of(body), Optional.of(header)); + Optional.of(to), Optional.of(cc), Optional.of(bcc), Optional.of(subject), Optional.of(body), Optional.of(header), + hasKeyword, notKeyword); FilterCondition filterCondition = FilterCondition.builder() .inMailboxes(Optional.of(ImmutableList.of("1"))) @@ -113,6 +124,8 @@ public class FilterConditionTest { .subject(subject) .body(body) .header(header) + .hasKeyword(hasKeyword) + .notKeyword(notKeyword) .build(); assertThat(filterCondition).isEqualToComparingFieldByField(expectedFilterCondition); @@ -122,4 +135,64 @@ public class FilterConditionTest { public void shouldRespectJavaBeanContract() { EqualsVerifier.forClass(FilterCondition.class).verify(); } + + @Test + public void buildShouldBuildFilterConditionWithHasKeywordWhenGivenHasKeyword() { + String hasKeyword = "$Draft"; + + FilterCondition filterCondition = FilterCondition.builder() + .hasKeyword(Optional.of(hasKeyword)) + .build(); + + assertThat(filterCondition.getHasKeyword().get()) + .isEqualTo(hasKeyword); + } + + @Test + public void buildShouldBuildFilterConditionWithoutHasKeywordWhenDoNotGivenHasKeyword() { + FilterCondition filterCondition = FilterCondition.builder() + .hasKeyword(Optional.empty()) + .build(); + + assertThat(filterCondition.getHasKeyword().isPresent()) + .isFalse(); + } + + @Test + public void buildShouldThrowWhenGivenInvalidKeywordAsHasKeyword() { + expectedException.expect(IllegalArgumentException.class); + + FilterCondition.builder() + .hasKeyword(Optional.of("$Draft%")) + .build(); + } + + @Test + public void buildShouldBuildFilterConditionWithNotKeywordWhenGivenNotKeyword() { + String notKeyword = "$Draft"; + + FilterCondition filterCondition = FilterCondition.builder() + .notKeyword(Optional.of(notKeyword)) + .build(); + assertThat(filterCondition.getNotKeyword().get()).isEqualTo(notKeyword); + } + + @Test + public void buildShouldBuildFilterConditionWithoutNotKeywordWhenDoNotGivenNotKeyword() { + FilterCondition filterCondition = FilterCondition.builder() + .notKeyword(Optional.empty()) + .build(); + + assertThat(filterCondition.getNotKeyword().isPresent()) + .isFalse(); + } + + @Test + public void buildShouldThrowWhenGivenInvalidKeywordAsNotKeyword() { + expectedException.expect(IllegalArgumentException.class); + + FilterCondition.builder() + .notKeyword(Optional.of("$Draft%")) + .build(); + } } http://git-wip-us.apache.org/repos/asf/james-project/blob/a6f1c105/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java index eb05436..aca9757 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/utils/FilterToSearchQueryTest.java @@ -24,6 +24,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.time.ZonedDateTime; import java.util.Date; +import java.util.Optional; import javax.mail.Flags.Flag; @@ -31,14 +32,17 @@ import org.apache.james.jmap.model.Filter; import org.apache.james.jmap.model.FilterCondition; import org.apache.james.jmap.model.FilterOperator; import org.apache.james.jmap.model.Header; +import org.apache.james.jmap.model.Keyword; import org.apache.james.mailbox.model.SearchQuery; import org.apache.james.mailbox.model.SearchQuery.AddressType; import org.apache.james.mailbox.model.SearchQuery.DateResolution; import org.junit.Test; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; public class FilterToSearchQueryTest { + private final static String FORWARDED = "forwarded"; @Test public void filterConditionShouldThrowWhenUnknownFilter() { @@ -476,4 +480,52 @@ public class FilterToSearchQueryTest { assertThat(searchQuery).isEqualTo(expectedSearchQuery); } + + @Test + public void filterConditionShouldMapWhenHasKeyword() { + SearchQuery expectedSearchQuery = new SearchQuery(); + expectedSearchQuery.andCriteria(SearchQuery.flagIsSet(Flag.FLAGGED)); + + SearchQuery searchQuery = new FilterToSearchQuery().convert(FilterCondition.builder() + .hasKeyword(Optional.of("$Flagged")) + .build()); + + assertThat(searchQuery).isEqualTo(expectedSearchQuery); + } + + @Test + public void filterConditionShouldMapWhenHasKeywordWithUserFlag() { + SearchQuery expectedSearchQuery = new SearchQuery(); + expectedSearchQuery.andCriteria(SearchQuery.flagIsSet(FORWARDED)); + + SearchQuery searchQuery = new FilterToSearchQuery().convert(FilterCondition.builder() + .hasKeyword(Optional.of(FORWARDED)) + .build()); + + assertThat(searchQuery).isEqualTo(expectedSearchQuery); + } + + @Test + public void filterConditionShouldMapWhenNotKeyword() { + SearchQuery expectedSearchQuery = new SearchQuery(); + expectedSearchQuery.andCriteria(SearchQuery.flagIsUnSet(Flag.FLAGGED)); + + SearchQuery searchQuery = new FilterToSearchQuery().convert(FilterCondition.builder() + .notKeyword(Optional.of("$Flagged")) + .build()); + + assertThat(searchQuery).isEqualTo(expectedSearchQuery); + } + + @Test + public void filterConditionShouldMapWhenNotKeywordWithUserFlag() { + SearchQuery expectedSearchQuery = new SearchQuery(); + expectedSearchQuery.andCriteria(SearchQuery.flagIsUnSet(FORWARDED)); + + SearchQuery searchQuery = new FilterToSearchQuery().convert(FilterCondition.builder() + .notKeyword(Optional.of(FORWARDED)) + .build()); + + assertThat(searchQuery).isEqualTo(expectedSearchQuery); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
