blob: 3a02e4efffe6a4e49897011a2c8eaa71b324d689 [file] [log] [blame]
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +02001package eu.mulk.mulkcms2.benki.posts;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +01002
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +01003import eu.mulk.mulkcms2.benki.accesscontrol.Role;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +01004import eu.mulk.mulkcms2.benki.bookmarks.Bookmark;
5import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +01006import eu.mulk.mulkcms2.benki.users.User;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +01007import eu.mulk.mulkcms2.benki.users.User_;
Matthias Andreas Benkard35cb1592020-01-24 11:05:20 +01008import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +01009import java.time.OffsetDateTime;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010010import java.util.ArrayList;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010011import java.util.List;
12import java.util.Objects;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010013import java.util.Set;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010014import javax.annotation.CheckForNull;
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +020015import javax.annotation.Nullable;
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020016import javax.json.bind.annotation.JsonbTransient;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010017import javax.persistence.Column;
18import javax.persistence.Entity;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010019import javax.persistence.FetchType;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010020import javax.persistence.GeneratedValue;
21import javax.persistence.GenerationType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010022import javax.persistence.Id;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010023import javax.persistence.Inheritance;
24import javax.persistence.InheritanceType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010025import javax.persistence.JoinColumn;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010026import javax.persistence.JoinTable;
27import javax.persistence.ManyToMany;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010028import javax.persistence.ManyToOne;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010029import javax.persistence.SequenceGenerator;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010030import javax.persistence.Table;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010031import javax.persistence.criteria.CriteriaBuilder;
32import javax.persistence.criteria.CriteriaQuery;
33import javax.persistence.criteria.From;
34import javax.persistence.criteria.JoinType;
35import javax.persistence.criteria.Predicate;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010036import org.hibernate.Session;
37import org.jboss.logging.Logger;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010038
39@Entity
Matthias Andreas Benkard57c9a8a2020-01-24 19:09:38 +010040@Table(name = "posts", schema = "benki")
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010041@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
42public abstract class Post extends PanacheEntityBase {
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010043
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +020044 private static final Logger log = Logger.getLogger(Post.class);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010045
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010046 @Id
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010047 @SequenceGenerator(
48 allocationSize = 1,
49 sequenceName = "posts_id_seq",
50 name = "posts_id_seq",
51 schema = "benki")
52 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "posts_id_seq")
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010053 @Column(name = "id", nullable = false)
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010054 public Integer id;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010055
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010056 @Column(name = "date", nullable = true)
Matthias Andreas Benkard1e7674c2020-04-18 20:28:51 +020057 @CheckForNull
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010058 public OffsetDateTime date;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010059
Matthias Andreas Benkardaa754802020-01-24 11:55:26 +010060 @ManyToOne(fetch = FetchType.LAZY)
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010061 @JoinColumn(name = "owner", referencedColumnName = "id")
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +020062 @CheckForNull
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020063 @JsonbTransient
Matthias Andreas Benkard35cb1592020-01-24 11:05:20 +010064 public User owner;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010065
66 @ManyToMany(fetch = FetchType.LAZY)
67 @JoinTable(
68 name = "user_visible_posts",
Matthias Andreas Benkard553de3e2020-01-27 05:33:15 +010069 schema = "benki",
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010070 joinColumns = @JoinColumn(name = "message"),
71 inverseJoinColumns = @JoinColumn(name = "user"))
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020072 @JsonbTransient
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010073 public Set<User> visibleTo;
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +010074
75 @ManyToMany(fetch = FetchType.LAZY)
76 @JoinTable(
77 name = "post_targets",
78 schema = "benki",
79 joinColumns = @JoinColumn(name = "message"),
80 inverseJoinColumns = @JoinColumn(name = "target"))
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020081 @JsonbTransient
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +010082 public Set<Role> targets;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010083
Matthias Andreas Benkard371164a2020-03-23 06:21:25 +010084 public abstract boolean isBookmark();
85
86 public abstract boolean isLazychatMessage();
87
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +020088 @CheckForNull
89 public abstract String getTitle();
90
91 @CheckForNull
92 public abstract String getDescriptionHtml();
93
94 @CheckForNull
95 public abstract String getUri();
96
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020097 public Visibility getVisibility() {
98 if (targets.isEmpty()) {
99 return Visibility.PRIVATE;
100 } else if (targets.contains(Role.getWorld())) {
101 return Visibility.PUBLIC;
102 } else {
103 // FIXME: There should really be a check whether targets.equals(owner.defaultTargets) here.
104 // Otherwise the actual visibility is DISCRETIONARY.
105 return Visibility.SEMIPRIVATE;
106 }
107 }
108
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100109 protected static <T extends Post> CriteriaQuery<T> queryViewable(
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100110 Class<T> entityClass,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200111 @CheckForNull User reader,
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100112 @CheckForNull User owner,
113 @CheckForNull Integer cursor,
114 CriteriaBuilder cb,
115 boolean forward) {
116 CriteriaQuery<T> query = cb.createQuery(entityClass);
117
118 var conditions = new ArrayList<Predicate>();
119
120 From<?, T> post;
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200121 if (reader == null) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100122 post = query.from(entityClass);
123 var target = post.join(Post_.targets);
124 conditions.add(cb.equal(target, Role.getWorld()));
125 } else {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100126 var root = query.from(User.class);
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200127 conditions.add(cb.equal(root, reader));
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200128 if (entityClass.isAssignableFrom(Post.class)) {
129 post = (From<?, T>) root.join(User_.visiblePosts);
130 } else if (entityClass.isAssignableFrom(Bookmark.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100131 post = (From<?, T>) root.join(User_.visibleBookmarks);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200132 } else if (entityClass.isAssignableFrom(LazychatMessage.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100133 post = (From<?, T>) root.join(User_.visibleLazychatMessages);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200134 } else {
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200135 throw new IllegalArgumentException();
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100136 }
137 }
138
139 query.select(post);
140 post.fetch(Post_.owner, JoinType.LEFT);
141
142 if (owner != null) {
143 conditions.add(cb.equal(post.get(Post_.owner), owner));
144 }
145
146 if (forward) {
147 query.orderBy(cb.desc(post.get(Post_.id)));
148 } else {
149 query.orderBy(cb.asc(post.get(Post_.id)));
150 }
151
152 if (cursor != null) {
153 if (forward) {
154 conditions.add(cb.le(post.get(Post_.id), cursor));
155 } else {
156 conditions.add(cb.gt(post.get(Post_.id), cursor));
157 }
158 }
159
160 query.where(conditions.toArray(new Predicate[0]));
161
162 return query;
163 }
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100164
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +0200165 public final boolean isVisibleTo(@Nullable User user) {
166 // FIXME: Make this more efficient.
167 return getVisibility() == Visibility.PUBLIC || (user != null && visibleTo.contains(user));
168 }
169
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100170 public static class PostPage<T extends Post> {
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +0200171 public @CheckForNull final Integer prevCursor;
172 public @CheckForNull final Integer cursor;
173 public @CheckForNull final Integer nextCursor;
174 public final List<T> posts;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100175
176 private PostPage(
177 @CheckForNull Integer c0,
178 @CheckForNull Integer c1,
179 @CheckForNull Integer c2,
180 List<T> resultList) {
181 this.prevCursor = c0;
182 this.cursor = c1;
183 this.nextCursor = c2;
184 this.posts = resultList;
185 }
186 }
187
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200188 public static List<Post> findViewable(
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200189 PostFilter postFilter, Session session, @CheckForNull User viewer, @CheckForNull User owner) {
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200190 return findViewable(postFilter, session, viewer, owner, null, null).posts;
191 }
192
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200193 public static PostPage<Post> findViewable(
194 PostFilter postFilter,
195 Session session,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200196 @CheckForNull User viewer,
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200197 @CheckForNull User owner,
198 @CheckForNull Integer cursor,
199 @CheckForNull Integer count) {
200 Class<? extends Post> entityClass;
201 switch (postFilter) {
202 case BOOKMARKS_ONLY:
203 entityClass = Bookmark.class;
204 break;
205 case LAZYCHAT_MESSAGES_ONLY:
206 entityClass = LazychatMessage.class;
207 break;
208 default:
209 entityClass = Post.class;
210 }
211 return findViewable(entityClass, session, viewer, owner, cursor, count);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100212 }
213
214 protected static <T extends Post> PostPage<T> findViewable(
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200215 Class<? extends T> entityClass,
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100216 Session session,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200217 @CheckForNull User viewer,
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100218 @CheckForNull User owner,
219 @CheckForNull Integer cursor,
220 @CheckForNull Integer count) {
221
222 if (cursor != null) {
223 Objects.requireNonNull(count);
224 }
225
226 var cb = session.getCriteriaBuilder();
227
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200228 var forwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, true);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100229 var forwardQuery = session.createQuery(forwardCriteria);
230
231 if (count != null) {
232 forwardQuery.setMaxResults(count + 1);
233 }
234
235 log.debug(forwardQuery.unwrap(org.hibernate.query.Query.class).getQueryString());
236
237 @CheckForNull Integer prevCursor = null;
238 @CheckForNull Integer nextCursor = null;
239
240 if (cursor != null) {
241 // Look backwards as well so we can find the prevCursor.
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200242 var backwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, false);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100243 var backwardQuery = session.createQuery(backwardCriteria);
244 backwardQuery.setMaxResults(count);
245 var backwardResults = backwardQuery.getResultList();
246 if (!backwardResults.isEmpty()) {
247 prevCursor = backwardResults.get(backwardResults.size() - 1).id;
248 }
249 }
250
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200251 var forwardResults = (List<T>) forwardQuery.getResultList();
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100252 if (count != null) {
253 if (forwardResults.size() == count + 1) {
254 nextCursor = forwardResults.get(count).id;
255 forwardResults.remove((int) count);
256 }
257 }
258
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +0200259 return new PostPage<>(prevCursor, cursor, nextCursor, forwardResults);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100260 }
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200261
262 public enum Visibility {
263 PUBLIC,
264 SEMIPRIVATE,
265 DISCRETIONARY,
266 PRIVATE,
267 }
268
269 @Override
270 public boolean equals(Object o) {
271 if (this == o) {
272 return true;
273 }
274 if (!(o instanceof Post)) {
275 return false;
276 }
277 Post post = (Post) o;
278 return Objects.equals(id, post.id);
279 }
280
281 @Override
282 public int hashCode() {
283 return Objects.hash(id);
284 }
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +0100285}