001/* 002 * Copyright 2002-2020 the original author or authors. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * https://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package org.springframework.web.server.i18n; 018 019import java.util.ArrayList; 020import java.util.List; 021import java.util.Locale; 022 023import org.springframework.context.i18n.LocaleContext; 024import org.springframework.context.i18n.SimpleLocaleContext; 025import org.springframework.http.HttpHeaders; 026import org.springframework.lang.Nullable; 027import org.springframework.util.CollectionUtils; 028import org.springframework.util.StringUtils; 029import org.springframework.web.server.ServerWebExchange; 030 031/** 032 * {@link LocaleContextResolver} implementation that simply uses the primary locale 033 * specified in the "Accept-Language" header of the HTTP request (that is, 034 * the locale sent by the client browser, normally that of the client's OS). 035 * 036 * <p>Note: Does not support {@link #setLocaleContext}, since the accept header 037 * can only be changed through changing the client's locale settings. 038 * 039 * @author Sebastien Deleuze 040 * @author Juergen Hoeller 041 * @since 5.0 042 * @see HttpHeaders#getAcceptLanguageAsLocales() 043 */ 044public class AcceptHeaderLocaleContextResolver implements LocaleContextResolver { 045 046 private final List<Locale> supportedLocales = new ArrayList<>(4); 047 048 @Nullable 049 private Locale defaultLocale; 050 051 052 /** 053 * Configure supported locales to check against the requested locales 054 * determined via {@link HttpHeaders#getAcceptLanguageAsLocales()}. 055 * @param locales the supported locales 056 */ 057 public void setSupportedLocales(List<Locale> locales) { 058 this.supportedLocales.clear(); 059 this.supportedLocales.addAll(locales); 060 } 061 062 /** 063 * Return the configured list of supported locales. 064 */ 065 public List<Locale> getSupportedLocales() { 066 return this.supportedLocales; 067 } 068 069 /** 070 * Configure a fixed default locale to fall back on if the request does not 071 * have an "Accept-Language" header (not set by default). 072 * @param defaultLocale the default locale to use 073 */ 074 public void setDefaultLocale(@Nullable Locale defaultLocale) { 075 this.defaultLocale = defaultLocale; 076 } 077 078 /** 079 * The configured default locale, if any. 080 * <p>This method may be overridden in subclasses. 081 */ 082 @Nullable 083 public Locale getDefaultLocale() { 084 return this.defaultLocale; 085 } 086 087 088 @Override 089 public LocaleContext resolveLocaleContext(ServerWebExchange exchange) { 090 List<Locale> requestLocales = null; 091 try { 092 requestLocales = exchange.getRequest().getHeaders().getAcceptLanguageAsLocales(); 093 } 094 catch (IllegalArgumentException ex) { 095 // Invalid Accept-Language header: treat as empty for matching purposes 096 } 097 return new SimpleLocaleContext(resolveSupportedLocale(requestLocales)); 098 } 099 100 @Nullable 101 private Locale resolveSupportedLocale(@Nullable List<Locale> requestLocales) { 102 if (CollectionUtils.isEmpty(requestLocales)) { 103 return getDefaultLocale(); // may be null 104 } 105 List<Locale> supportedLocales = getSupportedLocales(); 106 if (supportedLocales.isEmpty()) { 107 return requestLocales.get(0); // never null 108 } 109 110 Locale languageMatch = null; 111 for (Locale locale : requestLocales) { 112 if (supportedLocales.contains(locale)) { 113 if (languageMatch == null || languageMatch.getLanguage().equals(locale.getLanguage())) { 114 // Full match: language + country, possibly narrowed from earlier language-only match 115 return locale; 116 } 117 } 118 else if (languageMatch == null) { 119 // Let's try to find a language-only match as a fallback 120 for (Locale candidate : supportedLocales) { 121 if (!StringUtils.hasLength(candidate.getCountry()) && 122 candidate.getLanguage().equals(locale.getLanguage())) { 123 languageMatch = candidate; 124 break; 125 } 126 } 127 } 128 } 129 if (languageMatch != null) { 130 return languageMatch; 131 } 132 133 Locale defaultLocale = getDefaultLocale(); 134 return (defaultLocale != null ? defaultLocale : requestLocales.get(0)); 135 } 136 137 @Override 138 public void setLocaleContext(ServerWebExchange exchange, @Nullable LocaleContext locale) { 139 throw new UnsupportedOperationException( 140 "Cannot change HTTP accept header - use a different locale context resolution strategy"); 141 } 142 143}