Skip to content

Commit a92be6c

Browse files
committed
Redesign the HTTPD
1 parent 4fff172 commit a92be6c

26 files changed

Lines changed: 1897 additions & 223 deletions

src/main/java/dev/plex/HTTPDModule.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,11 @@ public void enable()
8888
new CommandsEndpoint();
8989
new SchematicDownloadEndpoint();
9090
new SchematicUploadEndpoint();
91+
new StatsEndpoint();
92+
new PlayersEndpoint();
93+
new AssetsEndpoint();
94+
new PunishmentsUIEndpoint();
95+
new IndefBansUIEndpoint();
9196

9297
ServletHolder uploadHolder = HTTPDModule.context.addServlet(SchematicUploadServlet.class, "/api/schematics/uploading");
9398

src/main/java/dev/plex/request/AbstractServlet.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import java.io.InputStreamReader;
1414
import java.lang.reflect.InvocationTargetException;
1515
import java.lang.reflect.Method;
16+
import java.nio.charset.StandardCharsets;
1617
import java.text.CharacterIterator;
1718
import java.text.StringCharacterIterator;
1819
import java.util.List;
@@ -39,7 +40,9 @@ public AbstractServlet()
3940
}
4041
GET_MAPPINGS.add(mapping);
4142
ServletHolder holder = new ServletHolder(this);
42-
HTTPDModule.context.addServlet(holder, getMapping.endpoint() + "*");
43+
String endpoint = getMapping.endpoint();
44+
String pattern = endpoint.endsWith("/") ? endpoint + "*" : endpoint;
45+
HTTPDModule.context.addServlet(holder, pattern);
4346
}
4447
}
4548
}
@@ -63,15 +66,22 @@ public void doGet(HttpServletRequest req, HttpServletResponse resp) throws Servl
6366
}*/
6467
GET_MAPPINGS.stream().filter(mapping -> endpointMatchesRequest(mapping.getMapping().endpoint(), requestPath)).forEach(mapping ->
6568
{
69+
resp.setCharacterEncoding("UTF-8");
6670
if (mapping.headers != null)
6771
{
6872
for (String headers : mapping.headers.headers())
6973
{
70-
String header = headers.split(";")[0];
71-
String value = headers.split(";")[1];
72-
resp.addHeader(header, value);
74+
String[] parts = headers.split(";", 2);
75+
if (parts.length == 2)
76+
{
77+
resp.addHeader(parts[0], parts[1]);
78+
}
7379
}
7480
}
81+
if (resp.getContentType() == null)
82+
{
83+
resp.setContentType("text/html; charset=UTF-8");
84+
}
7585
resp.setStatus(HttpServletResponse.SC_OK);
7686
try
7787
{
@@ -129,11 +139,10 @@ public static String readFile(InputStream filename)
129139
String page = readFileReal(filename);
130140
String[] info = page.split("\n", 3);
131141
base = base.replace("${TITLE}", info[0]);
132-
base = base.replace("${ACTIVE_" + info[1] + "}", "active\" aria-current=\"page");
142+
base = base.replace("${ACTIVE_" + info[1] + "}", "active");
133143
base = base.replace("${ACTIVE_HOME}", "");
134-
base = base.replace("${ACTIVE_ADMINS}", "");
144+
base = base.replace("${ACTIVE_PLAYERS}", "");
135145
base = base.replace("${ACTIVE_INDEFBANS}", "");
136-
base = base.replace("${ACTIVE_LIST}", "");
137146
base = base.replace("${ACTIVE_COMMANDS}", "");
138147
base = base.replace("${ACTIVE_PUNISHMENTS}", "");
139148
base = base.replace("${ACTIVE_SCHEMATICS}", "");
@@ -146,7 +155,7 @@ public static String readFileReal(InputStream filename)
146155
StringBuilder contentBuilder = new StringBuilder();
147156
try
148157
{
149-
BufferedReader in = new BufferedReader(new InputStreamReader(Objects.requireNonNull(filename)));
158+
BufferedReader in = new BufferedReader(new InputStreamReader(Objects.requireNonNull(filename), StandardCharsets.UTF_8));
150159
String str;
151160
while ((str = in.readLine()) != null)
152161
{
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package dev.plex.request.impl;
2+
3+
import dev.plex.request.AbstractServlet;
4+
import dev.plex.request.GetMapping;
5+
import dev.plex.request.MappingHeaders;
6+
import jakarta.servlet.http.HttpServletRequest;
7+
import jakarta.servlet.http.HttpServletResponse;
8+
9+
import java.io.IOException;
10+
import java.io.InputStream;
11+
import java.io.OutputStream;
12+
13+
public class AssetsEndpoint extends AbstractServlet
14+
{
15+
@GetMapping(endpoint = "/assets/dashboard.js")
16+
@MappingHeaders(headers = {"content-type;application/javascript; charset=utf-8", "cache-control;public, max-age=300"})
17+
public String dashboardJs(HttpServletRequest request, HttpServletResponse response)
18+
{
19+
return readFileReal(this.getClass().getResourceAsStream("/httpd/assets/dashboard.js"));
20+
}
21+
22+
@GetMapping(endpoint = "/assets/plexlogo.webp")
23+
@MappingHeaders(headers = {"content-type;image/webp", "cache-control;public, max-age=86400"})
24+
public String plexLogo(HttpServletRequest request, HttpServletResponse response)
25+
{
26+
serveResource("/httpd/assets/plexlogo.webp", response);
27+
return null;
28+
}
29+
30+
private static void serveResource(String classpathPath, HttpServletResponse response)
31+
{
32+
try (InputStream in = AssetsEndpoint.class.getResourceAsStream(classpathPath);
33+
OutputStream out = response.getOutputStream())
34+
{
35+
if (in == null)
36+
{
37+
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
38+
return;
39+
}
40+
in.transferTo(out);
41+
}
42+
catch (IOException ignored)
43+
{
44+
}
45+
}
46+
}

src/main/java/dev/plex/request/impl/CommandsEndpoint.java

Lines changed: 110 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -6,106 +6,151 @@
66
import dev.plex.request.GetMapping;
77
import jakarta.servlet.http.HttpServletRequest;
88
import jakarta.servlet.http.HttpServletResponse;
9+
910
import java.util.ArrayList;
1011
import java.util.Comparator;
1112
import java.util.List;
1213
import java.util.SortedMap;
1314
import java.util.TreeMap;
15+
1416
import org.bukkit.Bukkit;
1517
import org.bukkit.command.Command;
1618
import org.bukkit.command.CommandMap;
1719
import org.bukkit.command.PluginIdentifiableCommand;
1820

1921
public class CommandsEndpoint extends AbstractServlet
2022
{
21-
22-
private final StringBuilder list = new StringBuilder();
23-
private boolean loadedCommands = false;
23+
private String cachedHtml;
2424

2525
@GetMapping(endpoint = "/api/commands/")
2626
public String getCommands(HttpServletRequest request, HttpServletResponse response)
2727
{
28-
if (!loadedCommands)
28+
if (cachedHtml == null)
2929
{
30-
final SortedMap<String, List<Command>> commandMap = new TreeMap<>();
31-
final CommandMap map = Bukkit.getCommandMap();
32-
for (Command command : map.getKnownCommands().values())
30+
cachedHtml = buildSections();
31+
}
32+
String file = readFile(this.getClass().getResourceAsStream("/httpd/commands.html"));
33+
file = file.replace("${commands}", cachedHtml);
34+
return file;
35+
}
36+
37+
private static String buildSections()
38+
{
39+
final SortedMap<String, List<Command>> commandMap = new TreeMap<>();
40+
final CommandMap map = Bukkit.getCommandMap();
41+
for (Command command : map.getKnownCommands().values())
42+
{
43+
String plugin = "Bukkit";
44+
if (command instanceof PluginIdentifiableCommand pic)
3345
{
34-
String plugin = "Bukkit";
35-
if (command instanceof PluginIdentifiableCommand)
36-
{
37-
plugin = ((PluginIdentifiableCommand) command).getPlugin().getName();
38-
}
39-
40-
List<Command> pluginCommands = commandMap.computeIfAbsent(plugin, k -> new ArrayList<>());
41-
if (!pluginCommands.contains(command))
42-
{
43-
pluginCommands.add(command);
44-
}
46+
plugin = pic.getPlugin().getName();
4547
}
46-
47-
for (String key : commandMap.keySet())
48+
List<Command> pluginCommands = commandMap.computeIfAbsent(plugin, k -> new ArrayList<>());
49+
if (!pluginCommands.contains(command))
4850
{
49-
commandMap.get(key).sort(Comparator.comparing(Command::getName));
50-
StringBuilder rows = new StringBuilder();
51-
for (Command command : commandMap.get(key))
52-
{
53-
String permission = command.getPermission();
54-
if (command instanceof PlexCommand plexCmd)
55-
{
56-
CommandPermissions perms = plexCmd.getClass().getAnnotation(CommandPermissions.class);
57-
if (perms != null)
58-
{
59-
permission = (perms.permission().isBlank() ? "N/A" : perms.permission());
60-
}
61-
}
62-
63-
rows.append(createRow(command.getName(), command.getAliases(), command.getDescription(), command.getUsage(), permission));
64-
}
65-
66-
list.append(createTable(key, rows.toString())).append("\n");
51+
pluginCommands.add(command);
6752
}
53+
}
6854

69-
loadedCommands = true;
55+
StringBuilder sb = new StringBuilder();
56+
for (String key : commandMap.keySet())
57+
{
58+
List<Command> commands = commandMap.get(key);
59+
commands.sort(Comparator.comparing(Command::getName));
60+
sb.append(renderSection(key, commands));
7061
}
62+
return sb.toString();
63+
}
7164

72-
return commandsHTML(list.toString());
65+
private static String renderSection(String plugin, List<Command> commands)
66+
{
67+
StringBuilder cards = new StringBuilder();
68+
for (Command c : commands)
69+
{
70+
cards.append(renderCard(c));
71+
}
72+
String name = escapeHtml(plugin);
73+
return """
74+
<details class="command-section group mt-3 first:mt-0" data-plugin="%s" open>
75+
<summary class="group flex cursor-pointer list-none items-center justify-between gap-3 rounded-2xl px-2 py-3 transition-colors hover:bg-muted/40 [&::-webkit-details-marker]:hidden">
76+
<span class="flex items-center gap-2.5 text-lg font-medium tracking-tight">
77+
<svg class="size-4 text-muted-foreground transition-transform group-open:rotate-90" aria-hidden="true"><use href="#i-arrow-right"/></svg>
78+
%s
79+
</span>
80+
<span class="font-mono text-[11px] uppercase tracking-wider text-muted-foreground">
81+
%d %s
82+
</span>
83+
</summary>
84+
<div class="mt-3 grid gap-3 md:grid-cols-2 xl:grid-cols-3">
85+
%s
86+
</div>
87+
</details>
88+
""".formatted(name, name, commands.size(), commands.size() == 1 ? "command" : "commands", cards);
7389
}
7490

75-
private String commandsHTML(String commandsList)
91+
private static String renderCard(Command c)
7692
{
77-
String file = readFile(this.getClass().getResourceAsStream("/httpd/commands.html"));
78-
file = file.replace("${commands}", commandsList);
79-
return file;
93+
String name = escapeHtml(c.getName());
94+
String aliases = c.getAliases() == null || c.getAliases().isEmpty() ? "" : String.join(", ", c.getAliases());
95+
String description = c.getDescription() == null || c.getDescription().isBlank() ? "" : escapeHtml(c.getDescription());
96+
String usage = cleanUsage(c.getUsage());
97+
String permission = resolvePermission(c);
98+
99+
String aliasMarkup = aliases.isEmpty()
100+
? ""
101+
: "<span class=\"font-mono text-xs text-muted-foreground\">/ " + escapeHtml(aliases) + "</span>";
102+
103+
String descMarkup = description.isEmpty()
104+
? "<p class=\"mt-2 text-sm text-muted-foreground/70 italic\">No description provided.</p>"
105+
: "<p class=\"mt-2 text-sm text-muted-foreground\">" + description + "</p>";
106+
107+
String searchBlob = (name + " " + aliases + " " + description + " " + permission).toLowerCase();
108+
109+
return """
110+
<article class="ring-card group flex flex-col rounded-2xl bg-card p-4 transition-colors hover:bg-secondary/50" data-search="%s">
111+
<header class="flex flex-wrap items-baseline gap-2">
112+
<code class="rounded-md bg-muted px-2 py-0.5 font-mono text-sm font-medium text-foreground">/%s</code>
113+
%s
114+
</header>
115+
%s
116+
<dl class="mt-3 grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1.5 border-t border-border/60 pt-3 font-mono text-[11px]">
117+
<dt class="text-muted-foreground uppercase tracking-wider">usage</dt>
118+
<dd class="text-foreground/80 break-all">%s</dd>
119+
<dt class="text-muted-foreground uppercase tracking-wider">perm</dt>
120+
<dd class="text-foreground/80 break-all">%s</dd>
121+
</dl>
122+
</article>
123+
""".formatted(searchBlob, name, aliasMarkup, descMarkup, usage, permission);
80124
}
81125

82-
private String createTable(String pluginName, String commandRows)
126+
private static String resolvePermission(Command c)
83127
{
84-
return "<details id=\"" + pluginName + "\"><summary>" + pluginName + "</summary>\n"
85-
+ "<table id=\"" + pluginName + "Table\" class=\"table table-striped table-bordered\">\n"
86-
+ " <thead>\n <tr>\n <th scope=\"col\">Name (Aliases)</th>\n "
87-
+ "<th scope=\"col\">Description</th>\n "
88-
+ "<th scope=\"col\">Usage</th>\n "
89-
+ "<th scope=\"col\">Permission</th>\n </tr>\n</thead>\n"
90-
+ "<tbody>\n " + commandRows + "\n</tbody>\n</table>\n</details>";
128+
String permission = c.getPermission();
129+
if (c instanceof PlexCommand plexCmd)
130+
{
131+
CommandPermissions perms = plexCmd.getClass().getAnnotation(CommandPermissions.class);
132+
if (perms != null)
133+
{
134+
permission = perms.permission().isBlank() ? "N/A" : perms.permission();
135+
}
136+
}
137+
if (permission == null || permission.isBlank()) return "N/A";
138+
return escapeHtml(permission).replace(";", "<br>");
91139
}
92140

93-
private String createRow(String name, List<String> aliases, String description, String usage, String permission)
141+
private static String cleanUsage(String usage)
94142
{
95-
return " <tr>\n <th scope=\"row\">" + name
96-
+ (aliases.isEmpty() || aliases.toString().equals("[]") ? "" : " (" + String.join(", ", aliases) + ")") + "</th>\n"
97-
+ " <th scope=\"row\">" + description + "</th>\n"
98-
+ " <th scope=\"row\"><code>" + cleanUsage(usage) + "</code></th>\n"
99-
+ " <th scope=\"row\">" + (permission != null ? permission.replaceAll(";", "<br>") : "N/A") + "</th>\n </tr>";
143+
if (usage == null || usage.isBlank()) return "Not provided";
144+
String escaped = escapeHtml(usage);
145+
return escaped.startsWith("/") ? escaped : "/" + escaped;
100146
}
101147

102-
private String cleanUsage(String usage)
148+
private static String escapeHtml(String s)
103149
{
104-
usage = usage.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
105-
if (usage.isBlank())
106-
{
107-
usage = "Not Provided";
108-
}
109-
return usage.startsWith("/") || usage.equals("Not Provided") ? usage : "/" + usage;
150+
if (s == null) return "";
151+
return s.replace("&", "&amp;")
152+
.replace("<", "&lt;")
153+
.replace(">", "&gt;")
154+
.replace("\"", "&quot;");
110155
}
111156
}

0 commit comments

Comments
 (0)