Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
98b40a1
Initial commit med all kod (#16)
Kathify Feb 10, 2026
c1b5f70
build: configure pom.xml with needed plugin/tools. (#19)
eeebbaandersson Feb 10, 2026
bf4d977
Initial commit för tcp-server (#17)
Kathify Feb 10, 2026
148411e
Issue #12 (#21)
Xeutos Feb 10, 2026
696e1bc
updated pom.xml
Ericthilen Feb 11, 2026
de1b6cf
Add HttpRequest (method, path)
Ericthilen Feb 11, 2026
e21bc8c
Add FilterChain to execute Httpfilter pipeline
Ericthilen Feb 11, 2026
78e8df0
Add HttpResponse (status + headers + setHeader)
Ericthilen Feb 11, 2026
0ed2a79
Add RedirectRule (pattern match, targetUrl, 301/302
Ericthilen Feb 11, 2026
ab1d054
Add RedirectRulesLoader.compileSourcePattern (wildcard support
Ericthilen Feb 11, 2026
9e0610a
Add RedirectFilter tests (301/302/no match/wildcard)
Ericthilen Feb 11, 2026
8e144de
Add RedirectResponse DTO (location + statusCode)
Ericthilen Feb 11, 2026
53e11a3
Add RedirectFilter (set Location + stop chain + logging)
Ericthilen Feb 11, 2026
58882cc
Add RedirectFilter (set Location + stop chain + logging)
Ericthilen Feb 11, 2026
aaa4748
feat: improve redirect rule parsing and validation
Ericthilen Feb 11, 2026
8c9a173
refactor(server): extract TerminalHandler to own file
Ericthilen Feb 11, 2026
a0cefab
fix(server): make '*' not match '/' in redirect wildcards; update test
Ericthilen Feb 11, 2026
e5f50ef
extract TerminalHandler from FilterChain into a interface
Ericthilen Feb 11, 2026
512627d
make RedirectRulesLoader wildcard '*' not match '/' (avoid matching s…
Ericthilen Feb 11, 2026
42f23cd
removed unused RedirectResponse and .gitkeep placeholders
Ericthilen Feb 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -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
35 changes: 35 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}

64 changes: 49 additions & 15 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@
<version>1.0-SNAPSHOT</version>

<properties>
<maven.compiler.release>23</maven.compiler.release>
<maven.compiler.release>25</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.jupiter.version>6.0.2</junit.jupiter.version>
<junit.jupiter.version>5.11.4</junit.jupiter.version>
<assertj.core.version>3.27.7</assertj.core.version>
<mockito.version>5.21.0</mockito.version>
</properties>

<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
Expand All @@ -28,12 +29,24 @@
<version>${assertj.core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.3.0</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
Expand Down Expand Up @@ -63,24 +76,24 @@
<version>3.4.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.9.0</version>
<executions>
<execution>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>3.9.0</version>
<executions>
<execution>
<goals>
<goal>properties</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.4</version>
<configuration>
<argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
</configuration>
<configuration>
<argLine>@{argLine} -javaagent:${org.mockito:mockito-core:jar} -Xshare:off</argLine>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
Expand Down Expand Up @@ -118,6 +131,27 @@
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.diffplug.spotless</groupId>
<artifactId>spotless-maven-plugin</artifactId>
<version>3.2.1</version>
<configuration>
<java>
<removeUnusedImports/>
<formatAnnotations/>
</java>
</configuration>
<executions>
<execution>
<phase>verify</phase>
<goals>
<goal>
check
</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
4 changes: 2 additions & 2 deletions src/main/java/org/example/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

public class App {
public static void main(String[] args) {
System.out.println("Hello There!");
new TcpServer(8080).start();
}
}
}
28 changes: 28 additions & 0 deletions src/main/java/org/example/TcpServer.java
Original file line number Diff line number Diff line change
@@ -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();
}
Comment on lines +19 to +23
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Client socket is not closed in a try-with-resources — potential leak.

If anything throws between accept() and close() (e.g., an added handler in the future), the socket leaks. Use try-with-resources for safety.

Proposed fix
             while (true) {
-                Socket clientSocket = serverSocket.accept(); // block
-                System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
-                clientSocket.close();
+                try (Socket clientSocket = serverSocket.accept()) {
+                    System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
+                }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while (true) {
Socket clientSocket = serverSocket.accept(); // block
System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
clientSocket.close();
}
while (true) {
try (Socket clientSocket = serverSocket.accept()) {
System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
}
}
🤖 Prompt for AI Agents
In `@src/main/java/org/example/TcpServer.java` around lines 19 - 23, The loop in
TcpServer uses serverSocket.accept() and then calls clientSocket.close()
directly, which can leak the socket if an exception occurs; update the
accept/close handling to use a try-with-resources for the Socket returned by
serverSocket.accept() (e.g., wrap "Socket clientSocket = serverSocket.accept()"
in a try(...) block) so the socket is always closed, and adjust surrounding
exception handling as needed in the method containing the loop to handle
IOExceptions.

} catch (IOException e) {
throw new RuntimeException("Failed to start TCP server", e);
}
}
}
24 changes: 24 additions & 0 deletions src/main/java/org/example/server/FilterChain.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
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);
}
}
6 changes: 6 additions & 0 deletions src/main/java/org/example/server/HttpFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.example.server;

@FunctionalInterface
public interface HttpFilter {
void doFilter(HttpRequest request, HttpResponse response, FilterChain chain);
}
21 changes: 21 additions & 0 deletions src/main/java/org/example/server/HttpRequest.java
Original file line number Diff line number Diff line change
@@ -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;
}
}
26 changes: 26 additions & 0 deletions src/main/java/org/example/server/HttpResponse.java
Original file line number Diff line number Diff line change
@@ -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<String, String> headers = new LinkedHashMap<>();

public int status() {
return status;
}

public void setStatus(int status) {
this.status = status;
}

public Map<String, String> headers() {
return Collections.unmodifiableMap(headers);
}

public void setHeader(String name, String value) {
headers.put(name, value);
}
}
30 changes: 30 additions & 0 deletions src/main/java/org/example/server/RedirectFilter.java
Original file line number Diff line number Diff line change
@@ -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<RedirectRule> rules;

public RedirectFilter(List<RedirectRule> 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);
}
}
37 changes: 37 additions & 0 deletions src/main/java/org/example/server/RedirectRule.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
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();
}
Comment on lines +24 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

matches() will throw NPE when requestPath is null.

Pattern.matcher(null) throws NullPointerException. If any upstream component passes a null path (e.g., a malformed HTTP request where the path could not be parsed), the exception will bubble up the filter chain uncaught instead of cleanly returning false.

🛡️ Proposed fix
     public boolean matches(String requestPath) {
-        return sourcePattern.matcher(requestPath).matches();
+        if (requestPath == null) return false;
+        return sourcePattern.matcher(requestPath).matches();
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public boolean matches(String requestPath) {
return sourcePattern.matcher(requestPath).matches();
}
public boolean matches(String requestPath) {
if (requestPath == null) return false;
return sourcePattern.matcher(requestPath).matches();
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/org/example/filter/redirect/RedirectRule.java` around lines 27
- 29, The matches() method currently calls sourcePattern.matcher(requestPath)
which will throw a NullPointerException if requestPath is null; update
matches(String requestPath) in RedirectRule to guard against null by returning
false when requestPath is null (i.e., check requestPath == null before calling
sourcePattern.matcher(...).matches()), ensuring sourcePattern is still used only
when requestPath is non-null.


@Override
public String toString() {
return "RedirectRule{" +
"sourcePattern=" + sourcePattern +
", targetUrl='" + targetUrl + '\'' +
", statusCode=" + statusCode +
'}';
}
}

Loading