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