Namespaces,

finally. Creation of them still TODO
Also brought back account IDs.
This commit is contained in:
Minecon724 2024-09-16 16:03:58 +02:00
parent fa3fdc263b
commit 6197b4ec9a
Signed by: Minecon724
GPG key ID: 3CCC4D267742C8E8
12 changed files with 153 additions and 40 deletions

View file

@ -22,9 +22,6 @@ public class Startup {
@ConfigProperty(name = "talkpages.systemUser.name") @ConfigProperty(name = "talkpages.systemUser.name")
private String username; private String username;
@ConfigProperty(name = "talkpages.createExamplePage")
private boolean createExamplePage;
void installStaticRoute(@Observes StartupEvent startupEvent, Router router) { void installStaticRoute(@Observes StartupEvent startupEvent, Router router) {
router.route() router.route()
.path("/static/*") .path("/static/*")
@ -33,7 +30,7 @@ public class Startup {
@Transactional @Transactional
public void examplePage(@Observes StartupEvent ignoredEvent) { 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 // system user exists so assuming this is not the first run
return; return;
} }
@ -43,15 +40,18 @@ public class Startup {
Account account = new Account(username); Account account = new Account(username);
account.persistAndFlush(); account.persistAndFlush();
if (createExamplePage) { Page talkPagesPage = addPage(account, "TalkPages", "<p>A website where the users collaboratively create content</p><ul><li><a href=\"https://git.m724.eu/Minecon724/talkpages\">Source code</a></li></ul>");
addPage(account, "TalkPages", "<p>A website where the users collaboratively create content</p><ul><li><a href=\"https://git.m724.eu/Minecon724/talkpages\">Source code</a></li></ul>");
addPage(account, "Talkpages", "ambiguous for [TalkPages]"); addPage(account, "Talkpages", "ambiguous for [TalkPages]");
addPage(account, "TP", "@TalkPages"); addPage(account, "TP", "@TalkPages");
}
Page tosPage = addPage(account, "Terms of Service", "TODO");
addPage(account, "ToS", "@TalkPages/Terms of Service");
tosPage.setParentPage(talkPagesPage);
} }
@Transactional @Transactional
public void addPage(Account account, String title, String content) { public Page addPage(Account account, String title, String content) {
Page page = new Page(title); Page page = new Page(title);
PageRevision revision = new PageRevision(page, account, content); PageRevision revision = new PageRevision(page, account, content);
@ -59,6 +59,6 @@ public class Startup {
page.persistAndFlush(); page.persistAndFlush();
account.getRevisions().add(revision); account.getRevisions().add(revision);
return page;
} }
} }

View file

@ -25,7 +25,7 @@ public class AuthService {
*/ */
@Transactional @Transactional
Session register(String username, String password) throws UsernameExistsException { Session register(String username, String password) throws UsernameExistsException {
Account account = Account.findById(username); Account account = Account.findByName(username);
if (account != null) { if (account != null) {
throw new UsernameExistsException(); throw new UsernameExistsException();
@ -63,7 +63,7 @@ public class AuthService {
@Transactional @Transactional
Session authenticate(String username, String password) throws InvalidCredentialsException { Session authenticate(String username, String password) throws InvalidCredentialsException {
Account account = Account.findById(username); Account account = Account.findByName(username);
if (account == null) { if (account == null) {
throw new InvalidCredentialsException(false); throw new InvalidCredentialsException(false);

View file

@ -14,8 +14,13 @@ import java.util.List;
import java.util.Set; import java.util.Set;
// TODO consider moving away authentication to allow like multiple profiles on one account // 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 @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() {} private Account() {}
/** /**
@ -46,7 +51,6 @@ public class Account extends PanacheEntityBase {
// Columns // Columns
@Id
private String name; private String name;
private String slug; private String slug;
@ -72,4 +76,11 @@ public class Account extends PanacheEntityBase {
public List<PageRevision> getRevisions() { return revisions; } public List<PageRevision> getRevisions() { return revisions; }
public boolean isSystemAccount() { return password == null; } public boolean isSystemAccount() { return password == null; }
// Operations
public static Account findByName(String name) {
return Account.find("name", name).firstResult();
}
} }

View file

@ -1,35 +1,29 @@
package eu.m724.talkpages.orm.entity.content; 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.PanacheEntity;
import io.quarkus.hibernate.orm.panache.PanacheEntityBase;
import jakarta.persistence.*; import jakarta.persistence.*;
import org.hibernate.annotations.GenericGenerator;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
@Entity @Entity
@Table( @Table(
indexes = @Index(name = "idx_title", columnList = "title"), 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() {} private Page() {}
public Page(String title) { public Page(String title) {
// TODO maybe map space to _
this.slug = URLEncoder.encode(title, StandardCharsets.UTF_8).replace("+", "%20");
this.title = title; this.title = title;
} }
// Columns // Columns
@Id
private String slug;
// TODO is this really necessary
private String title; private String title;
@OneToOne @OneToOne
@ -38,21 +32,103 @@ public class Page extends PanacheEntityBase {
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true) @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
private List<PageRevision> revisions = new ArrayList<>(); private List<PageRevision> revisions = new ArrayList<>();
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE, CascadeType.REFRESH}, mappedBy = "parentPage")
private List<Page> subpages = new ArrayList<>();
@ManyToOne
@JoinColumn(name = "parentPage_id")
private Page parentPage = null;
// Hooks
@PreRemove
private void preRemove() {
subpages.forEach(page -> page.parentPage = null);
}
// Getters // Getters
public String getSlug() { return slug; }
public String getTitle() { return title; } public String getTitle() { return title; }
public PageRevision getLatestRevision() { return latestRevision; } public PageRevision getLatestRevision() { return latestRevision; }
public List<PageRevision> getRevisions() { return revisions; } public List<PageRevision> getRevisions() { return revisions; }
public List<Page> getSubpages() { return subpages; }
public Page getParentPage() { return parentPage; }
public void setLatestRevision(PageRevision pageRevision) { public void setLatestRevision(PageRevision pageRevision) {
revisions.add(pageRevision); revisions.add(pageRevision);
latestRevision = 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.<br>
* The list is empty if there's no parents.
*
* @return chain of parents
*/
public List<Page> getChain() {
List<Page> parents = new ArrayList<>();
Page parent = this.getParentPage();
while (parent != null) {
parents.add(parent);
parent = parent.getParentPage();
}
return parents.reversed();
}
// Operations // Operations
/**
* Find a {@link Page} by title. The title must not be encoded.<br>
* Separate namespaces with a slash (<code>/</code>)<br>
*
* @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<parts.length; i++) {
String part = parts[i];
// those checks apply to the previous iteration, but won't run on the last one
if (page == null)
break; // TODO maybe raise an error here
if (page.title.isBlank())
throw new IllegalStateException("Can't have empty title in the middle"); // TODO also enforce
page = page.getSubpages().stream()
.filter(p -> p.getTitle().equals(part))
.findFirst().orElse(null); // TODO optimize
}
return page;
}
public static Page findByTitle(String title) { public static Page findByTitle(String title) {
return find("title", title).firstResult(); return find("title", title).firstResult();
} }

View file

@ -9,7 +9,7 @@ import java.time.LocalDateTime;
@Entity @Entity
@Table( @Table(
indexes = @Index(columnList = "page_slug"), indexes = @Index(columnList = "page_id"),
uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"}) uniqueConstraints = @UniqueConstraint(columnNames = {"id", "index"})
) )
public class PageRevision extends PanacheEntity { public class PageRevision extends PanacheEntity {

View file

@ -34,7 +34,7 @@ public class HistoryResource {
} }
@GET @GET
@Path("/{title}") @Path("/{title:.+}")
public Response pageHistory(@PathParam("title") String title) { public Response pageHistory(@PathParam("title") String title) {
Page page = Page.findByTitle(title); Page page = Page.findByTitle(title);

View file

@ -35,16 +35,16 @@ public class PageResource {
} }
@GET @GET
@Path("/{pageId}") @Path("/{title:.+}")
public Response viewPage(@PathParam("pageId") String pageId, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) { public Response viewPage(@PathParam("title") String title, @QueryParam("revision") Integer revisionId, @QueryParam("redirectFrom") String redirectFrom) {
Page page = Page.findByTitle(pageId); Page page = Page.findByPath(title);
if (page == null) { if (page == null) {
List<Page> suggestions = Page.findByTitleIgnoreCase(pageId); List<Page> suggestions = Page.findByTitleIgnoreCase(title);
if (suggestions.size() == 1) { if (suggestions.size() == 1) {
return redirectService.page(suggestions.getFirst()).build(); 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) { if (revisionId == null) {
@ -52,7 +52,7 @@ public class PageResource {
if (revision.getContent().startsWith("@")) { if (revision.getContent().startsWith("@")) {
String target = revision.getContent().substring(1); String target = revision.getContent().substring(1);
if (redirectFrom == null) 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.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(); return Response.ok().entity(Templates.page(page, page.getLatestRevision(), false)).build();

View file

@ -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;
}
}

View file

@ -1,10 +1,9 @@
talkpages.homePage=/ talkpages.homePage=/
talkpages.systemUser.name=System talkpages.systemUser.name=System
talkpages.createExamplePage=true
quarkus.http.auth.basic=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.db-kind=h2
quarkus.datasource.username=username-default quarkus.datasource.username=username-default

View file

View file

@ -1,4 +1,4 @@
{#include layout} {#include layout customTitle=true noHeading=true}
{#header} {#header}
{#if http:param("redirectFrom") != null} {#if http:param("redirectFrom") != null}
<p>Redirected from {http:param("redirectFrom")}</p> <p>Redirected from {http:param("redirectFrom")}</p>
@ -15,9 +15,23 @@
<h4>This is the current revision #{revision.index} authored by {revision.author.name}.</h4> <h4>This is the current revision #{revision.index} authored by {revision.author.name}.</h4>
{/if} {/if}
{/if} {/if}
{/header} {/header}
{#pageTitle}{page.title}{/pageTitle} {#pageTitle}{page.getTitle} - TalkPages{/pageTitle}
{#if page.getChain.isEmpty} <!-- TODO don't repeat that -->
<h1>{page.getTitle}</h1>
{#else}
<h1>
<small>
{#for parent in page.getChain}
<a href="/page/{parent.getSlug}">{parent.getTitle}</a> /
{/for}
</small>
{page.getTitle}
</h1>
{/if}
{revision.content.sanitizeContent.raw} {revision.content.sanitizeContent.raw}

View file

@ -4,6 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- TODO opengraph tags and maybe some nice css. also add errors and stuff here --> <!-- TODO opengraph tags and maybe some nice css. also add errors and stuff here -->
<link rel="stylesheet" href="/static/style.css">
{#if theme:darkTheme} {#if theme:darkTheme}
<link rel="stylesheet" href="/static/dark.css"> <link rel="stylesheet" href="/static/dark.css">
{/if} {/if}
@ -11,7 +12,10 @@
</head> </head>
<body> <body>
{#insert header}{/} {#insert header}{/}
{#if !noHeading??}
<h1>{#insert pageTitle /}</h1> <h1>{#insert pageTitle /}</h1>
{/if}
{#insert}{/} {#insert}{/}
</body> </body>