From 98b40a1f09cc59c686898602bbf7a5a46441b111 Mon Sep 17 00:00:00 2001 From: Kathrin Trinh Date: Tue, 10 Feb 2026 14:44:16 +0100 Subject: [PATCH 01/20] Initial commit med all kod (#16) Add ci workflow for github actions. --- .github/workflows/ci.yml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..9610edff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: Java CI with Maven + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Get Java Version + run: | + JAVA_VERSION=$(mvn help:evaluate "-Dexpression=maven.compiler.release" -q -DforceStdout) + echo "JAVA_VERSION=$JAVA_VERSION" >> $GITHUB_ENV + + - name: Set up JDK ${{ env.JAVA_VERSION }} + uses: actions/setup-java@v4 + with: + java-version: ${{ env.JAVA_VERSION }} + distribution: 'temurin' + cache: maven + + - name: Compile with Maven + run: mvn -B compile --file pom.xml + + - name: Test with Maven + run: mvn -B test --file pom.xml \ No newline at end of file From c1b5f7009a6f2b19dcf63ea965bf44efd5453d7a Mon Sep 17 00:00:00 2001 From: Ebba Andersson Date: Tue, 10 Feb 2026 14:45:46 +0100 Subject: [PATCH 02/20] build: configure pom.xml with needed plugin/tools. (#19) * build: configure pom.xml with needed plugin/tools. Setup Java 25 environment with JUnit 5, Mockito, JaCoCo, Pitest, and Spotless * fix: add missing test scope for awaitility --- pom.xml | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 6b7ade11..bb03ce76 100644 --- a/pom.xml +++ b/pom.xml @@ -9,12 +9,13 @@ 1.0-SNAPSHOT - 23 + 25 UTF-8 6.0.2 3.27.7 5.21.0 + org.junit.jupiter @@ -28,12 +29,24 @@ ${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 + @@ -118,6 +131,39 @@ + + 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 bf4d977cb9a4b722cfc7f56f168a6afb0b78d1b4 Mon Sep 17 00:00:00 2001 From: Kathrin Trinh Date: Tue, 10 Feb 2026 15:14:19 +0100 Subject: [PATCH 03/20] =?UTF-8?q?Initial=20commit=20f=C3=B6r=20tcp-server?= =?UTF-8?q?=20(#17)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/org/example/App.java | 4 ++-- src/main/java/org/example/TcpServer.java | 28 ++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/example/TcpServer.java diff --git a/src/main/java/org/example/App.java b/src/main/java/org/example/App.java index 165e5cd5..66c9af10 100644 --- a/src/main/java/org/example/App.java +++ b/src/main/java/org/example/App.java @@ -2,6 +2,6 @@ public class App { public static void main(String[] args) { - System.out.println("Hello There!"); + new TcpServer(8080).start(); } -} +} \ No newline at end of file diff --git a/src/main/java/org/example/TcpServer.java b/src/main/java/org/example/TcpServer.java new file mode 100644 index 00000000..73ba0f27 --- /dev/null +++ b/src/main/java/org/example/TcpServer.java @@ -0,0 +1,28 @@ +package org.example; + +import java.io.IOException; +import java.net.ServerSocket; +import java.net.Socket; + +public class TcpServer { + + private final int port; + + public TcpServer(int port) { + this.port = port; + } + + public void start() { + System.out.println("Starting TCP server on port " + port); + + try (ServerSocket serverSocket = new ServerSocket(port)) { + while (true) { + Socket clientSocket = serverSocket.accept(); // block + System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress()); + clientSocket.close(); + } + } catch (IOException e) { + throw new RuntimeException("Failed to start TCP server", e); + } + } +} \ No newline at end of file From 148411e2d8dda4936f543fbd7a9e74efff668026 Mon Sep 17 00:00:00 2001 From: Elias Lennheimer <47382348+Xeutos@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:33:02 +0100 Subject: [PATCH 04/20] Issue #12 (#21) * release.yml that builds and publishes Docker image to GitHub packages on release. * Fixed unverified commit stopping pull request from being merged --- .github/workflows/release.yml | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..b86973f7 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,35 @@ +name: Publish Docker Image to Github Packages on Release +on: + release: + types: + - published +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v6.0.2 + - uses: docker/setup-qemu-action@v3.7.0 + - uses: docker/setup-buildx-action@v3.12.0 + - name: Log in to GHCR + uses: docker/login-action@v3.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5.10.0 + with: + images: ghcr.io/ithsjava25/webserver + - name: Build and push + uses: docker/build-push-action@v6.18.0 + with: + context: . + push: true + platforms: linux/amd64, linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + From 696e1bc5a84e6295393ff4b8dc81dad78a663de8 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:34:12 +0100 Subject: [PATCH 05/20] 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 06/20] 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 07/20] 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 08/20] 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 09/20] 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 ab1d054fdcc6c82ad7dae5423ba51fba78bdec1c Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:40:16 +0100 Subject: [PATCH 10/20] 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 9e0610a2831aff3c55e2d6c4d3dc493496357636 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:40:32 +0100 Subject: [PATCH 11/20] 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 8e144debd762e539daf03e42026842b5db7eb6b9 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:41:06 +0100 Subject: [PATCH 12/20] 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 53e11a36e127e076e7206d6b0d8d574667aae5de Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:41:27 +0100 Subject: [PATCH 13/20] 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 58882cc12a10e0804feecef07afe2a1780d0e3e8 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:49:28 +0100 Subject: [PATCH 14/20] 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 aaa4748195a9f97e08c960932fa40215c80689c7 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 17:50:48 +0100 Subject: [PATCH 15/20] 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 8c9a1732a063fdfadfc2d2cdceb34c7e309e6cd3 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:48:16 +0100 Subject: [PATCH 16/20] 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 a0cefabe15f143f7362f1a27b6947a19d1609b10 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:48:41 +0100 Subject: [PATCH 17/20] 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 e5f50efd931b0c1ced04cdd65723983d60753117 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:50:57 +0100 Subject: [PATCH 18/20] 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 512627dbafae664899c59252996b55372d39d6f0 Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:51:19 +0100 Subject: [PATCH 19/20] 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 42f23cd5d3b36e468cd310cb8cb4feb7ec53877b Mon Sep 17 00:00:00 2001 From: Eric Thilen Date: Wed, 11 Feb 2026 20:53:59 +0100 Subject: [PATCH 20/20] 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