blob: 8f2166c9992e4d6c5115849aa1ee7e11de0fbdfa [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 Benkardf5999552020-03-22 06:52:06 +010022import javax.annotation.CheckForNull;
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +020023import javax.annotation.Nullable;
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020024import javax.json.bind.annotation.JsonbTransient;
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020025import javax.persistence.CascadeType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010026import javax.persistence.Column;
27import javax.persistence.Entity;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010028import javax.persistence.FetchType;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010029import javax.persistence.GeneratedValue;
30import javax.persistence.GenerationType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010031import javax.persistence.Id;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010032import javax.persistence.Inheritance;
33import javax.persistence.InheritanceType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010034import javax.persistence.JoinColumn;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010035import javax.persistence.JoinTable;
36import javax.persistence.ManyToMany;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010037import javax.persistence.ManyToOne;
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020038import javax.persistence.MapKey;
39import javax.persistence.OneToMany;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010040import javax.persistence.SequenceGenerator;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010041import javax.persistence.Table;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010042import javax.persistence.criteria.CriteriaBuilder;
43import javax.persistence.criteria.CriteriaQuery;
44import javax.persistence.criteria.From;
45import javax.persistence.criteria.JoinType;
46import javax.persistence.criteria.Predicate;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010047import org.hibernate.Session;
48import org.jboss.logging.Logger;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010049
50@Entity
Matthias Andreas Benkard57c9a8a2020-01-24 19:09:38 +010051@Table(name = "posts", schema = "benki")
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010052@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020053public abstract class Post<Text extends PostText<?>> extends PanacheEntityBase {
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010054
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +020055 private static final Logger log = Logger.getLogger(Post.class);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010056
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010057 @Id
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010058 @SequenceGenerator(
59 allocationSize = 1,
60 sequenceName = "posts_id_seq",
61 name = "posts_id_seq",
62 schema = "benki")
63 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "posts_id_seq")
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010064 @Column(name = "id", nullable = false)
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010065 public Integer id;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010066
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010067 @Column(name = "date", nullable = true)
Matthias Andreas Benkard1e7674c2020-04-18 20:28:51 +020068 @CheckForNull
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010069 public OffsetDateTime date;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010070
Matthias Andreas Benkardaa754802020-01-24 11:55:26 +010071 @ManyToOne(fetch = FetchType.LAZY)
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010072 @JoinColumn(name = "owner", referencedColumnName = "id")
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +020073 @CheckForNull
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020074 @JsonbTransient
Matthias Andreas Benkard35cb1592020-01-24 11:05:20 +010075 public User owner;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010076
77 @ManyToMany(fetch = FetchType.LAZY)
78 @JoinTable(
79 name = "user_visible_posts",
Matthias Andreas Benkard553de3e2020-01-27 05:33:15 +010080 schema = "benki",
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010081 joinColumns = @JoinColumn(name = "message"),
82 inverseJoinColumns = @JoinColumn(name = "user"))
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020083 @JsonbTransient
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010084 public Set<User> visibleTo;
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +010085
86 @ManyToMany(fetch = FetchType.LAZY)
87 @JoinTable(
88 name = "post_targets",
89 schema = "benki",
90 joinColumns = @JoinColumn(name = "message"),
91 inverseJoinColumns = @JoinColumn(name = "target"))
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020092 @JsonbTransient
Matthias Andreas Benkard2d4f92e2020-02-09 16:15:07 +010093 public Set<Role> targets;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010094
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +020095 @OneToMany(
96 mappedBy = "post",
97 fetch = FetchType.LAZY,
98 cascade = CascadeType.ALL,
99 targetEntity = PostText.class)
100 @MapKey(name = "language")
101 public Map<String, Text> texts = new HashMap<>();
102
103 public Map<String, Text> getTexts() {
104 return texts;
105 }
106
Matthias Andreas Benkard371164a2020-03-23 06:21:25 +0100107 public abstract boolean isBookmark();
108
109 public abstract boolean isLazychatMessage();
110
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200111 @CheckForNull
112 public abstract String getTitle();
113
114 @CheckForNull
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200115 public abstract String getUri();
116
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200117 public Visibility getVisibility() {
118 if (targets.isEmpty()) {
119 return Visibility.PRIVATE;
120 } else if (targets.contains(Role.getWorld())) {
121 return Visibility.PUBLIC;
122 } else {
123 // FIXME: There should really be a check whether targets.equals(owner.defaultTargets) here.
124 // Otherwise the actual visibility is DISCRETIONARY.
125 return Visibility.SEMIPRIVATE;
126 }
127 }
128
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100129 protected static <T extends Post> CriteriaQuery<T> queryViewable(
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100130 Class<T> entityClass,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200131 @CheckForNull User reader,
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100132 @CheckForNull User owner,
133 @CheckForNull Integer cursor,
134 CriteriaBuilder cb,
135 boolean forward) {
136 CriteriaQuery<T> query = cb.createQuery(entityClass);
137
138 var conditions = new ArrayList<Predicate>();
139
140 From<?, T> post;
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200141 if (reader == null) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100142 post = query.from(entityClass);
143 var target = post.join(Post_.targets);
144 conditions.add(cb.equal(target, Role.getWorld()));
145 } else {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100146 var root = query.from(User.class);
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200147 conditions.add(cb.equal(root, reader));
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200148 if (entityClass.isAssignableFrom(Post.class)) {
149 post = (From<?, T>) root.join(User_.visiblePosts);
150 } else if (entityClass.isAssignableFrom(Bookmark.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100151 post = (From<?, T>) root.join(User_.visibleBookmarks);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200152 } else if (entityClass.isAssignableFrom(LazychatMessage.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100153 post = (From<?, T>) root.join(User_.visibleLazychatMessages);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200154 } else {
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200155 throw new IllegalArgumentException();
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100156 }
157 }
158
159 query.select(post);
160 post.fetch(Post_.owner, JoinType.LEFT);
161
162 if (owner != null) {
163 conditions.add(cb.equal(post.get(Post_.owner), owner));
164 }
165
166 if (forward) {
167 query.orderBy(cb.desc(post.get(Post_.id)));
168 } else {
169 query.orderBy(cb.asc(post.get(Post_.id)));
170 }
171
172 if (cursor != null) {
173 if (forward) {
174 conditions.add(cb.le(post.get(Post_.id), cursor));
175 } else {
176 conditions.add(cb.gt(post.get(Post_.id), cursor));
177 }
178 }
179
180 query.where(conditions.toArray(new Predicate[0]));
181
182 return query;
183 }
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100184
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +0200185 public final boolean isVisibleTo(@Nullable User user) {
186 // FIXME: Make this more efficient.
187 return getVisibility() == Visibility.PUBLIC || (user != null && visibleTo.contains(user));
188 }
189
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200190 @CheckForNull
191 public final String getDescriptionHtml() {
192 var text = getText();
193 if (text == null) {
194 return null;
195 }
196 return text.getDescriptionHtml();
197 }
198
199 public static class PostPage<T extends Post<? extends PostText>> {
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +0200200 public @CheckForNull final Integer prevCursor;
201 public @CheckForNull final Integer cursor;
202 public @CheckForNull final Integer nextCursor;
203 public final List<T> posts;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100204
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200205 private static final TimeZone timeZone = TimeZone.getDefault();
206
207 public PostPage(
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100208 @CheckForNull Integer c0,
209 @CheckForNull Integer c1,
210 @CheckForNull Integer c2,
211 List<T> resultList) {
212 this.prevCursor = c0;
213 this.cursor = c1;
214 this.nextCursor = c2;
215 this.posts = resultList;
216 }
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200217
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200218 public void cacheDescriptions() {
219 days().forEach(Day::cacheDescriptions);
220 }
221
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200222 public class Day {
223 public final @CheckForNull LocalDate date;
224 public final List<T> posts;
225
226 private Day(LocalDate date, List<T> posts) {
227 this.date = date;
228 this.posts = posts;
229 }
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200230
231 public void cacheDescriptions() {
232 for (var post : posts) {
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200233 post.getTexts().values().forEach(PostText::getDescriptionHtml);
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200234 }
235 }
Matthias Andreas Benkard1c2a8a72020-04-26 06:09:57 +0200236 }
237
238 public List<Day> days() {
239 return posts.stream()
240 .collect(Collectors.groupingBy(post -> post.date.toLocalDate()))
241 .entrySet()
242 .stream()
243 .map(x -> new Day(x.getKey(), x.getValue()))
244 .sorted(Comparator.comparing((Day day) -> day.date).reversed())
245 .collect(Collectors.toUnmodifiableList());
246 }
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100247 }
248
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200249 public static PostPage<Post<? extends PostText>> findViewable(
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200250 PostFilter postFilter, Session session, @CheckForNull User viewer, @CheckForNull User owner) {
Matthias Andreas Benkard60c08922020-06-13 19:22:25 +0200251 return findViewable(postFilter, session, viewer, owner, null, null);
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200252 }
253
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200254 public static PostPage<Post<? extends PostText>> findViewable(
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200255 PostFilter postFilter,
256 Session session,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200257 @CheckForNull User viewer,
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200258 @CheckForNull User owner,
259 @CheckForNull Integer cursor,
260 @CheckForNull Integer count) {
261 Class<? extends Post> entityClass;
262 switch (postFilter) {
263 case BOOKMARKS_ONLY:
264 entityClass = Bookmark.class;
265 break;
266 case LAZYCHAT_MESSAGES_ONLY:
267 entityClass = LazychatMessage.class;
268 break;
269 default:
270 entityClass = Post.class;
271 }
272 return findViewable(entityClass, session, viewer, owner, cursor, count);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100273 }
274
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200275 protected static <T extends Post<? extends PostText>> PostPage<T> findViewable(
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200276 Class<? extends T> entityClass,
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100277 Session session,
Matthias Andreas Benkardcf0fe882020-04-19 18:33:37 +0200278 @CheckForNull User viewer,
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100279 @CheckForNull User owner,
280 @CheckForNull Integer cursor,
281 @CheckForNull Integer count) {
282
283 if (cursor != null) {
284 Objects.requireNonNull(count);
285 }
286
287 var cb = session.getCriteriaBuilder();
288
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200289 var forwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, true);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100290 var forwardQuery = session.createQuery(forwardCriteria);
291
292 if (count != null) {
293 forwardQuery.setMaxResults(count + 1);
294 }
295
296 log.debug(forwardQuery.unwrap(org.hibernate.query.Query.class).getQueryString());
297
298 @CheckForNull Integer prevCursor = null;
299 @CheckForNull Integer nextCursor = null;
300
301 if (cursor != null) {
302 // Look backwards as well so we can find the prevCursor.
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200303 var backwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, false);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100304 var backwardQuery = session.createQuery(backwardCriteria);
305 backwardQuery.setMaxResults(count);
306 var backwardResults = backwardQuery.getResultList();
307 if (!backwardResults.isEmpty()) {
308 prevCursor = backwardResults.get(backwardResults.size() - 1).id;
309 }
310 }
311
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200312 var forwardResults = (List<T>) forwardQuery.getResultList();
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100313 if (count != null) {
314 if (forwardResults.size() == count + 1) {
315 nextCursor = forwardResults.get(count).id;
316 forwardResults.remove((int) count);
317 }
318 }
319
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200320 // Fetch texts (to avoid n+1 selects).
321 var postIds = forwardResults.stream().map(x -> x.id).collect(toList());
322
323 if (!postIds.isEmpty()) {
324 find("SELECT p FROM Post p LEFT JOIN FETCH p.texts WHERE p.id IN (?1)", postIds).stream()
325 .count();
326 }
327
Matthias Andreas Benkard593765d2020-04-18 20:44:07 +0200328 return new PostPage<>(prevCursor, cursor, nextCursor, forwardResults);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100329 }
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200330
Matthias Andreas Benkardd5498fc2020-08-23 21:51:00 +0200331 @CheckForNull
332 protected Text getText() {
333 var texts = getTexts();
334 if (texts.isEmpty()) {
335 return null;
336 } else if (texts.containsKey("")) {
337 return texts.get("");
338 } else if (texts.containsKey("en")) {
339 return texts.get("en");
340 } else {
341 return texts.values().stream().findAny().get();
342 }
343 }
344
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200345 public enum Visibility {
346 PUBLIC,
347 SEMIPRIVATE,
348 DISCRETIONARY,
349 PRIVATE,
350 }
351
352 @Override
353 public boolean equals(Object o) {
354 if (this == o) {
355 return true;
356 }
357 if (!(o instanceof Post)) {
358 return false;
359 }
360 Post post = (Post) o;
361 return Objects.equals(id, post.id);
362 }
363
364 @Override
365 public int hashCode() {
366 return Objects.hash(id);
367 }
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +0100368}