Factor common parts of BookmarkResource and LazychatResource into PostResource.

Change-Id: I6e5e123c67340e564c47448cf43b803f7d0cc809
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
new file mode 100644
index 0000000..fc9ba78
--- /dev/null
+++ b/src/main/java/eu/mulk/mulkcms2/benki/posts/Post.java
@@ -0,0 +1,225 @@
+package eu.mulk.mulkcms2.benki.posts;
+
+import eu.mulk.mulkcms2.benki.accesscontrol.Role;
+import eu.mulk.mulkcms2.benki.bookmarks.Bookmark;
+import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage;
+import eu.mulk.mulkcms2.benki.users.User;
+import eu.mulk.mulkcms2.benki.users.User_;
+import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
+import io.quarkus.security.identity.SecurityIdentity;
+import java.time.OffsetDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import javax.annotation.CheckForNull;
+import javax.persistence.Column;
+import javax.persistence.Entity;
+import javax.persistence.FetchType;
+import javax.persistence.GeneratedValue;
+import javax.persistence.GenerationType;
+import javax.persistence.Id;
+import javax.persistence.Inheritance;
+import javax.persistence.InheritanceType;
+import javax.persistence.JoinColumn;
+import javax.persistence.JoinTable;
+import javax.persistence.ManyToMany;
+import javax.persistence.ManyToOne;
+import javax.persistence.SequenceGenerator;
+import javax.persistence.Table;
+import javax.persistence.criteria.CriteriaBuilder;
+import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.From;
+import javax.persistence.criteria.JoinType;
+import javax.persistence.criteria.Predicate;
+import org.hibernate.Session;
+import org.jboss.logging.Logger;
+
+@Entity
+@Table(name = "posts", schema = "benki")
+@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
+public abstract class Post extends PanacheEntityBase {
+
+  private static Logger log = Logger.getLogger(Post.class);
+
+  @Id
+  @SequenceGenerator(
+      allocationSize = 1,
+      sequenceName = "posts_id_seq",
+      name = "posts_id_seq",
+      schema = "benki")
+  @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "posts_id_seq")
+  @Column(name = "id", nullable = false)
+  public Integer id;
+
+  @Column(name = "date", nullable = true)
+  public OffsetDateTime date;
+
+  @ManyToOne(fetch = FetchType.LAZY)
+  @JoinColumn(name = "owner", referencedColumnName = "id")
+  public User owner;
+
+  @ManyToMany(fetch = FetchType.LAZY)
+  @JoinTable(
+      name = "user_visible_posts",
+      schema = "benki",
+      joinColumns = @JoinColumn(name = "message"),
+      inverseJoinColumns = @JoinColumn(name = "user"))
+  public Set<User> visibleTo;
+
+  @ManyToMany(fetch = FetchType.LAZY)
+  @JoinTable(
+      name = "post_targets",
+      schema = "benki",
+      joinColumns = @JoinColumn(name = "message"),
+      inverseJoinColumns = @JoinColumn(name = "target"))
+  public Set<Role> targets;
+
+  public abstract boolean isBookmark();
+
+  public abstract boolean isLazychatMessage();
+
+  protected static <T extends Post> CriteriaQuery<T> queryViewable(
+      Class<T> entityClass,
+      SecurityIdentity readerIdentity,
+      @CheckForNull User owner,
+      @CheckForNull Integer cursor,
+      CriteriaBuilder cb,
+      boolean forward) {
+    CriteriaQuery<T> query = cb.createQuery(entityClass);
+
+    var conditions = new ArrayList<Predicate>();
+
+    From<?, T> post;
+    if (readerIdentity.isAnonymous()) {
+      post = query.from(entityClass);
+      var target = post.join(Post_.targets);
+      conditions.add(cb.equal(target, Role.getWorld()));
+    } else {
+      var userName = readerIdentity.getPrincipal().getName();
+      var user = User.findByNickname(userName);
+
+      var root = query.from(User.class);
+      conditions.add(cb.equal(root, user));
+      if (entityClass.isAssignableFrom(Bookmark.class)) {
+        post = (From<?, T>) root.join(User_.visibleBookmarks);
+      } else if (entityClass.isAssignableFrom(LazychatMessage.class)) {
+        post = (From<?, T>) root.join(User_.visibleLazychatMessages);
+      } else {
+        post = (From<?, T>) root.join(User_.visiblePosts);
+      }
+    }
+
+    query.select(post);
+    post.fetch(Post_.owner, JoinType.LEFT);
+
+    if (owner != null) {
+      conditions.add(cb.equal(post.get(Post_.owner), owner));
+    }
+
+    if (forward) {
+      query.orderBy(cb.desc(post.get(Post_.id)));
+    } else {
+      query.orderBy(cb.asc(post.get(Post_.id)));
+    }
+
+    if (cursor != null) {
+      if (forward) {
+        conditions.add(cb.le(post.get(Post_.id), cursor));
+      } else {
+        conditions.add(cb.gt(post.get(Post_.id), cursor));
+      }
+    }
+
+    query.where(conditions.toArray(new Predicate[0]));
+
+    return query;
+  }
+
+  public static class PostPage<T extends Post> {
+    public @CheckForNull Integer prevCursor;
+    public @CheckForNull Integer cursor;
+    public @CheckForNull Integer nextCursor;
+    public List<T> posts;
+
+    private PostPage(
+        @CheckForNull Integer c0,
+        @CheckForNull Integer c1,
+        @CheckForNull Integer c2,
+        List<T> resultList) {
+      this.prevCursor = c0;
+      this.cursor = c1;
+      this.nextCursor = c2;
+      this.posts = resultList;
+    }
+  }
+
+  public static PostPage<Post> findViewable(
+      PostFilter postFilter,
+      Session session,
+      SecurityIdentity viewer,
+      @CheckForNull User owner,
+      @CheckForNull Integer cursor,
+      @CheckForNull Integer count) {
+    Class<? extends Post> entityClass;
+    switch (postFilter) {
+      case BOOKMARKS_ONLY:
+        entityClass = Bookmark.class;
+        break;
+      case LAZYCHAT_MESSAGES_ONLY:
+        entityClass = LazychatMessage.class;
+        break;
+      default:
+        entityClass = Post.class;
+    }
+    return findViewable(entityClass, session, viewer, owner, cursor, count);
+  }
+
+  protected static <T extends Post> PostPage<T> findViewable(
+      Class<? extends T> entityClass,
+      Session session,
+      SecurityIdentity viewer,
+      @CheckForNull User owner,
+      @CheckForNull Integer cursor,
+      @CheckForNull Integer count) {
+
+    if (cursor != null) {
+      Objects.requireNonNull(count);
+    }
+
+    var cb = session.getCriteriaBuilder();
+
+    var forwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, true);
+    var forwardQuery = session.createQuery(forwardCriteria);
+
+    if (count != null) {
+      forwardQuery.setMaxResults(count + 1);
+    }
+
+    log.debug(forwardQuery.unwrap(org.hibernate.query.Query.class).getQueryString());
+
+    @CheckForNull Integer prevCursor = null;
+    @CheckForNull Integer nextCursor = null;
+
+    if (cursor != null) {
+      // Look backwards as well so we can find the prevCursor.
+      var backwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, false);
+      var backwardQuery = session.createQuery(backwardCriteria);
+      backwardQuery.setMaxResults(count);
+      var backwardResults = backwardQuery.getResultList();
+      if (!backwardResults.isEmpty()) {
+        prevCursor = backwardResults.get(backwardResults.size() - 1).id;
+      }
+    }
+
+    var forwardResults = (List<T>) forwardQuery.getResultList();
+    if (count != null) {
+      if (forwardResults.size() == count + 1) {
+        nextCursor = forwardResults.get(count).id;
+        forwardResults.remove((int) count);
+      }
+    }
+
+    return new PostPage<T>(prevCursor, cursor, nextCursor, forwardResults);
+  }
+}