1/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package libcore.java.util;
18
19import static java.util.Locale.LanguageRange.MAX_WEIGHT;
20
21import java.util.ArrayList;
22import java.util.Arrays;
23import java.util.Collections;
24import java.util.HashMap;
25import java.util.List;
26import java.util.Locale.LanguageRange;
27import java.util.Map;
28import java.util.stream.Collectors;
29
30import junit.framework.TestCase;
31
32/**
33 * Tests {@link LanguageRange}.
34 */
35public class LocaleLanguageRangeTest extends TestCase {
36
37    /**
38     * Checks that the constants for min/max weight don't accidentally change.
39     */
40    public void testWeight_constantValues() {
41        assertEquals(0.0, LanguageRange.MIN_WEIGHT);
42        assertEquals(1.0, MAX_WEIGHT);
43    }
44
45    public void testConstructor_defaultsToMaxWeight() {
46        assertEquals(MAX_WEIGHT, new LanguageRange("de-DE").getWeight());
47    }
48
49    public void testConstructor_invalidWeight() {
50        try {
51            new LanguageRange("de-DE", -0.00000001);
52            fail();
53        } catch (IllegalArgumentException expected) {
54
55        }
56        try {
57            new LanguageRange("de-DE", 1.00000001);
58            fail();
59        } catch (IllegalArgumentException expected) {
60        }
61        // These work:
62        new LanguageRange("de-DE", 0);
63        new LanguageRange("de-DE", 1);
64    }
65
66    public void testConstructor_nullRange() {
67        try {
68            new LanguageRange(null);
69            fail();
70        } catch (NullPointerException expected) {
71        }
72        try {
73            new LanguageRange(null, MAX_WEIGHT);
74            fail();
75        } catch (NullPointerException expected) {
76        }
77        new LanguageRange("de-DE", MAX_WEIGHT); // works
78    }
79
80    public void testConstructor_checksForAtLeastOneSubtag() {
81        assertRangeMalformed("");
82        // The fact that ArrayIndexOutOfBoundsException instead of
83        // IllegalArgumentException is thrown here is somewhat
84        // inconsistent; the checks below ensure that we're aware
85        // if we change the behavior in future.
86        try {
87            new LanguageRange("-");
88            fail();
89        } catch (ArrayIndexOutOfBoundsException expected) {
90        }
91        try {
92            new LanguageRange("--");
93            fail();
94        } catch (ArrayIndexOutOfBoundsException expected) {
95        }
96    }
97
98    public void testConstructor_checksForWellFormedSubtags() {
99        // first subtag must not have digits
100        assertRangeMalformed("012-xx");
101        assertRangeMalformed("b0b-xx");
102        new LanguageRange("bob-xx"); // okay
103        new LanguageRange("bob-01"); // okay
104
105        // subtags must be <= 8 characters
106        assertRangeMalformed("de-abcdefghi-xx");
107        new LanguageRange("de-abcdefgh-xx"); // okay
108
109        // "-" only between subtags and only one in a row
110        assertRangeMalformed("-de");
111        assertRangeMalformed("de-");
112        assertRangeMalformed("de--DE");
113        new LanguageRange("de-DE"); // okay
114        new LanguageRange("de"); // okay
115    }
116
117    public void testConstructor_acceptsWildcardSubtags() {
118        new LanguageRange("de-*");
119        new LanguageRange("*-DE");
120        new LanguageRange("de-*-DE");
121        new LanguageRange("*");
122    }
123
124    public void testEqualsAndHashCode() {
125        checkEqual(new LanguageRange("en-US"), new LanguageRange("en-US"));
126        checkNotEqual(new LanguageRange("en-US"), new LanguageRange("en-AU"));
127
128        checkEqual(new LanguageRange("en-US"),
129                new LanguageRange("en-US", LanguageRange.MAX_WEIGHT));
130        checkNotEqual(new LanguageRange("en-US"), new LanguageRange("en-US", 0.4));
131
132        checkEqual(new LanguageRange("en-US", 0.3), new LanguageRange("en-US", 0.3));
133        checkNotEqual(new LanguageRange("en-US", 0.3), new LanguageRange("en-US", 0.4));
134        checkNotEqual(new LanguageRange("ja-JP", 0.5), new LanguageRange("de-DE", 0.5));
135    }
136
137    private static <T> void checkEqual(T a, T b) {
138        assertEquals(a, b);
139        assertEquals(b, a);
140        assertEquals(a.hashCode(), b.hashCode());
141    }
142
143    private static <T> void checkNotEqual(T a, T b) {
144        assertFalse(a.equals(b));
145        assertFalse(b.equals(a));
146        assertTrue(a.hashCode() != b.hashCode());
147    }
148
149    public void testGetRange() {
150        assertEquals("de-de", new LanguageRange("de-DE", 0.12345).getRange());
151    }
152
153    public void testGetWeight() {
154        assertEquals(0.12345, new LanguageRange("de-DE", 0.12345).getWeight());
155    }
156
157    public void testMapEquivalents_emptyList() {
158        List<LanguageRange> noRange = Collections.emptyList();
159        assertEquals(noRange, LanguageRange.mapEquivalents(noRange, Collections.emptyMap()));
160        assertEquals(noRange, LanguageRange.mapEquivalents(noRange,
161                Collections.singletonMap("en-US", Arrays.asList("en-US", "en-AU", "en-UK"))));
162    }
163
164    public void testMapEquivalents_emptyMap_createsModifiableCopy() {
165        List<LanguageRange> inputRanges = Collections.unmodifiableList(Arrays.asList(
166                new LanguageRange("de-DE"),
167                new LanguageRange("ja-JP")));
168        List<LanguageRange> outputRanges =
169                LanguageRange.mapEquivalents(inputRanges, Collections.emptyMap());
170        assertEquals(inputRanges, outputRanges);
171        assertNotSame(inputRanges, outputRanges);
172        // result is modifiable
173        outputRanges.add(new LanguageRange("fr-FR"));
174        outputRanges.clear();
175    }
176
177    /**
178     * Tests the example from the {@link LanguageRange#mapEquivalents(List, Map)} documentation.
179     */
180    public void testMapEquivalents_exampleFromDocumentation() {
181        Map<String, List<String>> map = new HashMap<>();
182        map.put("zh", Collections.unmodifiableList(Arrays.asList("zh", "zh-Hans")));
183        map.put("zh-HK", Collections.singletonList("zh-HK"));
184        map.put("zh-TW", Collections.singletonList("zh-TW"));
185
186        List<LanguageRange> inputPriorityList = Arrays.asList(
187                new LanguageRange("zh"),
188                new LanguageRange("zh-CN"),
189                new LanguageRange("en"),
190                new LanguageRange("zh-TW"),
191                new LanguageRange("zh-TW")
192        );
193        List<LanguageRange> expectedOutput = Arrays.asList(
194                new LanguageRange("zh"),
195                new LanguageRange("zh-Hans"),
196                new LanguageRange("zh-CN"),
197                new LanguageRange("zh-Hans-CN"),
198                new LanguageRange("en"),
199                new LanguageRange("zh-TW"),
200                new LanguageRange("zh-TW")
201        );
202        List<LanguageRange> outputProrityList = LanguageRange
203                .mapEquivalents(inputPriorityList, map);
204        assertEquals(expectedOutput, outputProrityList);
205    }
206
207    public void testMapEquivalents_nullList() {
208        try {
209            LanguageRange.mapEquivalents(null, Collections.emptyMap());
210            fail();
211        } catch (NullPointerException expected) {
212        }
213    }
214
215    /**
216     * The documentation doesn't specify whether {@code mapEquivalents()} accepts a
217     * null map, but the current behavior is the same as for an empty map. This test
218     * ensures that we're aware if this behavior changse.
219     */
220    public void testMapEquivalents_nullMap() {
221        List<LanguageRange> priorityList = Collections.unmodifiableList(Arrays.asList(
222                new LanguageRange("de-DE"),
223                new LanguageRange("en-UK"),
224                new LanguageRange("zh-CN")));
225        assertEquals(priorityList, LanguageRange.mapEquivalents(priorityList, null));
226    }
227
228    /** Tests {@link LanguageRange#parse(String, Map)}. */
229    public void testMapEquivalents() {
230        List<LanguageRange> expected = Arrays.asList(
231                new LanguageRange("de-de", 1.0),
232                new LanguageRange("en-us", 0.7),
233                new LanguageRange("en-au", 0.7)
234        );
235        Map<String, List<String>> map = new HashMap<>();
236        map.put("fr", Arrays.asList("de-DE"));
237        map.put("en", Arrays.asList("en-US", "en-AU"));
238        String ranges = "Accept-Language: fr,en;q=0.7";
239        assertEquals(expected, LanguageRange.parse(ranges, map));
240        // Per the documentation, this should be equivalent
241        assertEquals(expected, LanguageRange.mapEquivalents(LanguageRange.parse(ranges), map));
242    }
243
244    /**
245     * Because {@code mapEquivalents(ranges, map)} behaves identically
246     * to {@code mapEquivalents(parse(ranges), map}, any equivalent
247     * locales from {@link sun.util.locale.LocaleEquivalentMaps},
248     * such as {@code "iw" -> "he"}, are expanded before the mapping
249     * from {@code map} is applied.
250     */
251    public void testParse_map_localeEquivalent() {
252        Map<String, List<String>> map = new HashMap<>();
253        map.put("iw", Arrays.asList("de-DE"));
254        map.put("en", Arrays.asList("en-US", "en-AU"));
255
256        List<LanguageRange> expectedOutput = Arrays.asList(
257                new LanguageRange("de-de", 1.0), // iw -> de-de (map)
258                new LanguageRange("he", 1.0), // iw -> he (LocaleEquivalentMaps)
259                new LanguageRange("en-us", 0.7), // en -> en-us (map)
260                new LanguageRange("en-au", 0.7)); // en -> en-au (map)
261
262        String ranges = "Accept-Language: iw,en;q=0.7";
263        assertEquals(expectedOutput, LanguageRange.parse(ranges, map));
264        // Per the documentation, this should be equivalent
265        assertEquals(expectedOutput,
266                LanguageRange.mapEquivalents(LanguageRange.parse(ranges), map));
267    }
268
269    /**
270     * Tests the example from the {@link LanguageRange#parse(String)} documentation.
271     */
272    public void testParse_acceptLanguage_exampleFromDocumentation() {
273        List<LanguageRange> expected = Arrays.asList(
274                new LanguageRange("iw", 1.0),
275                new LanguageRange("he", 1.0),
276                new LanguageRange("en-us", 0.7),
277                new LanguageRange("en", 0.3)
278        );
279        assertEquals(expected, LanguageRange.parse("Accept-Language: iw,en-us;q=0.7,en;q=0.3"));
280    }
281
282    /**
283     * Tests parsing the example from RFC 2616 section 14.4.
284     */
285    public void testParse_acceptLanguage_exampleFromRfc2616() {
286        List<LanguageRange> expected = Arrays.asList(
287                new LanguageRange("da", 1.0),
288                new LanguageRange("en-gb", 0.8),
289                new LanguageRange("en", 0.7)
290        );
291        assertEquals(expected, LanguageRange.parse("Accept-Language: da, en-gb;q=0.8, en;q=0.7"));
292    }
293
294    public void testParse_acceptLanguage_malformed() {
295        try {
296            LanguageRange.parse("Accept-Language: fr,en-us;q=1;q=0.5");
297            fail();
298        } catch (IllegalArgumentException expected) {
299        }
300        try {
301            LanguageRange.parse("Accept-Language: q=0.5");
302            fail();
303        } catch (IllegalArgumentException expected) {
304        }
305        try {
306            LanguageRange.parse("Accept-Language: ;q=0.5");
307            fail();
308        } catch (IllegalArgumentException expected) {
309        }
310        try {
311            LanguageRange.parse("Accept-Language: thislanguagetagistoolong;q=0.5");
312            fail();
313        } catch (IllegalArgumentException expected) {
314        }
315    }
316
317    /**
318     * The current implementation doesn't require a ' ' after the "Accept-Language:".
319     * This test ensures that we're aware if this behavior changes.
320     */
321    public void testParse_acceptLanguage_missingSpaceAfterColon() {
322        List<LanguageRange> languageRanges = Arrays.asList(
323                new LanguageRange("fr"),
324                new LanguageRange("en-us", 1)
325        );
326        assertEquals(languageRanges, LanguageRange.parse("Accept-Language:fr,en-us;q=1"));
327    }
328
329    public void testParse_acceptLanguage_wildCards() {
330        List<LanguageRange> expected = Arrays.asList(
331                new LanguageRange("da", 1.0),
332                new LanguageRange("en-*", 0.8),
333                new LanguageRange("*", 0.7)
334        );
335        assertEquals(expected, LanguageRange.parse("Accept-Language: da, en-*;q=0.8, *;q=0.7"));
336    }
337
338    public void testParse_acceptLanguage_weightValid() {
339        LanguageRange fr = new LanguageRange("fr");
340        assertEquals(Arrays.asList(fr, new LanguageRange("en-us", 1.0)),
341                LanguageRange.parse("Accept-Language: fr,en-us;q=1"));
342        assertEquals(Arrays.asList(fr, new LanguageRange("en-us", 0.1)),
343                LanguageRange.parse("Accept-Language: fr,en-us;q=.1"));
344        assertEquals(Arrays.asList(fr, new LanguageRange("en-us", 0.12345678901234567890)),
345                LanguageRange.parse("Accept-Language: fr,en-us;q=0.12345678901234567890"));
346        assertEquals(Arrays.asList(fr, new LanguageRange("en-us", 0)),
347                LanguageRange.parse("Accept-Language: fr,en-us;q=0"));
348    }
349
350    public void testParse_acceptLanguage_weightInvalid() {
351        try {
352            LanguageRange.parse("Accept-Language: iw,en-us;q=1.1");
353            fail();
354        } catch (IllegalArgumentException expected) {
355        }
356        try {
357            LanguageRange.parse("Accept-Language: iw,en-us;q=-0.1");
358            fail();
359        } catch (IllegalArgumentException expected) {
360        }
361    }
362
363    // Based on a test case that was contributed back to upstream maintainers through
364    // https://bugs.openjdk.java.net/browse/JDK-8166994
365    public void testParse_multiEquivalent_consistency() {
366        List<String> parsed = rangesToStrings(LanguageRange.parse("ccq-xx"));
367
368        assertEquals(parsed, rangesToStrings(LanguageRange.parse("ccq-xx"))); // consistency
369        assertEquals(Arrays.asList("ccq-xx", "ybd-xx", "rki-xx"), parsed); // expected result
370    }
371
372    /**
373     * Tests parsing a Locale range matching an entry from
374     * {@link sun.util.locale.LocaleEquivalentMaps#singleEquivMap}.
375     */
376    public void testParse_singleEquivalent() {
377        assertParseRanges("art-lojban", "jbo"); // example from RFC 4647 section 3.2
378        assertParseRanges("yue", "zh-yue");
379        assertParseRanges("yue-xx", "zh-yue-xx");
380    }
381
382    /**
383     * Tests parsing a Locale range matching an entry from
384     * {@link sun.util.locale.LocaleEquivalentMaps#multiEquivsMap}.
385     */
386    public void testParse_multiEquivalent() {
387        assertParseRanges("mst", "myt", "mry");
388        assertParseRanges("i-hak", "zh-hakka", "hak");
389    }
390
391    /**
392     * Tests parsing a Locale range matching an entry from
393     * {@link sun.util.locale.LocaleEquivalentMaps#regionVariantEquivMap}.
394     */
395    public void testParse_regionEquivalent() {
396        // Region ("-de" or "-dd") matches the end
397        assertParseRanges("de-de", "de-dd");
398        assertParseRanges("xx-dd", "xx-de");
399
400        // Region ("-de" or "-dd") matches the middle
401        assertParseRanges("xx-de-yy", "xx-dd-yy");
402        assertParseRanges("xx-dd-yy", "xx-de-yy");
403
404        assertParseRanges("xx-bu", "xx-mm");
405        assertParseRanges("xx-mm", "xx-bu");
406    }
407
408    /**
409     * Tests parsing a Locale range matching entries from both
410     * {@link sun.util.locale.LocaleEquivalentMaps#singleEquivMap} and
411     * {@link sun.util.locale.LocaleEquivalentMaps#regionVariantEquivMap}.
412     */
413    public void testParse_singleAndRegionEquivalent() {
414        assertParseRanges("sgn-ch-de", "sgg", "sgn-ch-dd");
415        assertParseRanges("sgn-ch-de-xx", "sgg-xx", "sgn-ch-dd-xx");
416    }
417
418    /**
419     * Asserts that {@code LanguageRange(ranges)} returns LanguageRanges whose
420     * {@link LanguageRange#getRange() Range string}s are {@code ranges} and
421     * {@code expectedAdditional}, in order.
422     */
423    private static void assertParseRanges(String ranges, String... expectedAdditional) {
424        List<String> expected = new ArrayList<>();
425        expected.add(ranges);
426        expected.addAll(Arrays.asList(expectedAdditional));
427
428        List<String> actual = rangesToStrings(LanguageRange.parse(ranges));
429
430        assertEquals(expected, actual);
431    }
432
433    private static List<String> rangesToStrings(List<LanguageRange> languageRanges) {
434        return languageRanges.stream().map(LanguageRange::getRange).collect(Collectors.toList());
435    }
436
437    private void assertRangeMalformed(String range) {
438        try {
439            new LanguageRange(range);
440            fail("Range should be recognized as malformed: " + range);
441        } catch (IllegalArgumentException expected) {
442            // Check for the exception that is thrown when a malformed subtag is detected.
443            // The exception message used here may change in future.
444            assertEquals("range=" + range.toLowerCase(), expected.getMessage());
445        }
446    }
447
448}
449