From c2fa49f2830510733de6ee7933371f024874daeb Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Sun, 22 Feb 2026 18:04:26 +0100 Subject: [PATCH 1/3] fix(i18n): ensure request_locale takes precedence over Accept-Language when supportedLocale is configured MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When supportedLocale was configured on the I18nInterceptor, the Accept-Language header match in AcceptLanguageLocaleHandler.find() returned early before SessionLocaleHandler/CookieLocaleHandler ever checked their explicit locale parameters (request_locale, request_cookie_locale). This made it impossible to switch locale via request parameters when supportedLocale was set. Changes: - Reorder AcceptLanguageLocaleHandler.find() to check request_only_locale before Accept-Language matching - Reorder SessionLocaleHandler.find() to check request_locale before super - Reorder CookieLocaleHandler.find() to check request_cookie_locale before super - Add isLocaleSupported() helper to validate locales against supportedLocale - Filter all locale sources (params, session, cookies) through supportedLocale - Add 4 tests covering the bug scenario and supportedLocale filtering 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../struts2/interceptor/I18nInterceptor.java | 61 ++++--- .../interceptor/I18nInterceptorTest.java | 84 ++++++++-- ...n-supportedlocale-breaks-request-locale.md | 152 ++++++++++++++++++ 3 files changed, 269 insertions(+), 28 deletions(-) create mode 100644 thoughts/shared/research/2026-02-22-WW-5549-i18n-supportedlocale-breaks-request-locale.md diff --git a/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java index d0837a95bf..3b4ff552f4 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java @@ -108,6 +108,10 @@ public void setSupportedLocale(String supportedLocale) { .collect(Collectors.toSet()); } + protected boolean isLocaleSupported(Locale locale) { + return supportedLocale.isEmpty() || supportedLocale.contains(locale); + } + @Inject public void setLocaleProviderFactory(LocaleProviderFactory localeProviderFactory) { this.localeProviderFactory = localeProviderFactory; @@ -277,16 +281,21 @@ protected AcceptLanguageLocaleHandler(ActionInvocation invocation) { @Override @SuppressWarnings("rawtypes") public Locale find() { + Locale locale = super.find(); + if (locale != null) { + return locale; + } + if (!supportedLocale.isEmpty()) { Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales(); while (locales.hasMoreElements()) { - Locale locale = (Locale) locales.nextElement(); - if (supportedLocale.contains(locale)) { - return locale; + Locale acceptLocale = (Locale) locales.nextElement(); + if (supportedLocale.contains(acceptLocale)) { + return acceptLocale; } } } - return super.find(); + return null; } } @@ -299,20 +308,23 @@ protected SessionLocaleHandler(ActionInvocation invocation) { @Override public Locale find() { - Locale requestOnlyLocale = super.find(); + LOG.debug("Searching locale in request under parameter {}", parameterName); + Parameter requestedLocale = findLocaleParameter(actionInvocation, parameterName); + if (requestedLocale.isDefined()) { + Locale locale = getLocaleFromParam(requestedLocale.getValue()); + if (locale != null && isLocaleSupported(locale)) { + return locale; + } + LOG.debug("Requested locale {} is not supported, ignoring", requestedLocale.getValue()); + } + Locale requestOnlyLocale = super.find(); if (requestOnlyLocale != null) { LOG.debug("Found locale under request only param, it won't be stored in session!"); shouldStore = false; return requestOnlyLocale; } - LOG.debug("Searching locale in request under parameter {}", parameterName); - Parameter requestedLocale = findLocaleParameter(actionInvocation, parameterName); - if (requestedLocale.isDefined()) { - return getLocaleFromParam(requestedLocale.getValue()); - } - return null; } @@ -348,6 +360,11 @@ public Locale read(ActionInvocation invocation) { } } + if (locale != null && !isLocaleSupported(locale)) { + LOG.debug("Stored session locale {} is not in supportedLocale, ignoring", locale); + locale = null; + } + if (locale == null) { LOG.debug("No Locale defined in session, fetching from current request and it won't be stored in session!"); shouldStore = false; @@ -367,19 +384,22 @@ protected CookieLocaleHandler(ActionInvocation invocation) { @Override public Locale find() { - Locale requestOnlySessionLocale = super.find(); + LOG.debug("Searching locale in request under parameter {}", requestCookieParameterName); + Parameter requestedLocale = findLocaleParameter(actionInvocation, requestCookieParameterName); + if (requestedLocale.isDefined()) { + Locale locale = getLocaleFromParam(requestedLocale.getValue()); + if (locale != null && isLocaleSupported(locale)) { + return locale; + } + LOG.debug("Requested cookie locale {} is not supported, ignoring", requestedLocale.getValue()); + } + Locale requestOnlySessionLocale = super.find(); if (requestOnlySessionLocale != null) { shouldStore = false; return requestOnlySessionLocale; } - LOG.debug("Searching locale in request under parameter {}", requestCookieParameterName); - Parameter requestedLocale = findLocaleParameter(actionInvocation, requestCookieParameterName); - if (requestedLocale.isDefined()) { - return getLocaleFromParam(requestedLocale.getValue()); - } - return null; } @@ -407,6 +427,11 @@ public Locale read(ActionInvocation invocation) { } } + if (locale != null && !isLocaleSupported(locale)) { + LOG.debug("Stored cookie locale {} is not in supportedLocale, ignoring", locale); + locale = null; + } + if (locale == null) { LOG.debug("No Locale defined in cookie, fetching from current request and it won't be stored!"); shouldStore = false; diff --git a/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java index 2617d9f61f..29588ef44c 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java @@ -35,6 +35,7 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; + import java.io.Serializable; import java.util.Arrays; import java.util.HashMap; @@ -205,7 +206,7 @@ public void testRealLocaleObjectInParams() throws Exception { } public void testRealLocalesInParams() throws Exception { - Locale[] locales = new Locale[] { Locale.CANADA_FRENCH }; + Locale[] locales = new Locale[]{Locale.CANADA_FRENCH}; assertTrue(locales.getClass().isArray()); prepare(I18nInterceptor.DEFAULT_PARAMETER, locales); interceptor.intercept(mai); @@ -278,6 +279,69 @@ public void testAcceptLanguageBasedLocale() throws Exception { assertEquals(new Locale("pl"), mai.getInvocationContext().getLocale()); } + public void testSupportedLocaleWithRequestLocale() throws Exception { + // given - supportedLocale configured + request_locale param with SESSION storage + request.setPreferredLocales(Arrays.asList(new Locale("en"))); + interceptor.setSupportedLocale("en,fr"); + prepare(I18nInterceptor.DEFAULT_PARAMETER, "fr"); + + // when + interceptor.intercept(mai); + + // then - request_locale wins over Accept-Language + assertEquals(new Locale("fr"), session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); + assertEquals(new Locale("fr"), mai.getInvocationContext().getLocale()); + } + + public void testSupportedLocaleRejectsUnsupportedRequestLocale() throws Exception { + // given - request_locale=es but supportedLocale="en,fr" + request.setPreferredLocales(Arrays.asList(new Locale("en"))); + interceptor.setSupportedLocale("en,fr"); + prepare(I18nInterceptor.DEFAULT_PARAMETER, "es"); + + // when + interceptor.intercept(mai); + + // then - es rejected, falls back to Accept-Language match (en) + assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); + assertEquals(new Locale("en"), mai.getInvocationContext().getLocale()); + } + + public void testSupportedLocaleRevalidatesSessionLocale() throws Exception { + // given - session has stored locale "de" but supportedLocale changed to "en,fr" + session.put(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE, new Locale("de")); + request.setPreferredLocales(Arrays.asList(new Locale("fr"))); + interceptor.setSupportedLocale("en,fr"); + + // when + interceptor.intercept(mai); + + // then - stored "de" rejected, falls back to Accept-Language match (fr) + assertEquals(new Locale("fr"), mai.getInvocationContext().getLocale()); + } + + public void testSupportedLocaleWithCookieStorage() throws Exception { + // given - supportedLocale configured + request_cookie_locale param with COOKIE storage + prepare(I18nInterceptor.DEFAULT_COOKIE_PARAMETER, "fr"); + request.setPreferredLocales(Arrays.asList(new Locale("en"))); + interceptor.setSupportedLocale("en,fr"); + + final Cookie cookie = new Cookie(I18nInterceptor.DEFAULT_COOKIE_ATTRIBUTE, "fr"); + HttpServletResponse response = EasyMock.createMock(HttpServletResponse.class); + response.addCookie(CookieMatcher.eqCookie(cookie)); + EasyMock.replay(response); + + ac.put(StrutsStatics.HTTP_RESPONSE, response); + interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name()); + + // when + interceptor.intercept(mai); + + // then - request_cookie_locale=fr wins + EasyMock.verify(response); + assertEquals(new Locale("fr"), mai.getInvocationContext().getLocale()); + } + public void testAcceptLanguageBasedLocaleWithFallbackToDefault() throws Exception { // given request.setPreferredLocales(Arrays.asList(new Locale("da_DK"), new Locale("es"))); @@ -308,9 +372,9 @@ public void setUp() throws Exception { session = new HashMap<>(); ac = ActionContext.of() - .bind() - .withSession(session) - .withParameters(HttpParameters.create().build()); + .bind() + .withSession(session) + .withParameters(HttpParameters.create().build()); request = new MockHttpServletRequest(); request.setSession(new MockHttpSession()); @@ -348,8 +412,8 @@ static class CookieMatcher implements IArgumentMatcher { public boolean matches(Object argument) { Cookie cookie = ((Cookie) argument); return - (cookie.getName().equals(expected.getName()) && - cookie.getValue().equals(expected.getValue())); + (cookie.getName().equals(expected.getName()) && + cookie.getValue().equals(expected.getValue())); } public static Cookie eqCookie(Cookie ck) { @@ -359,10 +423,10 @@ public static Cookie eqCookie(Cookie ck) { public void appendTo(StringBuffer buffer) { buffer - .append("Received") - .append(expected.getName()) - .append("/") - .append(expected.getValue()); + .append("Received") + .append(expected.getName()) + .append("/") + .append(expected.getValue()); } } diff --git a/thoughts/shared/research/2026-02-22-WW-5549-i18n-supportedlocale-breaks-request-locale.md b/thoughts/shared/research/2026-02-22-WW-5549-i18n-supportedlocale-breaks-request-locale.md new file mode 100644 index 0000000000..3125ec72bf --- /dev/null +++ b/thoughts/shared/research/2026-02-22-WW-5549-i18n-supportedlocale-breaks-request-locale.md @@ -0,0 +1,152 @@ +--- +date: 2026-02-22T12:00:00+01:00 +topic: "I18nInterceptor supportedLocale disables request_locale parameter" +tags: [research, codebase, i18n, interceptor, locale, WW-5549] +status: complete +git_commit: a21c763d8a8592f1056086134414123f6d8d168d +--- + +# Research: WW-5549 - I18nInterceptor supportedLocale disables request_locale + +**Date**: 2026-02-22 + +## Research Question + +When `supportedLocale` is configured on the i18n interceptor, the `request_locale` parameter stops working if the browser's Accept-Language header matches a supported locale. + +## Summary + +The bug is in the class hierarchy of `LocaleHandler` implementations. `AcceptLanguageLocaleHandler.find()` returns early when it finds a match between the browser's Accept-Language header and the `supportedLocale` set. Since `SessionLocaleHandler` and `CookieLocaleHandler` both extend `AcceptLanguageLocaleHandler` and call `super.find()` first, the explicit `request_locale` parameter is never checked when the Accept-Language header matches a supported locale. + +## Detailed Findings + +### Class Hierarchy + +``` +LocaleHandler (interface) + └── RequestLocaleHandler (storage=REQUEST, checks request_only_locale) + └── AcceptLanguageLocaleHandler (storage=ACCEPT_LANGUAGE, checks Accept-Language header) + ├── SessionLocaleHandler (storage=SESSION, checks request_locale + session) + └── CookieLocaleHandler (storage=COOKIE, checks request_cookie_locale + cookie) +``` + +### The Bug: AcceptLanguageLocaleHandler.find() — Line 279 + +```java +// I18nInterceptor.java:279-290 +@Override +public Locale find() { + if (!supportedLocale.isEmpty()) { + Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales(); + while (locales.hasMoreElements()) { + Locale locale = (Locale) locales.nextElement(); + if (supportedLocale.contains(locale)) { + return locale; // ← RETURNS HERE, never calls super.find() + } + } + } + return super.find(); // ← Only reached if supportedLocale is empty or no match +} +``` + +### SessionLocaleHandler.find() — Line 301 + +```java +// I18nInterceptor.java:300-317 +@Override +public Locale find() { + Locale requestOnlyLocale = super.find(); // ← calls AcceptLanguageLocaleHandler.find() + + if (requestOnlyLocale != null) { + LOG.debug("Found locale under request only param, it won't be stored in session!"); + shouldStore = false; // ← prevents session storage + return requestOnlyLocale; // ← returns WITHOUT checking request_locale + } + + // request_locale is only checked here, which is never reached when super.find() returns non-null + Parameter requestedLocale = findLocaleParameter(actionInvocation, parameterName); + if (requestedLocale.isDefined()) { + return getLocaleFromParam(requestedLocale.getValue()); + } + return null; +} +``` + +### Concrete Bug Scenario + +Configuration: `supportedLocale="fr,en"`, storage=SESSION (default) + +1. User has French browser (`Accept-Language: fr,en`) +2. App defaults to French — correct +3. User clicks "English" link with `?request_locale=en` +4. `SessionLocaleHandler.find()` calls `super.find()` → `AcceptLanguageLocaleHandler.find()` +5. Accept-Language header yields `fr`, which IS in `supportedLocale` +6. Returns `fr` immediately — `request_locale=en` is **never checked** +7. `shouldStore = false` — so even if it were checked, it wouldn't persist +8. Locale stays French despite explicit user request to switch to English + +### intercept() Flow + +```java +// I18nInterceptor.java:117-144 +LocaleHandler localeHandler = getLocaleHandler(invocation); // SessionLocaleHandler for default +Locale locale = localeHandler.find(); // BUG: returns Accept-Language match, skips request_locale +if (locale == null) { + locale = localeHandler.read(invocation); // never reached when find() returns non-null +} +if (localeHandler.shouldStore()) { + locale = localeHandler.store(invocation, locale); // shouldStore=false, skipped +} +useLocale(invocation, locale); // sets the wrong locale +``` + +### Root Cause + +`SessionLocaleHandler.find()` was designed to call `super.find()` to check `request_only_locale` (a non-persistent locale override). It interprets any non-null result from `super.find()` as "a request-only locale was found." But `AcceptLanguageLocaleHandler.find()` conflates two different things: + +1. A locale from `request_only_locale` parameter (legitimate non-persistent override) +2. A locale from Accept-Language header matching `supportedLocale` (ambient browser preference) + +Both return non-null from `super.find()`, and `SessionLocaleHandler` cannot distinguish between them. + +### The Same Bug Affects CookieLocaleHandler + +`CookieLocaleHandler.find()` (line 369) has the identical pattern — calls `super.find()` and returns early if non-null, skipping `request_cookie_locale`. + +## Code References + +- [`I18nInterceptor.java`](https://github.com/apache/struts/blob/a21c763d8a8592f1056086134414123f6d8d168d/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java) — Full interceptor + - Line 65: `supportedLocale` field declaration + - Line 103-109: `setSupportedLocale()` — parses comma-delimited string to `Set` + - Line 117-144: `intercept()` — main flow + - Line 152-167: `getLocaleHandler()` — factory for handler selection + - Line 229-269: `RequestLocaleHandler` — base handler, checks `request_only_locale` + - Line 271-292: `AcceptLanguageLocaleHandler` — **bug location** at line 279-290 + - Line 294-361: `SessionLocaleHandler` — **affected** at line 300-317 + - Line 363-419: `CookieLocaleHandler` — **also affected** at line 369-384 +- [`I18nInterceptorTest.java`](https://github.com/apache/struts/blob/a21c763d8a8592f1056086134414123f6d8d168d/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java) — Test class + - Line 266: `testAcceptLanguageBasedLocale` — only tests ACCEPT_LANGUAGE storage mode + - Line 281: `testAcceptLanguageBasedLocaleWithFallbackToDefault` — fallback test + +## Test Coverage Gaps + +1. **No test** for `supportedLocale` + `request_locale` used simultaneously +2. **No test** for `supportedLocale` with SESSION storage mode (the default!) +3. **No test** for `supportedLocale` with COOKIE storage mode +4. Existing `supportedLocale` tests only use `ACCEPT_LANGUAGE` storage mode where the bug doesn't manifest (because `AcceptLanguageLocaleHandler` is used directly, not through `SessionLocaleHandler`) + +## Fix Direction + +The `request_locale` / `request_cookie_locale` parameter (explicit user choice) should always take precedence over the Accept-Language header (ambient browser preference). Options: + +1. **Reorder in SessionLocaleHandler/CookieLocaleHandler**: Check `request_locale` **before** calling `super.find()`, so the explicit parameter always wins +2. **Reorder in AcceptLanguageLocaleHandler**: Check `super.find()` (request_only_locale) first, then fall back to Accept-Language matching — this would fix it for `AcceptLanguageLocaleHandler` itself but not for `SessionLocaleHandler`/`CookieLocaleHandler` which have their own `find()` override +3. **Restructure the hierarchy**: Separate Accept-Language matching from the `find()` chain so it doesn't interfere with explicit parameter checks + +Option 1 is the most targeted fix with minimal risk. + +## Open Questions + +1. Should `supportedLocale` also validate/filter `request_locale` values? (e.g., reject `request_locale=es` if `supportedLocale="en,fr"`) +2. Should the session-stored locale also be validated against `supportedLocale` on subsequent requests? +3. The `Locale::new` constructor (line 107) is deprecated — should this be updated to use `Locale.forLanguageTag()` or `Locale.Builder`? \ No newline at end of file From 5b01fe8d00a6cc53b6e3cafd0ecad9711814a2fe Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Wed, 25 Feb 2026 08:56:14 +0100 Subject: [PATCH 2/3] test(i18n): cover missing supportedLocale locale-selection paths Add regression tests for unsupported request_cookie_locale fallback, stored cookie revalidation, and request_only_locale precedence to lock in WW-5549 behavior across remaining branches. Co-authored-by: Cursor --- .../interceptor/I18nInterceptorTest.java | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java index 29588ef44c..e00c10a815 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/I18nInterceptorTest.java @@ -342,6 +342,60 @@ public void testSupportedLocaleWithCookieStorage() throws Exception { assertEquals(new Locale("fr"), mai.getInvocationContext().getLocale()); } + public void testSupportedLocaleRejectsUnsupportedRequestCookieLocale() throws Exception { + // given - request_cookie_locale=es but supportedLocale="en,fr" + prepare(I18nInterceptor.DEFAULT_COOKIE_PARAMETER, "es"); + request.setPreferredLocales(Arrays.asList(new Locale("en"))); + interceptor.setSupportedLocale("en,fr"); + + HttpServletResponse response = EasyMock.createStrictMock(HttpServletResponse.class); + EasyMock.replay(response); + + ac.put(StrutsStatics.HTTP_RESPONSE, response); + interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name()); + + // when + interceptor.intercept(mai); + + // then - unsupported request_cookie_locale ignored, falls back to Accept-Language match + EasyMock.verify(response); + assertEquals(new Locale("en"), mai.getInvocationContext().getLocale()); + } + + public void testSupportedLocaleRevalidatesStoredCookieLocale() throws Exception { + // given - cookie has stored "de" but supportedLocale changed to "en,fr" + request.setCookies(new Cookie(I18nInterceptor.DEFAULT_COOKIE_ATTRIBUTE, "de")); + request.setPreferredLocales(Arrays.asList(new Locale("it"))); + interceptor.setSupportedLocale("en,fr"); + + HttpServletResponse response = EasyMock.createStrictMock(HttpServletResponse.class); + EasyMock.replay(response); + + ac.put(StrutsStatics.HTTP_RESPONSE, response); + interceptor.setLocaleStorage(I18nInterceptor.Storage.COOKIE.name()); + + // when + interceptor.intercept(mai); + + // then - stored "de" rejected and fallback locale from invocation context is used + EasyMock.verify(response); + assertEquals(Locale.US, mai.getInvocationContext().getLocale()); + } + + public void testRequestOnlyLocalePrecedenceWithSupportedLocale() throws Exception { + // given - request_only_locale should win over Accept-Language match + prepare(I18nInterceptor.DEFAULT_REQUEST_ONLY_PARAMETER, "fr"); + request.setPreferredLocales(Arrays.asList(new Locale("en"))); + interceptor.setSupportedLocale("en,fr"); + + // when + interceptor.intercept(mai); + + // then - request_only_locale applied and not persisted + assertNull(session.get(I18nInterceptor.DEFAULT_SESSION_ATTRIBUTE)); + assertEquals(new Locale("fr"), mai.getInvocationContext().getLocale()); + } + public void testAcceptLanguageBasedLocaleWithFallbackToDefault() throws Exception { // given request.setPreferredLocales(Arrays.asList(new Locale("da_DK"), new Locale("es"))); From 94e70d79dca759f7c918fb8ae82fa025a12a1399 Mon Sep 17 00:00:00 2001 From: Lukasz Lenart Date: Wed, 25 Feb 2026 09:16:27 +0100 Subject: [PATCH 3/3] refactor(i18n): extract locale handlers with deprecated inner wrappers Move locale handler implementations into a dedicated interceptor.i18n package with reusable abstract bases, keep thin deprecated inner wrappers in I18nInterceptor for one release-cycle compatibility, and document the LocaleHandler contract. Co-authored-by: Cursor --- .../struts2/interceptor/I18nInterceptor.java | 301 ++++++++---------- .../i18n/AbstractLocaleHandler.java | 45 +++ .../i18n/AbstractStoredLocaleHandler.java | 85 +++++ .../i18n/AcceptLanguageLocaleHandler.java | 55 ++++ .../interceptor/i18n/CookieLocaleHandler.java | 89 ++++++ .../interceptor/i18n/LocaleHandler.java | 63 ++++ .../i18n/RequestLocaleHandler.java | 65 ++++ .../i18n/SessionLocaleHandler.java | 97 ++++++ 8 files changed, 639 insertions(+), 161 deletions(-) create mode 100644 core/src/main/java/org/apache/struts2/interceptor/i18n/AbstractLocaleHandler.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/i18n/AbstractStoredLocaleHandler.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/i18n/AcceptLanguageLocaleHandler.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/i18n/CookieLocaleHandler.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/i18n/LocaleHandler.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/i18n/RequestLocaleHandler.java create mode 100644 core/src/main/java/org/apache/struts2/interceptor/i18n/SessionLocaleHandler.java diff --git a/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java b/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java index 3b4ff552f4..63b14d23d8 100644 --- a/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java +++ b/core/src/main/java/org/apache/struts2/interceptor/I18nInterceptor.java @@ -26,18 +26,11 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; -import org.apache.struts2.ServletActionContext; import org.apache.struts2.dispatcher.HttpParameters; import org.apache.struts2.dispatcher.Parameter; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletResponse; -import jakarta.servlet.http.HttpSession; - import java.util.Collections; -import java.util.Enumeration; import java.util.Locale; -import java.util.Map; import java.util.Set; import java.util.stream.Collectors; @@ -223,223 +216,209 @@ protected void useLocale(ActionInvocation invocation, Locale locale) { /** * Uses to handle reading/storing Locale from/in different locations */ - protected interface LocaleHandler { - Locale find(); - Locale read(ActionInvocation invocation); - Locale store(ActionInvocation invocation, Locale locale); - boolean shouldStore(); + @Deprecated(forRemoval = true, since = "7.2.0") + protected interface LocaleHandler extends org.apache.struts2.interceptor.i18n.LocaleHandler { } + /** + * @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.RequestLocaleHandler}. + * Scheduled for removal in the next release cycle. + */ + @Deprecated(forRemoval = true, since = "7.2.0") protected class RequestLocaleHandler implements LocaleHandler { - protected ActionInvocation actionInvocation; - protected boolean shouldStore = true; + private final org.apache.struts2.interceptor.i18n.RequestLocaleHandler delegate; protected RequestLocaleHandler(ActionInvocation invocation) { - actionInvocation = invocation; - } - - public Locale find() { - LOG.debug("Searching locale in request under parameter {}", requestOnlyParameterName); + delegate = new org.apache.struts2.interceptor.i18n.RequestLocaleHandler(invocation, requestOnlyParameterName) { + @Override + protected Locale getLocaleFromParam(String requestedLocale) { + return I18nInterceptor.this.getLocaleFromParam(requestedLocale); + } - Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName); - if (requestedLocale.isDefined()) { - return getLocaleFromParam(requestedLocale.getValue()); - } + @Override + protected Parameter findLocaleParameter(ActionInvocation invocation, String parameterName) { + return I18nInterceptor.this.findLocaleParameter(invocation, parameterName); + } - return null; + @Override + protected boolean isLocaleSupported(Locale locale) { + return I18nInterceptor.this.isLocaleSupported(locale); + } + }; } @Override - public Locale store(ActionInvocation invocation, Locale locale) { - return locale; + public Locale find() { + return delegate.find(); } @Override public Locale read(ActionInvocation invocation) { - LOG.debug("Searching current Invocation context"); - // no overriding locale definition found, stay with current invocation (=browser) locale - Locale locale = invocation.getInvocationContext().getLocale(); - if (locale != null) { - LOG.debug("Applied invocation context locale: {}", locale); - } - return locale; + return delegate.read(invocation); + } + + @Override + public Locale store(ActionInvocation invocation, Locale locale) { + return delegate.store(invocation, locale); } @Override public boolean shouldStore() { - return shouldStore; + return delegate.shouldStore(); } } - protected class AcceptLanguageLocaleHandler extends RequestLocaleHandler { + /** + * @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.AcceptLanguageLocaleHandler}. + * Scheduled for removal in the next release cycle. + */ + @Deprecated(forRemoval = true, since = "7.2.0") + protected class AcceptLanguageLocaleHandler implements LocaleHandler { + + private final org.apache.struts2.interceptor.i18n.AcceptLanguageLocaleHandler delegate; protected AcceptLanguageLocaleHandler(ActionInvocation invocation) { - super(invocation); + delegate = new org.apache.struts2.interceptor.i18n.AcceptLanguageLocaleHandler( + invocation, requestOnlyParameterName, supportedLocale + ) { + @Override + protected Locale getLocaleFromParam(String requestedLocale) { + return I18nInterceptor.this.getLocaleFromParam(requestedLocale); + } + + @Override + protected Parameter findLocaleParameter(ActionInvocation invocation, String parameterName) { + return I18nInterceptor.this.findLocaleParameter(invocation, parameterName); + } + + @Override + protected boolean isLocaleSupported(Locale locale) { + return I18nInterceptor.this.isLocaleSupported(locale); + } + }; } @Override - @SuppressWarnings("rawtypes") public Locale find() { - Locale locale = super.find(); - if (locale != null) { - return locale; - } + return delegate.find(); + } - if (!supportedLocale.isEmpty()) { - Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales(); - while (locales.hasMoreElements()) { - Locale acceptLocale = (Locale) locales.nextElement(); - if (supportedLocale.contains(acceptLocale)) { - return acceptLocale; - } - } - } - return null; + @Override + public Locale read(ActionInvocation invocation) { + return delegate.read(invocation); + } + + @Override + public Locale store(ActionInvocation invocation, Locale locale) { + return delegate.store(invocation, locale); } + @Override + public boolean shouldStore() { + return delegate.shouldStore(); + } } - protected class SessionLocaleHandler extends AcceptLanguageLocaleHandler { + /** + * @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.SessionLocaleHandler}. + * Scheduled for removal in the next release cycle. + */ + @Deprecated(forRemoval = true, since = "7.2.0") + protected class SessionLocaleHandler implements LocaleHandler { + + private final org.apache.struts2.interceptor.i18n.SessionLocaleHandler delegate; protected SessionLocaleHandler(ActionInvocation invocation) { - super(invocation); - } - - @Override - public Locale find() { - LOG.debug("Searching locale in request under parameter {}", parameterName); - Parameter requestedLocale = findLocaleParameter(actionInvocation, parameterName); - if (requestedLocale.isDefined()) { - Locale locale = getLocaleFromParam(requestedLocale.getValue()); - if (locale != null && isLocaleSupported(locale)) { - return locale; + delegate = new org.apache.struts2.interceptor.i18n.SessionLocaleHandler( + invocation, requestOnlyParameterName, supportedLocale, parameterName, attributeName + ) { + @Override + protected Locale getLocaleFromParam(String requestedLocale) { + return I18nInterceptor.this.getLocaleFromParam(requestedLocale); } - LOG.debug("Requested locale {} is not supported, ignoring", requestedLocale.getValue()); - } - Locale requestOnlyLocale = super.find(); - if (requestOnlyLocale != null) { - LOG.debug("Found locale under request only param, it won't be stored in session!"); - shouldStore = false; - return requestOnlyLocale; - } + @Override + protected Parameter findLocaleParameter(ActionInvocation invocation, String parameterName) { + return I18nInterceptor.this.findLocaleParameter(invocation, parameterName); + } - return null; + @Override + protected boolean isLocaleSupported(Locale locale) { + return I18nInterceptor.this.isLocaleSupported(locale); + } + }; } @Override - public Locale store(ActionInvocation invocation, Locale locale) { - Map session = invocation.getInvocationContext().getSession(); - - if (session != null) { - String sessionId = ServletActionContext.getRequest().getSession().getId(); - synchronized (sessionId.intern()) { - session.put(attributeName, locale); - } - } - - return locale; + public Locale find() { + return delegate.find(); } @Override public Locale read(ActionInvocation invocation) { - Locale locale = null; - - LOG.debug("Checks session for saved locale"); - HttpSession session = ServletActionContext.getRequest().getSession(false); - - if (session != null) { - String sessionId = session.getId(); - synchronized (sessionId.intern()) { - Object sessionLocale = invocation.getInvocationContext().getSession().get(attributeName); - if (sessionLocale instanceof Locale) { - locale = (Locale) sessionLocale; - LOG.debug("Applied session locale: {}", locale); - } - } - } - - if (locale != null && !isLocaleSupported(locale)) { - LOG.debug("Stored session locale {} is not in supportedLocale, ignoring", locale); - locale = null; - } + return delegate.read(invocation); + } - if (locale == null) { - LOG.debug("No Locale defined in session, fetching from current request and it won't be stored in session!"); - shouldStore = false; - locale = super.read(invocation); - } else { - LOG.debug("Found stored Locale {} in session, using it!", locale); - } + @Override + public Locale store(ActionInvocation invocation, Locale locale) { + return delegate.store(invocation, locale); + } - return locale; + @Override + public boolean shouldStore() { + return delegate.shouldStore(); } } - protected class CookieLocaleHandler extends AcceptLanguageLocaleHandler { - protected CookieLocaleHandler(ActionInvocation invocation) { - super(invocation); - } + /** + * @deprecated Since 7.2.0, use {@link org.apache.struts2.interceptor.i18n.CookieLocaleHandler}. + * Scheduled for removal in the next release cycle. + */ + @Deprecated(forRemoval = true, since = "7.2.0") + protected class CookieLocaleHandler implements LocaleHandler { - @Override - public Locale find() { - LOG.debug("Searching locale in request under parameter {}", requestCookieParameterName); - Parameter requestedLocale = findLocaleParameter(actionInvocation, requestCookieParameterName); - if (requestedLocale.isDefined()) { - Locale locale = getLocaleFromParam(requestedLocale.getValue()); - if (locale != null && isLocaleSupported(locale)) { - return locale; + private final org.apache.struts2.interceptor.i18n.CookieLocaleHandler delegate; + + protected CookieLocaleHandler(ActionInvocation invocation) { + delegate = new org.apache.struts2.interceptor.i18n.CookieLocaleHandler( + invocation, requestOnlyParameterName, supportedLocale, requestCookieParameterName, attributeName + ) { + @Override + protected Locale getLocaleFromParam(String requestedLocale) { + return I18nInterceptor.this.getLocaleFromParam(requestedLocale); } - LOG.debug("Requested cookie locale {} is not supported, ignoring", requestedLocale.getValue()); - } - Locale requestOnlySessionLocale = super.find(); - if (requestOnlySessionLocale != null) { - shouldStore = false; - return requestOnlySessionLocale; - } + @Override + protected Parameter findLocaleParameter(ActionInvocation invocation, String parameterName) { + return I18nInterceptor.this.findLocaleParameter(invocation, parameterName); + } - return null; + @Override + protected boolean isLocaleSupported(Locale locale) { + return I18nInterceptor.this.isLocaleSupported(locale); + } + }; } @Override - public Locale store(ActionInvocation invocation, Locale locale) { - HttpServletResponse response = ServletActionContext.getResponse(); - - Cookie cookie = new Cookie(attributeName, locale.toString()); - cookie.setMaxAge(1209600); // two weeks - response.addCookie(cookie); - - return locale; + public Locale find() { + return delegate.find(); } @Override public Locale read(ActionInvocation invocation) { - Locale locale = null; - - Cookie[] cookies = ServletActionContext.getRequest().getCookies(); - if (cookies != null) { - for (Cookie cookie : cookies) { - if (attributeName.equals(cookie.getName())) { - locale = getLocaleFromParam(cookie.getValue()); - } - } - } + return delegate.read(invocation); + } - if (locale != null && !isLocaleSupported(locale)) { - LOG.debug("Stored cookie locale {} is not in supportedLocale, ignoring", locale); - locale = null; - } + @Override + public Locale store(ActionInvocation invocation, Locale locale) { + return delegate.store(invocation, locale); + } - if (locale == null) { - LOG.debug("No Locale defined in cookie, fetching from current request and it won't be stored!"); - shouldStore = false; - locale = super.read(invocation); - } else { - LOG.debug("Found stored Locale {} in cookie, using it!", locale); - } - return locale; + @Override + public boolean shouldStore() { + return delegate.shouldStore(); } } diff --git a/core/src/main/java/org/apache/struts2/interceptor/i18n/AbstractLocaleHandler.java b/core/src/main/java/org/apache/struts2/interceptor/i18n/AbstractLocaleHandler.java new file mode 100644 index 0000000000..a4899362b8 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/i18n/AbstractLocaleHandler.java @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.interceptor.i18n; + +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.dispatcher.Parameter; + +import java.util.Locale; + +public abstract class AbstractLocaleHandler implements LocaleHandler { + + protected final ActionInvocation actionInvocation; + protected boolean shouldStore = true; + + protected AbstractLocaleHandler(ActionInvocation invocation) { + this.actionInvocation = invocation; + } + + @Override + public boolean shouldStore() { + return shouldStore; + } + + protected abstract Locale getLocaleFromParam(String requestedLocale); + + protected abstract Parameter findLocaleParameter(ActionInvocation invocation, String parameterName); + + protected abstract boolean isLocaleSupported(Locale locale); +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/i18n/AbstractStoredLocaleHandler.java b/core/src/main/java/org/apache/struts2/interceptor/i18n/AbstractStoredLocaleHandler.java new file mode 100644 index 0000000000..604826b07d --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/i18n/AbstractStoredLocaleHandler.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.interceptor.i18n; + +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.dispatcher.Parameter; + +import java.util.Locale; +import java.util.Set; + +public abstract class AbstractStoredLocaleHandler extends AcceptLanguageLocaleHandler { + + private final String explicitParameterName; + + protected AbstractStoredLocaleHandler(ActionInvocation invocation, + String requestOnlyParameterName, + Set supportedLocale, + String explicitParameterName) { + super(invocation, requestOnlyParameterName, supportedLocale); + this.explicitParameterName = explicitParameterName; + } + + protected Locale findExplicitLocale(Logger logger, String unsupportedLogPattern) { + logger.debug("Searching locale in request under parameter {}", explicitParameterName); + Parameter requestedLocale = findLocaleParameter(actionInvocation, explicitParameterName); + if (requestedLocale.isDefined()) { + Locale locale = getLocaleFromParam(requestedLocale.getValue()); + if (locale != null && isLocaleSupported(locale)) { + return locale; + } + logger.debug(unsupportedLogPattern, requestedLocale.getValue()); + } + return null; + } + + protected Locale findRequestOnlyLocale(Logger logger, String requestOnlyFoundLogPattern) { + Locale requestOnlyLocale = super.find(); + if (requestOnlyLocale != null) { + if (requestOnlyFoundLogPattern != null) { + logger.debug(requestOnlyFoundLogPattern); + } + shouldStore = false; + return requestOnlyLocale; + } + return null; + } + + protected Locale normalizeStoredLocale(Logger logger, + Locale locale, + String unsupportedStoredLogPattern, + String missingStoredLogPattern, + String foundStoredLogPattern, + ActionInvocation invocation) { + if (locale != null && !isLocaleSupported(locale)) { + logger.debug(unsupportedStoredLogPattern, locale); + locale = null; + } + + if (locale == null) { + logger.debug(missingStoredLogPattern); + shouldStore = false; + return super.read(invocation); + } else { + logger.debug(foundStoredLogPattern, locale); + return locale; + } + } +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/i18n/AcceptLanguageLocaleHandler.java b/core/src/main/java/org/apache/struts2/interceptor/i18n/AcceptLanguageLocaleHandler.java new file mode 100644 index 0000000000..d1396ee2dc --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/i18n/AcceptLanguageLocaleHandler.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.interceptor.i18n; + +import org.apache.struts2.ActionInvocation; + +import java.util.Enumeration; +import java.util.Locale; +import java.util.Set; + +public abstract class AcceptLanguageLocaleHandler extends RequestLocaleHandler { + + private final Set supportedLocale; + + protected AcceptLanguageLocaleHandler(ActionInvocation invocation, String requestOnlyParameterName, Set supportedLocale) { + super(invocation, requestOnlyParameterName); + this.supportedLocale = supportedLocale; + } + + @Override + @SuppressWarnings("rawtypes") + public Locale find() { + Locale locale = super.find(); + if (locale != null) { + return locale; + } + + if (!supportedLocale.isEmpty()) { + Enumeration locales = actionInvocation.getInvocationContext().getServletRequest().getLocales(); + while (locales.hasMoreElements()) { + Locale acceptLocale = (Locale) locales.nextElement(); + if (supportedLocale.contains(acceptLocale)) { + return acceptLocale; + } + } + } + return null; + } +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/i18n/CookieLocaleHandler.java b/core/src/main/java/org/apache/struts2/interceptor/i18n/CookieLocaleHandler.java new file mode 100644 index 0000000000..de105ff71a --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/i18n/CookieLocaleHandler.java @@ -0,0 +1,89 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.interceptor.i18n; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.ServletActionContext; + +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletResponse; + +import java.util.Locale; +import java.util.Set; + +public abstract class CookieLocaleHandler extends AbstractStoredLocaleHandler { + + private static final Logger LOG = LogManager.getLogger(CookieLocaleHandler.class); + + private final String attributeName; + + protected CookieLocaleHandler(ActionInvocation invocation, + String requestOnlyParameterName, + Set supportedLocale, + String requestCookieParameterName, + String attributeName) { + super(invocation, requestOnlyParameterName, supportedLocale, requestCookieParameterName); + this.attributeName = attributeName; + } + + @Override + public Locale find() { + Locale locale = findExplicitLocale(LOG, "Requested cookie locale {} is not supported, ignoring"); + if (locale != null) { + return locale; + } + return findRequestOnlyLocale(LOG, null); + } + + @Override + public Locale store(ActionInvocation invocation, Locale locale) { + HttpServletResponse response = ServletActionContext.getResponse(); + + Cookie cookie = new Cookie(attributeName, locale.toString()); + cookie.setMaxAge(1209600); // two weeks + response.addCookie(cookie); + + return locale; + } + + @Override + public Locale read(ActionInvocation invocation) { + Locale locale = null; + + Cookie[] cookies = ServletActionContext.getRequest().getCookies(); + if (cookies != null) { + for (Cookie cookie : cookies) { + if (attributeName.equals(cookie.getName())) { + locale = getLocaleFromParam(cookie.getValue()); + } + } + } + + return normalizeStoredLocale( + LOG, + locale, + "Stored cookie locale {} is not in supportedLocale, ignoring", + "No Locale defined in cookie, fetching from current request and it won't be stored!", + "Found stored Locale {} in cookie, using it!", + invocation + ); + } +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/i18n/LocaleHandler.java b/core/src/main/java/org/apache/struts2/interceptor/i18n/LocaleHandler.java new file mode 100644 index 0000000000..78476dc784 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/i18n/LocaleHandler.java @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.interceptor.i18n; + +import org.apache.struts2.ActionInvocation; + +import java.util.Locale; + +/** + * Strategy used by {@code I18nInterceptor} to resolve and optionally persist the current request locale. + *

+ * Implementations encapsulate locale source-specific behavior (request parameters, session, cookies, + * or Accept-Language header), while the interceptor orchestrates the overall lifecycle. + */ +public interface LocaleHandler { + + /** + * Looks for an explicit locale override in request-scoped sources. + * + * @return a locale override or {@code null} when no explicit override is present + */ + Locale find(); + + /** + * Reads locale from persistent/context sources when {@link #find()} did not resolve one. + * + * @param invocation current action invocation + * @return resolved locale or {@code null} when no locale could be resolved + */ + Locale read(ActionInvocation invocation); + + /** + * Persists the resolved locale when storage is enabled for the current handler. + * + * @param invocation current action invocation + * @param locale locale to store + * @return the effective locale to apply to the invocation context + */ + Locale store(ActionInvocation invocation, Locale locale); + + /** + * Indicates if the locale should be persisted for the current request. + * + * @return {@code true} when {@link #store(ActionInvocation, Locale)} should be invoked + */ + boolean shouldStore(); +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/i18n/RequestLocaleHandler.java b/core/src/main/java/org/apache/struts2/interceptor/i18n/RequestLocaleHandler.java new file mode 100644 index 0000000000..4374da66fe --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/i18n/RequestLocaleHandler.java @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.interceptor.i18n; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.dispatcher.Parameter; + +import java.util.Locale; + +public abstract class RequestLocaleHandler extends AbstractLocaleHandler { + + private static final Logger LOG = LogManager.getLogger(RequestLocaleHandler.class); + + private final String requestOnlyParameterName; + + protected RequestLocaleHandler(ActionInvocation invocation, String requestOnlyParameterName) { + super(invocation); + this.requestOnlyParameterName = requestOnlyParameterName; + } + + @Override + public Locale find() { + LOG.debug("Searching locale in request under parameter {}", requestOnlyParameterName); + + Parameter requestedLocale = findLocaleParameter(actionInvocation, requestOnlyParameterName); + if (requestedLocale.isDefined()) { + return getLocaleFromParam(requestedLocale.getValue()); + } + + return null; + } + + @Override + public Locale store(ActionInvocation invocation, Locale locale) { + return locale; + } + + @Override + public Locale read(ActionInvocation invocation) { + LOG.debug("Searching current Invocation context"); + Locale locale = invocation.getInvocationContext().getLocale(); + if (locale != null) { + LOG.debug("Applied invocation context locale: {}", locale); + } + return locale; + } +} diff --git a/core/src/main/java/org/apache/struts2/interceptor/i18n/SessionLocaleHandler.java b/core/src/main/java/org/apache/struts2/interceptor/i18n/SessionLocaleHandler.java new file mode 100644 index 0000000000..c34e8c50d0 --- /dev/null +++ b/core/src/main/java/org/apache/struts2/interceptor/i18n/SessionLocaleHandler.java @@ -0,0 +1,97 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.struts2.interceptor.i18n; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.struts2.ActionInvocation; +import org.apache.struts2.ServletActionContext; + +import jakarta.servlet.http.HttpSession; + +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +public abstract class SessionLocaleHandler extends AbstractStoredLocaleHandler { + + private static final Logger LOG = LogManager.getLogger(SessionLocaleHandler.class); + + private final String attributeName; + + protected SessionLocaleHandler(ActionInvocation invocation, + String requestOnlyParameterName, + Set supportedLocale, + String parameterName, + String attributeName) { + super(invocation, requestOnlyParameterName, supportedLocale, parameterName); + this.attributeName = attributeName; + } + + @Override + public Locale find() { + Locale locale = findExplicitLocale(LOG, "Requested locale {} is not supported, ignoring"); + if (locale != null) { + return locale; + } + return findRequestOnlyLocale(LOG, "Found locale under request only param, it won't be stored in session!"); + } + + @Override + public Locale store(ActionInvocation invocation, Locale locale) { + Map session = invocation.getInvocationContext().getSession(); + + if (session != null) { + String sessionId = ServletActionContext.getRequest().getSession().getId(); + synchronized (sessionId.intern()) { + session.put(attributeName, locale); + } + } + + return locale; + } + + @Override + public Locale read(ActionInvocation invocation) { + Locale locale = null; + + LOG.debug("Checks session for saved locale"); + HttpSession session = ServletActionContext.getRequest().getSession(false); + + if (session != null) { + String sessionId = session.getId(); + synchronized (sessionId.intern()) { + Object sessionLocale = invocation.getInvocationContext().getSession().get(attributeName); + if (sessionLocale instanceof Locale) { + locale = (Locale) sessionLocale; + LOG.debug("Applied session locale: {}", locale); + } + } + } + + return normalizeStoredLocale( + LOG, + locale, + "Stored session locale {} is not in supportedLocale, ignoring", + "No Locale defined in session, fetching from current request and it won't be stored in session!", + "Found stored Locale {} in session, using it!", + invocation + ); + } +}