Implement http caching (#2840)

* Implement first response parts of http caching
* Implement cached response for static resources
* Implement HTTP caching for json responses
* Fix last seen value for online players
* Implement http caching for pages (.html)
* Use placeholder cache even with async requests.

Affects issues:
- Close #2813
This commit is contained in:
Aurora Lahtela 2023-01-22 10:18:14 +02:00 committed by GitHub
parent 0ddda27384
commit 88b4191f6b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 700 additions and 242 deletions

View File

@ -16,11 +16,10 @@
*/
package com.djrapitops.plan.delivery.web.resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.Optional;
import java.util.function.Supplier;
/**
* Represents a customizable resource.
@ -61,6 +60,18 @@ public interface WebResource {
* @throws IOException If the stream can not be read.
*/
static WebResource create(InputStream in) throws IOException {
return create(in, null);
}
/**
* Creates a new WebResource from an InputStream.
*
* @param in InputStream for the resource, closed after inside the method.
* @param lastModified Epoch millisecond the resource was last modified
* @return WebResource.
* @throws IOException If the stream can not be read.
*/
static WebResource create(InputStream in, Long lastModified) throws IOException {
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
int read;
byte[] bytes = new byte[1024];
@ -68,12 +79,36 @@ public interface WebResource {
out.write(bytes, 0, read);
}
return new ByteResource(out.toByteArray());
return new ByteResource(out.toByteArray(), lastModified);
} finally {
in.close();
}
}
/**
* Create a lazy WebResource that only reads contents if necessary.
*
* @param in Supplier for InputStream, a lazy method that reads input when necessary.
* @param lastModified Last modified date for the resource.
* @return WebResource.
*/
static WebResource create(Supplier<InputStream> in, Long lastModified) {
return new LazyWebResource(in, () -> {
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
InputStream input = in.get()) {
int read;
byte[] bytes = new byte[1024];
while ((read = input.read(bytes)) != -1) {
out.write(bytes, 0, read);
}
return out.toByteArray();
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}, lastModified);
}
byte[] asBytes();
/**
@ -85,11 +120,21 @@ public interface WebResource {
InputStream asStream();
default Optional<Long> getLastModified() {
return Optional.empty();
}
final class ByteResource implements WebResource {
private final byte[] content;
private final Long lastModified;
public ByteResource(byte[] content) {
this(content, null);
}
public ByteResource(byte[] content, Long lastModified) {
this.content = content;
this.lastModified = lastModified;
}
@Override
@ -106,5 +151,42 @@ public interface WebResource {
public InputStream asStream() {
return new ByteArrayInputStream(content);
}
@Override
public Optional<Long> getLastModified() {
return Optional.ofNullable(lastModified);
}
}
final class LazyWebResource implements WebResource {
private final Supplier<InputStream> inputStreamSupplier;
private final Supplier<byte[]> contentSupplier;
private final Long lastModified;
public LazyWebResource(Supplier<InputStream> inputStreamSupplier, Supplier<byte[]> contentSupplier, Long lastModified) {
this.inputStreamSupplier = inputStreamSupplier;
this.contentSupplier = contentSupplier;
this.lastModified = lastModified;
}
@Override
public byte[] asBytes() {
return contentSupplier.get();
}
@Override
public String asString() {
return new String(asBytes(), StandardCharsets.UTF_8);
}
@Override
public InputStream asStream() {
return inputStreamSupplier.get();
}
@Override
public Optional<Long> getLastModified() {
return Optional.ofNullable(lastModified);
}
}
}

View File

@ -96,7 +96,9 @@ public class PlanPlaceholderExtension extends PlaceholderExpansion {
if ("Server thread".equalsIgnoreCase(Thread.currentThread().getName())) {
return getCached(params, uuid);
}
return getPlaceholderValue(params, uuid);
return Optional.ofNullable(getCached(params, uuid))
.orElseGet(() -> getPlaceholderValue(params, uuid));
} catch (IllegalStateException e) {
if ("zip file closed".equals(e.getMessage())) {
return null; // Plan is disabled.

View File

@ -46,6 +46,7 @@ import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.Database;
import com.djrapitops.plan.storage.database.queries.containers.PlayerContainerQuery;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.database.queries.objects.SessionQueries;
import com.djrapitops.plan.utilities.comparators.DateHolderRecentComparator;
import com.djrapitops.plan.utilities.java.Lists;
import com.djrapitops.plan.utilities.java.Maps;
@ -91,6 +92,10 @@ public class PlayerJSONCreator {
this.graphs = graphs;
}
public long getLastSeen(UUID playerUUID) {
return dbSystem.getDatabase().query(SessionQueries.lastSeen(playerUUID));
}
public Map<String, Object> createJSONAsMap(UUID playerUUID) {
Database db = dbSystem.getDatabase();
@ -226,6 +231,7 @@ public class PlayerJSONCreator {
info.put("best_ping", bestPing != -1.0 ? bestPing + " ms" : unavailable);
info.put("registered", player.getValue(PlayerKeys.REGISTERED).map(year).orElse("-"));
info.put("last_seen", player.getValue(PlayerKeys.LAST_SEEN).map(year).orElse("-"));
info.put("last_seen_raw_value", player.getValue(PlayerKeys.LAST_SEEN).orElse(0L));
return info;
}

View File

@ -17,6 +17,7 @@
package com.djrapitops.plan.delivery.rendering.pages;
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import com.djrapitops.plan.identification.ServerInfo;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.theme.Theme;
@ -30,7 +31,7 @@ import com.djrapitops.plan.version.VersionChecker;
*/
public class LoginPage implements Page {
private final String template;
private final WebResource template;
private final ServerInfo serverInfo;
private final Locale locale;
private final Theme theme;
@ -38,7 +39,7 @@ public class LoginPage implements Page {
private final VersionChecker versionChecker;
LoginPage(
String htmlTemplate,
WebResource htmlTemplate,
ServerInfo serverInfo,
Locale locale,
Theme theme,
@ -51,12 +52,17 @@ public class LoginPage implements Page {
this.versionChecker = versionChecker;
}
@Override
public long lastModified() {
return template.getLastModified().orElseGet(System::currentTimeMillis);
}
@Override
public String toHtml() {
PlaceholderReplacer placeholders = new PlaceholderReplacer();
placeholders.put("command", getCommand());
placeholders.put("version", versionChecker.getCurrentVersion());
return UnaryChain.of(template)
return UnaryChain.of(template.asString())
.chain(theme::replaceThemeColors)
.chain(placeholders::apply)
.chain(locale::replaceLanguageInHtml)

View File

@ -23,4 +23,8 @@ package com.djrapitops.plan.delivery.rendering.pages;
*/
public interface Page {
String toHtml();
default long lastModified() {
return System.currentTimeMillis();
}
}

View File

@ -22,6 +22,7 @@ import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import com.djrapitops.plan.delivery.webserver.Addresses;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.extension.implementation.results.ExtensionData;
@ -41,7 +42,6 @@ import com.djrapitops.plan.storage.file.PlanFiles;
import com.djrapitops.plan.utilities.dev.Untrusted;
import com.djrapitops.plan.version.VersionChecker;
import dagger.Lazy;
import org.apache.commons.lang3.StringUtils;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -101,15 +101,12 @@ public class PageFactory {
return reactPage();
}
return new PlayersPage(getResource("players.html"), versionChecker.get(),
return new PlayersPage(getResourceAsString("players.html"), versionChecker.get(),
config.get(), theme.get(), serverInfo.get());
}
public Page reactPage() throws IOException {
String reactHtml = StringUtils.replace(
getResource("index.html"),
"/static", getBasePath() + "/static");
return () -> reactHtml;
return new ReactPage(getBasePath(), getResource("index.html"));
}
private String getBasePath() {
@ -135,7 +132,7 @@ public class PageFactory {
}
return new ServerPage(
getResource("server.html"),
getResourceAsString("server.html"),
server,
config.get(),
theme.get(),
@ -158,7 +155,7 @@ public class PageFactory {
}
return new PlayerPage(
getResource("player.html"), player,
getResourceAsString("player.html"), player,
versionChecker.get(),
config.get(),
this,
@ -207,7 +204,7 @@ public class PageFactory {
return reactPage();
}
return new NetworkPage(getResource("network.html"),
return new NetworkPage(getResourceAsString("network.html"),
dbSystem.get(),
versionChecker.get(),
config.get(),
@ -223,7 +220,7 @@ public class PageFactory {
public Page internalErrorPage(String message, @Untrusted Throwable error) {
try {
return new InternalErrorPage(
getResource("error.html"), message, error,
getResourceAsString("error.html"), message, error,
versionChecker.get());
} catch (IOException noParse) {
return () -> "Error occurred: " + error.toString() +
@ -234,20 +231,24 @@ public class PageFactory {
public Page errorPage(String title, String error) throws IOException {
return new ErrorMessagePage(
getResource("error.html"), title, error,
getResourceAsString("error.html"), title, error,
versionChecker.get(), theme.get());
}
public Page errorPage(Icon icon, String title, String error) throws IOException {
return new ErrorMessagePage(
getResource("error.html"), icon, title, error, theme.get(), versionChecker.get());
getResourceAsString("error.html"), icon, title, error, theme.get(), versionChecker.get());
}
public String getResource(String name) throws IOException {
public String getResourceAsString(String name) throws IOException {
return getResource(name).asString();
}
public WebResource getResource(String name) throws IOException {
try {
return ResourceService.getInstance().getResource("Plan", name,
() -> files.get().getResourceFromJar("web/" + name).asWebResource()
).asString();
);
} catch (UncheckedIOException readFail) {
throw readFail.getCause();
}
@ -274,7 +275,7 @@ public class PageFactory {
return reactPage();
}
return new QueryPage(
getResource("query.html"),
getResourceAsString("query.html"),
locale.get(), theme.get(), versionChecker.get()
);
}

View File

@ -0,0 +1,48 @@
/*
* 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.rendering.pages;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import org.apache.commons.lang3.StringUtils;
/**
* Represents React index.html.
*
* @author AuroraLS3
*/
public class ReactPage implements Page {
private final String basePath;
private final WebResource reactHtml;
public ReactPage(String basePath, WebResource reactHtml) {
this.basePath = basePath;
this.reactHtml = reactHtml;
}
@Override
public String toHtml() {
return StringUtils.replace(
reactHtml.asString(),
"/static", basePath + "/static");
}
@Override
public long lastModified() {
return reactHtml.getLastModified().orElseGet(System::currentTimeMillis);
}
}

View File

@ -0,0 +1,32 @@
/*
* 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.webserver;
/**
* @author AuroraLS3
*/
public class CacheStrategy {
public static final String CACHE_IN_BROWSER = "max-age: 2592000";
public static final String CHECK_ETAG = "no-cache";
public static final String CHECK_ETAG_USER_SPECIFIC = "no-cache, private";
private CacheStrategy() {
// Static variable class
}
}

View File

@ -17,6 +17,8 @@
package com.djrapitops.plan.delivery.webserver;
import com.djrapitops.plan.delivery.domain.container.PlayerContainer;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.html.icon.Family;
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
import com.djrapitops.plan.delivery.rendering.pages.Page;
@ -24,10 +26,13 @@ import com.djrapitops.plan.delivery.rendering.pages.PageFactory;
import com.djrapitops.plan.delivery.web.ResourceService;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.ResponseBuilder;
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resource.WebResource;
import com.djrapitops.plan.delivery.webserver.auth.FailReason;
import com.djrapitops.plan.exceptions.WebUserAuthException;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.settings.locale.Locale;
import com.djrapitops.plan.settings.locale.lang.ErrorPageLang;
@ -42,6 +47,7 @@ import com.djrapitops.plan.utilities.java.UnaryChain;
import dagger.Lazy;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.eclipse.jetty.http.HttpHeader;
import javax.inject.Inject;
import javax.inject.Singleton;
@ -51,6 +57,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
/**
* Factory for creating different {@link Response} objects.
@ -66,6 +73,7 @@ public class ResponseFactory {
private final DBSystem dbSystem;
private final Theme theme;
private final Lazy<Addresses> addresses;
private final Formatter<Long> httpLastModifiedFormatter;
@Inject
public ResponseFactory(
@ -73,6 +81,7 @@ public class ResponseFactory {
PageFactory pageFactory,
Locale locale,
DBSystem dbSystem,
Formatters formatters,
Theme theme,
Lazy<Addresses> addresses
) {
@ -82,6 +91,8 @@ public class ResponseFactory {
this.dbSystem = dbSystem;
this.theme = theme;
this.addresses = addresses;
httpLastModifiedFormatter = formatters.httpLastModifiedLong();
}
public WebResource getResource(@Untrusted String resourceName) {
@ -89,10 +100,27 @@ public class ResponseFactory {
() -> files.getResourceFromJar("web/" + resourceName).asWebResource());
}
private Response forPage(Page page) {
private static Response browserCachedNotChangedResponse() {
return Response.builder()
.setStatus(304)
.setContent(new byte[0])
.build();
}
private Response forPage(@Untrusted Request request, Page page) {
long modified = page.lastModified();
Optional<Long> etag = Identifiers.getEtag(request);
if (etag.isPresent() && modified == etag.get()) {
return browserCachedNotChangedResponse();
}
return Response.builder()
.setMimeType(MimeType.HTML)
.setContent(page.toHtml())
.setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CHECK_ETAG)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(modified))
.setHeader(HttpHeader.ETAG.asString(), modified)
.build();
}
@ -104,11 +132,11 @@ public class ResponseFactory {
.build();
}
public Response playersPageResponse() {
public Response playersPageResponse(@Untrusted Request request) {
try {
Optional<Response> error = checkDbClosedError();
if (error.isPresent()) return error.get();
return forPage(pageFactory.playersPage());
return forPage(request, pageFactory.playersPage());
} catch (IOException e) {
return forInternalError(e, "Failed to generate players page");
}
@ -137,25 +165,35 @@ public class ResponseFactory {
.build();
}
private Response getCachedOrNew(long modified, String fileName, Function<String, Response> newResponseFunction) {
WebResource resource = getResource(fileName);
Optional<Long> lastModified = resource.getLastModified();
if (lastModified.isPresent() && modified == lastModified.get()) {
return browserCachedNotChangedResponse();
} else {
return newResponseFunction.apply(fileName);
}
}
public Response internalErrorResponse(Throwable e, String cause) {
return forInternalError(e, cause);
}
public Response networkPageResponse() {
public Response networkPageResponse(@Untrusted Request request) {
Optional<Response> error = checkDbClosedError();
if (error.isPresent()) return error.get();
try {
return forPage(pageFactory.networkPage());
return forPage(request, pageFactory.networkPage());
} catch (IOException e) {
return forInternalError(e, "Failed to generate network page");
}
}
public Response serverPageResponse(ServerUUID serverUUID) {
public Response serverPageResponse(@Untrusted Request request, ServerUUID serverUUID) {
Optional<Response> error = checkDbClosedError();
if (error.isPresent()) return error.get();
try {
return forPage(pageFactory.serverPage(serverUUID));
return forPage(request, pageFactory.serverPage(serverUUID));
} catch (NotFoundException e) {
return notFound404(e.getMessage());
} catch (IOException e) {
@ -171,23 +209,36 @@ public class ResponseFactory {
.build();
}
public Response javaScriptResponse(long modified, @Untrusted String fileName) {
return getCachedOrNew(modified, fileName, this::javaScriptResponse);
}
public Response javaScriptResponse(@Untrusted String fileName) {
try {
String content = UnaryChain.of(getResource(fileName).asString())
WebResource resource = getResource(fileName);
String content = UnaryChain.of(resource.asString())
.chain(this::replaceMainAddressPlaceholder)
.chain(theme::replaceThemeColors)
.chain(resource -> {
if (fileName.startsWith("vendor/") || fileName.startsWith("/vendor/")) {return resource;}
return locale.replaceLanguageInJavascript(resource);
.chain(contents -> {
if (fileName.startsWith("vendor/") || fileName.startsWith("/vendor/")) {return contents;}
return locale.replaceLanguageInJavascript(contents);
})
.chain(resource -> StringUtils.replace(resource, "n.p=\"/\"",
.chain(contents -> StringUtils.replace(contents, "n.p=\"/\"",
"n.p=\"" + getBasePath() + "/\""))
.apply();
return Response.builder()
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(MimeType.JS)
.setContent(content)
.setStatus(200)
.build();
.setStatus(200);
if (fileName.contains("static")) {
resource.getLastModified().ifPresent(lastModified -> responseBuilder
// Can't cache main bundle in browser since base path might change
.setHeader(HttpHeader.CACHE_CONTROL.asString(), fileName.contains("main") ? CacheStrategy.CHECK_ETAG : CacheStrategy.CACHE_IN_BROWSER)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastModified))
.setHeader(HttpHeader.ETAG.asString(), lastModified));
}
return responseBuilder.build();
} catch (UncheckedIOException e) {
return notFound404("Javascript File not found");
}
@ -205,34 +256,64 @@ public class ResponseFactory {
return StringUtils.replace(resource, "PLAN_BASE_ADDRESS", address);
}
public Response cssResponse(long modified, @Untrusted String fileName) {
return getCachedOrNew(modified, fileName, this::cssResponse);
}
public Response cssResponse(@Untrusted String fileName) {
try {
String content = UnaryChain.of(getResource(fileName).asString())
WebResource resource = getResource(fileName);
String content = UnaryChain.of(resource.asString())
.chain(theme::replaceThemeColors)
.chain(resource -> StringUtils.replace(resource, "/static", getBasePath() + "/static"))
.chain(contents -> StringUtils.replace(contents, "/static", getBasePath() + "/static"))
.apply();
return Response.builder()
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(MimeType.CSS)
.setContent(content)
.setStatus(200)
.build();
.setStatus(200);
if (fileName.contains("static")) {
resource.getLastModified().ifPresent(lastModified -> responseBuilder
// Can't cache css bundles in browser since base path might change
.setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CHECK_ETAG)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastModified))
.setHeader(HttpHeader.ETAG.asString(), lastModified));
}
return responseBuilder.build();
} catch (UncheckedIOException e) {
return notFound404("CSS File not found");
}
}
public Response imageResponse(long modified, @Untrusted String fileName) {
return getCachedOrNew(modified, fileName, this::imageResponse);
}
public Response imageResponse(@Untrusted String fileName) {
try {
return Response.builder()
WebResource resource = getResource(fileName);
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(MimeType.IMAGE)
.setContent(getResource(fileName))
.setStatus(200)
.build();
.setContent(resource)
.setStatus(200);
if (fileName.contains("static")) {
resource.getLastModified().ifPresent(lastModified -> responseBuilder
.setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CACHE_IN_BROWSER)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastModified))
.setHeader(HttpHeader.ETAG.asString(), lastModified));
}
return responseBuilder.build();
} catch (UncheckedIOException e) {
return notFound404("Image File not found");
}
}
public Response fontResponse(long modified, @Untrusted String fileName) {
return getCachedOrNew(modified, fileName, this::fontResponse);
}
public Response fontResponse(@Untrusted String fileName) {
String type;
if (fileName.endsWith(".woff")) {
@ -247,10 +328,18 @@ public class ResponseFactory {
type = MimeType.FONT_BYTESTREAM;
}
try {
return Response.builder()
WebResource resource = getResource(fileName);
ResponseBuilder responseBuilder = Response.builder()
.setMimeType(type)
.setContent(getResource(fileName))
.build();
.setContent(resource);
if (fileName.contains("static")) {
resource.getLastModified().ifPresent(lastModified -> responseBuilder
.setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CACHE_IN_BROWSER)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastModified))
.setHeader(HttpHeader.ETAG.asString(), lastModified));
}
return responseBuilder.build();
} catch (UncheckedIOException e) {
return notFound404("Font File not found");
}
@ -273,9 +362,14 @@ public class ResponseFactory {
public Response robotsResponse() {
try {
WebResource resource = getResource("robots.txt");
Long lastModified = resource.getLastModified().orElseGet(System::currentTimeMillis);
return Response.builder()
.setMimeType("text/plain")
.setContent(getResource("robots.txt"))
.setContent(resource)
.setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CACHE_IN_BROWSER)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastModified))
.setHeader(HttpHeader.ETAG.asString(), lastModified)
.build();
} catch (UncheckedIOException e) {
return forInternalError(e, "Could not read robots.txt");
@ -415,9 +509,9 @@ public class ResponseFactory {
.build();
}
public Response playerPageResponse(UUID playerUUID) {
public Response playerPageResponse(@Untrusted Request request, UUID playerUUID) {
try {
return forPage(pageFactory.playerPage(playerUUID));
return forPage(request, pageFactory.playerPage(playerUUID));
} catch (IllegalStateException e) {
return playerNotFound404();
} catch (IOException e) {
@ -425,33 +519,33 @@ public class ResponseFactory {
}
}
public Response loginPageResponse() {
public Response loginPageResponse(@Untrusted Request request) {
try {
return forPage(pageFactory.loginPage());
return forPage(request, pageFactory.loginPage());
} catch (IOException e) {
return forInternalError(e, "Failed to generate player page");
}
}
public Response registerPageResponse() {
public Response registerPageResponse(@Untrusted Request request) {
try {
return forPage(pageFactory.registerPage());
return forPage(request, pageFactory.registerPage());
} catch (IOException e) {
return forInternalError(e, "Failed to generate player page");
}
}
public Response queryPageResponse() {
public Response queryPageResponse(@Untrusted Request request) {
try {
return forPage(pageFactory.queryPage());
return forPage(request, pageFactory.queryPage());
} catch (IOException e) {
return forInternalError(e, "Failed to generate query page");
}
}
public Response errorsPageResponse() {
public Response errorsPageResponse(@Untrusted Request request) {
try {
return forPage(pageFactory.errorsPage());
return forPage(request, pageFactory.errorsPage());
} catch (IOException e) {
return forInternalError(e, "Failed to generate errors page");
}
@ -468,12 +562,9 @@ public class ResponseFactory {
}
}
public Response reactPageResponse() {
public Response reactPageResponse(Request request) {
try {
return Response.builder()
.setMimeType(MimeType.HTML)
.setContent(pageFactory.reactPage().toHtml())
.build();
return forPage(request, pageFactory.reactPage());
} catch (UncheckedIOException | IOException e) {
return forInternalError(e, "Could not read index.html");
}

View File

@ -16,6 +16,8 @@
*/
package com.djrapitops.plan.delivery.webserver.cache;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.processing.Processing;
import com.djrapitops.plan.settings.config.PlanConfig;
@ -46,10 +48,12 @@ public class AsyncJSONResolverService {
private final Map<String, Future<JSONStorage.StoredJSON>> currentlyProcessing;
private final Map<String, Long> previousUpdates;
private final ReentrantLock accessLock; // Access lock prevents double processing same resource
private final Formatter<Long> httpLastModifiedFormatter;
@Inject
public AsyncJSONResolverService(
PlanConfig config,
Formatters formatters,
Processing processing,
JSONStorage jsonStorage
) {
@ -60,6 +64,8 @@ public class AsyncJSONResolverService {
currentlyProcessing = new ConcurrentHashMap<>();
previousUpdates = new ConcurrentHashMap<>();
accessLock = new ReentrantLock();
httpLastModifiedFormatter = formatters.httpLastModifiedLong();
}
public <T> JSONStorage.StoredJSON resolve(
@ -155,4 +161,8 @@ public class AsyncJSONResolverService {
return created;
});
}
public Formatter<Long> getHttpLastModifiedFormatter() {
return httpLastModifiedFormatter;
}
}

View File

@ -55,6 +55,7 @@ public enum DataID {
JOIN_ADDRESSES_BY_DAY;
public String of(ServerUUID serverUUID) {
if (serverUUID == null) return name();
return name() + '-' + serverUUID;
}
}

View File

@ -129,9 +129,11 @@ public class JSONFileStorage implements JSONStorage {
public Optional<StoredJSON> fetchJSON(String identifier) {
File[] stored = jsonDirectory.toFile().listFiles();
if (stored == null) return Optional.empty();
String lookForStart = identifier + '-';
for (File file : stored) {
String fileName = file.getName();
if (fileName.endsWith(JSON_FILE_EXTENSION) && fileName.startsWith(identifier + '-')) {
if (fileName.endsWith(JSON_FILE_EXTENSION) && fileName.startsWith(lookForStart)) {
return Optional.ofNullable(readStoredJSON(file));
}
}
@ -179,10 +181,12 @@ public class JSONFileStorage implements JSONStorage {
private Optional<StoredJSON> fetchJSONWithTimestamp(String identifier, long timestamp, BiPredicate<Matcher, Long> timestampComparator) {
File[] stored = jsonDirectory.toFile().listFiles();
if (stored == null) return Optional.empty();
String lookForStart = identifier + '-';
for (File file : stored) {
try {
String fileName = file.getName();
if (fileName.endsWith(JSON_FILE_EXTENSION) && fileName.startsWith(identifier + '-')) {
if (fileName.endsWith(JSON_FILE_EXTENSION) && fileName.startsWith(lookForStart)) {
Matcher timestampMatch = timestampRegex.matcher(fileName);
if (timestampMatch.find() && timestampComparator.test(timestampMatch, timestamp)) {
return Optional.ofNullable(readStoredJSON(file));
@ -270,6 +274,28 @@ public class JSONFileStorage implements JSONStorage {
});
}
@Override
public Optional<Long> getTimestamp(String identifier) {
File[] stored = jsonDirectory.toFile().listFiles();
if (stored == null) return Optional.empty();
String lookForStart = identifier + '-';
for (File file : stored) {
try {
String fileName = file.getName();
if (fileName.endsWith(JSON_FILE_EXTENSION) && fileName.startsWith(lookForStart)) {
Matcher timestampMatch = timestampRegex.matcher(fileName);
if (timestampMatch.find()) {
return Optional.of(Long.parseLong(timestampMatch.group(1)));
}
}
} catch (NumberFormatException e) {
// Ignore this file, malformed timestamp
}
}
return Optional.empty();
}
@Singleton
public static class CleanTask extends TaskSystem.Task {
private final PlanConfig config;

View File

@ -120,6 +120,16 @@ public class JSONMemoryStorageShim implements JSONStorage {
underlyingStorage.invalidateOlder(identifier, timestamp);
}
@Override
public Optional<Long> getTimestamp(String identifier) {
for (TimestampedIdentifier key : getCache().asMap().keySet()) {
if (key.identifier.equalsIgnoreCase(identifier)) {
return Optional.of(key.timestamp);
}
}
return Optional.empty();
}
static class TimestampedIdentifier {
private final String identifier;
private final long timestamp;

View File

@ -63,6 +63,8 @@ public interface JSONStorage extends SubSystem {
void invalidateOlder(String identifier, long timestamp);
Optional<Long> getTimestamp(String identifier);
final class StoredJSON {
public final String json;
public final long timestamp;
@ -72,6 +74,14 @@ public interface JSONStorage extends SubSystem {
this.timestamp = timestamp;
}
public String getJson() {
return json;
}
public long getTimestamp() {
return timestamp;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;

View File

@ -46,7 +46,7 @@ public class JettyResponseSender {
}
public void send() throws IOException {
if ("HEAD".equals(servletRequest.getMethod()) || response.getCode() == 204) {
if ("HEAD".equals(servletRequest.getMethod()) || response.getCode() == 204 || response.getCode() == 304) {
setResponseHeaders();
sendHeadResponse();
} else if (canGzip()) {
@ -117,7 +117,12 @@ public class JettyResponseSender {
private void beginSend() {
String length = response.getHeaders().get(HttpHeader.CONTENT_LENGTH.asString());
if (length == null || "0".equals(length) || response.getCode() == 204 || "HEAD".equals(servletRequest.getMethod())) {
if (length == null
|| "0".equals(length)
|| response.getCode() == 204
|| response.getCode() == 304
|| "HEAD".equals(servletRequest.getMethod())
) {
servletResponse.setHeader(HttpHeader.CONTENT_LENGTH.asString(), null);
}
// Return a content length of -1 for HTTP code 204 (No content)

View File

@ -40,7 +40,7 @@ public class ErrorsPageResolver implements Resolver {
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(responseFactory.errorsPageResponse());
return Optional.of(responseFactory.errorsPageResponse(request));
}
}

View File

@ -82,10 +82,11 @@ public class PlayerPageResolver implements Resolver {
return Optional.empty();
}
return path.getPart(1)
.map(playerName -> getResponse(request.getPath(), playerName));
.map(playerName -> getResponse(request, playerName));
}
private Response getResponse(@Untrusted URIPath path, @Untrusted String playerName) {
private Response getResponse(@Untrusted Request request, @Untrusted String playerName) {
@Untrusted URIPath path = request.getPath();
UUID playerUUID = uuidUtility.getUUIDOf(playerName);
if (playerUUID == null) return responseFactory.uuidNotFound404();
@ -98,6 +99,6 @@ public class PlayerPageResolver implements Resolver {
// Redirect /player/{uuid/name}/ to /player/{uuid}
return responseFactory.redirectResponse("../" + Html.encodeToURL(playerUUID.toString()));
}
return responseFactory.playerPageResponse(playerUUID);
return responseFactory.playerPageResponse(request, playerUUID);
}
}

View File

@ -51,6 +51,6 @@ public class PlayersPageResolver implements Resolver {
public Optional<Response> resolve(Request request) {
// Redirect /players/ to /players
if (request.getPath().getPart(1).isPresent()) return Optional.of(responseFactory.redirectResponse("/players"));
return Optional.of(responseFactory.playersPageResponse());
return Optional.of(responseFactory.playersPageResponse(request));
}
}

View File

@ -42,6 +42,6 @@ public class QueryPageResolver implements Resolver {
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(responseFactory.queryPageResponse());
return Optional.of(responseFactory.queryPageResponse(request));
}
}

View File

@ -80,17 +80,17 @@ public class ServerPageResolver implements Resolver {
return Optional.of(responseFactory.redirectResponse(directTo));
}
private Optional<Response> getServerPage(ServerUUID serverUUID, Request request) {
private Optional<Response> getServerPage(ServerUUID serverUUID, @Untrusted Request request) {
boolean toNetworkPage = serverInfo.getServer().isProxy() && serverInfo.getServerUUID().equals(serverUUID);
if (toNetworkPage) {
if (request.getPath().getPart(0).map("network"::equals).orElse(false)) {
return Optional.of(responseFactory.networkPageResponse());
return Optional.of(responseFactory.networkPageResponse(request));
} else {
// Accessing /server/Server <Bungee ID> which should be redirected to /network
return redirectToCurrentServer();
}
}
return Optional.of(responseFactory.serverPageResponse(serverUUID));
return Optional.of(responseFactory.serverPageResponse(request, serverUUID));
}
private Optional<ServerUUID> getServerUUID(@Untrusted URIPath path) {

View File

@ -21,6 +21,7 @@ import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.URIPath;
import com.djrapitops.plan.delivery.webserver.ResponseFactory;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.utilities.dev.Untrusted;
import org.apache.commons.lang3.StringUtils;
@ -53,17 +54,22 @@ public class StaticResourceResolver implements NoAuthResolver {
private Response getResponse(Request request) {
@Untrusted String resource = getPath(request).asString().substring(1);
@Untrusted Optional<Long> etag = Identifiers.getEtag(request);
if (resource.endsWith(".css")) {
return responseFactory.cssResponse(resource);
return etag.map(tag -> responseFactory.cssResponse(tag, resource))
.orElseGet(() -> responseFactory.cssResponse(resource));
}
if (resource.endsWith(".js")) {
return responseFactory.javaScriptResponse(resource);
return etag.map(tag -> responseFactory.javaScriptResponse(tag, resource))
.orElseGet(() -> responseFactory.javaScriptResponse(resource));
}
if (resource.endsWith(".png")) {
return responseFactory.imageResponse(resource);
return etag.map(tag -> responseFactory.imageResponse(tag, resource))
.orElseGet(() -> responseFactory.imageResponse(resource));
}
if (StringUtils.endsWithAny(resource, ".woff", ".woff2", ".eot", ".ttf")) {
return responseFactory.fontResponse(resource);
return etag.map(tag -> responseFactory.fontResponse(tag, resource))
.orElseGet(() -> responseFactory.fontResponse(resource));
}
return null;
}

View File

@ -52,6 +52,6 @@ public class LoginPageResolver implements NoAuthResolver {
.filter(redirectBackTo -> !redirectBackTo.startsWith("http"));
return Optional.of(responseFactory.redirectResponse(from.orElse("/")));
}
return Optional.of(responseFactory.loginPageResponse());
return Optional.of(responseFactory.loginPageResponse(request));
}
}

View File

@ -50,6 +50,6 @@ public class RegisterPageResolver implements NoAuthResolver {
if (user.isPresent() || !webServer.get().isAuthRequired()) {
return Optional.of(responseFactory.redirectResponse("/"));
}
return Optional.of(responseFactory.registerPageResponse());
return Optional.of(responseFactory.registerPageResponse(request));
}
}

View File

@ -17,8 +17,8 @@
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.domain.datatransfer.extension.ExtensionDataDto;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
@ -53,7 +53,7 @@ import java.util.stream.Collectors;
* @author AuroraLS3
*/
@Singleton
public class ExtensionJSONResolver implements Resolver {
public class ExtensionJSONResolver extends JSONResolver {
private final DBSystem dbSystem;
private final Identifiers identifiers;
@ -66,6 +66,9 @@ public class ExtensionJSONResolver implements Resolver {
this.jsonResolverService = jsonResolverService;
}
@Override
public Formatter<Long> getHttpLastModifiedFormatter() {return jsonResolverService.getHttpLastModifiedFormatter();}
@Override
public boolean canAccess(Request request) {
WebUser permissions = request.getUser().orElse(new WebUser(""));
@ -107,10 +110,7 @@ public class ExtensionJSONResolver implements Resolver {
private Response getResponse(Request request, ServerUUID serverUUID) {
JSONStorage.StoredJSON json = getJSON(request, serverUUID);
return Response.builder()
.setJSONContent(json.json)
.build();
return getCachedOrNewResponse(request, json);
}
private Map<String, List<ExtensionDataDto>> getExtensionData(ServerUUID serverUUID) {

View File

@ -16,9 +16,8 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.graphs.GraphJSONCreator;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
@ -51,7 +50,7 @@ import java.util.Optional;
*/
@Singleton
@Path("/v1/graph")
public class GraphsJSONResolver implements Resolver {
public class GraphsJSONResolver extends JSONResolver {
private final Identifiers identifiers;
private final AsyncJSONResolverService jsonResolverService;
@ -60,13 +59,17 @@ public class GraphsJSONResolver implements Resolver {
@Inject
public GraphsJSONResolver(
Identifiers identifiers,
AsyncJSONResolverService jsonResolverService, GraphJSONCreator graphJSON
AsyncJSONResolverService jsonResolverService,
GraphJSONCreator graphJSON
) {
this.identifiers = identifiers;
this.jsonResolverService = jsonResolverService;
this.graphJSON = graphJSON;
}
@Override
public Formatter<Long> getHttpLastModifiedFormatter() {return jsonResolverService.getHttpLastModifiedFormatter();}
@Override
public boolean canAccess(Request request) {
return request.getUser().orElse(new WebUser("")).hasPermission("page.server");
@ -126,10 +129,8 @@ public class GraphsJSONResolver implements Resolver {
DataID dataID = getDataID(type);
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(getGraphJSON(request, dataID).json)
.build();
JSONStorage.StoredJSON storedJSON = getGraphJSON(request, dataID);
return getCachedOrNewResponse(request, storedJSON);
}
private JSONStorage.StoredJSON getGraphJSON(@Untrusted Request request, DataID dataID) {

View File

@ -0,0 +1,67 @@
/*
* 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.webserver.resolver.json;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.webserver.CacheStrategy;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.utilities.dev.Untrusted;
import com.djrapitops.plan.utilities.java.Maps;
import org.eclipse.jetty.http.HttpHeader;
import java.util.Optional;
/**
* @author AuroraLS3
*/
public abstract class JSONResolver implements Resolver {
protected Response getCachedOrNewResponse(@Untrusted Request request, JSONStorage.StoredJSON storedJSON) {
if (storedJSON == null) {
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(Maps.builder(String.class, String.class)
.put("error", "Json failed to generate for some reason, see /Plan/logs for errors")
.build())
.build();
}
Optional<Long> browserCached = Identifiers.getEtag(request);
if (browserCached.isPresent() && browserCached.get() == storedJSON.getTimestamp()) {
return Response.builder()
.setStatus(304)
.setContent(new byte[0])
.build();
}
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(storedJSON.getJson())
.setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CHECK_ETAG_USER_SPECIFIC)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), getHttpLastModifiedFormatter().apply(storedJSON.getTimestamp()))
.setHeader(HttpHeader.ETAG.asString(), storedJSON.getTimestamp())
.build();
}
protected abstract Formatter<Long> getHttpLastModifiedFormatter();
}

View File

@ -16,9 +16,8 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.network.NetworkTabJSONCreator;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
@ -26,7 +25,6 @@ import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService;
import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.utilities.java.Maps;
import java.util.Optional;
import java.util.function.Supplier;
@ -36,7 +34,7 @@ import java.util.function.Supplier;
*
* @author AuroraLS3
*/
public class NetworkTabJSONResolver<T> implements Resolver {
public class NetworkTabJSONResolver<T> extends JSONResolver {
private final DataID dataID;
private final Supplier<T> jsonCreator;
@ -51,6 +49,9 @@ public class NetworkTabJSONResolver<T> implements Resolver {
this.asyncJSONResolverService = asyncJSONResolverService;
}
@Override
public Formatter<Long> getHttpLastModifiedFormatter() {return asyncJSONResolverService.getHttpLastModifiedFormatter();}
@Override
public boolean canAccess(Request request) {
return request.getUser().orElse(new WebUser("")).hasPermission("page.network");
@ -63,18 +64,6 @@ public class NetworkTabJSONResolver<T> implements Resolver {
private Response getResponse(Request request) {
JSONStorage.StoredJSON json = asyncJSONResolverService.resolve(Identifiers.getTimestamp(request), dataID, jsonCreator);
if (json == null) {
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(Maps.builder(String.class, String.class)
.put("error", "Json failed to generate for some reason, see /Plan/logs for errors")
.build())
.build();
}
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(json.json)
.build();
return getCachedOrNewResponse(request, json);
}
}

View File

@ -16,6 +16,8 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.formatting.Formatters;
import com.djrapitops.plan.delivery.rendering.json.PlayerJSONCreator;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
@ -23,6 +25,7 @@ import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.exception.BadRequestException;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.CacheStrategy;
import com.djrapitops.plan.identification.Identifiers;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@ -33,9 +36,11 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.jetty.http.HttpHeader;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
@ -45,11 +50,14 @@ public class PlayerJSONResolver implements Resolver {
private final Identifiers identifiers;
private final PlayerJSONCreator jsonCreator;
private final Formatter<Long> httpLastModifiedFormatter;
@Inject
public PlayerJSONResolver(Identifiers identifiers, PlayerJSONCreator jsonCreator) {
public PlayerJSONResolver(Identifiers identifiers, Formatters formatters, PlayerJSONCreator jsonCreator) {
this.identifiers = identifiers;
this.jsonCreator = jsonCreator;
httpLastModifiedFormatter = formatters.httpLastModifiedLong();
}
@Override
@ -88,9 +96,30 @@ public class PlayerJSONResolver implements Resolver {
private Response getResponse(Request request) {
UUID playerUUID = identifiers.getPlayerUUID(request); // Can throw BadRequestException
Optional<Long> etag = Identifiers.getEtag(request);
if (etag.isPresent()) {
long lastSeen = jsonCreator.getLastSeen(playerUUID);
if (etag.get() == lastSeen) {
return Response.builder()
.setStatus(304)
.setContent(new byte[0])
.build();
}
}
Map<String, Object> jsonAsMap = jsonCreator.createJSONAsMap(playerUUID);
long lastSeenRawValue = Optional.ofNullable(jsonAsMap.get("info"))
.map(Map.class::cast)
.map(info -> info.get("last_seen_raw_value"))
.map(Long.class::cast)
.orElseGet(System::currentTimeMillis);
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(jsonCreator.createJSONAsMap(playerUUID))
.setJSONContent(jsonAsMap)
.setHeader(HttpHeader.CACHE_CONTROL.asString(), CacheStrategy.CHECK_ETAG_USER_SPECIFIC)
.setHeader(HttpHeader.LAST_MODIFIED.asString(), httpLastModifiedFormatter.apply(lastSeenRawValue))
.setHeader(HttpHeader.ETAG.asString(), lastSeenRawValue)
.build();
}
}

View File

@ -16,9 +16,9 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.JSONFactory;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
@ -49,7 +49,7 @@ import java.util.Optional;
*/
@Singleton
@Path("/v1/kills")
public class PlayerKillsJSONResolver implements Resolver {
public class PlayerKillsJSONResolver extends JSONResolver {
private final Identifiers identifiers;
private final AsyncJSONResolverService jsonResolverService;
@ -66,6 +66,9 @@ public class PlayerKillsJSONResolver implements Resolver {
this.jsonFactory = jsonFactory;
}
@Override
public Formatter<Long> getHttpLastModifiedFormatter() {return jsonResolverService.getHttpLastModifiedFormatter();}
@Override
public boolean canAccess(Request request) {
return request.getUser().orElse(new WebUser("")).hasPermission("page.server");
@ -99,9 +102,6 @@ public class PlayerKillsJSONResolver implements Resolver {
JSONStorage.StoredJSON storedJSON = jsonResolverService.resolve(timestamp, DataID.KILLS, serverUUID,
theUUID -> Collections.singletonMap("player_kills", jsonFactory.serverPlayerKillsAsJSONMaps(theUUID))
);
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(storedJSON.json)
.build();
return getCachedOrNewResponse(request, storedJSON);
}
}

View File

@ -16,9 +16,9 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.JSONFactory;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
@ -49,7 +49,7 @@ import java.util.Optional;
*/
@Singleton
@Path("/v1/players")
public class PlayersTableJSONResolver implements Resolver {
public class PlayersTableJSONResolver extends JSONResolver {
private final Identifiers identifiers;
private final AsyncJSONResolverService jsonResolverService;
@ -66,6 +66,9 @@ public class PlayersTableJSONResolver implements Resolver {
this.jsonFactory = jsonFactory;
}
@Override
public Formatter<Long> getHttpLastModifiedFormatter() {return jsonResolverService.getHttpLastModifiedFormatter();}
@Override
public boolean canAccess(Request request) {
WebUser user = request.getUser().orElse(new WebUser(""));
@ -95,10 +98,8 @@ public class PlayersTableJSONResolver implements Resolver {
}
private Response getResponse(Request request) {
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(getStoredJSON(request).json)
.build();
JSONStorage.StoredJSON storedJSON = getStoredJSON(request);
return getCachedOrNewResponse(request, storedJSON);
}
private JSONStorage.StoredJSON getStoredJSON(@Untrusted Request request) {

View File

@ -16,14 +16,14 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.ServerTabJSONCreator;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
import com.djrapitops.plan.delivery.webserver.cache.AsyncJSONResolverService;
import com.djrapitops.plan.delivery.webserver.cache.DataID;
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
import com.djrapitops.plan.identification.Identifiers;
import com.djrapitops.plan.identification.ServerUUID;
import com.djrapitops.plan.utilities.dev.Untrusted;
@ -36,7 +36,7 @@ import java.util.function.Function;
*
* @author AuroraLS3
*/
public class ServerTabJSONResolver<T> implements Resolver {
public class ServerTabJSONResolver<T> extends JSONResolver {
private final DataID dataID;
private final Identifiers identifiers;
@ -53,6 +53,9 @@ public class ServerTabJSONResolver<T> implements Resolver {
this.asyncJSONResolverService = asyncJSONResolverService;
}
@Override
public Formatter<Long> getHttpLastModifiedFormatter() {return asyncJSONResolverService.getHttpLastModifiedFormatter();}
@Override
public boolean canAccess(Request request) {
return request.getUser().orElse(new WebUser("")).hasPermission("page.server");
@ -65,9 +68,7 @@ public class ServerTabJSONResolver<T> implements Resolver {
private Response getResponse(@Untrusted Request request) {
ServerUUID serverUUID = identifiers.getServerUUID(request); // Can throw BadRequestException
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(asyncJSONResolverService.resolve(Identifiers.getTimestamp(request), dataID, serverUUID, jsonCreator).json)
.build();
JSONStorage.StoredJSON storedJson = asyncJSONResolverService.resolve(Identifiers.getTimestamp(request), dataID, serverUUID, jsonCreator);
return getCachedOrNewResponse(request, storedJson);
}
}

View File

@ -16,9 +16,9 @@
*/
package com.djrapitops.plan.delivery.webserver.resolver.json;
import com.djrapitops.plan.delivery.formatting.Formatter;
import com.djrapitops.plan.delivery.rendering.json.JSONFactory;
import com.djrapitops.plan.delivery.web.resolver.MimeType;
import com.djrapitops.plan.delivery.web.resolver.Resolver;
import com.djrapitops.plan.delivery.web.resolver.Response;
import com.djrapitops.plan.delivery.web.resolver.request.Request;
import com.djrapitops.plan.delivery.web.resolver.request.WebUser;
@ -50,7 +50,7 @@ import java.util.Optional;
*/
@Singleton
@Path("/v1/sessions")
public class SessionsJSONResolver implements Resolver {
public class SessionsJSONResolver extends JSONResolver {
private final Identifiers identifiers;
private final AsyncJSONResolverService jsonResolverService;
@ -67,6 +67,9 @@ public class SessionsJSONResolver implements Resolver {
this.jsonFactory = jsonFactory;
}
@Override
public Formatter<Long> getHttpLastModifiedFormatter() {return jsonResolverService.getHttpLastModifiedFormatter();}
@Override
public boolean canAccess(Request request) {
return request.getUser().orElse(new WebUser("")).hasPermission("page.server");
@ -92,10 +95,8 @@ public class SessionsJSONResolver implements Resolver {
}
private Response getResponse(Request request) {
return Response.builder()
.setMimeType(MimeType.JSON)
.setJSONContent(getStoredJSON(request).json)
.build();
JSONStorage.StoredJSON result = getStoredJSON(request);
return getCachedOrNewResponse(request, result);
}
private JSONStorage.StoredJSON getStoredJSON(@Untrusted Request request) {

View File

@ -44,6 +44,6 @@ public class SwaggerPageResolver implements Resolver {
@Override
public Optional<Response> resolve(Request request) {
return Optional.of(responseFactory.reactPageResponse());
return Optional.of(responseFactory.reactPageResponse(request));
}
}

View File

@ -22,6 +22,7 @@ import com.djrapitops.plan.storage.database.DBSystem;
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
import com.djrapitops.plan.storage.database.queries.objects.UserIdentifierQueries;
import com.djrapitops.plan.utilities.dev.Untrusted;
import org.eclipse.jetty.http.HttpHeader;
import org.jetbrains.annotations.Nullable;
import javax.inject.Inject;
@ -67,9 +68,11 @@ public class Identifiers {
public static Optional<Long> getTimestamp(@Untrusted Request request) {
try {
long currentTime = System.currentTimeMillis();
long timestamp = request.getQuery().get("timestamp")
long timestamp = request.getHeader("X-Plan-Timestamp")
.map(Long::parseLong)
.orElse(currentTime);
.orElseGet(() -> request.getQuery().get("timestamp")
.map(Long::parseLong)
.orElse(currentTime));
if (currentTime + TimeUnit.SECONDS.toMillis(10L) < timestamp) {
return Optional.empty();
}
@ -79,6 +82,17 @@ public class Identifiers {
}
}
public static Optional<Long> getEtag(Request request) {
return request.getHeader(HttpHeader.IF_NONE_MATCH.asString())
.map(tag -> {
try {
return Long.parseLong(tag);
} catch (NumberFormatException notANumber) {
throw new BadRequestException("'" + HttpHeader.IF_NONE_MATCH.asString() + "'-header was not a number. Clear browser cache.");
}
});
}
/**
* Obtain UUID of the server.
*

View File

@ -33,6 +33,7 @@ import com.djrapitops.plan.storage.database.queries.objects.*;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
/**
@ -85,7 +86,11 @@ public class PlayerContainerQuery implements Query<PlayerContainer> {
return worldTimes;
});
container.putSupplier(PlayerKeys.LAST_SEEN, () -> SessionsMutator.forContainer(container).toLastSeen());
container.putSupplier(PlayerKeys.LAST_SEEN, () -> {
Optional<ActiveSession> activeSession = container.getValue(PlayerKeys.ACTIVE_SESSION);
if (activeSession.isPresent()) return System.currentTimeMillis();
return SessionsMutator.forContainer(container).toLastSeen();
});
container.putSupplier(PlayerKeys.PLAYER_KILLS, () -> db.query(KillQueries.fetchPlayerKillsOfPlayer(uuid)));
container.putSupplier(PlayerKeys.PLAYER_DEATHS_KILLS, () -> db.query(KillQueries.fetchPlayerDeathsOfPlayer(uuid)));
container.putSupplier(PlayerKeys.PLAYER_KILL_COUNT, () -> container.getValue(PlayerKeys.PLAYER_KILLS).map(Collection::size).orElse(0));

View File

@ -786,6 +786,14 @@ public class SessionQueries {
};
}
public static Query<Long> lastSeen(UUID playerUUID) {
String sql = SELECT + "MAX(" + SessionsTable.SESSION_END + ") as last_seen" +
FROM + SessionsTable.TABLE_NAME +
WHERE + SessionsTable.USER_ID + "=" + UsersTable.SELECT_USER_ID;
return db -> db.queryOptional(sql, set -> set.getLong("last_seen"), playerUUID)
.orElse(0L);
}
public static Query<Long> lastSeen(UUID playerUUID, ServerUUID serverUUID) {
String sql = SELECT + "MAX(" + SessionsTable.SESSION_END + ") as last_seen" +
FROM + SessionsTable.TABLE_NAME +

View File

@ -81,11 +81,13 @@ public interface Resource {
* @throws UncheckedIOException if fails to read the file.
*/
default WebResource asWebResource() {
try {
return WebResource.create(asInputStream());
} catch (IOException e) {
throw new UncheckedIOException("Failed to read '" + getResourceName() + "'", e);
}
return WebResource.create(() -> {
try {
return asInputStream();
} catch (IOException e) {
throw new UncheckedIOException("Failed to read '" + getResourceName() + "'", e);
}
}, getLastModifiedDate());
}
/**

View File

@ -14,8 +14,9 @@ const isCurrentAddress = (address) => {
export const baseAddress = javaReplaced.address.startsWith('PLAN_') || !isCurrentAddress(javaReplaced.address) ? "" : javaReplaced.address;
export const staticSite = javaReplaced.isStatic === 'true';
export const doSomeGetRequest = async (url, statusOptions) => {
return doSomeRequest(url, statusOptions, async () => axios.get(baseAddress + url));
export const doSomeGetRequest = async (url, updateRequested, statusOptions) => {
return doSomeRequest(url, statusOptions, async () => axios.get(baseAddress + url,
updateRequested ? {headers: {"X-Plan-Timestamp": updateRequested}} : {}));
}
export const doSomePostRequest = async (url, statusOptions, body) => {
@ -72,6 +73,6 @@ export const doSomeRequest = async (url, statusOptions, axiosFunction) => {
export const standard200option = {status: 200, get: response => response.data}
const exported404options = {status: 404, get: () => 'Data not yet exported'}
export const doGetRequest = async url => {
return doSomeGetRequest(url, staticSite ? [standard200option, exported404options] : [standard200option])
export const doGetRequest = async (url, updateRequested) => {
return doSomeGetRequest(url, updateRequested, staticSite ? [standard200option, exported404options] : [standard200option])
}

View File

@ -1,42 +1,42 @@
import {doGetRequest, staticSite} from "./backendConfiguration";
export const fetchNetworkOverview = async (updateRequested) => {
let url = `/v1/network/overview?timestamp=${updateRequested}`;
let url = `/v1/network/overview`;
if (staticSite) url = `/data/network-overview.json`;
return doGetRequest(url);
return doGetRequest(url, updateRequested);
}
export const fetchServersOverview = async (updateRequested) => {
let url = `/v1/network/servers?timestamp=${updateRequested}`;
let url = `/v1/network/servers`;
if (staticSite) url = `/data/network-servers.json`;
return doGetRequest(url);
return doGetRequest(url, updateRequested);
}
export const fetchServerPie = async (timestamp) => {
let url = `/v1/graph?type=serverPie&timestamp=${timestamp}`;
let url = `/v1/graph?type=serverPie`;
if (staticSite) url = `/data/graph-serverPie.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchNetworkSessionsOverview = async (timestamp) => {
let url = `/v1/network/sessionsOverview?timestamp=${timestamp}`;
let url = `/v1/network/sessionsOverview`;
if (staticSite) url = `/data/network-sessionsOverview.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchNetworkPlayerbaseOverview = async (timestamp) => {
let url = `/v1/network/playerbaseOverview?timestamp=${timestamp}`;
let url = `/v1/network/playerbaseOverview`;
if (staticSite) url = `/data/network-playerbaseOverview.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchNetworkPingTable = async (timestamp) => {
let url = `/v1/network/pingTable?timestamp=${timestamp}`;
let url = `/v1/network/pingTable`;
if (staticSite) url = `/data/network-pingTable.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchNetworkPerformanceOverview = async (timestamp, serverUUIDs) => {
let url = `/v1/network/performanceOverview?servers=${encodeURIComponent(JSON.stringify(serverUUIDs))}&timestamp=${timestamp}`;
return doGetRequest(url);
let url = `/v1/network/performanceOverview?servers=${encodeURIComponent(JSON.stringify(serverUUIDs))}`;
return doGetRequest(url, timestamp);
}

View File

@ -2,9 +2,9 @@ import {faMapSigns} from "@fortawesome/free-solid-svg-icons";
import {doSomeGetRequest, standard200option, staticSite} from "./backendConfiguration";
export const fetchPlayer = async (timestamp, uuid) => {
let url = `/v1/player?player=${uuid}&timestamp=${timestamp}`;
let url = `/v1/player?player=${uuid}`;
if (staticSite) url = `/player/${uuid}/player-${uuid}.json`
return doSomeGetRequest(url, [
return doSomeGetRequest(url, timestamp, [
standard200option,
{
status: staticSite ? 404 : 400,

View File

@ -3,49 +3,49 @@ import {doGetRequest, staticSite} from "./backendConfiguration";
export const fetchServerIdentity = async (timestamp, identifier) => {
let url = `/v1/serverIdentity?server=${identifier}`;
if (staticSite) url = `/data/serverIdentity-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchServerOverview = async (timestamp, identifier) => {
let url = `/v1/serverOverview?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/serverOverview?server=${identifier}`;
if (staticSite) url = `/data/serverOverview-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchOnlineActivityOverview = async (timestamp, identifier) => {
let url = `/v1/onlineOverview?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/onlineOverview?server=${identifier}`;
if (staticSite) url = `/data/onlineOverview-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchPlayerbaseOverview = async (timestamp, identifier) => {
let url = `/v1/playerbaseOverview?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/playerbaseOverview?server=${identifier}`;
if (staticSite) url = `/data/playerbaseOverview-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchSessionOverview = async (timestamp, identifier) => {
let url = `/v1/sessionsOverview?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/sessionsOverview?server=${identifier}`;
if (staticSite) url = `/data/sessionsOverview-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchPvpPve = async (timestamp, identifier) => {
let url = `/v1/playerVersus?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/playerVersus?server=${identifier}`;
if (staticSite) url = `/data/playerVersus-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchPerformanceOverview = async (timestamp, identifier) => {
let url = `/v1/performanceOverview?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/performanceOverview?server=${identifier}`;
if (staticSite) url = `/data/performanceOverview-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchExtensionData = async (timestamp, identifier) => {
let url = `/v1/extensionData?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/extensionData?server=${identifier}`;
if (staticSite) url = `/data/extensionData-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchSessions = async (timestamp, identifier) => {
@ -57,21 +57,21 @@ export const fetchSessions = async (timestamp, identifier) => {
}
const fetchSessionsServer = async (timestamp, identifier) => {
let url = `/v1/sessions?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/sessions?server=${identifier}`;
if (staticSite) url = `/data/sessions-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
const fetchSessionsNetwork = async (timestamp) => {
let url = `/v1/sessions?timestamp=${timestamp}`;
let url = `/v1/sessions`;
if (staticSite) url = `/data/sessions.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchKills = async (timestamp, identifier) => {
let url = `/v1/kills?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/kills?server=${identifier}`;
if (staticSite) url = `/data/kills-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchPlayers = async (timestamp, identifier) => {
@ -82,21 +82,21 @@ export const fetchPlayers = async (timestamp, identifier) => {
}
}
const fetchPlayersServer = async (timestamp, identifier) => {
let url = `/v1/players?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/players?server=${identifier}`;
if (staticSite) url = `/data/players-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
const fetchPlayersNetwork = async (timestamp) => {
let url = `/v1/players?timestamp=${timestamp}`;
let url = `/v1/players`;
if (staticSite) url = `/data/players.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchPingTable = async (timestamp, identifier) => {
let url = `/v1/pingTable?server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/pingTable?server=${identifier}`;
if (staticSite) url = `/data/pingTable-${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchPlayersOnlineGraph = async (timestamp, identifier) => {
@ -108,15 +108,15 @@ export const fetchPlayersOnlineGraph = async (timestamp, identifier) => {
}
const fetchPlayersOnlineGraphServer = async (timestamp, identifier) => {
let url = `/v1/graph?type=playersOnline&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=playersOnline&server=${identifier}`;
if (staticSite) url = `/data/graph-playersOnline_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
const fetchPlayersOnlineGraphNetwork = async (timestamp) => {
let url = `/v1/graph?type=playersOnline&timestamp=${timestamp}`;
let url = `/v1/graph?type=playersOnline`;
if (staticSite) url = `/data/graph-playersOnline.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchPlayerbaseDevelopmentGraph = async (timestamp, identifier) => {
@ -128,15 +128,15 @@ export const fetchPlayerbaseDevelopmentGraph = async (timestamp, identifier) =>
}
const fetchPlayerbaseDevelopmentGraphServer = async (timestamp, identifier) => {
let url = `/v1/graph?type=activity&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=activity&server=${identifier}`;
if (staticSite) url = `/data/graph-activity_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
const fetchPlayerbaseDevelopmentGraphNetwork = async (timestamp) => {
let url = `/v1/graph?type=activity&timestamp=${timestamp}`;
let url = `/v1/graph?type=activity`;
if (staticSite) url = `/data/graph-activity.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchDayByDayGraph = async (timestamp, identifier) => {
@ -148,15 +148,15 @@ export const fetchDayByDayGraph = async (timestamp, identifier) => {
}
const fetchDayByDayGraphServer = async (timestamp, identifier) => {
let url = `/v1/graph?type=uniqueAndNew&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=uniqueAndNew&server=${identifier}`;
if (staticSite) url = `/data/graph-uniqueAndNew_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
const fetchDayByDayGraphNetwork = async (timestamp) => {
let url = `/v1/graph?type=uniqueAndNew&timestamp=${timestamp}`;
let url = `/v1/graph?type=uniqueAndNew`;
if (staticSite) url = `/data/graph-uniqueAndNew.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchHourByHourGraph = async (timestamp, identifier) => {
@ -168,33 +168,33 @@ export const fetchHourByHourGraph = async (timestamp, identifier) => {
}
const fetchHourByHourGraphServer = async (timestamp, identifier) => {
let url = `/v1/graph?type=hourlyUniqueAndNew&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=hourlyUniqueAndNew&server=${identifier}`;
if (staticSite) url = `/data/graph-hourlyUniqueAndNew_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
const fetchHourByHourGraphNetwork = async (timestamp) => {
let url = `/v1/graph?type=hourlyUniqueAndNew&timestamp=${timestamp}`;
let url = `/v1/graph?type=hourlyUniqueAndNew`;
if (staticSite) url = `/data/graph-hourlyUniqueAndNew.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchServerCalendarGraph = async (timestamp, identifier) => {
let url = `/v1/graph?type=serverCalendar&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=serverCalendar&server=${identifier}`;
if (staticSite) url = `/data/graph-serverCalendar_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchPunchCardGraph = async (timestamp, identifier) => {
let url = `/v1/graph?type=punchCard&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=punchCard&server=${identifier}`;
if (staticSite) url = `/data/graph-punchCard_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchWorldPie = async (timestamp, identifier) => {
let url = `/v1/graph?type=worldPie&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=worldPie&server=${identifier}`;
if (staticSite) url = `/data/graph-worldPie_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchGeolocations = async (timestamp, identifier) => {
@ -206,27 +206,27 @@ export const fetchGeolocations = async (timestamp, identifier) => {
}
const fetchGeolocationsServer = async (timestamp, identifier) => {
let url = `/v1/graph?type=geolocation&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=geolocation&server=${identifier}`;
if (staticSite) url = `/data/graph-geolocation_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
const fetchGeolocationsNetwork = async (timestamp) => {
let url = `/v1/graph?type=geolocation&timestamp=${timestamp}`;
let url = `/v1/graph?type=geolocation`;
if (staticSite) url = `/data/graph-geolocation.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchOptimizedPerformance = async (timestamp, identifier, after) => {
let url = `/v1/graph?type=optimizedPerformance&server=${identifier}&timestamp=${timestamp}&after=${after}`;
export const fetchOptimizedPerformance = async (timestamp, identifier) => {
let url = `/v1/graph?type=optimizedPerformance&server=${identifier}`;
if (staticSite) url = `/data/graph-optimizedPerformance_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchPingGraph = async (timestamp, identifier) => {
let url = `/v1/graph?type=aggregatedPing&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=aggregatedPing&server=${identifier}`;
if (staticSite) url = `/data/graph-aggregatedPing_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchJoinAddressPie = async (timestamp, identifier) => {
@ -238,15 +238,15 @@ export const fetchJoinAddressPie = async (timestamp, identifier) => {
}
const fetchJoinAddressPieServer = async (timestamp, identifier) => {
let url = `/v1/graph?type=joinAddressPie&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=joinAddressPie&server=${identifier}`;
if (staticSite) url = `/data/graph-joinAddressPie_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
const fetchJoinAddressPieNetwork = async (timestamp) => {
let url = `/v1/graph?type=joinAddressPie&timestamp=${timestamp}`;
let url = `/v1/graph?type=joinAddressPie`;
if (staticSite) url = `/data/graph-joinAddressPie.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
export const fetchJoinAddressByDay = async (timestamp, identifier) => {
@ -258,13 +258,13 @@ export const fetchJoinAddressByDay = async (timestamp, identifier) => {
}
const fetchJoinAddressByDayServer = async (timestamp, identifier) => {
let url = `/v1/graph?type=joinAddressByDay&server=${identifier}&timestamp=${timestamp}`;
let url = `/v1/graph?type=joinAddressByDay&server=${identifier}`;
if (staticSite) url = `/data/graph-joinAddressByDay_${identifier}.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}
const fetchJoinAddressByDayNetwork = async (timestamp) => {
let url = `/v1/graph?type=joinAddressByDay&timestamp=${timestamp}`;
let url = `/v1/graph?type=joinAddressByDay`;
if (staticSite) url = `/data/graph-joinAddressByDay.json`;
return doGetRequest(url);
return doGetRequest(url, timestamp);
}

View File

@ -52,13 +52,11 @@ const NetworkPerformance = () => {
timestamp_f: ''
}
const time = new Date().getTime();
const monthMs = 2592000000;
const after = time - monthMs;
for (const index of visualizedServers) {
const server = serverOptions[index];
const {data, error} = await fetchOptimizedPerformance(time, encodeURIComponent(server.serverUUID), after);
const {data, error} = await fetchOptimizedPerformance(time, encodeURIComponent(server.serverUUID));
if (data) {
loaded.servers.push(server);
const values = data.values;