/* * Copyright (c) 2010, 2011, 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 * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ /* ******************************************************************************* * Copyright (C) 2009-2010, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ package sun.util.locale; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; public final class InternalLocaleBuilder { private static final CaseInsensitiveChar PRIVATEUSE_KEY = new CaseInsensitiveChar(LanguageTag.PRIVATEUSE); private String language = ""; private String script = ""; private String region = ""; private String variant = ""; private Map extensions; private Set uattributes; private Map ukeywords; public InternalLocaleBuilder() { } public InternalLocaleBuilder setLanguage(String language) throws LocaleSyntaxException { if (LocaleUtils.isEmpty(language)) { this.language = ""; } else { if (!LanguageTag.isLanguage(language)) { throw new LocaleSyntaxException("Ill-formed language: " + language, 0); } this.language = language; } return this; } public InternalLocaleBuilder setScript(String script) throws LocaleSyntaxException { if (LocaleUtils.isEmpty(script)) { this.script = ""; } else { if (!LanguageTag.isScript(script)) { throw new LocaleSyntaxException("Ill-formed script: " + script, 0); } this.script = script; } return this; } public InternalLocaleBuilder setRegion(String region) throws LocaleSyntaxException { if (LocaleUtils.isEmpty(region)) { this.region = ""; } else { if (!LanguageTag.isRegion(region)) { throw new LocaleSyntaxException("Ill-formed region: " + region, 0); } this.region = region; } return this; } public InternalLocaleBuilder setVariant(String variant) throws LocaleSyntaxException { if (LocaleUtils.isEmpty(variant)) { this.variant = ""; } else { // normalize separators to "_" String var = variant.replaceAll(LanguageTag.SEP, BaseLocale.SEP); int errIdx = checkVariants(var, BaseLocale.SEP); if (errIdx != -1) { throw new LocaleSyntaxException("Ill-formed variant: " + variant, errIdx); } this.variant = var; } return this; } public InternalLocaleBuilder addUnicodeLocaleAttribute(String attribute) throws LocaleSyntaxException { if (!UnicodeLocaleExtension.isAttribute(attribute)) { throw new LocaleSyntaxException("Ill-formed Unicode locale attribute: " + attribute); } // Use case insensitive string to prevent duplication if (uattributes == null) { uattributes = new HashSet<>(4); } uattributes.add(new CaseInsensitiveString(attribute)); return this; } public InternalLocaleBuilder removeUnicodeLocaleAttribute(String attribute) throws LocaleSyntaxException { if (attribute == null || !UnicodeLocaleExtension.isAttribute(attribute)) { throw new LocaleSyntaxException("Ill-formed Unicode locale attribute: " + attribute); } if (uattributes != null) { uattributes.remove(new CaseInsensitiveString(attribute)); } return this; } public InternalLocaleBuilder setUnicodeLocaleKeyword(String key, String type) throws LocaleSyntaxException { if (!UnicodeLocaleExtension.isKey(key)) { throw new LocaleSyntaxException("Ill-formed Unicode locale keyword key: " + key); } CaseInsensitiveString cikey = new CaseInsensitiveString(key); if (type == null) { if (ukeywords != null) { // null type is used for remove the key ukeywords.remove(cikey); } } else { if (type.length() != 0) { // normalize separator to "-" String tp = type.replaceAll(BaseLocale.SEP, LanguageTag.SEP); // validate StringTokenIterator itr = new StringTokenIterator(tp, LanguageTag.SEP); while (!itr.isDone()) { String s = itr.current(); if (!UnicodeLocaleExtension.isTypeSubtag(s)) { throw new LocaleSyntaxException("Ill-formed Unicode locale keyword type: " + type, itr.currentStart()); } itr.next(); } } if (ukeywords == null) { ukeywords = new HashMap<>(4); } ukeywords.put(cikey, type); } return this; } public InternalLocaleBuilder setExtension(char singleton, String value) throws LocaleSyntaxException { // validate key boolean isBcpPrivateuse = LanguageTag.isPrivateusePrefixChar(singleton); if (!isBcpPrivateuse && !LanguageTag.isExtensionSingletonChar(singleton)) { throw new LocaleSyntaxException("Ill-formed extension key: " + singleton); } boolean remove = LocaleUtils.isEmpty(value); CaseInsensitiveChar key = new CaseInsensitiveChar(singleton); if (remove) { if (UnicodeLocaleExtension.isSingletonChar(key.value())) { // clear entire Unicode locale extension if (uattributes != null) { uattributes.clear(); } if (ukeywords != null) { ukeywords.clear(); } } else { if (extensions != null && extensions.containsKey(key)) { extensions.remove(key); } } } else { // validate value String val = value.replaceAll(BaseLocale.SEP, LanguageTag.SEP); StringTokenIterator itr = new StringTokenIterator(val, LanguageTag.SEP); while (!itr.isDone()) { String s = itr.current(); boolean validSubtag; if (isBcpPrivateuse) { validSubtag = LanguageTag.isPrivateuseSubtag(s); } else { validSubtag = LanguageTag.isExtensionSubtag(s); } if (!validSubtag) { throw new LocaleSyntaxException("Ill-formed extension value: " + s, itr.currentStart()); } itr.next(); } if (UnicodeLocaleExtension.isSingletonChar(key.value())) { setUnicodeLocaleExtension(val); } else { if (extensions == null) { extensions = new HashMap<>(4); } extensions.put(key, val); } } return this; } /* * Set extension/private subtags in a single string representation */ public InternalLocaleBuilder setExtensions(String subtags) throws LocaleSyntaxException { if (LocaleUtils.isEmpty(subtags)) { clearExtensions(); return this; } subtags = subtags.replaceAll(BaseLocale.SEP, LanguageTag.SEP); StringTokenIterator itr = new StringTokenIterator(subtags, LanguageTag.SEP); List extensions = null; String privateuse = null; int parsed = 0; int start; // Make a list of extension subtags while (!itr.isDone()) { String s = itr.current(); if (LanguageTag.isExtensionSingleton(s)) { start = itr.currentStart(); String singleton = s; StringBuilder sb = new StringBuilder(singleton); itr.next(); while (!itr.isDone()) { s = itr.current(); if (LanguageTag.isExtensionSubtag(s)) { sb.append(LanguageTag.SEP).append(s); parsed = itr.currentEnd(); } else { break; } itr.next(); } if (parsed < start) { throw new LocaleSyntaxException("Incomplete extension '" + singleton + "'", start); } if (extensions == null) { extensions = new ArrayList<>(4); } extensions.add(sb.toString()); } else { break; } } if (!itr.isDone()) { String s = itr.current(); if (LanguageTag.isPrivateusePrefix(s)) { start = itr.currentStart(); StringBuilder sb = new StringBuilder(s); itr.next(); while (!itr.isDone()) { s = itr.current(); if (!LanguageTag.isPrivateuseSubtag(s)) { break; } sb.append(LanguageTag.SEP).append(s); parsed = itr.currentEnd(); itr.next(); } if (parsed <= start) { throw new LocaleSyntaxException("Incomplete privateuse:" + subtags.substring(start), start); } else { privateuse = sb.toString(); } } } if (!itr.isDone()) { throw new LocaleSyntaxException("Ill-formed extension subtags:" + subtags.substring(itr.currentStart()), itr.currentStart()); } return setExtensions(extensions, privateuse); } /* * Set a list of BCP47 extensions and private use subtags * BCP47 extensions are already validated and well-formed, but may contain duplicates */ private InternalLocaleBuilder setExtensions(List bcpExtensions, String privateuse) { clearExtensions(); if (!LocaleUtils.isEmpty(bcpExtensions)) { Set done = new HashSet<>(bcpExtensions.size()); for (String bcpExt : bcpExtensions) { CaseInsensitiveChar key = new CaseInsensitiveChar(bcpExt); // ignore duplicates if (!done.contains(key)) { // each extension string contains singleton, e.g. "a-abc-def" if (UnicodeLocaleExtension.isSingletonChar(key.value())) { setUnicodeLocaleExtension(bcpExt.substring(2)); } else { if (extensions == null) { extensions = new HashMap<>(4); } extensions.put(key, bcpExt.substring(2)); } } done.add(key); } } if (privateuse != null && !privateuse.isEmpty()) { // privateuse string contains prefix, e.g. "x-abc-def" if (extensions == null) { extensions = new HashMap<>(1); } extensions.put(new CaseInsensitiveChar(privateuse), privateuse.substring(2)); } return this; } /* * Reset Builder's internal state with the given language tag */ public InternalLocaleBuilder setLanguageTag(LanguageTag langtag) { clear(); if (!langtag.getExtlangs().isEmpty()) { language = langtag.getExtlangs().get(0); } else { String lang = langtag.getLanguage(); if (!lang.equals(LanguageTag.UNDETERMINED)) { language = lang; } } script = langtag.getScript(); region = langtag.getRegion(); List bcpVariants = langtag.getVariants(); if (!bcpVariants.isEmpty()) { StringBuilder var = new StringBuilder(bcpVariants.get(0)); int size = bcpVariants.size(); for (int i = 1; i < size; i++) { var.append(BaseLocale.SEP).append(bcpVariants.get(i)); } variant = var.toString(); } setExtensions(langtag.getExtensions(), langtag.getPrivateuse()); return this; } public InternalLocaleBuilder setLocale(BaseLocale base, LocaleExtensions localeExtensions) throws LocaleSyntaxException { String language = base.getLanguage(); String script = base.getScript(); String region = base.getRegion(); String variant = base.getVariant(); // Special backward compatibility support // Exception 1 - ja_JP_JP if (language.equals("ja") && region.equals("JP") && variant.equals("JP")) { // When locale ja_JP_JP is created, ca-japanese is always there. // The builder ignores the variant "JP" assert("japanese".equals(localeExtensions.getUnicodeLocaleType("ca"))); variant = ""; } // Exception 2 - th_TH_TH else if (language.equals("th") && region.equals("TH") && variant.equals("TH")) { // When locale th_TH_TH is created, nu-thai is always there. // The builder ignores the variant "TH" assert("thai".equals(localeExtensions.getUnicodeLocaleType("nu"))); variant = ""; } // Exception 3 - no_NO_NY else if (language.equals("no") && region.equals("NO") && variant.equals("NY")) { // no_NO_NY is a valid locale and used by Java 6 or older versions. // The build ignores the variant "NY" and change the language to "nn". language = "nn"; variant = ""; } // Validate base locale fields before updating internal state. // LocaleExtensions always store validated/canonicalized values, // so no checks are necessary. if (!language.isEmpty() && !LanguageTag.isLanguage(language)) { throw new LocaleSyntaxException("Ill-formed language: " + language); } if (!script.isEmpty() && !LanguageTag.isScript(script)) { throw new LocaleSyntaxException("Ill-formed script: " + script); } if (!region.isEmpty() && !LanguageTag.isRegion(region)) { throw new LocaleSyntaxException("Ill-formed region: " + region); } if (!variant.isEmpty()) { int errIdx = checkVariants(variant, BaseLocale.SEP); if (errIdx != -1) { throw new LocaleSyntaxException("Ill-formed variant: " + variant, errIdx); } } // The input locale is validated at this point. // Now, updating builder's internal fields. this.language = language; this.script = script; this.region = region; this.variant = variant; clearExtensions(); Set extKeys = (localeExtensions == null) ? null : localeExtensions.getKeys(); if (extKeys != null) { // map localeExtensions back to builder's internal format for (Character key : extKeys) { Extension e = localeExtensions.getExtension(key); if (e instanceof UnicodeLocaleExtension) { UnicodeLocaleExtension ue = (UnicodeLocaleExtension)e; for (String uatr : ue.getUnicodeLocaleAttributes()) { if (uattributes == null) { uattributes = new HashSet<>(4); } uattributes.add(new CaseInsensitiveString(uatr)); } for (String ukey : ue.getUnicodeLocaleKeys()) { if (ukeywords == null) { ukeywords = new HashMap<>(4); } ukeywords.put(new CaseInsensitiveString(ukey), ue.getUnicodeLocaleType(ukey)); } } else { if (extensions == null) { extensions = new HashMap<>(4); } extensions.put(new CaseInsensitiveChar(key), e.getValue()); } } } return this; } public InternalLocaleBuilder clear() { language = ""; script = ""; region = ""; variant = ""; clearExtensions(); return this; } public InternalLocaleBuilder clearExtensions() { if (extensions != null) { extensions.clear(); } if (uattributes != null) { uattributes.clear(); } if (ukeywords != null) { ukeywords.clear(); } return this; } public BaseLocale getBaseLocale() { String language = this.language; String script = this.script; String region = this.region; String variant = this.variant; // Special private use subtag sequence identified by "lvariant" will be // interpreted as Java variant. if (extensions != null) { String privuse = extensions.get(PRIVATEUSE_KEY); if (privuse != null) { StringTokenIterator itr = new StringTokenIterator(privuse, LanguageTag.SEP); boolean sawPrefix = false; int privVarStart = -1; while (!itr.isDone()) { if (sawPrefix) { privVarStart = itr.currentStart(); break; } if (LocaleUtils.caseIgnoreMatch(itr.current(), LanguageTag.PRIVUSE_VARIANT_PREFIX)) { sawPrefix = true; } itr.next(); } if (privVarStart != -1) { StringBuilder sb = new StringBuilder(variant); if (sb.length() != 0) { sb.append(BaseLocale.SEP); } sb.append(privuse.substring(privVarStart).replaceAll(LanguageTag.SEP, BaseLocale.SEP)); variant = sb.toString(); } } } return BaseLocale.getInstance(language, script, region, variant); } public LocaleExtensions getLocaleExtensions() { if (LocaleUtils.isEmpty(extensions) && LocaleUtils.isEmpty(uattributes) && LocaleUtils.isEmpty(ukeywords)) { return null; } LocaleExtensions lext = new LocaleExtensions(extensions, uattributes, ukeywords); return lext.isEmpty() ? null : lext; } /* * Remove special private use subtag sequence identified by "lvariant" * and return the rest. Only used by LocaleExtensions */ static String removePrivateuseVariant(String privuseVal) { StringTokenIterator itr = new StringTokenIterator(privuseVal, LanguageTag.SEP); // Note: privateuse value "abc-lvariant" is unchanged // because no subtags after "lvariant". int prefixStart = -1; boolean sawPrivuseVar = false; while (!itr.isDone()) { if (prefixStart != -1) { // Note: privateuse value "abc-lvariant" is unchanged // because no subtags after "lvariant". sawPrivuseVar = true; break; } if (LocaleUtils.caseIgnoreMatch(itr.current(), LanguageTag.PRIVUSE_VARIANT_PREFIX)) { prefixStart = itr.currentStart(); } itr.next(); } if (!sawPrivuseVar) { return privuseVal; } assert(prefixStart == 0 || prefixStart > 1); return (prefixStart == 0) ? null : privuseVal.substring(0, prefixStart -1); } /* * Check if the given variant subtags separated by the given * separator(s) are valid */ private int checkVariants(String variants, String sep) { StringTokenIterator itr = new StringTokenIterator(variants, sep); while (!itr.isDone()) { String s = itr.current(); if (!LanguageTag.isVariant(s)) { return itr.currentStart(); } itr.next(); } return -1; } /* * Private methods parsing Unicode Locale Extension subtags. * Duplicated attributes/keywords will be ignored. * The input must be a valid extension subtags (excluding singleton). */ private void setUnicodeLocaleExtension(String subtags) { // wipe out existing attributes/keywords if (uattributes != null) { uattributes.clear(); } if (ukeywords != null) { ukeywords.clear(); } StringTokenIterator itr = new StringTokenIterator(subtags, LanguageTag.SEP); // parse attributes while (!itr.isDone()) { if (!UnicodeLocaleExtension.isAttribute(itr.current())) { break; } if (uattributes == null) { uattributes = new HashSet<>(4); } uattributes.add(new CaseInsensitiveString(itr.current())); itr.next(); } // parse keywords CaseInsensitiveString key = null; String type; int typeStart = -1; int typeEnd = -1; while (!itr.isDone()) { if (key != null) { if (UnicodeLocaleExtension.isKey(itr.current())) { // next keyword - emit previous one assert(typeStart == -1 || typeEnd != -1); type = (typeStart == -1) ? "" : subtags.substring(typeStart, typeEnd); if (ukeywords == null) { ukeywords = new HashMap<>(4); } ukeywords.put(key, type); // reset keyword info CaseInsensitiveString tmpKey = new CaseInsensitiveString(itr.current()); key = ukeywords.containsKey(tmpKey) ? null : tmpKey; typeStart = typeEnd = -1; } else { if (typeStart == -1) { typeStart = itr.currentStart(); } typeEnd = itr.currentEnd(); } } else if (UnicodeLocaleExtension.isKey(itr.current())) { // 1. first keyword or // 2. next keyword, but previous one was duplicate key = new CaseInsensitiveString(itr.current()); if (ukeywords != null && ukeywords.containsKey(key)) { // duplicate key = null; } } if (!itr.hasNext()) { if (key != null) { // last keyword assert(typeStart == -1 || typeEnd != -1); type = (typeStart == -1) ? "" : subtags.substring(typeStart, typeEnd); if (ukeywords == null) { ukeywords = new HashMap<>(4); } ukeywords.put(key, type); } break; } itr.next(); } } static final class CaseInsensitiveString { private final String str, lowerStr; CaseInsensitiveString(String s) { str = s; lowerStr = LocaleUtils.toLowerString(s); } public String value() { return str; } @Override public int hashCode() { return lowerStr.hashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof CaseInsensitiveString)) { return false; } return lowerStr.equals(((CaseInsensitiveString)obj).lowerStr); } } static final class CaseInsensitiveChar { private final char ch, lowerCh; /** * Constructs a CaseInsensitiveChar with the first char of the * given s. */ private CaseInsensitiveChar(String s) { this(s.charAt(0)); } CaseInsensitiveChar(char c) { ch = c; lowerCh = LocaleUtils.toLower(ch); } public char value() { return ch; } @Override public int hashCode() { return lowerCh; } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (!(obj instanceof CaseInsensitiveChar)) { return false; } return lowerCh == ((CaseInsensitiveChar)obj).lowerCh; } } }