xref: /aosp_15_r20/external/cldr/tools/cldr-code/src/main/java/org/unicode/cldr/util/LocaleNormalizer.java (revision 912701f9769bb47905792267661f0baf2b85bed5)
1 package org.unicode.cldr.util;
2 
3 import java.util.Collection;
4 import java.util.Collections;
5 import java.util.Map;
6 import java.util.Set;
7 import java.util.TreeMap;
8 import java.util.stream.Collectors;
9 
10 /**
11  * Normalize and validate sets of locales. This class was split off from UserRegistry.java with the
12  * goal of encapsulation to support refactoring and implementation of new features such as warning a
13  * Manager who tries to assign to a Vetter unknown locales or locales that are not covered by their
14  * organization.
15  *
16  * <p>A single locale may be represented by a string like "fr_CA" for Canadian French, or by a
17  * CLDRLocale object.
18  *
19  * <p>A set of locales related to a particular Survey Tool user is compactly represented by a single
20  * string like "am fr_CA zh" (meaning "Amharic, Canadian French, and Chinese"). Survey Tool uses
21  * this compact representation for storage in the user database, and for browser inputting/editing
22  * forms, etc.
23  *
24  * <p>Otherwise the preferred representation is a LocaleSet, which encapsulates a Set<CLDRLocale>
25  * along with special handling for isAllLocales.
26  */
27 public class LocaleNormalizer {
28     public enum LocaleRejection {
29         outside_org_coverage("Outside org. coverage"),
30         unknown("Unknown");
31 
LocaleRejection(String message)32         LocaleRejection(String message) {
33             this.message = message;
34         }
35 
36         final String message;
37 
38         @Override
toString()39         public String toString() {
40             return message;
41         }
42     }
43 
44     /**
45      * Special constant for specifying access to no locales. Used with intlocs (not with locale
46      * access)
47      */
48     public static final String NO_LOCALES = "none";
49 
50     /** Special String constant for specifying access to all locales. */
51     public static final String ALL_LOCALES = StandardCodes.ALL_LOCALES;
52 
isAllLocales(String localeList)53     public static boolean isAllLocales(String localeList) {
54         return (localeList != null)
55                 && (localeList.contains(ALL_LOCALES) || localeList.trim().equals("all"));
56     }
57 
58     /** Special LocaleSet constant for specifying access to all locales. */
59     public static final LocaleSet ALL_LOCALES_SET = new LocaleSet(true);
60 
61     /**
62      * The actual set of locales used by CLDR. For Survey Tool, this may be set by SurveyMain during
63      * initialization. It is used for validation so it should not simply be ALL_LOCALES_SET.
64      */
65     private static LocaleSet knownLocales = null;
66 
setKnownLocales(Set<CLDRLocale> localeListSet)67     public static void setKnownLocales(Set<CLDRLocale> localeListSet) {
68         knownLocales = new LocaleSet();
69         knownLocales.addAll(localeListSet);
70     }
71 
72     /**
73      * Normalize the given locale-list string, removing invalid/duplicate locale names, and saving
74      * error/warning messages in this LocaleNormalizer object
75      *
76      * @param list the String like "zh aa test123"
77      * @return the normalized string like "aa zh"
78      */
normalize(String list)79     public String normalize(String list) {
80         return norm(this, list, null);
81     }
82 
83     /**
84      * Normalize the given locale-list string, removing invalid/duplicate locale names
85      *
86      * <p>Do not report any errors or warnings
87      *
88      * @param list the String like "zh aa test123"
89      * @return the normalized string like "aa zh"
90      */
normalizeQuietly(String list)91     public static String normalizeQuietly(String list) {
92         return norm(null, list, null);
93     }
94 
95     /**
96      * Normalize the given locale-list string, removing invalid/duplicate locale names, and saving
97      * error/warning messages in this LocaleNormalizer object
98      *
99      * @param list the String like "zh aa test123"
100      * @param orgLocaleSet the locales covered by a particular organization, used as a filter unless
101      *     null or ALL_LOCALES_SET
102      * @return the normalized string like "aa zh"
103      */
normalizeForSubset(String list, LocaleSet orgLocaleSet)104     public String normalizeForSubset(String list, LocaleSet orgLocaleSet) {
105         return norm(this, list, orgLocaleSet);
106     }
107 
108     /**
109      * Normalize the given locale-list string, removing invalid/duplicate locale names
110      *
111      * <p>Always filter out unknown locales. If orgLocaleSet isn't null, filter out locales missing
112      * from it.
113      *
114      * <p>This is static and has an optional LocaleNormalizer parameter that enables saving
115      * warning/error messages that can be shown to the user.
116      *
117      * @param locNorm the object to be filled in with warning/error messages, if not null
118      * @param list the String like "zh aa test123"
119      * @param orgLocaleSet the locales covered by a particular organization, used as a filter unless
120      *     null or ALL_LOCALES_SET
121      * @return the normalized string like "aa zh"
122      */
norm(LocaleNormalizer locNorm, String list, LocaleSet orgLocaleSet)123     private static String norm(LocaleNormalizer locNorm, String list, LocaleSet orgLocaleSet) {
124         if (list == null) {
125             return "";
126         }
127         list = list.trim();
128         if (list.isEmpty() || NO_LOCALES.equals(list)) {
129             return "";
130         }
131         if (isAllLocales(list)) {
132             return ALL_LOCALES;
133         }
134         final LocaleSet locSet = setFromString(locNorm, list, orgLocaleSet);
135         return locSet.toString();
136     }
137 
138     private Map<String, LocaleRejection> messages = null;
139 
addMessage(String locale, LocaleRejection rejection)140     private void addMessage(String locale, LocaleRejection rejection) {
141         if (messages == null) {
142             messages = new TreeMap<>();
143         }
144         messages.put(locale, rejection);
145     }
146 
hasMessage()147     public boolean hasMessage() {
148         return messages != null && !messages.isEmpty();
149     }
150 
getMessagePlain()151     public String getMessagePlain() {
152         return String.join("\n", getMessageArrayPlain());
153     }
154 
getMessageHtml()155     public String getMessageHtml() {
156         return String.join("<br />\n", getMessageArrayPlain());
157     }
158 
getMessageArrayPlain()159     public String[] getMessageArrayPlain() {
160         return getMessagesPlain().toArray(new String[0]);
161     }
162 
getMessagesPlain()163     public Collection<String> getMessagesPlain() {
164         return getMessages().entrySet().stream()
165                 .map(e -> (e.getValue() + ": " + e.getKey()))
166                 .collect(Collectors.toList());
167     }
168 
getMessages()169     public Map<String, LocaleRejection> getMessages() {
170         if (messages == null) return Collections.emptyMap();
171         return Collections.unmodifiableMap(messages);
172     }
173 
setFromStringQuietly(String locales, LocaleSet orgLocaleSet)174     public static LocaleSet setFromStringQuietly(String locales, LocaleSet orgLocaleSet) {
175         return setFromString(null, locales, orgLocaleSet);
176     }
177 
setFromString( LocaleNormalizer locNorm, String localeList, LocaleSet orgLocaleSet)178     private static LocaleSet setFromString(
179             LocaleNormalizer locNorm, String localeList, LocaleSet orgLocaleSet) {
180         if (isAllLocales(localeList)) {
181             if (orgLocaleSet == null || orgLocaleSet.isAllLocales()) {
182                 return ALL_LOCALES_SET;
183             }
184             return intersectKnownWithOrgLocales(orgLocaleSet);
185         }
186         final LocaleSet newSet = new LocaleSet();
187         if (localeList == null || (localeList = localeList.trim()).length() == 0) {
188             return newSet;
189         }
190         final String[] array = localeList.split("[, \t\u00a0\\s]+"); // whitespace
191         for (String s : array) {
192             CLDRLocale locale = CLDRLocale.getInstance(s);
193             if (knownLocales == null || knownLocales.contains(locale)) {
194                 if (orgLocaleSet == null || orgLocaleSet.containsLocaleOrParent(locale)) {
195                     newSet.add(locale);
196                 } else if (locNorm != null) {
197                     locNorm.addMessage(locale.getBaseName(), LocaleRejection.outside_org_coverage);
198                 }
199             } else if (locNorm != null) {
200                 locNorm.addMessage(locale.getBaseName(), LocaleRejection.unknown);
201             }
202         }
203         return newSet;
204     }
205 
intersectKnownWithOrgLocales(LocaleSet orgLocaleSet)206     private static LocaleSet intersectKnownWithOrgLocales(LocaleSet orgLocaleSet) {
207         if (knownLocales == null) {
208             final LocaleSet orgSetCopy = new LocaleSet();
209             orgSetCopy.addAll(orgLocaleSet.getSet());
210             return orgSetCopy;
211         }
212         final LocaleSet intersection = new LocaleSet();
213         for (CLDRLocale locale : knownLocales.getSet()) {
214             if (orgLocaleSet.containsLocaleOrParent(locale)) {
215                 intersection.add(locale);
216             }
217         }
218         return intersection;
219     }
220 }
221