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

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


The following commit(s) were added to refs/heads/master by this push:
     new d367f654d0 JAMES-4186 [REFACTOR] EmaiQueryOptimizer interface to wrap 
email query view logic in JMAP EmailQueryMethod (#2963)
d367f654d0 is described below

commit d367f654d07ab45f25a40d42ff9bfcd101361a55
Author: Rene Cordier <[email protected]>
AuthorDate: Mon Mar 9 09:19:40 2026 +0700

    JAMES-4186 [REFACTOR] EmaiQueryOptimizer interface to wrap email query view 
logic in JMAP EmailQueryMethod (#2963)
---
 .../james/jmap/rfc8621/RFC8621MethodsModule.java   |   5 +
 .../james/jmap/method/EmailQueryMethod.scala       |  96 +++-------------
 .../james/jmap/method/EmailQueryOptimizer.scala    | 121 +++++++++++++++++++++
 3 files changed, 139 insertions(+), 83 deletions(-)

diff --git 
a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
 
b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
index 66437c8c70..02a498d25f 100644
--- 
a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
+++ 
b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
@@ -63,6 +63,8 @@ import org.apache.james.jmap.method.EmailGetMethod;
 import org.apache.james.jmap.method.EmailImportMethod;
 import org.apache.james.jmap.method.EmailParseMethod;
 import org.apache.james.jmap.method.EmailQueryMethod;
+import org.apache.james.jmap.method.EmailQueryOptimizer;
+import org.apache.james.jmap.method.EmailQueryViewOptimizer;
 import org.apache.james.jmap.method.EmailSetMethod;
 import org.apache.james.jmap.method.EmailSubmissionSetMethod;
 import org.apache.james.jmap.method.IdentityChangesMethod;
@@ -204,6 +206,9 @@ public class RFC8621MethodsModule extends AbstractModule {
         Multibinder<ConnectionDescriptionSupplier> 
connectionDescriptionSupplierMultibinder = Multibinder.newSetBinder(binder(), 
ConnectionDescriptionSupplier.class);
         
connectionDescriptionSupplierMultibinder.addBinding().to(WebSocketRoutes.class);
         
connectionDescriptionSupplierMultibinder.addBinding().to(EventSourceRoutes.class);
+
+        Multibinder<EmailQueryOptimizer> emailQueryOptimizerMultibinder = 
Multibinder.newSetBinder(binder(), EmailQueryOptimizer.class);
+        
emailQueryOptimizerMultibinder.addBinding().to(EmailQueryViewOptimizer.class);
     }
 
     @ProvidesIntoSet
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
index 467e29c72e..162c2c2da0 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
@@ -18,27 +18,21 @@
  ****************************************************************/
 package org.apache.james.jmap.method
 
-import java.time.ZonedDateTime
-
 import cats.implicits._
 import eu.timepit.refined.auto._
 import jakarta.inject.Inject
 import jakarta.mail.Flags.Flag.DELETED
-import org.apache.james.jmap.JMAPConfiguration
-import org.apache.james.jmap.api.projections.EmailQueryViewManager
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, 
JMAP_CORE, JMAP_MAIL}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.core.Limit.Limit
 import org.apache.james.jmap.core.Position.Position
 import org.apache.james.jmap.core.{CanCalculateChanges, Invocation, Limit, 
Position, QueryState, SessionTranslator}
 import org.apache.james.jmap.json.EmailQuerySerializer
-import org.apache.james.jmap.mail.{Comparator, EmailQueryRequest, 
EmailQueryResponse, FilterCondition, UnsupportedRequestParameterException}
+import org.apache.james.jmap.mail.{Comparator, EmailQueryRequest, 
EmailQueryResponse, UnsupportedRequestParameterException}
 import org.apache.james.jmap.routes.SessionSupplier
 import org.apache.james.jmap.utils.search.MailboxFilter
 import org.apache.james.jmap.utils.search.MailboxFilter.QueryFilter
-import org.apache.james.mailbox.exception.MailboxNotFoundException
-import org.apache.james.mailbox.model.MultimailboxesSearchQuery.Namespace
-import org.apache.james.mailbox.model.{MailboxId, MessageId, 
MultimailboxesSearchQuery, SearchOptions, SearchQuery}
+import org.apache.james.mailbox.model.{MessageId, MultimailboxesSearchQuery, 
SearchOptions, SearchQuery}
 import org.apache.james.mailbox.{MailboxManager, MailboxSession}
 import org.apache.james.metrics.api.MetricFactory
 import org.apache.james.util.streams.{Offset, Limit => JavaLimit}
@@ -51,8 +45,9 @@ class EmailQueryMethod @Inject() (serializer: 
EmailQuerySerializer,
                                   val metricFactory: MetricFactory,
                                   val sessionSupplier: SessionSupplier,
                                   val sessionTranslator: SessionTranslator,
-                                  val configuration: JMAPConfiguration,
-                                  val emailQueryViewManager: 
EmailQueryViewManager) extends MethodRequiringAccountId[EmailQueryRequest] {
+                                  val javaEmailQueryOptimizers: 
java.util.Set[EmailQueryOptimizer]) extends 
MethodRequiringAccountId[EmailQueryRequest] {
+  private val emailQueryOptimizers: Set[EmailQueryOptimizer] = 
javaEmailQueryOptimizers.asScala.toSet
+
   override val methodName: MethodName = MethodName("Email/query")
   override val requiredCapabilities: Set[CapabilityIdentifier] = 
Set(JMAP_CORE, JMAP_MAIL)
 
@@ -92,81 +87,17 @@ class EmailQueryMethod @Inject() (serializer: 
EmailQuerySerializer,
     }
 
   private def executeQuery(session: MailboxSession, request: 
EmailQueryRequest, searchQuery: MultimailboxesSearchQuery, position: Position, 
limit: Limit): SMono[EmailQueryResponse] = {
-    val ids: SMono[Seq[MessageId]] = request match {
-      case request: EmailQueryRequest if 
matchesInMailboxSortedByReceivedAt(request) =>
-        queryViewForListingSortedByReceivedAt(session, position, limit, 
request, searchQuery.getNamespace)
-      case request: EmailQueryRequest if 
matchesInMailboxAfterSortedByReceivedAt(request) =>
-        queryViewForContentAfterSortedByReceivedAt(session, position, limit, 
request, searchQuery.getNamespace)
-      case request: EmailQueryRequest if 
matchesInMailboxBeforeSortedByReceivedAt(request) =>
-        queryViewForContentBeforeSortedByReceivedAt(session, position, limit, 
request, searchQuery.getNamespace)
-      case _ => executeQueryAgainstSearchIndex(session, searchQuery, position, 
limit)
-    }
+    val ids: SMono[Seq[MessageId]] = executeQueryOptimizers(session, request, 
searchQuery, position, limit)
+      .getOrElse(executeQueryAgainstSearchIndex(session, searchQuery, 
position, limit))
+      .collectSeq()
 
     ids.map(ids => toResponse(request, position, limit, ids))
   }
 
-
-  private def queryViewForContentAfterSortedByReceivedAt(mailboxSession: 
MailboxSession, position: Position, limitToUse: Limit, request: 
EmailQueryRequest, namespace: Namespace): SMono[Seq[MessageId]] = {
-    val condition: FilterCondition = 
request.filter.get.asInstanceOf[FilterCondition]
-    val mailboxId: MailboxId = condition.inMailbox.get
-    val after: ZonedDateTime = condition.after.get.asUTC
-    val collapseThreads: Boolean = getCollapseThreads(request)
-
-    val queryViewEntries: SFlux[MessageId] = 
SFlux.fromPublisher(emailQueryViewManager.getEmailQueryView(mailboxSession.getUser)
-      .listMailboxContentSinceAfterSortedByReceivedAt(mailboxId, after, 
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
-
-    fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession, 
position, limitToUse, namespace)
-  }
-
-  private def queryViewForContentBeforeSortedByReceivedAt(mailboxSession: 
MailboxSession, position: Position, limitToUse: Limit, request: 
EmailQueryRequest, namespace: Namespace): SMono[Seq[MessageId]] = {
-    val condition: FilterCondition = 
request.filter.get.asInstanceOf[FilterCondition]
-    val mailboxId: MailboxId = condition.inMailbox.get
-    val before: ZonedDateTime = condition.before.get.asUTC
-    val collapseThreads: Boolean = getCollapseThreads(request)
-
-    val queryViewEntries: SFlux[MessageId] = 
SFlux.fromPublisher(emailQueryViewManager.getEmailQueryView(mailboxSession.getUser)
-      .listMailboxContentBeforeSortedByReceivedAt(mailboxId, before, 
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
-
-    fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession, 
position, limitToUse, namespace)
-  }
-
-  private def queryViewForListingSortedByReceivedAt(mailboxSession: 
MailboxSession, position: Position, limitToUse: Limit, request: 
EmailQueryRequest, namespace: Namespace): SMono[Seq[MessageId]] = {
-    val mailboxId: MailboxId = 
request.filter.get.asInstanceOf[FilterCondition].inMailbox.get
-    val collapseThreads: Boolean = getCollapseThreads(request)
-
-    val queryViewEntries: SFlux[MessageId] = 
SFlux.fromPublisher(emailQueryViewManager
-      
.getEmailQueryView(mailboxSession.getUser).listMailboxContentSortedByReceivedAt(mailboxId,
 JavaLimit.from(limitToUse.value + position.value), collapseThreads))
-
-    fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession, 
position, limitToUse, namespace)
-  }
-
-  private def fromQueryViewEntries(mailboxId: MailboxId, queryViewEntries: 
SFlux[MessageId], mailboxSession: MailboxSession, position: Position, 
limitToUse: Limit, namespace: Namespace): SMono[Seq[MessageId]] =
-    SMono(mailboxManager.getMailboxReactive(mailboxId, mailboxSession))
-      .filter(messageManager => 
namespace.keepAccessible(messageManager.getMailboxEntity))
-      .flatMap(_ => queryViewEntries
-          .drop(position.value)
-          .take(limitToUse.value)
-          .collectSeq())
-      .switchIfEmpty(SMono.just[Seq[MessageId]](Seq()))
-      .onErrorResume({
-        case _: MailboxNotFoundException => SMono.just[Seq[MessageId]](Seq())
-        case e => SMono.error[Seq[MessageId]](e)
-      })
-
-  private def matchesInMailboxSortedByReceivedAt(request: EmailQueryRequest): 
Boolean =
-    configuration.isEmailQueryViewEnabled &&
-      request.filter.exists(_.inMailboxFilterOnly) &&
-      request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
-
-  private def matchesInMailboxAfterSortedByReceivedAt(request: 
EmailQueryRequest): Boolean =
-    configuration.isEmailQueryViewEnabled &&
-      request.filter.exists(_.inMailboxAndAfterFilterOnly) &&
-      request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
-
-  private def matchesInMailboxBeforeSortedByReceivedAt(request: 
EmailQueryRequest): Boolean =
-    configuration.isEmailQueryViewEnabled &&
-      request.filter.exists(_.inMailboxAndBeforeFilterOnly) &&
-      request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+  private def executeQueryOptimizers(session: MailboxSession, request: 
EmailQueryRequest, searchQuery: MultimailboxesSearchQuery, position: Position, 
limit: Limit): Option[SFlux[MessageId]] =
+    emailQueryOptimizers.iterator
+      .map(_.apply(request, session, searchQuery, position, limit))
+      .collectFirst { case Some(result) => result }
 
   private def getCollapseThreads(request: EmailQueryRequest): Boolean =
     request.collapseThreads match {
@@ -182,12 +113,11 @@ class EmailQueryMethod @Inject() (serializer: 
EmailQuerySerializer,
       position = position,
       limit = Some(limitToUse).filterNot(used => 
request.limit.map(_.value).contains(used.value)))
 
-  private def executeQueryAgainstSearchIndex(mailboxSession: MailboxSession, 
searchQuery: MultimailboxesSearchQuery, position: Position, limitToUse: Limit): 
SMono[Seq[MessageId]] =
+  private def executeQueryAgainstSearchIndex(mailboxSession: MailboxSession, 
searchQuery: MultimailboxesSearchQuery, position: Position, limitToUse: Limit): 
SFlux[MessageId] =
     SFlux.fromPublisher(mailboxManager.search(
         searchQuery.addCriterion(SearchQuery.flagIsUnSet(DELETED)),
         mailboxSession,
         SearchOptions.of(Offset.from(position.value), 
JavaLimit.limit(limitToUse.value))))
-      .collectSeq()
 
   private def searchQueryFromRequest(request: EmailQueryRequest, capabilities: 
Set[CapabilityIdentifier], session: MailboxSession): 
Either[UnsupportedOperationException, MultimailboxesSearchQuery] = {
     val comparators: List[Comparator] = request.sort.getOrElse(Set()).toList
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryOptimizer.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryOptimizer.scala
new file mode 100644
index 0000000000..75450be410
--- /dev/null
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryOptimizer.scala
@@ -0,0 +1,121 @@
+/****************************************************************
+ * 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.jmap.method
+
+import java.time.ZonedDateTime
+
+import jakarta.inject.Inject
+import org.apache.james.jmap.JMAPConfiguration
+import org.apache.james.jmap.api.projections.EmailQueryViewManager
+import org.apache.james.jmap.core.Limit.Limit
+import org.apache.james.jmap.core.Position.Position
+import org.apache.james.jmap.mail.{Comparator, EmailQueryRequest, 
FilterCondition}
+import org.apache.james.mailbox.exception.MailboxNotFoundException
+import org.apache.james.mailbox.model.MultimailboxesSearchQuery.Namespace
+import org.apache.james.mailbox.model.{MailboxId, MessageId, 
MultimailboxesSearchQuery}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession}
+import org.apache.james.util.streams.{Limit => JavaLimit}
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+trait EmailQueryOptimizer {
+  def apply(request: EmailQueryRequest, session: MailboxSession, searchQuery: 
MultimailboxesSearchQuery, position: Position, limit: Limit): 
Option[SFlux[MessageId]]
+}
+
+class EmailQueryViewOptimizer @Inject() (mailboxManager: MailboxManager,
+                                         val configuration: JMAPConfiguration,
+                                         val emailQueryViewManager: 
EmailQueryViewManager) extends EmailQueryOptimizer {
+  override def apply(request: EmailQueryRequest, session: MailboxSession, 
searchQuery: MultimailboxesSearchQuery, position: Position, limit: Limit): 
Option[SFlux[MessageId]] =
+    if (configuration.isEmailQueryViewEnabled) {
+      request match {
+        case request: EmailQueryRequest if 
matchesInMailboxSortedByReceivedAt(request) =>
+          Some(queryViewForListingSortedByReceivedAt(session, position, limit, 
request, searchQuery.getNamespace))
+        case request: EmailQueryRequest if 
matchesInMailboxAfterSortedByReceivedAt(request) =>
+          Some(queryViewForContentAfterSortedByReceivedAt(session, position, 
limit, request, searchQuery.getNamespace))
+        case request: EmailQueryRequest if 
matchesInMailboxBeforeSortedByReceivedAt(request) =>
+          Some(queryViewForContentBeforeSortedByReceivedAt(session, position, 
limit, request, searchQuery.getNamespace))
+        case _ => None
+      }
+    } else {
+      None
+    }
+
+  private def matchesInMailboxSortedByReceivedAt(request: EmailQueryRequest): 
Boolean =
+    request.filter.exists(_.inMailboxFilterOnly) &&
+      request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+
+  private def matchesInMailboxAfterSortedByReceivedAt(request: 
EmailQueryRequest): Boolean =
+    request.filter.exists(_.inMailboxAndAfterFilterOnly) &&
+      request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+
+  private def matchesInMailboxBeforeSortedByReceivedAt(request: 
EmailQueryRequest): Boolean =
+    request.filter.exists(_.inMailboxAndBeforeFilterOnly) &&
+      request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+
+  private def queryViewForListingSortedByReceivedAt(mailboxSession: 
MailboxSession, position: Position, limitToUse: Limit, request: 
EmailQueryRequest, namespace: Namespace): SFlux[MessageId] = {
+    val mailboxId: MailboxId = 
request.filter.get.asInstanceOf[FilterCondition].inMailbox.get
+    val collapseThreads: Boolean = getCollapseThreads(request)
+
+    val queryViewEntries: SFlux[MessageId] = 
SFlux.fromPublisher(emailQueryViewManager
+      
.getEmailQueryView(mailboxSession.getUser).listMailboxContentSortedByReceivedAt(mailboxId,
 JavaLimit.from(limitToUse.value + position.value), collapseThreads))
+
+    fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession, 
position, limitToUse, namespace)
+  }
+
+  private def getCollapseThreads(request: EmailQueryRequest): Boolean =
+    request.collapseThreads match {
+      case Some(collapseThreads) => collapseThreads.value
+      case None => false
+    }
+
+  private def fromQueryViewEntries(mailboxId: MailboxId, queryViewEntries: 
SFlux[MessageId], mailboxSession: MailboxSession, position: Position, 
limitToUse: Limit, namespace: Namespace): SFlux[MessageId] =
+    SMono(mailboxManager.getMailboxReactive(mailboxId, mailboxSession))
+      .filter(messageManager => 
namespace.keepAccessible(messageManager.getMailboxEntity))
+      .flatMapMany(_ => queryViewEntries
+        .drop(position.value)
+        .take(limitToUse.value))
+      .onErrorResume({
+        case _: MailboxNotFoundException => SFlux.empty
+        case e => SFlux.error[MessageId](e)
+      })
+
+  private def queryViewForContentAfterSortedByReceivedAt(mailboxSession: 
MailboxSession, position: Position, limitToUse: Limit, request: 
EmailQueryRequest, namespace: Namespace): SFlux[MessageId] = {
+    val condition: FilterCondition = 
request.filter.get.asInstanceOf[FilterCondition]
+    val mailboxId: MailboxId = condition.inMailbox.get
+    val after: ZonedDateTime = condition.after.get.asUTC
+    val collapseThreads: Boolean = getCollapseThreads(request)
+
+    val queryViewEntries: SFlux[MessageId] = 
SFlux.fromPublisher(emailQueryViewManager.getEmailQueryView(mailboxSession.getUser)
+      .listMailboxContentSinceAfterSortedByReceivedAt(mailboxId, after, 
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
+
+    fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession, 
position, limitToUse, namespace)
+  }
+
+  private def queryViewForContentBeforeSortedByReceivedAt(mailboxSession: 
MailboxSession, position: Position, limitToUse: Limit, request: 
EmailQueryRequest, namespace: Namespace): SFlux[MessageId] = {
+    val condition: FilterCondition = 
request.filter.get.asInstanceOf[FilterCondition]
+    val mailboxId: MailboxId = condition.inMailbox.get
+    val before: ZonedDateTime = condition.before.get.asUTC
+    val collapseThreads: Boolean = getCollapseThreads(request)
+
+    val queryViewEntries: SFlux[MessageId] = 
SFlux.fromPublisher(emailQueryViewManager.getEmailQueryView(mailboxSession.getUser)
+      .listMailboxContentBeforeSortedByReceivedAt(mailboxId, before, 
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
+
+    fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession, 
position, limitToUse, namespace)
+  }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to