From 7f0341087eb21ce7f9bde71e4b9a19a9843ec77e Mon Sep 17 00:00:00 2001
From: Rsl1122 <24460436+Rsl1122@users.noreply.github.com>
Date: Tue, 11 Feb 2020 11:24:28 +0200
Subject: [PATCH] Implemented a CompositeResolver
Allows building tree-like structure for resolution
---
.../web/resolver/CompositeResolver.java | 101 ++++++++++++++++++
.../plan/delivery/web/resolver/Resolver.java | 2 +-
.../web/resolver/ResponseBuilder.java | 8 +-
.../plan/delivery/web/resolver/URLTarget.java | 15 +++
4 files changed, 123 insertions(+), 3 deletions(-)
create mode 100644 Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/CompositeResolver.java
diff --git a/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/CompositeResolver.java b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/CompositeResolver.java
new file mode 100644
index 000000000..1c82c69ed
--- /dev/null
+++ b/Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resolver/CompositeResolver.java
@@ -0,0 +1,101 @@
+/*
+ * 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.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+/**
+ * Utility Resolver for organizing resolution in a tree-like structure.
+ *
+ * CompositeResolver removes first part of the target with {@link URLTarget#omitFirst()}
+ * before calling the child Resolvers.
+ *
+ * @author Rsl1122
+ */
+public final class CompositeResolver implements Resolver {
+
+ private final List prefixes;
+ private final List resolvers;
+
+ CompositeResolver() {
+ this.prefixes = new ArrayList<>();
+ this.resolvers = new ArrayList<>();
+ }
+
+ public static CompositeResolver.Builder builder() {
+ return new Builder();
+ }
+
+ private Optional getResolver(URLTarget target) {
+ return target.getPart(0).flatMap(this::find);
+ }
+
+ private Optional find(String prefix) {
+ for (int i = 0; i < prefixes.size(); i++) {
+ if (prefixes.get(i).equals(prefix)) {
+ return Optional.of(resolvers.get(i));
+ }
+ }
+ return Optional.empty();
+ }
+
+ void add(String prefix, Resolver resolver) {
+ if (prefix == null) throw new IllegalArgumentException("Prefix can not be null");
+ if (resolver == null) throw new IllegalArgumentException("Resolver can not be null");
+ prefixes.add(prefix);
+ resolvers.add(resolver);
+ }
+
+ @Override
+ public boolean canAccess(WebUser permissions, URLTarget target, Parameters parameters) {
+ return getResolver(target)
+ .map(resolver -> resolver.canAccess(permissions, target.omitFirst(), parameters))
+ .orElse(true);
+ }
+
+ @Override
+ public Optional resolve(URLTarget target, Parameters parameters) {
+ return getResolver(target)
+ .flatMap(resolver -> resolver.resolve(target.omitFirst(), parameters));
+ }
+
+ public static class Builder {
+ private final CompositeResolver composite;
+
+ private Builder() {
+ this.composite = new CompositeResolver();
+ }
+
+ /**
+ * Add a new resolver to the CompositeResolver.
+ *
+ * @param prefix Start of the target (first part of the target string, eg "example" in "/example/target/", or "" in "/")
+ * @param resolver Resolver to call for this target, {@link URLTarget#omitFirst()} will be called for Resolver method calls.
+ * @return this builder.
+ */
+ public Builder add(String prefix, Resolver resolver) {
+ composite.add(prefix, resolver);
+ return this;
+ }
+
+ public CompositeResolver build() {
+ return composite;
+ }
+ }
+}
\ No newline at end of file
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
index fec439e73..5d452979f 100644
--- 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
@@ -28,7 +28,7 @@ public interface Resolver {
* @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)
+ * @return true if allowed or invalid target, false if response should be 403 (forbidden)
*/
boolean canAccess(WebUser permissions, URLTarget target, Parameters parameters);
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
index d6066b0e9..b557a2c2b 100644
--- 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
@@ -84,7 +84,7 @@ public class ResponseBuilder {
}
public ResponseBuilder setContent(String content, Charset charset) {
- String mimeType = response.headers.get("Content-Type");
+ String mimeType = getMimeType();
response.charset = charset;
if (mimeType != null) {
@@ -118,13 +118,17 @@ public class ResponseBuilder {
public Response build() {
byte[] content = response.bytes;
exceptionIf(content == null, "Content not defined for Response");
- String mimeType = response.getHeaders().get("Content-Type");
+ String mimeType = getMimeType();
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 String getMimeType() {
+ return response.headers.get("Content-Type");
+ }
+
private void exceptionIf(boolean value, String errorMsg) {
if (value) throw new InvalidResponseException(errorMsg);
}
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
index ccaf54bb2..326f3f567 100644
--- 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
@@ -17,6 +17,7 @@
package com.djrapitops.plan.delivery.web.resolver;
import java.util.Arrays;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
@@ -34,6 +35,7 @@ public final class URLTarget {
private List parse(String target) {
String[] partArray = target.split("/");
// Ignores index 0, assuming target starts with /
+ if (partArray.length == 1) return Collections.emptyList();
return Arrays.asList(partArray)
.subList(1, partArray.length);
}
@@ -63,4 +65,17 @@ public final class URLTarget {
public boolean endsWith(String suffix) {
return full.endsWith(suffix);
}
+
+ /**
+ * Immutable modification, removes first part of the target string.
+ *
+ * Example: URLTarget "/example/target" return value of omitFirst URLTarget is "/target"
+ * Example: URLTarget "/example" return value of omitFirst URLTarget is "/"
+ * Example: URLTarget "/" return value of omitFirst URLTarget is ""
+ *
+ * @return new URLTarget with first part removed.
+ */
+ public URLTarget omitFirst() {
+ return new URLTarget(full.replaceFirst("/" + getPart(0).orElse(""), ""));
+ }
}