Skip to content

Commit 89c5ddf

Browse files
Resolve TcpServer merge conflict (keep main version)
2 parents 9334691 + c0e3de6 commit 89c5ddf

18 files changed

+940
-0
lines changed

Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM maven:3-eclipse-temurin-25-alpine AS build
2+
WORKDIR /build
3+
COPY src/ src/
4+
COPY pom.xml pom.xml
5+
RUN mvn compile
6+
7+
FROM eclipse-temurin:25-jre-alpine
8+
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
9+
COPY --from=build /build/target/classes/ /app/
10+
ENTRYPOINT ["java", "-classpath", "/app", "org.example.App"]
11+
USER appuser

pom.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,16 @@
1414
<junit.jupiter.version>6.0.2</junit.jupiter.version>
1515
<assertj.core.version>3.27.7</assertj.core.version>
1616
<mockito.version>5.21.0</mockito.version>
17+
<bucket4j.version>8.14.0</bucket4j.version>
18+
1719
</properties>
1820

1921
<dependencies>
22+
<dependency>
23+
<groupId>com.bucket4j</groupId>
24+
<artifactId>bucket4j_jdk17-core</artifactId>
25+
<version>${bucket4j.version}</version>
26+
</dependency>
2027
<dependency>
2128
<groupId>org.junit.jupiter</groupId>
2229
<artifactId>junit-jupiter</artifactId>
@@ -47,6 +54,18 @@
4754
<version>4.3.0</version>
4855
<scope>test</scope>
4956
</dependency>
57+
58+
<dependency>
59+
<groupId>tools.jackson.core</groupId>
60+
<artifactId>jackson-databind</artifactId>
61+
<version>3.0.3</version>
62+
</dependency>
63+
<dependency>
64+
<groupId>tools.jackson.dataformat</groupId>
65+
<artifactId>jackson-dataformat-yaml</artifactId>
66+
<version>3.0.3</version>
67+
</dependency>
68+
5069
</dependencies>
5170
<build>
5271
<plugins>
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.example;
2+
3+
import org.example.httpparser.HttpParser;
4+
5+
import java.io.IOException;
6+
import java.net.Socket;
7+
8+
public class ConnectionHandler implements AutoCloseable {
9+
10+
Socket client;
11+
String uri;
12+
13+
public ConnectionHandler(Socket client) {
14+
this.client = client;
15+
}
16+
17+
public void runConnectionHandler() throws IOException {
18+
StaticFileHandler sfh = new StaticFileHandler();
19+
HttpParser parser = new HttpParser();
20+
parser.setReader(client.getInputStream());
21+
parser.parseRequest();
22+
parser.parseHttp();
23+
resolveTargetFile(parser.getUri());
24+
sfh.sendGetRequest(client.getOutputStream(), uri);
25+
}
26+
27+
private void resolveTargetFile(String uri) {
28+
if (uri.matches("/$")) { //matches(/)
29+
this.uri = "index.html";
30+
} else if (uri.matches("^(?!.*\\.html$).*$")) {
31+
this.uri = uri.concat(".html");
32+
} else {
33+
this.uri = uri;
34+
}
35+
36+
}
37+
38+
@Override
39+
public void close() throws Exception {
40+
client.close();
41+
}
42+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package org.example;
2+
3+
import org.example.http.HttpResponseBuilder;
4+
5+
import java.io.File;
6+
import java.io.IOException;
7+
import java.io.OutputStream;
8+
import java.io.PrintWriter;
9+
import java.nio.file.Files;
10+
import java.util.Map;
11+
12+
public class StaticFileHandler {
13+
private final String WEB_ROOT;
14+
private byte[] fileBytes;
15+
private int statusCode;
16+
17+
//Constructor for production
18+
public StaticFileHandler() {
19+
WEB_ROOT = "www";
20+
}
21+
22+
//Constructor for tests, otherwise the www folder won't be seen
23+
public StaticFileHandler(String webRoot){
24+
WEB_ROOT = webRoot;
25+
}
26+
27+
private void handleGetRequest(String uri) throws IOException {
28+
29+
File file = new File(WEB_ROOT, uri);
30+
if(file.exists()) {
31+
fileBytes = Files.readAllBytes(file.toPath());
32+
statusCode = 200;
33+
} else {
34+
File errorFile = new File(WEB_ROOT, "pageNotFound.html");
35+
if(errorFile.exists()) {
36+
fileBytes = Files.readAllBytes(errorFile.toPath());
37+
} else {
38+
fileBytes = "404 Not Found".getBytes();
39+
}
40+
statusCode = 404;
41+
}
42+
}
43+
44+
public void sendGetRequest(OutputStream outputStream, String uri) throws IOException{
45+
handleGetRequest(uri);
46+
47+
HttpResponseBuilder response = new HttpResponseBuilder();
48+
response.setStatusCode(statusCode);
49+
response.setHeaders(Map.of("Content-Type", "text/html; charset=utf-8"));
50+
response.setBody(fileBytes);
51+
PrintWriter writer = new PrintWriter(outputStream, true);
52+
writer.println(response.build());
53+
54+
}
55+
56+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package org.example;
2+
3+
import java.io.IOException;
4+
import java.net.ServerSocket;
5+
import java.net.Socket;
6+
7+
public class TcpServer {
8+
9+
private final int port;
10+
11+
public TcpServer(int port) {
12+
this.port = port;
13+
}
14+
15+
public void start() {
16+
System.out.println("Starting TCP server on port " + port);
17+
18+
try (ServerSocket serverSocket = new ServerSocket(port)) {
19+
while (true) {
20+
Socket clientSocket = serverSocket.accept(); // block
21+
System.out.println("Client connected: " + clientSocket.getRemoteSocketAddress());
22+
Thread.ofVirtual().start(() -> handleClient(clientSocket));
23+
}
24+
} catch (IOException e) {
25+
throw new RuntimeException("Failed to start TCP server", e);
26+
}
27+
}
28+
29+
private void handleClient(Socket client) {
30+
try (ConnectionHandler connectionHandler = new ConnectionHandler(client)) {
31+
connectionHandler.runConnectionHandler();
32+
} catch (Exception e) {
33+
throw new RuntimeException("Error handling client connection " + e);
34+
}
35+
}
36+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package org.example.config;
2+
3+
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
4+
import com.fasterxml.jackson.annotation.JsonProperty;
5+
6+
@JsonIgnoreProperties(ignoreUnknown = true)
7+
public record AppConfig(
8+
@JsonProperty("server") ServerConfig server,
9+
@JsonProperty("logging") LoggingConfig logging
10+
) {
11+
public static AppConfig defaults() {
12+
return new AppConfig(ServerConfig.defaults(), LoggingConfig.defaults());
13+
}
14+
15+
public AppConfig withDefaultsApplied() {
16+
ServerConfig serverConfig = (server == null ? ServerConfig.defaults() : server.withDefaultsApplied());
17+
LoggingConfig loggingConfig = (logging == null ? LoggingConfig.defaults() : logging.withDefaultsApplied());
18+
return new AppConfig(serverConfig, loggingConfig);
19+
}
20+
21+
@JsonIgnoreProperties(ignoreUnknown = true)
22+
public record ServerConfig(
23+
@JsonProperty("port") Integer port,
24+
@JsonProperty("rootDir") String rootDir
25+
) {
26+
public static ServerConfig defaults() {
27+
return new ServerConfig(8080, "./www");
28+
}
29+
30+
public ServerConfig withDefaultsApplied() {
31+
int p = (port == null ? 8080 : port);
32+
if (p < 1 || p > 65535) {
33+
throw new IllegalArgumentException("Invalid port number: " + p + ". Port must be between 1 and 65535");
34+
}
35+
String rd = (rootDir == null || rootDir.isBlank()) ? "./www" : rootDir;
36+
return new ServerConfig(p, rd);
37+
}
38+
}
39+
40+
@JsonIgnoreProperties(ignoreUnknown = true)
41+
public record LoggingConfig(
42+
@JsonProperty("level") String level
43+
) {
44+
public static LoggingConfig defaults() {
45+
return new LoggingConfig("INFO");
46+
}
47+
48+
public LoggingConfig withDefaultsApplied() {
49+
String lvl = (level == null || level.isBlank()) ? "INFO" : level;
50+
return new LoggingConfig(lvl);
51+
}
52+
}
53+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.example.config;
2+
3+
import tools.jackson.databind.ObjectMapper;
4+
import tools.jackson.databind.json.JsonMapper;
5+
import tools.jackson.dataformat.yaml.YAMLFactory;
6+
import tools.jackson.dataformat.yaml.YAMLMapper;
7+
8+
import java.io.InputStream;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.Objects;
12+
13+
public final class ConfigLoader {
14+
15+
private static volatile AppConfig cached;
16+
17+
private ConfigLoader() {}
18+
19+
public static AppConfig loadOnce(Path configPath) {
20+
if (cached != null) return cached;
21+
22+
synchronized (ConfigLoader.class) {
23+
if (cached == null){
24+
cached = load(configPath).withDefaultsApplied();
25+
}
26+
return cached;
27+
}
28+
}
29+
30+
public static AppConfig get(){
31+
if (cached == null){
32+
throw new IllegalStateException("Config not loaded. call ConfigLoader.loadOnce(...) at startup.");
33+
}
34+
return cached;
35+
36+
}
37+
38+
public static AppConfig load(Path configPath) {
39+
Objects.requireNonNull(configPath, "configPath");
40+
41+
if (!Files.exists(configPath)) {
42+
return AppConfig.defaults();
43+
}
44+
45+
ObjectMapper objectMapper = createMapperFor(configPath);
46+
47+
try (InputStream stream = Files.newInputStream(configPath)){
48+
AppConfig config = objectMapper.readValue(stream, AppConfig.class);
49+
return config == null ? AppConfig.defaults() : config;
50+
} catch (Exception e){
51+
throw new IllegalStateException("failed to read config file " + configPath.toAbsolutePath(), e);
52+
}
53+
}
54+
55+
private static ObjectMapper createMapperFor(Path configPath) {
56+
String name = configPath.getFileName().toString().toLowerCase();
57+
58+
if (name.endsWith(".yml") || name.endsWith(".yaml")) {
59+
return YAMLMapper.builder(new YAMLFactory()).build();
60+
61+
} else if (name.endsWith(".json")) {
62+
return JsonMapper.builder().build();
63+
} else {
64+
return YAMLMapper.builder(new YAMLFactory()).build();
65+
}
66+
}
67+
68+
static void resetForTests() {
69+
cached = null;
70+
}
71+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.example.http;
2+
//
3+
import java.nio.charset.StandardCharsets;
4+
import java.util.LinkedHashMap;
5+
import java.util.Map;
6+
7+
public class HttpResponseBuilder {
8+
9+
private static final String PROTOCOL = "HTTP/1.1";
10+
private int statusCode = 200;
11+
private String body = "";
12+
private byte[] bytebody;
13+
private Map<String, String> headers = new LinkedHashMap<>();
14+
15+
private static final String CRLF = "\r\n";
16+
17+
18+
public void setStatusCode(int statusCode) {
19+
this.statusCode = statusCode;
20+
}
21+
public void setBody(String body) {
22+
this.body = body != null ? body : "";
23+
}
24+
25+
public void setBody(byte[] body) {
26+
this.bytebody = body;
27+
}
28+
public void setHeaders(Map<String, String> headers) {
29+
this.headers = new LinkedHashMap<>(headers);
30+
}
31+
32+
private static final Map<Integer, String> REASON_PHRASES = Map.of(
33+
200, "OK",
34+
201, "Created",
35+
400, "Bad Request",
36+
404, "Not Found",
37+
500, "Internal Server Error");
38+
public String build(){
39+
StringBuilder sb = new StringBuilder();
40+
int contentLength;
41+
if(body.isEmpty() && bytebody != null){
42+
contentLength = bytebody.length;
43+
setBody(new String(bytebody, StandardCharsets.UTF_8));
44+
}else{
45+
contentLength = body.getBytes(StandardCharsets.UTF_8).length;
46+
}
47+
48+
49+
String reason = REASON_PHRASES.getOrDefault(statusCode, "OK");
50+
sb.append(PROTOCOL).append(" ").append(statusCode).append(" ").append(reason).append(CRLF);
51+
headers.forEach((k,v) -> sb.append(k).append(": ").append(v).append(CRLF));
52+
sb.append("Content-Length: ")
53+
.append(contentLength);
54+
sb.append(CRLF);
55+
sb.append("Connection: close").append(CRLF);
56+
sb.append(CRLF);
57+
sb.append(body);
58+
return sb.toString();
59+
60+
}
61+
62+
}

0 commit comments

Comments
 (0)