Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Plan/api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ compileJava {
options.release = 8
}

def apiVersion = "5.8-R0.1"
def apiVersion = "5.9-R0.1"

publishing {
repositories {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <https://www.gnu.org/licenses/>.
*/
package com.djrapitops.plan.delivery.web.resolver;

import com.djrapitops.plan.delivery.web.resolver.request.Request;

import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionException;

/**
* Interface for asynchronously resolving requests of Plan webserver.
*
* @author AuroraLS3
*/
public interface AsyncResolver extends Resolver {

/**
* Implement asynchronous request resolution.
*
* @param request HTTP request, contains all information necessary to resolve the request.
* @return Future of Optional Response or empty if the response should be 404 (not found).
* @see Response for return value
* @see Request#getPath() for path /example/path etc
* @see Request#getQuery() for parameters ?param=value etc
*/
CompletableFuture<Optional<Response>> resolveAsync(Request request);

@Override
default Optional<Response> resolve(Request request) {
try {
return resolveAsync(request).join();
} catch (CompletionException e) {
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause instanceof Error) {
throw (Error) cause;
}
throw e;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.function.Function;
import java.util.function.Predicate;

Expand All @@ -36,7 +37,7 @@
*
* @author AuroraLS3
*/
public final class CompositeResolver implements Resolver {
public final class CompositeResolver implements AsyncResolver {

private final List<String> prefixes;
private final List<Resolver> resolvers;
Expand Down Expand Up @@ -94,9 +95,17 @@ public boolean canAccess(Request request) {
}

@Override
public Optional<Response> resolve(Request request) {
public CompletableFuture<Optional<Response>> resolveAsync(Request request) {
Request forThis = request.omitFirstInPath();
return getResolver(forThis.getPath()).flatMap(resolver -> resolver.resolve(forThis));
Optional<Resolver> found = getResolver(forThis.getPath());
if (!found.isPresent()) {
return CompletableFuture.completedFuture(Optional.empty());
}
Resolver resolver = found.get();
if (resolver instanceof AsyncResolver) {
return ((AsyncResolver) resolver).resolveAsync(forThis);
}
return CompletableFuture.supplyAsync(() -> resolver.resolve(forThis));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

/**
* Represents a response that will be sent over HTTP.
Expand All @@ -32,7 +33,7 @@ public final class Response {

final Map<String, String> headers;
int code = 200;
byte[] bytes;
CompletableFuture<byte[]> bytes;
Charset charset; // can be null (raw bytes)

Response() {
Expand All @@ -44,11 +45,19 @@ public static ResponseBuilder builder() {
}

public byte[] getBytes() {
return bytes;
return bytes != null ? bytes.join() : new byte[0];
}

public CompletableFuture<byte[]> getBytesAsync() {
return bytes != null ? bytes : CompletableFuture.completedFuture(new byte[0]);
}

public String getAsString() {
return new String(bytes, StandardCharsets.UTF_8);
return new String(getBytes(), StandardCharsets.UTF_8);
}

public CompletableFuture<String> getAsStringAsync() {
return getBytesAsync().thenApply(b -> new String(b, charset != null ? charset : StandardCharsets.UTF_8));
}

public int getCode() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;

public class ResponseBuilder {

Expand All @@ -30,17 +31,6 @@ public class ResponseBuilder {
this.response = new Response();
}

/**
* Set MIME Type of the Response.
*
* @param mimeType MIME type of the Response <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types">Documentation</a>
* @return this builder.
* @see MimeType for common MIME types.
*/
public ResponseBuilder setMimeType(String mimeType) {
return setHeader("Content-Type", mimeType);
}

/**
* Set HTTP Status code.
* <p>
Expand Down Expand Up @@ -86,11 +76,17 @@ public ResponseBuilder setContent(WebResource resource) {
}

public ResponseBuilder setContent(byte[] bytes) {
response.bytes = bytes;
return setHeader("Content-Length", bytes.length)
byte[] safeBytes = bytes != null ? bytes : new byte[0];
response.bytes = CompletableFuture.completedFuture(safeBytes);
return setHeader("Content-Length", safeBytes.length)
.setHeader("Accept-Ranges", "bytes"); // Does not compress
}

public ResponseBuilder setContent(CompletableFuture<byte[]> bytesFuture) {
response.bytes = bytesFuture != null ? bytesFuture : CompletableFuture.completedFuture(new byte[0]);
return this;
}

public ResponseBuilder setContent(String utf8String) {
return setContent(utf8String, StandardCharsets.UTF_8);
}
Expand All @@ -112,6 +108,22 @@ public ResponseBuilder setContent(String content, Charset charset) {
.removeHeader("Accept-Ranges"); // Can compress
}

public ResponseBuilder setContent(CompletableFuture<String> stringFuture, Charset charset) {
if (stringFuture == null) return setContent(new byte[0]);
Charset effectiveCharset = charset != null ? charset : StandardCharsets.UTF_8;
String mimeType = getMimeType();
response.charset = effectiveCharset;

if (mimeType != null) {
String[] parts = mimeType.split(";");
if (parts.length == 1) {
setMimeType(parts[0] + "; charset=" + effectiveCharset.name().toLowerCase());
}
}
removeHeader("Accept-Ranges");
return setContent(stringFuture.thenApply(string -> string == null ? null : string.getBytes(effectiveCharset)));
}

/**
* Set content as serialized JSON object.
*
Expand All @@ -120,13 +132,21 @@ public ResponseBuilder setContent(String content, Charset charset) {
*/
public ResponseBuilder setJSONContent(Object objectToSerialize) {
if (objectToSerialize instanceof String) return setJSONContent((String) objectToSerialize);
if (objectToSerialize instanceof CompletableFuture) {
CompletableFuture<?> future = (CompletableFuture<?>) objectToSerialize;
return setJSONContent(future.thenApply(obj -> obj instanceof String ? (String) obj : new Gson().toJson(obj)));
}
return setJSONContent(new Gson().toJson(objectToSerialize));
}

public ResponseBuilder setJSONContent(String json) {
return setMimeType(MimeType.JSON).setContent(json);
}

public ResponseBuilder setJSONContent(CompletableFuture<String> jsonFuture) {
return setMimeType(MimeType.JSON).setContent(jsonFuture, StandardCharsets.UTF_8);
}

/**
* Finish building.
*
Expand All @@ -137,15 +157,16 @@ public ResponseBuilder setJSONContent(String json) {
* @see #setMimeType(String) to set MIME-type.
*/
public Response build() {
byte[] content = response.bytes;
if(content == null && response.code == 204) {
CompletableFuture<byte[]> contentFuture = response.bytes;
if (contentFuture == null && response.code == 204) {
// HTTP Code 204 requires no response, so there is no need to validate it.
return response;
}
exceptionIf(content == null, "Content not defined for Response");
exceptionIf(contentFuture == null, "Content not defined for Response");
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");
boolean hasContent = response.bytes != null && response.bytes.isDone() && response.bytes.join().length > 0;
exceptionIf(hasContent && mimeType == null, "MIME Type not defined for Response");
exceptionIf(hasContent && mimeType.isEmpty(), "MIME Type empty for Response");
exceptionIf(response.code < 100 || response.code >= 600, "HTTP Status code out of bounds (" + response.code + ")");
return response;
}
Expand All @@ -154,6 +175,17 @@ private String getMimeType() {
return response.headers.get("Content-Type");
}

/**
* Set MIME Type of the Response.
*
* @param mimeType MIME type of the Response <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types">Documentation</a>
* @return this builder.
* @see MimeType for common MIME types.
*/
public ResponseBuilder setMimeType(String mimeType) {
return setHeader("Content-Type", mimeType);
}

private void exceptionIf(boolean value, String errorMsg) {
if (value) throw new InvalidResponseException(errorMsg);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import java.util.Map;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;

/**
* Represents a HTTP request to use with {@link Resolver}.
Expand All @@ -34,7 +35,7 @@ public final class Request {
private final URIQuery query;
private final WebUser user;
private final Map<String, String> headers;
private final byte[] requestBody;
private final CompletableFuture<byte[]> requestBody;
private final String accessIpAddress;

/**
Expand Down Expand Up @@ -65,12 +66,27 @@ public Request(String method, URIPath path, URIQuery query, WebUser user, Map<St
* @param accessIpAddress IP address this request is coming from.
*/
public Request(String method, URIPath path, URIQuery query, WebUser user, Map<String, String> headers, byte[] requestBody, String accessIpAddress) {
this(method, path, query, user, headers, CompletableFuture.completedFuture(requestBody != null ? requestBody : new byte[0]), accessIpAddress);
}

/**
* Constructor.
*
* @param method HTTP method, GET, PUT, POST, etc
* @param path Requested path /example/target
* @param query Request parameters ?param=value etc
* @param user Web user doing the request (if authenticated)
* @param headers Request headers <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers">Documentation</a>
* @param requestBody CompletableFuture of raw body bytes
* @param accessIpAddress IP address this request is coming from.
*/
public Request(String method, URIPath path, URIQuery query, WebUser user, Map<String, String> headers, CompletableFuture<byte[]> requestBody, String accessIpAddress) {
this.method = method;
this.path = path;
this.query = query;
this.user = user;
this.headers = headers;
this.requestBody = requestBody;
this.requestBody = requestBody != null ? requestBody : CompletableFuture.completedFuture(new byte[0]);
this.accessIpAddress = accessIpAddress;
}

Expand Down Expand Up @@ -109,7 +125,7 @@ public Request(String method, String target, WebUser user, Map<String, String> h
}
this.user = user;
this.headers = headers;
this.requestBody = new byte[0];
this.requestBody = CompletableFuture.completedFuture(new byte[0]);
this.accessIpAddress = accessIpAddress;
}

Expand Down Expand Up @@ -142,10 +158,20 @@ public URIQuery getQuery() {

/**
* Get the raw body, if present.
* Blocks until the body is available if fetched asynchronously.
*
* @return byte[].
*/
public byte[] getRequestBody() {
return requestBody.join();
}

/**
* Get the raw body as a {@link CompletableFuture}.
*
* @return CompletableFuture of byte[].
*/
public CompletableFuture<byte[]> getRequestBodyAsync() {
return requestBody;
}

Expand Down Expand Up @@ -184,7 +210,7 @@ public String toString() {
", query=" + query +
", user=" + user +
", headers=" + headers +
", body=" + requestBody.length +
", body=" + (requestBody.isDone() && !requestBody.isCompletedExceptionally() ? requestBody.join().length : "async") +
'}';
}
}
6 changes: 3 additions & 3 deletions Plan/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ clean {
allprojects {
ext {
majorVersion = "5"
minorVersion = "8"
minorVersion = "9"
buildVersion = providers.provider {
def command = "git rev-list --count HEAD"
def buildInfo = command.execute().text.trim()
Expand All @@ -37,7 +37,7 @@ allprojects {
}

group = "com.djrapitops"
version = project.hasProperty("isRelease") ? "$fullVersionFilename" : "5.8-SNAPSHOT"
version = project.hasProperty("isRelease") ? "$fullVersionFilename" : "5.9-SNAPSHOT"
}

subprojects {
Expand Down Expand Up @@ -69,7 +69,7 @@ subprojects {
commonsCodecVersion = "1.22.1"
caffeineVersion = "3.2.4"
jetbrainsAnnotationsVersion = "26.1.0"
jettyVersion = "11.0.26"
jettyVersion = "12.1.12"
mysqlVersion = "9.7.0"
mariadbVersion = "3.5.10"
sqliteVersion = "3.42.0.1"
Expand Down
2 changes: 1 addition & 1 deletion Plan/bukkit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ dependencies {
}

compileJava {
options.release = 11
options.release = 17
}

processResources {
Expand Down
Loading
Loading