Merge remote-tracking branch 'origin/master'
# Conflicts: # .idea/workspace.xml # flash-extensions/flash-ext-view-jte/jte-classes/gg/jte/generated/ondemand/pages/JtehomeGenerated.class # flash-extensions/flash-ext-view-jte/src/main/java/dev/relism/flash/ext/view/jte/JteExtension.java
This commit is contained in:
+205
@@ -0,0 +1,205 @@
|
||||
package dev.relism.ext.view.jte;
|
||||
|
||||
import dev.relism.http.HttpStatus;
|
||||
import dev.relism.models.Request;
|
||||
import dev.relism.models.Response;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.zip.CRC32;
|
||||
|
||||
final class JteStaticServing {
|
||||
private static final Logger log = LoggerFactory.getLogger(JteStaticServing.class);
|
||||
private static final String STATIC_ROOT = "static";
|
||||
private static final Pattern VERSIONED_FILE = Pattern.compile(".*\\.[a-fA-F0-9]{8,}\\..*");
|
||||
|
||||
private static final Map<String, String> MIME = Map.ofEntries(
|
||||
Map.entry("css", "text/css"),
|
||||
Map.entry("js", "text/javascript"),
|
||||
Map.entry("mjs", "text/javascript"),
|
||||
Map.entry("html", "text/html"),
|
||||
Map.entry("txt", "text/plain"),
|
||||
Map.entry("json", "application/json"),
|
||||
Map.entry("xml", "application/xml"),
|
||||
Map.entry("wasm", "application/wasm"),
|
||||
Map.entry("svg", "image/svg+xml"),
|
||||
Map.entry("png", "image/png"),
|
||||
Map.entry("jpg", "image/jpeg"),
|
||||
Map.entry("jpeg", "image/jpeg"),
|
||||
Map.entry("gif", "image/gif"),
|
||||
Map.entry("webp", "image/webp"),
|
||||
Map.entry("ico", "image/x-icon"),
|
||||
Map.entry("avif", "image/avif"),
|
||||
Map.entry("woff", "font/woff"),
|
||||
Map.entry("woff2", "font/woff2"),
|
||||
Map.entry("ttf", "font/ttf"),
|
||||
Map.entry("otf", "font/otf"),
|
||||
Map.entry("map", "application/json"),
|
||||
Map.entry("pdf", "application/pdf"),
|
||||
Map.entry("zip", "application/zip")
|
||||
);
|
||||
|
||||
private final String prefix;
|
||||
private final long largeThreshold;
|
||||
|
||||
private JteStaticServing(JteSettings settings) {
|
||||
this.prefix = settings.staticPrefix();
|
||||
this.largeThreshold = settings.largeFileThresholdBytes();
|
||||
}
|
||||
|
||||
static JteStaticServing load(JteSettings settings) {
|
||||
if (JteStaticServing.class.getClassLoader().getResource(STATIC_ROOT) == null) {
|
||||
log.warn("flash-ext-view-jte static serving enabled but '/static' classpath root is missing; static routes disabled");
|
||||
return null;
|
||||
}
|
||||
return new JteStaticServing(settings);
|
||||
}
|
||||
|
||||
boolean serve(Request req, Response res, boolean headOnly) {
|
||||
String requestPath = req.path();
|
||||
if (!requestPath.startsWith(prefix)) return false;
|
||||
|
||||
String relative = requestPath.substring(prefix.length());
|
||||
if (relative.isEmpty() || "/".equals(relative)) return false;
|
||||
if (!relative.startsWith("/") || relative.contains("..") || relative.contains("\\")) return false;
|
||||
|
||||
String routePath = relative;
|
||||
byte[] raw = read(routePath);
|
||||
if (raw == null) return false;
|
||||
|
||||
String etag = etag(raw);
|
||||
String inm = req.header("If-None-Match");
|
||||
if (etag.equals(inm)) {
|
||||
res.status(HttpStatus.NOT_MODIFIED);
|
||||
res.header("ETag", etag);
|
||||
res.header("Vary", "Accept-Encoding");
|
||||
res.header("Cache-Control", cacheControl(isVersioned(routePath, req)));
|
||||
return true;
|
||||
}
|
||||
|
||||
boolean gzipAccepted = accepts(req, "gzip");
|
||||
byte[] gz = gzipAccepted ? read(routePath + ".gz") : null;
|
||||
byte[] selected = gz != null ? gz : raw;
|
||||
|
||||
res.type(mime(routePath));
|
||||
res.header("ETag", etag);
|
||||
res.header("Cache-Control", cacheControl(isVersioned(routePath, req)));
|
||||
res.header("Vary", "Accept-Encoding");
|
||||
if (gz != null) res.header("Content-Encoding", "gzip");
|
||||
|
||||
if (headOnly) {
|
||||
res.body(new byte[0]);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (selected.length >= largeThreshold) {
|
||||
String range = req.header("Range");
|
||||
if (gz == null && range != null) {
|
||||
RangeSlice slice = parseRange(range, selected.length);
|
||||
if (slice == null) {
|
||||
res.status(416);
|
||||
res.header("Content-Range", "bytes */" + selected.length);
|
||||
res.body(new byte[0]);
|
||||
return true;
|
||||
}
|
||||
res.status(HttpStatus.PARTIAL_CONTENT);
|
||||
res.header("Accept-Ranges", "bytes");
|
||||
res.header("Content-Range", "bytes " + slice.start + "-" + slice.end + "/" + selected.length);
|
||||
res.stream(new ByteArrayInputStream(selected, slice.start, slice.length), slice.length);
|
||||
return true;
|
||||
}
|
||||
res.stream(new ByteArrayInputStream(selected), selected.length);
|
||||
return true;
|
||||
}
|
||||
|
||||
res.body(selected);
|
||||
return true;
|
||||
}
|
||||
|
||||
static String etagFor(byte[] raw) { return etag(raw); }
|
||||
static String mimeFor(String routePath) { return mime(routePath); }
|
||||
static boolean versionedPath(String routePath) { return VERSIONED_FILE.matcher(routePath).matches(); }
|
||||
static int rangeLength(String rangeHeader, int size) {
|
||||
RangeSlice slice = parseRange(rangeHeader, size);
|
||||
return slice == null ? -1 : slice.length;
|
||||
}
|
||||
|
||||
private static String cacheControl(boolean versioned) {
|
||||
return versioned ? "public, max-age=31536000, immutable" : "no-cache";
|
||||
}
|
||||
|
||||
private static boolean accepts(Request req, String encoding) {
|
||||
String value = req.header("Accept-Encoding");
|
||||
return value != null && value.contains(encoding);
|
||||
}
|
||||
|
||||
private static byte[] read(String routePath) {
|
||||
String cp = STATIC_ROOT + routePath;
|
||||
try (InputStream in = resource(cp.startsWith("/") ? cp.substring(1) : cp)) {
|
||||
return in == null ? null : in.readAllBytes();
|
||||
} catch (Exception e) {
|
||||
throw new IllegalStateException("Failed to read static asset: " + routePath, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static InputStream resource(String path) {
|
||||
return JteStaticServing.class.getClassLoader().getResourceAsStream(path);
|
||||
}
|
||||
|
||||
private static String etag(byte[] raw) {
|
||||
CRC32 crc = new CRC32();
|
||||
crc.update(raw, 0, raw.length);
|
||||
return '"' + Long.toHexString(crc.getValue()) + '-' + raw.length + '"';
|
||||
}
|
||||
|
||||
private static String mime(String routePath) {
|
||||
int dot = routePath.lastIndexOf('.');
|
||||
if (dot < 0 || dot == routePath.length() - 1) return "application/octet-stream";
|
||||
String ext = routePath.substring(dot + 1).toLowerCase();
|
||||
return MIME.getOrDefault(ext, "application/octet-stream");
|
||||
}
|
||||
|
||||
private static boolean isVersioned(String routePath, Request req) {
|
||||
if (VERSIONED_FILE.matcher(routePath).matches()) return true;
|
||||
return req.query("v") != null || req.query("hash") != null;
|
||||
}
|
||||
|
||||
private static RangeSlice parseRange(String rangeHeader, int size) {
|
||||
if (!rangeHeader.startsWith("bytes=")) return null;
|
||||
String value = rangeHeader.substring(6).trim();
|
||||
int comma = value.indexOf(',');
|
||||
if (comma >= 0) value = value.substring(0, comma).trim();
|
||||
int dash = value.indexOf('-');
|
||||
if (dash < 0) return null;
|
||||
|
||||
String startPart = value.substring(0, dash).trim();
|
||||
String endPart = value.substring(dash + 1).trim();
|
||||
|
||||
try {
|
||||
int start;
|
||||
int end;
|
||||
if (startPart.isEmpty()) {
|
||||
int suffixLen = Integer.parseInt(endPart);
|
||||
if (suffixLen <= 0) return null;
|
||||
if (suffixLen > size) suffixLen = size;
|
||||
start = size - suffixLen;
|
||||
end = size - 1;
|
||||
} else {
|
||||
start = Integer.parseInt(startPart);
|
||||
if (start < 0 || start >= size) return null;
|
||||
end = endPart.isEmpty() ? size - 1 : Integer.parseInt(endPart);
|
||||
if (end < start) return null;
|
||||
if (end >= size) end = size - 1;
|
||||
}
|
||||
return new RangeSlice(start, end, end - start + 1);
|
||||
} catch (NumberFormatException ex) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private record RangeSlice(int start, int end, int length) {}
|
||||
}
|
||||
+125
@@ -8,6 +8,8 @@ import java.nio.file.Path;
|
||||
* jte runtime settings with Flash-sensitive defaults.
|
||||
*/
|
||||
public final class JteSettings {
|
||||
private static final long DEFAULT_LARGE_FILE_THRESHOLD_BYTES = 64L * 1024L;
|
||||
|
||||
private final String templateRoot;
|
||||
private final gg.jte.ContentType contentType;
|
||||
private final boolean developmentMode;
|
||||
@@ -15,6 +17,14 @@ public final class JteSettings {
|
||||
private final boolean binaryStaticContent;
|
||||
private final Path dynamicClassesPath;
|
||||
private final Path precompiledClassesPath;
|
||||
private final boolean serveStatics;
|
||||
private final String staticPrefix;
|
||||
private final long largeFileThresholdBytes;
|
||||
private final boolean staticCorsEnabled;
|
||||
private final String staticCorsAllowOrigin;
|
||||
private final String staticCorsAllowMethods;
|
||||
private final String staticCorsAllowHeaders;
|
||||
private final int staticCorsMaxAge;
|
||||
|
||||
private JteSettings(Builder b) {
|
||||
this.templateRoot = b.templateRoot;
|
||||
@@ -24,6 +34,14 @@ public final class JteSettings {
|
||||
this.binaryStaticContent = b.binaryStaticContent;
|
||||
this.dynamicClassesPath = b.dynamicClassesPath;
|
||||
this.precompiledClassesPath = b.precompiledClassesPath;
|
||||
this.serveStatics = b.serveStatics;
|
||||
this.staticPrefix = b.staticPrefix;
|
||||
this.largeFileThresholdBytes = b.largeFileThresholdBytes;
|
||||
this.staticCorsEnabled = b.staticCorsEnabled;
|
||||
this.staticCorsAllowOrigin = b.staticCorsAllowOrigin;
|
||||
this.staticCorsAllowMethods = b.staticCorsAllowMethods;
|
||||
this.staticCorsAllowHeaders = b.staticCorsAllowHeaders;
|
||||
this.staticCorsMaxAge = b.staticCorsMaxAge;
|
||||
}
|
||||
|
||||
public static Builder builder() {
|
||||
@@ -37,6 +55,34 @@ public final class JteSettings {
|
||||
boolean binaryStaticContent() { return binaryStaticContent; }
|
||||
Path dynamicClassesPath() { return dynamicClassesPath; }
|
||||
Path precompiledClassesPath() { return precompiledClassesPath; }
|
||||
boolean serveStatics() { return serveStatics; }
|
||||
String staticPrefix() { return staticPrefix; }
|
||||
long largeFileThresholdBytes() { return largeFileThresholdBytes; }
|
||||
boolean staticCorsEnabled() { return staticCorsEnabled; }
|
||||
String staticCorsAllowOrigin() { return staticCorsAllowOrigin; }
|
||||
String staticCorsAllowMethods() { return staticCorsAllowMethods; }
|
||||
String staticCorsAllowHeaders() { return staticCorsAllowHeaders; }
|
||||
int staticCorsMaxAge() { return staticCorsMaxAge; }
|
||||
|
||||
Builder toBuilder() {
|
||||
Builder b = new Builder();
|
||||
b.templateRoot = templateRoot;
|
||||
b.contentType = contentType;
|
||||
b.developmentMode = developmentMode;
|
||||
b.usePrecompiled = usePrecompiled;
|
||||
b.binaryStaticContent = binaryStaticContent;
|
||||
b.dynamicClassesPath = dynamicClassesPath;
|
||||
b.precompiledClassesPath = precompiledClassesPath;
|
||||
b.serveStatics = serveStatics;
|
||||
b.staticPrefix = staticPrefix;
|
||||
b.largeFileThresholdBytes = largeFileThresholdBytes;
|
||||
b.staticCorsEnabled = staticCorsEnabled;
|
||||
b.staticCorsAllowOrigin = staticCorsAllowOrigin;
|
||||
b.staticCorsAllowMethods = staticCorsAllowMethods;
|
||||
b.staticCorsAllowHeaders = staticCorsAllowHeaders;
|
||||
b.staticCorsMaxAge = staticCorsMaxAge;
|
||||
return b;
|
||||
}
|
||||
|
||||
public static final class Builder {
|
||||
private String templateRoot = "/templates";
|
||||
@@ -46,6 +92,14 @@ public final class JteSettings {
|
||||
private boolean binaryStaticContent;
|
||||
private Path dynamicClassesPath = Path.of("jte-classes");
|
||||
private Path precompiledClassesPath = Path.of("jte-classes");
|
||||
private boolean serveStatics = true;
|
||||
private String staticPrefix = "/static";
|
||||
private long largeFileThresholdBytes = DEFAULT_LARGE_FILE_THRESHOLD_BYTES;
|
||||
private boolean staticCorsEnabled;
|
||||
private String staticCorsAllowOrigin = "*";
|
||||
private String staticCorsAllowMethods = "GET,HEAD,OPTIONS";
|
||||
private String staticCorsAllowHeaders = "*";
|
||||
private int staticCorsMaxAge = 3600;
|
||||
|
||||
public Builder templateRoot(String templateRoot) {
|
||||
String root = templateRoot == null ? "" : templateRoot.trim();
|
||||
@@ -84,6 +138,52 @@ public final class JteSettings {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder serveStatics(boolean serveStatics) {
|
||||
this.serveStatics = serveStatics;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder staticPrefix(String staticPrefix) {
|
||||
this.staticPrefix = normalizeStaticPrefix(staticPrefix);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder largeFileThresholdBytes(long largeFileThresholdBytes) {
|
||||
if (largeFileThresholdBytes <= 0) {
|
||||
throw new IllegalArgumentException("largeFileThresholdBytes must be > 0");
|
||||
}
|
||||
this.largeFileThresholdBytes = largeFileThresholdBytes;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder enableStaticCors(boolean enabled) {
|
||||
this.staticCorsEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder staticCorsAllowOrigin(String allowOrigin) {
|
||||
this.staticCorsAllowOrigin = requireCorsValue(allowOrigin, "staticCorsAllowOrigin");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder staticCorsAllowMethods(String allowMethods) {
|
||||
this.staticCorsAllowMethods = requireCorsValue(allowMethods, "staticCorsAllowMethods");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder staticCorsAllowHeaders(String allowHeaders) {
|
||||
this.staticCorsAllowHeaders = requireCorsValue(allowHeaders, "staticCorsAllowHeaders");
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder staticCorsMaxAge(int maxAgeSeconds) {
|
||||
if (maxAgeSeconds < 0) {
|
||||
throw new IllegalArgumentException("staticCorsMaxAge must be >= 0");
|
||||
}
|
||||
this.staticCorsMaxAge = maxAgeSeconds;
|
||||
return this;
|
||||
}
|
||||
|
||||
public JteSettings build() {
|
||||
boolean resolvedDev = developmentMode != null ? developmentMode : Flash.DEV;
|
||||
boolean resolvedPrecompiled = usePrecompiled != null ? usePrecompiled : !resolvedDev;
|
||||
@@ -95,6 +195,14 @@ public final class JteSettings {
|
||||
resolved.binaryStaticContent = this.binaryStaticContent;
|
||||
resolved.dynamicClassesPath = this.dynamicClassesPath;
|
||||
resolved.precompiledClassesPath = this.precompiledClassesPath;
|
||||
resolved.serveStatics = this.serveStatics;
|
||||
resolved.staticPrefix = this.staticPrefix;
|
||||
resolved.largeFileThresholdBytes = this.largeFileThresholdBytes;
|
||||
resolved.staticCorsEnabled = this.staticCorsEnabled;
|
||||
resolved.staticCorsAllowOrigin = this.staticCorsAllowOrigin;
|
||||
resolved.staticCorsAllowMethods = this.staticCorsAllowMethods;
|
||||
resolved.staticCorsAllowHeaders = this.staticCorsAllowHeaders;
|
||||
resolved.staticCorsMaxAge = this.staticCorsMaxAge;
|
||||
return new JteSettings(resolved);
|
||||
}
|
||||
|
||||
@@ -105,5 +213,22 @@ public final class JteSettings {
|
||||
if (normalized.isEmpty()) return "/";
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String normalizeStaticPrefix(String prefix) {
|
||||
String normalized = prefix == null ? "" : prefix.trim().replace('\\', '/');
|
||||
if (normalized.isEmpty()) throw new IllegalArgumentException("staticPrefix must not be blank");
|
||||
if (!normalized.startsWith("/")) normalized = '/' + normalized;
|
||||
while (normalized.length() > 1 && normalized.endsWith("/")) {
|
||||
normalized = normalized.substring(0, normalized.length() - 1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
private static String requireCorsValue(String value, String field) {
|
||||
if (value == null || value.isBlank()) {
|
||||
throw new IllegalArgumentException(field + " must not be blank");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package dev.relism.ext.view.jte;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class JteStaticServingTest {
|
||||
|
||||
@Test
|
||||
void etag_is_stable_for_same_payload_and_changes_for_different() {
|
||||
byte[] a = "hello".getBytes();
|
||||
byte[] b = "hello".getBytes();
|
||||
byte[] c = "hello!".getBytes();
|
||||
|
||||
String etagA = JteStaticServing.etagFor(a);
|
||||
String etagB = JteStaticServing.etagFor(b);
|
||||
String etagC = JteStaticServing.etagFor(c);
|
||||
|
||||
assertEquals(etagA, etagB);
|
||||
assertNotEquals(etagA, etagC);
|
||||
}
|
||||
|
||||
@Test
|
||||
void mime_resolution_supports_common_extensions_and_defaults() {
|
||||
assertEquals("text/css", JteStaticServing.mimeFor("/sample.css"));
|
||||
assertEquals("text/javascript", JteStaticServing.mimeFor("/sample.js"));
|
||||
assertEquals("application/wasm", JteStaticServing.mimeFor("/sample.wasm"));
|
||||
assertEquals("application/octet-stream", JteStaticServing.mimeFor("/sample.unknown"));
|
||||
assertEquals("application/octet-stream", JteStaticServing.mimeFor("/sample"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void versioned_path_detection_by_filename_hash() {
|
||||
assertTrue(JteStaticServing.versionedPath("/app.4f3a2c1b.js"));
|
||||
assertTrue(JteStaticServing.versionedPath("/app.abcdefabcdef.js"));
|
||||
assertFalse(JteStaticServing.versionedPath("/app.js"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void range_parsing_supports_basic_cases() {
|
||||
assertEquals(10, JteStaticServing.rangeLength("bytes=0-9", 100));
|
||||
assertEquals(100, JteStaticServing.rangeLength("bytes=0-", 100));
|
||||
assertEquals(10, JteStaticServing.rangeLength("bytes=-10", 100));
|
||||
assertEquals(-1, JteStaticServing.rangeLength("bytes=100-120", 100));
|
||||
assertEquals(-1, JteStaticServing.rangeLength("invalid", 100));
|
||||
}
|
||||
}
|
||||
+106
@@ -8,6 +8,11 @@ import dev.relism.flash.routing.GET;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@@ -65,6 +70,69 @@ class JteExtensionTest {
|
||||
assertTrue(ex.getMessage().contains("must declare @Template"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fluent_static_settings_are_applied() {
|
||||
JteExtension ext = new JteExtension()
|
||||
.templateRoot("templates")
|
||||
.serveStatics(false)
|
||||
.staticPrefix("assets")
|
||||
.withStaticCors()
|
||||
.staticCors(cfg -> cfg
|
||||
.staticCorsAllowOrigin("https://cdn.example.com"));
|
||||
|
||||
JteSettings settings = readSettings(ext);
|
||||
assertEquals("/templates", settings.templateRoot());
|
||||
assertFalse(settings.serveStatics());
|
||||
assertEquals("/assets", settings.staticPrefix());
|
||||
assertTrue(settings.staticCorsEnabled());
|
||||
assertEquals("https://cdn.example.com", settings.staticCorsAllowOrigin());
|
||||
}
|
||||
|
||||
@Test
|
||||
void routes_register_static_wildcard_when_enabled() {
|
||||
JteExtension ext = new JteExtension(cfg -> cfg.staticPrefix("/assets"));
|
||||
TestRegistrar app = new TestRegistrar();
|
||||
ext.routes(app, new dev.relism.extension.FlashContext());
|
||||
assertTrue(app.routes.containsKey("GET /assets/**"));
|
||||
assertTrue(app.routes.containsKey("HEAD /assets/**"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void routes_do_not_register_static_when_disabled() {
|
||||
JteExtension ext = new JteExtension().serveStatics(false);
|
||||
TestRegistrar app = new TestRegistrar();
|
||||
ext.routes(app, new dev.relism.extension.FlashContext());
|
||||
assertTrue(app.routes.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void static_handler_serves_existing_asset() throws Exception {
|
||||
JteExtension ext = new JteExtension(cfg -> cfg
|
||||
.staticPrefix("/assets")
|
||||
.largeFileThresholdBytes(1));
|
||||
TestRegistrar app = new TestRegistrar();
|
||||
ext.routes(app, new dev.relism.extension.FlashContext());
|
||||
|
||||
Request req = request("/assets/sample.css", null, null, null);
|
||||
Response res = new Response(200, dev.relism.http.ContentType.TEXT_PLAIN);
|
||||
Object out = app.routes.get("GET /assets/**").handle(req, res);
|
||||
|
||||
assertEquals(null, out);
|
||||
assertEquals("text/css", new String(res.getContentType(), StandardCharsets.UTF_8));
|
||||
assertTrue(res.isStreaming() || res.getBody() != null);
|
||||
assertEquals(200, res.getStatusCode());
|
||||
}
|
||||
|
||||
private static JteSettings readSettings(JteExtension ext) {
|
||||
try {
|
||||
var f = JteExtension.class.getDeclaredField("settings");
|
||||
f.setAccessible(true);
|
||||
return (JteSettings) f.get(ext);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void invokeValidate(JteExtension ext, Class<? extends RequestHandler> type) throws Exception {
|
||||
Method m = JteExtension.class.getDeclaredMethod("validateHandlerClass", Class.class);
|
||||
m.setAccessible(true);
|
||||
@@ -77,4 +145,42 @@ class JteExtensionTest {
|
||||
throw new RuntimeException(cause);
|
||||
}
|
||||
}
|
||||
|
||||
private static Request request(String path, String query, String acceptEncoding, String origin) {
|
||||
dev.relism.models.HeaderMap headers = new dev.relism.models.HeaderMap();
|
||||
String raw = "Host: localhost\r\n";
|
||||
if (acceptEncoding != null) raw += "Accept-Encoding: " + acceptEncoding + "\r\n";
|
||||
if (origin != null) raw += "Origin: " + origin + "\r\n";
|
||||
byte[] bytes = raw.getBytes(StandardCharsets.UTF_8);
|
||||
headers.reset(bytes, 0, bytes.length);
|
||||
|
||||
dev.relism.models.RequestLine line = new dev.relism.models.RequestLine(
|
||||
dev.relism.http.HttpMethod.GET,
|
||||
new dev.relism.routing.routers.fastpathrouter.FastPathViews.StringByteView(path),
|
||||
query == null ? null : new dev.relism.routing.routers.fastpathrouter.FastPathViews.StringByteView(query),
|
||||
new dev.relism.routing.routers.fastpathrouter.FastPathViews.StringByteView("HTTP/1.1"),
|
||||
headers
|
||||
);
|
||||
return new Request(line, new byte[0]);
|
||||
}
|
||||
|
||||
private static final class TestRegistrar extends dev.relism.extension.FlashRegistrar<TestRegistrar> {
|
||||
private final Map<String, RequestHandler> routes = new HashMap<>();
|
||||
private final List<dev.relism.routing.Middleware> mws = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public dev.relism.extension.FlashContext ctx() {
|
||||
return new dev.relism.extension.FlashContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addRoute(dev.relism.http.HttpMethod method, String path, RequestHandler handler, List<dev.relism.routing.Middleware> mw) {
|
||||
routes.put(method.name() + " " + path, handler);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void addMiddleware(dev.relism.routing.Middleware mw) {
|
||||
mws.add(mw);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+36
@@ -20,6 +20,14 @@ class JteSettingsTest {
|
||||
assertFalse(settings.binaryStaticContent());
|
||||
assertEquals(Path.of("jte-classes"), settings.dynamicClassesPath());
|
||||
assertEquals(Path.of("jte-classes"), settings.precompiledClassesPath());
|
||||
assertTrue(settings.serveStatics());
|
||||
assertEquals("/static", settings.staticPrefix());
|
||||
assertEquals(64L * 1024L, settings.largeFileThresholdBytes());
|
||||
assertFalse(settings.staticCorsEnabled());
|
||||
assertEquals("*", settings.staticCorsAllowOrigin());
|
||||
assertEquals("GET,HEAD,OPTIONS", settings.staticCorsAllowMethods());
|
||||
assertEquals("*", settings.staticCorsAllowHeaders());
|
||||
assertEquals(3600, settings.staticCorsMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -32,6 +40,14 @@ class JteSettingsTest {
|
||||
.binaryStaticContent(true)
|
||||
.dynamicClassesPath(Path.of("var", "jte-dev"))
|
||||
.precompiledClassesPath(Path.of("var", "jte-prod"))
|
||||
.serveStatics(false)
|
||||
.staticPrefix("assets")
|
||||
.largeFileThresholdBytes(1024)
|
||||
.enableStaticCors(true)
|
||||
.staticCorsAllowOrigin("https://cdn.example.com")
|
||||
.staticCorsAllowMethods("GET,HEAD")
|
||||
.staticCorsAllowHeaders("Origin,Content-Type")
|
||||
.staticCorsMaxAge(120)
|
||||
.build();
|
||||
|
||||
assertEquals("/src/main/jte", settings.templateRoot());
|
||||
@@ -41,6 +57,14 @@ class JteSettingsTest {
|
||||
assertTrue(settings.binaryStaticContent());
|
||||
assertEquals(Path.of("var", "jte-dev"), settings.dynamicClassesPath());
|
||||
assertEquals(Path.of("var", "jte-prod"), settings.precompiledClassesPath());
|
||||
assertFalse(settings.serveStatics());
|
||||
assertEquals("/assets", settings.staticPrefix());
|
||||
assertEquals(1024, settings.largeFileThresholdBytes());
|
||||
assertTrue(settings.staticCorsEnabled());
|
||||
assertEquals("https://cdn.example.com", settings.staticCorsAllowOrigin());
|
||||
assertEquals("GET,HEAD", settings.staticCorsAllowMethods());
|
||||
assertEquals("Origin,Content-Type", settings.staticCorsAllowHeaders());
|
||||
assertEquals(120, settings.staticCorsMaxAge());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -60,10 +84,22 @@ class JteSettingsTest {
|
||||
assertThrows(IllegalArgumentException.class, () -> JteSettings.builder().templateRoot(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void staticPrefix_normalizes_andRejectsBlank() {
|
||||
assertEquals("/static", JteSettings.builder().staticPrefix("static/").build().staticPrefix());
|
||||
assertEquals("/assets", JteSettings.builder().staticPrefix("/assets/").build().staticPrefix());
|
||||
assertThrows(IllegalArgumentException.class, () -> JteSettings.builder().staticPrefix(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void nulls_areRejectedForRequiredObjects() {
|
||||
assertThrows(NullPointerException.class, () -> JteSettings.builder().contentType(null));
|
||||
assertThrows(NullPointerException.class, () -> JteSettings.builder().dynamicClassesPath(null));
|
||||
assertThrows(NullPointerException.class, () -> JteSettings.builder().precompiledClassesPath(null));
|
||||
assertThrows(IllegalArgumentException.class, () -> JteSettings.builder().largeFileThresholdBytes(0));
|
||||
assertThrows(IllegalArgumentException.class, () -> JteSettings.builder().staticCorsAllowOrigin(" "));
|
||||
assertThrows(IllegalArgumentException.class, () -> JteSettings.builder().staticCorsAllowMethods(" "));
|
||||
assertThrows(IllegalArgumentException.class, () -> JteSettings.builder().staticCorsAllowHeaders(" "));
|
||||
assertThrows(IllegalArgumentException.class, () -> JteSettings.builder().staticCorsMaxAge(-1));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
body { color: #111; }
|
||||
@@ -0,0 +1 @@
|
||||
gzipped-sample
|
||||
Reference in New Issue
Block a user