blob: c40226599bb9a18ecc8d2e34a07189a2708718d1 [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 Benkardf5999552020-03-22 06:52:06 +01009import io.quarkus.security.identity.SecurityIdentity;
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 Benkard3d399f32020-03-22 07:23:07 +010012import java.util.List;
13import java.util.Objects;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010014import java.util.Set;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010015import javax.annotation.CheckForNull;
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +020016import javax.annotation.Nullable;
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +020017import javax.json.bind.annotation.JsonbTransient;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010018import javax.persistence.Column;
19import javax.persistence.Entity;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010020import javax.persistence.FetchType;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010021import javax.persistence.GeneratedValue;
22import javax.persistence.GenerationType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010023import javax.persistence.Id;
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010024import javax.persistence.Inheritance;
25import javax.persistence.InheritanceType;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010026import javax.persistence.JoinColumn;
Matthias Andreas Benkardf9c74272020-01-24 11:51:35 +010027import javax.persistence.JoinTable;
28import javax.persistence.ManyToMany;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010029import javax.persistence.ManyToOne;
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010030import javax.persistence.SequenceGenerator;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010031import javax.persistence.Table;
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +010032import javax.persistence.criteria.CriteriaBuilder;
33import javax.persistence.criteria.CriteriaQuery;
34import javax.persistence.criteria.From;
35import javax.persistence.criteria.JoinType;
36import javax.persistence.criteria.Predicate;
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010037import org.hibernate.Session;
38import org.jboss.logging.Logger;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010039
40@Entity
Matthias Andreas Benkard57c9a8a2020-01-24 19:09:38 +010041@Table(name = "posts", schema = "benki")
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010042@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
43public abstract class Post extends PanacheEntityBase {
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010044
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +010045 private static Logger log = Logger.getLogger(Post.class);
46
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010047 @Id
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010048 @SequenceGenerator(
49 allocationSize = 1,
50 sequenceName = "posts_id_seq",
51 name = "posts_id_seq",
52 schema = "benki")
53 @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "posts_id_seq")
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010054 @Column(name = "id", nullable = false)
Matthias Andreas Benkard0246c3e2020-01-27 05:39:08 +010055 public Integer id;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010056
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010057 @Column(name = "date", nullable = true)
Matthias Andreas Benkard1e7674c2020-04-18 20:28:51 +020058 @CheckForNull
Matthias Andreas Benkardd9b95882020-01-24 11:42:49 +010059 public OffsetDateTime date;
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010060
Matthias Andreas Benkardaa754802020-01-24 11:55:26 +010061 @ManyToOne(fetch = FetchType.LAZY)
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +010062 @JoinColumn(name = "owner", referencedColumnName = "id")
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,
111 SecurityIdentity readerIdentity,
112 @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;
121 if (readerIdentity.isAnonymous()) {
122 post = query.from(entityClass);
123 var target = post.join(Post_.targets);
124 conditions.add(cb.equal(target, Role.getWorld()));
125 } else {
126 var userName = readerIdentity.getPrincipal().getName();
127 var user = User.findByNickname(userName);
128
129 var root = query.from(User.class);
130 conditions.add(cb.equal(root, user));
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200131 if (entityClass.isAssignableFrom(Post.class)) {
132 post = (From<?, T>) root.join(User_.visiblePosts);
133 } else if (entityClass.isAssignableFrom(Bookmark.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100134 post = (From<?, T>) root.join(User_.visibleBookmarks);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200135 } else if (entityClass.isAssignableFrom(LazychatMessage.class)) {
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100136 post = (From<?, T>) root.join(User_.visibleLazychatMessages);
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200137 } else {
Matthias Andreas Benkardca4d7942020-04-18 14:13:41 +0200138 throw new IllegalArgumentException();
Matthias Andreas Benkardf5999552020-03-22 06:52:06 +0100139 }
140 }
141
142 query.select(post);
143 post.fetch(Post_.owner, JoinType.LEFT);
144
145 if (owner != null) {
146 conditions.add(cb.equal(post.get(Post_.owner), owner));
147 }
148
149 if (forward) {
150 query.orderBy(cb.desc(post.get(Post_.id)));
151 } else {
152 query.orderBy(cb.asc(post.get(Post_.id)));
153 }
154
155 if (cursor != null) {
156 if (forward) {
157 conditions.add(cb.le(post.get(Post_.id), cursor));
158 } else {
159 conditions.add(cb.gt(post.get(Post_.id), cursor));
160 }
161 }
162
163 query.where(conditions.toArray(new Predicate[0]));
164
165 return query;
166 }
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100167
Matthias Andreas Benkard6cfe16b2020-04-18 15:36:04 +0200168 public final boolean isVisibleTo(@Nullable User user) {
169 // FIXME: Make this more efficient.
170 return getVisibility() == Visibility.PUBLIC || (user != null && visibleTo.contains(user));
171 }
172
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100173 public static class PostPage<T extends Post> {
174 public @CheckForNull Integer prevCursor;
175 public @CheckForNull Integer cursor;
176 public @CheckForNull Integer nextCursor;
177 public List<T> posts;
178
179 private PostPage(
180 @CheckForNull Integer c0,
181 @CheckForNull Integer c1,
182 @CheckForNull Integer c2,
183 List<T> resultList) {
184 this.prevCursor = c0;
185 this.cursor = c1;
186 this.nextCursor = c2;
187 this.posts = resultList;
188 }
189 }
190
Matthias Andreas Benkardd5ae0d52020-03-29 18:57:22 +0200191 public static List<Post> findViewable(
192 PostFilter postFilter, Session session, SecurityIdentity viewer, @CheckForNull User owner) {
193 return findViewable(postFilter, session, viewer, owner, null, null).posts;
194 }
195
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200196 public static PostPage<Post> findViewable(
197 PostFilter postFilter,
198 Session session,
199 SecurityIdentity viewer,
200 @CheckForNull User owner,
201 @CheckForNull Integer cursor,
202 @CheckForNull Integer count) {
203 Class<? extends Post> entityClass;
204 switch (postFilter) {
205 case BOOKMARKS_ONLY:
206 entityClass = Bookmark.class;
207 break;
208 case LAZYCHAT_MESSAGES_ONLY:
209 entityClass = LazychatMessage.class;
210 break;
211 default:
212 entityClass = Post.class;
213 }
214 return findViewable(entityClass, session, viewer, owner, cursor, count);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100215 }
216
217 protected static <T extends Post> PostPage<T> findViewable(
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200218 Class<? extends T> entityClass,
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100219 Session session,
220 SecurityIdentity viewer,
221 @CheckForNull User owner,
222 @CheckForNull Integer cursor,
223 @CheckForNull Integer count) {
224
225 if (cursor != null) {
226 Objects.requireNonNull(count);
227 }
228
229 var cb = session.getCriteriaBuilder();
230
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200231 var forwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, true);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100232 var forwardQuery = session.createQuery(forwardCriteria);
233
234 if (count != null) {
235 forwardQuery.setMaxResults(count + 1);
236 }
237
238 log.debug(forwardQuery.unwrap(org.hibernate.query.Query.class).getQueryString());
239
240 @CheckForNull Integer prevCursor = null;
241 @CheckForNull Integer nextCursor = null;
242
243 if (cursor != null) {
244 // Look backwards as well so we can find the prevCursor.
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200245 var backwardCriteria = queryViewable(entityClass, viewer, owner, cursor, cb, false);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100246 var backwardQuery = session.createQuery(backwardCriteria);
247 backwardQuery.setMaxResults(count);
248 var backwardResults = backwardQuery.getResultList();
249 if (!backwardResults.isEmpty()) {
250 prevCursor = backwardResults.get(backwardResults.size() - 1).id;
251 }
252 }
253
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200254 var forwardResults = (List<T>) forwardQuery.getResultList();
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100255 if (count != null) {
256 if (forwardResults.size() == count + 1) {
257 nextCursor = forwardResults.get(count).id;
258 forwardResults.remove((int) count);
259 }
260 }
261
Matthias Andreas Benkard4940b292020-03-29 18:41:07 +0200262 return new PostPage<T>(prevCursor, cursor, nextCursor, forwardResults);
Matthias Andreas Benkard3d399f32020-03-22 07:23:07 +0100263 }
Matthias Andreas Benkard06e6c812020-04-13 17:01:35 +0200264
265 public enum Visibility {
266 PUBLIC,
267 SEMIPRIVATE,
268 DISCRETIONARY,
269 PRIVATE,
270 }
271
272 @Override
273 public boolean equals(Object o) {
274 if (this == o) {
275 return true;
276 }
277 if (!(o instanceof Post)) {
278 return false;
279 }
280 Post post = (Post) o;
281 return Objects.equals(id, post.id);
282 }
283
284 @Override
285 public int hashCode() {
286 return Objects.hash(id);
287 }
Matthias Andreas Benkard734879e2020-01-24 10:47:37 +0100288}