From cd8a1bdb8bcf3fb903c7c88bdfd5c49c194f0c65 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Thu, 15 Feb 2018 13:14:49 +0100 Subject: [PATCH] AcceptHeaderLocaleContextResolver leniently handles invalid header value Also falls back to language-only match among its supported locales now. Issue: SPR-16500 Issue: SPR-16457 --- .../org/springframework/http/HttpHeaders.java | 6 +- .../AcceptHeaderLocaleContextResolver.java | 70 ++++++++++-------- .../i18n/FixedLocaleContextResolver.java | 12 ++-- ...cceptHeaderLocaleContextResolverTests.java | 71 ++++++++++++++++--- .../i18n/FixedLocaleContextResolverTests.java | 40 +++++++---- .../i18n/AcceptHeaderLocaleResolver.java | 6 +- 6 files changed, 139 insertions(+), 66 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java index 5ef7be1f677..c2132ba38e6 100644 --- a/spring-web/src/main/java/org/springframework/http/HttpHeaders.java +++ b/spring-web/src/main/java/org/springframework/http/HttpHeaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -473,6 +473,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * a list of supported locales you can pass the returned list to * {@link Locale#filter(List, Collection)}. * @since 5.0 + * @throws IllegalArgumentException if the value cannot be converted to a language range */ public List getAcceptLanguage() { String value = getFirst(ACCEPT_LANGUAGE); @@ -494,6 +495,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * {@link java.util.Locale.LanguageRange} to a {@link Locale}. * @return the locales or an empty list * @since 5.0 + * @throws IllegalArgumentException if the value cannot be converted to a locale */ public List getAcceptLanguageAsLocales() { List ranges = getAcceptLanguage(); @@ -879,7 +881,7 @@ public class HttpHeaders implements MultiValueMap, Serializable * by the {@code Date} header. *

The date is returned as the number of milliseconds since * January 1, 1970 GMT. Returns -1 when the date is unknown. - * @throws IllegalArgumentException if the value can't be converted to a date + * @throws IllegalArgumentException if the value cannot be converted to a date */ public long getDate() { return getFirstDate(DATE); diff --git a/spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java b/spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java index 13084aa3fc1..165835599bb 100644 --- a/spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java +++ b/spring-web/src/main/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -23,8 +23,9 @@ import java.util.Locale; import org.springframework.context.i18n.LocaleContext; import org.springframework.context.i18n.SimpleLocaleContext; import org.springframework.http.HttpHeaders; -import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; import org.springframework.web.server.ServerWebExchange; /** @@ -32,10 +33,11 @@ import org.springframework.web.server.ServerWebExchange; * specified in the "Accept-Language" header of the HTTP request (that is, * the locale sent by the client browser, normally that of the client's OS). * - *

Note: Does not support {@code setLocale}, since the accept header + *

Note: Does not support {@link #setLocaleContext}, since the accept header * can only be changed through changing the client's locale settings. * * @author Sebastien Deleuze + * @author Juergen Hoeller * @since 5.0 */ public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver { @@ -51,11 +53,9 @@ public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver * determined via {@link HttpHeaders#getAcceptLanguageAsLocales()}. * @param locales the supported locales */ - public void setSupportedLocales(@Nullable List locales) { + public void setSupportedLocales(List locales) { this.supportedLocales.clear(); - if (locales != null) { - this.supportedLocales.addAll(locales); - } + this.supportedLocales.addAll(locales); } /** @@ -82,42 +82,50 @@ public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver return this.defaultLocale; } + @Override public LocaleContext resolveLocaleContext(ServerWebExchange exchange) { - ServerHttpRequest request = exchange.getRequest(); - List acceptableLocales = request.getHeaders().getAcceptLanguageAsLocales(); - if (this.defaultLocale != null && acceptableLocales.isEmpty()) { - return new SimpleLocaleContext(this.defaultLocale); - } - Locale requestLocale = acceptableLocales.isEmpty() ? null : acceptableLocales.get(0); - if (isSupportedLocale(requestLocale)) { - return new SimpleLocaleContext(requestLocale); + List requestLocales = null; + try { + requestLocales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales(); } - Locale supportedLocale = findSupportedLocale(request); - if (supportedLocale != null) { - return new SimpleLocaleContext(supportedLocale); + catch (IllegalArgumentException ex) { + // Invalid Accept-Language header: treat as empty for matching purposes } - return (this.defaultLocale != null ? new SimpleLocaleContext(this.defaultLocale) : - new SimpleLocaleContext(requestLocale)); + return new SimpleLocaleContext(resolveSupportedLocale(requestLocales)); } - private boolean isSupportedLocale(@Nullable Locale locale) { - if (locale == null) { - return false; + @Nullable + private Locale resolveSupportedLocale(@Nullable List requestLocales) { + if (CollectionUtils.isEmpty(requestLocales)) { + return this.defaultLocale; // may be null + } + List supported = getSupportedLocales(); + if (supported.isEmpty()) { + return requestLocales.get(0); // never null } - List supportedLocales = getSupportedLocales(); - return (supportedLocales.isEmpty() || supportedLocales.contains(locale)); - } - @Nullable - private Locale findSupportedLocale(ServerHttpRequest request) { - List requestLocales = request.getHeaders().getAcceptLanguageAsLocales(); + Locale languageMatch = null; for (Locale locale : requestLocales) { - if (getSupportedLocales().contains(locale)) { + if (supported.contains(locale)) { + // Full match: typically language + country return locale; } + else if (languageMatch == null) { + // Let's try to find a language-only match as a fallback + for (Locale candidate : supported) { + if (!StringUtils.hasLength(candidate.getCountry()) && + candidate.getLanguage().equals(locale.getLanguage())) { + languageMatch = candidate; + } + } + } } - return null; + if (languageMatch != null) { + return languageMatch; + } + + return (this.defaultLocale != null ? this.defaultLocale : requestLocales.get(0)); } @Override diff --git a/spring-web/src/main/java/org/springframework/web/server/i18n/FixedLocaleContextResolver.java b/spring-web/src/main/java/org/springframework/web/server/i18n/FixedLocaleContextResolver.java index a3a1e77a8b3..027ce331831 100644 --- a/spring-web/src/main/java/org/springframework/web/server/i18n/FixedLocaleContextResolver.java +++ b/spring-web/src/main/java/org/springframework/web/server/i18n/FixedLocaleContextResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -26,12 +26,11 @@ import org.springframework.util.Assert; import org.springframework.web.server.ServerWebExchange; /** - * {@link LocaleContextResolver} implementation that always returns - * a fixed default locale and optionally time zone. - * Default is the current JVM's default locale. + * {@link LocaleContextResolver} implementation that always returns a fixed locale + * and optionally time zone. Default is the current JVM's default locale. * - *

Note: Does not support {@code setLocale(Context)}, as the fixed - * locale and time zone cannot be changed. + *

Note: Does not support {@link #setLocaleContext}, as the fixed locale and + * time zone cannot be changed. * * @author Sebastien Deleuze * @since 5.0 @@ -71,6 +70,7 @@ public class FixedLocaleContextResolver implements LocaleContextResolver { this.timeZone = timeZone; } + @Override public LocaleContext resolveLocaleContext(ServerWebExchange exchange) { return new TimeZoneAwareLocaleContext() { diff --git a/spring-web/src/test/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolverTests.java b/spring-web/src/test/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolverTests.java index 3835ea1f856..22ede5a4d3a 100644 --- a/spring-web/src/test/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/i18n/AcceptHeaderLocaleContextResolverTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -22,37 +22,39 @@ import java.util.Locale; import org.junit.Test; +import org.springframework.http.HttpHeaders; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.web.test.server.MockServerWebExchange; import org.springframework.web.server.ServerWebExchange; import static java.util.Locale.*; -import static org.junit.Assert.assertEquals; +import static org.junit.Assert.*; /** * Unit tests for {@link AcceptHeaderLocaleContextResolver}. * * @author Sebastien Deleuze + * @author Juergen Hoeller */ public class AcceptHeaderLocaleContextResolverTests { - private AcceptHeaderLocaleContextResolver resolver = new AcceptHeaderLocaleContextResolver(); + private final AcceptHeaderLocaleContextResolver resolver = new AcceptHeaderLocaleContextResolver(); @Test - public void resolve() throws Exception { + public void resolve() { assertEquals(CANADA, this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale()); assertEquals(US, this.resolver.resolveLocaleContext(exchange(US, CANADA)).getLocale()); } @Test - public void resolvePreferredSupported() throws Exception { + public void resolvePreferredSupported() { this.resolver.setSupportedLocales(Collections.singletonList(CANADA)); assertEquals(CANADA, this.resolver.resolveLocaleContext(exchange(US, CANADA)).getLocale()); } @Test - public void resolvePreferredNotSupported() throws Exception { + public void resolvePreferredNotSupported() { this.resolver.setSupportedLocales(Collections.singletonList(CANADA)); assertEquals(US, this.resolver.resolveLocaleContext(exchange(US, UK)).getLocale()); } @@ -61,14 +63,65 @@ public class AcceptHeaderLocaleContextResolverTests { public void resolvePreferredNotSupportedWithDefault() { this.resolver.setSupportedLocales(Arrays.asList(US, JAPAN)); this.resolver.setDefaultLocale(JAPAN); + assertEquals(JAPAN, this.resolver.resolveLocaleContext(exchange(KOREA)).getLocale()); + } + + @Test + public void resolvePreferredAgainstLanguageOnly() { + this.resolver.setSupportedLocales(Collections.singletonList(ENGLISH)); + assertEquals(ENGLISH, this.resolver.resolveLocaleContext(exchange(GERMANY, US, UK)).getLocale()); + } + + @Test + public void resolveMissingAcceptLanguageHeader() { + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + assertNull(this.resolver.resolveLocaleContext(exchange).getLocale()); + } - MockServerHttpRequest request = MockServerHttpRequest.get("/").acceptLanguageAsLocales(KOREA).build(); + @Test + public void resolveMissingAcceptLanguageHeaderWithDefault() { + this.resolver.setDefaultLocale(US); + + MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + assertEquals(US, this.resolver.resolveLocaleContext(exchange).getLocale()); + } + + @Test + public void resolveEmptyAcceptLanguageHeader() { + MockServerHttpRequest request = MockServerHttpRequest.get("/").header(HttpHeaders.ACCEPT_LANGUAGE, "").build(); MockServerWebExchange exchange = MockServerWebExchange.from(request); - assertEquals(JAPAN, this.resolver.resolveLocaleContext(exchange).getLocale()); + assertNull(this.resolver.resolveLocaleContext(exchange).getLocale()); + } + + @Test + public void resolveEmptyAcceptLanguageHeaderWithDefault() { + this.resolver.setDefaultLocale(US); + + MockServerHttpRequest request = MockServerHttpRequest.get("/").header(HttpHeaders.ACCEPT_LANGUAGE, "").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + assertEquals(US, this.resolver.resolveLocaleContext(exchange).getLocale()); + } + + @Test + public void resolveInvalidAcceptLanguageHeader() { + MockServerHttpRequest request = MockServerHttpRequest.get("/").header(HttpHeaders.ACCEPT_LANGUAGE, "en_US").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + assertNull(this.resolver.resolveLocaleContext(exchange).getLocale()); + } + + @Test + public void resolveInvalidAcceptLanguageHeaderWithDefault() { + this.resolver.setDefaultLocale(US); + + MockServerHttpRequest request = MockServerHttpRequest.get("/").header(HttpHeaders.ACCEPT_LANGUAGE, "en_US").build(); + MockServerWebExchange exchange = MockServerWebExchange.from(request); + assertEquals(US, this.resolver.resolveLocaleContext(exchange).getLocale()); } @Test - public void defaultLocale() throws Exception { + public void defaultLocale() { this.resolver.setDefaultLocale(JAPANESE); MockServerHttpRequest request = MockServerHttpRequest.get("/").build(); MockServerWebExchange exchange = MockServerWebExchange.from(request); diff --git a/spring-web/src/test/java/org/springframework/web/server/i18n/FixedLocaleContextResolverTests.java b/spring-web/src/test/java/org/springframework/web/server/i18n/FixedLocaleContextResolverTests.java index 298d1f9ccdc..bc20ad2a950 100644 --- a/spring-web/src/test/java/org/springframework/web/server/i18n/FixedLocaleContextResolverTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/i18n/FixedLocaleContextResolverTests.java @@ -1,3 +1,19 @@ +/* + * Copyright 2002-2018 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. + * 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.springframework.web.server.i18n; import java.time.ZoneId; @@ -12,10 +28,8 @@ import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.mock.web.test.server.MockServerWebExchange; import org.springframework.web.server.ServerWebExchange; -import static java.util.Locale.CANADA; -import static java.util.Locale.FRANCE; -import static java.util.Locale.US; -import static org.junit.Assert.assertEquals; +import static java.util.Locale.*; +import static org.junit.Assert.*; /** * Unit tests for {@link FixedLocaleContextResolver}. @@ -24,8 +38,6 @@ import static org.junit.Assert.assertEquals; */ public class FixedLocaleContextResolverTests { - private FixedLocaleContextResolver resolver; - @Before public void setup() { Locale.setDefault(US); @@ -33,23 +45,23 @@ public class FixedLocaleContextResolverTests { @Test public void resolveDefaultLocale() { - this.resolver = new FixedLocaleContextResolver(); - assertEquals(US, this.resolver.resolveLocaleContext(exchange()).getLocale()); - assertEquals(US, this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale()); + FixedLocaleContextResolver resolver = new FixedLocaleContextResolver(); + assertEquals(US, resolver.resolveLocaleContext(exchange()).getLocale()); + assertEquals(US, resolver.resolveLocaleContext(exchange(CANADA)).getLocale()); } @Test public void resolveCustomizedLocale() { - this.resolver = new FixedLocaleContextResolver(FRANCE); - assertEquals(FRANCE, this.resolver.resolveLocaleContext(exchange()).getLocale()); - assertEquals(FRANCE, this.resolver.resolveLocaleContext(exchange(CANADA)).getLocale()); + FixedLocaleContextResolver resolver = new FixedLocaleContextResolver(FRANCE); + assertEquals(FRANCE, resolver.resolveLocaleContext(exchange()).getLocale()); + assertEquals(FRANCE, resolver.resolveLocaleContext(exchange(CANADA)).getLocale()); } @Test public void resolveCustomizedAndTimeZoneLocale() { TimeZone timeZone = TimeZone.getTimeZone(ZoneId.of("UTC")); - this.resolver = new FixedLocaleContextResolver(FRANCE, timeZone); - TimeZoneAwareLocaleContext context = (TimeZoneAwareLocaleContext)this.resolver.resolveLocaleContext(exchange()); + FixedLocaleContextResolver resolver = new FixedLocaleContextResolver(FRANCE, timeZone); + TimeZoneAwareLocaleContext context = (TimeZoneAwareLocaleContext) resolver.resolveLocaleContext(exchange()); assertEquals(FRANCE, context.getLocale()); assertEquals(timeZone, context.getTimeZone()); } diff --git a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java index 7a3f7dc2cd0..d1ee4d81a38 100644 --- a/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java +++ b/spring-webmvc/src/main/java/org/springframework/web/servlet/i18n/AcceptHeaderLocaleResolver.java @@ -55,11 +55,9 @@ public class AcceptHeaderLocaleResolver implements LocaleResolver { * @param locales the supported locales * @since 4.3 */ - public void setSupportedLocales(@Nullable List locales) { + public void setSupportedLocales(List locales) { this.supportedLocales.clear(); - if (locales != null) { - this.supportedLocales.addAll(locales); - } + this.supportedLocales.addAll(locales); } /**