blob: aa15fa24814faaf6b1ca925b843fe8045cdf6d90 [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 Benkard1c2a8a72020-04-26 06:09:57 +02009import java.time.LocalDate;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010010import java.time.OffsetDateTime;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010011import java.util.ArrayList;
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +020012import java.util.Comparator;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010013import java.util.List;
14import java.util.Objects;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010015import java.util.Set;
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +020016import java.util.TimeZone;
17import java.util.stream.Collectors;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010018import javax.annotation.CheckForNull;
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +020019import javax.annotation.Nullable;
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020020import javax.json.bind.annotation.JsonbTransient;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010021import javax.persistence.Column;
22import javax.persistence.Entity;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010023import javax.persistence.FetchType;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010024import javax.persistence.GeneratedValue;
25import javax.persistence.GenerationType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010026import javax.persistence.Id;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010027import javax.persistence.Inheritance;
28import javax.persistence.InheritanceType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010029import javax.persistence.JoinColumn;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010030import javax.persistence.JoinTable;
31import javax.persistence.ManyToMany;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010032import javax.persistence.ManyToOne;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010033import javax.persistence.SequenceGenerator;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010034import javax.persistence.Table;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010035import javax.persistence.criteria.CriteriaBuilder;
36import javax.persistence.criteria.CriteriaQuery;
37import javax.persistence.criteria.From;
38import javax.persistence.criteria.JoinType;
39import javax.persistence.criteria.Predicate;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010040import org.hibernate.Session;
41import org.jboss.logging.Logger;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010042
43@Entity
Matthias Andreas Benkard57c9a8a2020-01-24 19:09:38 +010044@Table(name = "posts", schema = "benki")
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010045@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
46public abstract class Post extends PanacheEntityBase {
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010047
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +020048 private static final Logger log = Logger.getLogger(Post.class);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010049
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +020050 private static final int DESCRIPTION_CACHE_VERSION = 1;
51
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010052 @Id
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010053 @SequenceGenerator(
54 allocationSize = 1,
55 sequenceName = "posts_id_seq",
56 name = "posts_id_seq",
57 schema = "benki")
58 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "posts_id_seq")
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010059 @Column(name = "id", nullable = false)
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010060 public Integer id;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010061
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010062 @Column(name = "date", nullable = true)
Matthias Andreas Benkard1e7674c2020-04-18 20:28:51 +020063 @CheckForNull
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010064 public OffsetDateTime date;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010065
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +020066 @Column(name = "cached_description_version", nullable = true)
67 @CheckForNull
68 public Integer cachedDescriptionVersion;
69
70 @Column(name = "cached_description_html", nullable = true)
71 @CheckForNull
72 public String cachedDescriptionHtml;
73
Matthias Andreas Benkardaa754802020-01-24 11:55:26 +010074 @ManyToOne(fetch = FetchType.LAZY)
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010075 @JoinColumn(name = "owner", referencedColumnName = "id")
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +020076 @CheckForNull
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020077 @JsonbTransient
Matthias Andreas Benkard35cb1592020-01-24 11:05:20 +010078 public User owner;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010079
80 @ManyToMany(fetch = FetchType.LAZY)
81 @JoinTable(
82 name = "user_visible_posts",
Matthias Andreas Benkard553de3e2020-01-27 05:33:15 +010083 schema = "benki",
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010084 joinColumns = @JoinColumn(name = "message"),
85 inverseJoinColumns = @JoinColumn(name = "user"))
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020086 @JsonbTransient
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010087 public Set<User> visibleTo;
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +010088
89 @ManyToMany(fetch = FetchType.LAZY)
90 @JoinTable(
91 name = "post_targets",
92 schema = "benki",
93 joinColumns = @JoinColumn(name = "message"),
94 inverseJoinColumns = @JoinColumn(name = "target"))
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020095 @JsonbTransient
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +010096 public Set<Role> targets;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010097
Matthias Andreas Benkard371164a2020-03-23 06:21:25 +010098 public abstract boolean isBookmark();
99
100 public abstract boolean isLazychatMessage();
101
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200102 @CheckForNull
103 public abstract String getTitle();
104
105 @CheckForNull
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200106 public final String getDescriptionHtml() {
107 if (cachedDescriptionHtml != null &&
108 cachedDescriptionVersion != null &&
109 cachedDescriptionVersion >= DESCRIPTION_CACHE_VERSION){
110 return cachedDescriptionHtml;
111 } else {
112 @CheckForNull var descriptionHtml = computeDescriptionHtml();
113 cachedDescriptionHtml = descriptionHtml;
114 cachedDescriptionVersion = DESCRIPTION_CACHE_VERSION;
115 return descriptionHtml;
116 }
117 }
118
119 @CheckForNull
120 protected abstract String computeDescriptionHtml();
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200121
122 @CheckForNull
123 public abstract String getUri();
124
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200125 public Visibility getVisibility() {
126 if (targets.isEmpty()) {
127 return Visibility.PRIVATE;
128 } else if (targets.contains(Role.getWorld())) {
129 return Visibility.PUBLIC;
130 } else {
131 // FIXME: There should really be a check whether targets.equals(owner.defaultTargets) here.
132 // Otherwise the actual visibility is DISCRETIONARY.
133 return Visibility.SEMIPRIVATE;
134 }
135 }
136
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100137 protected static <T extends Post> CriteriaQuery<T> queryViewable(
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100138 Class<T> entityClass,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200139 @CheckForNull User reader,
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100140 @CheckForNull User owner,
141 @CheckForNull Integer cursor,
142 CriteriaBuilder cb,
143 boolean forward) {
144 CriteriaQuery<T> query = cb.createQuery(entityClass);
145
146 var conditions = new ArrayList<Predicate>();
147
148 From<?, T> post;
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200149 if (reader == null) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100150 post = query.from(entityClass);
151 var target = post.join(Post_.targets);
152 conditions.add(cb.equal(target, Role.getWorld()));
153 } else {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100154 var root = query.from(User.class);
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200155 conditions.add(cb.equal(root, reader));
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200156 if (entityClass.isAssignableFrom(Post.class)) {
157 post = (From<?, T>) root.join(User_.visiblePosts);
158 } else if (entityClass.isAssignableFrom(Bookmark.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100159 post = (From<?, T>) root.join(User_.visibleBookmarks);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200160 } else if (entityClass.isAssignableFrom(LazychatMessage.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100161 post = (From<?, T>) root.join(User_.visibleLazychatMessages);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200162 } else {
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200163 throw new IllegalArgumentException();
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100164 }
165 }
166
167 query.select(post);
168 post.fetch(Post_.owner, JoinType.LEFT);
169
170 if (owner != null) {
171 conditions.add(cb.equal(post.get(Post_.owner), owner));
172 }
173
174 if (forward) {
175 query.orderBy(cb.desc(post.get(Post_.id)));
176 } else {
177 query.orderBy(cb.asc(post.get(Post_.id)));
178 }
179
180 if (cursor != null) {
181 if (forward) {
182 conditions.add(cb.le(post.get(Post_.id), cursor));
183 } else {
184 conditions.add(cb.gt(post.get(Post_.id), cursor));
185 }
186 }
187
188 query.where(conditions.toArray(new Predicate[0]));
189
190 return query;
191 }
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100192
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +0200193 public final boolean isVisibleTo(@Nullable User user) {
194 // FIXME: Make this more efficient.
195 return getVisibility() == Visibility.PUBLIC || (user != null && visibleTo.contains(user));
196 }
197
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100198 public static class PostPage<T extends Post> {
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +0200199 public @CheckForNull final Integer prevCursor;
200 public @CheckForNull final Integer cursor;
201 public @CheckForNull final Integer nextCursor;
202 public final List<T> posts;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100203
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200204 private static final TimeZone timeZone = TimeZone.getDefault();
205
206 public PostPage(
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100207 @CheckForNull Integer c0,
208 @CheckForNull Integer c1,
209 @CheckForNull Integer c2,
210 List<T> resultList) {
211 this.prevCursor = c0;
212 this.cursor = c1;
213 this.nextCursor = c2;
214 this.posts = resultList;
215 }
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200216
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200217 public void cacheDescriptions() {
218 days().forEach(Day::cacheDescriptions);
219 }
220
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200221 public class Day {
222 public final @CheckForNull LocalDate date;
223 public final List<T> posts;
224
225 private Day(LocalDate date, List<T> posts) {
226 this.date = date;
227 this.posts = posts;
228 }
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200229
230 public void cacheDescriptions() {
231 for (var post : posts) {
232 post.getDescriptionHtml();
233 }
234 }
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200235 }
236
237 public List<Day> days() {
238 return posts.stream()
239 .collect(Collectors.groupingBy(post -> post.date.toLocalDate()))
240 .entrySet()
241 .stream()
242 .map(x -> new Day(x.getKey(), x.getValue()))
243 .sorted(Comparator.comparing((Day day) -> day.date).reversed())
244 .collect(Collectors.toUnmodifiableList());
245 }
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100246 }
247
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200248 public static PostPage<Post> findViewable(
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200249 PostFilter postFilter, Session session, @CheckForNull User viewer, @CheckForNull User owner) {
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200250 return findViewable(postFilter, session, viewer, owner, null, null);
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200251 }
252
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200253 public static PostPage<Post> findViewable(
254 PostFilter postFilter,
255 Session session,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200256 @CheckForNull User viewer,
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200257 @CheckForNull User owner,
258 @CheckForNull Integer cursor,
259 @CheckForNull Integer count) {
260 Class<? extends Post> entityClass;
261 switch (postFilter) {
262 case BOOKMARKS_ONLY:
263 entityClass = Bookmark.class;
264 break;
265 case LAZYCHAT_MESSAGES_ONLY:
266 entityClass = LazychatMessage.class;
267 break;
268 default:
269 entityClass = Post.class;
270 }
271 return findViewable(entityClass, session, viewer, owner, cursor, count);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100272 }
273
274 protected static <T extends Post> PostPage<T> findViewable(
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200275 Class<? extends T> entityClass,
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100276 Session session,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200277 @CheckForNull User viewer,
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100278 @CheckForNull User owner,
279 @CheckForNull Integer cursor,
280 @CheckForNull Integer count) {
281
282 if (cursor != null) {
283 Objects.requireNonNull(count);
284 }
285
286 var cb = session.getCriteriaBuilder();
287
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200288 var forwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, true);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100289 var forwardQuery = session.createQuery(forwardCriteria);
290
291 if (count != null) {
292 forwardQuery.setMaxResults(count + 1);
293 }
294
295 log.debug(forwardQuery.unwrap(org.hibernate.query.Query.class).getQueryString());
296
297 @CheckForNull Integer prevCursor = null;
298 @CheckForNull Integer nextCursor = null;
299
300 if (cursor != null) {
301 // Look backwards as well so we can find the prevCursor.
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200302 var backwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, false);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100303 var backwardQuery = session.createQuery(backwardCriteria);
304 backwardQuery.setMaxResults(count);
305 var backwardResults = backwardQuery.getResultList();
306 if (!backwardResults.isEmpty()) {
307 prevCursor = backwardResults.get(backwardResults.size() - 1).id;
308 }
309 }
310
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200311 var forwardResults = (List<T>) forwardQuery.getResultList();
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100312 if (count != null) {
313 if (forwardResults.size() == count + 1) {
314 nextCursor = forwardResults.get(count).id;
315 forwardResults.remove((int) count);
316 }
317 }
318
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +0200319 return new PostPage<>(prevCursor, cursor, nextCursor, forwardResults);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100320 }
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200321
322 public enum Visibility {
323 PUBLIC,
324 SEMIPRIVATE,
325 DISCRETIONARY,
326 PRIVATE,
327 }
328
329 @Override
330 public boolean equals(Object o) {
331 if (this == o) {
332 return true;
333 }
334 if (!(o instanceof Post)) {
335 return false;
336 }
337 Post post = (Post) o;
338 return Objects.equals(id, post.id);
339 }
340
341 @Override
342 public int hashCode() {
343 return Objects.hash(id);
344 }
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +0100345}