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