blob: fd023d71f765b1f9ece29cbc91156058273fd3bc [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 Benkardd5498fc2020-08-23 21:51:00 +02003import static java.util.stream.Collectors.toList;
4
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +01005import eu.mulk.mulkcms2.benki.accesscontrol.Role;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +01006import eu.mulk.mulkcms2.benki.bookmarks.Bookmark;
7import eu.mulk.mulkcms2.benki.lazychat.LazychatMessage;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +01008import eu.mulk.mulkcms2.benki.users.User;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +01009import eu.mulk.mulkcms2.benki.users.User_;
Matthias Andreas Benkard35cb1592020-01-24 11:05:20 +010010import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +020011import java.time.LocalDate;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010012import java.time.OffsetDateTime;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010013import java.util.ArrayList;
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +020014import java.util.Comparator;
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020015import java.util.HashMap;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010016import java.util.List;
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020017import java.util.Map;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010018import java.util.Objects;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010019import java.util.Set;
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +020020import java.util.TimeZone;
21import java.util.stream.Collectors;
Matthias Andreas Benkard8563a3c2020-09-16 17:57:24 +020022import java.util.stream.Stream;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010023import javax.annotation.CheckForNull;
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +020024import javax.annotation.Nullable;
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020025import javax.json.bind.annotation.JsonbTransient;
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020026import javax.persistence.CascadeType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010027import javax.persistence.Column;
28import javax.persistence.Entity;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010029import javax.persistence.FetchType;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010030import javax.persistence.GeneratedValue;
31import javax.persistence.GenerationType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010032import javax.persistence.Id;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010033import javax.persistence.Inheritance;
34import javax.persistence.InheritanceType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010035import javax.persistence.JoinColumn;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010036import javax.persistence.JoinTable;
37import javax.persistence.ManyToMany;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010038import javax.persistence.ManyToOne;
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020039import javax.persistence.MapKey;
40import javax.persistence.OneToMany;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010041import javax.persistence.SequenceGenerator;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010042import javax.persistence.Table;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010043import javax.persistence.criteria.CriteriaBuilder;
44import javax.persistence.criteria.CriteriaQuery;
45import javax.persistence.criteria.From;
46import javax.persistence.criteria.JoinType;
47import javax.persistence.criteria.Predicate;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010048import org.hibernate.Session;
49import org.jboss.logging.Logger;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010050
51@Entity
Matthias Andreas Benkard57c9a8a2020-01-24 19:09:38 +010052@Table(name = "posts", schema = "benki")
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010053@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020054public abstract class Post<Text extends PostText<?>> extends PanacheEntityBase {
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010055
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +020056 private static final Logger log = Logger.getLogger(Post.class);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010057
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010058 @Id
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010059 @SequenceGenerator(
60 allocationSize = 1,
61 sequenceName = "posts_id_seq",
62 name = "posts_id_seq",
63 schema = "benki")
64 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "posts_id_seq")
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010065 @Column(name = "id", nullable = false)
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010066 public Integer id;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010067
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010068 @Column(name = "date", nullable = true)
Matthias Andreas Benkard1e7674c2020-04-18 20:28:51 +020069 @CheckForNull
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010070 public OffsetDateTime date;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010071
Matthias Andreas Benkardaa754802020-01-24 11:55:26 +010072 @ManyToOne(fetch = FetchType.LAZY)
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010073 @JoinColumn(name = "owner", referencedColumnName = "id")
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +020074 @CheckForNull
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020075 @JsonbTransient
Matthias Andreas Benkard35cb1592020-01-24 11:05:20 +010076 public User owner;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010077
78 @ManyToMany(fetch = FetchType.LAZY)
79 @JoinTable(
80 name = "user_visible_posts",
Matthias Andreas Benkard553de3e2020-01-27 05:33:15 +010081 schema = "benki",
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010082 joinColumns = @JoinColumn(name = "message"),
83 inverseJoinColumns = @JoinColumn(name = "user"))
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020084 @JsonbTransient
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010085 public Set<User> visibleTo;
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +010086
87 @ManyToMany(fetch = FetchType.LAZY)
88 @JoinTable(
89 name = "post_targets",
90 schema = "benki",
91 joinColumns = @JoinColumn(name = "message"),
92 inverseJoinColumns = @JoinColumn(name = "target"))
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020093 @JsonbTransient
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +010094 public Set<Role> targets;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010095
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020096 @OneToMany(
97 mappedBy = "post",
98 fetch = FetchType.LAZY,
99 cascade = CascadeType.ALL,
100 targetEntity = PostText.class)
101 @MapKey(name = "language")
102 public Map<String, Text> texts = new HashMap<>();
103
104 public Map<String, Text> getTexts() {
105 return texts;
106 }
107
Matthias Andreas Benkard371164a2020-03-23 06:21:25 +0100108 public abstract boolean isBookmark();
109
110 public abstract boolean isLazychatMessage();
111
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200112 @CheckForNull
113 public abstract String getTitle();
114
115 @CheckForNull
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200116 public abstract String getUri();
117
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200118 public Visibility getVisibility() {
119 if (targets.isEmpty()) {
120 return Visibility.PRIVATE;
121 } else if (targets.contains(Role.getWorld())) {
122 return Visibility.PUBLIC;
123 } else {
124 // FIXME: There should really be a check whether targets.equals(owner.defaultTargets) here.
125 // Otherwise the actual visibility is DISCRETIONARY.
126 return Visibility.SEMIPRIVATE;
127 }
128 }
129
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100130 protected static <T extends Post> CriteriaQuery<T> queryViewable(
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100131 Class<T> entityClass,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200132 @CheckForNull User reader,
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100133 @CheckForNull User owner,
134 @CheckForNull Integer cursor,
135 CriteriaBuilder cb,
Matthias Andreas Benkard8563a3c2020-09-16 17:57:24 +0200136 boolean forward,
137 @CheckForNull String searchQuery) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100138 CriteriaQuery<T> query = cb.createQuery(entityClass);
139
140 var conditions = new ArrayList<Predicate>();
141
142 From<?, T> post;
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200143 if (reader == null) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100144 post = query.from(entityClass);
145 var target = post.join(Post_.targets);
146 conditions.add(cb.equal(target, Role.getWorld()));
147 } else {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100148 var root = query.from(User.class);
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200149 conditions.add(cb.equal(root, reader));
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200150 if (entityClass.isAssignableFrom(Post.class)) {
151 post = (From<?, T>) root.join(User_.visiblePosts);
152 } else if (entityClass.isAssignableFrom(Bookmark.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100153 post = (From<?, T>) root.join(User_.visibleBookmarks);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200154 } else if (entityClass.isAssignableFrom(LazychatMessage.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100155 post = (From<?, T>) root.join(User_.visibleLazychatMessages);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200156 } else {
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200157 throw new IllegalArgumentException();
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100158 }
159 }
160
161 query.select(post);
162 post.fetch(Post_.owner, JoinType.LEFT);
163
164 if (owner != null) {
165 conditions.add(cb.equal(post.get(Post_.owner), owner));
166 }
167
168 if (forward) {
169 query.orderBy(cb.desc(post.get(Post_.id)));
170 } else {
171 query.orderBy(cb.asc(post.get(Post_.id)));
172 }
173
174 if (cursor != null) {
175 if (forward) {
176 conditions.add(cb.le(post.get(Post_.id), cursor));
177 } else {
178 conditions.add(cb.gt(post.get(Post_.id), cursor));
179 }
180 }
181
Matthias Andreas Benkard8563a3c2020-09-16 17:57:24 +0200182 if (searchQuery != null && !searchQuery.isBlank()) {
183 var postTexts = post.join(Post_.texts);
184 var localizedSearches =
185 Stream.of("de", "en")
186 .map(
187 language ->
188 cb.isTrue(
189 cb.function(
190 "post_matches_websearch",
191 Boolean.class,
192 postTexts.get(PostText_.searchTerms),
193 cb.literal(language),
194 cb.literal(searchQuery))))
195 .toArray(n -> new Predicate[n]);
196 conditions.add(cb.or(localizedSearches));
197 }
198
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100199 query.where(conditions.toArray(new Predicate[0]));
200
201 return query;
202 }
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100203
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +0200204 public final boolean isVisibleTo(@Nullable User user) {
205 // FIXME: Make this more efficient.
206 return getVisibility() == Visibility.PUBLIC || (user != null && visibleTo.contains(user));
207 }
208
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200209 @CheckForNull
210 public final String getDescriptionHtml() {
211 var text = getText();
212 if (text == null) {
213 return null;
214 }
215 return text.getDescriptionHtml();
216 }
217
218 public static class PostPage<T extends Post<? extends PostText>> {
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +0200219 public @CheckForNull final Integer prevCursor;
220 public @CheckForNull final Integer cursor;
221 public @CheckForNull final Integer nextCursor;
222 public final List<T> posts;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100223
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200224 private static final TimeZone timeZone = TimeZone.getDefault();
225
226 public PostPage(
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100227 @CheckForNull Integer c0,
228 @CheckForNull Integer c1,
229 @CheckForNull Integer c2,
230 List<T> resultList) {
231 this.prevCursor = c0;
232 this.cursor = c1;
233 this.nextCursor = c2;
234 this.posts = resultList;
235 }
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200236
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200237 public void cacheDescriptions() {
238 days().forEach(Day::cacheDescriptions);
239 }
240
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200241 public class Day {
242 public final @CheckForNull LocalDate date;
243 public final List<T> posts;
244
245 private Day(LocalDate date, List<T> posts) {
246 this.date = date;
247 this.posts = posts;
248 }
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200249
250 public void cacheDescriptions() {
251 for (var post : posts) {
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200252 post.getTexts().values().forEach(PostText::getDescriptionHtml);
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200253 }
254 }
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200255 }
256
257 public List<Day> days() {
258 return posts.stream()
259 .collect(Collectors.groupingBy(post -> post.date.toLocalDate()))
260 .entrySet()
261 .stream()
262 .map(x -> new Day(x.getKey(), x.getValue()))
263 .sorted(Comparator.comparing((Day day) -> day.date).reversed())
264 .collect(Collectors.toUnmodifiableList());
265 }
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100266 }
267
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200268 public static PostPage<Post<? extends PostText>> findViewable(
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200269 PostFilter postFilter, Session session, @CheckForNull User viewer, @CheckForNull User owner) {
Matthias Andreas Benkard8563a3c2020-09-16 17:57:24 +0200270 return findViewable(postFilter, session, viewer, owner, null, null, null);
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200271 }
272
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200273 public static PostPage<Post<? extends PostText>> findViewable(
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200274 PostFilter postFilter,
275 Session session,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200276 @CheckForNull User viewer,
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200277 @CheckForNull User owner,
278 @CheckForNull Integer cursor,
Matthias Andreas Benkard8563a3c2020-09-16 17:57:24 +0200279 @CheckForNull Integer count,
280 @CheckForNull String searchQuery) {
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200281 Class<? extends Post> entityClass;
282 switch (postFilter) {
283 case BOOKMARKS_ONLY:
284 entityClass = Bookmark.class;
285 break;
286 case LAZYCHAT_MESSAGES_ONLY:
287 entityClass = LazychatMessage.class;
288 break;
289 default:
290 entityClass = Post.class;
291 }
Matthias Andreas Benkard8563a3c2020-09-16 17:57:24 +0200292 return findViewable(entityClass, session, viewer, owner, cursor, count, searchQuery);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100293 }
294
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200295 protected static <T extends Post<? extends PostText>> PostPage<T> findViewable(
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200296 Class<? extends T> entityClass,
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100297 Session session,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200298 @CheckForNull User viewer,
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100299 @CheckForNull User owner,
300 @CheckForNull Integer cursor,
Matthias Andreas Benkard8563a3c2020-09-16 17:57:24 +0200301 @CheckForNull Integer count,
302 @CheckForNull String searchQuery) {
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100303
304 if (cursor != null) {
305 Objects.requireNonNull(count);
306 }
307
308 var cb = session.getCriteriaBuilder();
309
Matthias Andreas Benkard8563a3c2020-09-16 17:57:24 +0200310 var forwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, true, searchQuery);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100311 var forwardQuery = session.createQuery(forwardCriteria);
312
313 if (count != null) {
314 forwardQuery.setMaxResults(count + 1);
315 }
316
317 log.debug(forwardQuery.unwrap(org.hibernate.query.Query.class).getQueryString());
318
319 @CheckForNull Integer prevCursor = null;
320 @CheckForNull Integer nextCursor = null;
321
322 if (cursor != null) {
323 // Look backwards as well so we can find the prevCursor.
Matthias Andreas Benkard8563a3c2020-09-16 17:57:24 +0200324 var backwardCriteria =
325 queryViewable(entityClass, viewer, owner, cursor, cb, false, searchQuery);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100326 var backwardQuery = session.createQuery(backwardCriteria);
327 backwardQuery.setMaxResults(count);
328 var backwardResults = backwardQuery.getResultList();
329 if (!backwardResults.isEmpty()) {
330 prevCursor = backwardResults.get(backwardResults.size() - 1).id;
331 }
332 }
333
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200334 var forwardResults = (List<T>) forwardQuery.getResultList();
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100335 if (count != null) {
336 if (forwardResults.size() == count + 1) {
337 nextCursor = forwardResults.get(count).id;
338 forwardResults.remove((int) count);
339 }
340 }
341
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200342 // Fetch texts (to avoid n+1 selects).
343 var postIds = forwardResults.stream().map(x -> x.id).collect(toList());
344
345 if (!postIds.isEmpty()) {
346 find("SELECT p FROM Post p LEFT JOIN FETCH p.texts WHERE p.id IN (?1)", postIds).stream()
347 .count();
348 }
349
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +0200350 return new PostPage<>(prevCursor, cursor, nextCursor, forwardResults);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100351 }
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200352
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200353 @CheckForNull
354 protected Text getText() {
355 var texts = getTexts();
356 if (texts.isEmpty()) {
357 return null;
358 } else if (texts.containsKey("")) {
359 return texts.get("");
360 } else if (texts.containsKey("en")) {
361 return texts.get("en");
362 } else {
363 return texts.values().stream().findAny().get();
364 }
365 }
366
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200367 public enum Visibility {
368 PUBLIC,
369 SEMIPRIVATE,
370 DISCRETIONARY,
371 PRIVATE,
372 }
373
374 @Override
375 public boolean equals(Object o) {
376 if (this == o) {
377 return true;
378 }
379 if (!(o instanceof Post)) {
380 return false;
381 }
382 Post post = (Post) o;
383 return Objects.equals(id, post.id);
384 }
385
386 @Override
387 public int hashCode() {
388 return Objects.hash(id);
389 }
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +0100390}