diff --git a/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java b/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java index 82cc13d9eac6..1173bae9f0b6 100644 --- a/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java +++ b/spring-web/src/main/java/org/springframework/web/util/CookieGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,13 +30,12 @@ * given response. * *

Can serve as base class for components that generate specific cookies, - * such as CookieLocaleResolver and CookieThemeResolver. + * such as CookieThemeResolver. * * @author Juergen Hoeller * @since 1.1.4 * @see #addCookie * @see #removeCookie - * @see org.springframework.web.servlet.i18n.CookieLocaleResolver * @see org.springframework.web.servlet.theme.CookieThemeResolver */ public class CookieGenerator { diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java index 0572bc70b7ad..4a1cc9c8ab0c 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/CookieLocaleResolver.java @@ -16,6 +16,7 @@ package org.springframework.web.servlet.i18n; +import java.time.Duration; import java.util.Locale; import java.util.TimeZone; import java.util.function.Function; @@ -23,16 +24,17 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.springframework.context.i18n.LocaleContext; -import org.springframework.context.i18n.SimpleLocaleContext; import org.springframework.context.i18n.TimeZoneAwareLocaleContext; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseCookie; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; -import org.springframework.web.servlet.LocaleContextResolver; import org.springframework.web.servlet.LocaleResolver; -import org.springframework.web.util.CookieGenerator; import org.springframework.web.util.WebUtils; /** @@ -57,7 +59,7 @@ * @see #setDefaultLocale * @see #setDefaultTimeZone */ -public class CookieLocaleResolver extends CookieGenerator implements LocaleContextResolver { +public class CookieLocaleResolver extends AbstractLocaleContextResolver { /** * The name of the request attribute that holds the {@code Locale}. @@ -86,16 +88,39 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte */ public static final String DEFAULT_COOKIE_NAME = CookieLocaleResolver.class.getName() + ".LOCALE"; + /** + * The default cookie max age (persists until browser shutdown) if none is + * explicitly set. + */ + public static final Duration DEFAULT_COOKIE_MAX_AGE = Duration.ofSeconds(-1); - private boolean languageTagCompliant = true; + /** + * The default cookie path used if none is explicitly set. + */ + public static final String DEFAULT_COOKIE_PATH = "/"; - private boolean rejectInvalidCookies = true; + private static final Log logger = LogFactory.getLog(CookieLocaleResolver.class); + + + private String cookieName; + + private Duration cookieMaxAge = DEFAULT_COOKIE_MAX_AGE; @Nullable - private Locale defaultLocale; + private String cookiePath = DEFAULT_COOKIE_PATH; @Nullable - private TimeZone defaultTimeZone; + private String cookieDomain; + + private boolean cookieSecure; + + private boolean cookieHttpOnly; + + private String cookieSameSite = "Lax"; + + private boolean languageTagCompliant = true; + + private boolean rejectInvalidCookies = true; private Function defaultLocaleFunction = request -> { Locale defaultLocale = getDefaultLocale(); @@ -104,15 +129,99 @@ public class CookieLocaleResolver extends CookieGenerator implements LocaleConte private Function defaultTimeZoneFunction = request -> getDefaultTimeZone(); + + /** + * Create a new instance of {@link CookieLocaleResolver} using the supplied + * cookie name. + * @param cookieName the cookie name + * @since 6.0 + */ + public CookieLocaleResolver(String cookieName) { + Assert.notNull(cookieName, "cookieName must not be null"); + this.cookieName = cookieName; + } + /** * Create a new instance of {@link CookieLocaleResolver} using the * {@linkplain #DEFAULT_COOKIE_NAME default cookie name}. */ public CookieLocaleResolver() { - setCookieName(DEFAULT_COOKIE_NAME); + this(DEFAULT_COOKIE_NAME); } + /** + * Set the name of cookie created by this resolver. + * @param cookieName the cookie name + * @deprecated since 6.0, in favor of {@link #CookieLocaleResolver(String)} + */ + @Deprecated + public void setCookieName(String cookieName) { + Assert.notNull(cookieName, "cookieName must not be null"); + this.cookieName = cookieName; + } + + /** + * Set the cookie "Max-Age" attribute. + * @since 6.0 + * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#maxAge(Duration) + */ + public void setCookieMaxAge(Duration cookieMaxAge) { + Assert.notNull(cookieMaxAge, "cookieMaxAge must not be null"); + this.cookieMaxAge = cookieMaxAge; + } + + /** + * Variant of {@link #setCookieMaxAge(Duration)} accepting a value in + * seconds. + */ + @Deprecated + public void setCookieMaxAge(@Nullable Integer cookieMaxAge) { + setCookieMaxAge(Duration.ofSeconds((cookieMaxAge != null) ? cookieMaxAge : -1)); + } + + /** + * Set the cookie "Path" attribute. + * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#path(String) + */ + public void setCookiePath(@Nullable String cookiePath) { + this.cookiePath = cookiePath; + } + + /** + * Set the cookie "Domain" attribute. + * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#domain(String) + */ + public void setCookieDomain(@Nullable String cookieDomain) { + this.cookieDomain = cookieDomain; + } + + /** + * Add the "Secure" attribute to the cookie. + * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#secure(boolean) + */ + public void setCookieSecure(boolean cookieSecure) { + this.cookieSecure = cookieSecure; + } + + /** + * Add the "HttpOnly" attribute to the cookie. + * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#httpOnly(boolean) + */ + public void setCookieHttpOnly(boolean cookieHttpOnly) { + this.cookieHttpOnly = cookieHttpOnly; + } + + /** + * Add the "SameSite" attribute to the cookie. + * @since 6.0 + * @see org.springframework.http.ResponseCookie.ResponseCookieBuilder#sameSite(String) + */ + public void setCookieSameSite(String cookieSameSite) { + Assert.notNull(cookieSameSite, "cookieSameSite must not be null"); + this.cookieSameSite = cookieSameSite; + } + /** * Specify whether this resolver's cookies should be compliant with BCP 47 * language tags instead of Java's legacy locale specification format. @@ -161,40 +270,6 @@ public boolean isRejectInvalidCookies() { return this.rejectInvalidCookies; } - /** - * Set a fixed locale that this resolver will return if no cookie is found. - */ - public void setDefaultLocale(@Nullable Locale defaultLocale) { - this.defaultLocale = defaultLocale; - } - - /** - * Return the fixed locale that this resolver will return if no cookie is found, - * if any. - */ - @Nullable - protected Locale getDefaultLocale() { - return this.defaultLocale; - } - - /** - * Set a fixed time zone that this resolver will return if no cookie is found. - * @since 4.0 - */ - public void setDefaultTimeZone(@Nullable TimeZone defaultTimeZone) { - this.defaultTimeZone = defaultTimeZone; - } - - /** - * Return the fixed time zone that this resolver will return if no cookie is found, - * if any. - * @since 4.0 - */ - @Nullable - protected TimeZone getDefaultTimeZone() { - return this.defaultTimeZone; - } - /** * Set the function used to determine the default locale for the given request, * called if no locale cookie has been found. @@ -256,46 +331,43 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) { TimeZone timeZone = null; // Retrieve and parse cookie value. - String cookieName = getCookieName(); - if (cookieName != null) { - Cookie cookie = WebUtils.getCookie(request, cookieName); - if (cookie != null) { - String value = cookie.getValue(); - String localePart = value; - String timeZonePart = null; - int separatorIndex = localePart.indexOf('/'); - if (separatorIndex == -1) { - // Leniently accept older cookies separated by a space... - separatorIndex = localePart.indexOf(' '); - } - if (separatorIndex >= 0) { - localePart = value.substring(0, separatorIndex); - timeZonePart = value.substring(separatorIndex + 1); + Cookie cookie = WebUtils.getCookie(request, this.cookieName); + if (cookie != null) { + String value = cookie.getValue(); + String localePart = value; + String timeZonePart = null; + int separatorIndex = localePart.indexOf('/'); + if (separatorIndex == -1) { + // Leniently accept older cookies separated by a space... + separatorIndex = localePart.indexOf(' '); + } + if (separatorIndex >= 0) { + localePart = value.substring(0, separatorIndex); + timeZonePart = value.substring(separatorIndex + 1); + } + try { + locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null); + if (timeZonePart != null) { + timeZone = StringUtils.parseTimeZoneString(timeZonePart); } - try { - locale = (!"-".equals(localePart) ? parseLocaleValue(localePart) : null); - if (timeZonePart != null) { - timeZone = StringUtils.parseTimeZoneString(timeZonePart); - } + } + catch (IllegalArgumentException ex) { + if (isRejectInvalidCookies() && + request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { + throw new IllegalStateException("Encountered invalid locale cookie '" + + this.cookieName + "': [" + value + "] due to: " + ex.getMessage()); } - catch (IllegalArgumentException ex) { - if (isRejectInvalidCookies() && - request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) { - throw new IllegalStateException("Encountered invalid locale cookie '" + - cookieName + "': [" + value + "] due to: " + ex.getMessage()); - } - else { - // Lenient handling (e.g. error dispatch): ignore locale/timezone parse exceptions - if (logger.isDebugEnabled()) { - logger.debug("Ignoring invalid locale cookie '" + cookieName + - "': [" + value + "] due to: " + ex.getMessage()); - } + else { + // Lenient handling (e.g. error dispatch): ignore locale/timezone parse exceptions + if (logger.isDebugEnabled()) { + logger.debug("Ignoring invalid locale cookie '" + this.cookieName + + "': [" + value + "] due to: " + ex.getMessage()); } } - if (logger.isTraceEnabled()) { - logger.trace("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale + - "'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : "")); - } + } + if (logger.isTraceEnabled()) { + logger.trace("Parsed cookie value [" + cookie.getValue() + "] into locale '" + locale + + "'" + (timeZone != null ? " and time zone '" + timeZone.getID() + "'" : "")); } } @@ -306,11 +378,6 @@ private void parseLocaleCookieIfNecessary(HttpServletRequest request) { } } - @Override - public void setLocale(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable Locale locale) { - setLocaleContext(request, response, (locale != null ? new SimpleLocaleContext(locale) : null)); - } - @Override public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletResponse response, @Nullable LocaleContext localeContext) { @@ -319,17 +386,35 @@ public void setLocaleContext(HttpServletRequest request, @Nullable HttpServletRe Locale locale = null; TimeZone timeZone = null; + ResponseCookie cookie; if (localeContext != null) { locale = localeContext.getLocale(); if (localeContext instanceof TimeZoneAwareLocaleContext timeZoneAwareLocaleContext) { timeZone = timeZoneAwareLocaleContext.getTimeZone(); } - addCookie(response, - (locale != null ? toLocaleValue(locale) : "-") + (timeZone != null ? '/' + timeZone.getID() : "")); + cookie = ResponseCookie.from(this.cookieName, + (locale != null ? toLocaleValue(locale) : "-") + + (timeZone != null ? '/' + timeZone.getID() : "")) + .maxAge(this.cookieMaxAge) + .path(this.cookiePath) + .domain(this.cookieDomain) + .secure(this.cookieSecure) + .httpOnly(this.cookieHttpOnly) + .sameSite(this.cookieSameSite) + .build(); } else { - removeCookie(response); + // a cookie with empty value and max age 0 + cookie = ResponseCookie.from(this.cookieName, "") + .maxAge(Duration.ZERO) + .path(this.cookiePath) + .domain(this.cookieDomain) + .secure(this.cookieSecure) + .httpOnly(this.cookieHttpOnly) + .sameSite(this.cookieSameSite) + .build(); } + response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString()); request.setAttribute(LOCALE_REQUEST_ATTRIBUTE_NAME, (locale != null ? locale : this.defaultLocaleFunction.apply(request))); request.setAttribute(TIME_ZONE_REQUEST_ATTRIBUTE_NAME, diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/CookieLocaleResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/CookieLocaleResolverTests.java index 7cc0467b2af5..e2dfd3edd9df 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/CookieLocaleResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/CookieLocaleResolverTests.java @@ -16,6 +16,7 @@ package org.springframework.web.servlet.i18n; +import java.time.Duration; import java.util.Locale; import java.util.TimeZone; @@ -27,6 +28,8 @@ import org.springframework.context.i18n.SimpleLocaleContext; import org.springframework.context.i18n.SimpleTimeZoneAwareLocaleContext; import org.springframework.context.i18n.TimeZoneAwareLocaleContext; +import org.springframework.http.HttpHeaders; +import org.springframework.web.testfixture.servlet.MockCookie; import org.springframework.web.testfixture.servlet.MockHttpServletRequest; import org.springframework.web.testfixture.servlet.MockHttpServletResponse; import org.springframework.web.util.WebUtils; @@ -57,7 +60,7 @@ void resolveLocale() { Cookie cookie = new Cookie("LanguageKoekje", "nl"); request.setCookies(cookie); - resolver.setCookieName("LanguageKoekje"); + resolver = new CookieLocaleResolver("LanguageKoekje"); Locale loc = resolver.resolveLocale(request); assertThat(loc.getLanguage()).isEqualTo("nl"); } @@ -67,7 +70,7 @@ void resolveLocaleContext() { Cookie cookie = new Cookie("LanguageKoekje", "nl"); request.setCookies(cookie); - resolver.setCookieName("LanguageKoekje"); + resolver = new CookieLocaleResolver("LanguageKoekje"); LocaleContext loc = resolver.resolveLocaleContext(request); assertThat(loc.getLocale().getLanguage()).isEqualTo("nl"); assertThat(loc).isInstanceOf(TimeZoneAwareLocaleContext.class); @@ -79,7 +82,7 @@ void resolveLocaleContextWithTimeZone() { Cookie cookie = new Cookie("LanguageKoekje", "nl GMT+1"); request.setCookies(cookie); - resolver.setCookieName("LanguageKoekje"); + resolver = new CookieLocaleResolver("LanguageKoekje"); LocaleContext loc = resolver.resolveLocaleContext(request); assertThat(loc.getLocale().getLanguage()).isEqualTo("nl"); assertThat(loc).isInstanceOf(TimeZoneAwareLocaleContext.class); @@ -91,7 +94,7 @@ void resolveLocaleContextWithInvalidLocale() { Cookie cookie = new Cookie("LanguageKoekje", "++ GMT+1"); request.setCookies(cookie); - resolver.setCookieName("LanguageKoekje"); + resolver = new CookieLocaleResolver("LanguageKoekje"); assertThatIllegalStateException().isThrownBy(() -> resolver.resolveLocaleContext(request)) .withMessageContaining("LanguageKoekje") .withMessageContaining("++ GMT+1"); @@ -104,8 +107,8 @@ void resolveLocaleContextWithInvalidLocaleOnErrorDispatch() { Cookie cookie = new Cookie("LanguageKoekje", "++ GMT+1"); request.setCookies(cookie); + resolver = new CookieLocaleResolver("LanguageKoekje"); resolver.setDefaultTimeZone(TimeZone.getTimeZone("GMT+2")); - resolver.setCookieName("LanguageKoekje"); LocaleContext loc = resolver.resolveLocaleContext(request); assertThat(loc.getLocale()).isEqualTo(Locale.GERMAN); assertThat(loc).isInstanceOf(TimeZoneAwareLocaleContext.class); @@ -117,7 +120,7 @@ void resolveLocaleContextWithInvalidTimeZone() { Cookie cookie = new Cookie("LanguageKoekje", "nl X-MT"); request.setCookies(cookie); - resolver.setCookieName("LanguageKoekje"); + resolver = new CookieLocaleResolver("LanguageKoekje"); assertThatIllegalStateException().isThrownBy(() -> resolver.resolveLocaleContext(request)) .withMessageContaining("LanguageKoekje") .withMessageContaining("nl X-MT"); @@ -129,8 +132,8 @@ void resolveLocaleContextWithInvalidTimeZoneOnErrorDispatch() { Cookie cookie = new Cookie("LanguageKoekje", "nl X-MT"); request.setCookies(cookie); + resolver = new CookieLocaleResolver("LanguageKoekje"); resolver.setDefaultTimeZone(TimeZone.getTimeZone("GMT+2")); - resolver.setCookieName("LanguageKoekje"); LocaleContext loc = resolver.resolveLocaleContext(request); assertThat(loc.getLocale().getLanguage()).isEqualTo("nl"); assertThat(loc).isInstanceOf(TimeZoneAwareLocaleContext.class); @@ -141,12 +144,13 @@ void resolveLocaleContextWithInvalidTimeZoneOnErrorDispatch() { void setAndResolveLocale() { resolver.setLocale(request, response, new Locale("nl", "")); - Cookie cookie = response.getCookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME); + MockCookie cookie = MockCookie.parse(response.getHeader(HttpHeaders.SET_COOKIE)); assertThat(cookie).isNotNull(); assertThat(cookie.getName()).isEqualTo(CookieLocaleResolver.DEFAULT_COOKIE_NAME); assertThat(cookie.getDomain()).isNull(); assertThat(cookie.getPath()).isEqualTo(CookieLocaleResolver.DEFAULT_COOKIE_PATH); assertThat(cookie.getSecure()).isFalse(); + assertThat(cookie.getSameSite()).isEqualTo("Lax"); request = new MockHttpServletRequest(); request.setCookies(cookie); @@ -208,12 +212,13 @@ void setAndResolveLocaleContextWithTimeZoneOnly() { void setAndResolveLocaleWithCountry() { resolver.setLocale(request, response, new Locale("de", "AT")); - Cookie cookie = response.getCookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME); + MockCookie cookie = MockCookie.parse(response.getHeader(HttpHeaders.SET_COOKIE)); assertThat(cookie).isNotNull(); assertThat(cookie.getName()).isEqualTo(CookieLocaleResolver.DEFAULT_COOKIE_NAME); assertThat(cookie.getDomain()).isNull(); assertThat(cookie.getPath()).isEqualTo(CookieLocaleResolver.DEFAULT_COOKIE_PATH); assertThat(cookie.getSecure()).isFalse(); + assertThat(cookie.getSameSite()).isEqualTo("Lax"); assertThat(cookie.getValue()).isEqualTo("de-AT"); request = new MockHttpServletRequest(); @@ -230,12 +235,13 @@ void setAndResolveLocaleWithCountryAsLegacyJava() { resolver.setLanguageTagCompliant(false); resolver.setLocale(request, response, new Locale("de", "AT")); - Cookie cookie = response.getCookie(CookieLocaleResolver.DEFAULT_COOKIE_NAME); + MockCookie cookie = MockCookie.parse(response.getHeader(HttpHeaders.SET_COOKIE)); assertThat(cookie).isNotNull(); assertThat(cookie.getName()).isEqualTo(CookieLocaleResolver.DEFAULT_COOKIE_NAME); assertThat(cookie.getDomain()).isNull(); assertThat(cookie.getPath()).isEqualTo(CookieLocaleResolver.DEFAULT_COOKIE_PATH); assertThat(cookie.getSecure()).isFalse(); + assertThat(cookie.getSameSite()).isEqualTo("Lax"); assertThat(cookie.getValue()).isEqualTo("de_AT"); request = new MockHttpServletRequest(); @@ -249,26 +255,27 @@ void setAndResolveLocaleWithCountryAsLegacyJava() { @Test void customCookie() { - resolver.setCookieName("LanguageKoek"); + resolver = new CookieLocaleResolver("LanguageKoek"); resolver.setCookieDomain(".springframework.org"); resolver.setCookiePath("/mypath"); - resolver.setCookieMaxAge(10000); + resolver.setCookieMaxAge(Duration.ofSeconds(10000)); resolver.setCookieSecure(true); + resolver.setCookieSameSite("Lax"); resolver.setLocale(request, response, new Locale("nl", "")); - Cookie cookie = response.getCookie("LanguageKoek"); + MockCookie cookie = MockCookie.parse(response.getHeader(HttpHeaders.SET_COOKIE)); assertThat(cookie).isNotNull(); assertThat(cookie.getName()).isEqualTo("LanguageKoek"); assertThat(cookie.getDomain()).isEqualTo(".springframework.org"); assertThat(cookie.getPath()).isEqualTo("/mypath"); assertThat(cookie.getMaxAge()).isEqualTo(10000); assertThat(cookie.getSecure()).isTrue(); + assertThat(cookie.getSameSite()).isEqualTo("Lax"); request = new MockHttpServletRequest(); request.setCookies(cookie); - resolver = new CookieLocaleResolver(); - resolver.setCookieName("LanguageKoek"); + resolver = new CookieLocaleResolver("LanguageKoek"); Locale loc = resolver.resolveLocale(request); assertThat(loc.getLanguage()).isEqualTo("nl"); } diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/LocaleResolverTests.java b/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/LocaleResolverTests.java index a6fd5a187e62..3888670e3209 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/LocaleResolverTests.java +++ b/spring-webmvc/src/test/java/org/springframework/web/servlet/i18n/LocaleResolverTests.java @@ -99,6 +99,8 @@ private void doTest(LocaleResolver localeResolver, boolean shouldSet) { if (localeContextResolver instanceof AbstractLocaleContextResolver) { ((AbstractLocaleContextResolver) localeContextResolver).setDefaultTimeZone(TimeZone.getTimeZone("GMT+1")); + request.removeAttribute(CookieLocaleResolver.LOCALE_REQUEST_ATTRIBUTE_NAME); + localeContextResolver.resolveLocaleContext(request); assertThat(TimeZone.getTimeZone("GMT+1")).isEqualTo(((TimeZoneAwareLocaleContext) localeContext).getTimeZone()); } @@ -134,6 +136,8 @@ private void doTest(LocaleResolver localeResolver, boolean shouldSet) { if (localeContextResolver instanceof AbstractLocaleContextResolver) { ((AbstractLocaleContextResolver) localeContextResolver).setDefaultLocale(Locale.GERMANY); + request.removeAttribute(CookieLocaleResolver.LOCALE_REQUEST_ATTRIBUTE_NAME); + localeContextResolver.resolveLocaleContext(request); assertThat(localeContext.getLocale()).isEqualTo(Locale.GERMANY); } }