From a2ea276e83ea3c83daee354e9cb08e5001a6247c Mon Sep 17 00:00:00 2001 From: Justin Lu Date: Tue, 12 May 2026 16:23:06 +0000 Subject: [PATCH] 8381644: Locale matching APIs should describe `null` element behavior Reviewed-by: naoto --- .../share/classes/java/util/Locale.java | 41 +++++++++----- .../sun/util/locale/LocaleMatcher.java | 17 ++++-- .../java/util/Locale/LocaleMatchingTest.java | 56 ++++++++++++------- 3 files changed, 75 insertions(+), 39 deletions(-) diff --git a/src/java.base/share/classes/java/util/Locale.java b/src/java.base/share/classes/java/util/Locale.java index 6b071cd15b2..2bab271a489 100644 --- a/src/java.base/share/classes/java/util/Locale.java +++ b/src/java.base/share/classes/java/util/Locale.java @@ -3493,7 +3493,8 @@ public final class Locale implements Cloneable, Serializable { * sorted in descending order based on priority or weight, or an empty * list if nothing matches. The list is modifiable. * @throws NullPointerException if {@code priorityList} or {@code locales} - * is {@code null} + * are {@code null}. {@code NullPointerException} may be thrown if any + * elements within either {@code Collection} are {@code null}. * @throws IllegalArgumentException if one or more extended language ranges * are included in the given list when * {@link FilteringMode#REJECT_EXTENDED_RANGES} is specified @@ -3503,6 +3504,8 @@ public final class Locale implements Cloneable, Serializable { public static List filter(List priorityList, Collection locales, FilteringMode mode) { + Objects.requireNonNull(priorityList); + Objects.requireNonNull(locales); return LocaleMatcher.filter(priorityList, locales, mode); } @@ -3522,12 +3525,15 @@ public final class Locale implements Cloneable, Serializable { * sorted in descending order based on priority or weight, or an empty * list if nothing matches. The list is modifiable. * @throws NullPointerException if {@code priorityList} or {@code locales} - * is {@code null} + * are {@code null}. {@code NullPointerException} may be thrown if any + * elements within either {@code Collection} are {@code null}. * * @since 1.8 */ public static List filter(List priorityList, Collection locales) { + Objects.requireNonNull(priorityList); + Objects.requireNonNull(locales); return filter(priorityList, locales, FilteringMode.AUTOSELECT_FILTERING); } @@ -3553,8 +3559,9 @@ public final class Locale implements Cloneable, Serializable { * @return a list of matching language tags sorted in descending order * based on priority or weight, or an empty list if nothing matches. * The list is modifiable. - * @throws NullPointerException if {@code priorityList} or {@code tags} is - * {@code null} + * @throws NullPointerException if {@code priorityList} or {@code tags} + * are {@code null}. {@code NullPointerException} may be thrown if any + * elements within either {@code Collection} are {@code null}. * @throws IllegalArgumentException if one or more extended language ranges * are included in the given list when * {@link FilteringMode#REJECT_EXTENDED_RANGES} is specified @@ -3564,6 +3571,8 @@ public final class Locale implements Cloneable, Serializable { public static List filterTags(List priorityList, Collection tags, FilteringMode mode) { + Objects.requireNonNull(priorityList); + Objects.requireNonNull(tags); return LocaleMatcher.filterTags(priorityList, tags, mode); } @@ -3590,13 +3599,15 @@ public final class Locale implements Cloneable, Serializable { * @return a list of matching language tags sorted in descending order * based on priority or weight, or an empty list if nothing matches. * The list is modifiable. - * @throws NullPointerException if {@code priorityList} or {@code tags} is - * {@code null} - * + * @throws NullPointerException if {@code priorityList} or {@code tags} + * are {@code null}. {@code NullPointerException} may be thrown if any + * elements within either {@code Collection} are {@code null}. * @since 1.8 */ public static List filterTags(List priorityList, Collection tags) { + Objects.requireNonNull(priorityList); + Objects.requireNonNull(tags); return filterTags(priorityList, tags, FilteringMode.AUTOSELECT_FILTERING); } @@ -3609,13 +3620,15 @@ public final class Locale implements Cloneable, Serializable { * @param locales {@code Locale} instances used for matching * @return the best matching {@code Locale} instance chosen based on * priority or weight, or {@code null} if nothing matches. - * @throws NullPointerException if {@code priorityList} or {@code locales} is - * {@code null} - * + * @throws NullPointerException if {@code priorityList} or {@code locales} + * are {@code null}. {@code NullPointerException} may be thrown if any + * elements within either {@code Collection} are {@code null}. * @since 1.8 */ public static Locale lookup(List priorityList, Collection locales) { + Objects.requireNonNull(priorityList); + Objects.requireNonNull(locales); return LocaleMatcher.lookup(priorityList, locales); } @@ -3631,13 +3644,15 @@ public final class Locale implements Cloneable, Serializable { * @param tags language tags used for matching * @return the best matching language tag chosen based on priority or * weight, or {@code null} if nothing matches. - * @throws NullPointerException if {@code priorityList} or {@code tags} is - * {@code null} - * + * @throws NullPointerException if {@code priorityList} or {@code tags} + * are {@code null}. {@code NullPointerException} may be thrown if any + * elements within either {@code Collection} are {@code null}. * @since 1.8 */ public static String lookupTag(List priorityList, Collection tags) { + Objects.requireNonNull(priorityList); + Objects.requireNonNull(tags); return LocaleMatcher.lookupTag(priorityList, tags); } diff --git a/src/java.base/share/classes/sun/util/locale/LocaleMatcher.java b/src/java.base/share/classes/sun/util/locale/LocaleMatcher.java index 72844b966d0..bc5115e1ff1 100644 --- a/src/java.base/share/classes/sun/util/locale/LocaleMatcher.java +++ b/src/java.base/share/classes/sun/util/locale/LocaleMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2012, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -30,12 +30,17 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; -import java.util.Locale.*; -import static java.util.Locale.FilteringMode.*; -import static java.util.Locale.LanguageRange.*; +import java.util.Locale.FilteringMode; +import java.util.Locale.LanguageRange; import java.util.Map; -import java.util.Set; -import java.util.TreeSet; +import java.util.Objects; + +import static java.util.Locale.FilteringMode.AUTOSELECT_FILTERING; +import static java.util.Locale.FilteringMode.EXTENDED_FILTERING; +import static java.util.Locale.FilteringMode.MAP_EXTENDED_RANGES; +import static java.util.Locale.FilteringMode.REJECT_EXTENDED_RANGES; +import static java.util.Locale.LanguageRange.MAX_WEIGHT; +import static java.util.Locale.LanguageRange.MIN_WEIGHT; /** * Implementation for BCP47 Locale matching diff --git a/test/jdk/java/util/Locale/LocaleMatchingTest.java b/test/jdk/java/util/Locale/LocaleMatchingTest.java index 4509968b4e4..c5d8a00d458 100644 --- a/test/jdk/java/util/Locale/LocaleMatchingTest.java +++ b/test/jdk/java/util/Locale/LocaleMatchingTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2014, 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2014, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -23,13 +23,14 @@ /* * @test - * @bug 7069824 8042360 8032842 8175539 8210443 8242010 8276302 + * @bug 7069824 8042360 8032842 8175539 8210443 8242010 8276302 8381644 * @summary Verify implementation for Locale matching. * @run junit/othervm LocaleMatchingTest */ import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; import java.util.ArrayList; @@ -41,6 +42,7 @@ import java.util.Locale; import java.util.Locale.FilteringMode; import java.util.Locale.LanguageRange; import java.util.Map; +import java.util.stream.Stream; import static java.util.Locale.FilteringMode.*; import static java.util.Locale.LanguageRange.*; @@ -200,12 +202,13 @@ public class LocaleMatchingTest { }; } - static Object[][] LFilterNPEData() { - return new Object[][] { - // Range, LanguageTags, FilteringMode - {"en;q=0.2, ja-*-JP, fr-JP", null, REJECT_EXTENDED_RANGES}, - {null, "de-DE, en, ja-JP-hepburn, fr, he, ja-Latn-JP", REJECT_EXTENDED_RANGES}, - }; + static Stream LFilterLookupNPEData() { + return Stream.of( + // null Locale list + Arguments.of(List.of(), null), + // null priority list + Arguments.of(null, List.of()) + ); } static Object[][] LFilterTagsData() { @@ -392,19 +395,12 @@ public class LocaleMatchingTest { ranges, tags, expectedLocales, actualLocales)); } - @MethodSource("LFilterNPEData") + @MethodSource("LFilterLookupNPEData") @ParameterizedTest - void testLFilterNPE(String ranges, String tags, FilteringMode mode) { - if (ranges == null) { - // Ranges are null - assertThrows(NullPointerException.class, () -> LanguageRange.parse(ranges)); - } else { - // Tags are null - List priorityList = LanguageRange.parse(ranges); - List tagList = generateLocales(tags); - assertThrows(NullPointerException.class, - () -> showLocales(Locale.filter(priorityList, tagList, mode))); - } + void testLFilterNPE(List priorityList, List locList) { + assertThrows(NullPointerException.class, () -> Locale.filter(priorityList, locList)); + // Exercise 3-arg variant + assertThrows(NullPointerException.class, () -> Locale.filter(priorityList, locList, REJECT_EXTENDED_RANGES)); } @Test @@ -433,6 +429,14 @@ public class LocaleMatchingTest { ranges, tags, expectedTags, actualTags)); } + @MethodSource("LFilterLookupNPEData") + @ParameterizedTest + void testLFilterTagsNPE(List priorityList, List tagList) { + assertThrows(NullPointerException.class, () -> Locale.filterTags(priorityList, tagList)); + // Exercise 3-arg variant + assertThrows(NullPointerException.class, () -> Locale.filterTags(priorityList, tagList, REJECT_EXTENDED_RANGES)); + } + @Test void testLFilterTagsIAE() { String ranges = "de-*-DE"; @@ -454,6 +458,12 @@ public class LocaleMatchingTest { ranges, tags, expectedLocale, actualLocale)); } + @MethodSource("LFilterLookupNPEData") + @ParameterizedTest + void testLLookupNPE(List priorityList, List locList) { + assertThrows(NullPointerException.class, () -> Locale.lookup(priorityList, locList)); + } + @MethodSource("LLookupTagData") @ParameterizedTest void testLLookupTag(String ranges, String tags, String expectedTag) { @@ -464,6 +474,12 @@ public class LocaleMatchingTest { ranges, tags, expectedTag, actualTag)); } + @MethodSource("LFilterLookupNPEData") + @ParameterizedTest + void testLLookupTagsNPE(List priorityList, List tagList) { + assertThrows(NullPointerException.class, () -> Locale.lookupTag(priorityList, tagList)); + } + private static List generateLocales(String tags) { if (tags == null) { return null;