From 696e1bc5a84e6295393ff4b8dc81dad78a663de8 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:34:12 +0100 Subject: [PATCH 01/73] updated pom.xml --- pom.xml | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/pom.xml b/pom.xml index bb03ce76..104baa8e 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ 25 UTF-8 - 6.0.2 + 5.11.4 3.27.7 5.21.0 @@ -76,24 +76,24 @@ 3.4.0 - org.apache.maven.plugins - maven-dependency-plugin - 3.9.0 - - - - properties - - - + org.apache.maven.plugins + maven-dependency-plugin + 3.9.0 + + + + properties + + + org.apache.maven.plugins maven-surefire-plugin 3.5.4 - - @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off - + + @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off + org.apache.maven.plugins @@ -131,18 +131,6 @@ - - org.pitest - pitest-maven - 1.22.0 - - - org.pitest - pitest-junit5-plugin - 1.2.2 - - - com.diffplug.spotless spotless-maven-plugin From de1b6cfa5f5a6db8794b534a193a16af32d72ca5 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:38:01 +0100 Subject: [PATCH 02/73] Add HttpRequest (method, path) --- .../java/org/example/server/HttpRequest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/org/example/server/HttpRequest.java diff --git a/src/main/java/org/example/server/HttpRequest.java b/src/main/java/org/example/server/HttpRequest.java new file mode 100644 index 00000000..3146c645 --- /dev/null +++ b/src/main/java/org/example/server/HttpRequest.java @@ -0,0 +1,21 @@ +package org.example.server; + +import java.util.Objects; + +public final class HttpRequest { + private final String method; + private final String path; + + public HttpRequest(String method, String path) { + this.method = Objects.requireNonNull(method, "method"); + this.path = Objects.requireNonNull(path, "path"); + } + + public String method() { + return method; + } + + public String path() { + return path; + } +} From e21bc8c19363c84b5a949843756f746eec974a7e Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:38:54 +0100 Subject: [PATCH 03/73] Add FilterChain to execute Httpfilter pipeline --- .../java/org/example/server/FilterChain.java | 28 +++++++++++++++++++ .../java/org/example/server/HttpFilter.java | 6 ++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/org/example/server/FilterChain.java create mode 100644 src/main/java/org/example/server/HttpFilter.java diff --git a/src/main/java/org/example/server/FilterChain.java b/src/main/java/org/example/server/FilterChain.java new file mode 100644 index 00000000..ecc7cd3a --- /dev/null +++ b/src/main/java/org/example/server/FilterChain.java @@ -0,0 +1,28 @@ +package org.example.server; + +import java.util.Objects; + +public final class FilterChain { + @FunctionalInterface + public interface TerminalHandler { + void handle(HttpRequest request, HttpResponse response); + } + + private final HttpFilter[] filters; + private final TerminalHandler terminal; + private int index = 0; + + public FilterChain(HttpFilter[] filters, TerminalHandler terminal) { + this.filters = Objects.requireNonNull(filters, "filters"); + this.terminal = Objects.requireNonNull(terminal, "terminal"); + } + + public void doFilter(HttpRequest request, HttpResponse response) { + if (index < filters.length) { + HttpFilter current = filters[index++]; + current.doFilter(request, response, this); + return; + } + terminal.handle(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/server/HttpFilter.java b/src/main/java/org/example/server/HttpFilter.java new file mode 100644 index 00000000..5ac84ed5 --- /dev/null +++ b/src/main/java/org/example/server/HttpFilter.java @@ -0,0 +1,6 @@ +package org.example.server; + +@FunctionalInterface +public interface HttpFilter { + void doFilter(HttpRequest request, HttpResponse response, FilterChain chain); +} From 78e8df08117d3d98a749a9c4ddd9b8b11632bf0f Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:39:19 +0100 Subject: [PATCH 04/73] Add HttpResponse (status + headers + setHeader) --- .../java/org/example/server/HttpResponse.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/org/example/server/HttpResponse.java diff --git a/src/main/java/org/example/server/HttpResponse.java b/src/main/java/org/example/server/HttpResponse.java new file mode 100644 index 00000000..07ad145a --- /dev/null +++ b/src/main/java/org/example/server/HttpResponse.java @@ -0,0 +1,26 @@ +package org.example.server; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class HttpResponse { + private int status = 200; + private final Map headers = new LinkedHashMap<>(); + + public int status() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public Map headers() { + return Collections.unmodifiableMap(headers); + } + + public void setHeader(String name, String value) { + headers.put(name, value); + } +} From 0ed2a796b307bd855f3ef86ed8961ad07d627c8e Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:40:04 +0100 Subject: [PATCH 05/73] Add RedirectRule (pattern match, targetUrl, 301/302 --- .../java/org/example/server/RedirectRule.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/org/example/server/RedirectRule.java diff --git a/src/main/java/org/example/server/RedirectRule.java b/src/main/java/org/example/server/RedirectRule.java new file mode 100644 index 00000000..df9fada0 --- /dev/null +++ b/src/main/java/org/example/server/RedirectRule.java @@ -0,0 +1,32 @@ +package org.example.server; + +import java.util.regex.Pattern; + +public final class RedirectRule { + private final Pattern sourcePattern; + private final String targetUrl; + private final int statusCode; + + public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { + this.sourcePattern = sourcePattern; + this.targetUrl = targetUrl; + this.statusCode = statusCode; + } + + public Pattern getSourcePattern() { return sourcePattern; } + public String getTargetUrl() { return targetUrl; } + public int getStatusCode() { return statusCode; } + + public boolean matches(String requestPath) { + return sourcePattern.matcher(requestPath).matches(); + } + + @Override + public String toString() { + return "RedirectRule{" + + "sourcePattern=" + sourcePattern + + ", targetUrl='" + targetUrl + '\'' + + ", statusCode=" + statusCode + + '}'; + } +} From 3e4c746b37fb54f404f9a4357b41246c11c33497 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:40:16 +0100 Subject: [PATCH 06/73] Add RedirectRulesLoader.compileSourcePattern (wildcard support --- .../example/server/RedirectRulesLoader.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/org/example/server/RedirectRulesLoader.java diff --git a/src/main/java/org/example/server/RedirectRulesLoader.java b/src/main/java/org/example/server/RedirectRulesLoader.java new file mode 100644 index 00000000..a7956d16 --- /dev/null +++ b/src/main/java/org/example/server/RedirectRulesLoader.java @@ -0,0 +1,34 @@ +package org.example.server; + +import java.util.regex.Pattern; + +public final class RedirectRulesLoader { + private RedirectRulesLoader() {} + + public static Pattern compileSourcePattern(String sourcePath) { + if (sourcePath == null || sourcePath.isBlank()) { + throw new IllegalArgumentException("sourcePath must not be blank"); + } + + String regex; + if (sourcePath.contains("*")) { + regex = wildcardToRegex(sourcePath); + } else { + regex = Pattern.quote(sourcePath); + } + return Pattern.compile("^" + regex + "$"); + } + + private static String wildcardToRegex(String wildcard) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < wildcard.length(); i++) { + char c = wildcard.charAt(i); + if (c == '*') { + sb.append(".*"); + } else { + sb.append(Pattern.quote(String.valueOf(c))); + } + } + return sb.toString(); + } +} From 1a5ce4db0a0847dfe069ca9ffb389d652274fd04 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:40:32 +0100 Subject: [PATCH 07/73] Add RedirectFilter tests (301/302/no match/wildcard) --- .../example/server/RedirectFilterTest.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/test/java/org/example/server/RedirectFilterTest.java diff --git a/src/test/java/org/example/server/RedirectFilterTest.java b/src/test/java/org/example/server/RedirectFilterTest.java new file mode 100644 index 00000000..5a21bb6a --- /dev/null +++ b/src/test/java/org/example/server/RedirectFilterTest.java @@ -0,0 +1,75 @@ +package org.example.server; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; + +class RedirectFilterTest { + + @Test + void returns_301_redirect_and_stops_pipeline() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) + )); + + AtomicBoolean terminalCalled = new AtomicBoolean(false); + FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); + + HttpRequest req = new HttpRequest("GET", "/old-page"); + HttpResponse res = new HttpResponse(); + + chain.doFilter(req, res); + + assertThat(res.status()).isEqualTo(301); + assertThat(res.headers()).containsEntry("Location", "/new-page"); + assertThat(terminalCalled.get()).isFalse(); + } + + @Test + void returns_302_redirect() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) + )); + + FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> res.setStatus(200)); + + HttpRequest req = new HttpRequest("GET", "/temp"); + HttpResponse res = new HttpResponse(); + + chain.doFilter(req, res); + + assertThat(res.status()).isEqualTo(302); + assertThat(res.headers()).containsEntry("Location", "https://example.com/temporary"); + } + + @Test + void no_matching_rule_calls_next_in_chain() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) + )); + + AtomicBoolean terminalCalled = new AtomicBoolean(false); + FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); + + HttpRequest req = new HttpRequest("GET", "/nope"); + HttpResponse res = new HttpResponse(); + + chain.doFilter(req, res); + + assertThat(terminalCalled.get()).isTrue(); + assertThat(res.status()).isEqualTo(200); + assertThat(res.headers()).doesNotContainKey("Location"); + } + + @Test + void wildcard_matching_docs_star() { + var p = RedirectRulesLoader.compileSourcePattern("/docs/*"); + assertThat(p.matcher("/docs/test").matches()).isTrue(); + assertThat(p.matcher("/docs/any/path").matches()).isTrue(); + assertThat(p.matcher("/doc/test").matches()).isFalse(); + } +} From f3f39b3d3e5374d8f3b37444523f3933631ef1b9 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:41:06 +0100 Subject: [PATCH 08/73] Add RedirectResponse DTO (location + statusCode) --- .../org/example/server/RedirectResponse.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/org/example/server/RedirectResponse.java diff --git a/src/main/java/org/example/server/RedirectResponse.java b/src/main/java/org/example/server/RedirectResponse.java new file mode 100644 index 00000000..9aba54e6 --- /dev/null +++ b/src/main/java/org/example/server/RedirectResponse.java @@ -0,0 +1,19 @@ +package org.example.server; + +public class RedirectResponse { + private String location; + private int statusCode; + + public RedirectResponse(String location, int statusCode) { + this.location = location; + this.statusCode = statusCode; + } + + public String getLocation() { + return location; + } + + public int getStatusCode() { + return statusCode; + } +} From 699c9d7ccc2c05db896bb20ec7a3b40ad809b267 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:41:27 +0100 Subject: [PATCH 09/73] Add RedirectFilter (set Location + stop chain + logging) --- .../org/example/server/RedirectFilter.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/org/example/server/RedirectFilter.java diff --git a/src/main/java/org/example/server/RedirectFilter.java b/src/main/java/org/example/server/RedirectFilter.java new file mode 100644 index 00000000..07097fdd --- /dev/null +++ b/src/main/java/org/example/server/RedirectFilter.java @@ -0,0 +1,30 @@ +package org.example.server; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +public final class RedirectFilter implements HttpFilter { + private static final Logger LOG = Logger.getLogger(RedirectFilter.class.getName()); + private final List rules; + + public RedirectFilter(List rules) { + this.rules = List.copyOf(Objects.requireNonNull(rules, "rules")); + } + + @Override + public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) { + String path = request.path(); + + for (RedirectRule rule : rules) { + if (rule.matches(path)) { + LOG.info(() -> "Redirecting " + path + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); + response.setStatus(rule.getStatusCode()); + response.setHeader("Location", rule.getTargetUrl()); + return; // STOP pipeline + } + } + + chain.doFilter(request, response); + } +} From f48524bcaa0ec174e2d511f7f8cc08d2aee34569 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:49:28 +0100 Subject: [PATCH 10/73] Add RedirectFilter (set Location + stop chain + logging) --- .../org/example/server/RedirectFilterTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/java/org/example/server/RedirectFilterTest.java b/src/test/java/org/example/server/RedirectFilterTest.java index 5a21bb6a..16d3b02b 100644 --- a/src/test/java/org/example/server/RedirectFilterTest.java +++ b/src/test/java/org/example/server/RedirectFilterTest.java @@ -7,6 +7,7 @@ import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class RedirectFilterTest { @@ -72,4 +73,19 @@ void wildcard_matching_docs_star() { assertThat(p.matcher("/docs/any/path").matches()).isTrue(); assertThat(p.matcher("/doc/test").matches()).isFalse(); } + + @Test + void regex_matching_via_loader_prefix() { + var p = RedirectRulesLoader.compileSourcePattern("regex:^/docs/(v1|v2)$"); + assertThat(p.matcher("/docs/v1").matches()).isTrue(); + assertThat(p.matcher("/docs/v2").matches()).isTrue(); + assertThat(p.matcher("/docs/v3").matches()).isFalse(); + } + + @Test + void redirect_rule_rejects_invalid_status_code() { + assertThatThrownBy(() -> new RedirectRule(Pattern.compile("^/x$"), "/y", 307)) + .isInstanceOf(IllegalArgumentException.class); + } } + From 37a823105ed88586b0c983eb51bf92d35f469fb5 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:50:48 +0100 Subject: [PATCH 11/73] feat: improve redirect rule parsing and validation feat: add regex support and enforce 301/302 redirects --- .../java/org/example/server/RedirectRule.java | 9 +++++++-- .../example/server/RedirectRulesLoader.java | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/example/server/RedirectRule.java b/src/main/java/org/example/server/RedirectRule.java index df9fada0..27640afd 100644 --- a/src/main/java/org/example/server/RedirectRule.java +++ b/src/main/java/org/example/server/RedirectRule.java @@ -1,5 +1,6 @@ package org.example.server; +import java.util.Objects; import java.util.regex.Pattern; public final class RedirectRule { @@ -8,8 +9,11 @@ public final class RedirectRule { private final int statusCode; public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { - this.sourcePattern = sourcePattern; - this.targetUrl = targetUrl; + this.sourcePattern = Objects.requireNonNull(sourcePattern, "sourcePattern"); + this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl"); + if (statusCode != 301 && statusCode != 302) { + throw new IllegalArgumentException("statusCode must be 301 or 302"); + } this.statusCode = statusCode; } @@ -30,3 +34,4 @@ public String toString() { '}'; } } + diff --git a/src/main/java/org/example/server/RedirectRulesLoader.java b/src/main/java/org/example/server/RedirectRulesLoader.java index a7956d16..1922dc81 100644 --- a/src/main/java/org/example/server/RedirectRulesLoader.java +++ b/src/main/java/org/example/server/RedirectRulesLoader.java @@ -3,6 +3,8 @@ import java.util.regex.Pattern; public final class RedirectRulesLoader { + private static final String REGEX_PREFIX = "regex:"; + private RedirectRulesLoader() {} public static Pattern compileSourcePattern(String sourcePath) { @@ -10,11 +12,21 @@ public static Pattern compileSourcePattern(String sourcePath) { throw new IllegalArgumentException("sourcePath must not be blank"); } + String trimmed = sourcePath.trim(); + + if (trimmed.startsWith(REGEX_PREFIX)) { + String rawRegex = trimmed.substring(REGEX_PREFIX.length()); + if (rawRegex.isBlank()) { + throw new IllegalArgumentException("regex sourcePath must not be blank"); + } + return Pattern.compile(rawRegex); + } + String regex; - if (sourcePath.contains("*")) { - regex = wildcardToRegex(sourcePath); + if (trimmed.contains("*")) { + regex = wildcardToRegex(trimmed); } else { - regex = Pattern.quote(sourcePath); + regex = Pattern.quote(trimmed); } return Pattern.compile("^" + regex + "$"); } @@ -32,3 +44,4 @@ private static String wildcardToRegex(String wildcard) { return sb.toString(); } } + From 30f3b6ce35b963f8aa78a9a503e3d328b873217f Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:48:16 +0100 Subject: [PATCH 12/73] refactor(server): extract TerminalHandler to own file --- src/main/java/org/example/server/TerminalHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/org/example/server/TerminalHandler.java diff --git a/src/main/java/org/example/server/TerminalHandler.java b/src/main/java/org/example/server/TerminalHandler.java new file mode 100644 index 00000000..dc3bcfde --- /dev/null +++ b/src/main/java/org/example/server/TerminalHandler.java @@ -0,0 +1,6 @@ +package org.example.server; + +@FunctionalInterface +public interface TerminalHandler { + void handle(HttpRequest request, HttpResponse response); +} \ No newline at end of file From 5fbd3dbd01dafdc84a1d8fd3d81d3226d9477771 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:48:41 +0100 Subject: [PATCH 13/73] fix(server): make '*' not match '/' in redirect wildcards; update test --- src/test/java/org/example/server/RedirectFilterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/example/server/RedirectFilterTest.java b/src/test/java/org/example/server/RedirectFilterTest.java index 16d3b02b..e7a1a448 100644 --- a/src/test/java/org/example/server/RedirectFilterTest.java +++ b/src/test/java/org/example/server/RedirectFilterTest.java @@ -70,7 +70,7 @@ void no_matching_rule_calls_next_in_chain() { void wildcard_matching_docs_star() { var p = RedirectRulesLoader.compileSourcePattern("/docs/*"); assertThat(p.matcher("/docs/test").matches()).isTrue(); - assertThat(p.matcher("/docs/any/path").matches()).isTrue(); + assertThat(p.matcher("/docs/any/path").matches()).isFalse(); assertThat(p.matcher("/doc/test").matches()).isFalse(); } From b06171e5dbfc663b6a77d8e68384b1d6c8c5132c Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:50:57 +0100 Subject: [PATCH 14/73] extract TerminalHandler from FilterChain into a interface --- src/main/java/org/example/server/FilterChain.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/org/example/server/FilterChain.java b/src/main/java/org/example/server/FilterChain.java index ecc7cd3a..378c2a51 100644 --- a/src/main/java/org/example/server/FilterChain.java +++ b/src/main/java/org/example/server/FilterChain.java @@ -3,10 +3,6 @@ import java.util.Objects; public final class FilterChain { - @FunctionalInterface - public interface TerminalHandler { - void handle(HttpRequest request, HttpResponse response); - } private final HttpFilter[] filters; private final TerminalHandler terminal; From 172671c3be38b370a57c746d939efe7975713989 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:51:19 +0100 Subject: [PATCH 15/73] make RedirectRulesLoader wildcard '*' not match '/' (avoid matching subdirectories) --- src/main/java/org/example/server/RedirectRulesLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/server/RedirectRulesLoader.java b/src/main/java/org/example/server/RedirectRulesLoader.java index 1922dc81..26f1254c 100644 --- a/src/main/java/org/example/server/RedirectRulesLoader.java +++ b/src/main/java/org/example/server/RedirectRulesLoader.java @@ -36,7 +36,7 @@ private static String wildcardToRegex(String wildcard) { for (int i = 0; i < wildcard.length(); i++) { char c = wildcard.charAt(i); if (c == '*') { - sb.append(".*"); + sb.append("[^/]*"); } else { sb.append(Pattern.quote(String.valueOf(c))); } From 6513e43ac0c724eae2f191790be0be35d06e3d02 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:53:59 +0100 Subject: [PATCH 16/73] removed unused RedirectResponse and .gitkeep placeholders --- .../org/example/server/RedirectResponse.java | 19 ------------------- src/main/resources/.gitkeep | 0 src/test/resources/.gitkeep | 0 3 files changed, 19 deletions(-) delete mode 100644 src/main/java/org/example/server/RedirectResponse.java delete mode 100644 src/main/resources/.gitkeep delete mode 100644 src/test/resources/.gitkeep diff --git a/src/main/java/org/example/server/RedirectResponse.java b/src/main/java/org/example/server/RedirectResponse.java deleted file mode 100644 index 9aba54e6..00000000 --- a/src/main/java/org/example/server/RedirectResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.example.server; - -public class RedirectResponse { - private String location; - private int statusCode; - - public RedirectResponse(String location, int statusCode) { - this.location = location; - this.statusCode = statusCode; - } - - public String getLocation() { - return location; - } - - public int getStatusCode() { - return statusCode; - } -} diff --git a/src/main/resources/.gitkeep b/src/main/resources/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/.gitkeep b/src/test/resources/.gitkeep deleted file mode 100644 index e69de29b..00000000 From 101d1f4f098d9fa84f7693a204fef794c0b6cc09 Mon Sep 17 00:00:00 2001 From: Viktor Lindell Date: Fri, 20 Feb 2026 15:50:04 +0100 Subject: [PATCH 17/73] Update redirect rules and tests --- .../filter/redirect/RedirectFilter.java | 45 +++++++ .../example/filter/redirect/RedirectRule.java | 35 ++++++ .../filter/redirect/RedirectRulesLoader.java | 46 +++++++ .../filter/redirect/RedirectFilterTest.java | 113 ++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 src/main/java/org/example/filter/redirect/RedirectFilter.java create mode 100644 src/main/java/org/example/filter/redirect/RedirectRule.java create mode 100644 src/main/java/org/example/filter/redirect/RedirectRulesLoader.java create mode 100644 src/test/java/org/example/filter/redirect/RedirectFilterTest.java diff --git a/src/main/java/org/example/filter/redirect/RedirectFilter.java b/src/main/java/org/example/filter/redirect/RedirectFilter.java new file mode 100644 index 00000000..f180c9ad --- /dev/null +++ b/src/main/java/org/example/filter/redirect/RedirectFilter.java @@ -0,0 +1,45 @@ +package org.example.filter.redirect; + +import org.example.filter.Filter; +import org.example.filter.FilterChain; +import org.example.http.HttpResponseBuilder; +import org.example.httpparser.HttpRequest; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +public final class RedirectFilter implements Filter { + private static final Logger LOG = Logger.getLogger(RedirectFilter.class.getName()); + private final List rules; + + public RedirectFilter(List rules) { + this.rules = List.copyOf(Objects.requireNonNull(rules, "rules")); + } + + @Override + public void init() { + // no-op + } + + @Override + public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) { + String path = request.getPath(); + + for (RedirectRule rule : rules) { + if (rule.matches(path)) { + LOG.info(() -> "Redirecting " + path + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); + response.setStatusCode(rule.getStatusCode()); + response.setHeader("Location", rule.getTargetUrl()); + return; // STOP pipeline + } + } + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // no-op + } +} \ No newline at end of file diff --git a/src/main/java/org/example/filter/redirect/RedirectRule.java b/src/main/java/org/example/filter/redirect/RedirectRule.java new file mode 100644 index 00000000..b7b0d64d --- /dev/null +++ b/src/main/java/org/example/filter/redirect/RedirectRule.java @@ -0,0 +1,35 @@ +package org.example.filter.redirect; +import java.util.Objects; +import java.util.regex.Pattern; + +public final class RedirectRule { + private final Pattern sourcePattern; + private final String targetUrl; + private final int statusCode; + + public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { + this.sourcePattern = Objects.requireNonNull(sourcePattern, "sourcePattern"); + this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl"); + if (statusCode != 301 && statusCode != 302) { + throw new IllegalArgumentException("statusCode must be 301 or 302"); + } + this.statusCode = statusCode; + } + + public Pattern getSourcePattern() { return sourcePattern; } + public String getTargetUrl() { return targetUrl; } + public int getStatusCode() { return statusCode; } + + public boolean matches(String requestPath) { + return sourcePattern.matcher(requestPath).matches(); + } + + @Override + public String toString() { + return "RedirectRule{" + + "sourcePattern=" + sourcePattern + + ", targetUrl='" + targetUrl + '\'' + + ", statusCode=" + statusCode + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java b/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java new file mode 100644 index 00000000..431d0589 --- /dev/null +++ b/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java @@ -0,0 +1,46 @@ +package org.example.filter.redirect; + +import java.util.regex.Pattern; + +public final class RedirectRulesLoader { + private static final String REGEX_PREFIX = "regex:"; + + private RedirectRulesLoader() {} + + public static Pattern compileSourcePattern(String sourcePath) { + if (sourcePath == null || sourcePath.isBlank()) { + throw new IllegalArgumentException("sourcePath must not be blank"); + } + + String trimmed = sourcePath.trim(); + + if (trimmed.startsWith(REGEX_PREFIX)) { + String rawRegex = trimmed.substring(REGEX_PREFIX.length()); + if (rawRegex.isBlank()) { + throw new IllegalArgumentException("regex sourcePath must not be blank"); + } + return Pattern.compile(rawRegex); + } + + String regex; + if (trimmed.contains("*")) { + regex = wildcardToRegex(trimmed); + } else { + regex = Pattern.quote(trimmed); + } + return Pattern.compile("^" + regex + "$"); + } + + private static String wildcardToRegex(String wildcard) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < wildcard.length(); i++) { + char c = wildcard.charAt(i); + if (c == '*') { + sb.append("[^/]*"); + } else { + sb.append(Pattern.quote(String.valueOf(c))); + } + } + return sb.toString(); + } +} diff --git a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java new file mode 100644 index 00000000..f5d32f21 --- /dev/null +++ b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java @@ -0,0 +1,113 @@ +package org.example.filter.redirect; + +import org.example.filter.FilterChain; +import org.example.http.HttpResponseBuilder; +import org.example.httpparser.HttpRequest; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RedirectFilterTest { + + private static HttpRequest request(String path) { + return new HttpRequest( + "GET", + path, + "HTTP/1.1", + Map.of(), + null + ); + } + + private static String responseAsString(HttpResponseBuilder response) { + return new String(response.build(), StandardCharsets.UTF_8); + } + + @Test + void returns_301_redirect_and_stops_pipeline() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) + )); + + AtomicBoolean chainCalled = new AtomicBoolean(false); + FilterChain chain = (req, res) -> chainCalled.set(true); + + HttpResponseBuilder res = new HttpResponseBuilder(); + + filter.doFilter(request("/old-page"), res, chain); + + String raw = responseAsString(res); + assertThat(raw).contains("HTTP/1.1 301 Moved Permanently"); + assertThat(raw).contains("Location: /new-page"); + assertThat(chainCalled.get()).isFalse(); + } + + @Test + void returns_302_redirect() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) + )); + + FilterChain chain = (req, res) -> res.setStatusCode(200); + + HttpResponseBuilder res = new HttpResponseBuilder(); + + filter.doFilter(request("/temp"), res, chain); + + String raw = responseAsString(res); + assertThat(raw).contains("HTTP/1.1 302 Found"); + assertThat(raw).contains("Location: https://example.com/temporary"); + } + + @Test + void no_matching_rule_calls_next_in_chain() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) + )); + + AtomicBoolean chainCalled = new AtomicBoolean(false); + FilterChain chain = (req, res) -> { + chainCalled.set(true); + res.setStatusCode(200); + res.setBody("terminal"); + }; + + HttpResponseBuilder res = new HttpResponseBuilder(); + + filter.doFilter(request("/nope"), res, chain); + + String raw = responseAsString(res); + assertThat(chainCalled.get()).isTrue(); + assertThat(raw).contains("HTTP/1.1 200 OK"); + assertThat(raw).doesNotContain("Location:"); + } + + @Test + void wildcard_matching_docs_star() { + var p = RedirectRulesLoader.compileSourcePattern("/docs/*"); + assertThat(p.matcher("/docs/test").matches()).isTrue(); + assertThat(p.matcher("/docs/any/path").matches()).isFalse(); + assertThat(p.matcher("/doc/test").matches()).isFalse(); + } + + @Test + void regex_matching_via_loader_prefix() { + var p = RedirectRulesLoader.compileSourcePattern("regex:^/docs/(v1|v2)$"); + assertThat(p.matcher("/docs/v1").matches()).isTrue(); + assertThat(p.matcher("/docs/v2").matches()).isTrue(); + assertThat(p.matcher("/docs/v3").matches()).isFalse(); + } + + @Test + void redirect_rule_rejects_invalid_status_code() { + assertThatThrownBy(() -> new RedirectRule(Pattern.compile("^/x$"), "/y", 307)) + .isInstanceOf(IllegalArgumentException.class); + } +} From 5fdac734071003ce66472ae06526fc05a1dc4551 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:24:25 +0100 Subject: [PATCH 18/73] Update RedirectFilter.java --- .../java/org/example/filter/redirect/RedirectFilter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectFilter.java b/src/main/java/org/example/filter/redirect/RedirectFilter.java index f180c9ad..1c28b83c 100644 --- a/src/main/java/org/example/filter/redirect/RedirectFilter.java +++ b/src/main/java/org/example/filter/redirect/RedirectFilter.java @@ -27,8 +27,8 @@ public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterCh String path = request.getPath(); for (RedirectRule rule : rules) { - if (rule.matches(path)) { - LOG.info(() -> "Redirecting " + path + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); + final String sanitizedPath = path.replaceAll("[\\r\\n]", "_"); + LOG.info(() -> "Redirecting " + sanitizedPath + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); response.setStatusCode(rule.getStatusCode()); response.setHeader("Location", rule.getTargetUrl()); return; // STOP pipeline @@ -42,4 +42,4 @@ public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterCh public void destroy() { // no-op } -} \ No newline at end of file +} From e90052d586ade3e4bc62b617f4c32e2a1d27717a Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:27:57 +0100 Subject: [PATCH 19/73] Update RedirectRule.java --- src/main/java/org/example/filter/redirect/RedirectRule.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectRule.java b/src/main/java/org/example/filter/redirect/RedirectRule.java index b7b0d64d..ba0a45dd 100644 --- a/src/main/java/org/example/filter/redirect/RedirectRule.java +++ b/src/main/java/org/example/filter/redirect/RedirectRule.java @@ -10,6 +10,10 @@ public final class RedirectRule { public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { this.sourcePattern = Objects.requireNonNull(sourcePattern, "sourcePattern"); this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl"); + if (this.targetUrl.isBlank()) { + throw new IllegalArgumentException("targetUrl must not be blank"); +} + if (statusCode != 301 && statusCode != 302) { throw new IllegalArgumentException("statusCode must be 301 or 302"); } @@ -32,4 +36,4 @@ public String toString() { ", statusCode=" + statusCode + '}'; } -} \ No newline at end of file +} From bb1caffe019c77c06ebfc48d6dc74422ea235ff0 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:35:04 +0100 Subject: [PATCH 20/73] Update RedirectFilter.java --- .../filter/redirect/RedirectFilter.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectFilter.java b/src/main/java/org/example/filter/redirect/RedirectFilter.java index 1c28b83c..1a02f42b 100644 --- a/src/main/java/org/example/filter/redirect/RedirectFilter.java +++ b/src/main/java/org/example/filter/redirect/RedirectFilter.java @@ -24,20 +24,29 @@ public void init() { @Override public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) { - String path = request.getPath(); - - for (RedirectRule rule : rules) { - final String sanitizedPath = path.replaceAll("[\\r\\n]", "_"); - LOG.info(() -> "Redirecting " + sanitizedPath + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); - response.setStatusCode(rule.getStatusCode()); - response.setHeader("Location", rule.getTargetUrl()); - return; // STOP pipeline - } - } + String path = request.getPath(); + if (path == null) { chain.doFilter(request, response); + return; + } + + for (RedirectRule rule : rules) { + if (rule.matches(path)) { // <-- DENNA RADEN SKA FINNAS + final String sanitizedPath = path.replaceAll("[\\r\\n]", "_"); + LOG.info(() -> "Redirecting " + sanitizedPath + " -> " + + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); + + response.setStatusCode(rule.getStatusCode()); + response.setHeader("Location", rule.getTargetUrl()); + return; // STOP pipeline + } } + chain.doFilter(request, response); +} + + @Override public void destroy() { // no-op From 23df773f586041c991b919c247472a9911350bf8 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:36:52 +0100 Subject: [PATCH 21/73] Update RedirectRulesLoader.java --- .../java/org/example/filter/redirect/RedirectRulesLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java b/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java index 431d0589..779cf5d4 100644 --- a/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java +++ b/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java @@ -15,7 +15,7 @@ public static Pattern compileSourcePattern(String sourcePath) { String trimmed = sourcePath.trim(); if (trimmed.startsWith(REGEX_PREFIX)) { - String rawRegex = trimmed.substring(REGEX_PREFIX.length()); + String rawRegex = trimmed.substring(REGEX_PREFIX.length()).strip(); if (rawRegex.isBlank()) { throw new IllegalArgumentException("regex sourcePath must not be blank"); } From ad54bc96f7f2b1a69f55bb0553fc9e4d3775f47c Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:42:07 +0100 Subject: [PATCH 22/73] Update RedirectFilterTest.java --- .../filter/redirect/RedirectFilterTest.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java index f5d32f21..eb55c345 100644 --- a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java +++ b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java @@ -50,20 +50,27 @@ void returns_301_redirect_and_stops_pipeline() { } @Test - void returns_302_redirect() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) - )); +void returns_302_redirect() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) + )); - FilterChain chain = (req, res) -> res.setStatusCode(200); + AtomicBoolean chainCalled = new AtomicBoolean(false); + FilterChain chain = (req, res) -> { + chainCalled.set(true); + res.setStatusCode(200); + }; - HttpResponseBuilder res = new HttpResponseBuilder(); + HttpResponseBuilder res = new HttpResponseBuilder(); - filter.doFilter(request("/temp"), res, chain); + filter.doFilter(request("/temp"), res, chain); + + String raw = responseAsString(res); + assertThat(raw).contains("HTTP/1.1 302 Found"); + assertThat(raw).contains("Location: https://example.com/temporary"); + assertThat(chainCalled.get()).isFalse(); + } - String raw = responseAsString(res); - assertThat(raw).contains("HTTP/1.1 302 Found"); - assertThat(raw).contains("Location: https://example.com/temporary"); } @Test From 1d1f881acec8b5914b2bec51986a8019d78cb9fe Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:43:32 +0100 Subject: [PATCH 23/73] Delete pom.xml --- pom.xml | 176 -------------------------------------------------------- 1 file changed, 176 deletions(-) delete mode 100644 pom.xml diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 6f4e28af..00000000 --- a/pom.xml +++ /dev/null @@ -1,176 +0,0 @@ - - - 4.0.0 - - org.example - JavaTemplate - 1.0-SNAPSHOT - - - 25 - UTF-8 - 5.11.4 - 3.27.7 - 5.21.0 - 8.14.0 - - - - - - com.bucket4j - bucket4j_jdk17-core - ${bucket4j.version} - - - org.junit.jupiter - junit-jupiter - ${junit.jupiter.version} - test - - - org.assertj - assertj-core - ${assertj.core.version} - test - - - org.mockito - mockito-core - ${mockito.version} - test - - - org.mockito - mockito-junit-jupiter - ${mockito.version} - test - - - org.awaitility - awaitility - 4.3.0 - test - - - - tools.jackson.core - jackson-databind - 3.0.3 - - - tools.jackson.dataformat - jackson-dataformat-yaml - 3.0.3 - - - - - - - org.apache.maven.plugins - maven-clean-plugin - 3.5.0 - - - org.apache.maven.plugins - maven-compiler-plugin - 3.15.0 - - - org.apache.maven.plugins - maven-install-plugin - 3.1.4 - - - org.apache.maven.plugins - maven-jar-plugin - 3.5.0 - - - org.apache.maven.plugins - maven-resources-plugin - 3.4.0 - - - org.apache.maven.plugins - maven-dependency-plugin - 3.9.0 - - - - properties - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.4 - - @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off - - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.5.4 - - - - integration-test - verify - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.14 - - true - - - - default-prepare-agent - - prepare-agent - - - - default-report - prepare-package - - report - - - - - - com.diffplug.spotless - spotless-maven-plugin - 3.2.1 - - - - - - - - - verify - - - check - - - - - - - - From ab4d5efdfb0586b320d043e1afd73430fcd0fb41 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:44:36 +0100 Subject: [PATCH 24/73] Delete src/test/java/org/example/server/RedirectFilterTest.java --- .../example/server/RedirectFilterTest.java | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 src/test/java/org/example/server/RedirectFilterTest.java diff --git a/src/test/java/org/example/server/RedirectFilterTest.java b/src/test/java/org/example/server/RedirectFilterTest.java deleted file mode 100644 index e7a1a448..00000000 --- a/src/test/java/org/example/server/RedirectFilterTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.example.server; - -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.regex.Pattern; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class RedirectFilterTest { - - @Test - void returns_301_redirect_and_stops_pipeline() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) - )); - - AtomicBoolean terminalCalled = new AtomicBoolean(false); - FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); - - HttpRequest req = new HttpRequest("GET", "/old-page"); - HttpResponse res = new HttpResponse(); - - chain.doFilter(req, res); - - assertThat(res.status()).isEqualTo(301); - assertThat(res.headers()).containsEntry("Location", "/new-page"); - assertThat(terminalCalled.get()).isFalse(); - } - - @Test - void returns_302_redirect() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) - )); - - FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> res.setStatus(200)); - - HttpRequest req = new HttpRequest("GET", "/temp"); - HttpResponse res = new HttpResponse(); - - chain.doFilter(req, res); - - assertThat(res.status()).isEqualTo(302); - assertThat(res.headers()).containsEntry("Location", "https://example.com/temporary"); - } - - @Test - void no_matching_rule_calls_next_in_chain() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) - )); - - AtomicBoolean terminalCalled = new AtomicBoolean(false); - FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); - - HttpRequest req = new HttpRequest("GET", "/nope"); - HttpResponse res = new HttpResponse(); - - chain.doFilter(req, res); - - assertThat(terminalCalled.get()).isTrue(); - assertThat(res.status()).isEqualTo(200); - assertThat(res.headers()).doesNotContainKey("Location"); - } - - @Test - void wildcard_matching_docs_star() { - var p = RedirectRulesLoader.compileSourcePattern("/docs/*"); - assertThat(p.matcher("/docs/test").matches()).isTrue(); - assertThat(p.matcher("/docs/any/path").matches()).isFalse(); - assertThat(p.matcher("/doc/test").matches()).isFalse(); - } - - @Test - void regex_matching_via_loader_prefix() { - var p = RedirectRulesLoader.compileSourcePattern("regex:^/docs/(v1|v2)$"); - assertThat(p.matcher("/docs/v1").matches()).isTrue(); - assertThat(p.matcher("/docs/v2").matches()).isTrue(); - assertThat(p.matcher("/docs/v3").matches()).isFalse(); - } - - @Test - void redirect_rule_rejects_invalid_status_code() { - assertThatThrownBy(() -> new RedirectRule(Pattern.compile("^/x$"), "/y", 307)) - .isInstanceOf(IllegalArgumentException.class); - } -} - From b7c5eda3b3b4035a0579756d6fe8c47a5bfdbf8e Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:45:07 +0100 Subject: [PATCH 25/73] Delete src/main/java/org/example/server/HttpResponse.java --- .../java/org/example/server/HttpResponse.java | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 src/main/java/org/example/server/HttpResponse.java diff --git a/src/main/java/org/example/server/HttpResponse.java b/src/main/java/org/example/server/HttpResponse.java deleted file mode 100644 index 07ad145a..00000000 --- a/src/main/java/org/example/server/HttpResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.example.server; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -public final class HttpResponse { - private int status = 200; - private final Map headers = new LinkedHashMap<>(); - - public int status() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public Map headers() { - return Collections.unmodifiableMap(headers); - } - - public void setHeader(String name, String value) { - headers.put(name, value); - } -} From 8d348a0799a4225136782ee639c18a3e3f1738ef Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:49:31 +0100 Subject: [PATCH 26/73] Delete src/main/java/org/example/server/HttpRequest.java --- .../java/org/example/server/HttpRequest.java | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 src/main/java/org/example/server/HttpRequest.java diff --git a/src/main/java/org/example/server/HttpRequest.java b/src/main/java/org/example/server/HttpRequest.java deleted file mode 100644 index 3146c645..00000000 --- a/src/main/java/org/example/server/HttpRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.example.server; - -import java.util.Objects; - -public final class HttpRequest { - private final String method; - private final String path; - - public HttpRequest(String method, String path) { - this.method = Objects.requireNonNull(method, "method"); - this.path = Objects.requireNonNull(path, "path"); - } - - public String method() { - return method; - } - - public String path() { - return path; - } -} From ed50be15c8d5119e1d932e72df6489a2769ae9a1 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:50:21 +0100 Subject: [PATCH 27/73] Delete src/main/java/org/example/server/FilterChain.java --- .../java/org/example/server/FilterChain.java | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 src/main/java/org/example/server/FilterChain.java diff --git a/src/main/java/org/example/server/FilterChain.java b/src/main/java/org/example/server/FilterChain.java deleted file mode 100644 index 378c2a51..00000000 --- a/src/main/java/org/example/server/FilterChain.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.example.server; - -import java.util.Objects; - -public final class FilterChain { - - private final HttpFilter[] filters; - private final TerminalHandler terminal; - private int index = 0; - - public FilterChain(HttpFilter[] filters, TerminalHandler terminal) { - this.filters = Objects.requireNonNull(filters, "filters"); - this.terminal = Objects.requireNonNull(terminal, "terminal"); - } - - public void doFilter(HttpRequest request, HttpResponse response) { - if (index < filters.length) { - HttpFilter current = filters[index++]; - current.doFilter(request, response, this); - return; - } - terminal.handle(request, response); - } -} \ No newline at end of file From 1f857d2500c9bb5bf42c2fd645effbed54ead3a0 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:50:52 +0100 Subject: [PATCH 28/73] Delete src/main/java/org/example/server/HttpFilter.java --- src/main/java/org/example/server/HttpFilter.java | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/main/java/org/example/server/HttpFilter.java diff --git a/src/main/java/org/example/server/HttpFilter.java b/src/main/java/org/example/server/HttpFilter.java deleted file mode 100644 index 5ac84ed5..00000000 --- a/src/main/java/org/example/server/HttpFilter.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.example.server; - -@FunctionalInterface -public interface HttpFilter { - void doFilter(HttpRequest request, HttpResponse response, FilterChain chain); -} From 047e9c11c4f6b07949c00cc852bb210928670968 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:51:25 +0100 Subject: [PATCH 29/73] Delete src/main/java/org/example/server/TerminalHandler.java --- src/main/java/org/example/server/TerminalHandler.java | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/main/java/org/example/server/TerminalHandler.java diff --git a/src/main/java/org/example/server/TerminalHandler.java b/src/main/java/org/example/server/TerminalHandler.java deleted file mode 100644 index dc3bcfde..00000000 --- a/src/main/java/org/example/server/TerminalHandler.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.example.server; - -@FunctionalInterface -public interface TerminalHandler { - void handle(HttpRequest request, HttpResponse response); -} \ No newline at end of file From ed9c02f6fce7da656743d67491597f1094f95104 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:51:51 +0100 Subject: [PATCH 30/73] Delete src/main/java/org/example/server/RedirectRule.java --- .../java/org/example/server/RedirectRule.java | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/main/java/org/example/server/RedirectRule.java diff --git a/src/main/java/org/example/server/RedirectRule.java b/src/main/java/org/example/server/RedirectRule.java deleted file mode 100644 index 27640afd..00000000 --- a/src/main/java/org/example/server/RedirectRule.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.example.server; - -import java.util.Objects; -import java.util.regex.Pattern; - -public final class RedirectRule { - private final Pattern sourcePattern; - private final String targetUrl; - private final int statusCode; - - public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { - this.sourcePattern = Objects.requireNonNull(sourcePattern, "sourcePattern"); - this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl"); - if (statusCode != 301 && statusCode != 302) { - throw new IllegalArgumentException("statusCode must be 301 or 302"); - } - this.statusCode = statusCode; - } - - public Pattern getSourcePattern() { return sourcePattern; } - public String getTargetUrl() { return targetUrl; } - public int getStatusCode() { return statusCode; } - - public boolean matches(String requestPath) { - return sourcePattern.matcher(requestPath).matches(); - } - - @Override - public String toString() { - return "RedirectRule{" + - "sourcePattern=" + sourcePattern + - ", targetUrl='" + targetUrl + '\'' + - ", statusCode=" + statusCode + - '}'; - } -} - From 7e35661bb83ed7452aff969a27a35d9f70d11563 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:52:21 +0100 Subject: [PATCH 31/73] Delete src/main/java/org/example/server/RedirectFilter.java --- .../org/example/server/RedirectFilter.java | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 src/main/java/org/example/server/RedirectFilter.java diff --git a/src/main/java/org/example/server/RedirectFilter.java b/src/main/java/org/example/server/RedirectFilter.java deleted file mode 100644 index 07097fdd..00000000 --- a/src/main/java/org/example/server/RedirectFilter.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.example.server; - -import java.util.List; -import java.util.Objects; -import java.util.logging.Logger; - -public final class RedirectFilter implements HttpFilter { - private static final Logger LOG = Logger.getLogger(RedirectFilter.class.getName()); - private final List rules; - - public RedirectFilter(List rules) { - this.rules = List.copyOf(Objects.requireNonNull(rules, "rules")); - } - - @Override - public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) { - String path = request.path(); - - for (RedirectRule rule : rules) { - if (rule.matches(path)) { - LOG.info(() -> "Redirecting " + path + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); - response.setStatus(rule.getStatusCode()); - response.setHeader("Location", rule.getTargetUrl()); - return; // STOP pipeline - } - } - - chain.doFilter(request, response); - } -} From 9b041a816564e7be7d64d88af8e6fc8b6d56e440 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:53:07 +0100 Subject: [PATCH 32/73] Delete src/main/java/org/example/server/RedirectRulesLoader.java --- .../example/server/RedirectRulesLoader.java | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/main/java/org/example/server/RedirectRulesLoader.java diff --git a/src/main/java/org/example/server/RedirectRulesLoader.java b/src/main/java/org/example/server/RedirectRulesLoader.java deleted file mode 100644 index 26f1254c..00000000 --- a/src/main/java/org/example/server/RedirectRulesLoader.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.example.server; - -import java.util.regex.Pattern; - -public final class RedirectRulesLoader { - private static final String REGEX_PREFIX = "regex:"; - - private RedirectRulesLoader() {} - - public static Pattern compileSourcePattern(String sourcePath) { - if (sourcePath == null || sourcePath.isBlank()) { - throw new IllegalArgumentException("sourcePath must not be blank"); - } - - String trimmed = sourcePath.trim(); - - if (trimmed.startsWith(REGEX_PREFIX)) { - String rawRegex = trimmed.substring(REGEX_PREFIX.length()); - if (rawRegex.isBlank()) { - throw new IllegalArgumentException("regex sourcePath must not be blank"); - } - return Pattern.compile(rawRegex); - } - - String regex; - if (trimmed.contains("*")) { - regex = wildcardToRegex(trimmed); - } else { - regex = Pattern.quote(trimmed); - } - return Pattern.compile("^" + regex + "$"); - } - - private static String wildcardToRegex(String wildcard) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < wildcard.length(); i++) { - char c = wildcard.charAt(i); - if (c == '*') { - sb.append("[^/]*"); - } else { - sb.append(Pattern.quote(String.valueOf(c))); - } - } - return sb.toString(); - } -} - From f40e14110b6f0416cb5bd218d84b2a4711525eab Mon Sep 17 00:00:00 2001 From: Viktor Lindell Date: Mon, 23 Feb 2026 15:04:24 +0100 Subject: [PATCH 33/73] Add redirect filter rules and tests --- .../filter/redirect/RedirectFilterTest.java | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java index eb55c345..9748ac64 100644 --- a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java +++ b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java @@ -17,13 +17,7 @@ class RedirectFilterTest { private static HttpRequest request(String path) { - return new HttpRequest( - "GET", - path, - "HTTP/1.1", - Map.of(), - null - ); + return new HttpRequest("GET", path, "HTTP/1.1", Map.of(), null); } private static String responseAsString(HttpResponseBuilder response) { @@ -50,27 +44,22 @@ void returns_301_redirect_and_stops_pipeline() { } @Test -void returns_302_redirect() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) - )); - - AtomicBoolean chainCalled = new AtomicBoolean(false); - FilterChain chain = (req, res) -> { - chainCalled.set(true); - res.setStatusCode(200); - }; + void returns_302_redirect_and_stops_pipeline() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) + )); - HttpResponseBuilder res = new HttpResponseBuilder(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + FilterChain chain = (req, res) -> chainCalled.set(true); - filter.doFilter(request("/temp"), res, chain); + HttpResponseBuilder res = new HttpResponseBuilder(); - String raw = responseAsString(res); - assertThat(raw).contains("HTTP/1.1 302 Found"); - assertThat(raw).contains("Location: https://example.com/temporary"); - assertThat(chainCalled.get()).isFalse(); - } + filter.doFilter(request("/temp"), res, chain); + String raw = responseAsString(res); + assertThat(raw).contains("HTTP/1.1 302 Found"); + assertThat(raw).contains("Location: https://example.com/temporary"); + assertThat(chainCalled.get()).isFalse(); } @Test From ebfe583e679662a9e74b6aee00761586124e930b Mon Sep 17 00:00:00 2001 From: Viktor Lindell Date: Mon, 23 Feb 2026 16:09:45 +0100 Subject: [PATCH 34/73] Restore pom.xml from main --- pom.xml | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 pom.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..8a82b235 --- /dev/null +++ b/pom.xml @@ -0,0 +1,188 @@ + + + 4.0.0 + + org.example + JavaTemplate + 1.0-SNAPSHOT + + + 25 + UTF-8 + 6.0.2 + 3.27.7 + 5.21.0 + 8.14.0 + + + + + + com.bucket4j + bucket4j_jdk17-core + ${bucket4j.version} + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + org.assertj + assertj-core + ${assertj.core.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.awaitility + awaitility + 4.3.0 + test + + + + tools.jackson.core + jackson-databind + 3.0.3 + + + tools.jackson.dataformat + jackson-dataformat-yaml + 3.0.3 + + + + + + + org.apache.maven.plugins + maven-clean-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-resources-plugin + 3.4.0 + + + org.apache.maven.plugins + maven-dependency-plugin + 3.9.0 + + + + properties + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.4 + + + + integration-test + verify + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + true + + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + + + org.pitest + pitest-maven + 1.22.0 + + + org.pitest + pitest-junit5-plugin + 1.2.2 + + + + + com.diffplug.spotless + spotless-maven-plugin + 3.2.1 + + + + + + + + + verify + + + check + + + + + + + + From 5be82bf949c508855e23888e46477ccb0629e439 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:34:12 +0100 Subject: [PATCH 35/73] updated pom.xml --- pom.xml | 40 ++++++++++++++-------------------------- 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/pom.xml b/pom.xml index 8a82b235..6f4e28af 100644 --- a/pom.xml +++ b/pom.xml @@ -11,7 +11,7 @@ 25 UTF-8 - 6.0.2 + 5.11.4 3.27.7 5.21.0 8.14.0 @@ -95,24 +95,24 @@ 3.4.0 - org.apache.maven.plugins - maven-dependency-plugin - 3.9.0 - - - - properties - - - + org.apache.maven.plugins + maven-dependency-plugin + 3.9.0 + + + + properties + + + org.apache.maven.plugins maven-surefire-plugin 3.5.4 - - @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off - + + @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off + org.apache.maven.plugins @@ -150,18 +150,6 @@ - - org.pitest - pitest-maven - 1.22.0 - - - org.pitest - pitest-junit5-plugin - 1.2.2 - - - com.diffplug.spotless spotless-maven-plugin From a44524f33f93f5f6434b072e998ff4cca88bc1d5 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:38:01 +0100 Subject: [PATCH 36/73] Add HttpRequest (method, path) --- .../java/org/example/server/HttpRequest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/main/java/org/example/server/HttpRequest.java diff --git a/src/main/java/org/example/server/HttpRequest.java b/src/main/java/org/example/server/HttpRequest.java new file mode 100644 index 00000000..3146c645 --- /dev/null +++ b/src/main/java/org/example/server/HttpRequest.java @@ -0,0 +1,21 @@ +package org.example.server; + +import java.util.Objects; + +public final class HttpRequest { + private final String method; + private final String path; + + public HttpRequest(String method, String path) { + this.method = Objects.requireNonNull(method, "method"); + this.path = Objects.requireNonNull(path, "path"); + } + + public String method() { + return method; + } + + public String path() { + return path; + } +} From 0121f641de4c9112f8efc4540f90dcef7b335e6b Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:38:54 +0100 Subject: [PATCH 37/73] Add FilterChain to execute Httpfilter pipeline --- .../java/org/example/server/FilterChain.java | 28 +++++++++++++++++++ .../java/org/example/server/HttpFilter.java | 6 ++++ 2 files changed, 34 insertions(+) create mode 100644 src/main/java/org/example/server/FilterChain.java create mode 100644 src/main/java/org/example/server/HttpFilter.java diff --git a/src/main/java/org/example/server/FilterChain.java b/src/main/java/org/example/server/FilterChain.java new file mode 100644 index 00000000..ecc7cd3a --- /dev/null +++ b/src/main/java/org/example/server/FilterChain.java @@ -0,0 +1,28 @@ +package org.example.server; + +import java.util.Objects; + +public final class FilterChain { + @FunctionalInterface + public interface TerminalHandler { + void handle(HttpRequest request, HttpResponse response); + } + + private final HttpFilter[] filters; + private final TerminalHandler terminal; + private int index = 0; + + public FilterChain(HttpFilter[] filters, TerminalHandler terminal) { + this.filters = Objects.requireNonNull(filters, "filters"); + this.terminal = Objects.requireNonNull(terminal, "terminal"); + } + + public void doFilter(HttpRequest request, HttpResponse response) { + if (index < filters.length) { + HttpFilter current = filters[index++]; + current.doFilter(request, response, this); + return; + } + terminal.handle(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/org/example/server/HttpFilter.java b/src/main/java/org/example/server/HttpFilter.java new file mode 100644 index 00000000..5ac84ed5 --- /dev/null +++ b/src/main/java/org/example/server/HttpFilter.java @@ -0,0 +1,6 @@ +package org.example.server; + +@FunctionalInterface +public interface HttpFilter { + void doFilter(HttpRequest request, HttpResponse response, FilterChain chain); +} From ffb5c1c649efb81d0ccade9511d38174bb8fd54e Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:39:19 +0100 Subject: [PATCH 38/73] Add HttpResponse (status + headers + setHeader) --- .../java/org/example/server/HttpResponse.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/org/example/server/HttpResponse.java diff --git a/src/main/java/org/example/server/HttpResponse.java b/src/main/java/org/example/server/HttpResponse.java new file mode 100644 index 00000000..07ad145a --- /dev/null +++ b/src/main/java/org/example/server/HttpResponse.java @@ -0,0 +1,26 @@ +package org.example.server; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +public final class HttpResponse { + private int status = 200; + private final Map headers = new LinkedHashMap<>(); + + public int status() { + return status; + } + + public void setStatus(int status) { + this.status = status; + } + + public Map headers() { + return Collections.unmodifiableMap(headers); + } + + public void setHeader(String name, String value) { + headers.put(name, value); + } +} From 757e94deb858466482ec8afc8423ee4f3adaa531 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:40:04 +0100 Subject: [PATCH 39/73] Add RedirectRule (pattern match, targetUrl, 301/302 --- .../java/org/example/server/RedirectRule.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/org/example/server/RedirectRule.java diff --git a/src/main/java/org/example/server/RedirectRule.java b/src/main/java/org/example/server/RedirectRule.java new file mode 100644 index 00000000..df9fada0 --- /dev/null +++ b/src/main/java/org/example/server/RedirectRule.java @@ -0,0 +1,32 @@ +package org.example.server; + +import java.util.regex.Pattern; + +public final class RedirectRule { + private final Pattern sourcePattern; + private final String targetUrl; + private final int statusCode; + + public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { + this.sourcePattern = sourcePattern; + this.targetUrl = targetUrl; + this.statusCode = statusCode; + } + + public Pattern getSourcePattern() { return sourcePattern; } + public String getTargetUrl() { return targetUrl; } + public int getStatusCode() { return statusCode; } + + public boolean matches(String requestPath) { + return sourcePattern.matcher(requestPath).matches(); + } + + @Override + public String toString() { + return "RedirectRule{" + + "sourcePattern=" + sourcePattern + + ", targetUrl='" + targetUrl + '\'' + + ", statusCode=" + statusCode + + '}'; + } +} From 4bf14275f04287d513e5d9fd0fcf95c841ba307d Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:40:16 +0100 Subject: [PATCH 40/73] Add RedirectRulesLoader.compileSourcePattern (wildcard support --- .../example/server/RedirectRulesLoader.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/org/example/server/RedirectRulesLoader.java diff --git a/src/main/java/org/example/server/RedirectRulesLoader.java b/src/main/java/org/example/server/RedirectRulesLoader.java new file mode 100644 index 00000000..a7956d16 --- /dev/null +++ b/src/main/java/org/example/server/RedirectRulesLoader.java @@ -0,0 +1,34 @@ +package org.example.server; + +import java.util.regex.Pattern; + +public final class RedirectRulesLoader { + private RedirectRulesLoader() {} + + public static Pattern compileSourcePattern(String sourcePath) { + if (sourcePath == null || sourcePath.isBlank()) { + throw new IllegalArgumentException("sourcePath must not be blank"); + } + + String regex; + if (sourcePath.contains("*")) { + regex = wildcardToRegex(sourcePath); + } else { + regex = Pattern.quote(sourcePath); + } + return Pattern.compile("^" + regex + "$"); + } + + private static String wildcardToRegex(String wildcard) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < wildcard.length(); i++) { + char c = wildcard.charAt(i); + if (c == '*') { + sb.append(".*"); + } else { + sb.append(Pattern.quote(String.valueOf(c))); + } + } + return sb.toString(); + } +} From 06266b41376b1247c75c402b856b9d54c62560c2 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:40:32 +0100 Subject: [PATCH 41/73] Add RedirectFilter tests (301/302/no match/wildcard) --- .../example/server/RedirectFilterTest.java | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 src/test/java/org/example/server/RedirectFilterTest.java diff --git a/src/test/java/org/example/server/RedirectFilterTest.java b/src/test/java/org/example/server/RedirectFilterTest.java new file mode 100644 index 00000000..5a21bb6a --- /dev/null +++ b/src/test/java/org/example/server/RedirectFilterTest.java @@ -0,0 +1,75 @@ +package org.example.server; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; + +class RedirectFilterTest { + + @Test + void returns_301_redirect_and_stops_pipeline() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) + )); + + AtomicBoolean terminalCalled = new AtomicBoolean(false); + FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); + + HttpRequest req = new HttpRequest("GET", "/old-page"); + HttpResponse res = new HttpResponse(); + + chain.doFilter(req, res); + + assertThat(res.status()).isEqualTo(301); + assertThat(res.headers()).containsEntry("Location", "/new-page"); + assertThat(terminalCalled.get()).isFalse(); + } + + @Test + void returns_302_redirect() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) + )); + + FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> res.setStatus(200)); + + HttpRequest req = new HttpRequest("GET", "/temp"); + HttpResponse res = new HttpResponse(); + + chain.doFilter(req, res); + + assertThat(res.status()).isEqualTo(302); + assertThat(res.headers()).containsEntry("Location", "https://example.com/temporary"); + } + + @Test + void no_matching_rule_calls_next_in_chain() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) + )); + + AtomicBoolean terminalCalled = new AtomicBoolean(false); + FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); + + HttpRequest req = new HttpRequest("GET", "/nope"); + HttpResponse res = new HttpResponse(); + + chain.doFilter(req, res); + + assertThat(terminalCalled.get()).isTrue(); + assertThat(res.status()).isEqualTo(200); + assertThat(res.headers()).doesNotContainKey("Location"); + } + + @Test + void wildcard_matching_docs_star() { + var p = RedirectRulesLoader.compileSourcePattern("/docs/*"); + assertThat(p.matcher("/docs/test").matches()).isTrue(); + assertThat(p.matcher("/docs/any/path").matches()).isTrue(); + assertThat(p.matcher("/doc/test").matches()).isFalse(); + } +} From d4011bc1cc85293ef0d61953579cc090b20ce381 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:41:06 +0100 Subject: [PATCH 42/73] Add RedirectResponse DTO (location + statusCode) --- .../org/example/server/RedirectResponse.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/org/example/server/RedirectResponse.java diff --git a/src/main/java/org/example/server/RedirectResponse.java b/src/main/java/org/example/server/RedirectResponse.java new file mode 100644 index 00000000..9aba54e6 --- /dev/null +++ b/src/main/java/org/example/server/RedirectResponse.java @@ -0,0 +1,19 @@ +package org.example.server; + +public class RedirectResponse { + private String location; + private int statusCode; + + public RedirectResponse(String location, int statusCode) { + this.location = location; + this.statusCode = statusCode; + } + + public String getLocation() { + return location; + } + + public int getStatusCode() { + return statusCode; + } +} From 3574f0891b3ba757dc269f21c2f234cad55fbce1 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:41:27 +0100 Subject: [PATCH 43/73] Add RedirectFilter (set Location + stop chain + logging) --- .../org/example/server/RedirectFilter.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/main/java/org/example/server/RedirectFilter.java diff --git a/src/main/java/org/example/server/RedirectFilter.java b/src/main/java/org/example/server/RedirectFilter.java new file mode 100644 index 00000000..07097fdd --- /dev/null +++ b/src/main/java/org/example/server/RedirectFilter.java @@ -0,0 +1,30 @@ +package org.example.server; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +public final class RedirectFilter implements HttpFilter { + private static final Logger LOG = Logger.getLogger(RedirectFilter.class.getName()); + private final List rules; + + public RedirectFilter(List rules) { + this.rules = List.copyOf(Objects.requireNonNull(rules, "rules")); + } + + @Override + public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) { + String path = request.path(); + + for (RedirectRule rule : rules) { + if (rule.matches(path)) { + LOG.info(() -> "Redirecting " + path + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); + response.setStatus(rule.getStatusCode()); + response.setHeader("Location", rule.getTargetUrl()); + return; // STOP pipeline + } + } + + chain.doFilter(request, response); + } +} From 0f133da920b428ffb430ab0ad31f281b3e2c55df Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:49:28 +0100 Subject: [PATCH 44/73] Add RedirectFilter (set Location + stop chain + logging) --- .../org/example/server/RedirectFilterTest.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/test/java/org/example/server/RedirectFilterTest.java b/src/test/java/org/example/server/RedirectFilterTest.java index 5a21bb6a..16d3b02b 100644 --- a/src/test/java/org/example/server/RedirectFilterTest.java +++ b/src/test/java/org/example/server/RedirectFilterTest.java @@ -7,6 +7,7 @@ import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class RedirectFilterTest { @@ -72,4 +73,19 @@ void wildcard_matching_docs_star() { assertThat(p.matcher("/docs/any/path").matches()).isTrue(); assertThat(p.matcher("/doc/test").matches()).isFalse(); } + + @Test + void regex_matching_via_loader_prefix() { + var p = RedirectRulesLoader.compileSourcePattern("regex:^/docs/(v1|v2)$"); + assertThat(p.matcher("/docs/v1").matches()).isTrue(); + assertThat(p.matcher("/docs/v2").matches()).isTrue(); + assertThat(p.matcher("/docs/v3").matches()).isFalse(); + } + + @Test + void redirect_rule_rejects_invalid_status_code() { + assertThatThrownBy(() -> new RedirectRule(Pattern.compile("^/x$"), "/y", 307)) + .isInstanceOf(IllegalArgumentException.class); + } } + From 0abfe570a25c7f87866046689b47abdf11a462d1 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:50:48 +0100 Subject: [PATCH 45/73] feat: improve redirect rule parsing and validation feat: add regex support and enforce 301/302 redirects --- .../java/org/example/server/RedirectRule.java | 9 +++++++-- .../example/server/RedirectRulesLoader.java | 19 ++++++++++++++++--- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/example/server/RedirectRule.java b/src/main/java/org/example/server/RedirectRule.java index df9fada0..27640afd 100644 --- a/src/main/java/org/example/server/RedirectRule.java +++ b/src/main/java/org/example/server/RedirectRule.java @@ -1,5 +1,6 @@ package org.example.server; +import java.util.Objects; import java.util.regex.Pattern; public final class RedirectRule { @@ -8,8 +9,11 @@ public final class RedirectRule { private final int statusCode; public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { - this.sourcePattern = sourcePattern; - this.targetUrl = targetUrl; + this.sourcePattern = Objects.requireNonNull(sourcePattern, "sourcePattern"); + this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl"); + if (statusCode != 301 && statusCode != 302) { + throw new IllegalArgumentException("statusCode must be 301 or 302"); + } this.statusCode = statusCode; } @@ -30,3 +34,4 @@ public String toString() { '}'; } } + diff --git a/src/main/java/org/example/server/RedirectRulesLoader.java b/src/main/java/org/example/server/RedirectRulesLoader.java index a7956d16..1922dc81 100644 --- a/src/main/java/org/example/server/RedirectRulesLoader.java +++ b/src/main/java/org/example/server/RedirectRulesLoader.java @@ -3,6 +3,8 @@ import java.util.regex.Pattern; public final class RedirectRulesLoader { + private static final String REGEX_PREFIX = "regex:"; + private RedirectRulesLoader() {} public static Pattern compileSourcePattern(String sourcePath) { @@ -10,11 +12,21 @@ public static Pattern compileSourcePattern(String sourcePath) { throw new IllegalArgumentException("sourcePath must not be blank"); } + String trimmed = sourcePath.trim(); + + if (trimmed.startsWith(REGEX_PREFIX)) { + String rawRegex = trimmed.substring(REGEX_PREFIX.length()); + if (rawRegex.isBlank()) { + throw new IllegalArgumentException("regex sourcePath must not be blank"); + } + return Pattern.compile(rawRegex); + } + String regex; - if (sourcePath.contains("*")) { - regex = wildcardToRegex(sourcePath); + if (trimmed.contains("*")) { + regex = wildcardToRegex(trimmed); } else { - regex = Pattern.quote(sourcePath); + regex = Pattern.quote(trimmed); } return Pattern.compile("^" + regex + "$"); } @@ -32,3 +44,4 @@ private static String wildcardToRegex(String wildcard) { return sb.toString(); } } + From 6420cabbbed28055ff41006bd8b0b7c6aff1a5ec Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:48:16 +0100 Subject: [PATCH 46/73] refactor(server): extract TerminalHandler to own file --- src/main/java/org/example/server/TerminalHandler.java | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 src/main/java/org/example/server/TerminalHandler.java diff --git a/src/main/java/org/example/server/TerminalHandler.java b/src/main/java/org/example/server/TerminalHandler.java new file mode 100644 index 00000000..dc3bcfde --- /dev/null +++ b/src/main/java/org/example/server/TerminalHandler.java @@ -0,0 +1,6 @@ +package org.example.server; + +@FunctionalInterface +public interface TerminalHandler { + void handle(HttpRequest request, HttpResponse response); +} \ No newline at end of file From 2c03c8f2947993208355cbce9f96a60574b8644b Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:48:41 +0100 Subject: [PATCH 47/73] fix(server): make '*' not match '/' in redirect wildcards; update test --- src/test/java/org/example/server/RedirectFilterTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/example/server/RedirectFilterTest.java b/src/test/java/org/example/server/RedirectFilterTest.java index 16d3b02b..e7a1a448 100644 --- a/src/test/java/org/example/server/RedirectFilterTest.java +++ b/src/test/java/org/example/server/RedirectFilterTest.java @@ -70,7 +70,7 @@ void no_matching_rule_calls_next_in_chain() { void wildcard_matching_docs_star() { var p = RedirectRulesLoader.compileSourcePattern("/docs/*"); assertThat(p.matcher("/docs/test").matches()).isTrue(); - assertThat(p.matcher("/docs/any/path").matches()).isTrue(); + assertThat(p.matcher("/docs/any/path").matches()).isFalse(); assertThat(p.matcher("/doc/test").matches()).isFalse(); } From 831b1d860726b971fa5efaeeb01c7d23276dd52a Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:50:57 +0100 Subject: [PATCH 48/73] extract TerminalHandler from FilterChain into a interface --- src/main/java/org/example/server/FilterChain.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/java/org/example/server/FilterChain.java b/src/main/java/org/example/server/FilterChain.java index ecc7cd3a..378c2a51 100644 --- a/src/main/java/org/example/server/FilterChain.java +++ b/src/main/java/org/example/server/FilterChain.java @@ -3,10 +3,6 @@ import java.util.Objects; public final class FilterChain { - @FunctionalInterface - public interface TerminalHandler { - void handle(HttpRequest request, HttpResponse response); - } private final HttpFilter[] filters; private final TerminalHandler terminal; From 43f0db22ab8fc0a2570354c7f521087b20991260 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:51:19 +0100 Subject: [PATCH 49/73] make RedirectRulesLoader wildcard '*' not match '/' (avoid matching subdirectories) --- src/main/java/org/example/server/RedirectRulesLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/server/RedirectRulesLoader.java b/src/main/java/org/example/server/RedirectRulesLoader.java index 1922dc81..26f1254c 100644 --- a/src/main/java/org/example/server/RedirectRulesLoader.java +++ b/src/main/java/org/example/server/RedirectRulesLoader.java @@ -36,7 +36,7 @@ private static String wildcardToRegex(String wildcard) { for (int i = 0; i < wildcard.length(); i++) { char c = wildcard.charAt(i); if (c == '*') { - sb.append(".*"); + sb.append("[^/]*"); } else { sb.append(Pattern.quote(String.valueOf(c))); } From 3afa435c89907a1e5bca77e21e5915fa9c1e289c Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:53:59 +0100 Subject: [PATCH 50/73] removed unused RedirectResponse and .gitkeep placeholders --- .../org/example/server/RedirectResponse.java | 19 ------------------- src/main/resources/.gitkeep | 0 src/test/resources/.gitkeep | 0 3 files changed, 19 deletions(-) delete mode 100644 src/main/java/org/example/server/RedirectResponse.java delete mode 100644 src/main/resources/.gitkeep delete mode 100644 src/test/resources/.gitkeep diff --git a/src/main/java/org/example/server/RedirectResponse.java b/src/main/java/org/example/server/RedirectResponse.java deleted file mode 100644 index 9aba54e6..00000000 --- a/src/main/java/org/example/server/RedirectResponse.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.example.server; - -public class RedirectResponse { - private String location; - private int statusCode; - - public RedirectResponse(String location, int statusCode) { - this.location = location; - this.statusCode = statusCode; - } - - public String getLocation() { - return location; - } - - public int getStatusCode() { - return statusCode; - } -} diff --git a/src/main/resources/.gitkeep b/src/main/resources/.gitkeep deleted file mode 100644 index e69de29b..00000000 diff --git a/src/test/resources/.gitkeep b/src/test/resources/.gitkeep deleted file mode 100644 index e69de29b..00000000 From a5cf9868e48d23bab303dc99a30eddc37367813b Mon Sep 17 00:00:00 2001 From: Viktor Lindell Date: Fri, 20 Feb 2026 15:50:04 +0100 Subject: [PATCH 51/73] Update redirect rules and tests --- .../filter/redirect/RedirectFilter.java | 45 +++++++ .../example/filter/redirect/RedirectRule.java | 35 ++++++ .../filter/redirect/RedirectRulesLoader.java | 46 +++++++ .../filter/redirect/RedirectFilterTest.java | 113 ++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 src/main/java/org/example/filter/redirect/RedirectFilter.java create mode 100644 src/main/java/org/example/filter/redirect/RedirectRule.java create mode 100644 src/main/java/org/example/filter/redirect/RedirectRulesLoader.java create mode 100644 src/test/java/org/example/filter/redirect/RedirectFilterTest.java diff --git a/src/main/java/org/example/filter/redirect/RedirectFilter.java b/src/main/java/org/example/filter/redirect/RedirectFilter.java new file mode 100644 index 00000000..f180c9ad --- /dev/null +++ b/src/main/java/org/example/filter/redirect/RedirectFilter.java @@ -0,0 +1,45 @@ +package org.example.filter.redirect; + +import org.example.filter.Filter; +import org.example.filter.FilterChain; +import org.example.http.HttpResponseBuilder; +import org.example.httpparser.HttpRequest; + +import java.util.List; +import java.util.Objects; +import java.util.logging.Logger; + +public final class RedirectFilter implements Filter { + private static final Logger LOG = Logger.getLogger(RedirectFilter.class.getName()); + private final List rules; + + public RedirectFilter(List rules) { + this.rules = List.copyOf(Objects.requireNonNull(rules, "rules")); + } + + @Override + public void init() { + // no-op + } + + @Override + public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) { + String path = request.getPath(); + + for (RedirectRule rule : rules) { + if (rule.matches(path)) { + LOG.info(() -> "Redirecting " + path + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); + response.setStatusCode(rule.getStatusCode()); + response.setHeader("Location", rule.getTargetUrl()); + return; // STOP pipeline + } + } + + chain.doFilter(request, response); + } + + @Override + public void destroy() { + // no-op + } +} \ No newline at end of file diff --git a/src/main/java/org/example/filter/redirect/RedirectRule.java b/src/main/java/org/example/filter/redirect/RedirectRule.java new file mode 100644 index 00000000..b7b0d64d --- /dev/null +++ b/src/main/java/org/example/filter/redirect/RedirectRule.java @@ -0,0 +1,35 @@ +package org.example.filter.redirect; +import java.util.Objects; +import java.util.regex.Pattern; + +public final class RedirectRule { + private final Pattern sourcePattern; + private final String targetUrl; + private final int statusCode; + + public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { + this.sourcePattern = Objects.requireNonNull(sourcePattern, "sourcePattern"); + this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl"); + if (statusCode != 301 && statusCode != 302) { + throw new IllegalArgumentException("statusCode must be 301 or 302"); + } + this.statusCode = statusCode; + } + + public Pattern getSourcePattern() { return sourcePattern; } + public String getTargetUrl() { return targetUrl; } + public int getStatusCode() { return statusCode; } + + public boolean matches(String requestPath) { + return sourcePattern.matcher(requestPath).matches(); + } + + @Override + public String toString() { + return "RedirectRule{" + + "sourcePattern=" + sourcePattern + + ", targetUrl='" + targetUrl + '\'' + + ", statusCode=" + statusCode + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java b/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java new file mode 100644 index 00000000..431d0589 --- /dev/null +++ b/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java @@ -0,0 +1,46 @@ +package org.example.filter.redirect; + +import java.util.regex.Pattern; + +public final class RedirectRulesLoader { + private static final String REGEX_PREFIX = "regex:"; + + private RedirectRulesLoader() {} + + public static Pattern compileSourcePattern(String sourcePath) { + if (sourcePath == null || sourcePath.isBlank()) { + throw new IllegalArgumentException("sourcePath must not be blank"); + } + + String trimmed = sourcePath.trim(); + + if (trimmed.startsWith(REGEX_PREFIX)) { + String rawRegex = trimmed.substring(REGEX_PREFIX.length()); + if (rawRegex.isBlank()) { + throw new IllegalArgumentException("regex sourcePath must not be blank"); + } + return Pattern.compile(rawRegex); + } + + String regex; + if (trimmed.contains("*")) { + regex = wildcardToRegex(trimmed); + } else { + regex = Pattern.quote(trimmed); + } + return Pattern.compile("^" + regex + "$"); + } + + private static String wildcardToRegex(String wildcard) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < wildcard.length(); i++) { + char c = wildcard.charAt(i); + if (c == '*') { + sb.append("[^/]*"); + } else { + sb.append(Pattern.quote(String.valueOf(c))); + } + } + return sb.toString(); + } +} diff --git a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java new file mode 100644 index 00000000..f5d32f21 --- /dev/null +++ b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java @@ -0,0 +1,113 @@ +package org.example.filter.redirect; + +import org.example.filter.FilterChain; +import org.example.http.HttpResponseBuilder; +import org.example.httpparser.HttpRequest; +import org.junit.jupiter.api.Test; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Pattern; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RedirectFilterTest { + + private static HttpRequest request(String path) { + return new HttpRequest( + "GET", + path, + "HTTP/1.1", + Map.of(), + null + ); + } + + private static String responseAsString(HttpResponseBuilder response) { + return new String(response.build(), StandardCharsets.UTF_8); + } + + @Test + void returns_301_redirect_and_stops_pipeline() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) + )); + + AtomicBoolean chainCalled = new AtomicBoolean(false); + FilterChain chain = (req, res) -> chainCalled.set(true); + + HttpResponseBuilder res = new HttpResponseBuilder(); + + filter.doFilter(request("/old-page"), res, chain); + + String raw = responseAsString(res); + assertThat(raw).contains("HTTP/1.1 301 Moved Permanently"); + assertThat(raw).contains("Location: /new-page"); + assertThat(chainCalled.get()).isFalse(); + } + + @Test + void returns_302_redirect() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) + )); + + FilterChain chain = (req, res) -> res.setStatusCode(200); + + HttpResponseBuilder res = new HttpResponseBuilder(); + + filter.doFilter(request("/temp"), res, chain); + + String raw = responseAsString(res); + assertThat(raw).contains("HTTP/1.1 302 Found"); + assertThat(raw).contains("Location: https://example.com/temporary"); + } + + @Test + void no_matching_rule_calls_next_in_chain() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) + )); + + AtomicBoolean chainCalled = new AtomicBoolean(false); + FilterChain chain = (req, res) -> { + chainCalled.set(true); + res.setStatusCode(200); + res.setBody("terminal"); + }; + + HttpResponseBuilder res = new HttpResponseBuilder(); + + filter.doFilter(request("/nope"), res, chain); + + String raw = responseAsString(res); + assertThat(chainCalled.get()).isTrue(); + assertThat(raw).contains("HTTP/1.1 200 OK"); + assertThat(raw).doesNotContain("Location:"); + } + + @Test + void wildcard_matching_docs_star() { + var p = RedirectRulesLoader.compileSourcePattern("/docs/*"); + assertThat(p.matcher("/docs/test").matches()).isTrue(); + assertThat(p.matcher("/docs/any/path").matches()).isFalse(); + assertThat(p.matcher("/doc/test").matches()).isFalse(); + } + + @Test + void regex_matching_via_loader_prefix() { + var p = RedirectRulesLoader.compileSourcePattern("regex:^/docs/(v1|v2)$"); + assertThat(p.matcher("/docs/v1").matches()).isTrue(); + assertThat(p.matcher("/docs/v2").matches()).isTrue(); + assertThat(p.matcher("/docs/v3").matches()).isFalse(); + } + + @Test + void redirect_rule_rejects_invalid_status_code() { + assertThatThrownBy(() -> new RedirectRule(Pattern.compile("^/x$"), "/y", 307)) + .isInstanceOf(IllegalArgumentException.class); + } +} From d85ea46efc81edd03e69cd0c1f6dfd43025bd508 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:24:25 +0100 Subject: [PATCH 52/73] Update RedirectFilter.java --- .../java/org/example/filter/redirect/RedirectFilter.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectFilter.java b/src/main/java/org/example/filter/redirect/RedirectFilter.java index f180c9ad..1c28b83c 100644 --- a/src/main/java/org/example/filter/redirect/RedirectFilter.java +++ b/src/main/java/org/example/filter/redirect/RedirectFilter.java @@ -27,8 +27,8 @@ public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterCh String path = request.getPath(); for (RedirectRule rule : rules) { - if (rule.matches(path)) { - LOG.info(() -> "Redirecting " + path + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); + final String sanitizedPath = path.replaceAll("[\\r\\n]", "_"); + LOG.info(() -> "Redirecting " + sanitizedPath + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); response.setStatusCode(rule.getStatusCode()); response.setHeader("Location", rule.getTargetUrl()); return; // STOP pipeline @@ -42,4 +42,4 @@ public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterCh public void destroy() { // no-op } -} \ No newline at end of file +} From 5e34302a882677f5810cde05eac0e78553d73c5d Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:27:57 +0100 Subject: [PATCH 53/73] Update RedirectRule.java --- src/main/java/org/example/filter/redirect/RedirectRule.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectRule.java b/src/main/java/org/example/filter/redirect/RedirectRule.java index b7b0d64d..ba0a45dd 100644 --- a/src/main/java/org/example/filter/redirect/RedirectRule.java +++ b/src/main/java/org/example/filter/redirect/RedirectRule.java @@ -10,6 +10,10 @@ public final class RedirectRule { public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { this.sourcePattern = Objects.requireNonNull(sourcePattern, "sourcePattern"); this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl"); + if (this.targetUrl.isBlank()) { + throw new IllegalArgumentException("targetUrl must not be blank"); +} + if (statusCode != 301 && statusCode != 302) { throw new IllegalArgumentException("statusCode must be 301 or 302"); } @@ -32,4 +36,4 @@ public String toString() { ", statusCode=" + statusCode + '}'; } -} \ No newline at end of file +} From 96843ae48094e916a4e5a196f9c465147d7857ad Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:35:04 +0100 Subject: [PATCH 54/73] Update RedirectFilter.java --- .../filter/redirect/RedirectFilter.java | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectFilter.java b/src/main/java/org/example/filter/redirect/RedirectFilter.java index 1c28b83c..1a02f42b 100644 --- a/src/main/java/org/example/filter/redirect/RedirectFilter.java +++ b/src/main/java/org/example/filter/redirect/RedirectFilter.java @@ -24,20 +24,29 @@ public void init() { @Override public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) { - String path = request.getPath(); - - for (RedirectRule rule : rules) { - final String sanitizedPath = path.replaceAll("[\\r\\n]", "_"); - LOG.info(() -> "Redirecting " + sanitizedPath + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); - response.setStatusCode(rule.getStatusCode()); - response.setHeader("Location", rule.getTargetUrl()); - return; // STOP pipeline - } - } + String path = request.getPath(); + if (path == null) { chain.doFilter(request, response); + return; + } + + for (RedirectRule rule : rules) { + if (rule.matches(path)) { // <-- DENNA RADEN SKA FINNAS + final String sanitizedPath = path.replaceAll("[\\r\\n]", "_"); + LOG.info(() -> "Redirecting " + sanitizedPath + " -> " + + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); + + response.setStatusCode(rule.getStatusCode()); + response.setHeader("Location", rule.getTargetUrl()); + return; // STOP pipeline + } } + chain.doFilter(request, response); +} + + @Override public void destroy() { // no-op From 45d217141bc92c63207252fbe5b49c0f20794e9a Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:36:52 +0100 Subject: [PATCH 55/73] Update RedirectRulesLoader.java --- .../java/org/example/filter/redirect/RedirectRulesLoader.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java b/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java index 431d0589..779cf5d4 100644 --- a/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java +++ b/src/main/java/org/example/filter/redirect/RedirectRulesLoader.java @@ -15,7 +15,7 @@ public static Pattern compileSourcePattern(String sourcePath) { String trimmed = sourcePath.trim(); if (trimmed.startsWith(REGEX_PREFIX)) { - String rawRegex = trimmed.substring(REGEX_PREFIX.length()); + String rawRegex = trimmed.substring(REGEX_PREFIX.length()).strip(); if (rawRegex.isBlank()) { throw new IllegalArgumentException("regex sourcePath must not be blank"); } From 5d36ee2cd03b679b5e35a6747322cdb108df667e Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:42:07 +0100 Subject: [PATCH 56/73] Update RedirectFilterTest.java --- .../filter/redirect/RedirectFilterTest.java | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java index f5d32f21..eb55c345 100644 --- a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java +++ b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java @@ -50,20 +50,27 @@ void returns_301_redirect_and_stops_pipeline() { } @Test - void returns_302_redirect() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) - )); +void returns_302_redirect() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) + )); - FilterChain chain = (req, res) -> res.setStatusCode(200); + AtomicBoolean chainCalled = new AtomicBoolean(false); + FilterChain chain = (req, res) -> { + chainCalled.set(true); + res.setStatusCode(200); + }; - HttpResponseBuilder res = new HttpResponseBuilder(); + HttpResponseBuilder res = new HttpResponseBuilder(); - filter.doFilter(request("/temp"), res, chain); + filter.doFilter(request("/temp"), res, chain); + + String raw = responseAsString(res); + assertThat(raw).contains("HTTP/1.1 302 Found"); + assertThat(raw).contains("Location: https://example.com/temporary"); + assertThat(chainCalled.get()).isFalse(); + } - String raw = responseAsString(res); - assertThat(raw).contains("HTTP/1.1 302 Found"); - assertThat(raw).contains("Location: https://example.com/temporary"); } @Test From c62ed084add22d20cd12ac7c61723242541d0a9d Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:43:32 +0100 Subject: [PATCH 57/73] Delete pom.xml --- pom.xml | 176 -------------------------------------------------------- 1 file changed, 176 deletions(-) delete mode 100644 pom.xml diff --git a/pom.xml b/pom.xml deleted file mode 100644 index 6f4e28af..00000000 --- a/pom.xml +++ /dev/null @@ -1,176 +0,0 @@ - - - 4.0.0 - - org.example - JavaTemplate - 1.0-SNAPSHOT - - - 25 - UTF-8 - 5.11.4 - 3.27.7 - 5.21.0 - 8.14.0 - - - - - - com.bucket4j - bucket4j_jdk17-core - ${bucket4j.version} - - - org.junit.jupiter - junit-jupiter - ${junit.jupiter.version} - test - - - org.assertj - assertj-core - ${assertj.core.version} - test - - - org.mockito - mockito-core - ${mockito.version} - test - - - org.mockito - mockito-junit-jupiter - ${mockito.version} - test - - - org.awaitility - awaitility - 4.3.0 - test - - - - tools.jackson.core - jackson-databind - 3.0.3 - - - tools.jackson.dataformat - jackson-dataformat-yaml - 3.0.3 - - - - - - - org.apache.maven.plugins - maven-clean-plugin - 3.5.0 - - - org.apache.maven.plugins - maven-compiler-plugin - 3.15.0 - - - org.apache.maven.plugins - maven-install-plugin - 3.1.4 - - - org.apache.maven.plugins - maven-jar-plugin - 3.5.0 - - - org.apache.maven.plugins - maven-resources-plugin - 3.4.0 - - - org.apache.maven.plugins - maven-dependency-plugin - 3.9.0 - - - - properties - - - - - - org.apache.maven.plugins - maven-surefire-plugin - 3.5.4 - - @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off - - - - org.apache.maven.plugins - maven-failsafe-plugin - 3.5.4 - - - - integration-test - verify - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.14 - - true - - - - default-prepare-agent - - prepare-agent - - - - default-report - prepare-package - - report - - - - - - com.diffplug.spotless - spotless-maven-plugin - 3.2.1 - - - - - - - - - verify - - - check - - - - - - - - From ab6a8d761beab4c8ed09e0825b19038286c76b87 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:44:36 +0100 Subject: [PATCH 58/73] Delete src/test/java/org/example/server/RedirectFilterTest.java --- .../example/server/RedirectFilterTest.java | 91 ------------------- 1 file changed, 91 deletions(-) delete mode 100644 src/test/java/org/example/server/RedirectFilterTest.java diff --git a/src/test/java/org/example/server/RedirectFilterTest.java b/src/test/java/org/example/server/RedirectFilterTest.java deleted file mode 100644 index e7a1a448..00000000 --- a/src/test/java/org/example/server/RedirectFilterTest.java +++ /dev/null @@ -1,91 +0,0 @@ -package org.example.server; - -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.regex.Pattern; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -class RedirectFilterTest { - - @Test - void returns_301_redirect_and_stops_pipeline() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) - )); - - AtomicBoolean terminalCalled = new AtomicBoolean(false); - FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); - - HttpRequest req = new HttpRequest("GET", "/old-page"); - HttpResponse res = new HttpResponse(); - - chain.doFilter(req, res); - - assertThat(res.status()).isEqualTo(301); - assertThat(res.headers()).containsEntry("Location", "/new-page"); - assertThat(terminalCalled.get()).isFalse(); - } - - @Test - void returns_302_redirect() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) - )); - - FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> res.setStatus(200)); - - HttpRequest req = new HttpRequest("GET", "/temp"); - HttpResponse res = new HttpResponse(); - - chain.doFilter(req, res); - - assertThat(res.status()).isEqualTo(302); - assertThat(res.headers()).containsEntry("Location", "https://example.com/temporary"); - } - - @Test - void no_matching_rule_calls_next_in_chain() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/old-page$"), "/new-page", 301) - )); - - AtomicBoolean terminalCalled = new AtomicBoolean(false); - FilterChain chain = new FilterChain(new HttpFilter[] {filter}, (req, res) -> terminalCalled.set(true)); - - HttpRequest req = new HttpRequest("GET", "/nope"); - HttpResponse res = new HttpResponse(); - - chain.doFilter(req, res); - - assertThat(terminalCalled.get()).isTrue(); - assertThat(res.status()).isEqualTo(200); - assertThat(res.headers()).doesNotContainKey("Location"); - } - - @Test - void wildcard_matching_docs_star() { - var p = RedirectRulesLoader.compileSourcePattern("/docs/*"); - assertThat(p.matcher("/docs/test").matches()).isTrue(); - assertThat(p.matcher("/docs/any/path").matches()).isFalse(); - assertThat(p.matcher("/doc/test").matches()).isFalse(); - } - - @Test - void regex_matching_via_loader_prefix() { - var p = RedirectRulesLoader.compileSourcePattern("regex:^/docs/(v1|v2)$"); - assertThat(p.matcher("/docs/v1").matches()).isTrue(); - assertThat(p.matcher("/docs/v2").matches()).isTrue(); - assertThat(p.matcher("/docs/v3").matches()).isFalse(); - } - - @Test - void redirect_rule_rejects_invalid_status_code() { - assertThatThrownBy(() -> new RedirectRule(Pattern.compile("^/x$"), "/y", 307)) - .isInstanceOf(IllegalArgumentException.class); - } -} - From 05dfe82907c41572f089d2fdecee9a2fd4501eb9 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:45:07 +0100 Subject: [PATCH 59/73] Delete src/main/java/org/example/server/HttpResponse.java --- .../java/org/example/server/HttpResponse.java | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 src/main/java/org/example/server/HttpResponse.java diff --git a/src/main/java/org/example/server/HttpResponse.java b/src/main/java/org/example/server/HttpResponse.java deleted file mode 100644 index 07ad145a..00000000 --- a/src/main/java/org/example/server/HttpResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.example.server; - -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.Map; - -public final class HttpResponse { - private int status = 200; - private final Map headers = new LinkedHashMap<>(); - - public int status() { - return status; - } - - public void setStatus(int status) { - this.status = status; - } - - public Map headers() { - return Collections.unmodifiableMap(headers); - } - - public void setHeader(String name, String value) { - headers.put(name, value); - } -} From 76d5bd22673a7b29a82651bc7ec066ccb3261268 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:49:31 +0100 Subject: [PATCH 60/73] Delete src/main/java/org/example/server/HttpRequest.java --- .../java/org/example/server/HttpRequest.java | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 src/main/java/org/example/server/HttpRequest.java diff --git a/src/main/java/org/example/server/HttpRequest.java b/src/main/java/org/example/server/HttpRequest.java deleted file mode 100644 index 3146c645..00000000 --- a/src/main/java/org/example/server/HttpRequest.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.example.server; - -import java.util.Objects; - -public final class HttpRequest { - private final String method; - private final String path; - - public HttpRequest(String method, String path) { - this.method = Objects.requireNonNull(method, "method"); - this.path = Objects.requireNonNull(path, "path"); - } - - public String method() { - return method; - } - - public String path() { - return path; - } -} From 4f6fd8589554cb3bbcf5157bd53d1754d439ffd1 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:50:21 +0100 Subject: [PATCH 61/73] Delete src/main/java/org/example/server/FilterChain.java --- .../java/org/example/server/FilterChain.java | 24 ------------------- 1 file changed, 24 deletions(-) delete mode 100644 src/main/java/org/example/server/FilterChain.java diff --git a/src/main/java/org/example/server/FilterChain.java b/src/main/java/org/example/server/FilterChain.java deleted file mode 100644 index 378c2a51..00000000 --- a/src/main/java/org/example/server/FilterChain.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.example.server; - -import java.util.Objects; - -public final class FilterChain { - - private final HttpFilter[] filters; - private final TerminalHandler terminal; - private int index = 0; - - public FilterChain(HttpFilter[] filters, TerminalHandler terminal) { - this.filters = Objects.requireNonNull(filters, "filters"); - this.terminal = Objects.requireNonNull(terminal, "terminal"); - } - - public void doFilter(HttpRequest request, HttpResponse response) { - if (index < filters.length) { - HttpFilter current = filters[index++]; - current.doFilter(request, response, this); - return; - } - terminal.handle(request, response); - } -} \ No newline at end of file From 8e29d155e80aa7f87d74cc43e2273ca7411488b6 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:50:52 +0100 Subject: [PATCH 62/73] Delete src/main/java/org/example/server/HttpFilter.java --- src/main/java/org/example/server/HttpFilter.java | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/main/java/org/example/server/HttpFilter.java diff --git a/src/main/java/org/example/server/HttpFilter.java b/src/main/java/org/example/server/HttpFilter.java deleted file mode 100644 index 5ac84ed5..00000000 --- a/src/main/java/org/example/server/HttpFilter.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.example.server; - -@FunctionalInterface -public interface HttpFilter { - void doFilter(HttpRequest request, HttpResponse response, FilterChain chain); -} From 0428e7ea5638f805023a09b4842a0053fa37490b Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:51:25 +0100 Subject: [PATCH 63/73] Delete src/main/java/org/example/server/TerminalHandler.java --- src/main/java/org/example/server/TerminalHandler.java | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 src/main/java/org/example/server/TerminalHandler.java diff --git a/src/main/java/org/example/server/TerminalHandler.java b/src/main/java/org/example/server/TerminalHandler.java deleted file mode 100644 index dc3bcfde..00000000 --- a/src/main/java/org/example/server/TerminalHandler.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.example.server; - -@FunctionalInterface -public interface TerminalHandler { - void handle(HttpRequest request, HttpResponse response); -} \ No newline at end of file From 3ff5c2221eb2c72bafeb2d2d30f84edfc88a714a Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:51:51 +0100 Subject: [PATCH 64/73] Delete src/main/java/org/example/server/RedirectRule.java --- .../java/org/example/server/RedirectRule.java | 37 ------------------- 1 file changed, 37 deletions(-) delete mode 100644 src/main/java/org/example/server/RedirectRule.java diff --git a/src/main/java/org/example/server/RedirectRule.java b/src/main/java/org/example/server/RedirectRule.java deleted file mode 100644 index 27640afd..00000000 --- a/src/main/java/org/example/server/RedirectRule.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.example.server; - -import java.util.Objects; -import java.util.regex.Pattern; - -public final class RedirectRule { - private final Pattern sourcePattern; - private final String targetUrl; - private final int statusCode; - - public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { - this.sourcePattern = Objects.requireNonNull(sourcePattern, "sourcePattern"); - this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl"); - if (statusCode != 301 && statusCode != 302) { - throw new IllegalArgumentException("statusCode must be 301 or 302"); - } - this.statusCode = statusCode; - } - - public Pattern getSourcePattern() { return sourcePattern; } - public String getTargetUrl() { return targetUrl; } - public int getStatusCode() { return statusCode; } - - public boolean matches(String requestPath) { - return sourcePattern.matcher(requestPath).matches(); - } - - @Override - public String toString() { - return "RedirectRule{" + - "sourcePattern=" + sourcePattern + - ", targetUrl='" + targetUrl + '\'' + - ", statusCode=" + statusCode + - '}'; - } -} - From 371d28dbc21f714505458ac4413603ee146f1cad Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:52:21 +0100 Subject: [PATCH 65/73] Delete src/main/java/org/example/server/RedirectFilter.java --- .../org/example/server/RedirectFilter.java | 30 ------------------- 1 file changed, 30 deletions(-) delete mode 100644 src/main/java/org/example/server/RedirectFilter.java diff --git a/src/main/java/org/example/server/RedirectFilter.java b/src/main/java/org/example/server/RedirectFilter.java deleted file mode 100644 index 07097fdd..00000000 --- a/src/main/java/org/example/server/RedirectFilter.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.example.server; - -import java.util.List; -import java.util.Objects; -import java.util.logging.Logger; - -public final class RedirectFilter implements HttpFilter { - private static final Logger LOG = Logger.getLogger(RedirectFilter.class.getName()); - private final List rules; - - public RedirectFilter(List rules) { - this.rules = List.copyOf(Objects.requireNonNull(rules, "rules")); - } - - @Override - public void doFilter(HttpRequest request, HttpResponse response, FilterChain chain) { - String path = request.path(); - - for (RedirectRule rule : rules) { - if (rule.matches(path)) { - LOG.info(() -> "Redirecting " + path + " -> " + rule.getTargetUrl() + " (" + rule.getStatusCode() + ")"); - response.setStatus(rule.getStatusCode()); - response.setHeader("Location", rule.getTargetUrl()); - return; // STOP pipeline - } - } - - chain.doFilter(request, response); - } -} From 7bd0b82951b3e16aa375bc406d951a97da5c8755 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Mon, 23 Feb 2026 13:53:07 +0100 Subject: [PATCH 66/73] Delete src/main/java/org/example/server/RedirectRulesLoader.java --- .../example/server/RedirectRulesLoader.java | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 src/main/java/org/example/server/RedirectRulesLoader.java diff --git a/src/main/java/org/example/server/RedirectRulesLoader.java b/src/main/java/org/example/server/RedirectRulesLoader.java deleted file mode 100644 index 26f1254c..00000000 --- a/src/main/java/org/example/server/RedirectRulesLoader.java +++ /dev/null @@ -1,47 +0,0 @@ -package org.example.server; - -import java.util.regex.Pattern; - -public final class RedirectRulesLoader { - private static final String REGEX_PREFIX = "regex:"; - - private RedirectRulesLoader() {} - - public static Pattern compileSourcePattern(String sourcePath) { - if (sourcePath == null || sourcePath.isBlank()) { - throw new IllegalArgumentException("sourcePath must not be blank"); - } - - String trimmed = sourcePath.trim(); - - if (trimmed.startsWith(REGEX_PREFIX)) { - String rawRegex = trimmed.substring(REGEX_PREFIX.length()); - if (rawRegex.isBlank()) { - throw new IllegalArgumentException("regex sourcePath must not be blank"); - } - return Pattern.compile(rawRegex); - } - - String regex; - if (trimmed.contains("*")) { - regex = wildcardToRegex(trimmed); - } else { - regex = Pattern.quote(trimmed); - } - return Pattern.compile("^" + regex + "$"); - } - - private static String wildcardToRegex(String wildcard) { - StringBuilder sb = new StringBuilder(); - for (int i = 0; i < wildcard.length(); i++) { - char c = wildcard.charAt(i); - if (c == '*') { - sb.append("[^/]*"); - } else { - sb.append(Pattern.quote(String.valueOf(c))); - } - } - return sb.toString(); - } -} - From da8c1d5530b8c911a337bb6ae04cecfeadb2578a Mon Sep 17 00:00:00 2001 From: Viktor Lindell Date: Mon, 23 Feb 2026 15:04:24 +0100 Subject: [PATCH 67/73] Add redirect filter rules and tests --- .../filter/redirect/RedirectFilterTest.java | 37 +++++++------------ 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java index eb55c345..9748ac64 100644 --- a/src/test/java/org/example/filter/redirect/RedirectFilterTest.java +++ b/src/test/java/org/example/filter/redirect/RedirectFilterTest.java @@ -17,13 +17,7 @@ class RedirectFilterTest { private static HttpRequest request(String path) { - return new HttpRequest( - "GET", - path, - "HTTP/1.1", - Map.of(), - null - ); + return new HttpRequest("GET", path, "HTTP/1.1", Map.of(), null); } private static String responseAsString(HttpResponseBuilder response) { @@ -50,27 +44,22 @@ void returns_301_redirect_and_stops_pipeline() { } @Test -void returns_302_redirect() { - RedirectFilter filter = new RedirectFilter(List.of( - new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) - )); - - AtomicBoolean chainCalled = new AtomicBoolean(false); - FilterChain chain = (req, res) -> { - chainCalled.set(true); - res.setStatusCode(200); - }; + void returns_302_redirect_and_stops_pipeline() { + RedirectFilter filter = new RedirectFilter(List.of( + new RedirectRule(Pattern.compile("^/temp$"), "https://example.com/temporary", 302) + )); - HttpResponseBuilder res = new HttpResponseBuilder(); + AtomicBoolean chainCalled = new AtomicBoolean(false); + FilterChain chain = (req, res) -> chainCalled.set(true); - filter.doFilter(request("/temp"), res, chain); + HttpResponseBuilder res = new HttpResponseBuilder(); - String raw = responseAsString(res); - assertThat(raw).contains("HTTP/1.1 302 Found"); - assertThat(raw).contains("Location: https://example.com/temporary"); - assertThat(chainCalled.get()).isFalse(); - } + filter.doFilter(request("/temp"), res, chain); + String raw = responseAsString(res); + assertThat(raw).contains("HTTP/1.1 302 Found"); + assertThat(raw).contains("Location: https://example.com/temporary"); + assertThat(chainCalled.get()).isFalse(); } @Test From ae7379380b4f575c0780381bfc8ed98b48b35d7b Mon Sep 17 00:00:00 2001 From: Viktor Lindell Date: Mon, 23 Feb 2026 16:09:45 +0100 Subject: [PATCH 68/73] Restore pom.xml from main --- pom.xml | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 188 insertions(+) create mode 100644 pom.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..8a82b235 --- /dev/null +++ b/pom.xml @@ -0,0 +1,188 @@ + + + 4.0.0 + + org.example + JavaTemplate + 1.0-SNAPSHOT + + + 25 + UTF-8 + 6.0.2 + 3.27.7 + 5.21.0 + 8.14.0 + + + + + + com.bucket4j + bucket4j_jdk17-core + ${bucket4j.version} + + + org.junit.jupiter + junit-jupiter + ${junit.jupiter.version} + test + + + org.assertj + assertj-core + ${assertj.core.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + + + org.mockito + mockito-junit-jupiter + ${mockito.version} + test + + + org.awaitility + awaitility + 4.3.0 + test + + + + tools.jackson.core + jackson-databind + 3.0.3 + + + tools.jackson.dataformat + jackson-dataformat-yaml + 3.0.3 + + + + + + + org.apache.maven.plugins + maven-clean-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-compiler-plugin + 3.15.0 + + + org.apache.maven.plugins + maven-install-plugin + 3.1.4 + + + org.apache.maven.plugins + maven-jar-plugin + 3.5.0 + + + org.apache.maven.plugins + maven-resources-plugin + 3.4.0 + + + org.apache.maven.plugins + maven-dependency-plugin + 3.9.0 + + + + properties + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.4 + + @{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.5.4 + + + + integration-test + verify + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.14 + + true + + + + default-prepare-agent + + prepare-agent + + + + default-report + prepare-package + + report + + + + + + org.pitest + pitest-maven + 1.22.0 + + + org.pitest + pitest-junit5-plugin + 1.2.2 + + + + + com.diffplug.spotless + spotless-maven-plugin + 3.2.1 + + + + + + + + + verify + + + check + + + + + + + + From 7c5d1d3af2e2043ff625c061b6f2d97e043d72b2 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Wed, 25 Feb 2026 13:59:56 +0100 Subject: [PATCH 69/73] Update RedirectRule.java --- src/main/java/org/example/filter/redirect/RedirectRule.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectRule.java b/src/main/java/org/example/filter/redirect/RedirectRule.java index ba0a45dd..168b8d35 100644 --- a/src/main/java/org/example/filter/redirect/RedirectRule.java +++ b/src/main/java/org/example/filter/redirect/RedirectRule.java @@ -25,7 +25,7 @@ public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { public int getStatusCode() { return statusCode; } public boolean matches(String requestPath) { - return sourcePattern.matcher(requestPath).matches(); + return requestPath != null && sourcePattern.matcher(requestPath).matches(); } @Override From 3bd041935a15006ceb948dd296d91671006eda0e Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Wed, 25 Feb 2026 14:31:55 +0100 Subject: [PATCH 70/73] Update RedirectRule.java --- src/main/java/org/example/filter/redirect/RedirectRule.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectRule.java b/src/main/java/org/example/filter/redirect/RedirectRule.java index 168b8d35..e510d12d 100644 --- a/src/main/java/org/example/filter/redirect/RedirectRule.java +++ b/src/main/java/org/example/filter/redirect/RedirectRule.java @@ -1,4 +1,5 @@ package org.example.filter.redirect; + import java.util.Objects; import java.util.regex.Pattern; @@ -12,7 +13,10 @@ public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { this.targetUrl = Objects.requireNonNull(targetUrl, "targetUrl"); if (this.targetUrl.isBlank()) { throw new IllegalArgumentException("targetUrl must not be blank"); -} + } + if (this.targetUrl.indexOf('\r') >= 0 || this.targetUrl.indexOf('\n') >= 0) { + throw new IllegalArgumentException("targetUrl must not contain CR/LF"); + } if (statusCode != 301 && statusCode != 302) { throw new IllegalArgumentException("statusCode must be 301 or 302"); From f63637b7a1b5e5d45c032ef2996de8e3dcd83147 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 25 Feb 2026 14:54:57 +0100 Subject: [PATCH 71/73] Extend header-injection guard to null-byte characters. Fixed issue from coderabbit --- src/main/java/org/example/filter/redirect/RedirectRule.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectRule.java b/src/main/java/org/example/filter/redirect/RedirectRule.java index e510d12d..946f804c 100644 --- a/src/main/java/org/example/filter/redirect/RedirectRule.java +++ b/src/main/java/org/example/filter/redirect/RedirectRule.java @@ -14,8 +14,9 @@ public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { if (this.targetUrl.isBlank()) { throw new IllegalArgumentException("targetUrl must not be blank"); } - if (this.targetUrl.indexOf('\r') >= 0 || this.targetUrl.indexOf('\n') >= 0) { - throw new IllegalArgumentException("targetUrl must not contain CR/LF"); + if (this.targetUrl.indexOf('\r') >= 0 || this.targetUrl.indexOf('\n') >= 0 + || this.targetUrl.indexOf('\0') >= 0) { + throw new IllegalArgumentException("targetUrl must not contain CR/LF or null byte"); } if (statusCode != 301 && statusCode != 302) { From 838a917e0f53c14ffabcf1000655296e211477a2 Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Wed, 25 Feb 2026 15:29:25 +0100 Subject: [PATCH 72/73] Update RedirectRule.java --- src/main/java/org/example/filter/redirect/RedirectRule.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectRule.java b/src/main/java/org/example/filter/redirect/RedirectRule.java index 946f804c..9482684c 100644 --- a/src/main/java/org/example/filter/redirect/RedirectRule.java +++ b/src/main/java/org/example/filter/redirect/RedirectRule.java @@ -19,8 +19,8 @@ public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { throw new IllegalArgumentException("targetUrl must not contain CR/LF or null byte"); } - if (statusCode != 301 && statusCode != 302) { - throw new IllegalArgumentException("statusCode must be 301 or 302"); + if (statusCode != 301 && statusCode != 302 && statusCode != 307 && statusCode != 308) { + throw new IllegalArgumentException("statusCode must be 301, 302, 307, or 308"); } this.statusCode = statusCode; } From 30fba9cefe5f6d60dbf95f3b96084e80cef8e10f Mon Sep 17 00:00:00 2001 From: viktorlindell12 Date: Wed, 25 Feb 2026 15:54:22 +0100 Subject: [PATCH 73/73] Update RedirectRule.java --- src/main/java/org/example/filter/redirect/RedirectRule.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/example/filter/redirect/RedirectRule.java b/src/main/java/org/example/filter/redirect/RedirectRule.java index 9482684c..946f804c 100644 --- a/src/main/java/org/example/filter/redirect/RedirectRule.java +++ b/src/main/java/org/example/filter/redirect/RedirectRule.java @@ -19,8 +19,8 @@ public RedirectRule(Pattern sourcePattern, String targetUrl, int statusCode) { throw new IllegalArgumentException("targetUrl must not contain CR/LF or null byte"); } - if (statusCode != 301 && statusCode != 302 && statusCode != 307 && statusCode != 308) { - throw new IllegalArgumentException("statusCode must be 301, 302, 307, or 308"); + if (statusCode != 301 && statusCode != 302) { + throw new IllegalArgumentException("statusCode must be 301 or 302"); } this.statusCode = statusCode; }