Book Marx: Implement bookmark submission.

Change-Id: Ieb1fef8565ed0e17de9590d5207ba11ebfe6f177
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/Role.java b/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/Role.java
index 7c0db71..4bfba8e 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/Role.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/accesscontrol/Role.java
@@ -2,7 +2,6 @@
 
 import eu.mulk.mulkcms2.benki.generic.PostTarget;
 import eu.mulk.mulkcms2.benki.users.User;
-import eu.mulk.mulkcms2.benki.users.UserDefaultTarget;
 import eu.mulk.mulkcms2.benki.users.UserRole;
 import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
 import java.util.Collection;
@@ -59,8 +58,8 @@
   @ManyToMany(mappedBy = "effectiveSubroles", fetch = FetchType.LAZY)
   public Set<Role> effectiveSuperroles;
 
-  @OneToMany(mappedBy = "target", fetch = FetchType.LAZY)
-  public Collection<UserDefaultTarget> usersUsedByAsDefaultTarget;
+  @ManyToMany(mappedBy = "defaultTargets", fetch = FetchType.LAZY)
+  public Set<User> usersUsedByAsDefaultTarget;
 
   @OneToMany(mappedBy = "role", fetch = FetchType.LAZY)
   public Collection<UserRole> directUsers;
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
index 45c7087..463551c 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/bookmarks/BookmarkResource.java
@@ -10,15 +10,27 @@
 import io.quarkus.qute.TemplateInstance;
 import io.quarkus.qute.api.ResourcePath;
 import io.quarkus.security.identity.SecurityIdentity;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.time.OffsetDateTime;
 import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
 import java.time.temporal.TemporalAccessor;
 import java.util.List;
+import java.util.Set;
 import javax.inject.Inject;
 import javax.json.spi.JsonProvider;
+import javax.transaction.Transactional;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import javax.validation.constraints.Pattern;
+import javax.ws.rs.BadRequestException;
+import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
+import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
+import javax.ws.rs.core.Response;
 import org.jboss.logging.Logger;
 
 @Path("/bookmarks")
@@ -41,7 +53,7 @@
 
   @GET
   @Produces(TEXT_HTML)
-  public TemplateInstance getPage() {
+  public TemplateInstance getIndex() {
     List<Bookmark> bookmarks;
     if (identity.isAnonymous()) {
       Role world = Role.find("from Role r join r.tags tag where tag = 'world'").singleResult();
@@ -62,7 +74,44 @@
                   user.id)
               .list();
     }
-    return bookmarkList.data("bookmarks", bookmarks);
+    return bookmarkList
+        .data("bookmarks", bookmarks)
+        .data("authenticated", !identity.isAnonymous());
+  }
+
+  @POST
+  @Transactional
+  public Response postBookmark(
+      @FormParam("uri") URI uri,
+      @FormParam("title") @NotEmpty String title,
+      @FormParam("description") String description,
+      @FormParam("visibility") @NotNull @Pattern(regexp = "public|semiprivate|private") String visibility)
+      throws URISyntaxException {
+
+    var userName = identity.getPrincipal().getName();
+    User user =
+        User.find("from BenkiUser u join u.nicknames n where ?1 = n", userName).singleResult();
+
+    var bookmark = new Bookmark();
+    bookmark.uri = uri.toString();
+    bookmark.title = title;
+    bookmark.tags = Set.of();
+    bookmark.description = description != null ? description : "";
+    bookmark.owner = user;
+    bookmark.date = OffsetDateTime.now();
+
+    if (visibility.equals("public")) {
+      Role world = Role.find("from Role r join r.tags tag where tag = 'world'").singleResult();
+      bookmark.targets = Set.of(world);
+    } else if (visibility.equals("semiprivate")) {
+      bookmark.targets = Set.copyOf(user.defaultTargets);
+    } else if (!visibility.equals("private")) {
+      throw new BadRequestException(String.format("invalid visibility “%s”", visibility));
+    }
+
+    bookmark.persistAndFlush();
+
+    return Response.seeOther(new URI("/bookmarks")).build();
   }
 
   @TemplateExtension
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/users/User.java b/src/main/java/eu/mulk/mulkcms2/benki/users/User.java
index 31c13dd..1a0dae6 100644
--- a/src/main/java/eu/mulk/mulkcms2/benki/users/User.java
+++ b/src/main/java/eu/mulk/mulkcms2/benki/users/User.java
@@ -68,8 +68,13 @@
   @OneToMany(mappedBy = "owner", fetch = FetchType.LAZY)
   public Collection<Post> posts;
 
-  @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
-  public Collection<UserDefaultTarget> defaultTargets;
+  @ManyToMany(fetch = FetchType.LAZY)
+  @JoinTable(
+      name = "user_default_target",
+      schema = "benki",
+      joinColumns = @JoinColumn(name = "user"),
+      inverseJoinColumns = @JoinColumn(name = "target"))
+  public Set<Role> defaultTargets;
 
   @ElementCollection(fetch = FetchType.LAZY)
   @CollectionTable(
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/users/UserDefaultTarget.java b/src/main/java/eu/mulk/mulkcms2/benki/users/UserDefaultTarget.java
deleted file mode 100644
index 94efef1..0000000
--- a/src/main/java/eu/mulk/mulkcms2/benki/users/UserDefaultTarget.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package eu.mulk.mulkcms2.benki.users;
-
-import eu.mulk.mulkcms2.benki.accesscontrol.Role;
-import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
-import javax.persistence.Column;
-import javax.persistence.Entity;
-import javax.persistence.FetchType;
-import javax.persistence.Id;
-import javax.persistence.IdClass;
-import javax.persistence.JoinColumn;
-import javax.persistence.ManyToOne;
-import javax.persistence.Table;
-
-@Entity
-@Table(name = "user_default_target", schema = "benki")
-@IdClass(UserDefaultTargetPK.class)
-public class UserDefaultTarget extends PanacheEntityBase {
-
-  @Id
-  @Column(name = "user", nullable = false)
-  public int userId;
-
-  @Id
-  @Column(name = "target", nullable = false)
-  public int targetId;
-
-  @ManyToOne(fetch = FetchType.LAZY)
-  @JoinColumn(name = "user", referencedColumnName = "id", nullable = false)
-  public User user;
-
-  @ManyToOne(fetch = FetchType.LAZY)
-  @JoinColumn(name = "target", referencedColumnName = "id", nullable = false)
-  public Role target;
-}
diff --git a/src/main/java/eu/mulk/mulkcms2/benki/users/UserDefaultTargetPK.java b/src/main/java/eu/mulk/mulkcms2/benki/users/UserDefaultTargetPK.java
deleted file mode 100644
index 4a41e7b..0000000
--- a/src/main/java/eu/mulk/mulkcms2/benki/users/UserDefaultTargetPK.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package eu.mulk.mulkcms2.benki.users;
-
-import java.io.Serializable;
-import javax.persistence.Column;
-import javax.persistence.Id;
-
-public class UserDefaultTargetPK implements Serializable {
-
-  private int userId;
-  private int targetId;
-
-  @Column(name = "user", nullable = false)
-  @Id
-  public int getUserId() {
-    return userId;
-  }
-
-  public void setUserId(int userId) {
-    this.userId = userId;
-  }
-
-  @Column(name = "target", nullable = false)
-  @Id
-  public int getTargetId() {
-    return targetId;
-  }
-
-  public void setTargetId(int targetId) {
-    this.targetId = targetId;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (this == o) {
-      return true;
-    }
-    if (o == null || getClass() != o.getClass()) {
-      return false;
-    }
-
-    UserDefaultTargetPK that = (UserDefaultTargetPK) o;
-
-    if (userId != that.userId) {
-      return false;
-    }
-    if (targetId != that.targetId) {
-      return false;
-    }
-
-    return true;
-  }
-
-  @Override
-  public int hashCode() {
-    int result = userId;
-    result = 31 * result + targetId;
-    return result;
-  }
-}
diff --git a/src/main/resources/META-INF/resources/cms2/base.css b/src/main/resources/META-INF/resources/cms2/base.css
index e04e5d9..735b297 100644
--- a/src/main/resources/META-INF/resources/cms2/base.css
+++ b/src/main/resources/META-INF/resources/cms2/base.css
@@ -159,3 +159,7 @@
   padding: 0.3em;
   background: #f0f8f0;
 }
+
+#bookmark-submission textarea {
+  min-width: calc(100% - 12em);
+}
diff --git a/src/main/resources/templates/benki/bookmarks/bookmarkList.html b/src/main/resources/templates/benki/bookmarks/bookmarkList.html
index 0d392c8..9d4c706 100644
--- a/src/main/resources/templates/benki/bookmarks/bookmarkList.html
+++ b/src/main/resources/templates/benki/bookmarks/bookmarkList.html
@@ -1,4 +1,5 @@
 {@java.util.List<eu.mulk.mulkcms2.benki.bookmarks.Bookmark> bookmarks}
+{@java.lang.Boolean authenticated}
 
 {#include base.html}
 
@@ -10,6 +11,44 @@
 
 {#body}
 
+{#if authenticated}
+  <section id="bookmark-submission">
+    <form class="pure-form pure-form-aligned" method="post">
+      <fieldset>
+        <legend>Submit Bookmark</legend>
+
+        <div class="pure-control-group">
+          <label for="title-input">Title:</label>
+          <input name="title" id="title-input" type="text" placeholder="Title" required/>
+        </div>
+
+        <div class="pure-control-group">
+          <label for="uri-input">URI:</label>
+          <input name="uri" id="uri-input" type="text" placeholder="URI" required/>
+        </div>
+
+        <div class="pure-control-group">
+          <label for="description-input">Description:</label>
+          <textarea name="description" id="description-input" placeholder="Description"></textarea>
+        </div>
+
+        <div class="pure-control-group">
+          <label for="visibility-input">Visibility:</label>
+          <select id="visibility-input" name="visibility" required>
+            <option value="public">Public</option>
+            <option value="semiprivate" selected>Semiprivate</option>
+            <option value="private">Private</option>
+          </select>
+        </div>
+
+        <div class="pure-controls">
+          <button type="submit" class="pure-button pure-button-primary">Submit Bookmark</button>
+        </div>
+      </fieldset>
+    </form>
+  </section>
+{/if}
+
 {#for bookmark in bookmarks}
   {#with bookmark}
     <article class="bookmark">