Skip to content

Commit 32cbdd3

Browse files
authored
Merge branch 'main' into issue36
2 parents 2f8312b + fa1599a commit 32cbdd3

14 files changed

Lines changed: 600 additions & 8 deletions

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@
6565
<artifactId>jackson-dataformat-yaml</artifactId>
6666
<version>3.0.3</version>
6767
</dependency>
68+
<dependency>
69+
<groupId>com.aayushatharva.brotli4j</groupId>
70+
<artifactId>brotli4j</artifactId>
71+
<version>1.20.0</version>
72+
</dependency>
6873

6974
</dependencies>
7075
<build>

src/main/java/org/example/App.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.example.config.AppConfig;
44
import org.example.config.ConfigLoader;
55

6+
import java.net.Socket;
67
import java.nio.file.Path;
78

89
public class App {
@@ -16,7 +17,7 @@ public static void main(String[] args) {
1617

1718
int port = resolvePort(args, appConfig.server().port());
1819

19-
new TcpServer(port).start();
20+
new TcpServer(port, ConnectionHandler::new).start();
2021
}
2122

2223
static int resolvePort(String[] args, int configPort) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package org.example;
2+
3+
import java.net.Socket;
4+
5+
public interface ConnectionFactory {
6+
ConnectionHandler create(Socket socket);
7+
}

src/main/java/org/example/ConnectionHandler.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,18 @@
2121
public class ConnectionHandler implements AutoCloseable {
2222
private final Socket client;
2323
private final List<Filter> filters;
24+
String webRoot;
2425

2526
public ConnectionHandler(Socket client) {
2627
this.client = client;
2728
this.filters = buildFilters();
29+
this.webRoot = null;
30+
}
31+
32+
public ConnectionHandler(Socket client, String webRoot) {
33+
this.client = client;
34+
this.webRoot = webRoot;
35+
this.filters = buildFilters();
2836
}
2937

3038
private List<Filter> buildFilters() {
@@ -38,6 +46,14 @@ private List<Filter> buildFilters() {
3846
}
3947

4048
public void runConnectionHandler() throws IOException {
49+
StaticFileHandler sfh;
50+
51+
if (webRoot != null) {
52+
sfh = new StaticFileHandler(webRoot);
53+
} else {
54+
sfh = new StaticFileHandler();
55+
}
56+
4157
HttpParser parser = new HttpParser();
4258
parser.setReader(client.getInputStream());
4359
parser.parseRequest();
@@ -83,6 +99,8 @@ private String sanitizeUri(String uri) {
8399
.replace("\0", "")
84100
.replaceAll("^/+", "")
85101
.replaceAll("^$", "index.html");
102+
resolveTargetFile(parser.getUri());
103+
sfh.sendGetRequest(client.getOutputStream(), uri);
86104
}
87105

88106
private HttpResponseBuilder applyFilters(HttpRequest request) {
@@ -92,6 +110,14 @@ private HttpResponseBuilder applyFilters(HttpRequest request) {
92110
return response;
93111
}
94112

113+
private void resolveTargetFile(String uri) {
114+
if (uri == null || "/".equals(uri)) {
115+
this.uri = "index.html";
116+
} else {
117+
this.uri = uri.startsWith("/") ? uri.substring(1) : uri;
118+
}
119+
}
120+
95121
@Override
96122
public void close() throws Exception {
97123
client.close();
@@ -117,4 +143,4 @@ private IpFilter createIpFilterFromConfig(AppConfig.IpFilterConfig config) {
117143
filter.init();
118144
return filter;
119145
}
120-
}
146+
}

src/main/java/org/example/TcpServer.java

Lines changed: 44 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
package org.example;
22

3+
import org.example.http.HttpResponseBuilder;
4+
35
import java.io.IOException;
6+
import java.io.OutputStream;
7+
import java.io.PrintWriter;
48
import java.net.ServerSocket;
59
import java.net.Socket;
10+
import java.nio.charset.StandardCharsets;
11+
import java.util.Map;
612

713
public class TcpServer {
814

915
private final int port;
16+
private final ConnectionFactory connectionFactory;
1017

11-
public TcpServer(int port) {
18+
public TcpServer(int port, ConnectionFactory connectionFactory) {
1219
this.port = port;
20+
this.connectionFactory = connectionFactory;
1321
}
1422

1523
public void start() {
@@ -26,11 +34,42 @@ public void start() {
2634
}
2735
}
2836

29-
private void handleClient(Socket client) {
30-
try (ConnectionHandler connectionHandler = new ConnectionHandler(client)) {
31-
connectionHandler.runConnectionHandler();
37+
protected void handleClient(Socket client) {
38+
try(client){
39+
processRequest(client);
40+
} catch (Exception e) {
41+
throw new RuntimeException("Failed to close socket", e);
42+
}
43+
}
44+
45+
private void processRequest(Socket client) throws Exception {
46+
ConnectionHandler handler = null;
47+
try{
48+
handler = connectionFactory.create(client);
49+
handler.runConnectionHandler();
3250
} catch (Exception e) {
33-
throw new RuntimeException("Error handling client connection " + e);
51+
handleInternalServerError(client);
52+
} finally {
53+
if(handler != null)
54+
handler.close();
55+
}
56+
}
57+
58+
59+
private void handleInternalServerError(Socket client){
60+
HttpResponseBuilder response = new HttpResponseBuilder();
61+
response.setStatusCode(HttpResponseBuilder.SC_INTERNAL_SERVER_ERROR);
62+
response.setHeaders(Map.of("Content-Type", "text/plain; charset=utf-8"));
63+
response.setBody("⚠️ Internal Server Error 500 ⚠️");
64+
65+
if (!client.isClosed()) {
66+
try {
67+
OutputStream out = client.getOutputStream();
68+
out.write(response.build());
69+
out.flush();
70+
} catch (IOException e) {
71+
System.err.println("Failed to send 500 response: " + e.getMessage());
72+
}
3473
}
3574
}
3675
}

src/main/java/org/example/config/ConfigLoader.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ private static ObjectMapper createMapperFor(Path configPath) {
6565
}
6666
}
6767

68-
static void resetForTests() {
68+
public static void resetForTests() {
6969
cached = null;
7070
}
7171
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package org.example.filter;
2+
3+
import org.example.http.HttpResponseBuilder;
4+
import org.example.httpparser.HttpRequest;
5+
6+
import java.util.Map;
7+
8+
/**
9+
* Filter that determines the preferred locale for an HTTP request using cookies and headers.
10+
* <p>
11+
* First, it checks for a locale set in a cookie named "user-lang". If the cookie is missing,
12+
* blank, or malformed, it falls back to the Accept-Language header. If neither is present
13+
* or valid, the filter defaults to "en-US".
14+
* <p>
15+
* The selected locale is stored in a ThreadLocal variable so it can be accessed throughout
16+
* the processing of the request.
17+
* <p>
18+
* This filter does not modify the response or stop the filter chain; it only sets the
19+
* current locale and forwards the request to the next filter.
20+
* <p>
21+
* ThreadLocal cleanup is performed after the filter chain completes to prevent memory leaks.
22+
*/
23+
public class LocaleFilterWithCookie implements Filter {
24+
25+
private static final String DEFAULT_LOCALE = "en-US";
26+
private static final String LOCALE_COOKIE_NAME = "user-lang";
27+
private static final ThreadLocal<String> currentLocale = new ThreadLocal<>();
28+
29+
@Override
30+
public void init() {
31+
}
32+
33+
@Override
34+
public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) {
35+
try {
36+
String locale = resolveLocale(request);
37+
currentLocale.set(locale);
38+
39+
chain.doFilter(request, response);
40+
} finally {
41+
currentLocale.remove();
42+
}
43+
}
44+
45+
@Override
46+
public void destroy() {
47+
}
48+
49+
public static String getCurrentLocale() {
50+
String locale = currentLocale.get();
51+
if (locale != null) {
52+
return locale;
53+
} else {
54+
return DEFAULT_LOCALE;
55+
}
56+
}
57+
58+
private String resolveLocale(HttpRequest request) {
59+
String cookieLocale = extractLocaleFromCookie(request);
60+
if (cookieLocale != null && !cookieLocale.isBlank()) {
61+
return cookieLocale;
62+
}
63+
64+
String headerLocale = extractLocaleFromHeader(request);
65+
if (headerLocale != null && !headerLocale.isBlank()) {
66+
return headerLocale;
67+
}
68+
69+
return DEFAULT_LOCALE;
70+
}
71+
72+
/**
73+
* Extracts the locale from the "user-lang" cookie if present.
74+
* <p>
75+
* If the cookie header is missing, blank, or malformed, returns null.
76+
*/
77+
private String extractLocaleFromCookie(HttpRequest request) {
78+
Map<String, String> headers = request.getHeaders();
79+
if (headers == null) {
80+
return null;
81+
}
82+
83+
String cookieHeader = headers.entrySet().stream()
84+
.filter(e -> e.getKey() != null && e.getKey().equalsIgnoreCase("Cookie"))
85+
.map(Map.Entry::getValue)
86+
.findFirst()
87+
.orElse(null);
88+
89+
if (cookieHeader == null || cookieHeader.isBlank()) {
90+
return null;
91+
}
92+
93+
String[] cookies = cookieHeader.split(";");
94+
for (String cookie : cookies) {
95+
String[] parts = cookie.trim().split("=", 2);
96+
if (parts.length == 2) {
97+
String name = parts[0].trim();
98+
String value = parts[1].trim();
99+
if (LOCALE_COOKIE_NAME.equals(name) && !value.isBlank()) {
100+
return value;
101+
}
102+
}
103+
}
104+
105+
return null;
106+
}
107+
108+
/**
109+
* Extracts the preferred locale from the Accept-Language header of the request.
110+
* <p>
111+
* If the header is missing, blank, or malformed, returns null.
112+
* The first language tag is used and any optional quality value (e.g., ";q=0.9") is stripped.
113+
*/
114+
private String extractLocaleFromHeader(HttpRequest request) {
115+
Map<String, String> headers = request.getHeaders();
116+
if (headers == null) {
117+
return null;
118+
}
119+
120+
String acceptLanguage = headers.entrySet().stream()
121+
.filter(e -> e.getKey() != null && e.getKey().equalsIgnoreCase("Accept-Language"))
122+
.map(Map.Entry::getValue)
123+
.findFirst()
124+
.orElse(null);
125+
126+
if (acceptLanguage == null || acceptLanguage.isBlank()) {
127+
return null;
128+
}
129+
130+
String[] parts = acceptLanguage.split(",");
131+
if (parts.length == 0 || parts[0].isBlank()) {
132+
return null;
133+
}
134+
135+
String locale = parts[0].split(";")[0].trim();
136+
return locale.isEmpty() ? null : locale;
137+
}
138+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package org.example.filter;
2+
3+
import org.example.http.HttpResponseBuilder;
4+
import org.example.httpparser.HttpRequest;
5+
6+
import java.util.logging.Logger;
7+
8+
public class LoggingFilter implements Filter {
9+
10+
private static final Logger logg = Logger.getLogger(LoggingFilter.class.getName());
11+
12+
@Override
13+
public void init() {
14+
//No initialization needed
15+
}
16+
17+
@Override
18+
public void doFilter(HttpRequest request, HttpResponseBuilder response, FilterChain chain) {
19+
long startTime = System.nanoTime();
20+
21+
try {
22+
chain.doFilter(request, response);
23+
} catch (Exception e) {
24+
if(response.getStatusCode() == HttpResponseBuilder.SC_OK)
25+
response.setStatusCode(HttpResponseBuilder.SC_INTERNAL_SERVER_ERROR);
26+
} finally {
27+
long endTime = System.nanoTime();
28+
long processingTimeInMs = (endTime - startTime) / 1000000;
29+
30+
String message = String.format("REQUEST: %s %s | STATUS: %s | TIME: %dms",
31+
request.getMethod(), request.getPath(), response.getStatusCode(), processingTimeInMs);
32+
33+
logg.info(message);
34+
}
35+
36+
}
37+
38+
@Override
39+
public void destroy() {
40+
//No initialization needed
41+
}
42+
}

src/main/resources/test.jpg

5.68 KB
Loading

0 commit comments

Comments
 (0)