diff --git a/src/main/java/eu/m724/talkpages/Startup.java b/src/main/java/eu/m724/talkpages/Startup.java index ca0cf0c..50d7020 100644 --- a/src/main/java/eu/m724/talkpages/Startup.java +++ b/src/main/java/eu/m724/talkpages/Startup.java @@ -22,9 +22,6 @@ public class Startup { @ConfigProperty(name = "talkpages.systemUser.name") private String username; - @ConfigProperty(name = "talkpages.createExamplePage") - private boolean createExamplePage; - void installStaticRoute(@Observes StartupEvent startupEvent, Router router) { router.route() .path("/static/*") @@ -33,7 +30,7 @@ public class Startup { @Transactional public void examplePage(@Observes StartupEvent ignoredEvent) { - if (Account.findById(1) != null) { + if (Account.findByName(username) != null) { // system user exists so assuming this is not the first run return; } @@ -43,15 +40,18 @@ public class Startup { Account account = new Account(username); account.persistAndFlush(); - if (createExamplePage) { - addPage(account, "TalkPages", "

A website where the users collaboratively create content

"); - addPage(account, "Talkpages", "ambiguous for [TalkPages]"); - addPage(account, "TP", "@TalkPages"); - } + Page talkPagesPage = addPage(account, "TalkPages", "

A website where the users collaboratively create content

"); + addPage(account, "Talkpages", "ambiguous for [TalkPages]"); + addPage(account, "TP", "@TalkPages"); + + Page tosPage = addPage(account, "Terms of Service", "TODO"); + addPage(account, "ToS", "@TalkPages/Terms of Service"); + + tosPage.setParentPage(talkPagesPage); } @Transactional - public void addPage(Account account, String title, String content) { + public Page addPage(Account account, String title, String content) { Page page = new Page(title); PageRevision revision = new PageRevision(page, account, content); @@ -59,6 +59,6 @@ public class Startup { page.persistAndFlush(); account.getRevisions().add(revision); - + return page; } } diff --git a/src/main/java/eu/m724/talkpages/auth/AuthService.java b/src/main/java/eu/m724/talkpages/auth/AuthService.java index 7c3548b..90ffe12 100644 --- a/src/main/java/eu/m724/talkpages/auth/AuthService.java +++ b/src/main/java/eu/m724/talkpages/auth/AuthService.java @@ -25,7 +25,7 @@ public class AuthService { */ @Transactional Session register(String username, String password) throws UsernameExistsException { - Account account = Account.findById(username); + Account account = Account.findByName(username); if (account != null) { throw new UsernameExistsException(); @@ -63,7 +63,7 @@ public class AuthService { @Transactional Session authenticate(String username, String password) throws InvalidCredentialsException { - Account account = Account.findById(username); + Account account = Account.findByName(username); if (account == null) { throw new InvalidCredentialsException(false); diff --git a/src/main/java/eu/m724/talkpages/orm/entity/auth/Account.java b/src/main/java/eu/m724/talkpages/orm/entity/auth/Account.java index 8ed040d..fc1ee22 100644 --- a/src/main/java/eu/m724/talkpages/orm/entity/auth/Account.java +++ b/src/main/java/eu/m724/talkpages/orm/entity/auth/Account.java @@ -14,8 +14,13 @@ import java.util.List; import java.util.Set; // TODO consider moving away authentication to allow like multiple profiles on one account +// TODO also consider giving different, namespaced ids to system and addressed accounts @Entity -public class Account extends PanacheEntityBase { +@Table( + indexes = @Index(name = "idx_name", columnList = "name"), + uniqueConstraints = @UniqueConstraint(columnNames = "name") +) +public class Account extends PanacheEntity { private Account() {} /** @@ -46,7 +51,6 @@ public class Account extends PanacheEntityBase { // Columns - @Id private String name; private String slug; @@ -72,4 +76,11 @@ public class Account extends PanacheEntityBase { public List getRevisions() { return revisions; } public boolean isSystemAccount() { return password == null; } + + + // Operations + + public static Account findByName(String name) { + return Account.find("name", name).firstResult(); + } } diff --git a/src/main/java/eu/m724/talkpages/orm/entity/content/Page.java b/src/main/java/eu/m724/talkpages/orm/entity/content/Page.java index 487cdef..3910686 100644 --- a/src/main/java/eu/m724/talkpages/orm/entity/content/Page.java +++ b/src/main/java/eu/m724/talkpages/orm/entity/content/Page.java @@ -1,35 +1,29 @@ package eu.m724.talkpages.orm.entity.content; +import eu.m724.talkpages.page.action.NoSuchNamespaceException; import io.quarkus.hibernate.orm.panache.PanacheEntity; -import io.quarkus.hibernate.orm.panache.PanacheEntityBase; import jakarta.persistence.*; -import org.hibernate.annotations.GenericGenerator; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; @Entity @Table( indexes = @Index(name = "idx_title", columnList = "title"), - uniqueConstraints = @UniqueConstraint(columnNames = "title") + uniqueConstraints = @UniqueConstraint(columnNames = {"title", "parentPage"}) ) -public class Page extends PanacheEntityBase { +public class Page extends PanacheEntity { private Page() {} public Page(String title) { - // TODO maybe map space to _ - this.slug = URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20"); this.title = title; } // Columns - @Id - private String slug; - - // TODO is this really necessary private String title; @OneToOne @@ -38,21 +32,103 @@ public class Page extends PanacheEntityBase { @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) private List revisions = new ArrayList<>(); + @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}, mappedBy = "parentPage") + private List subpages = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "parentPage_id") + private Page parentPage = null; + + + // Hooks + + @PreRemove + private void preRemove() { + subpages.forEach(page -> page.parentPage = null); + } + // Getters - public String getSlug() { return slug; } public String getTitle() { return title; } public PageRevision getLatestRevision() { return latestRevision; } public List getRevisions() { return revisions; } + public List getSubpages() { return subpages; } + public Page getParentPage() { return parentPage; } public void setLatestRevision(PageRevision pageRevision) { revisions.add(pageRevision); latestRevision = pageRevision; } + public void setParentPage(Page parentPage) { + // Remove from current parent, if any + if (this.parentPage != null) + parentPage.subpages.remove(this); + + // Set the parent page and add as a child + this.parentPage = parentPage; + if (parentPage != null) + parentPage.getSubpages().add(this); + } + + public String getSlug() { + // TODO maybe map space to _ + return URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20"); + } + + /** + * Chain of parents. The broadest (root) one is the first one.
+ * The list is empty if there's no parents. + * + * @return chain of parents + */ + public List getChain() { + List parents = new ArrayList<>(); + + Page parent = this.getParentPage(); + while (parent != null) { + parents.add(parent); + parent = parent.getParentPage(); + } + + return parents.reversed(); + } + // Operations + /** + * Find a {@link Page} by title. The title must not be encoded.
+ * Separate namespaces with a slash (/)
+ * + * @param title the title, not encoded + * @return page if found, else null + * @throws IllegalStateException if + */ + public static Page findByPath(String title) { + String[] parts = title.split("/", -1); + + Page page = findByTitle(parts[0]); + + for (int i=1; i p.getTitle().equals(part)) + .findFirst().orElse(null); // TODO optimize + } + + return page; + } + public static Page findByTitle(String title) { return find("title", title).firstResult(); } diff --git a/src/main/java/eu/m724/talkpages/orm/entity/content/PageRevision.java b/src/main/java/eu/m724/talkpages/orm/entity/content/PageRevision.java index 44fc68a..37f8ebd 100644 --- a/src/main/java/eu/m724/talkpages/orm/entity/content/PageRevision.java +++ b/src/main/java/eu/m724/talkpages/orm/entity/content/PageRevision.java @@ -9,7 +9,7 @@ import java.time.LocalDateTime; @Entity @Table( - indexes = @Index(columnList = "page_slug"), + indexes = @Index(columnList = "page_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"}) ) public class PageRevision extends PanacheEntity { @@ -49,7 +49,7 @@ public class PageRevision extends PanacheEntity { private Account author; // TODO wondering about a table only for content and meta and title perhaps - @Column(columnDefinition="text") + @Column(columnDefinition = "text") private String content; private int delta; diff --git a/src/main/java/eu/m724/talkpages/page/HistoryResource.java b/src/main/java/eu/m724/talkpages/page/HistoryResource.java index 7149d46..5379384 100644 --- a/src/main/java/eu/m724/talkpages/page/HistoryResource.java +++ b/src/main/java/eu/m724/talkpages/page/HistoryResource.java @@ -34,7 +34,7 @@ public class HistoryResource { } @GET - @Path("/{title}") + @Path("/{title:.+}") public Response pageHistory(@PathParam("title") String title) { Page page = Page.findByTitle(title); diff --git a/src/main/java/eu/m724/talkpages/page/PageResource.java b/src/main/java/eu/m724/talkpages/page/PageResource.java index 16c6da9..43067b8 100644 --- a/src/main/java/eu/m724/talkpages/page/PageResource.java +++ b/src/main/java/eu/m724/talkpages/page/PageResource.java @@ -35,16 +35,16 @@ public class PageResource { } @GET - @Path("/{pageId}") - public Response viewPage(@PathParam("pageId") String pageId, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) { - Page page = Page.findByTitle(pageId); + @Path("/{title:.+}") + public Response viewPage(@PathParam("title") String title, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) { + Page page = Page.findByPath(title); if (page == null) { - List suggestions = Page.findByTitleIgnoreCase(pageId); + List suggestions = Page.findByTitleIgnoreCase(title); if (suggestions.size() == 1) { return redirectService.page(suggestions.getFirst()).build(); } - return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(pageId, suggestions)).build(); + return Response.status(Response.Status.NOT_FOUND).entity(Templates.notFound(title, suggestions)).build(); } if (revisionId == null) { @@ -52,7 +52,7 @@ public class PageResource { if (revision.getContent().startsWith("@")) { String target = revision.getContent().substring(1); if (redirectFrom == null) - redirectFrom = pageId; + redirectFrom = title; return Response.temporaryRedirect(URI.create("/page/" + redirectService.titleEncoded(target) + "?redirectFrom=" + URLEncoder.encode(redirectFrom, StandardCharsets.UTF_8))).build(); } return Response.ok().entity(Templates.page(page, page.getLatestRevision(), false)).build(); diff --git a/src/main/java/eu/m724/talkpages/page/action/NoSuchNamespaceException.java b/src/main/java/eu/m724/talkpages/page/action/NoSuchNamespaceException.java new file mode 100644 index 0000000..537b4c3 --- /dev/null +++ b/src/main/java/eu/m724/talkpages/page/action/NoSuchNamespaceException.java @@ -0,0 +1,9 @@ +package eu.m724.talkpages.page.action; + +public class NoSuchNamespaceException extends RuntimeException { + public final String namespace; + + public NoSuchNamespaceException(String namespace) { + this.namespace = namespace; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index d5d713d..d0e7e4d 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,10 +1,9 @@ talkpages.homePage=/ talkpages.systemUser.name=System -talkpages.createExamplePage=true quarkus.http.auth.basic=true -quarkus.hibernate-orm.database.generation=drop-and-create +quarkus.hibernate-orm.database.generation=update quarkus.datasource.db-kind=h2 quarkus.datasource.username=username-default diff --git a/src/main/resources/static/style.css b/src/main/resources/static/style.css new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/templates/PageResource/page.html b/src/main/resources/templates/PageResource/page.html index cf3ca9b..a43ed37 100644 --- a/src/main/resources/templates/PageResource/page.html +++ b/src/main/resources/templates/PageResource/page.html @@ -1,4 +1,4 @@ -{#include layout} +{#include layout customTitle=true noHeading=true} {#header} {#if http:param("redirectFrom") != null}

Redirected from {http:param("redirectFrom")}

@@ -15,9 +15,23 @@

This is the current revision #{revision.index} authored by {revision.author.name}.

{/if} {/if} + {/header} - {#pageTitle}{page.title}{/pageTitle} + {#pageTitle}{page.getTitle} - TalkPages{/pageTitle} + + {#if page.getChain.isEmpty} +

{page.getTitle}

+ {#else} +

+ + {#for parent in page.getChain} + {parent.getTitle} / + {/for} + + {page.getTitle} +

+ {/if} {revision.content.sanitizeContent.raw} diff --git a/src/main/resources/templates/layout.html b/src/main/resources/templates/layout.html index 0b6038d..5135d35 100644 --- a/src/main/resources/templates/layout.html +++ b/src/main/resources/templates/layout.html @@ -4,6 +4,7 @@ + {#if theme:darkTheme} {/if} @@ -11,7 +12,10 @@ {#insert header}{/} -

{#insert pageTitle /}

+ + {#if !noHeading??} +

{#insert pageTitle /}

+ {/if} {#insert}{/}