KB73 Add full text search to post lists.

Change-Id: Ib8333b39cef1d7035ab7fac0ff8a03b400adcb40
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
index 8f2166c..fd023d7 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
@@ -19,6 +19,7 @@
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import javax.json.bind.annotation.JsonbTransient;
@@ -132,7 +133,8 @@
       @CheckForNull User owner,
       @CheckForNull Integer cursor,
       CriteriaBuilder cb,
-      boolean forward) {
+      boolean forward,
+      @CheckForNull String searchQuery) {
     CriteriaQuery<T> query = cb.createQuery(entityClass);
 
     var conditions = new ArrayList<Predicate>();
@@ -177,6 +179,23 @@
       }
     }
 
+    if (searchQuery != null && !searchQuery.isBlank()) {
+      var postTexts = post.join(Post_.texts);
+      var localizedSearches =
+          Stream.of("de", "en")
+              .map(
+                  language ->
+                      cb.isTrue(
+                          cb.function(
+                              "post_matches_websearch",
+                              Boolean.class,
+                              postTexts.get(PostText_.searchTerms),
+                              cb.literal(language),
+                              cb.literal(searchQuery))))
+              .toArray(n -> new Predicate[n]);
+      conditions.add(cb.or(localizedSearches));
+    }
+
     query.where(conditions.toArray(new Predicate[0]));
 
     return query;
@@ -248,7 +267,7 @@
 
   public static PostPage<Post<? extends PostText>> findViewable(
       PostFilter postFilter, Session session, @CheckForNull User viewer, @CheckForNull User owner) {
-    return findViewable(postFilter, session, viewer, owner, null, null);
+    return findViewable(postFilter, session, viewer, owner, null, null, null);
   }
 
   public static PostPage<Post<? extends PostText>> findViewable(
@@ -257,7 +276,8 @@
       @CheckForNull User viewer,
       @CheckForNull User owner,
       @CheckForNull Integer cursor,
-      @CheckForNull Integer count) {
+      @CheckForNull Integer count,
+      @CheckForNull String searchQuery) {
     Class<? extends Post> entityClass;
     switch (postFilter) {
       case BOOKMARKS_ONLY:
@@ -269,7 +289,7 @@
       default:
         entityClass = Post.class;
     }
-    return findViewable(entityClass, session, viewer, owner, cursor, count);
+    return findViewable(entityClass, session, viewer, owner, cursor, count, searchQuery);
   }
 
   protected static <T extends Post<? extends PostText>> PostPage<T> findViewable(
@@ -278,7 +298,8 @@
       @CheckForNull User viewer,
       @CheckForNull User owner,
       @CheckForNull Integer cursor,
-      @CheckForNull Integer count) {
+      @CheckForNull Integer count,
+      @CheckForNull String searchQuery) {
 
     if (cursor != null) {
       Objects.requireNonNull(count);
@@ -286,7 +307,7 @@
 
     var cb = session.getCriteriaBuilder();
 
-    var forwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, true);
+    var forwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, true, searchQuery);
     var forwardQuery = session.createQuery(forwardCriteria);
 
     if (count != null) {
@@ -300,7 +321,8 @@
 
     if (cursor != null) {
       // Look backwards as well so we can find the prevCursor.
-      var backwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, false);
+      var backwardCriteria =
+          queryViewable(entityClass, viewer, owner, cursor, cb, false, searchQuery);
       var backwardQuery = session.createQuery(backwardCriteria);
       backwardQuery.setMaxResults(count);
       var backwardResults = backwardQuery.getResultList();
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java
index 47e1594..59cacee 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostResource.java
@@ -116,13 +116,14 @@
   @Transactional
   public TemplateInstance getIndex(
       @QueryParam("i") @CheckForNull Integer cursor,
-      @QueryParam("n") @CheckForNull Integer maxResults) {
+      @QueryParam("n") @CheckForNull Integer maxResults,
+      @QueryParam("search-query") @CheckForNull String searchQuery) {
 
     maxResults = maxResults == null ? defaultMaxResults : maxResults;
 
     @CheckForNull var reader = getCurrentUser();
     var session = entityManager.unwrap(Session.class);
-    var q = Post.findViewable(postFilter, session, reader, null, cursor, maxResults);
+    var q = Post.findViewable(postFilter, session, reader, null, cursor, maxResults, searchQuery);
 
     q.cacheDescriptions();
 
@@ -142,7 +143,8 @@
         .data("hasNextPage", q.nextCursor != null)
         .data("previousCursor", q.prevCursor)
         .data("nextCursor", q.nextCursor)
-        .data("pageSize", maxResults);
+        .data("pageSize", maxResults)
+        .data("searchQuery", searchQuery);
   }
 
   @GET
@@ -159,7 +161,7 @@
     @CheckForNull var reader = getCurrentUser();
     var owner = User.findByNickname(ownerName);
     var session = entityManager.unwrap(Session.class);
-    var q = Post.findViewable(postFilter, session, reader, owner, cursor, maxResults);
+    var q = Post.findViewable(postFilter, session, reader, owner, cursor, maxResults, null);
 
     q.cacheDescriptions();
 
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostText.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostText.java
index 8b1697c..25955bd 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/posts/PostText.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/PostText.java
@@ -13,6 +13,8 @@
 import javax.persistence.JoinColumn;
 import javax.persistence.ManyToOne;
 import javax.persistence.Table;
+import org.hibernate.annotations.Generated;
+import org.hibernate.annotations.GenerationTime;
 
 @Entity
 @Table(name = "post_texts", schema = "benki")
@@ -38,6 +40,10 @@
   @CheckForNull
   public String cachedDescriptionHtml;
 
+  @Column(name = "search_terms")
+  @Generated(GenerationTime.ALWAYS)
+  public String searchTerms;
+
   @ManyToOne(fetch = FetchType.LAZY, targetEntity = Post.class)
   @JoinColumn(name = "post", referencedColumnName = "id", nullable = false)
   @JsonbTransient