LocalePriorityList.java revision 7935b1839a081ed19ae0d33029ad3c09632a2caa
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(&quot;af, en, fr;q=0.9&quot;).build();
42 *
43 * list2 = LanguagePriorityList
44 *  .add(ULocale.forString(&quot;af&quot;))
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