AcceptHeaderLocaleContextResolver leniently handles invalid header value

Also falls back to language-only match among its supported locales now.

Issue: SPR-16500
Issue: SPR-16457
This commit is contained in:
Juergen Hoeller
2018-02-15 13:14:49 +01:00
parent 067ad4c57a
commit cd8a1bdb8b
6 changed files with 142 additions and 69 deletions

View File

@@ -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<String, String>, 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<Locale.LanguageRange> getAcceptLanguage() {
String value = getFirst(ACCEPT_LANGUAGE);
@@ -494,6 +495,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, 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<Locale> getAcceptLanguageAsLocales() {
List<Locale.LanguageRange> ranges = getAcceptLanguage();
@@ -879,7 +881,7 @@ public class HttpHeaders implements MultiValueMap<String, String>, Serializable
* by the {@code Date} header.
* <p>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);

View File

@@ -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).
*
* <p>Note: Does not support {@code setLocale}, since the accept header
* <p>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<Locale> locales) {
public void setSupportedLocales(List<Locale> 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<Locale> acceptableLocales = request.getHeaders().getAcceptLanguageAsLocales();
if (this.defaultLocale != null && acceptableLocales.isEmpty()) {
return new SimpleLocaleContext(this.defaultLocale);
List<Locale> requestLocales = null;
try {
requestLocales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales();
}
Locale requestLocale = acceptableLocales.isEmpty() ? null : acceptableLocales.get(0);
if (isSupportedLocale(requestLocale)) {
return new SimpleLocaleContext(requestLocale);
catch (IllegalArgumentException ex) {
// Invalid Accept-Language header: treat as empty for matching purposes
}
Locale supportedLocale = findSupportedLocale(request);
if (supportedLocale != null) {
return new SimpleLocaleContext(supportedLocale);
}
return (this.defaultLocale != null ? new SimpleLocaleContext(this.defaultLocale) :
new SimpleLocaleContext(requestLocale));
}
private boolean isSupportedLocale(@Nullable Locale locale) {
if (locale == null) {
return false;
}
List<Locale> supportedLocales = getSupportedLocales();
return (supportedLocales.isEmpty() || supportedLocales.contains(locale));
return new SimpleLocaleContext(resolveSupportedLocale(requestLocales));
}
@Nullable
private Locale findSupportedLocale(ServerHttpRequest request) {
List<Locale> requestLocales = request.getHeaders().getAcceptLanguageAsLocales();
private Locale resolveSupportedLocale(@Nullable List<Locale> requestLocales) {
if (CollectionUtils.isEmpty(requestLocales)) {
return this.defaultLocale; // may be null
}
List<Locale> supported = getSupportedLocales();
if (supported.isEmpty()) {
return requestLocales.get(0); // never null
}
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

View File

@@ -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.
*
* <p>Note: Does not support {@code setLocale(Context)}, as the fixed
* locale and time zone cannot be changed.
* <p>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() {