2 *******************************************************************************
3 * Copyright (C) 2010-2011, Google, Inc.; International Business Machines *
4 * Corporation and others. All Rights Reserved. *
5 *******************************************************************************
8 package com.ibm.icu.util;
10 import java.util.Collections;
11 import java.util.Comparator;
12 import java.util.Iterator;
13 import java.util.LinkedHashMap;
14 import java.util.LinkedHashSet;
16 import java.util.Map.Entry;
18 import java.util.TreeMap;
19 import java.util.regex.Matcher;
20 import java.util.regex.Pattern;
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"
37 * <p>There are various ways to build a LanguagePriorityList, such
38 * as using the following equivalent patterns:
41 * list = LanguagePriorityList.add("af, en, fr;q=0.9").build();
43 * list2 = LanguagePriorityList
44 * .add(ULocale.forString("af"))
45 * .add(ULocale.ENGLISH)
46 * .add(ULocale.FRENCH, 0.9d)
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,
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:
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:
65 * @author markdavis@google.com
68 public class LocalePriorityList implements Iterable<ULocale> {
69 private static final double D0 = 0.0d;
70 private static final Double D1 = 1.0d;
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;
78 * Add a language code to the list being built, with weight 1.0.
80 * @param languageCode locale/language to be added
81 * @return internal builder, for chaining
84 public static Builder add(ULocale languageCode) {
85 return new Builder().add(languageCode);
89 * Add a language code to the list being built, with specified weight.
91 * @param languageCode locale/language to be added
92 * @param weight value from 0.0 to 1.0
93 * @return internal builder, for chaining
96 public static Builder add(ULocale languageCode, final double weight) {
97 return new Builder().add(languageCode, weight);
101 * Add a language priority list.
103 * @param languagePriorityList list to add all the members of
104 * @return internal builder, for chaining
107 public static Builder add(LocalePriorityList languagePriorityList) {
108 return new Builder().add(languagePriorityList);
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}.
115 * @param acceptLanguageString String in rfc2616 format (but leniently parsed)
116 * @return internal builder, for chaining
119 public static Builder add(String acceptLanguageString) {
120 return new Builder().add(acceptLanguageString);
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.
127 * @param language to get weight of
131 public Double getWeight(ULocale language) {
132 return languagesAndWeights.get(language);
140 public String toString() {
141 final StringBuilder result = new StringBuilder();
142 for (final ULocale language : languagesAndWeights.keySet()) {
143 if (result.length() != 0) {
146 result.append(language);
147 double weight = languagesAndWeights.get(language);
149 result.append(";q=").append(weight);
152 return result.toString();
159 public Iterator<ULocale> iterator() {
160 return languagesAndWeights.keySet().iterator();
168 public boolean equals(final Object o) {
176 final LocalePriorityList that = (LocalePriorityList) o;
177 return languagesAndWeights.equals(that.languagesAndWeights);
178 } catch (final RuntimeException e) {
188 public int hashCode() {
189 return languagesAndWeights.hashCode();
192 // ==================== Privates ====================
195 private LocalePriorityList(final Map<ULocale, Double> languageToWeight) {
196 this.languagesAndWeights = languageToWeight;
200 * Class used for building LanguagePriorityLists
203 public static class Builder {
205 * These store the input languages and weights, in chronological order,
206 * where later additions override previous ones.
208 private final Map<ULocale, Double> languageToWeight
209 = new LinkedHashMap<ULocale, Double>();
212 * Private constructor, only used by LocalePriorityList
218 * Creates a LocalePriorityList. This is equivalent to
219 * {@link Builder#build(boolean) Builder.build(false)}.
221 * @return A LocalePriorityList
224 public LocalePriorityList build() {
229 * Creates a LocalePriorityList.
231 * @param preserveWeights when true, the weights originally came
232 * from a language priority list specified by add() are preserved.
233 * @return A LocalePriorityList
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>>(
240 for (final ULocale lang : languageToWeight.keySet()) {
241 Double weight = languageToWeight.get(lang);
242 Set<ULocale> s = doubleCheck.get(weight);
244 doubleCheck.put(weight, s = new LinkedHashSet<ULocale>());
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);
257 return new LocalePriorityList(Collections.unmodifiableMap(temp));
261 * Adds a LocalePriorityList
263 * @param languagePriorityList a LocalePriorityList
264 * @return this, for chaining
268 final LocalePriorityList languagePriorityList) {
269 for (final ULocale language : languagePriorityList.languagesAndWeights
271 add(language, languagePriorityList.languagesAndWeights.get(language));
277 * Adds a new language code, with weight = 1.0.
279 * @param languageCode to add with weight 1.0
280 * @return this, for chaining
283 public Builder add(final ULocale languageCode) {
284 return add(languageCode, D1);
288 * Adds language codes, with each having weight = 1.0.
290 * @param languageCodes List of language codes.
291 * @return this, for chaining.
294 public Builder add(ULocale... languageCodes) {
295 for (final ULocale languageCode : languageCodes) {
296 add(languageCode, D1);
302 * Adds a new supported languageCode, with specified weight. Overrides any
303 * previous weight for the language.
305 * @param languageCode language/locale to add
306 * @param weight value between 0.0 and 1.1
307 * @return this, for chaining.
310 public Builder add(final ULocale languageCode,
312 if (languageToWeight.containsKey(languageCode)) {
313 languageToWeight.remove(languageCode);
316 return this; // skip zeros
317 } else if (weight > D1) {
320 languageToWeight.put(languageCode, weight);
327 * @param acceptLanguageList in rfc2616 format
328 * @return this, for chaining.
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: "
342 add(language, weight);
343 } else if (item.length() != 0) {
344 add(new ULocale(item));
351 private static Comparator<Double> myDescendingDouble = new Comparator<Double>() {
352 public int compare(Double o1, Double o2) {
353 return -o1.compareTo(o2);