Skip to content

Commit 7652687

Browse files
Feature/LocaleFilter (#81)
* Re-commit LocaleFilter + tests to clean branch for PR * Update LocaleFilter to handle quality weights and improve javadoc * Fix: rename test method to reflect actual headers scenario * Fix: ensure resolveLocale never returns empty string; strip quality weights
1 parent ff4cd12 commit 7652687

2 files changed

Lines changed: 194 additions & 0 deletions

File tree

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 extracts the preferred locale from the Accept-Language header of an HTTP request.
10+
* <p>
11+
* If the Accept-Language header is missing, blank, or malformed, the filter defaults to "en-US".
12+
* The selected locale is stored in a ThreadLocal variable so it can be accessed during the request.
13+
* <p>
14+
* This filter does not modify the response or stop the filter chain; it simply sets the
15+
* current locale and forwards the request to the next filter in the chain.
16+
* <p>
17+
* ThreadLocal cleanup is performed after the filter chain completes to prevent memory leaks.
18+
*/
19+
public class LocaleFilter implements Filter {
20+
21+
private static final String DEFAULT_LOCALE = "en-US";
22+
private static final ThreadLocal<String> currentLocale = new ThreadLocal<>();
23+
24+
@Override
25+
public void init() {
26+
}
27+
28+
@Override
29+
public void doFilter(HttpRequest request,
30+
HttpResponseBuilder response,
31+
FilterChain chain) {
32+
try {
33+
String locale = resolveLocale(request);
34+
currentLocale.set(locale);
35+
36+
chain.doFilter(request, response);
37+
} finally {
38+
currentLocale.remove();
39+
}
40+
}
41+
42+
@Override
43+
public void destroy() {
44+
}
45+
46+
public static String getCurrentLocale() {
47+
String locale = currentLocale.get();
48+
if (locale != null) {
49+
return locale;
50+
} else {
51+
return DEFAULT_LOCALE;
52+
}
53+
}
54+
55+
/**
56+
* Determines the preferred locale from the Accept-Language header of the request.
57+
* If the header is missing, blank, or malformed, this method returns the default locale "en-US".
58+
* The first language tag is used, and any optional quality value (e.g., ";q=0.9") is stripped.
59+
* If the request itself is null, the default locale is also returned.
60+
*/
61+
private String resolveLocale(HttpRequest request) {
62+
63+
if (request == null) {
64+
return DEFAULT_LOCALE;
65+
}
66+
67+
Map<String, String> headers = request.getHeaders();
68+
if (headers == null || headers.isEmpty()) {
69+
return DEFAULT_LOCALE;
70+
}
71+
72+
String acceptLanguage = null;
73+
74+
for (Map.Entry<String, String> entry : headers.entrySet()) {
75+
if (entry.getKey() != null &&
76+
entry.getKey().equalsIgnoreCase("Accept-Language")) {
77+
acceptLanguage = entry.getValue();
78+
break;
79+
}
80+
}
81+
82+
if (acceptLanguage == null || acceptLanguage.isBlank()) {
83+
return DEFAULT_LOCALE;
84+
}
85+
86+
String[] parts = acceptLanguage.split(",");
87+
if (parts[0].isBlank()) {
88+
return DEFAULT_LOCALE;
89+
}
90+
91+
String locale = parts[0].split(";")[0].trim();
92+
if (locale.isEmpty()) {
93+
return DEFAULT_LOCALE;
94+
} else {
95+
return locale;
96+
}
97+
}
98+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package org.example.filter;
2+
3+
import org.example.http.HttpResponseBuilder;
4+
import org.example.httpparser.HttpRequest;
5+
import org.junit.jupiter.api.Test;
6+
7+
import java.util.HashMap;
8+
import java.util.Map;
9+
10+
import static org.junit.jupiter.api.Assertions.assertEquals;
11+
12+
class LocaleFilterTest {
13+
14+
@Test
15+
void shouldUseFirstLanguageFromHeader() {
16+
Map<String, String> headers = new HashMap<>();
17+
headers.put("Accept-Language", "sv-SE,sv;q=0.9,en;q=0.8");
18+
19+
HttpRequest request = new HttpRequest("GET", "/", "HTTP/1.1", headers, null);
20+
HttpResponseBuilder response = new HttpResponseBuilder();
21+
22+
LocaleFilter filter = new LocaleFilter();
23+
24+
filter.doFilter(request, response, (req, res) -> {
25+
assertEquals("sv-SE", LocaleFilter.getCurrentLocale());
26+
});
27+
28+
assertEquals("en-US", LocaleFilter.getCurrentLocale());
29+
}
30+
31+
@Test
32+
void shouldUseDefaultWhenHeaderMissing() {
33+
Map<String, String> headers = new HashMap<>();
34+
35+
HttpRequest request = new HttpRequest("GET", "/", "HTTP/1.1", headers, null);
36+
HttpResponseBuilder response = new HttpResponseBuilder();
37+
38+
LocaleFilter filter = new LocaleFilter();
39+
40+
filter.doFilter(request, response, (req, res) -> {
41+
assertEquals("en-US", LocaleFilter.getCurrentLocale());
42+
});
43+
}
44+
45+
@Test
46+
void shouldUseDefaultWhenHeaderBlank() {
47+
Map<String, String> headers = new HashMap<>();
48+
headers.put("Accept-Language", " ");
49+
50+
HttpRequest request = new HttpRequest("GET", "/", "HTTP/1.1", headers, null);
51+
HttpResponseBuilder response = new HttpResponseBuilder();
52+
53+
LocaleFilter filter = new LocaleFilter();
54+
55+
filter.doFilter(request, response, (req, res) -> {
56+
assertEquals("en-US", LocaleFilter.getCurrentLocale());
57+
});
58+
}
59+
60+
@Test
61+
void shouldHandleCaseInsensitiveHeader() {
62+
Map<String, String> headers = new HashMap<>();
63+
headers.put("accept-language", "fr-FR");
64+
65+
HttpRequest request = new HttpRequest("GET", "/", "HTTP/1.1", headers, null);
66+
HttpResponseBuilder response = new HttpResponseBuilder();
67+
68+
LocaleFilter filter = new LocaleFilter();
69+
70+
filter.doFilter(request, response, (req, res) -> {
71+
assertEquals("fr-FR", LocaleFilter.getCurrentLocale());
72+
});
73+
}
74+
75+
@Test
76+
void shouldUseDefaultWhenRequestIsNull() {
77+
LocaleFilter filter = new LocaleFilter();
78+
HttpResponseBuilder response = new HttpResponseBuilder();
79+
80+
filter.doFilter(null, response, (req, res) -> {
81+
assertEquals("en-US", LocaleFilter.getCurrentLocale());
82+
});
83+
}
84+
85+
@Test
86+
void shouldUseDefaultWhenHeadersAreEmpty() {
87+
HttpRequest request = new HttpRequest("GET", "/", "HTTP/1.1", null, null);
88+
HttpResponseBuilder response = new HttpResponseBuilder();
89+
90+
LocaleFilter filter = new LocaleFilter();
91+
92+
filter.doFilter(request, response, (req, res) -> {
93+
assertEquals("en-US", LocaleFilter.getCurrentLocale());
94+
});
95+
}
96+
}

0 commit comments

Comments
 (0)