001// Copyright 2006, 2007, 2009, 2010, 2012, 2026 The Apache Software Foundation 002// 003// Licensed under the Apache License, Version 2.0 (the "License"); 004// you may not use this file except in compliance with the License. 005// You may obtain a copy of the License at 006// 007// http://www.apache.org/licenses/LICENSE-2.0 008// 009// Unless required by applicable law or agreed to in writing, software 010// distributed under the License is distributed on an "AS IS" BASIS, 011// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 012// See the License for the specific language governing permissions and 013// limitations under the License. 014 015package org.apache.tapestry5.internal.services; 016 017import java.util.Collections; 018import java.util.List; 019import java.util.Locale; 020import java.util.Map; 021import java.util.Set; 022 023import org.apache.tapestry5.OptionModel; 024import org.apache.tapestry5.SelectModel; 025import org.apache.tapestry5.SymbolConstants; 026import org.apache.tapestry5.commons.util.CollectionFactory; 027import org.apache.tapestry5.http.services.Request; 028import org.apache.tapestry5.internal.OptionModelImpl; 029import org.apache.tapestry5.internal.SelectModelImpl; 030import org.apache.tapestry5.internal.TapestryInternalUtils; 031import org.apache.tapestry5.ioc.annotations.Symbol; 032import org.apache.tapestry5.ioc.services.ThreadLocale; 033import org.apache.tapestry5.services.LocalizationSetter; 034import org.apache.tapestry5.services.PersistentLocale; 035 036/** 037 * Given a set of supported locales, for a specified desired locale, sets the current thread's locale to a supported 038 * locale that is closest to the desired. 039 */ 040public class LocalizationSetterImpl implements LocalizationSetter 041{ 042 /** 043 * Upper bound on the number of entries retained in {@link #localeCache}. 044 * 045 * <p> 046 * The cache lives for the lifetime of this (singleton) service. 047 * The configured supported locales are pre-seeded and never evicted, so this budget only applies 048 * to request-derived names. 049 * </p> 050 */ 051 static final int MAX_LOCALE_CACHE_SIZE = 1000; 052 053 /** 054 * Maximum length of a single underscore-separated term (language, country or variant) of a 055 * cacheable locale name. 056 */ 057 static final int MAX_TERM_LENGTH = 16; 058 059 private final Request request; 060 061 private final ThreadLocale threadLocale; 062 063 private final Locale defaultLocale; 064 065 private final Set<String> supportedLocaleNames; 066 067 private final List<Locale> supportedLocales; 068 069 private final Map<String, Locale> localeCache = CollectionFactory.newConcurrentMap(); 070 071 private final PersistentLocale persistentLocale; 072 073 public LocalizationSetterImpl(Request request, 074 075 PersistentLocale persistentLocale, 076 077 ThreadLocale threadLocale, 078 079 @Symbol(SymbolConstants.SUPPORTED_LOCALES) 080 String localeNames) 081 { 082 this.request = request; 083 084 this.persistentLocale = persistentLocale; 085 this.threadLocale = threadLocale; 086 087 this.supportedLocaleNames = CollectionFactory.newSet(); 088 089 String[] names = TapestryInternalUtils.splitAtCommas(localeNames); 090 091 for (String name : names) 092 { 093 supportedLocaleNames.add(name.toLowerCase()); 094 } 095 096 supportedLocales = parseNames(names); 097 098 defaultLocale = supportedLocales.get(0); 099 } 100 101 private List<Locale> parseNames(String[] localeNames) 102 { 103 List<Locale> list = CollectionFactory.newList(); 104 105 for (String name : localeNames) 106 { 107 Locale locale = constructLocale(name); 108 109 // Pre-seed the cache with the supported locales. 110 // They are trusted configuration and the hot resolution target. 111 localeCache.put(name, locale); 112 113 list.add(locale); 114 } 115 116 return Collections.unmodifiableList(list); 117 } 118 119 public Locale toLocale(String localeName) 120 { 121 Locale result = localeCache.get(localeName); 122 123 if (result == null) 124 { 125 result = constructLocale(localeName); 126 127 if (isCacheableLocaleName(localeName) && localeCache.size() < MAX_LOCALE_CACHE_SIZE) 128 { 129 localeCache.put(localeName, result); 130 } 131 } 132 133 return result; 134 } 135 136 /** 137 * Whether a locale name is structurally plausible enough to be worth storing in the long-lived 138 * {@link #localeCache}. 139 * 140 * <p>This is a deliberately strict, cheap guard, instead of a full full locale validation: 141 * at most three underscore-separated terms, each ASCII-alphanumeric and bounded by 142 * {@link #MAX_TERM_LENGTH}. 143 */ 144 static boolean isCacheableLocaleName(String localeName) 145 { 146 final int length = localeName.length(); 147 148 if (length == 0) 149 { 150 return false; 151 } 152 153 int terms = 1; 154 int termLength = 0; 155 156 for (int i = 0; i < length; i++) 157 { 158 final char c = localeName.charAt(i); 159 160 if (c == '_') 161 { 162 // matches constructLocale()'s own limit of three terms 163 if (++terms > 3) 164 { 165 return false; 166 } 167 termLength = 0; 168 } 169 else if (!isAsciiLetterOrDigit(c) || ++termLength > MAX_TERM_LENGTH) 170 { 171 return false; 172 } 173 } 174 175 return true; 176 } 177 178 private static boolean isAsciiLetterOrDigit(char c) 179 { 180 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'); 181 } 182 183 private Locale constructLocale(String name) 184 { 185 String[] terms = name.split("_"); 186 187 switch (terms.length) 188 { 189 case 1: 190 return new Locale(terms[0], ""); 191 192 case 2: 193 return new Locale(terms[0], terms[1]); 194 195 case 3: 196 return new Locale(terms[0], terms[1], terms[2]); 197 198 default: 199 throw new IllegalArgumentException(); 200 } 201 } 202 203 public boolean setLocaleFromLocaleName(String localeName) 204 { 205 boolean supported = isSupportedLocaleName(localeName); 206 207 if (supported) 208 { 209 Locale locale = findClosestSupportedLocale(toLocale(localeName)); 210 211 persistentLocale.set(locale); 212 213 threadLocale.setLocale(locale); 214 } 215 else 216 { 217 Locale requestLocale = request.getLocale(); 218 219 Locale supportedLocale = findClosestSupportedLocale(requestLocale); 220 221 threadLocale.setLocale(supportedLocale); 222 } 223 224 return supported; 225 } 226 227 public void setNonPersistentLocaleFromLocaleName(String localeName) 228 { 229 Locale requested = toLocale(localeName); 230 231 Locale supported = findClosestSupportedLocale(requested); 232 233 threadLocale.setLocale(supported); 234 } 235 236 public void setNonPersistentLocaleFromRequest(Request request) 237 { 238 Locale locale = request.getLocale(); 239 setNonPersistentLocaleFromLocaleName(locale.toString()); 240 } 241 242 private Locale findClosestSupportedLocale(Locale desiredLocale) 243 { 244 String localeName = desiredLocale.toString(); 245 246 while (true) 247 { 248 if (isSupportedLocaleName(localeName)) 249 return toLocale(localeName); 250 251 localeName = stripTerm(localeName); 252 253 if (localeName.length() == 0) 254 break; 255 } 256 257 return defaultLocale; 258 } 259 260 static String stripTerm(String localeName) 261 { 262 int scorex = localeName.lastIndexOf('_'); 263 264 return scorex < 0 ? "" : localeName.substring(0, scorex); 265 } 266 267 public List<Locale> getSupportedLocales() 268 { 269 return supportedLocales; 270 } 271 272 public boolean isSupportedLocaleName(String localeName) 273 { 274 return supportedLocaleNames.contains(localeName.toLowerCase()); 275 } 276 277 public SelectModel getSupportedLocalesModel() 278 { 279 List<OptionModel> options = CollectionFactory.newList(); 280 281 for (Locale l : supportedLocales) 282 { 283 options.add(new OptionModelImpl(l.getDisplayName(l), l)); 284 } 285 286 return new SelectModelImpl(null, options); 287 } 288 289}