1/* 2 ******************************************************************************* 3 * Copyright (C) 2010-2014, Google, Inc.; International Business Machines * 4 * Corporation and others. All Rights Reserved. * 5 ******************************************************************************* 6 */ 7 8package com.ibm.icu.util; 9 10import java.util.Collections; 11import java.util.Comparator; 12import java.util.Iterator; 13import java.util.LinkedHashMap; 14import java.util.LinkedHashSet; 15import java.util.Map; 16import java.util.Map.Entry; 17import java.util.Set; 18import java.util.TreeMap; 19import java.util.regex.Matcher; 20import java.util.regex.Pattern; 21 22/** 23 * Provides an immutable list of languages (locales) in priority order. 24 * The string format is based on the Accept-Language format 25 * {@link "http://www.ietf.org/rfc/rfc2616.txt"}, such as 26 * "af, en, fr;q=0.9". Syntactically it is slightly 27 * more lenient, in allowing extra whitespace between elements, extra commas, 28 * and more than 3 decimals (on input), and pins between 0 and 1. 29 * <p>In theory, Accept-Language indicates the relative 'quality' of each item, 30 * but in practice, all of the browsers just take an ordered list, like 31 * "en, fr, de", and synthesize arbitrary quality values that put these in the 32 * right order, like: "en, fr;q=0.7, de;q=0.3". The quality values in these de facto 33 * semantics thus have <b>nothing</b> to do with the relative qualities of the 34 * original. Accept-Language also doesn't 35 * specify the interpretation of multiple instances, eg what "en, fr, en;q=.5" 36 * means. 37 * <p>There are various ways to build a LanguagePriorityList, such 38 * as using the following equivalent patterns: 39 * 40 * <pre> 41 * list = LanguagePriorityList.add("af, en, fr;q=0.9").build(); 42 * 43 * list2 = LanguagePriorityList 44 * .add(ULocale.forString("af")) 45 * .add(ULocale.ENGLISH) 46 * .add(ULocale.FRENCH, 0.9d) 47 * .build(); 48 * </pre> 49 * When the list is built, the internal values are sorted in descending order by 50 * weight, and then by input order. That is, if two languages have the same weight, the first one in the original order 51 * comes first. If exactly the same language tag appears multiple times, 52 * the last one wins. 53 * 54 * There are two options when building. If preserveWeights are on, then "de;q=0.3, ja;q=0.3, en, fr;q=0.7, de " would result in the following: 55 * <pre> en;q=1.0 56 * de;q=1.0 57 * fr;q=0.7 58 * ja;q=0.3</pre> 59 * If it is off (the default), then all weights are reset to 1.0 after reordering. 60 * This is to match the effect of the Accept-Language semantics as used in browsers, and results in the following: 61 * * <pre> en;q=1.0 62 * de;q=1.0 63 * fr;q=1.0 64 * ja;q=1.0</pre> 65 * @author markdavis@google.com 66 * @stable ICU 4.4 67 */ 68public class LocalePriorityList implements Iterable<ULocale> { 69 private static final double D0 = 0.0d; 70 private static final Double D1 = 1.0d; 71 72 private static final Pattern languageSplitter = Pattern.compile("\\s*,\\s*"); 73 private static final Pattern weightSplitter = Pattern 74 .compile("\\s*(\\S*)\\s*;\\s*q\\s*=\\s*(\\S*)"); 75 private final Map<ULocale, Double> languagesAndWeights; 76 77 /** 78 * Add a language code to the list being built, with weight 1.0. 79 * 80 * @param languageCode locale/language to be added 81 * @return internal builder, for chaining 82 * @stable ICU 4.4 83 */ 84 public static Builder add(ULocale... languageCode) { 85 return new Builder().add(languageCode); 86 } 87 88 /** 89 * Add a language code to the list being built, with specified weight. 90 * 91 * @param languageCode locale/language to be added 92 * @param weight value from 0.0 to 1.0 93 * @return internal builder, for chaining 94 * @stable ICU 4.4 95 */ 96 public static Builder add(ULocale languageCode, final double weight) { 97 return new Builder().add(languageCode, weight); 98 } 99 100 /** 101 * Add a language priority list. 102 * 103 * @param languagePriorityList list to add all the members of 104 * @return internal builder, for chaining 105 * @stable ICU 4.4 106 */ 107 public static Builder add(LocalePriorityList languagePriorityList) { 108 return new Builder().add(languagePriorityList); 109 } 110 111 /** 112 * Add language codes to the list being built, using a string in rfc2616 113 * (lenient) format, where each language is a valid {@link ULocale}. 114 * 115 * @param acceptLanguageString String in rfc2616 format (but leniently parsed) 116 * @return internal builder, for chaining 117 * @stable ICU 4.4 118 */ 119 public static Builder add(String acceptLanguageString) { 120 return new Builder().add(acceptLanguageString); 121 } 122 123 /** 124 * Return the weight for a given language, or null if there is none. Note that 125 * the weights may be adjusted from those used to build the list. 126 * 127 * @param language to get weight of 128 * @return weight 129 * @stable ICU 4.4 130 */ 131 public Double getWeight(ULocale language) { 132 return languagesAndWeights.get(language); 133 } 134 135 /** 136 * {@inheritDoc} 137 * @stable ICU 4.4 138 */ 139 @Override 140 public String toString() { 141 final StringBuilder result = new StringBuilder(); 142 for (final ULocale language : languagesAndWeights.keySet()) { 143 if (result.length() != 0) { 144 result.append(", "); 145 } 146 result.append(language); 147 double weight = languagesAndWeights.get(language); 148 if (weight != D1) { 149 result.append(";q=").append(weight); 150 } 151 } 152 return result.toString(); 153 } 154 155 /** 156 * {@inheritDoc} 157 * @stable ICU 4.4 158 */ 159 public Iterator<ULocale> iterator() { 160 return languagesAndWeights.keySet().iterator(); 161 } 162 163 /** 164 * {@inheritDoc} 165 * @stable ICU 4.4 166 */ 167 @Override 168 public boolean equals(final Object o) { 169 if (o == null) { 170 return false; 171 } 172 if (this == o) { 173 return true; 174 } 175 try { 176 final LocalePriorityList that = (LocalePriorityList) o; 177 return languagesAndWeights.equals(that.languagesAndWeights); 178 } catch (final RuntimeException e) { 179 return false; 180 } 181 } 182 183 /** 184 * {@inheritDoc} 185 * @stable ICU 4.4 186 */ 187 @Override 188 public int hashCode() { 189 return languagesAndWeights.hashCode(); 190 } 191 192 // ==================== Privates ==================== 193 194 195 private LocalePriorityList(final Map<ULocale, Double> languageToWeight) { 196 this.languagesAndWeights = languageToWeight; 197 } 198 199 /** 200 * Class used for building LanguagePriorityLists 201 * @stable ICU 4.4 202 */ 203 public static class Builder { 204 /** 205 * These store the input languages and weights, in chronological order, 206 * where later additions override previous ones. 207 */ 208 private final Map<ULocale, Double> languageToWeight 209 = new LinkedHashMap<ULocale, Double>(); 210 211 /* 212 * Private constructor, only used by LocalePriorityList 213 */ 214 private Builder() { 215 } 216 217 /** 218 * Creates a LocalePriorityList. This is equivalent to 219 * {@link Builder#build(boolean) Builder.build(false)}. 220 * 221 * @return A LocalePriorityList 222 * @stable ICU 4.4 223 */ 224 public LocalePriorityList build() { 225 return build(false); 226 } 227 228 /** 229 * Creates a LocalePriorityList. 230 * 231 * @param preserveWeights when true, the weights originally came 232 * from a language priority list specified by add() are preserved. 233 * @return A LocalePriorityList 234 * @stable ICU 4.4 235 */ 236 public LocalePriorityList build(boolean preserveWeights) { 237 // Walk through the input list, collecting the items with the same weights. 238 final Map<Double, Set<ULocale>> doubleCheck = new TreeMap<Double, Set<ULocale>>( 239 myDescendingDouble); 240 for (final ULocale lang : languageToWeight.keySet()) { 241 Double weight = languageToWeight.get(lang); 242 Set<ULocale> s = doubleCheck.get(weight); 243 if (s == null) { 244 doubleCheck.put(weight, s = new LinkedHashSet<ULocale>()); 245 } 246 s.add(lang); 247 } 248 // We now have a bunch of items sorted by weight, then chronologically. 249 // We can now create a list in the right order 250 final Map<ULocale, Double> temp = new LinkedHashMap<ULocale, Double>(); 251 for (Entry<Double, Set<ULocale>> langEntry : doubleCheck.entrySet()) { 252 final Double weight = langEntry.getKey(); 253 for (final ULocale lang : langEntry.getValue()) { 254 temp.put(lang, preserveWeights ? weight : D1); 255 } 256 } 257 return new LocalePriorityList(Collections.unmodifiableMap(temp)); 258 } 259 260 /** 261 * Adds a LocalePriorityList 262 * 263 * @param languagePriorityList a LocalePriorityList 264 * @return this, for chaining 265 * @stable ICU 4.4 266 */ 267 public Builder add( 268 final LocalePriorityList languagePriorityList) { 269 for (final ULocale language : languagePriorityList.languagesAndWeights 270 .keySet()) { 271 add(language, languagePriorityList.languagesAndWeights.get(language)); 272 } 273 return this; 274 } 275 276 /** 277 * Adds a new language code, with weight = 1.0. 278 * 279 * @param languageCode to add with weight 1.0 280 * @return this, for chaining 281 * @stable ICU 4.4 282 */ 283 public Builder add(final ULocale languageCode) { 284 return add(languageCode, D1); 285 } 286 287 /** 288 * Adds language codes, with each having weight = 1.0. 289 * 290 * @param languageCodes List of language codes. 291 * @return this, for chaining. 292 * @stable ICU 4.4 293 */ 294 public Builder add(ULocale... languageCodes) { 295 for (final ULocale languageCode : languageCodes) { 296 add(languageCode, D1); 297 } 298 return this; 299 } 300 301 /** 302 * Adds a new supported languageCode, with specified weight. Overrides any 303 * previous weight for the language. 304 * 305 * @param languageCode language/locale to add 306 * @param weight value between 0.0 and 1.1 307 * @return this, for chaining. 308 * @stable ICU 4.4 309 */ 310 public Builder add(final ULocale languageCode, 311 double weight) { 312 if (languageToWeight.containsKey(languageCode)) { 313 languageToWeight.remove(languageCode); 314 } 315 if (weight <= D0) { 316 return this; // skip zeros 317 } else if (weight > D1) { 318 weight = D1; 319 } 320 languageToWeight.put(languageCode, weight); 321 return this; 322 } 323 324 /** 325 * Adds rfc2616 list. 326 * 327 * @param acceptLanguageList in rfc2616 format 328 * @return this, for chaining. 329 * @stable ICU 4.4 330 */ 331 public Builder add(final String acceptLanguageList) { 332 final String[] items = languageSplitter.split(acceptLanguageList.trim()); 333 final Matcher itemMatcher = weightSplitter.matcher(""); 334 for (final String item : items) { 335 if (itemMatcher.reset(item).matches()) { 336 final ULocale language = new ULocale(itemMatcher.group(1)); 337 final double weight = Double.parseDouble(itemMatcher.group(2)); 338 if (!(weight >= D0 && weight <= D1)) { // do ! for NaN 339 throw new IllegalArgumentException("Illegal weight, must be 0..1: " 340 + weight); 341 } 342 add(language, weight); 343 } else if (item.length() != 0) { 344 add(new ULocale(item)); 345 } 346 } 347 return this; 348 } 349 } 350 351 private static Comparator<Double> myDescendingDouble = new Comparator<Double>() { 352 public int compare(Double o1, Double o2) { 353 return -o1.compareTo(o2); 354 } 355 }; 356} 357