2 ****************************************************************************************
\r
3 * Copyright (C) 2010, Google, Inc.; International Business Machines Corporation and *
\r
4 * others. All Rights Reserved. *
\r
5 ****************************************************************************************
\r
8 package com.ibm.icu.util;
\r
10 import java.util.Collections;
\r
11 import java.util.Comparator;
\r
12 import java.util.Iterator;
\r
13 import java.util.LinkedHashMap;
\r
14 import java.util.LinkedHashSet;
\r
15 import java.util.Map;
\r
16 import java.util.Set;
\r
17 import java.util.TreeMap;
\r
18 import java.util.regex.Matcher;
\r
19 import java.util.regex.Pattern;
\r
22 * Provides an immutable list of languages (locales) in priority order.
\r
23 * The string format is based on the Accept-Language format
\r
24 * {@link "http://www.ietf.org/rfc/rfc2616.txt"}, such as
\r
25 * "af, en, fr;q=0.9". Syntactically it is slightly
\r
26 * more lenient, in allowing extra whitespace between elements, extra commas,
\r
27 * and more than 3 decimals (on input), and pins between 0 and 1.
\r
28 * <p>In theory, Accept-Language indicates the relative 'quality' of each item,
\r
29 * but in practice, all of the browsers just take an ordered list, like
\r
30 * "en, fr, de", and synthesize arbitrary quality values that put these in the
\r
31 * right order, like: "en, fr;q=0.7, de;q=0.3". The quality values in these de facto
\r
32 * semantics thus have <b>nothing</b> to do with the relative qualities of the
\r
33 * original. Accept-Language also doesn't
\r
34 * specify the interpretation of multiple instances, eg what "en, fr, en;q=.5"
\r
36 * <p>There are various ways to build a LanguagePriorityList, such
\r
37 * as using the following equivalent patterns:
\r
40 * list = LanguagePriorityList.add("af, en, fr;q=0.9").build();
\r
42 * list2 = LanguagePriorityList
\r
43 * .add(ULocale.forString("af"))
\r
44 * .add(ULocale.ENGLISH)
\r
45 * .add(ULocale.FRENCH, 0.9d)
\r
48 * When the list is built, the internal values are sorted in descending order by
\r
49 * weight, and then by input order. That is, if two languages have the same weight, the first one in the original order
\r
50 * comes first. If exactly the same language tag appears multiple times,
\r
51 * the last one wins.
\r
53 * 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:
\r
58 * If it is off (the default), then all weights are reset to 1.0 after reordering.
\r
59 * This is to match the effect of the Accept-Language semantics as used in browsers, and results in the following:
\r
64 * @author markdavis@google.com
\r
66 * @provisional This API might change or be removed in a future release.
\r
68 public class LocalePriorityList implements Iterable<ULocale> {
\r
69 private static final double D0 = 0.0d;
\r
70 private static final Double D1 = 1.0d;
\r
72 private static final Pattern languageSplitter = Pattern.compile("\\s*,\\s*");
\r
73 private static final Pattern weightSplitter = Pattern
\r
74 .compile("\\s*(\\S*)\\s*;\\s*q\\s*=\\s*(\\S*)");
\r
75 private final Map<ULocale, Double> languagesAndWeights;
\r
78 * Add a language code to the list being built, with weight 1.0.
\r
80 * @param languageCode locale/language to be added
\r
81 * @return internal builder, for chaining
\r
83 * @provisional This API might change or be removed in a future release.
\r
85 public static Builder add(ULocale languageCode) {
\r
86 return new Builder().add(languageCode);
\r
90 * Add a language code to the list being built, with specified weight.
\r
92 * @param languageCode locale/language to be added
\r
93 * @param weight value from 0.0 to 1.0
\r
94 * @return internal builder, for chaining
\r
96 * @provisional This API might change or be removed in a future release.
\r
98 public static Builder add(ULocale languageCode, final double weight) {
\r
99 return new Builder().add(languageCode, weight);
\r
103 * Add a language priority list.
\r
105 * @param languagePriorityList list to add all the members of
\r
106 * @return internal builder, for chaining
\r
108 * @provisional This API might change or be removed in a future release.
\r
110 public static Builder add(LocalePriorityList languagePriorityList) {
\r
111 return new Builder().add(languagePriorityList);
\r
115 * Add language codes to the list being built, using a string in rfc2616
\r
116 * (lenient) format, where each language is a valid {@link ULocale}.
\r
118 * @param acceptLanguageString String in rfc2616 format (but leniently parsed)
\r
119 * @return internal builder, for chaining
\r
121 * @provisional This API might change or be removed in a future release.
\r
123 public static Builder add(String acceptLanguageString) {
\r
124 return new Builder().add(acceptLanguageString);
\r
128 * Return the weight for a given language, or null if there is none. Note that
\r
129 * the weights may be adjusted from those used to build the list.
\r
131 * @param language to get weight of
\r
134 * @provisional This API might change or be removed in a future release.
\r
136 public Double getWeight(ULocale language) {
\r
137 return languagesAndWeights.get(language);
\r
143 * @provisional This API might change or be removed in a future release.
\r
146 public String toString() {
\r
147 final StringBuilder result = new StringBuilder();
\r
148 for (final ULocale language : languagesAndWeights.keySet()) {
\r
149 if (result.length() != 0) {
\r
150 result.append(", ");
\r
152 result.append(language);
\r
153 double weight = languagesAndWeights.get(language);
\r
154 if (weight != D1) {
\r
155 result.append(";q=").append(weight);
\r
158 return result.toString();
\r
164 * @provisional This API might change or be removed in a future release.
\r
166 public Iterator<ULocale> iterator() {
\r
167 return languagesAndWeights.keySet().iterator();
\r
173 * @provisional This API might change or be removed in a future release.
\r
176 public boolean equals(final Object o) {
\r
178 final LocalePriorityList that = (LocalePriorityList) o;
\r
179 return languagesAndWeights.equals(that.languagesAndWeights);
\r
180 } catch (final RuntimeException e) {
\r
188 * @provisional This API might change or be removed in a future release.
\r
191 public int hashCode() {
\r
192 return languagesAndWeights.hashCode();
\r
195 // ==================== Privates ====================
\r
198 private LocalePriorityList(final Map<ULocale, Double> languageToWeight) {
\r
199 this.languagesAndWeights = languageToWeight;
\r
203 * Class used for building LanguagePriorityLists
\r
205 * @provisional This API might change or be removed in a future release.
\r
207 public static class Builder {
\r
209 * These store the input languages and weights, in chronological order,
\r
210 * where later additions override previous ones.
\r
212 private final Map<ULocale, Double> languageToWeight
\r
213 = new LinkedHashMap<ULocale, Double>();
\r
216 * Private constructor, only used by LocalePriorityList
\r
218 private Builder() {
\r
222 * Creates a LocalePriorityList. This is equivalent to
\r
223 * {@link Builder#build(boolean) Builder.build(false)}.
\r
225 * @return A LocalePriorityList
\r
227 * @provisional This API might change or be removed in a future release.
\r
229 public LocalePriorityList build() {
\r
230 return build(false);
\r
234 * Creates a LocalePriorityList.
\r
236 * @param preserveWeights when true, the weights originally came
\r
237 * from a language priority list specified by add() are preserved.
\r
238 * @return A LocalePriorityList
\r
240 * @provisional This API might change or be removed in a future release.
\r
242 public LocalePriorityList build(boolean preserveWeights) {
\r
243 // Walk through the input list, collecting the items with the same weights.
\r
244 final Map<Double, Set<ULocale>> doubleCheck = new TreeMap<Double, Set<ULocale>>(
\r
245 myDescendingDouble);
\r
246 for (final ULocale lang : languageToWeight.keySet()) {
\r
247 Double weight = languageToWeight.get(lang);
\r
248 Set<ULocale> s = doubleCheck.get(weight);
\r
250 doubleCheck.put(weight, s = new LinkedHashSet<ULocale>());
\r
254 // We now have a bunch of items sorted by weight, then chronologically.
\r
255 // We can now create a list in the right order
\r
256 final Map<ULocale, Double> temp = new LinkedHashMap<ULocale, Double>();
\r
257 for (final Double weight : doubleCheck.keySet()) {
\r
258 for (final ULocale lang : doubleCheck.get(weight)) {
\r
259 temp.put(lang, preserveWeights ? weight : D1);
\r
262 return new LocalePriorityList(Collections.unmodifiableMap(temp));
\r
266 * Adds a LocalePriorityList
\r
268 * @param languagePriorityList a LocalePriorityList
\r
269 * @return this, for chaining
\r
271 * @provisional This API might change or be removed in a future release.
\r
273 public Builder add(
\r
274 final LocalePriorityList languagePriorityList) {
\r
275 for (final ULocale language : languagePriorityList.languagesAndWeights
\r
277 add(language, languagePriorityList.languagesAndWeights.get(language));
\r
283 * Adds a new language code, with weight = 1.0.
\r
285 * @param languageCode to add with weight 1.0
\r
286 * @return this, for chaining
\r
288 * @provisional This API might change or be removed in a future release.
\r
290 public Builder add(final ULocale languageCode) {
\r
291 return add(languageCode, D1);
\r
295 * Adds language codes, with each having weight = 1.0.
\r
297 * @param languageCodes List of language codes.
\r
298 * @return this, for chaining.
\r
300 * @provisional This API might change or be removed in a future release.
\r
302 public Builder add(ULocale... languageCodes) {
\r
303 for (final ULocale languageCode : languageCodes) {
\r
304 add(languageCode, D1);
\r
310 * Adds a new supported languageCode, with specified weight. Overrides any
\r
311 * previous weight for the language.
\r
313 * @param languageCode language/locale to add
\r
314 * @param weight value between 0.0 and 1.1
\r
315 * @return this, for chaining.
\r
317 * @provisional This API might change or be removed in a future release.
\r
319 public Builder add(final ULocale languageCode,
\r
321 if (languageToWeight.containsKey(languageCode)) {
\r
322 languageToWeight.remove(languageCode);
\r
324 if (weight <= D0) {
\r
325 return this; // skip zeros
\r
326 } else if (weight > D1) {
\r
329 languageToWeight.put(languageCode, weight);
\r
334 * Adds rfc2616 list.
\r
336 * @param acceptLanguageList in rfc2616 format
\r
337 * @return this, for chaining.
\r
339 * @provisional This API might change or be removed in a future release.
\r
341 public Builder add(final String acceptLanguageList) {
\r
342 final String[] items = languageSplitter.split(acceptLanguageList.trim());
\r
343 final Matcher itemMatcher = weightSplitter.matcher("");
\r
344 for (final String item : items) {
\r
345 if (itemMatcher.reset(item).matches()) {
\r
346 final ULocale language = new ULocale(itemMatcher.group(1));
\r
347 final double weight = Double.parseDouble(itemMatcher.group(2));
\r
348 if (!(weight >= D0 && weight <= D1)) { // do ! for NaN
\r
349 throw new IllegalArgumentException("Illegal weight, must be 0..1: "
\r
352 add(language, weight);
\r
353 } else if (item.length() != 0) {
\r
354 add(new ULocale(item));
\r
361 private static Comparator<Double> myDescendingDouble = new Comparator<Double>() {
\r
362 public int compare(Double o1, Double o2) {
\r
363 return -o1.compareTo(o2);
\r