Add localized texts to Benki post model.

Change-Id: I123cfe2ff06f85dc14c705b21d723d1c68fd2e00
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 2bd9ade..8f2166c 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
@@ -1,5 +1,7 @@
 package eu.mulk.mulkcms2.benki.posts;
 
+import static java.util.stream.Collectors.toList;
+
 import eu.mulk.mulkcms2.benki.accesscontrol.Role;
 import eu.mulk.mulkcms2.benki.bookmarks.Bookmark;
 import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage;
@@ -10,7 +12,9 @@
 import java.time.OffsetDateTime;
 import java.util.ArrayList;
 import java.util.Comparator;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
 import java.util.TimeZone;
@@ -18,6 +22,7 @@
 import javax.annotation.CheckForNull;
 import javax.annotation.Nullable;
 import javax.json.bind.annotation.JsonbTransient;
+import javax.persistence.CascadeType;
 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.FetchType;
@@ -30,6 +35,8 @@
 import javax.persistence.JoinTable;
 import javax.persistence.ManyToMany;
 import javax.persistence.ManyToOne;
+import javax.persistence.MapKey;
+import javax.persistence.OneToMany;
 import javax.persistence.SequenceGenerator;
 import javax.persistence.Table;
 import javax.persistence.criteria.CriteriaBuilder;
@@ -43,12 +50,10 @@
 @Entity
 @Table(name = "posts", schema = "benki")
 @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
-public abstract class Post extends PanacheEntityBase {
+public abstract class Post<Text extends PostText<?>> extends PanacheEntityBase {
 
   private static final Logger log = Logger.getLogger(Post.class);
 
-  private static final int DESCRIPTION_CACHE_VERSION = 1;
-
   @Id
   @SequenceGenerator(
       allocationSize = 1,
@@ -63,14 +68,6 @@
   @CheckForNull
   public OffsetDateTime date;
 
-  @Column(name = "cached_description_version", nullable = true)
-  @CheckForNull
-  public Integer cachedDescriptionVersion;
-
-  @Column(name = "cached_description_html", nullable = true)
-  @CheckForNull
-  public String cachedDescriptionHtml;
-
   @ManyToOne(fetch = FetchType.LAZY)
   @JoinColumn(name = "owner", referencedColumnName = "id")
   @CheckForNull
@@ -95,6 +92,18 @@
   @JsonbTransient
   public Set<Role> targets;
 
+  @OneToMany(
+      mappedBy = "post",
+      fetch = FetchType.LAZY,
+      cascade = CascadeType.ALL,
+      targetEntity = PostText.class)
+  @MapKey(name = "language")
+  public Map<String, Text> texts = new HashMap<>();
+
+  public Map<String, Text> getTexts() {
+    return texts;
+  }
+
   public abstract boolean isBookmark();
 
   public abstract boolean isLazychatMessage();
@@ -103,23 +112,6 @@
   public abstract String getTitle();
 
   @CheckForNull
-  public final String getDescriptionHtml() {
-    if (cachedDescriptionHtml != null
-        && cachedDescriptionVersion != null
-        && cachedDescriptionVersion >= DESCRIPTION_CACHE_VERSION) {
-      return cachedDescriptionHtml;
-    } else {
-      @CheckForNull var descriptionHtml = computeDescriptionHtml();
-      cachedDescriptionHtml = descriptionHtml;
-      cachedDescriptionVersion = DESCRIPTION_CACHE_VERSION;
-      return descriptionHtml;
-    }
-  }
-
-  @CheckForNull
-  protected abstract String computeDescriptionHtml();
-
-  @CheckForNull
   public abstract String getUri();
 
   public Visibility getVisibility() {
@@ -195,7 +187,16 @@
     return getVisibility() == Visibility.PUBLIC || (user != null && visibleTo.contains(user));
   }
 
-  public static class PostPage<T extends Post> {
+  @CheckForNull
+  public final String getDescriptionHtml() {
+    var text = getText();
+    if (text == null) {
+      return null;
+    }
+    return text.getDescriptionHtml();
+  }
+
+  public static class PostPage<T extends Post<? extends PostText>> {
     public @CheckForNull final Integer prevCursor;
     public @CheckForNull final Integer cursor;
     public @CheckForNull final Integer nextCursor;
@@ -229,7 +230,7 @@
 
       public void cacheDescriptions() {
         for (var post : posts) {
-          post.getDescriptionHtml();
+          post.getTexts().values().forEach(PostText::getDescriptionHtml);
         }
       }
     }
@@ -245,12 +246,12 @@
     }
   }
 
-  public static PostPage<Post> findViewable(
+  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);
   }
 
-  public static PostPage<Post> findViewable(
+  public static PostPage<Post<? extends PostText>> findViewable(
       PostFilter postFilter,
       Session session,
       @CheckForNull User viewer,
@@ -271,7 +272,7 @@
     return findViewable(entityClass, session, viewer, owner, cursor, count);
   }
 
-  protected static <T extends Post> PostPage<T> findViewable(
+  protected static <T extends Post<? extends PostText>> PostPage<T> findViewable(
       Class<? extends T> entityClass,
       Session session,
       @CheckForNull User viewer,
@@ -316,9 +317,31 @@
       }
     }
 
+    // Fetch texts (to avoid n+1 selects).
+    var postIds = forwardResults.stream().map(x -> x.id).collect(toList());
+
+    if (!postIds.isEmpty()) {
+      find("SELECT p FROM Post p LEFT JOIN FETCH p.texts WHERE p.id IN (?1)", postIds).stream()
+          .count();
+    }
+
     return new PostPage<>(prevCursor, cursor, nextCursor, forwardResults);
   }
 
+  @CheckForNull
+  protected Text getText() {
+    var texts = getTexts();
+    if (texts.isEmpty()) {
+      return null;
+    } else if (texts.containsKey("")) {
+      return texts.get("");
+    } else if (texts.containsKey("en")) {
+      return texts.get("en");
+    } else {
+      return texts.values().stream().findAny().get();
+    }
+  }
+
   public enum Visibility {
     PUBLIC,
     SEMIPRIVATE,