Skip to content

Commit 88b4191

Browse files
authored
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
1 parent 0ddda27 commit 88b4191

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+700
-242
lines changed

Plan/api/src/main/java/com/djrapitops/plan/delivery/web/resource/WebResource.java

Lines changed: 87 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,10 @@
1616
*/
1717
package com.djrapitops.plan.delivery.web.resource;
1818

19-
import java.io.ByteArrayInputStream;
20-
import java.io.ByteArrayOutputStream;
21-
import java.io.IOException;
22-
import java.io.InputStream;
19+
import java.io.*;
2320
import java.nio.charset.StandardCharsets;
21+
import java.util.Optional;
22+
import java.util.function.Supplier;
2423

2524
/**
2625
* Represents a customizable resource.
@@ -61,19 +60,55 @@ static WebResource create(String utf8String) {
6160
* @throws IOException If the stream can not be read.
6261
*/
6362
static WebResource create(InputStream in) throws IOException {
63+
return create(in, null);
64+
}
65+
66+
/**
67+
* Creates a new WebResource from an InputStream.
68+
*
69+
* @param in InputStream for the resource, closed after inside the method.
70+
* @param lastModified Epoch millisecond the resource was last modified
71+
* @return WebResource.
72+
* @throws IOException If the stream can not be read.
73+
*/
74+
static WebResource create(InputStream in, Long lastModified) throws IOException {
6475
try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
6576
int read;
6677
byte[] bytes = new byte[1024];
6778
while ((read = in.read(bytes)) != -1) {
6879
out.write(bytes, 0, read);
6980
}
7081

71-
return new ByteResource(out.toByteArray());
82+
return new ByteResource(out.toByteArray(), lastModified);
7283
} finally {
7384
in.close();
7485
}
7586
}
7687

88+
/**
89+
* Create a lazy WebResource that only reads contents if necessary.
90+
*
91+
* @param in Supplier for InputStream, a lazy method that reads input when necessary.
92+
* @param lastModified Last modified date for the resource.
93+
* @return WebResource.
94+
*/
95+
static WebResource create(Supplier<InputStream> in, Long lastModified) {
96+
return new LazyWebResource(in, () -> {
97+
try (ByteArrayOutputStream out = new ByteArrayOutputStream();
98+
InputStream input = in.get()) {
99+
int read;
100+
byte[] bytes = new byte[1024];
101+
while ((read = input.read(bytes)) != -1) {
102+
out.write(bytes, 0, read);
103+
}
104+
105+
return out.toByteArray();
106+
} catch (IOException e) {
107+
throw new UncheckedIOException(e);
108+
}
109+
}, lastModified);
110+
}
111+
77112
byte[] asBytes();
78113

79114
/**
@@ -85,11 +120,21 @@ static WebResource create(InputStream in) throws IOException {
85120

86121
InputStream asStream();
87122

123+
default Optional<Long> getLastModified() {
124+
return Optional.empty();
125+
}
126+
88127
final class ByteResource implements WebResource {
89128
private final byte[] content;
129+
private final Long lastModified;
90130

91131
public ByteResource(byte[] content) {
132+
this(content, null);
133+
}
134+
135+
public ByteResource(byte[] content, Long lastModified) {
92136
this.content = content;
137+
this.lastModified = lastModified;
93138
}
94139

95140
@Override
@@ -106,5 +151,42 @@ public String asString() {
106151
public InputStream asStream() {
107152
return new ByteArrayInputStream(content);
108153
}
154+
155+
@Override
156+
public Optional<Long> getLastModified() {
157+
return Optional.ofNullable(lastModified);
158+
}
159+
}
160+
161+
final class LazyWebResource implements WebResource {
162+
private final Supplier<InputStream> inputStreamSupplier;
163+
private final Supplier<byte[]> contentSupplier;
164+
private final Long lastModified;
165+
166+
public LazyWebResource(Supplier<InputStream> inputStreamSupplier, Supplier<byte[]> contentSupplier, Long lastModified) {
167+
this.inputStreamSupplier = inputStreamSupplier;
168+
this.contentSupplier = contentSupplier;
169+
this.lastModified = lastModified;
170+
}
171+
172+
@Override
173+
public byte[] asBytes() {
174+
return contentSupplier.get();
175+
}
176+
177+
@Override
178+
public String asString() {
179+
return new String(asBytes(), StandardCharsets.UTF_8);
180+
}
181+
182+
@Override
183+
public InputStream asStream() {
184+
return inputStreamSupplier.get();
185+
}
186+
187+
@Override
188+
public Optional<Long> getLastModified() {
189+
return Optional.ofNullable(lastModified);
190+
}
109191
}
110192
}

Plan/bukkit/src/main/java/com/djrapitops/plan/addons/placeholderapi/PlanPlaceholderExtension.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,9 @@ public String onRequest(OfflinePlayer player, @Untrusted String params) {
9696
if ("Server thread".equalsIgnoreCase(Thread.currentThread().getName())) {
9797
return getCached(params, uuid);
9898
}
99-
return getPlaceholderValue(params, uuid);
99+
100+
return Optional.ofNullable(getCached(params, uuid))
101+
.orElseGet(() -> getPlaceholderValue(params, uuid));
100102
} catch (IllegalStateException e) {
101103
if ("zip file closed".equals(e.getMessage())) {
102104
return null; // Plan is disabled.

Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/json/PlayerJSONCreator.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import com.djrapitops.plan.storage.database.Database;
4747
import com.djrapitops.plan.storage.database.queries.containers.PlayerContainerQuery;
4848
import com.djrapitops.plan.storage.database.queries.objects.ServerQueries;
49+
import com.djrapitops.plan.storage.database.queries.objects.SessionQueries;
4950
import com.djrapitops.plan.utilities.comparators.DateHolderRecentComparator;
5051
import com.djrapitops.plan.utilities.java.Lists;
5152
import com.djrapitops.plan.utilities.java.Maps;
@@ -91,6 +92,10 @@ public PlayerJSONCreator(
9192
this.graphs = graphs;
9293
}
9394

95+
public long getLastSeen(UUID playerUUID) {
96+
return dbSystem.getDatabase().query(SessionQueries.lastSeen(playerUUID));
97+
}
98+
9499
public Map<String, Object> createJSONAsMap(UUID playerUUID) {
95100
Database db = dbSystem.getDatabase();
96101

@@ -226,6 +231,7 @@ private Map<String, Object> createInfoJSONMap(PlayerContainer player, Map<Server
226231
info.put("best_ping", bestPing != -1.0 ? bestPing + " ms" : unavailable);
227232
info.put("registered", player.getValue(PlayerKeys.REGISTERED).map(year).orElse("-"));
228233
info.put("last_seen", player.getValue(PlayerKeys.LAST_SEEN).map(year).orElse("-"));
234+
info.put("last_seen_raw_value", player.getValue(PlayerKeys.LAST_SEEN).orElse(0L));
229235

230236
return info;
231237
}

Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/LoginPage.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.djrapitops.plan.delivery.rendering.pages;
1818

1919
import com.djrapitops.plan.delivery.formatting.PlaceholderReplacer;
20+
import com.djrapitops.plan.delivery.web.resource.WebResource;
2021
import com.djrapitops.plan.identification.ServerInfo;
2122
import com.djrapitops.plan.settings.locale.Locale;
2223
import com.djrapitops.plan.settings.theme.Theme;
@@ -30,15 +31,15 @@
3031
*/
3132
public class LoginPage implements Page {
3233

33-
private final String template;
34+
private final WebResource template;
3435
private final ServerInfo serverInfo;
3536
private final Locale locale;
3637
private final Theme theme;
3738

3839
private final VersionChecker versionChecker;
3940

4041
LoginPage(
41-
String htmlTemplate,
42+
WebResource htmlTemplate,
4243
ServerInfo serverInfo,
4344
Locale locale,
4445
Theme theme,
@@ -51,12 +52,17 @@ public class LoginPage implements Page {
5152
this.versionChecker = versionChecker;
5253
}
5354

55+
@Override
56+
public long lastModified() {
57+
return template.getLastModified().orElseGet(System::currentTimeMillis);
58+
}
59+
5460
@Override
5561
public String toHtml() {
5662
PlaceholderReplacer placeholders = new PlaceholderReplacer();
5763
placeholders.put("command", getCommand());
5864
placeholders.put("version", versionChecker.getCurrentVersion());
59-
return UnaryChain.of(template)
65+
return UnaryChain.of(template.asString())
6066
.chain(theme::replaceThemeColors)
6167
.chain(placeholders::apply)
6268
.chain(locale::replaceLanguageInHtml)

Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/Page.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,8 @@
2323
*/
2424
public interface Page {
2525
String toHtml();
26+
27+
default long lastModified() {
28+
return System.currentTimeMillis();
29+
}
2630
}

Plan/common/src/main/java/com/djrapitops/plan/delivery/rendering/pages/PageFactory.java

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import com.djrapitops.plan.delivery.rendering.html.icon.Icon;
2323
import com.djrapitops.plan.delivery.web.ResourceService;
2424
import com.djrapitops.plan.delivery.web.resolver.exception.NotFoundException;
25+
import com.djrapitops.plan.delivery.web.resource.WebResource;
2526
import com.djrapitops.plan.delivery.webserver.Addresses;
2627
import com.djrapitops.plan.delivery.webserver.cache.JSONStorage;
2728
import com.djrapitops.plan.extension.implementation.results.ExtensionData;
@@ -41,7 +42,6 @@
4142
import com.djrapitops.plan.utilities.dev.Untrusted;
4243
import com.djrapitops.plan.version.VersionChecker;
4344
import dagger.Lazy;
44-
import org.apache.commons.lang3.StringUtils;
4545

4646
import javax.inject.Inject;
4747
import javax.inject.Singleton;
@@ -101,15 +101,12 @@ public Page playersPage() throws IOException {
101101
return reactPage();
102102
}
103103

104-
return new PlayersPage(getResource("players.html"), versionChecker.get(),
104+
return new PlayersPage(getResourceAsString("players.html"), versionChecker.get(),
105105
config.get(), theme.get(), serverInfo.get());
106106
}
107107

108108
public Page reactPage() throws IOException {
109-
String reactHtml = StringUtils.replace(
110-
getResource("index.html"),
111-
"/static", getBasePath() + "/static");
112-
return () -> reactHtml;
109+
return new ReactPage(getBasePath(), getResource("index.html"));
113110
}
114111

115112
private String getBasePath() {
@@ -135,7 +132,7 @@ public Page serverPage(ServerUUID serverUUID) throws IOException {
135132
}
136133

137134
return new ServerPage(
138-
getResource("server.html"),
135+
getResourceAsString("server.html"),
139136
server,
140137
config.get(),
141138
theme.get(),
@@ -158,7 +155,7 @@ public Page playerPage(UUID playerUUID) throws IOException {
158155
}
159156

160157
return new PlayerPage(
161-
getResource("player.html"), player,
158+
getResourceAsString("player.html"), player,
162159
versionChecker.get(),
163160
config.get(),
164161
this,
@@ -207,7 +204,7 @@ public Page networkPage() throws IOException {
207204
return reactPage();
208205
}
209206

210-
return new NetworkPage(getResource("network.html"),
207+
return new NetworkPage(getResourceAsString("network.html"),
211208
dbSystem.get(),
212209
versionChecker.get(),
213210
config.get(),
@@ -223,7 +220,7 @@ public Page networkPage() throws IOException {
223220
public Page internalErrorPage(String message, @Untrusted Throwable error) {
224221
try {
225222
return new InternalErrorPage(
226-
getResource("error.html"), message, error,
223+
getResourceAsString("error.html"), message, error,
227224
versionChecker.get());
228225
} catch (IOException noParse) {
229226
return () -> "Error occurred: " + error.toString() +
@@ -234,20 +231,24 @@ public Page internalErrorPage(String message, @Untrusted Throwable error) {
234231

235232
public Page errorPage(String title, String error) throws IOException {
236233
return new ErrorMessagePage(
237-
getResource("error.html"), title, error,
234+
getResourceAsString("error.html"), title, error,
238235
versionChecker.get(), theme.get());
239236
}
240237

241238
public Page errorPage(Icon icon, String title, String error) throws IOException {
242239
return new ErrorMessagePage(
243-
getResource("error.html"), icon, title, error, theme.get(), versionChecker.get());
240+
getResourceAsString("error.html"), icon, title, error, theme.get(), versionChecker.get());
244241
}
245242

246-
public String getResource(String name) throws IOException {
243+
public String getResourceAsString(String name) throws IOException {
244+
return getResource(name).asString();
245+
}
246+
247+
public WebResource getResource(String name) throws IOException {
247248
try {
248249
return ResourceService.getInstance().getResource("Plan", name,
249250
() -> files.get().getResourceFromJar("web/" + name).asWebResource()
250-
).asString();
251+
);
251252
} catch (UncheckedIOException readFail) {
252253
throw readFail.getCause();
253254
}
@@ -274,7 +275,7 @@ public Page queryPage() throws IOException {
274275
return reactPage();
275276
}
276277
return new QueryPage(
277-
getResource("query.html"),
278+
getResourceAsString("query.html"),
278279
locale.get(), theme.get(), versionChecker.get()
279280
);
280281
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* This file is part of Player Analytics (Plan).
3+
*
4+
* Plan is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU Lesser General Public License v3 as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* Plan is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public License
15+
* along with Plan. If not, see <https://www.gnu.org/licenses/>.
16+
*/
17+
package com.djrapitops.plan.delivery.rendering.pages;
18+
19+
import com.djrapitops.plan.delivery.web.resource.WebResource;
20+
import org.apache.commons.lang3.StringUtils;
21+
22+
/**
23+
* Represents React index.html.
24+
*
25+
* @author AuroraLS3
26+
*/
27+
public class ReactPage implements Page {
28+
29+
private final String basePath;
30+
private final WebResource reactHtml;
31+
32+
public ReactPage(String basePath, WebResource reactHtml) {
33+
this.basePath = basePath;
34+
this.reactHtml = reactHtml;
35+
}
36+
37+
@Override
38+
public String toHtml() {
39+
return StringUtils.replace(
40+
reactHtml.asString(),
41+
"/static", basePath + "/static");
42+
}
43+
44+
@Override
45+
public long lastModified() {
46+
return reactHtml.getLastModified().orElseGet(System::currentTimeMillis);
47+
}
48+
}

0 commit comments

Comments
 (0)