diff --git a/Plan/api/build.gradle b/Plan/api/build.gradle
index 786fe4d12..6d30c6656 100644
--- a/Plan/api/build.gradle
+++ b/Plan/api/build.gradle
@@ -4,6 +4,7 @@ plugins {
dependencies {
compileOnly group: 'org.apache.commons', name: 'commons-lang3', version: '3.9'
+ compileOnly "com.google.code.gson:gson:$gsonVersion"
}
ext.apiVersion = '5.0-R0.3'
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/ResolverService.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/ResolverService.java
new file mode 100644
index 000000000..71de1ea1f
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/ResolverService.java
@@ -0,0 +1,71 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web;
+
+import com.djrapitops.plan.delivery.web.resolver.Resolver;
+
+import java.util.Optional;
+import java.util.regex.Pattern;
+
+/**
+ * Service for modifying webserver request resolution.
+ *
+ * It is recommended to use plugin based namespace in your custom targets,
+ * eg. "/flyplugin/flying" to avoid collisions with other plugins.
+ * You can also use {@link #getResolver(String)} to check if target is already resolved.
+ *
+ * @author Rsl1122
+ */
+public interface ResolverService {
+
+ /**
+ * Register a new resolver.
+ *
+ * @param pluginName Name of the plugin that is registering (For error messages)
+ * @param start Start of the target to match against, eg "/example" will send "/example/target" etc to the Resolver.
+ * @param resolver {@link Resolver} to use for this
+ * @throws IllegalArgumentException If pluginName is null or empty.
+ */
+ void registerResolver(String pluginName, String start, Resolver resolver);
+
+ /**
+ * Register a new resolver with regex that maches start of target.
+ *
+ * NOTICE: It is recommended to avoid too generic regex like "/.*" to not override existing resolvers.
+ *
+ * Parameters (?param=value) are not included in the regex matching.
+ *
+ * @param pluginName Name of the plugin that is registering (For error messages)
+ * @param pattern Regex Pattern, "/example.*" will send "/exampletarget" etc to the Resolver.
+ * @param resolver {@link Resolver} to use for this.
+ * @throws IllegalArgumentException If pluginName is null or empty.
+ */
+ void registerResolverForMatches(String pluginName, Pattern pattern, Resolver resolver);
+
+ /**
+ * Obtain a {@link Resolver} for a target.
+ *
+ * First matching resolver will be returned.
+ * {@link #registerResolver} resolvers have higher priority than {@link #registerResolverForMatches}.
+ *
+ * Can be used when making Resolver middleware.
+ *
+ * @param target "/example/target"
+ * @return Resolver if registered or empty.
+ */
+ Optional getResolver(String target);
+}
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/MimeType.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/MimeType.java
new file mode 100644
index 000000000..d31d1725d
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/MimeType.java
@@ -0,0 +1,35 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web.resolver;
+
+public final class MimeType {
+ public static final String HTML = "text/html";
+ public static final String CSS = "text/css";
+ public static final String JSON = "application/json";
+ public static final String JS = "application/javascript";
+ public static final String IMAGE = "image/gif";
+ public static final String FAVICON = "image/x-icon";
+ public static final String FONT_TFF = "application/x-font-ttf";
+ public static final String FONT_WOFF = "application/font-woff";
+ public static final String FONT_WOFF2 = "application/font-woff2";
+ public static final String FONT_EOT = "application/vnd.ms-fontobject";
+ public static final String FONT_BYTESTREAM = "application/octet-stream";
+
+ private MimeType() {
+ // Static variable class
+ }
+}
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/Parameters.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/Parameters.java
new file mode 100644
index 000000000..759ada8ee
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/Parameters.java
@@ -0,0 +1,44 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web.resolver;
+
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Represents URI parameters described with {@code ?param=value¶m2=value2} in the URL.
+ *
+ * @author Rsl1122
+ */
+public final class Parameters {
+
+ private final Map byKey;
+
+ public Parameters(Map byKey) {
+ this.byKey = byKey;
+ }
+
+ /**
+ * Obtain an URI parameter by key.
+ *
+ * @param key Case-sensitive key, eg. 'param' in {@code ?param=value¶m2=value2}
+ * @return The value in the URL or empty if key is not specified in the URL.
+ */
+ public Optional get(String key) {
+ return Optional.ofNullable(byKey.get(key));
+ }
+}
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/Resolver.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/Resolver.java
new file mode 100644
index 000000000..fec439e73
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/Resolver.java
@@ -0,0 +1,49 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web.resolver;
+
+import java.util.Optional;
+
+public interface Resolver {
+
+ /**
+ * Implement access control if authorization is enabled.
+ *
+ * Is not called when access control is not active.
+ *
+ * @param permissions WebUser that is accessing this page.
+ * @param target Target that is being accessed, /example/target
+ * @param parameters Parameters in the URL, ?param=value etc.
+ * @return true if allowed, false if response should be 403 (forbidden)
+ */
+ boolean canAccess(WebUser permissions, URLTarget target, Parameters parameters);
+
+ /**
+ * Implement request resolution.
+ *
+ * @param target Target that is being accessed, /example/target
+ * @param parameters Parameters in the URL, ?param=value etc.
+ * @return Response or empty if the response should be 404 (not found).
+ * @see Response for return value
+ */
+ Optional resolve(URLTarget target, Parameters parameters);
+
+ default ResponseBuilder newResponseBuilder() {
+ return new ResponseBuilder();
+ }
+
+}
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/Response.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/Response.java
new file mode 100644
index 000000000..d81e3f9c3
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/Response.java
@@ -0,0 +1,57 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web.resolver;
+
+import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+/**
+ * Represents a response that will be sent over HTTP.
+ *
+ * @author Rsl1122
+ * @see MimeType for MIME types that are commonly used.
+ */
+public final class Response {
+
+ final Map headers;
+ int code = 200;
+ byte[] bytes;
+ Charset charset; // can be null (raw bytes)
+
+ Response() {
+ headers = new HashMap<>();
+ }
+
+ public byte[] getBytes() {
+ return bytes;
+ }
+
+ public int getCode() {
+ return code;
+ }
+
+ public Map getHeaders() {
+ return headers;
+ }
+
+ public Optional getCharset() {
+ return Optional.ofNullable(charset);
+ }
+
+}
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/ResponseBuilder.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/ResponseBuilder.java
new file mode 100644
index 000000000..d6066b0e9
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/ResponseBuilder.java
@@ -0,0 +1,142 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web.resolver;
+
+import com.google.gson.Gson;
+
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
+
+public class ResponseBuilder {
+
+ private final Response response;
+
+ ResponseBuilder() {
+ this.response = new Response();
+ }
+
+ /**
+ * Set MIME Type of the Response.
+ *
+ * @param mimeType MIME type of the Response https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types
+ * @return this builder.
+ * @see MimeType for common MIME types.
+ */
+ public ResponseBuilder setMimeType(String mimeType) {
+ return setHeader("Content-Type", mimeType);
+ }
+
+ /**
+ * Set HTTP Status code.
+ *
+ * Default status code is 200 (OK) if not set.
+ *
+ * @param code 1xx, 2xx, 3xx, 4xx, 5xx, https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
+ * @return this builder.
+ */
+ public ResponseBuilder setStatus(int code) {
+ return this;
+ }
+
+ /**
+ * Set HTTP Response header.
+ *
+ * @param header Key of the header. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers
+ * @param value Value for the header.
+ * @return this builder.
+ */
+ public ResponseBuilder setHeader(String header, Object value) {
+ response.headers.put(header, value.toString());
+ return this;
+ }
+
+ /**
+ * Utility method for building redirects.
+ *
+ * @param url URL to redirect the client to with 302 Found.
+ * @return https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location
+ */
+ public ResponseBuilder redirectTo(String url) {
+ return setStatus(302).setHeader("Location", url).setContent(new byte[]{});
+ }
+
+ public ResponseBuilder setContent(byte[] bytes) {
+ response.bytes = bytes;
+ return setHeader("Content-Length", bytes.length);
+ }
+
+ public ResponseBuilder setContent(String utf8String) {
+ return setContent(utf8String, StandardCharsets.UTF_8);
+ }
+
+ public ResponseBuilder setContent(String content, Charset charset) {
+ String mimeType = response.headers.get("Content-Type");
+ response.charset = charset;
+
+ if (mimeType != null) {
+ String[] parts = mimeType.split(";");
+ if (parts.length == 1) {
+ setMimeType(parts[0] + "; charset=" + charset.name().toLowerCase());
+ }
+ }
+
+ return setContent(content.getBytes(charset));
+ }
+
+ public ResponseBuilder setJSONContent(Object objectToSerialize) {
+ return setJSONContent(new Gson().toJson(objectToSerialize));
+ }
+
+ public ResponseBuilder setJSONContent(String json) {
+ setContent(json);
+ return setMimeType(MimeType.JSON);
+ }
+
+ /**
+ * Finish building.
+ *
+ * @return Response.
+ * @throws InvalidResponseException if content was not defined (not even empty byte array).
+ * @throws InvalidResponseException if content has bytes, but MIME-type is not defined.
+ * @throws InvalidResponseException if status code is outside range 100-599.
+ * @see #setMimeType(String) to set MIME-type.
+ */
+ public Response build() {
+ byte[] content = response.bytes;
+ exceptionIf(content == null, "Content not defined for Response");
+ String mimeType = response.getHeaders().get("Content-Type");
+ exceptionIf(content.length > 0 && mimeType == null, "MIME Type not defined for Response");
+ exceptionIf(content.length > 0 && mimeType.isEmpty(), "MIME Type empty for Response");
+ exceptionIf(response.code < 100 || response.code >= 600, "HTTP Status code out of bounds (" + response.code + ")");
+ return response;
+ }
+
+ private void exceptionIf(boolean value, String errorMsg) {
+ if (value) throw new InvalidResponseException(errorMsg);
+ }
+
+ /**
+ * Thrown when {@link ResponseBuilder} is missing some parameters.
+ *
+ * @author Rsl1122
+ */
+ public static class InvalidResponseException extends IllegalStateException {
+ public InvalidResponseException(String s) {
+ super(s);
+ }
+ }
+}
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/URLTarget.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/URLTarget.java
new file mode 100644
index 000000000..ccaf54bb2
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/URLTarget.java
@@ -0,0 +1,66 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web.resolver;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+public final class URLTarget {
+
+ // Internal representation
+ private final List parts;
+ private final String full;
+
+ public URLTarget(String target) {
+ this.parts = parse(target);
+ full = target;
+ }
+
+ private List parse(String target) {
+ String[] partArray = target.split("/");
+ // Ignores index 0, assuming target starts with /
+ return Arrays.asList(partArray)
+ .subList(1, partArray.length);
+ }
+
+ /**
+ * Obtain the full target.
+ *
+ * @return Example: "/target/path/in/url"
+ */
+ public String asString() {
+ return full;
+ }
+
+ /**
+ * Obtain part of the target by index of slashes in the URL.
+ *
+ * @param index Index from root, eg. /0/1/2/3 etc
+ * @return part after a '/' in the target, Example "target" for 0 in "/target/example", "" for 0 in "/", "" for 1 in "/example/"
+ */
+ public Optional getPart(int index) {
+ if (index >= parts.size()) {
+ return Optional.empty();
+ }
+ return Optional.of(parts.get(index));
+ }
+
+ public boolean endsWith(String suffix) {
+ return full.endsWith(suffix);
+ }
+}
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/WebUser.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/WebUser.java
new file mode 100644
index 000000000..90ef2efff
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/WebUser.java
@@ -0,0 +1,45 @@
+/*
+ * This file is part of Player Analytics (Plan).
+ *
+ * Plan is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License v3 as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Plan is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Plan. If not, see .
+ */
+package com.djrapitops.plan.delivery.web.resolver;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+public final class WebUser {
+
+ private final String name;
+ private final Set permissions;
+
+ public WebUser(String name) {
+ this.name = name;
+ this.permissions = new HashSet<>();
+ }
+
+ public WebUser(String name, String... permissions) {
+ this(name);
+ this.permissions.addAll(Arrays.asList(permissions));
+ }
+
+ public boolean hasPermission(String permission) {
+ return permissions.contains(permission);
+ }
+
+ public String getName() {
+ return name;
+ }
+}