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