blob: 1abf71429359aa3a3818ea54dbf9ff5298f06ea6 [file] [log] [blame]
/*
**********************************************************************
* Copyright (c) 2002-2019, International Business Machines
* Corporation and others. All Rights Reserved.
**********************************************************************
* Author: Mark Davis
**********************************************************************
*/
package org.unicode.cldr.util;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMap.Builder;
import com.google.common.collect.ImmutableSet;
import com.google.common.util.concurrent.UncheckedExecutionException;
import com.ibm.icu.impl.Relation;
import com.ibm.icu.impl.Row;
import com.ibm.icu.impl.Row.R2;
import com.ibm.icu.impl.Utility;
import com.ibm.icu.text.MessageFormat;
import com.ibm.icu.text.PluralRules;
import com.ibm.icu.text.SimpleDateFormat;
import com.ibm.icu.text.Transform;
import com.ibm.icu.text.UnicodeSet;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.Freezable;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.Output;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.VersionInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FilenameFilter;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import org.unicode.cldr.test.CheckMetazones;
import org.unicode.cldr.util.DayPeriodInfo.DayPeriod;
import org.unicode.cldr.util.GrammarInfo.GrammaticalFeature;
import org.unicode.cldr.util.GrammarInfo.GrammaticalScope;
import org.unicode.cldr.util.GrammarInfo.GrammaticalTarget;
import org.unicode.cldr.util.LocaleInheritanceInfo.Reason;
import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo;
import org.unicode.cldr.util.SupplementalDataInfo.PluralInfo.Count;
import org.unicode.cldr.util.SupplementalDataInfo.PluralType;
import org.unicode.cldr.util.With.SimpleIterator;
import org.unicode.cldr.util.XMLFileReader.AllHandler;
import org.unicode.cldr.util.XMLSource.ResolvingSource;
import org.unicode.cldr.util.XPathParts.Comments;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.XMLReaderFactory;
/**
* This is a class that represents the contents of a CLDR file, as <key,value> pairs, where the key
* is a "cleaned" xpath (with non-distinguishing attributes removed), and the value is an object
* that contains the full xpath plus a value, which is a string, or a node (the latter for atomic
* elements).
*
* <p><b>WARNING: The API on this class is likely to change.</b> Having the full xpath on the value
* is clumsy; I need to change it to having the key be an object that contains the full xpath, but
* then sorts as if it were clean.
*
* <p>Each instance also contains a set of associated comments for each xpath.
*
* @author medavis
*/
/*
* Notes:
* http://xml.apache.org/xerces2-j/faq-grammars.html#faq-3
* http://developers.sun.com/dev/coolstuff/xml/readme.html
* http://lists.xml.org/archives/xml-dev/200007/msg00284.html
* http://java.sun.com/j2se/1.4.2/docs/api/org/xml/sax/DTDHandler.html
*/
public class CLDRFile implements Freezable<CLDRFile>, Iterable<String>, LocaleStringProvider {
private static final String GETNAME_LOCALE_SEPARATOR =
"//ldml/localeDisplayNames/localeDisplayPattern/localeSeparator";
private static final String GETNAME_LOCALE_PATTERN =
"//ldml/localeDisplayNames/localeDisplayPattern/localePattern";
private static final String GETNAME_LOCALE_KEY_TYPE_PATTERN =
"//ldml/localeDisplayNames/localeDisplayPattern/localeKeyTypePattern";
private static final ImmutableSet<String> casesNominativeOnly =
ImmutableSet.of(GrammaticalFeature.grammaticalCase.getDefault(null));
/**
* Variable to control whether File reads are buffered; this will about halve the time spent in
* loadFromFile() and Factory.make() from about 20 % to about 10 %. It will also noticeably
* improve the different unit tests take in the TestAll fixture. TRUE - use buffering (default)
* FALSE - do not use buffering
*/
private static final boolean USE_LOADING_BUFFER = true;
private static final boolean DEBUG = false;
public static final Pattern ALT_PROPOSED_PATTERN =
PatternCache.get(".*\\[@alt=\"[^\"]*proposed[^\"]*\"].*");
public static final Pattern DRAFT_PATTERN = PatternCache.get("\\[@draft=\"([^\"]*)\"\\]");
public static final Pattern XML_SPACE_PATTERN =
PatternCache.get("\\[@xml:space=\"([^\"]*)\"\\]");
private static boolean LOG_PROGRESS = false;
public static boolean HACK_ORDER = false;
private static boolean DEBUG_LOGGING = false;
public static final String SUPPLEMENTAL_NAME = "supplementalData";
public static final String SUPPLEMENTAL_METADATA = "supplementalMetadata";
public static final String SUPPLEMENTAL_PREFIX = "supplemental";
public static final String GEN_VERSION = "45";
public static final List<String> SUPPLEMENTAL_NAMES =
Arrays.asList(
"characters",
"coverageLevels",
"dayPeriods",
"genderList",
"grammaticalFeatures",
"languageInfo",
"languageGroup",
"likelySubtags",
"metaZones",
"numberingSystems",
"ordinals",
"pluralRanges",
"plurals",
"postalCodeData",
"rgScope",
"supplementalData",
"supplementalMetadata",
"telephoneCodeData",
"units",
"windowsZones");
private Set<String> extraPaths = null;
private boolean locked;
private DtdType dtdType;
private DtdData dtdData;
XMLSource dataSource; // TODO(jchye): make private
private File supplementalDirectory;
/**
* Does the value in question either match or inherent the current value?
*
* <p>To match, the value in question and the current value must be non-null and equal.
*
* <p>To inherit the current value, the value in question must be INHERITANCE_MARKER and the
* current value must equal the bailey value.
*
* <p>This CLDRFile is only used here for getBaileyValue, not to get curValue
*
* @param value the value in question
* @param curValue the current value, that is, XMLSource.getValueAtDPath(xpathString)
* @param xpathString the path identifier
* @return true if it matches or inherits, else false
*/
public boolean equalsOrInheritsCurrentValue(String value, String curValue, String xpathString) {
if (value == null || curValue == null) {
return false;
}
if (value.equals(curValue)) {
return true;
}
if (value.equals(CldrUtility.INHERITANCE_MARKER)) {
String baileyValue = getBaileyValue(xpathString, null, null);
if (baileyValue == null) {
/* This may happen for Invalid XPath; InvalidXPathException may be thrown. */
return false;
}
if (curValue.equals(baileyValue)) {
return true;
}
}
return false;
}
public XMLSource getResolvingDataSource() {
if (!isResolved()) {
throw new IllegalArgumentException(
"CLDRFile must be resolved for getResolvingDataSource");
}
// dataSource instanceof XMLSource.ResolvingSource
return dataSource;
}
public enum DraftStatus {
unconfirmed,
provisional,
contributed,
approved;
public static DraftStatus forString(String string) {
return string == null
? DraftStatus.approved
: DraftStatus.valueOf(string.toLowerCase(Locale.ENGLISH));
}
/**
* Get the draft status from a full xpath
*
* @param xpath
* @return
*/
public static DraftStatus forXpath(String xpath) {
final String status =
XPathParts.getFrozenInstance(xpath).getAttributeValue(-1, "draft");
return forString(status);
}
/** Return the XPath suffix for this draft status or "" for approved. */
public String asXpath() {
if (this == approved) {
return "";
} else {
return "[@draft=\"" + name() + "\"]";
}
}
/** update this XPath with this draft status */
public String updateXPath(final String fullXpath) {
final XPathParts xpp = XPathParts.getFrozenInstance(fullXpath).cloneAsThawed();
final String oldDraft = xpp.getAttributeValue(-1, "draft");
if (forString(oldDraft) == this) {
return fullXpath; // no change;
}
if (this == approved) {
xpp.removeAttribute(-1, "draft");
} else {
xpp.setAttribute(-1, "draft", this.name());
}
return xpp.toString();
}
}
@Override
public String toString() {
return "{"
+ "locked="
+ locked
+ " locale="
+ dataSource.getLocaleID()
+ " dataSource="
+ dataSource.toString()
+ "}";
}
public String toString(String regex) {
return "{"
+ "locked="
+ locked
+ " locale="
+ dataSource.getLocaleID()
+ " regex="
+ regex
+ " dataSource="
+ dataSource.toString(regex)
+ "}";
}
// for refactoring
public CLDRFile setNonInheriting(boolean isSupplemental) {
if (locked) {
throw new UnsupportedOperationException("Attempt to modify locked object");
}
dataSource.setNonInheriting(isSupplemental);
return this;
}
public boolean isNonInheriting() {
return dataSource.isNonInheriting();
}
private static final boolean DEBUG_CLDR_FILE = false;
private String creationTime = null; // only used if DEBUG_CLDR_FILE
/**
* Construct a new CLDRFile.
*
* @param dataSource must not be null
*/
public CLDRFile(XMLSource dataSource) {
this.dataSource = dataSource;
if (DEBUG_CLDR_FILE) {
creationTime =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
.format(Calendar.getInstance().getTime());
System.out.println("📂 Created new CLDRFile(dataSource) at " + creationTime);
}
}
/**
* get Unresolved CLDRFile
*
* @param localeId
* @param dirs
* @param minimalDraftStatus
*/
public CLDRFile(String localeId, List<File> dirs, DraftStatus minimalDraftStatus) {
// order matters
this.dataSource = XMLSource.getFrozenInstance(localeId, dirs, minimalDraftStatus);
this.dtdType = dataSource.getXMLNormalizingDtdType();
this.dtdData = DtdData.getInstance(this.dtdType);
}
public CLDRFile(XMLSource dataSource, XMLSource... resolvingParents) {
List<XMLSource> sourceList = new ArrayList<>();
sourceList.add(dataSource);
sourceList.addAll(Arrays.asList(resolvingParents));
this.dataSource = new ResolvingSource(sourceList);
if (DEBUG_CLDR_FILE) {
creationTime =
new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'")
.format(Calendar.getInstance().getTime());
System.out.println(
"📂 Created new CLDRFile(dataSource, XMLSource... resolvingParents) at "
+ creationTime);
}
}
public static CLDRFile loadFromFile(
File f, String localeName, DraftStatus minimalDraftStatus, XMLSource source) {
String fullFileName = f.getAbsolutePath();
try {
fullFileName = PathUtilities.getNormalizedPathString(f);
if (DEBUG_LOGGING) {
System.out.println("Parsing: " + fullFileName);
Log.logln(LOG_PROGRESS, "Parsing: " + fullFileName);
}
final CLDRFile cldrFile;
if (USE_LOADING_BUFFER) {
// Use Buffering - improves performance at little cost to memory footprint
// try (InputStream fis = new BufferedInputStream(new FileInputStream(f),32000);) {
try (InputStream fis = InputStreamFactory.createInputStream(f)) {
cldrFile = load(fullFileName, localeName, fis, minimalDraftStatus, source);
return cldrFile;
}
} else {
// previous version - do not use buffering
try (InputStream fis = new FileInputStream(f); ) {
cldrFile = load(fullFileName, localeName, fis, minimalDraftStatus, source);
return cldrFile;
}
}
} catch (Exception e) {
// use a StringBuilder to construct the message.
StringBuilder sb = new StringBuilder("Cannot read the file '");
sb.append(fullFileName);
sb.append("': ");
sb.append(e.getMessage());
throw new ICUUncheckedIOException(sb.toString(), e);
}
}
public static CLDRFile loadFromFiles(
List<File> dirs, String localeName, DraftStatus minimalDraftStatus, XMLSource source) {
try {
if (DEBUG_LOGGING) {
System.out.println("Parsing: " + dirs);
Log.logln(LOG_PROGRESS, "Parsing: " + dirs);
}
if (USE_LOADING_BUFFER) {
// Use Buffering - improves performance at little cost to memory footprint
// try (InputStream fis = new BufferedInputStream(new FileInputStream(f),32000);) {
CLDRFile cldrFile = new CLDRFile(source);
for (File dir : dirs) {
File f = new File(dir, localeName + ".xml");
try (InputStream fis = InputStreamFactory.createInputStream(f)) {
cldrFile.loadFromInputStream(
PathUtilities.getNormalizedPathString(f),
localeName,
fis,
minimalDraftStatus,
false);
}
}
return cldrFile;
} else {
throw new IllegalArgumentException("Must use USE_LOADING_BUFFER");
}
} catch (Exception e) {
// e.printStackTrace();
// use a StringBuilder to construct the message.
StringBuilder sb = new StringBuilder("Cannot read the file '");
sb.append(dirs);
throw new ICUUncheckedIOException(sb.toString(), e);
}
}
/**
* Produce a CLDRFile from a localeName, given a directory. (Normally a Factory is used to
* create CLDRFiles.)
*
* @param f
* @param localeName
* @param minimalDraftStatus
*/
public static CLDRFile loadFromFile(File f, String localeName, DraftStatus minimalDraftStatus) {
return loadFromFile(f, localeName, minimalDraftStatus, new SimpleXMLSource(localeName));
}
public static CLDRFile loadFromFiles(
List<File> dirs, String localeName, DraftStatus minimalDraftStatus) {
return loadFromFiles(dirs, localeName, minimalDraftStatus, new SimpleXMLSource(localeName));
}
static CLDRFile load(
String fileName, String localeName, InputStream fis, DraftStatus minimalDraftStatus) {
return load(fileName, localeName, fis, minimalDraftStatus, new SimpleXMLSource(localeName));
}
/**
* Load a CLDRFile from a file input stream.
*
* @param localeName
* @param fis
*/
private static CLDRFile load(
String fileName,
String localeName,
InputStream fis,
DraftStatus minimalDraftStatus,
XMLSource source) {
CLDRFile cldrFile = new CLDRFile(source);
return cldrFile.loadFromInputStream(fileName, localeName, fis, minimalDraftStatus, false);
}
/**
* Load a CLDRFile from a file input stream.
*
* @param localeName
* @param fis
*/
private static CLDRFile load(
String fileName,
String localeName,
InputStream fis,
DraftStatus minimalDraftStatus,
XMLSource source,
boolean leniency) {
CLDRFile cldrFile = new CLDRFile(source);
return cldrFile.loadFromInputStream(
fileName, localeName, fis, minimalDraftStatus, leniency);
}
static CLDRFile load(
String fileName,
String localeName,
InputStream fis,
DraftStatus minimalDraftStatus,
boolean leniency) {
return load(
fileName,
localeName,
fis,
minimalDraftStatus,
new SimpleXMLSource(localeName),
leniency);
}
/**
* Low-level function, only normally used for testing.
*
* @param fileName
* @param localeName
* @param fis
* @param minimalDraftStatus
* @param leniency if true, skip dtd validation
* @return
*/
public CLDRFile loadFromInputStream(
String fileName,
String localeName,
InputStream fis,
DraftStatus minimalDraftStatus,
boolean leniency) {
CLDRFile cldrFile = this;
MyDeclHandler DEFAULT_DECLHANDLER = new MyDeclHandler(cldrFile, minimalDraftStatus);
XMLFileReader.read(fileName, fis, -1, !leniency, DEFAULT_DECLHANDLER);
if (DEFAULT_DECLHANDLER.isSupplemental < 0) {
throw new IllegalArgumentException(
"root of file must be either ldml or supplementalData");
}
cldrFile.setNonInheriting(DEFAULT_DECLHANDLER.isSupplemental > 0);
if (DEFAULT_DECLHANDLER.overrideCount > 0) {
throw new IllegalArgumentException(
"Internal problems: either data file has duplicate path, or"
+ " CLDRFile.isDistinguishing() or CLDRFile.isOrdered() need updating: "
+ DEFAULT_DECLHANDLER.overrideCount
+ "; The exact problems are printed on the console above.");
}
if (localeName == null) {
cldrFile.dataSource.setLocaleID(cldrFile.getLocaleIDFromIdentity());
}
return cldrFile;
}
/**
* Clone the object. Produces unlocked version
*
* @see com.ibm.icu.util.Freezable
*/
@Override
public CLDRFile cloneAsThawed() {
try {
CLDRFile result = (CLDRFile) super.clone();
result.locked = false;
result.dataSource = result.dataSource.cloneAsThawed();
return result;
} catch (CloneNotSupportedException e) {
throw new InternalError("should never happen");
}
}
/** Prints the contents of the file (the xpaths/values) to the console. */
public CLDRFile show() {
for (Iterator<String> it2 = iterator(); it2.hasNext(); ) {
String xpath = it2.next();
System.out.println(getFullXPath(xpath) + " =>\t" + getStringValue(xpath));
}
return this;
}
private static final Map<String, Object> nullOptions =
Collections.unmodifiableMap(new TreeMap<String, Object>());
/**
* Write the corresponding XML file out, with the normal formatting and indentation. Will update
* the identity element, including version, and other items. If the CLDRFile is empty, the DTD
* type will be //ldml.
*/
public void write(PrintWriter pw) {
write(pw, nullOptions);
}
/**
* Write the corresponding XML file out, with the normal formatting and indentation. Will update
* the identity element, including version, and other items. If the CLDRFile is empty, the DTD
* type will be //ldml.
*
* @param pw writer to print to
* @param options map of options for writing
* @return true if we write the file, false if we cancel due to skipping all paths
*/
public boolean write(PrintWriter pw, Map<String, ?> options) {
final CldrXmlWriter xmlWriter = new CldrXmlWriter(this, pw, options);
xmlWriter.write();
return true;
}
/** Get a string value from an xpath. */
@Override
public String getStringValue(String xpath) {
try {
String result = dataSource.getValueAtPath(xpath);
if (result == null && dataSource.isResolving()) {
final String fallbackPath = getFallbackPath(xpath, false, true);
// often fallbackPath equals xpath -- in such cases, isn't it a waste of time to
// call getValueAtPath again?
if (fallbackPath != null) {
result = dataSource.getValueAtPath(fallbackPath);
}
}
if (isResolved()
&& GlossonymConstructor.valueIsBogus(result)
&& GlossonymConstructor.pathIsEligible(xpath)) {
final String constructedValue = new GlossonymConstructor(this).getValue(xpath);
if (constructedValue != null) {
result = constructedValue;
}
}
return result;
} catch (Exception e) {
throw new UncheckedExecutionException("Bad path: " + xpath, e);
}
}
/**
* Get GeorgeBailey value: that is, what the value would be if it were not directly contained in
* the file at that path. If the value is null or INHERITANCE_MARKER (with resolving), then
* baileyValue = resolved value. A non-resolving CLDRFile will always return null.
*/
public String getBaileyValue(
String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound) {
String result = dataSource.getBaileyValue(xpath, pathWhereFound, localeWhereFound);
if ((result == null || result.equals(CldrUtility.INHERITANCE_MARKER))
&& dataSource.isResolving()) {
final String fallbackPath =
getFallbackPath(
xpath, false,
false); // return null if there is no different sideways path
if (xpath.equals(fallbackPath)) {
getFallbackPath(xpath, false, true);
throw new IllegalArgumentException(); // should never happen
}
if (fallbackPath != null) {
result = dataSource.getValueAtPath(fallbackPath);
if (result != null) {
Status status = new Status();
if (localeWhereFound != null) {
localeWhereFound.value = dataSource.getSourceLocaleID(fallbackPath, status);
}
if (pathWhereFound != null) {
pathWhereFound.value = status.pathWhereFound;
}
}
}
}
if (isResolved()
&& GlossonymConstructor.valueIsBogus(result)
&& GlossonymConstructor.pathIsEligible(xpath)) {
final GlossonymConstructor gc = new GlossonymConstructor(this);
final String constructedValue =
gc.getValueAndTrack(xpath, pathWhereFound, localeWhereFound);
if (constructedValue != null) {
result = constructedValue;
}
}
return result;
}
/**
* Return a list of all paths which contributed to the value, as well as all bailey values. This
* is used to explain inheritance and bailey values. The list must be interpreted in order. When
* {@link LocaleInheritanceInfo.Reason#isTerminal()} return true, that indicates a successful
* lookup and partitions values from subsequent bailey values.
*
* @see #getBaileyValue(String, Output, Output)
* @see #getSourceLocaleIdExtended(String, Status, boolean)
*/
public List<LocaleInheritanceInfo> getPathsWhereFound(String xpath) {
if (!isResolved()) {
throw new IllegalArgumentException(
"getPathsWhereFound() is only valid on a resolved CLDRFile");
}
LinkedList<LocaleInheritanceInfo> list = new LinkedList<>();
// first, call getSourceLocaleIdExtended to populate the list
Status status = new Status();
getSourceLocaleIdExtended(xpath, status, false, list);
final String path1 = status.pathWhereFound;
// For now, the only special case is Glossonym
if (path1.equals(GlossonymConstructor.PSEUDO_PATH)) {
// it's a Glossonym, so as the GlossonymConstructor what the paths are. Sort paths in
// reverse order.
final Set<String> xpaths =
new GlossonymConstructor(this)
.getPathsWhereFound(
xpath, new TreeSet<String>(Comparator.reverseOrder()));
for (final String subpath : xpaths) {
final String locale2 = getSourceLocaleIdExtended(subpath, status, true);
final String path2 = status.pathWhereFound;
// Paths are in reverse order (c-b-a) so we insert them at the top of our list.
list.addFirst(new LocaleInheritanceInfo(locale2, path2, Reason.constructed));
}
// now the list contains:
// constructed: a
// constructed: b
// constructed: c
// (none) - this is where the glossonym was
// (bailey value(s))
}
return list;
}
static final class SimpleAltPicker implements Transform<String, String> {
public final String alt;
public SimpleAltPicker(String alt) {
this.alt = alt;
}
@Override
public String transform(@SuppressWarnings("unused") String source) {
return alt;
}
}
/**
* Only call if xpath doesn't exist in the current file.
*
* <p>For now, just handle counts and cases: see getCountPath Also handle extraPaths
*
* @param xpath
* @param winning TODO
* @param checkExtraPaths TODO
* @return
*/
private String getFallbackPath(String xpath, boolean winning, boolean checkExtraPaths) {
if (GrammaticalFeature.pathHasFeature(xpath) != null) {
return getCountPathWithFallback(xpath, Count.other, winning);
}
if (checkExtraPaths && getRawExtraPaths().contains(xpath)) {
return xpath;
}
return null;
}
/**
* Get the full path from a distinguished path.
*
* @param xpath the distinguished path
* @return the full path
* <p>Examples:
* <p>xpath = //ldml/localeDisplayNames/scripts/script[@type="Adlm"] result =
* //ldml/localeDisplayNames/scripts/script[@type="Adlm"][@draft="unconfirmed"]
* <p>xpath =
* //ldml/dates/calendars/calendar[@type="hebrew"]/dateFormats/dateFormatLength[@type="full"]/dateFormat[@type="standard"]/pattern[@type="standard"]
* result =
* //ldml/dates/calendars/calendar[@type="hebrew"]/dateFormats/dateFormatLength[@type="full"]/dateFormat[@type="standard"]/pattern[@type="standard"][@numbers="hebr"]
*/
public String getFullXPath(String xpath) {
if (xpath == null) {
throw new NullPointerException("Null distinguishing xpath");
}
String result = dataSource.getFullPath(xpath);
return result != null
? result
: xpath; // we can't add any non-distinguishing values if there is nothing there.
// if (result == null && dataSource.isResolving()) {
// String fallback = getFallbackPath(xpath, true);
// if (fallback != null) {
// // TODO, add attributes from fallback into main
// result = xpath;
// }
// }
// return result;
}
/**
* Get the last modified date (if available) from a distinguished path.
*
* @return date or null if not available.
*/
public Date getLastModifiedDate(String xpath) {
return dataSource.getChangeDateAtDPath(xpath);
}
/**
* Find out where the value was found (for resolving locales). Returns {@link
* XMLSource#CODE_FALLBACK_ID} as the location if nothing is found
*
* @param distinguishedXPath path (must be distinguished!)
* @param status the distinguished path where the item was found. Pass in null if you don't
* care.
*/
@Override
public String getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status) {
return getSourceLocaleIdExtended(
distinguishedXPath, status, true /* skipInheritanceMarker */);
}
/**
* Find out where the value was found (for resolving locales). Returns {@link
* XMLSource#CODE_FALLBACK_ID} as the location if nothing is found
*
* @param distinguishedXPath path (must be distinguished!)
* @param status the distinguished path where the item was found. Pass in null if you don't
* care.
* @param skipInheritanceMarker if true, skip sources in which value is INHERITANCE_MARKER
* @return the locale id as a string
*/
public String getSourceLocaleIdExtended(
String distinguishedXPath, CLDRFile.Status status, boolean skipInheritanceMarker) {
return getSourceLocaleIdExtended(distinguishedXPath, status, skipInheritanceMarker, null);
}
public String getSourceLocaleIdExtended(
String distinguishedXPath,
CLDRFile.Status status,
boolean skipInheritanceMarker,
List<LocaleInheritanceInfo> list) {
String result =
dataSource.getSourceLocaleIdExtended(
distinguishedXPath, status, skipInheritanceMarker, list);
if (result == XMLSource.CODE_FALLBACK_ID && dataSource.isResolving()) {
final String fallbackPath = getFallbackPath(distinguishedXPath, false, true);
if (fallbackPath != null && !fallbackPath.equals(distinguishedXPath)) {
if (list != null) {
list.add(
new LocaleInheritanceInfo(
getLocaleID(), distinguishedXPath, Reason.fallback, null));
}
result =
dataSource.getSourceLocaleIdExtended(
fallbackPath, status, skipInheritanceMarker, list);
}
if (result == XMLSource.CODE_FALLBACK_ID
&& getConstructedValue(distinguishedXPath) != null) {
if (status != null) {
status.pathWhereFound = GlossonymConstructor.PSEUDO_PATH;
}
return getLocaleID();
}
}
return result;
}
/**
* return true if the path in this file (without resolution)
*
* @param path
* @return
*/
public boolean isHere(String path) {
return dataSource.isHere(path);
}
/**
* Add a new element to a CLDRFile.
*
* @param currentFullXPath
* @param value
*/
public CLDRFile add(String currentFullXPath, String value) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
// StringValue v = new StringValue(value, currentFullXPath);
Log.logln(
LOG_PROGRESS,
"ADDING: \t" + currentFullXPath + " \t" + value + "\t" + currentFullXPath);
// xpath = xpath.intern();
try {
dataSource.putValueAtPath(currentFullXPath, value);
} catch (RuntimeException e) {
throw (IllegalArgumentException)
new IllegalArgumentException(
"failed adding " + currentFullXPath + ",\t" + value)
.initCause(e);
}
return this;
}
/** Note where this element was parsed. */
public CLDRFile addSourceLocation(String currentFullXPath, XMLSource.SourceLocation location) {
dataSource.addSourceLocation(currentFullXPath, location);
return this;
}
/**
* Get the line and column for a path
*
* @param path xpath or fullpath
*/
public XMLSource.SourceLocation getSourceLocation(String path) {
final String fullPath = getFullXPath(path);
return dataSource.getSourceLocation(fullPath);
}
public CLDRFile addComment(String xpath, String comment, Comments.CommentType type) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
// System.out.println("Adding comment: <" + xpath + "> '" + comment + "'");
Log.logln(LOG_PROGRESS, "ADDING Comment: \t" + type + "\t" + xpath + " \t" + comment);
if (xpath == null || xpath.length() == 0) {
dataSource
.getXpathComments()
.setFinalComment(
CldrUtility.joinWithSeparation(
dataSource.getXpathComments().getFinalComment(),
XPathParts.NEWLINE,
comment));
} else {
xpath = getDistinguishingXPath(xpath, null);
dataSource.getXpathComments().addComment(type, xpath, comment);
}
return this;
}
// TODO Change into enum, update docs
public static final int MERGE_KEEP_MINE = 0,
MERGE_REPLACE_MINE = 1,
MERGE_ADD_ALTERNATE = 2,
MERGE_REPLACE_MY_DRAFT = 3;
/**
* Merges elements from another CLDR file. Note: when both have the same xpath key, the keepMine
* determines whether "my" values are kept or the other files values are kept.
*
* @param other
* @param conflict_resolution
*/
public CLDRFile putAll(CLDRFile other, int conflict_resolution) {
if (locked) {
throw new UnsupportedOperationException("Attempt to modify locked object");
}
if (conflict_resolution == MERGE_KEEP_MINE) {
dataSource.putAll(other.dataSource, MERGE_KEEP_MINE);
} else if (conflict_resolution == MERGE_REPLACE_MINE) {
dataSource.putAll(other.dataSource, MERGE_REPLACE_MINE);
} else if (conflict_resolution == MERGE_REPLACE_MY_DRAFT) {
// first find all my alt=..proposed items
Set<String> hasDraftVersion = new HashSet<>();
for (Iterator<String> it = dataSource.iterator(); it.hasNext(); ) {
String cpath = it.next();
String fullpath = getFullXPath(cpath);
if (fullpath.indexOf("[@draft") >= 0) {
hasDraftVersion.add(
getNondraftNonaltXPath(cpath)); // strips the alt and the draft
}
}
// only replace draft items!
// this is either an item with draft in the fullpath
// or an item with draft and alt in the full path
for (Iterator<String> it = other.iterator(); it.hasNext(); ) {
String cpath = it.next();
cpath = getNondraftNonaltXPath(cpath);
String newValue = other.getStringValue(cpath);
String newFullPath = getNondraftNonaltXPath(other.getFullXPath(cpath));
// another hack; need to add references back in
newFullPath = addReferencesIfNeeded(newFullPath, getFullXPath(cpath));
if (!hasDraftVersion.contains(cpath)) {
if (cpath.startsWith("//ldml/identity/"))
continue; // skip, since the error msg is not needed.
String myVersion = getStringValue(cpath);
if (myVersion == null || !newValue.equals(myVersion)) {
Log.logln(
getLocaleID()
+ "\tDenied attempt to replace non-draft"
+ CldrUtility.LINE_SEPARATOR
+ "\tcurr: ["
+ cpath
+ ",\t"
+ myVersion
+ "]"
+ CldrUtility.LINE_SEPARATOR
+ "\twith: ["
+ newValue
+ "]");
continue;
}
}
Log.logln(getLocaleID() + "\tVETTED: [" + newFullPath + ",\t" + newValue + "]");
dataSource.putValueAtPath(newFullPath, newValue);
}
} else if (conflict_resolution == MERGE_ADD_ALTERNATE) {
for (Iterator<String> it = other.iterator(); it.hasNext(); ) {
String key = it.next();
String otherValue = other.getStringValue(key);
String myValue = dataSource.getValueAtPath(key);
if (myValue == null) {
dataSource.putValueAtPath(other.getFullXPath(key), otherValue);
} else if (!(myValue.equals(otherValue)
&& equalsIgnoringDraft(getFullXPath(key), other.getFullXPath(key)))
&& !key.startsWith("//ldml/identity")) {
for (int i = 0; ; ++i) {
String prop = "proposed" + (i == 0 ? "" : String.valueOf(i));
XPathParts parts =
XPathParts.getFrozenInstance(other.getFullXPath(key))
.cloneAsThawed(); // not frozen, for addAttribut
String fullPath = parts.addAttribute("alt", prop).toString();
String path = getDistinguishingXPath(fullPath, null);
if (dataSource.getValueAtPath(path) != null) {
continue;
}
dataSource.putValueAtPath(fullPath, otherValue);
break;
}
}
}
} else {
throw new IllegalArgumentException("Illegal operand: " + conflict_resolution);
}
dataSource
.getXpathComments()
.setInitialComment(
CldrUtility.joinWithSeparation(
dataSource.getXpathComments().getInitialComment(),
XPathParts.NEWLINE,
other.dataSource.getXpathComments().getInitialComment()));
dataSource
.getXpathComments()
.setFinalComment(
CldrUtility.joinWithSeparation(
dataSource.getXpathComments().getFinalComment(),
XPathParts.NEWLINE,
other.dataSource.getXpathComments().getFinalComment()));
dataSource.getXpathComments().joinAll(other.dataSource.getXpathComments());
return this;
}
/** */
private String addReferencesIfNeeded(String newFullPath, String fullXPath) {
if (fullXPath == null || fullXPath.indexOf("[@references=") < 0) {
return newFullPath;
}
XPathParts parts = XPathParts.getFrozenInstance(fullXPath);
String accummulatedReferences = null;
for (int i = 0; i < parts.size(); ++i) {
Map<String, String> attributes = parts.getAttributes(i);
String references = attributes.get("references");
if (references == null) {
continue;
}
if (accummulatedReferences == null) {
accummulatedReferences = references;
} else {
accummulatedReferences += ", " + references;
}
}
if (accummulatedReferences == null) {
return newFullPath;
}
XPathParts newParts = XPathParts.getFrozenInstance(newFullPath);
Map<String, String> attributes = newParts.getAttributes(newParts.size() - 1);
String references = attributes.get("references");
if (references == null) references = accummulatedReferences;
else references += ", " + accummulatedReferences;
attributes.put("references", references);
System.out.println(
"Changing " + newFullPath + " plus " + fullXPath + " to " + newParts.toString());
return newParts.toString();
}
/** Removes an element from a CLDRFile. */
public CLDRFile remove(String xpath) {
remove(xpath, false);
return this;
}
/** Removes an element from a CLDRFile. */
public CLDRFile remove(String xpath, boolean butComment) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
if (butComment) {
appendFinalComment(
dataSource.getFullPath(xpath) + "::<" + dataSource.getValueAtPath(xpath) + ">");
}
dataSource.removeValueAtPath(xpath);
return this;
}
/** Removes all xpaths from a CLDRFile. */
public CLDRFile removeAll(Set<String> xpaths, boolean butComment) {
if (butComment) appendFinalComment("Illegal attributes removed:");
for (Iterator<String> it = xpaths.iterator(); it.hasNext(); ) {
remove(it.next(), butComment);
}
return this;
}
/** Code should explicitly include CODE_FALLBACK */
public static final Pattern specialsToKeep =
PatternCache.get(
"/("
+ "measurementSystemName"
+ "|codePattern"
+ "|calendar\\[\\@type\\=\"[^\"]*\"\\]/(?!dateTimeFormats/appendItems)"
+ // gregorian
"|numbers/symbols/(decimal/group)"
+ "|timeZoneNames/(hourFormat|gmtFormat|regionFormat)"
+ "|pattern"
+ ")");
public static final Pattern specialsToPushFromRoot =
PatternCache.get(
"/("
+ "calendar\\[\\@type\\=\"gregorian\"\\]/"
+ "(?!fields)"
+ "(?!dateTimeFormats/appendItems)"
+ "(?!.*\\[@type=\"format\"].*\\[@type=\"narrow\"])"
+ "(?!.*\\[@type=\"stand-alone\"].*\\[@type=\"(abbreviated|wide)\"])"
+ "|numbers/symbols/(decimal/group)"
+ "|timeZoneNames/(hourFormat|gmtFormat|regionFormat)"
+ ")");
private static final boolean MINIMIZE_ALT_PROPOSED = false;
public interface RetentionTest {
public enum Retention {
RETAIN,
REMOVE,
RETAIN_IF_DIFFERENT
}
public Retention getRetention(String path);
}
/** Removes all items with same value */
public CLDRFile removeDuplicates(
CLDRFile other,
boolean butComment,
RetentionTest keepIfMatches,
Collection<String> removedItems) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
// Matcher specialPathMatcher = dontRemoveSpecials ? specialsToKeep.matcher("") : null;
boolean first = true;
if (removedItems == null) {
removedItems = new ArrayList<>();
} else {
removedItems.clear();
}
Set<String> checked = new HashSet<>();
for (Iterator<String> it = iterator();
it.hasNext(); ) { // see what items we have that the other also has
String curXpath = it.next();
boolean logicDuplicate = true;
if (!checked.contains(curXpath)) {
// we compare logic Group and only remove when all are duplicate
Set<String> logicGroups = LogicalGrouping.getPaths(this, curXpath);
if (logicGroups != null) {
Iterator<String> iter = logicGroups.iterator();
while (iter.hasNext() && logicDuplicate) {
String xpath = iter.next();
switch (keepIfMatches.getRetention(xpath)) {
case RETAIN:
logicDuplicate = false;
continue;
case RETAIN_IF_DIFFERENT:
String currentValue = dataSource.getValueAtPath(xpath);
if (currentValue == null) {
logicDuplicate = false;
continue;
}
String otherXpath = xpath;
String otherValue = other.dataSource.getValueAtPath(otherXpath);
if (!currentValue.equals(otherValue)) {
if (MINIMIZE_ALT_PROPOSED) {
otherXpath = CLDRFile.getNondraftNonaltXPath(xpath);
if (otherXpath.equals(xpath)) {
logicDuplicate = false;
continue;
}
otherValue = other.dataSource.getValueAtPath(otherXpath);
if (!currentValue.equals(otherValue)) {
logicDuplicate = false;
continue;
}
} else {
logicDuplicate = false;
continue;
}
}
String keepValue =
XMLSource.getPathsAllowingDuplicates().get(xpath);
if (keepValue != null && keepValue.equals(currentValue)) {
logicDuplicate = false;
continue;
}
// we've now established that the values are the same
String currentFullXPath = dataSource.getFullPath(xpath);
String otherFullXPath = other.dataSource.getFullPath(otherXpath);
if (!equalsIgnoringDraft(currentFullXPath, otherFullXPath)) {
logicDuplicate = false;
continue;
}
if (DEBUG) {
keepIfMatches.getRetention(xpath);
}
break;
case REMOVE:
if (DEBUG) {
keepIfMatches.getRetention(xpath);
}
break;
}
}
if (first) {
first = false;
if (butComment) appendFinalComment("Duplicates removed:");
}
}
// we can't remove right away, since that disturbs the iterator.
checked.addAll(logicGroups);
if (logicDuplicate) {
removedItems.addAll(logicGroups);
}
// remove(xpath, butComment);
}
}
// now remove them safely
for (String xpath : removedItems) {
remove(xpath, butComment);
}
return this;
}
/**
* @return Returns the finalComment.
*/
public String getFinalComment() {
return dataSource.getXpathComments().getFinalComment();
}
/**
* @return Returns the finalComment.
*/
public String getInitialComment() {
return dataSource.getXpathComments().getInitialComment();
}
/**
* @return Returns the xpath_comments. Cloned for safety.
*/
public XPathParts.Comments getXpath_comments() {
return (XPathParts.Comments) dataSource.getXpathComments().clone();
}
/**
* @return Returns the locale ID. In the case of a supplemental data file, it is
* SUPPLEMENTAL_NAME.
*/
@Override
public String getLocaleID() {
return dataSource.getLocaleID();
}
/**
* @return the Locale ID, as declared in the //ldml/identity element
*/
public String getLocaleIDFromIdentity() {
ULocale.Builder lb = new ULocale.Builder();
for (Iterator<String> i = iterator("//ldml/identity/"); i.hasNext(); ) {
XPathParts xpp = XPathParts.getFrozenInstance(i.next());
String k = xpp.getElement(-1);
String v = xpp.getAttributeValue(-1, "type");
if (k.equals("language")) {
lb = lb.setLanguage(v);
} else if (k.equals("script")) {
lb = lb.setScript(v);
} else if (k.equals("territory")) {
lb = lb.setRegion(v);
} else if (k.equals("variant")) {
lb = lb.setVariant(v);
}
}
return lb.build().toString(); // TODO: CLDRLocale ?
}
/**
* @see com.ibm.icu.util.Freezable#isFrozen()
*/
@Override
public synchronized boolean isFrozen() {
return locked;
}
/**
* @see com.ibm.icu.util.Freezable#freeze()
*/
@Override
public synchronized CLDRFile freeze() {
locked = true;
dataSource.freeze();
return this;
}
public CLDRFile clearComments() {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
dataSource.setXpathComments(new XPathParts.Comments());
return this;
}
/** Sets a final comment, replacing everything that was there. */
public CLDRFile setFinalComment(String comment) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
dataSource.getXpathComments().setFinalComment(comment);
return this;
}
/** Adds a comment to the final list of comments. */
public CLDRFile appendFinalComment(String comment) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
dataSource
.getXpathComments()
.setFinalComment(
CldrUtility.joinWithSeparation(
dataSource.getXpathComments().getFinalComment(),
XPathParts.NEWLINE,
comment));
return this;
}
/** Sets the initial comment, replacing everything that was there. */
public CLDRFile setInitialComment(String comment) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
dataSource.getXpathComments().setInitialComment(comment);
return this;
}
// ========== STATIC UTILITIES ==========
/**
* Utility to restrict to files matching a given regular expression. The expression does not
* contain ".xml". Note that supplementalData is always skipped, and root is always included.
*/
public static Set<String> getMatchingXMLFiles(File sourceDirs[], Matcher m) {
Set<String> s = new TreeSet<>();
for (File dir : sourceDirs) {
if (!dir.exists()) {
throw new IllegalArgumentException("Directory doesn't exist:\t" + dir.getPath());
}
if (!dir.isDirectory()) {
throw new IllegalArgumentException(
"Input isn't a file directory:\t" + dir.getPath());
}
File[] files = dir.listFiles();
for (int i = 0; i < files.length; ++i) {
String name = files[i].getName();
if (!name.endsWith(".xml") || name.startsWith(".")) continue;
// if (name.startsWith(SUPPLEMENTAL_NAME)) continue;
String locale = name.substring(0, name.length() - 4); // drop .xml
if (!m.reset(locale).matches()) continue;
s.add(locale);
}
}
return s;
}
@Override
public Iterator<String> iterator() {
return dataSource.iterator();
}
public synchronized Iterator<String> iterator(String prefix) {
return dataSource.iterator(prefix);
}
public Iterator<String> iterator(Matcher pathFilter) {
return dataSource.iterator(pathFilter);
}
public Iterator<String> iterator(String prefix, Comparator<String> comparator) {
Iterator<String> it =
(prefix == null || prefix.length() == 0)
? dataSource.iterator()
: dataSource.iterator(prefix);
if (comparator == null) return it;
Set<String> orderedSet = new TreeSet<>(comparator);
it.forEachRemaining(orderedSet::add);
return orderedSet.iterator();
}
public Iterable<String> fullIterable() {
return new FullIterable(this);
}
public static class FullIterable implements Iterable<String>, SimpleIterator<String> {
private final CLDRFile file;
private final Iterator<String> fileIterator;
private Iterator<String> extraPaths;
FullIterable(CLDRFile file) {
this.file = file;
this.fileIterator = file.iterator();
}
@Override
public Iterator<String> iterator() {
return With.toIterator(this);
}
@Override
public String next() {
if (fileIterator.hasNext()) {
return fileIterator.next();
}
if (extraPaths == null) {
extraPaths = file.getExtraPaths().iterator();
}
if (extraPaths.hasNext()) {
return extraPaths.next();
}
return null;
}
}
public static String getDistinguishingXPath(String xpath, String[] normalizedPath) {
return DistinguishedXPath.getDistinguishingXPath(xpath, normalizedPath);
}
private static boolean equalsIgnoringDraft(String path1, String path2) {
if (path1 == path2) {
return true;
}
if (path1 == null || path2 == null) {
return false;
}
// TODO: optimize
if (path1.indexOf("[@draft=") < 0 && path2.indexOf("[@draft=") < 0) {
return path1.equals(path2);
}
return getNondraftNonaltXPath(path1).equals(getNondraftNonaltXPath(path2));
}
/*
* TODO: clarify the need for syncObject.
* Formerly, an XPathParts object named "nondraftParts" was used for this purpose, but
* there was no evident reason for it to be an XPathParts object rather than any other
* kind of object.
*/
private static Object syncObject = new Object();
public static String getNondraftNonaltXPath(String xpath) {
if (xpath.indexOf("draft=\"") < 0 && xpath.indexOf("alt=\"") < 0) {
return xpath;
}
synchronized (syncObject) {
XPathParts parts =
XPathParts.getFrozenInstance(xpath)
.cloneAsThawed(); // can't be frozen since we call removeAttributes
String restore;
HashSet<String> toRemove = new HashSet<>();
for (int i = 0; i < parts.size(); ++i) {
if (parts.getAttributeCount(i) == 0) {
continue;
}
Map<String, String> attributes = parts.getAttributes(i);
toRemove.clear();
restore = null;
for (Iterator<String> it = attributes.keySet().iterator(); it.hasNext(); ) {
String attribute = it.next();
if (attribute.equals("draft")) {
toRemove.add(attribute);
} else if (attribute.equals("alt")) {
String value = attributes.get(attribute);
int proposedPos = value.indexOf("proposed");
if (proposedPos >= 0) {
toRemove.add(attribute);
if (proposedPos > 0) {
restore =
value.substring(
0, proposedPos - 1); // is of form xxx-proposedyyy
}
}
}
}
parts.removeAttributes(i, toRemove);
if (restore != null) {
attributes.put("alt", restore);
}
}
return parts.toString();
}
}
/**
* Determine if an attribute is a distinguishing attribute.
*
* @param elementName
* @param attribute
* @return
*/
public static boolean isDistinguishing(DtdType type, String elementName, String attribute) {
return DtdData.getInstance(type).isDistinguishing(elementName, attribute);
}
/** Utility to create a validating XML reader. */
public static XMLReader createXMLReader(boolean validating) {
String[] testList = {
"org.apache.xerces.parsers.SAXParser",
"org.apache.crimson.parser.XMLReaderImpl",
"gnu.xml.aelfred2.XmlReader",
"com.bluecast.xml.Piccolo",
"oracle.xml.parser.v2.SAXParser",
""
};
XMLReader result = null;
for (int i = 0; i < testList.length; ++i) {
try {
result =
(testList[i].length() != 0)
? XMLReaderFactory.createXMLReader(testList[i])
: XMLReaderFactory.createXMLReader();
result.setFeature("http://xml.org/sax/features/validation", validating);
break;
} catch (SAXException e1) {
}
}
if (result == null)
throw new NoClassDefFoundError(
"No SAX parser is available, or unable to set validation correctly");
return result;
}
/**
* Return a directory to supplemental data used by this CLDRFile. If the CLDRFile is not
* normally disk-based, the returned directory may be temporary and not guaranteed to exist past
* the lifetime of the CLDRFile. The directory should be considered read-only.
*/
public File getSupplementalDirectory() {
if (supplementalDirectory == null) {
// ask CLDRConfig.
supplementalDirectory =
CLDRConfig.getInstance().getSupplementalDataInfo().getDirectory();
}
return supplementalDirectory;
}
public CLDRFile setSupplementalDirectory(File supplementalDirectory) {
this.supplementalDirectory = supplementalDirectory;
return this;
}
/**
* Convenience function to return a list of XML files in the Supplemental directory.
*
* @return all files ending in ".xml"
* @see #getSupplementalDirectory()
*/
public File[] getSupplementalXMLFiles() {
return getSupplementalDirectory()
.listFiles(
new FilenameFilter() {
@Override
public boolean accept(
@SuppressWarnings("unused") File dir, String name) {
return name.endsWith(".xml");
}
});
}
/**
* Convenience function to return a specific supplemental file
*
* @param filename the file to return
* @return the file (may not exist)
* @see #getSupplementalDirectory()
*/
public File getSupplementalFile(String filename) {
return new File(getSupplementalDirectory(), filename);
}
public static boolean isSupplementalName(String localeName) {
return SUPPLEMENTAL_NAMES.contains(localeName);
}
// static String[] keys = {"calendar", "collation", "currency"};
//
// static String[] calendar_keys = {"buddhist", "chinese", "gregorian", "hebrew", "islamic",
// "islamic-civil",
// "japanese"};
// static String[] collation_keys = {"phonebook", "traditional", "direct", "pinyin", "stroke",
// "posix", "big5han",
// "gb2312han"};
/* */
/**
* Value that contains a node. WARNING: this is not done yet, and may change. In particular, we
* don't want to return a Node, since that is mutable, and makes caching unsafe!!
*/
/*
* static public class NodeValue extends Value {
* private Node nodeValue;
*/
/**
* Creation. WARNING, may change.
*
* @param value
* @param currentFullXPath
*/
/*
* public NodeValue(Node value, String currentFullXPath) {
* super(currentFullXPath);
* this.nodeValue = value;
* }
*/
/** boilerplate */
/*
* public boolean hasSameValue(Object other) {
* if (super.hasSameValue(other)) return false;
* return nodeValue.equals(((NodeValue)other).nodeValue);
* }
*/
/** boilerplate */
/*
* public String getStringValue() {
* return nodeValue.toString();
* }
* (non-Javadoc)
*
* @see org.unicode.cldr.util.CLDRFile.Value#changePath(java.lang.String)
*
* public Value changePath(String string) {
* return new NodeValue(nodeValue, string);
* }
* }
*/
private static class MyDeclHandler implements AllHandler {
private static UnicodeSet whitespace = new UnicodeSet("[:whitespace:]");
private DraftStatus minimalDraftStatus;
private static final boolean SHOW_START_END = false;
private int commentStack;
private boolean justPopped = false;
private String lastChars = "";
// private String currentXPath = "/";
private String currentFullXPath = "/";
private String comment = null;
private Map<String, String> attributeOrder;
private DtdData dtdData;
private CLDRFile target;
private String lastActiveLeafNode;
private String lastLeafNode;
private int isSupplemental = -1;
private int[] orderedCounter =
new int[30]; // just make deep enough to handle any CLDR file.
private String[] orderedString =
new String[30]; // just make deep enough to handle any CLDR file.
private int level = 0;
private int overrideCount = 0;
private Locator documentLocator = null;
MyDeclHandler(CLDRFile target, DraftStatus minimalDraftStatus) {
this.target = target;
this.minimalDraftStatus = minimalDraftStatus;
}
private String show(Attributes attributes) {
if (attributes == null) return "null";
String result = "";
for (int i = 0; i < attributes.getLength(); ++i) {
String attribute = attributes.getQName(i);
String value = attributes.getValue(i);
result += "[@" + attribute + "=\"" + value + "\"]"; // TODO quote the value??
}
return result;
}
private void push(String qName, Attributes attributes) {
// SHOW_ALL &&
Log.logln(LOG_PROGRESS, "push\t" + qName + "\t" + show(attributes));
++level;
if (!qName.equals(orderedString[level])) {
// orderedCounter[level] = 0;
orderedString[level] = qName;
}
if (lastChars.length() != 0) {
if (whitespace.containsAll(lastChars)) lastChars = "";
else
throw new IllegalArgumentException(
"Must not have mixed content: "
+ qName
+ ", "
+ show(attributes)
+ ", Content: "
+ lastChars);
}
// currentXPath += "/" + qName;
currentFullXPath += "/" + qName;
// if (!isSupplemental) ldmlComparator.addElement(qName);
if (dtdData.isOrdered(qName)) {
currentFullXPath += orderingAttribute();
}
if (attributes.getLength() > 0) {
attributeOrder.clear();
for (int i = 0; i < attributes.getLength(); ++i) {
String attribute = attributes.getQName(i);
String value = attributes.getValue(i);
// if (!isSupplemental) ldmlComparator.addAttribute(attribute); // must do
// BEFORE put
// ldmlComparator.addValue(value);
// special fix to remove version
// <!ATTLIST version number CDATA #REQUIRED >
// <!ATTLIST version cldrVersion CDATA #FIXED "24" >
if (attribute.equals("cldrVersion") && (qName.equals("version"))) {
((SimpleXMLSource) target.dataSource)
.setDtdVersionInfo(VersionInfo.getInstance(value));
} else {
putAndFixDeprecatedAttribute(qName, attribute, value);
}
}
for (Iterator<String> it = attributeOrder.keySet().iterator(); it.hasNext(); ) {
String attribute = it.next();
String value = attributeOrder.get(attribute);
String both =
"[@" + attribute + "=\"" + value + "\"]"; // TODO quote the value??
currentFullXPath += both;
// distinguishing = key, registry, alt, and type (except for the type attribute
// on the elements
// default and mapping).
// if (isDistinguishing(qName, attribute)) {
// currentXPath += both;
// }
}
}
if (comment != null) {
if (currentFullXPath.equals("//ldml")
|| currentFullXPath.equals("//supplementalData")) {
target.setInitialComment(comment);
} else {
target.addComment(
currentFullXPath, comment, XPathParts.Comments.CommentType.PREBLOCK);
}
comment = null;
}
justPopped = false;
lastActiveLeafNode = null;
Log.logln(LOG_PROGRESS, "currentFullXPath\t" + currentFullXPath);
}
private String orderingAttribute() {
return "[@_q=\"" + (orderedCounter[level]++) + "\"]";
}
private void putAndFixDeprecatedAttribute(String element, String attribute, String value) {
if (attribute.equals("draft")) {
if (value.equals("true")) value = "approved";
else if (value.equals("false")) value = "unconfirmed";
} else if (attribute.equals("type")) {
if (changedTypes.contains(element)
&& isSupplemental < 1) { // measurementSystem for example did not
// change from 'type' to 'choice'.
attribute = "choice";
}
}
// else if (element.equals("dateFormatItem")) {
// if (attribute.equals("id")) {
// String newValue = dateGenerator.getBaseSkeleton(value);
// if (!fixedSkeletons.contains(newValue)) {
// fixedSkeletons.add(newValue);
// if (!value.equals(newValue)) {
// System.out.println(value + " => " + newValue);
// }
// value = newValue;
// }
// }
// }
attributeOrder.put(attribute, value);
}
// private Set<String> fixedSkeletons = new HashSet();
// private DateTimePatternGenerator dateGenerator =
// DateTimePatternGenerator.getEmptyInstance();
/** Types which changed from 'type' to 'choice', but not in supplemental data. */
private static Set<String> changedTypes =
new HashSet<>(
Arrays.asList(
new String[] {
"abbreviationFallback",
"default",
"mapping",
"measurementSystem",
"preferenceOrdering"
}));
Matcher draftMatcher = DRAFT_PATTERN.matcher("");
/**
* Adds a parsed XPath to the CLDRFile.
*
* @param fullXPath
* @param value
*/
private void addPath(String fullXPath, String value) {
String former = target.getStringValue(fullXPath);
if (former != null) {
String formerPath = target.getFullXPath(fullXPath);
if (!former.equals(value) || !fullXPath.equals(formerPath)) {
if (!fullXPath.startsWith("//ldml/identity/version")
&& !fullXPath.startsWith("//ldml/identity/generation")) {
warnOnOverride(former, formerPath);
}
}
}
value = trimWhitespaceSpecial(value);
target.add(fullXPath, value)
.addSourceLocation(fullXPath, new XMLSource.SourceLocation(documentLocator));
}
private void pop(String qName) {
Log.logln(LOG_PROGRESS, "pop\t" + qName);
--level;
if (lastChars.length() != 0 || justPopped == false) {
boolean acceptItem = minimalDraftStatus == DraftStatus.unconfirmed;
if (!acceptItem) {
if (draftMatcher.reset(currentFullXPath).find()) {
DraftStatus foundStatus = DraftStatus.valueOf(draftMatcher.group(1));
if (minimalDraftStatus.compareTo(foundStatus) <= 0) {
// what we found is greater than or equal to our status
acceptItem = true;
}
} else {
acceptItem =
true; // if not found, then the draft status is approved, so it is
// always ok
}
}
if (acceptItem) {
// Change any deprecated orientation attributes into values
// for backwards compatibility.
boolean skipAdd = false;
if (currentFullXPath.startsWith("//ldml/layout/orientation")) {
XPathParts parts = XPathParts.getFrozenInstance(currentFullXPath);
String value = parts.getAttributeValue(-1, "characters");
if (value != null) {
addPath("//ldml/layout/orientation/characterOrder", value);
skipAdd = true;
}
value = parts.getAttributeValue(-1, "lines");
if (value != null) {
addPath("//ldml/layout/orientation/lineOrder", value);
skipAdd = true;
}
}
if (!skipAdd) {
addPath(currentFullXPath, lastChars);
}
lastLeafNode = lastActiveLeafNode = currentFullXPath;
}
lastChars = "";
} else {
Log.logln(
LOG_PROGRESS && lastActiveLeafNode != null,
"pop: zeroing last leafNode: " + lastActiveLeafNode);
lastActiveLeafNode = null;
if (comment != null) {
target.addComment(
lastLeafNode, comment, XPathParts.Comments.CommentType.POSTBLOCK);
comment = null;
}
}
// currentXPath = stripAfter(currentXPath, qName);
currentFullXPath = stripAfter(currentFullXPath, qName);
justPopped = true;
}
static Pattern WHITESPACE_WITH_LF = PatternCache.get("\\s*\\u000a\\s*");
Matcher whitespaceWithLf = WHITESPACE_WITH_LF.matcher("");
static final UnicodeSet CONTROLS = new UnicodeSet("[:cc:]");
/**
* Trim leading whitespace if there is a linefeed among them, then the same with trailing.
*
* @param source
* @return
*/
private String trimWhitespaceSpecial(String source) {
if (DEBUG && CONTROLS.containsSome(source)) {
System.out.println("*** " + source);
}
if (!source.contains("\n")) {
return source;
}
source = whitespaceWithLf.reset(source).replaceAll("\n");
return source;
}
private void warnOnOverride(String former, String formerPath) {
String distinguishing = CLDRFile.getDistinguishingXPath(formerPath, null);
System.out.println(
"\tERROR in "
+ target.getLocaleID()
+ ";\toverriding old value <"
+ former
+ "> at path "
+ distinguishing
+ "\twith\t<"
+ lastChars
+ ">"
+ CldrUtility.LINE_SEPARATOR
+ "\told fullpath: "
+ formerPath
+ CldrUtility.LINE_SEPARATOR
+ "\tnew fullpath: "
+ currentFullXPath);
overrideCount += 1;
}
private static String stripAfter(String input, String qName) {
int pos = findLastSlash(input);
if (qName != null) {
// assert input.substring(pos+1).startsWith(qName);
if (!input.substring(pos + 1).startsWith(qName)) {
throw new IllegalArgumentException("Internal Error: should never get here.");
}
}
return input.substring(0, pos);
}
private static int findLastSlash(String input) {
int braceStack = 0;
char inQuote = 0;
for (int i = input.length() - 1; i >= 0; --i) {
char ch = input.charAt(i);
switch (ch) {
case '\'':
case '"':
if (inQuote == 0) {
inQuote = ch;
} else if (inQuote == ch) {
inQuote = 0; // come out of quote
}
break;
case '/':
if (inQuote == 0 && braceStack == 0) {
return i;
}
break;
case '[':
if (inQuote == 0) {
--braceStack;
}
break;
case ']':
if (inQuote == 0) {
++braceStack;
}
break;
}
}
return -1;
}
// SAX items we need to catch
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
Log.logln(
LOG_PROGRESS || SHOW_START_END,
"startElement uri\t"
+ uri
+ "\tlocalName "
+ localName
+ "\tqName "
+ qName
+ "\tattributes "
+ show(attributes));
try {
if (isSupplemental < 0) { // set by first element
attributeOrder =
new TreeMap<>(
// HACK for ldmlIcu
dtdData.dtdType == DtdType.ldml
? CLDRFile.getAttributeOrdering()
: dtdData.getAttributeComparator());
isSupplemental = target.dtdType == DtdType.ldml ? 0 : 1;
}
push(qName, attributes);
} catch (RuntimeException e) {
e.printStackTrace();
throw e;
}
}
@Override
public void endElement(String uri, String localName, String qName) throws SAXException {
Log.logln(
LOG_PROGRESS || SHOW_START_END,
"endElement uri\t" + uri + "\tlocalName " + localName + "\tqName " + qName);
try {
pop(qName);
} catch (RuntimeException e) {
// e.printStackTrace();
throw e;
}
}
// static final char XML_LINESEPARATOR = (char) 0xA;
// static final String XML_LINESEPARATOR_STRING = String.valueOf(XML_LINESEPARATOR);
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
try {
String value = new String(ch, start, length);
Log.logln(LOG_PROGRESS, "characters:\t" + value);
// we will strip leading and trailing line separators in another place.
// if (value.indexOf(XML_LINESEPARATOR) >= 0) {
// value = value.replace(XML_LINESEPARATOR, '\u0020');
// }
lastChars += value;
justPopped = false;
} catch (RuntimeException e) {
e.printStackTrace();
throw e;
}
}
@Override
public void startDTD(String name, String publicId, String systemId) throws SAXException {
Log.logln(
LOG_PROGRESS,
"startDTD name: "
+ name
+ ", publicId: "
+ publicId
+ ", systemId: "
+ systemId);
commentStack++;
target.dtdType = DtdType.fromElement(name);
target.dtdData = dtdData = DtdData.getInstance(target.dtdType);
}
@Override
public void endDTD() throws SAXException {
Log.logln(LOG_PROGRESS, "endDTD");
commentStack--;
}
@Override
public void comment(char[] ch, int start, int length) throws SAXException {
final String string = new String(ch, start, length);
Log.logln(LOG_PROGRESS, commentStack + " comment " + string);
try {
if (commentStack != 0) return;
String comment0 = trimWhitespaceSpecial(string).trim();
if (lastActiveLeafNode != null) {
target.addComment(
lastActiveLeafNode, comment0, XPathParts.Comments.CommentType.LINE);
} else {
comment =
(comment == null ? comment0 : comment + XPathParts.NEWLINE + comment0);
}
} catch (RuntimeException e) {
e.printStackTrace();
throw e;
}
}
@Override
public void ignorableWhitespace(char[] ch, int start, int length) throws SAXException {
if (LOG_PROGRESS)
Log.logln(
LOG_PROGRESS,
"ignorableWhitespace length: "
+ length
+ ": "
+ Utility.hex(new String(ch, start, length)));
// if (lastActiveLeafNode != null) {
for (int i = start; i < start + length; ++i) {
if (ch[i] == '\n') {
Log.logln(
LOG_PROGRESS && lastActiveLeafNode != null,
"\\n: zeroing last leafNode: " + lastActiveLeafNode);
lastActiveLeafNode = null;
break;
}
}
// }
}
@Override
public void startDocument() throws SAXException {
Log.logln(LOG_PROGRESS, "startDocument");
commentStack = 0; // initialize
}
@Override
public void endDocument() throws SAXException {
Log.logln(LOG_PROGRESS, "endDocument");
try {
if (comment != null)
target.addComment(null, comment, XPathParts.Comments.CommentType.LINE);
} catch (RuntimeException e) {
e.printStackTrace();
throw e;
}
}
// ==== The following are just for debuggin =====
@Override
public void elementDecl(String name, String model) throws SAXException {
Log.logln(LOG_PROGRESS, "Attribute\t" + name + "\t" + model);
}
@Override
public void attributeDecl(
String eName, String aName, String type, String mode, String value)
throws SAXException {
Log.logln(
LOG_PROGRESS,
"Attribute\t"
+ eName
+ "\t"
+ aName
+ "\t"
+ type
+ "\t"
+ mode
+ "\t"
+ value);
}
@Override
public void internalEntityDecl(String name, String value) throws SAXException {
Log.logln(LOG_PROGRESS, "Internal Entity\t" + name + "\t" + value);
}
@Override
public void externalEntityDecl(String name, String publicId, String systemId)
throws SAXException {
Log.logln(LOG_PROGRESS, "Internal Entity\t" + name + "\t" + publicId + "\t" + systemId);
}
@Override
public void processingInstruction(String target, String data) throws SAXException {
Log.logln(LOG_PROGRESS, "processingInstruction: " + target + ", " + data);
}
@Override
public void skippedEntity(String name) throws SAXException {
Log.logln(LOG_PROGRESS, "skippedEntity: " + name);
}
@Override
public void setDocumentLocator(Locator locator) {
Log.logln(LOG_PROGRESS, "setDocumentLocator Locator " + locator);
documentLocator = locator;
}
@Override
public void startPrefixMapping(String prefix, String uri) throws SAXException {
Log.logln(LOG_PROGRESS, "startPrefixMapping prefix: " + prefix + ", uri: " + uri);
}
@Override
public void endPrefixMapping(String prefix) throws SAXException {
Log.logln(LOG_PROGRESS, "endPrefixMapping prefix: " + prefix);
}
@Override
public void startEntity(String name) throws SAXException {
Log.logln(LOG_PROGRESS, "startEntity name: " + name);
}
@Override
public void endEntity(String name) throws SAXException {
Log.logln(LOG_PROGRESS, "endEntity name: " + name);
}
@Override
public void startCDATA() throws SAXException {
Log.logln(LOG_PROGRESS, "startCDATA");
}
@Override
public void endCDATA() throws SAXException {
Log.logln(LOG_PROGRESS, "endCDATA");
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ErrorHandler#error(org.xml.sax.SAXParseException)
*/
@Override
public void error(SAXParseException exception) throws SAXException {
Log.logln(LOG_PROGRESS || true, "error: " + showSAX(exception));
throw exception;
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ErrorHandler#fatalError(org.xml.sax.SAXParseException)
*/
@Override
public void fatalError(SAXParseException exception) throws SAXException {
Log.logln(LOG_PROGRESS, "fatalError: " + showSAX(exception));
throw exception;
}
/*
* (non-Javadoc)
*
* @see org.xml.sax.ErrorHandler#warning(org.xml.sax.SAXParseException)
*/
@Override
public void warning(SAXParseException exception) throws SAXException {
Log.logln(LOG_PROGRESS, "warning: " + showSAX(exception));
throw exception;
}
}
/** Show a SAX exception in a readable form. */
public static String showSAX(SAXParseException exception) {
return exception.getMessage()
+ ";\t SystemID: "
+ exception.getSystemId()
+ ";\t PublicID: "
+ exception.getPublicId()
+ ";\t LineNumber: "
+ exception.getLineNumber()
+ ";\t ColumnNumber: "
+ exception.getColumnNumber();
}
/** Says whether the whole file is draft */
public boolean isDraft() {
String item = iterator().next();
return item.startsWith("//ldml[@draft=\"unconfirmed\"]");
}
// public Collection keySet(Matcher regexMatcher, Collection output) {
// if (output == null) output = new ArrayList(0);
// for (Iterator it = keySet().iterator(); it.hasNext();) {
// String path = (String)it.next();
// if (regexMatcher.reset(path).matches()) {
// output.add(path);
// }
// }
// return output;
// }
// public Collection keySet(String regexPattern, Collection output) {
// return keySet(PatternCache.get(regexPattern).matcher(""), output);
// }
/**
* Gets the type of a given xpath, eg script, territory, ... TODO move to separate class
*
* @param xpath
* @return
*/
public static int getNameType(String xpath) {
for (int i = 0; i < NameTable.length; ++i) {
if (!xpath.startsWith(NameTable[i][0])) continue;
if (xpath.indexOf(NameTable[i][1], NameTable[i][0].length()) >= 0) return i;
}
return -1;
}
/** Gets the display name for a type */
public static String getNameTypeName(int index) {
try {
return getNameName(index);
} catch (Exception e) {
return "Illegal Type Name: " + index;
}
}
public static final int NO_NAME = -1,
LANGUAGE_NAME = 0,
SCRIPT_NAME = 1,
TERRITORY_NAME = 2,
VARIANT_NAME = 3,
CURRENCY_NAME = 4,
CURRENCY_SYMBOL = 5,
TZ_EXEMPLAR = 6,
TZ_START = TZ_EXEMPLAR,
TZ_GENERIC_LONG = 7,
TZ_GENERIC_SHORT = 8,
TZ_STANDARD_LONG = 9,
TZ_STANDARD_SHORT = 10,
TZ_DAYLIGHT_LONG = 11,
TZ_DAYLIGHT_SHORT = 12,
TZ_LIMIT = 13,
KEY_NAME = 13,
KEY_TYPE_NAME = 14,
SUBDIVISION_NAME = 15,
LIMIT_TYPES = 15;
private static final String[][] NameTable = {
{"//ldml/localeDisplayNames/languages/language[@type=\"", "\"]", "language"},
{"//ldml/localeDisplayNames/scripts/script[@type=\"", "\"]", "script"},
{"//ldml/localeDisplayNames/territories/territory[@type=\"", "\"]", "territory"},
{"//ldml/localeDisplayNames/variants/variant[@type=\"", "\"]", "variant"},
{"//ldml/numbers/currencies/currency[@type=\"", "\"]/displayName", "currency"},
{"//ldml/numbers/currencies/currency[@type=\"", "\"]/symbol", "currency-symbol"},
{"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/exemplarCity", "exemplar-city"},
{"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/long/generic", "tz-generic-long"},
{"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/short/generic", "tz-generic-short"},
{"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/long/standard", "tz-standard-long"},
{"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/short/standard", "tz-standard-short"},
{"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/long/daylight", "tz-daylight-long"},
{"//ldml/dates/timeZoneNames/zone[@type=\"", "\"]/short/daylight", "tz-daylight-short"},
{"//ldml/localeDisplayNames/keys/key[@type=\"", "\"]", "key"},
{"//ldml/localeDisplayNames/types/type[@key=\"", "\"][@type=\"", "\"]", "key|type"},
{"//ldml/localeDisplayNames/subdivisions/subdivision[@type=\"", "\"]", "subdivision"},
/**
* <long> <generic>Newfoundland Time</generic> <standard>Newfoundland Standard
* Time</standard> <daylight>Newfoundland Daylight Time</daylight> </long> - <short>
* <generic>NT</generic> <standard>NST</standard> <daylight>NDT</daylight> </short>
*/
};
// private static final String[] TYPE_NAME = {"language", "script", "territory", "variant",
// "currency",
// "currency-symbol",
// "tz-exemplar",
// "tz-generic-long", "tz-generic-short"};
public Iterator<String> getAvailableIterator(int type) {
return iterator(NameTable[type][0]);
}
/**
* @return the xpath used to access data of a given type
*/
public static String getKey(int type, String code) {
switch (type) {
case VARIANT_NAME:
code = code.toUpperCase(Locale.ROOT);
break;
case KEY_NAME:
code = fixKeyName(code);
break;
case TZ_DAYLIGHT_LONG:
case TZ_DAYLIGHT_SHORT:
case TZ_EXEMPLAR:
case TZ_GENERIC_LONG:
case TZ_GENERIC_SHORT:
case TZ_STANDARD_LONG:
case TZ_STANDARD_SHORT:
code = getLongTzid(code);
break;
}
String[] nameTableRow = NameTable[type];
if (code.contains("|")) {
String[] codes = code.split("\\|");
return nameTableRow[0]
+ fixKeyName(codes[0])
+ nameTableRow[1]
+ codes[1]
+ nameTableRow[2];
} else {
return nameTableRow[0] + code + nameTableRow[1];
}
}
static final Relation<R2<String, String>, String> bcp47AliasMap =
CLDRConfig.getInstance().getSupplementalDataInfo().getBcp47Aliases();
public static String getLongTzid(String code) {
if (!code.contains("/")) {
Set<String> codes = bcp47AliasMap.get(Row.of("tz", code));
if (codes != null && !codes.isEmpty()) {
code = codes.iterator().next();
}
}
return code;
}
static final ImmutableMap<String, String> FIX_KEY_NAME;
static {
Builder<String, String> temp = ImmutableMap.builder();
for (String s :
Arrays.asList(
"colAlternate",
"colBackwards",
"colCaseFirst",
"colCaseLevel",
"colNormalization",
"colNumeric",
"colReorder",
"colStrength")) {
temp.put(s.toLowerCase(Locale.ROOT), s);
}
FIX_KEY_NAME = temp.build();
}
private static String fixKeyName(String code) {
String result = FIX_KEY_NAME.get(code);
return result == null ? code : result;
}
/**
* @return the code used to access data of a given type from the path. Null if not found.
*/
public static String getCode(String path) {
int type = getNameType(path);
if (type < 0) {
throw new IllegalArgumentException("Illegal type in path: " + path);
}
String[] nameTableRow = NameTable[type];
int start = nameTableRow[0].length();
int end = path.indexOf(nameTableRow[1], start);
return path.substring(start, end);
}
/**
* @param type a string such as "language", "script", "territory", "region", ...
* @return the corresponding integer
*/
public static int typeNameToCode(String type) {
if (type.equalsIgnoreCase("region")) {
type = "territory";
}
for (int i = 0; i < LIMIT_TYPES; ++i) {
if (type.equalsIgnoreCase(getNameName(i))) {
return i;
}
}
return -1;
}
/** For use in getting short names. */
public static final Transform<String, String> SHORT_ALTS =
new Transform<>() {
@Override
public String transform(@SuppressWarnings("unused") String source) {
return "short";
}
};
/** Returns the name of a type. */
public static String getNameName(int choice) {
String[] nameTableRow = NameTable[choice];
return nameTableRow[nameTableRow.length - 1];
}
/**
* Get standard ordering for elements.
*
* @return ordered collection with items.
* @deprecated
*/
@Deprecated
public static List<String> getElementOrder() {
return Collections.emptyList(); // elementOrdering.getOrder(); // already unmodifiable
}
/**
* Get standard ordering for attributes.
*
* @return ordered collection with items.
*/
public static List<String> getAttributeOrder() {
return getAttributeOrdering().getOrder(); // already unmodifiable
}
public static boolean isOrdered(String element, DtdType type) {
return DtdData.getInstance(type).isOrdered(element);
}
private static Comparator<String> ldmlComparator =
DtdData.getInstance(DtdType.ldmlICU).getDtdComparator(null);
private static final Map<String, Map<String, String>> defaultSuppressionMap;
static {
String[][] data = {
{"ldml", "version", GEN_VERSION},
{"version", "cldrVersion", "*"},
{"orientation", "characters", "left-to-right"},
{"orientation", "lines", "top-to-bottom"},
{"weekendStart", "time", "00:00"},
{"weekendEnd", "time", "24:00"},
{"dateFormat", "type", "standard"},
{"timeFormat", "type", "standard"},
{"dateTimeFormat", "type", "standard"},
{"decimalFormat", "type", "standard"},
{"scientificFormat", "type", "standard"},
{"percentFormat", "type", "standard"},
{"pattern", "type", "standard"},
{"currency", "type", "standard"},
{"transform", "visibility", "external"},
{"*", "_q", "*"},
};
Map<String, Map<String, String>> tempmain = asMap(data, true);
defaultSuppressionMap = Collections.unmodifiableMap(tempmain);
}
public static Map<String, Map<String, String>> getDefaultSuppressionMap() {
return defaultSuppressionMap;
}
@SuppressWarnings({"rawtypes", "unchecked"})
private static Map asMap(String[][] data, boolean tree) {
Map tempmain = tree ? (Map) new TreeMap() : new HashMap();
int len = data[0].length; // must be same for all elements
for (int i = 0; i < data.length; ++i) {
Map temp = tempmain;
if (len != data[i].length) {
throw new IllegalArgumentException("Must be square array: fails row " + i);
}
for (int j = 0; j < len - 2; ++j) {
Map newTemp = (Map) temp.get(data[i][j]);
if (newTemp == null) {
temp.put(data[i][j], newTemp = tree ? (Map) new TreeMap() : new HashMap());
}
temp = newTemp;
}
temp.put(data[i][len - 2], data[i][len - 1]);
}
return tempmain;
}
/** Removes a comment. */
public CLDRFile removeComment(String string) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
dataSource.getXpathComments().removeComment(string);
return this;
}
/**
* @param draftStatus TODO
*/
public CLDRFile makeDraft(DraftStatus draftStatus) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
for (Iterator<String> it = dataSource.iterator(); it.hasNext(); ) {
String path = it.next();
XPathParts parts =
XPathParts.getFrozenInstance(dataSource.getFullPath(path))
.cloneAsThawed(); // not frozen, for addAttribute
parts.addAttribute("draft", draftStatus.toString());
dataSource.putValueAtPath(parts.toString(), dataSource.getValueAtPath(path));
}
return this;
}
public UnicodeSet getExemplarSet(String type, WinningChoice winningChoice) {
return getExemplarSet(type, winningChoice, UnicodeSet.CASE);
}
public UnicodeSet getExemplarSet(ExemplarType type, WinningChoice winningChoice) {
return getExemplarSet(type, winningChoice, UnicodeSet.CASE);
}
static final UnicodeSet HACK_CASE_CLOSURE_SET =
new UnicodeSet(
"[ſẛffẞ{i̇}\u1F71\u1F73\u1F75\u1F77\u1F79\u1F7B\u1F7D\u1FBB\u1FBE\u1FC9\u1FCB\u1FD3\u1FDB\u1FE3\u1FEB\u1FF9\u1FFB\u2126\u212A\u212B]")
.freeze();
public enum ExemplarType {
main,
auxiliary,
index,
punctuation,
numbers;
public static ExemplarType fromString(String type) {
return type.isEmpty() ? main : valueOf(type);
}
}
public UnicodeSet getExemplarSet(String type, WinningChoice winningChoice, int option) {
return getExemplarSet(ExemplarType.fromString(type), winningChoice, option);
}
public UnicodeSet getExemplarSet(ExemplarType type, WinningChoice winningChoice, int option) {
UnicodeSet result = getRawExemplarSet(type, winningChoice);
if (result.isEmpty()) {
return result.cloneAsThawed();
}
UnicodeSet toNuke = new UnicodeSet(HACK_CASE_CLOSURE_SET).removeAll(result);
result.closeOver(UnicodeSet.CASE);
result.removeAll(toNuke);
result.remove(0x20);
return result;
}
public UnicodeSet getRawExemplarSet(ExemplarType type, WinningChoice winningChoice) {
String path = getExemplarPath(type);
if (winningChoice == WinningChoice.WINNING) {
path = getWinningPath(path);
}
String v = getStringValueWithBailey(path);
if (v == null) {
return UnicodeSet.EMPTY;
}
UnicodeSet result = SimpleUnicodeSetFormatter.parseLenient(v);
return result;
}
public static String getExemplarPath(ExemplarType type) {
return "//ldml/characters/exemplarCharacters"
+ (type == ExemplarType.main ? "" : "[@type=\"" + type + "\"]");
}
public enum NumberingSystem {
latin(null),
defaultSystem("//ldml/numbers/defaultNumberingSystem"),
nativeSystem("//ldml/numbers/otherNumberingSystems/native"),
traditional("//ldml/numbers/otherNumberingSystems/traditional"),
finance("//ldml/numbers/otherNumberingSystems/finance");
public final String path;
private NumberingSystem(String path) {
this.path = path;
}
}
public UnicodeSet getExemplarsNumeric(NumberingSystem system) {
String numberingSystem = system.path == null ? "latn" : getStringValue(system.path);
if (numberingSystem == null) {
return UnicodeSet.EMPTY;
}
return getExemplarsNumeric(numberingSystem);
}
public UnicodeSet getExemplarsNumeric(String numberingSystem) {
UnicodeSet result = new UnicodeSet();
SupplementalDataInfo sdi = CLDRConfig.getInstance().getSupplementalDataInfo();
String[] symbolPaths = {
"decimal", "group", "percentSign", "perMille", "plusSign", "minusSign",
// "infinity"
};
String digits = sdi.getDigits(numberingSystem);
if (digits != null) { // TODO, get other characters, see ticket:8316
result.addAll(digits);
}
for (String path : symbolPaths) {
String fullPath =
"//ldml/numbers/symbols[@numberSystem=\"" + numberingSystem + "\"]/" + path;
String value = getStringValue(fullPath);
if (value != null) {
result.add(value);
}
}
return result;
}
public String getCurrentMetazone(String zone) {
for (Iterator<String> it2 = iterator(); it2.hasNext(); ) {
String xpath = it2.next();
if (xpath.startsWith(
"//ldml/dates/timeZoneNames/zone[@type=\"" + zone + "\"]/usesMetazone")) {
XPathParts parts = XPathParts.getFrozenInstance(xpath);
if (!parts.containsAttribute("to")) {
return parts.getAttributeValue(4, "mzone");
}
}
}
return null;
}
public boolean isResolved() {
return dataSource.isResolving();
}
// WARNING: this must go AFTER attributeOrdering is set; otherwise it uses a null comparator!!
/*
* TODO: clarify the warning. There is nothing named "attributeOrdering" in this file.
* This member distinguishedXPath is accessed only by the function getNonDistinguishingAttributes.
*/
private static final DistinguishedXPath distinguishedXPath = new DistinguishedXPath();
public static final String distinguishedXPathStats() {
return DistinguishedXPath.stats();
}
private static class DistinguishedXPath {
public static final String stats() {
return "distinguishingMap:"
+ distinguishingMap.size()
+ " "
+ "normalizedPathMap:"
+ normalizedPathMap.size();
}
private static Map<String, String> distinguishingMap = new ConcurrentHashMap<>();
private static Map<String, String> normalizedPathMap = new ConcurrentHashMap<>();
static {
distinguishingMap.put("", ""); // seed this to make the code simpler
}
public static String getDistinguishingXPath(String xpath, String[] normalizedPath) {
// For example, this removes [@xml:space="preserve"] from a path with element
// foreignSpaceReplacement.
// synchronized (distinguishingMap) {
String result = distinguishingMap.get(xpath);
if (result == null) {
XPathParts distinguishingParts =
XPathParts.getFrozenInstance(xpath)
.cloneAsThawed(); // not frozen, for removeAttributes
DtdType type = distinguishingParts.getDtdData().dtdType;
Set<String> toRemove = new HashSet<>();
// first clean up draft and alt
String draft = null;
String alt = null;
String references = "";
// note: we only need to clean up items that are NOT on the last element,
// so we go up to size() - 1.
// note: each successive item overrides the previous one. That's intended
for (int i = 0; i < distinguishingParts.size() - 1; ++i) {
if (distinguishingParts.getAttributeCount(i) == 0) {
continue;
}
toRemove.clear();
Map<String, String> attributes = distinguishingParts.getAttributes(i);
for (String attribute : attributes.keySet()) {
if (attribute.equals("draft")) {
draft = attributes.get(attribute);
toRemove.add(attribute);
} else if (attribute.equals("alt")) {
alt = attributes.get(attribute);
toRemove.add(attribute);
} else if (attribute.equals("references")) {
if (references.length() != 0) references += " ";
references += attributes.get("references");
toRemove.add(attribute);
}
}
distinguishingParts.removeAttributes(i, toRemove);
}
if (draft != null || alt != null || references.length() != 0) {
// get the last element that is not ordered.
int placementIndex = distinguishingParts.size() - 1;
while (true) {
String element = distinguishingParts.getElement(placementIndex);
if (!DtdData.getInstance(type).isOrdered(element)) break;
--placementIndex;
}
if (draft != null) {
distinguishingParts.putAttributeValue(placementIndex, "draft", draft);
}
if (alt != null) {
distinguishingParts.putAttributeValue(placementIndex, "alt", alt);
}
if (references.length() != 0) {
distinguishingParts.putAttributeValue(
placementIndex, "references", references);
}
String newXPath = distinguishingParts.toString();
if (!newXPath.equals(xpath)) {
normalizedPathMap.put(xpath, newXPath); // store differences
}
}
// now remove non-distinguishing attributes (if non-inheriting)
for (int i = 0; i < distinguishingParts.size(); ++i) {
if (distinguishingParts.getAttributeCount(i) == 0) {
continue;
}
String element = distinguishingParts.getElement(i);
toRemove.clear();
for (String attribute : distinguishingParts.getAttributeKeys(i)) {
if (!isDistinguishing(type, element, attribute)) {
toRemove.add(attribute);
}
}
distinguishingParts.removeAttributes(i, toRemove);
}
result = distinguishingParts.toString();
if (result.equals(xpath)) { // don't save the copy if we don't have to.
result = xpath;
}
distinguishingMap.put(xpath, result);
}
if (normalizedPath != null) {
normalizedPath[0] = normalizedPathMap.get(xpath);
if (normalizedPath[0] == null) {
normalizedPath[0] = xpath;
}
}
return result;
}
public Map<String, String> getNonDistinguishingAttributes(
String fullPath, Map<String, String> result, Set<String> skipList) {
if (result == null) {
result = new LinkedHashMap<>();
} else {
result.clear();
}
XPathParts distinguishingParts = XPathParts.getFrozenInstance(fullPath);
DtdType type = distinguishingParts.getDtdData().dtdType;
for (int i = 0; i < distinguishingParts.size(); ++i) {
String element = distinguishingParts.getElement(i);
Map<String, String> attributes = distinguishingParts.getAttributes(i);
for (Iterator<String> it = attributes.keySet().iterator(); it.hasNext(); ) {
String attribute = it.next();
if (!isDistinguishing(type, element, attribute)
&& !skipList.contains(attribute)) {
result.put(attribute, attributes.get(attribute));
}
}
}
return result;
}
}
/** Fillin value for {@link CLDRFile#getSourceLocaleID(String, Status)} */
public static class Status {
/**
* XPath where originally found. May be {@link GlossonymConstructor#PSEUDO_PATH} if the
* value was constructed.
*
* @see GlossonymnConstructor
*/
public String pathWhereFound;
@Override
public String toString() {
return pathWhereFound;
}
}
public static boolean isLOG_PROGRESS() {
return LOG_PROGRESS;
}
public static void setLOG_PROGRESS(boolean log_progress) {
LOG_PROGRESS = log_progress;
}
public boolean isEmpty() {
return !dataSource.iterator().hasNext();
}
public Map<String, String> getNonDistinguishingAttributes(
String fullPath, Map<String, String> result, Set<String> skipList) {
return distinguishedXPath.getNonDistinguishingAttributes(fullPath, result, skipList);
}
public String getDtdVersion() {
return dataSource.getDtdVersionInfo().toString();
}
public VersionInfo getDtdVersionInfo() {
VersionInfo result = dataSource.getDtdVersionInfo();
if (result != null || isEmpty()) {
return result;
}
// for old files, pick the version from the @version attribute
String path = dataSource.iterator().next();
String full = getFullXPath(path);
XPathParts parts = XPathParts.getFrozenInstance(full);
String versionString = parts.findFirstAttributeValue("version");
return versionString == null ? null : VersionInfo.getInstance(versionString);
}
private boolean contains(Map<String, String> a, Map<String, String> b) {
for (Iterator<String> it = b.keySet().iterator(); it.hasNext(); ) {
String key = it.next();
String otherValue = a.get(key);
if (otherValue == null) {
return false;
}
String value = b.get(key);
if (!otherValue.equals(value)) {
return false;
}
}
return true;
}
public String getFullXPath(String path, boolean ignoreOtherLeafAttributes) {
String result = getFullXPath(path);
if (result != null) return result;
XPathParts parts = XPathParts.getFrozenInstance(path);
Map<String, String> lastAttributes = parts.getAttributes(parts.size() - 1);
String base =
parts.toString(parts.size() - 1)
+ "/"
+ parts.getElement(parts.size() - 1); // trim final element
for (Iterator<String> it = iterator(base); it.hasNext(); ) {
String otherPath = it.next();
XPathParts other = XPathParts.getFrozenInstance(otherPath);
if (other.size() != parts.size()) {
continue;
}
Map<String, String> lastOtherAttributes = other.getAttributes(other.size() - 1);
if (!contains(lastOtherAttributes, lastAttributes)) {
continue;
}
if (result == null) {
result = getFullXPath(otherPath);
} else {
throw new IllegalArgumentException("Multiple values for path: " + path);
}
}
return result;
}
/**
* Return true if this item is the "winner" in the survey tool
*
* @param path
* @return
*/
public boolean isWinningPath(String path) {
return dataSource.isWinningPath(path);
}
/**
* Returns the "winning" path, for use in the survey tool tests, out of all those paths that
* only differ by having "alt proposed". The exact meaning may be tweaked over time, but the
* user's choice (vote) has precedence, then any undisputed choice, then the "best" choice of
* the remainders. A value is always returned if there is a valid path, and the returned value
* is always a valid path <i>in the resolved file</i>; that is, it may be valid in the parent,
* or valid because of aliasing.
*
* @param path
* @return path, perhaps with an alt proposed added.
*/
public String getWinningPath(String path) {
return dataSource.getWinningPath(path);
}
/**
* Shortcut for getting the string value for the winning path
*
* @param path
* @return
*/
public String getWinningValue(String path) {
final String winningPath = getWinningPath(path);
return winningPath == null ? null : getStringValue(winningPath);
}
/**
* Shortcut for getting the string value for the winning path. If the winning value is an {@link
* CldrUtility#INHERITANCE_MARKER} (used in survey tool), then the Bailey value is returned.
*
* @param path
* @return the winning value
*/
public String getWinningValueWithBailey(String path) {
final String winningPath = getWinningPath(path);
return winningPath == null ? null : getStringValueWithBailey(winningPath);
}
/**
* Shortcut for getting the string value for a path. If the string value is an {@link
* CldrUtility#INHERITANCE_MARKER} (used in survey tool), then the Bailey value is returned.
*
* @param path
* @return the string value
*/
public String getStringValueWithBailey(String path) {
return getStringValueWithBailey(path, null, null);
}
/**
* Shortcut for getting the string value for a path. If the string value is an {@link
* CldrUtility#INHERITANCE_MARKER} (used in survey tool), then the Bailey value is returned.
*
* @param path the given xpath
* @param pathWhereFound if not null, to be filled in with the path where the value is actually
* found. May be {@link GlossonymConstructor#PSEUDO_PATH} if constructed.
* @param localeWhereFound if not null, to be filled in with the locale where the value is
* actually found. May be {@link XMLSource#CODE_FALLBACK_ID} if not in root.
* @return the string value
*/
public String getStringValueWithBailey(
String path, Output<String> pathWhereFound, Output<String> localeWhereFound) {
String value = getStringValue(path);
if (CldrUtility.INHERITANCE_MARKER.equals(value)) {
value = getBaileyValue(path, pathWhereFound, localeWhereFound);
} else if (localeWhereFound != null || pathWhereFound != null) {
final Status status = new Status();
final String localeWhereFound2 = getSourceLocaleID(path, status);
if (localeWhereFound != null) {
localeWhereFound.value = localeWhereFound2;
}
if (pathWhereFound != null) {
pathWhereFound.value = status.pathWhereFound;
}
}
return value;
}
/**
* Return the distinguished paths that have the specified value. The pathPrefix and pathMatcher
* can be used to restrict the returned paths to those matching. The pathMatcher can be null
* (equals .*).
*
* @param valueToMatch
* @param pathPrefix
* @return
*/
public Set<String> getPathsWithValue(
String valueToMatch, String pathPrefix, Matcher pathMatcher, Set<String> result) {
if (result == null) {
result = new HashSet<>();
}
dataSource.getPathsWithValue(valueToMatch, pathPrefix, result);
if (pathMatcher == null) {
return result;
}
for (Iterator<String> it = result.iterator(); it.hasNext(); ) {
String path = it.next();
if (!pathMatcher.reset(path).matches()) {
it.remove();
}
}
return result;
}
/**
* Return the distinguished paths that match the pathPrefix and pathMatcher The pathMatcher can
* be null (equals .*).
*/
public Set<String> getPaths(String pathPrefix, Matcher pathMatcher, Set<String> result) {
if (result == null) {
result = new HashSet<>();
}
for (Iterator<String> it = dataSource.iterator(pathPrefix); it.hasNext(); ) {
String path = it.next();
if (pathMatcher != null && !pathMatcher.reset(path).matches()) {
continue;
}
result.add(path);
}
return result;
}
public enum WinningChoice {
NORMAL,
WINNING
}
/**
* Used in TestUser to get the "winning" path. Simple implementation just for testing.
*
* @author markdavis
*/
static class WinningComparator implements Comparator<String> {
String user;
public WinningComparator(String user) {
this.user = user;
}
/**
* if it contains the user, sort first. Otherwise use normal string sorting. A better
* implementation would look at the number of votes next, and whither there was an approved
* or provisional path.
*/
@Override
public int compare(String o1, String o2) {
if (o1.contains(user)) {
if (!o2.contains(user)) {
return -1; // if it contains user
}
} else if (o2.contains(user)) {
return 1; // if it contains user
}
return o1.compareTo(o2);
}
}
/**
* This is a test class used to simulate what the survey tool would do.
*
* @author markdavis
*/
public static class TestUser extends CLDRFile {
Map<String, String> userOverrides = new HashMap<>();
public TestUser(CLDRFile baseFile, String user, boolean resolved) {
super(resolved ? baseFile.dataSource : baseFile.dataSource.getUnresolving());
if (!baseFile.isResolved()) {
throw new IllegalArgumentException("baseFile must be resolved");
}
Relation<String, String> pathMap =
Relation.of(
new HashMap<String, Set<String>>(),
TreeSet.class,
new WinningComparator(user));
for (String path : baseFile) {
String newPath = getNondraftNonaltXPath(path);
pathMap.put(newPath, path);
}
// now reduce the storage by just getting the winning ones
// so map everything but the first path to the first path
for (String path : pathMap.keySet()) {
String winner = null;
for (String rowPath : pathMap.getAll(path)) {
if (winner == null) {
winner = rowPath;
continue;
}
userOverrides.put(rowPath, winner);
}
}
}
@Override
public String getWinningPath(String path) {
String trial = userOverrides.get(path);
if (trial != null) {
return trial;
}
return path;
}
}
/**
* Returns the extra paths, skipping those that are already represented in the locale.
*
* @return
*/
public Collection<String> getExtraPaths() {
Set<String> toAddTo = new HashSet<>();
toAddTo.addAll(getRawExtraPaths());
for (String path : this) {
toAddTo.remove(path);
}
return toAddTo;
}
/**
* Returns the extra paths, skipping those that are already represented in the locale.
*
* @return
*/
public Collection<String> getExtraPaths(String prefix, Collection<String> toAddTo) {
for (String item : getRawExtraPaths()) {
if (item.startsWith(prefix)
&& dataSource.getValueAtPath(item) == null) { // don't use getStringValue, since
// it recurses.
toAddTo.add(item);
}
}
return toAddTo;
}
// extraPaths contains the raw extra paths.
// It requires filtering in those cases where we don't want duplicate paths.
/**
* Returns the raw extra paths, irrespective of what paths are already represented in the
* locale.
*
* @return
*/
public Set<String> getRawExtraPaths() {
if (extraPaths == null) {
extraPaths =
ImmutableSet.<String>builder()
.addAll(getRawExtraPathsPrivate())
.addAll(CONST_EXTRA_PATHS)
.build();
if (DEBUG) {
System.out.println(getLocaleID() + "\textras: " + extraPaths.size());
}
}
return extraPaths;
}
/**
* Add (possibly over four thousand) extra paths to the given collection. These are paths that
* typically don't have a reasonable fallback value that could be added to root. Some of them
* are common to all locales, and some of them are specific to the given locale, based on
* features like the plural rules for the locale.
*
* <p>The ones that are constant for all locales should go into CONST_EXTRA_PATHS.
*
* @return toAddTo (the collection)
* <p>Called only by getRawExtraPaths.
* <p>"Raw" refers to the fact that some of the paths may duplicate paths that are already
* in this CLDRFile (in the xml and/or votes), in which case they will later get filtered by
* getExtraPaths (removed from toAddTo) rather than re-added.
* <p>NOTE: values may be null for some "extra" paths in locales for which no explicit
* values have been submitted. Both unit tests and Survey Tool client code generate errors
* or warnings for null value, but allow null value for certain exceptional extra paths. See
* the functions named extraPathAllowsNullValue in TestPaths.java and in the JavaScript
* client code. Make sure that updates here are reflected there and vice versa.
* <p>Reference: https://unicode-org.atlassian.net/browse/CLDR-11238
*/
private List<String> getRawExtraPathsPrivate() {
Set<String> toAddTo = new HashSet<>();
SupplementalDataInfo supplementalData = CLDRConfig.getInstance().getSupplementalDataInfo();
// units
PluralInfo plurals = supplementalData.getPlurals(PluralType.cardinal, getLocaleID());
if (plurals == null && DEBUG) {
System.err.println(
"No "
+ PluralType.cardinal
+ " plurals for "
+ getLocaleID()
+ " in "
+ supplementalData.getDirectory().getAbsolutePath());
}
Set<Count> pluralCounts = Collections.emptySet();
if (plurals != null) {
pluralCounts = plurals.getAdjustedCounts();
Set<Count> pluralCountsRaw = plurals.getCounts();
if (pluralCountsRaw.size() != 1) {
// we get all the root paths with count
addPluralCounts(toAddTo, pluralCounts, pluralCountsRaw, this);
}
}
// dayPeriods
String locale = getLocaleID();
DayPeriodInfo dayPeriods =
supplementalData.getDayPeriods(DayPeriodInfo.Type.format, locale);
if (dayPeriods != null) {
LinkedHashSet<DayPeriod> items = new LinkedHashSet<>(dayPeriods.getPeriods());
items.add(DayPeriod.am);
items.add(DayPeriod.pm);
for (String context : new String[] {"format", "stand-alone"}) {
for (String width : new String[] {"narrow", "abbreviated", "wide"}) {
for (DayPeriod dayPeriod : items) {
// ldml/dates/calendars/calendar[@type="gregorian"]/dayPeriods/dayPeriodContext[@type="format"]/dayPeriodWidth[@type="wide"]/dayPeriod[@type="am"]
toAddTo.add(
"//ldml/dates/calendars/calendar[@type=\"gregorian\"]/dayPeriods/"
+ "dayPeriodContext[@type=\""
+ context
+ "\"]/dayPeriodWidth[@type=\""
+ width
+ "\"]/dayPeriod[@type=\""
+ dayPeriod
+ "\"]");
}
}
}
}
// metazones
Set<String> zones = supplementalData.getAllMetazones();
for (String zone : zones) {
final boolean metazoneUsesDST = CheckMetazones.metazoneUsesDST(zone);
for (String width : new String[] {"long", "short"}) {
for (String type : new String[] {"generic", "standard", "daylight"}) {
if (metazoneUsesDST || type.equals("standard")) {
// Only add /standard for non-DST metazones
final String path =
"//ldml/dates/timeZoneNames/metazone[@type=\""
+ zone
+ "\"]/"
+ width
+ "/"
+ type;
toAddTo.add(path);
}
}
}
}
// // Individual zone overrides
// final String[] overrides = {
// "Pacific/Honolulu\"]/short/generic",
// "Pacific/Honolulu\"]/short/standard",
// "Pacific/Honolulu\"]/short/daylight",
// "Europe/Dublin\"]/long/daylight",
// "Europe/London\"]/long/daylight",
// "Etc/UTC\"]/long/standard",
// "Etc/UTC\"]/short/standard"
// };
// for (String override : overrides) {
// toAddTo.add("//ldml/dates/timeZoneNames/zone[@type=\"" + override);
// }
// Currencies
Set<String> codes = supplementalData.getBcp47Keys().getAll("cu");
for (String code : codes) {
String currencyCode = code.toUpperCase();
toAddTo.add(
"//ldml/numbers/currencies/currency[@type=\"" + currencyCode + "\"]/symbol");
toAddTo.add(
"//ldml/numbers/currencies/currency[@type=\""
+ currencyCode
+ "\"]/displayName");
if (!pluralCounts.isEmpty()) {
for (Count count : pluralCounts) {
toAddTo.add(
"//ldml/numbers/currencies/currency[@type=\""
+ currencyCode
+ "\"]/displayName[@count=\""
+ count.toString()
+ "\"]");
}
}
}
// grammatical info
GrammarInfo grammarInfo = supplementalData.getGrammarInfo(getLocaleID(), true);
if (grammarInfo != null) {
if (grammarInfo.hasInfo(GrammaticalTarget.nominal)) {
Collection<String> genders =
grammarInfo.get(
GrammaticalTarget.nominal,
GrammaticalFeature.grammaticalGender,
GrammaticalScope.units);
Collection<String> rawCases =
grammarInfo.get(
GrammaticalTarget.nominal,
GrammaticalFeature.grammaticalCase,
GrammaticalScope.units);
Collection<String> nomCases = rawCases.isEmpty() ? casesNominativeOnly : rawCases;
Collection<Count> adjustedPlurals = pluralCounts;
// There was code here allowing fewer plurals to be used, but is retracted for now
// (needs more thorough integration in logical groups, etc.)
// This note is left for 'blame' to find the old code in case we revive that.
// TODO use UnitPathType to get paths
if (!genders.isEmpty()) {
for (String unit : GrammarInfo.getUnitsToAddGrammar()) {
toAddTo.add(
"//ldml/units/unitLength[@type=\"long\"]/unit[@type=\""
+ unit
+ "\"]/gender");
}
for (Count plural : adjustedPlurals) {
for (String gender : genders) {
for (String case1 : nomCases) {
final String grammaticalAttributes =
GrammarInfo.getGrammaticalInfoAttributes(
grammarInfo,
UnitPathType.power,
plural.toString(),
gender,
case1);
toAddTo.add(
"//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power2\"]/compoundUnitPattern1"
+ grammaticalAttributes);
toAddTo.add(
"//ldml/units/unitLength[@type=\"long\"]/compoundUnit[@type=\"power3\"]/compoundUnitPattern1"
+ grammaticalAttributes);
}
}
}
// <genderMinimalPairs gender="masculine">Der {0} ist
// …</genderMinimalPairs>
for (String gender : genders) {
toAddTo.add(
"//ldml/numbers/minimalPairs/genderMinimalPairs[@gender=\""
+ gender
+ "\"]");
}
}
if (!rawCases.isEmpty()) {
for (String case1 : rawCases) {
// <caseMinimalPairs case="nominative">{0} kostet
// €3,50.</caseMinimalPairs>
toAddTo.add(
"//ldml/numbers/minimalPairs/caseMinimalPairs[@case=\""
+ case1
+ "\"]");
for (Count plural : adjustedPlurals) {
for (String unit : GrammarInfo.getUnitsToAddGrammar()) {
toAddTo.add(
"//ldml/units/unitLength[@type=\"long\"]/unit[@type=\""
+ unit
+ "\"]/unitPattern"
+ GrammarInfo.getGrammaticalInfoAttributes(
grammarInfo,
UnitPathType.unit,
plural.toString(),
null,
case1));
}
}
}
}
}
}
return toAddTo.stream().map(String::intern).collect(Collectors.toList());
}
private void addPluralCounts(
Collection<String> toAddTo,
final Set<Count> pluralCounts,
final Set<Count> pluralCountsRaw,
Iterable<String> file) {
for (String path : file) {
String countAttr = "[@count=\"other\"]";
int countPos = path.indexOf(countAttr);
if (countPos < 0) {
continue;
}
Set<Count> pluralCountsNeeded =
path.startsWith("//ldml/numbers/minimalPairs") ? pluralCountsRaw : pluralCounts;
if (pluralCountsNeeded.size() > 1) {
String start = path.substring(0, countPos) + "[@count=\"";
String end = "\"]" + path.substring(countPos + countAttr.length());
for (Count count : pluralCounts) {
if (count == Count.other) {
continue;
}
toAddTo.add(start + count + end);
}
}
}
}
/**
* Get the path with the given count, case, or gender, with fallback. The fallback acts like an
* alias in root.
*
* <p>Count:
*
* <p>It acts like there is an alias in root from count=n to count=one, then for currency
* display names from count=one to no count <br>
* For unitPatterns, falls back to Count.one. <br>
* For others, falls back to Count.one, then no count.
*
* <p>Case
*
* <p>The fallback is to no case, which = nominative.
*
* <p>Case
*
* <p>The fallback is to no case, which = nominative.
*
* @param xpath
* @param count Count may be null. Returns null if nothing is found.
* @param winning TODO
* @return
*/
public String getCountPathWithFallback(String xpath, Count count, boolean winning) {
String result;
XPathParts parts =
XPathParts.getFrozenInstance(xpath)
.cloneAsThawed(); // not frozen, addAttribute in getCountPathWithFallback2
// In theory we should do all combinations of gender, case, count (and eventually
// definiteness), but for simplicity
// we just successively try "zeroing" each one in a set order.
// tryDefault modifies the parts in question
Output<String> newPath = new Output<>();
if (tryDefault(parts, "gender", null, newPath)) {
return newPath.value;
}
if (tryDefault(parts, "case", null, newPath)) {
return newPath.value;
}
boolean isDisplayName = parts.contains("displayName");
String actualCount = parts.getAttributeValue(-1, "count");
if (actualCount != null) {
if (CldrUtility.DIGITS.containsAll(actualCount)) {
try {
int item = Integer.parseInt(actualCount);
String locale = getLocaleID();
SupplementalDataInfo sdi = CLDRConfig.getInstance().getSupplementalDataInfo();
PluralRules rules =
sdi.getPluralRules(
new ULocale(locale), PluralRules.PluralType.CARDINAL);
String keyword = rules.select(item);
Count itemCount = Count.valueOf(keyword);
result = getCountPathWithFallback2(parts, xpath, itemCount, winning);
if (result != null && isNotRoot(result)) {
return result;
}
} catch (NumberFormatException e) {
}
}
// try the given count first
result = getCountPathWithFallback2(parts, xpath, count, winning);
if (result != null && isNotRoot(result)) {
return result;
}
// now try fallback
if (count != Count.other) {
result = getCountPathWithFallback2(parts, xpath, Count.other, winning);
if (result != null && isNotRoot(result)) {
return result;
}
}
// now try deletion (for currency)
if (isDisplayName) {
result = getCountPathWithFallback2(parts, xpath, null, winning);
}
return result;
}
return null;
}
/**
* Modify the parts by setting the attribute in question to the default value (typically null to
* clear). If there is a value for that path, use it.
*/
public boolean tryDefault(
XPathParts parts, String attribute, String defaultValue, Output<String> newPath) {
String oldValue = parts.getAttributeValue(-1, attribute);
if (oldValue != null) {
parts.setAttribute(-1, attribute, null);
newPath.value = parts.toString();
if (dataSource.getValueAtPath(newPath.value) != null) {
return true;
}
}
return false;
}
private String getCountPathWithFallback2(
XPathParts parts, String xpathWithNoCount, Count count, boolean winning) {
parts.addAttribute("count", count == null ? null : count.toString());
String newPath = parts.toString();
if (!newPath.equals(xpathWithNoCount)) {
if (winning) {
String temp = getWinningPath(newPath);
if (temp != null) {
newPath = temp;
}
}
if (dataSource.getValueAtPath(newPath) != null) {
return newPath;
}
// return getWinningPath(newPath);
}
return null;
}
/**
* Returns a value to be used for "filling in" a "Change" value in the survey tool. Currently
* returns the following.
*
* <ul>
* <li>The "winning" value (if not inherited). Example: if "Donnerstag" has the most votes for
* 'thursday', then clicking on the empty field will fill in "Donnerstag"
* <li>The singular form. Example: if the value for 'hour' is "heure", then clicking on the
* entry field for 'hours' will insert "heure".
* <li>The parent's value. Example: if I'm in [de_CH] and there are no proposals for
* 'thursday', then clicking on the empty field will fill in "Donnerstag" from [de].
* <li>Otherwise don't fill in anything, and return null.
* </ul>
*
* @return
*/
public String getFillInValue(String distinguishedPath) {
String winningPath = getWinningPath(distinguishedPath);
if (isNotRoot(winningPath)) {
return getStringValue(winningPath);
}
String fallbackPath = getFallbackPath(winningPath, true, true);
if (fallbackPath != null) {
String value = getWinningValue(fallbackPath);
if (value != null) {
return value;
}
}
return getStringValue(winningPath);
}
/**
* returns true if the source of the path exists, and is neither root nor code-fallback
*
* @param distinguishedPath
* @return
*/
public boolean isNotRoot(String distinguishedPath) {
String source = getSourceLocaleID(distinguishedPath, null);
return source != null
&& !source.equals("root")
&& !source.equals(XMLSource.CODE_FALLBACK_ID);
}
public boolean isAliasedAtTopLevel() {
return iterator("//ldml/alias").hasNext();
}
public static Comparator<String> getComparator(DtdType dtdType) {
if (dtdType == null) {
return ldmlComparator;
}
switch (dtdType) {
case ldml:
case ldmlICU:
return ldmlComparator;
default:
return DtdData.getInstance(dtdType).getDtdComparator(null);
}
}
public Comparator<String> getComparator() {
return getComparator(dtdType);
}
public DtdType getDtdType() {
return dtdType != null ? dtdType : dataSource.getDtdType();
}
public DtdData getDtdData() {
return dtdData != null ? dtdData : DtdData.getInstance(getDtdType());
}
public static Comparator<String> getPathComparator(String path) {
DtdType fileDtdType = DtdType.fromPath(path);
return getComparator(fileDtdType);
}
public static MapComparator<String> getAttributeOrdering() {
return DtdData.getInstance(DtdType.ldmlICU).getAttributeComparator();
}
public CLDRFile getUnresolved() {
if (!isResolved()) {
return this;
}
XMLSource source = dataSource.getUnresolving();
return new CLDRFile(source);
}
public static Comparator<String> getAttributeValueComparator(String element, String attribute) {
return DtdData.getAttributeValueComparator(DtdType.ldml, element, attribute);
}
public void setDtdType(DtdType dtdType) {
if (locked) throw new UnsupportedOperationException("Attempt to modify locked object");
this.dtdType = dtdType;
}
public void disableCaching() {
dataSource.disableCaching();
}
/**
* Get a constructed value for the given path, if it is a path for which values can be
* constructed
*
* @param xpath the given path, such as
* //ldml/localeDisplayNames/languages/language[@type="zh_Hans"]
* @return the constructed value, or null if this path doesn't have a constructed value
*/
public String getConstructedValue(String xpath) {
if (isResolved() && GlossonymConstructor.pathIsEligible(xpath)) {
return new GlossonymConstructor(this).getValue(xpath);
}
return null;
}
/**
* Get the string value for the given path in this locale, without resolving to any other path
* or locale.
*
* @param xpath the given path
* @return the string value, unresolved
*/
private String getStringValueUnresolved(String xpath) {
CLDRFile sourceFileUnresolved = this.getUnresolved();
return sourceFileUnresolved.getStringValue(xpath);
}
/**
* Create an overriding LocaleStringProvider for testing and example generation
*
* @param pathAndValueOverrides
* @return
*/
public LocaleStringProvider makeOverridingStringProvider(
Map<String, String> pathAndValueOverrides) {
return new OverridingStringProvider(pathAndValueOverrides);
}
public class OverridingStringProvider implements LocaleStringProvider {
private final Map<String, String> pathAndValueOverrides;
public OverridingStringProvider(Map<String, String> pathAndValueOverrides) {
this.pathAndValueOverrides = pathAndValueOverrides;
}
@Override
public String getStringValue(String xpath) {
String value = pathAndValueOverrides.get(xpath);
return value != null ? value : CLDRFile.this.getStringValue(xpath);
}
@Override
public String getLocaleID() {
return CLDRFile.this.getLocaleID();
}
@Override
public String getSourceLocaleID(String xpath, Status status) {
if (pathAndValueOverrides.containsKey(xpath)) {
if (status != null) {
status.pathWhereFound = xpath;
}
return getLocaleID() + "-override";
}
return CLDRFile.this.getSourceLocaleID(xpath, status);
}
}
public String getKeyName(String key) {
String result = getStringValue("//ldml/localeDisplayNames/keys/key[@type=\"" + key + "\"]");
if (result == null) {
Relation<R2<String, String>, String> toAliases =
SupplementalDataInfo.getInstance().getBcp47Aliases();
Set<String> aliases = toAliases.get(Row.of(key, ""));
if (aliases != null) {
for (String alias : aliases) {
result =
getStringValue(
"//ldml/localeDisplayNames/keys/key[@type=\"" + alias + "\"]");
if (result != null) {
break;
}
}
}
}
return result;
}
public String getKeyValueName(String key, String value) {
String result =
getStringValue(
"//ldml/localeDisplayNames/types/type[@key=\""
+ key
+ "\"][@type=\""
+ value
+ "\"]");
if (result == null) {
Relation<R2<String, String>, String> toAliases =
SupplementalDataInfo.getInstance().getBcp47Aliases();
Set<String> keyAliases = toAliases.get(Row.of(key, ""));
Set<String> valueAliases = toAliases.get(Row.of(key, value));
if (keyAliases != null || valueAliases != null) {
if (keyAliases == null) {
keyAliases = Collections.singleton(key);
}
if (valueAliases == null) {
valueAliases = Collections.singleton(value);
}
for (String keyAlias : keyAliases) {
for (String valueAlias : valueAliases) {
result =
getStringValue(
"//ldml/localeDisplayNames/types/type[@key=\""
+ keyAlias
+ "\"][@type=\""
+ valueAlias
+ "\"]");
if (result != null) {
break;
}
}
}
}
}
return result;
}
/*
*******************************************************************************************
* TODO: move the code below here -- that is, the many (currently ten as of 2022-06-01)
* versions of getName and their subroutines and data -- to a new class in a separate file,
* and enable tracking similar to existing "pathWhereFound/localeWhereFound" but more general.
*
* Reference: https://unicode-org.atlassian.net/browse/CLDR-15830
*******************************************************************************************
*/
static final Joiner JOIN_HYPHEN = Joiner.on('-');
static final Joiner JOIN_UNDERBAR = Joiner.on('_');
/** Utility for getting a name, given a type and code. */
public String getName(String type, String code) {
return getName(typeNameToCode(type), code);
}
public String getName(int type, String code) {
return getName(type, code, null, null);
}
public String getName(int type, String code, Set<String> paths) {
return getName(type, code, null, paths);
}
public String getName(int type, String code, Transform<String, String> altPicker) {
return getName(type, code, altPicker, null);
}
/**
* Returns the name of the given bcp47 identifier. Note that extensions must be specified using
* the old "\@key=type" syntax.
*
* @param localeOrTZID
* @return
*/
public synchronized String getName(String localeOrTZID) {
return getName(localeOrTZID, false);
}
public String getName(
LanguageTagParser lparser,
boolean onlyConstructCompound,
Transform<String, String> altPicker) {
return getName(lparser, onlyConstructCompound, altPicker, null);
}
/**
* @param paths if non-null, will contain contributory paths on return
*/
public String getName(
LanguageTagParser lparser,
boolean onlyConstructCompound,
Transform<String, String> altPicker,
Set<String> paths) {
return getName(
lparser,
onlyConstructCompound,
altPicker,
getWinningValueWithBailey(GETNAME_LOCALE_KEY_TYPE_PATTERN),
getWinningValueWithBailey(GETNAME_LOCALE_PATTERN),
getWinningValueWithBailey(GETNAME_LOCALE_SEPARATOR),
paths);
}
public synchronized String getName(
String localeOrTZID,
boolean onlyConstructCompound,
String localeKeyTypePattern,
String localePattern,
String localeSeparator) {
return getName(
localeOrTZID,
onlyConstructCompound,
localeKeyTypePattern,
localePattern,
localeSeparator,
null,
null);
}
/**
* Returns the name of the given bcp47 identifier. Note that extensions must be specified using
* the old "\@key=type" syntax.
*
* @param localeOrTZID the locale or timezone ID
* @param onlyConstructCompound
* @return
*/
public synchronized String getName(String localeOrTZID, boolean onlyConstructCompound) {
return getName(localeOrTZID, onlyConstructCompound, null);
}
/**
* Returns the name of the given bcp47 identifier. Note that extensions must be specified using
* the old "\@key=type" syntax.
*
* @param localeOrTZID the locale or timezone ID
* @param onlyConstructCompound if true, returns "English (United Kingdom)" instead of "British
* English"
* @param altPicker Used to select particular alts. For example, SHORT_ALTS can be used to get
* "English (U.K.)" instead of "English (United Kingdom)"
* @return
*/
public synchronized String getName(
String localeOrTZID,
boolean onlyConstructCompound,
Transform<String, String> altPicker) {
return getName(localeOrTZID, onlyConstructCompound, altPicker, null);
}
/**
* Returns the name of the given bcp47 identifier. Note that extensions must be specified using
* the old "\@key=type" syntax.
*
* @param localeOrTZID the locale or timezone ID
* @param onlyConstructCompound if true, returns "English (United Kingdom)" instead of "British
* English"
* @param altPicker Used to select particular alts. For example, SHORT_ALTS can be used to get
* "English (U.K.)" instead of "English (United Kingdom)"
* @return
*/
public synchronized String getName(
String localeOrTZID,
boolean onlyConstructCompound,
Transform<String, String> altPicker,
Set<String> paths) {
return getName(
localeOrTZID,
onlyConstructCompound,
getWinningValueWithBailey(GETNAME_LOCALE_KEY_TYPE_PATTERN),
getWinningValueWithBailey(GETNAME_LOCALE_PATTERN),
getWinningValueWithBailey(GETNAME_LOCALE_SEPARATOR),
altPicker,
paths);
}
/**
* Returns the name of the given bcp47 identifier. Note that extensions must be specified using
* the old "\@key=type" syntax. Only used by ExampleGenerator.
*
* @param localeOrTZID the locale or timezone ID
* @param onlyConstructCompound
* @param localeKeyTypePattern the pattern used to format key-type pairs
* @param localePattern the pattern used to format primary/secondary subtags
* @param localeSeparator the list separator for secondary subtags
* @param paths if non-null, fillin with contributory paths
* @return
*/
public synchronized String getName(
String localeOrTZID,
boolean onlyConstructCompound,
String localeKeyTypePattern,
String localePattern,
String localeSeparator,
Transform<String, String> altPicker,
Set<String> paths) {
// Hack for seed
if (localePattern == null) {
localePattern = "{0} ({1})";
}
boolean isCompound = localeOrTZID.contains("_");
String name =
isCompound && onlyConstructCompound
? null
: getName(LANGUAGE_NAME, localeOrTZID, altPicker, paths);
// TODO - handle arbitrary combinations
if (name != null && !name.contains("_") && !name.contains("-")) {
name = replaceBracketsForName(name);
return name;
}
LanguageTagParser lparser = new LanguageTagParser().set(localeOrTZID);
return getName(
lparser,
onlyConstructCompound,
altPicker,
localeKeyTypePattern,
localePattern,
localeSeparator,
paths);
}
public String getName(
LanguageTagParser lparser,
boolean onlyConstructCompound,
Transform<String, String> altPicker,
String localeKeyTypePattern,
String localePattern,
String localeSeparator,
Set<String> paths) {
String name;
String original = null;
// we need to check for prefixes, for lang+script or lang+country
boolean haveScript = false;
boolean haveRegion = false;
// try lang+script
if (onlyConstructCompound) {
name = getName(LANGUAGE_NAME, original = lparser.getLanguage(), altPicker, paths);
if (name == null) name = original;
} else {
String x = lparser.toString(LanguageTagParser.LANGUAGE_SCRIPT_REGION);
name = getName(LANGUAGE_NAME, x, altPicker, paths);
if (name != null) {
haveScript = haveRegion = true;
} else {
name =
getName(
LANGUAGE_NAME,
lparser.toString(LanguageTagParser.LANGUAGE_SCRIPT),
altPicker,
paths);
if (name != null) {
haveScript = true;
} else {
name =
getName(
LANGUAGE_NAME,
lparser.toString(LanguageTagParser.LANGUAGE_REGION),
altPicker,
paths);
if (name != null) {
haveRegion = true;
} else {
name =
getName(
LANGUAGE_NAME,
original = lparser.getLanguage(),
altPicker,
paths);
if (name == null) {
name = original;
}
}
}
}
}
name = replaceBracketsForName(name);
String extras = "";
if (!haveScript) {
extras =
addDisplayName(
lparser.getScript(),
SCRIPT_NAME,
localeSeparator,
extras,
altPicker,
paths);
}
if (!haveRegion) {
extras =
addDisplayName(
lparser.getRegion(),
TERRITORY_NAME,
localeSeparator,
extras,
altPicker,
paths);
}
List<String> variants = lparser.getVariants();
for (String orig : variants) {
extras = addDisplayName(orig, VARIANT_NAME, localeSeparator, extras, altPicker, paths);
}
// Look for key-type pairs.
main:
for (Map.Entry<String, List<String>> extension :
lparser.getLocaleExtensionsDetailed().entrySet()) {
String key = extension.getKey();
if (key.equals("h0")) {
continue;
}
List<String> keyValue = extension.getValue();
String oldFormatType =
(key.equals("ca") ? JOIN_HYPHEN : JOIN_UNDERBAR)
.join(keyValue); // default value
// Check if key/type pairs exist in the CLDRFile first.
String value = getKeyValueName(key, oldFormatType);
if (value != null) {
value = replaceBracketsForName(value);
} else {
// if we fail, then we construct from the key name and the value
String kname = getKeyName(key);
if (kname == null) {
kname = key; // should not happen, but just in case
}
switch (key) {
case "t":
List<String> hybrid = lparser.getLocaleExtensionsDetailed().get("h0");
if (hybrid != null) {
kname = getKeyValueName("h0", JOIN_UNDERBAR.join(hybrid));
}
oldFormatType = getName(oldFormatType);
break;
case "h0":
continue main;
case "cu":
oldFormatType =
getName(
CURRENCY_SYMBOL,
oldFormatType.toUpperCase(Locale.ROOT),
paths);
break;
case "tz":
if (paths != null) {
throw new IllegalArgumentException(
"Error: getName(…) with paths doesn't handle timezones.");
}
oldFormatType =
getTZName(
oldFormatType, "VVVV"); // TODO: paths not handled here, yet
break;
case "kr":
oldFormatType = getReorderName(localeSeparator, keyValue, paths);
break;
case "rg":
case "sd":
oldFormatType = getName(SUBDIVISION_NAME, oldFormatType, paths);
break;
default:
oldFormatType = JOIN_HYPHEN.join(keyValue);
}
value =
MessageFormat.format(
localeKeyTypePattern, new Object[] {kname, oldFormatType});
if (paths != null) {
paths.add(GETNAME_LOCALE_KEY_TYPE_PATTERN);
}
value = replaceBracketsForName(value);
}
if (paths != null && !extras.isEmpty()) {
paths.add(GETNAME_LOCALE_SEPARATOR);
}
extras =
extras.isEmpty()
? value
: MessageFormat.format(localeSeparator, new Object[] {extras, value});
}
// now handle stray extensions
for (Map.Entry<String, List<String>> extension :
lparser.getExtensionsDetailed().entrySet()) {
String value =
MessageFormat.format(
localeKeyTypePattern,
new Object[] {
extension.getKey(), JOIN_HYPHEN.join(extension.getValue())
});
if (paths != null) {
paths.add(GETNAME_LOCALE_KEY_TYPE_PATTERN);
}
extras =
extras.isEmpty()
? value
: MessageFormat.format(localeSeparator, new Object[] {extras, value});
}
// fix this -- shouldn't be hardcoded!
if (extras.length() == 0) {
return name;
}
if (paths != null) {
paths.add(GETNAME_LOCALE_PATTERN);
}
return MessageFormat.format(localePattern, new Object[] {name, extras});
}
private static final String replaceBracketsForName(String value) {
value = value.replace('(', '[').replace(')', ']').replace('(', '[').replace(')', ']');
return value;
}
/**
* Utility for getting the name, given a code.
*
* @param type
* @param code
* @param codeToAlt - if not null, is called on the code. If the result is not null, then that
* is used for an alt value. If the alt path has a value it is used, otherwise the normal
* one is used. For example, the transform could return "short" for PS or HK or MO, but not
* US or GB.
* @param paths if non-null, will have contributory paths on return
* @return
*/
public String getName(
int type, String code, Transform<String, String> codeToAlt, Set<String> paths) {
String path = getKey(type, code);
String result = null;
if (codeToAlt != null) {
String alt = codeToAlt.transform(code);
if (alt != null) {
String altPath = path + "[@alt=\"" + alt + "\"]";
result = getStringValueWithBaileyNotConstructed(altPath);
if (paths != null && result != null) {
paths.add(altPath);
}
}
}
if (result == null) {
result = getStringValueWithBaileyNotConstructed(path);
if (paths != null && result != null) {
paths.add(path);
}
}
if (getLocaleID().equals("en")) {
CLDRFile.Status status = new CLDRFile.Status();
String sourceLocale = getSourceLocaleID(path, status);
if (result == null || !sourceLocale.equals("en")) {
if (type == LANGUAGE_NAME) {
Set<String> set = Iso639Data.getNames(code);
if (set != null) {
return set.iterator().next();
}
Map<String, Map<String, String>> map =
StandardCodes.getLStreg().get("language");
Map<String, String> info = map.get(code);
if (info != null) {
result = info.get("Description");
}
} else if (type == TERRITORY_NAME) {
result = getLstrFallback("region", code, paths);
} else if (type == SCRIPT_NAME) {
result = getLstrFallback("script", code, paths);
}
}
}
return result;
}
static final Pattern CLEAN_DESCRIPTION = Pattern.compile("([^\\(\\[]*)[\\(\\[].*");
static final Splitter DESCRIPTION_SEP = Splitter.on('▪');
private String getLstrFallback(String codeType, String code, Set<String> paths) {
Map<String, String> info = StandardCodes.getLStreg().get(codeType).get(code);
if (info != null) {
String temp = info.get("Description");
if (!temp.equalsIgnoreCase("Private use")) {
List<String> temp2 = DESCRIPTION_SEP.splitToList(temp);
temp = temp2.get(0);
final Matcher matcher = CLEAN_DESCRIPTION.matcher(temp);
if (matcher.lookingAt()) {
temp = matcher.group(1).trim();
}
return temp;
}
}
return null;
}
/**
* Gets timezone name. Not optimized.
*
* @param tzcode
* @return
*/
private String getTZName(String tzcode, String format) {
String longid = getLongTzid(tzcode);
if (tzcode.length() == 4 && !tzcode.equals("gaza")) {
return longid;
}
TimezoneFormatter tzf = new TimezoneFormatter(this);
return tzf.getFormattedZone(longid, format, 0);
}
private String getReorderName(
String localeSeparator, List<String> keyValues, Set<String> paths) {
String result = null;
for (String value : keyValues) {
String name =
getName(
SCRIPT_NAME,
Character.toUpperCase(value.charAt(0)) + value.substring(1),
paths);
if (name == null) {
name = getKeyValueName("kr", value);
if (name == null) {
name = value;
}
}
result =
result == null
? name
: MessageFormat.format(localeSeparator, new Object[] {result, name});
}
return result;
}
/**
* Adds the display name for a subtag to a string.
*
* @param subtag the subtag
* @param type the type of the subtag
* @param separatorPattern the pattern to be used for separating display names in the resultant
* string
* @param extras the string to be added to
* @return the modified display name string
*/
private String addDisplayName(
String subtag,
int type,
String separatorPattern,
String extras,
Transform<String, String> altPicker,
Set<String> paths) {
if (subtag.length() == 0) {
return extras;
}
String sname = getName(type, subtag, altPicker, paths);
if (sname == null) {
sname = subtag;
}
sname = replaceBracketsForName(sname);
if (extras.length() == 0) {
extras += sname;
} else {
extras = MessageFormat.format(separatorPattern, new Object[] {extras, sname});
}
return extras;
}
/**
* Like getStringValueWithBailey, but reject constructed values, to prevent circularity problems
* with getName
*
* <p>Since GlossonymConstructor uses getName to CREATE constructed values, circularity problems
* would occur if getName in turn used GlossonymConstructor to get constructed Bailey values.
* Note that getStringValueWithBailey only returns a constructed value if the value would
* otherwise be "bogus", and getName has no use for bogus values, so there is no harm in
* returning null rather than code-fallback or other bogus values.
*
* @param path the given xpath
* @return the string value, or null
*/
private String getStringValueWithBaileyNotConstructed(String path) {
Output<String> pathWhereFound = new Output<>();
final String value = getStringValueWithBailey(path, pathWhereFound, null);
if (value == null || GlossonymConstructor.PSEUDO_PATH.equals(pathWhereFound.toString())) {
return null;
}
return value;
}
/**
* A set of paths to be added to getRawExtraPaths(). These are constant across locales, and
* don't have good fallback values in root. NOTE: if this is changed, you'll need to modify
* TestPaths.extraPathAllowsNullValue
*/
static final Set<String> CONST_EXTRA_PATHS =
CharUtilities.internImmutableSet(
Set.of(
// Individual zone overrides — were in getRawExtraPaths
"//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Honolulu\"]/short/generic",
"//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Honolulu\"]/short/standard",
"//ldml/dates/timeZoneNames/zone[@type=\"Pacific/Honolulu\"]/short/daylight",
"//ldml/dates/timeZoneNames/zone[@type=\"Europe/Dublin\"]/long/daylight",
"//ldml/dates/timeZoneNames/zone[@type=\"Europe/London\"]/long/daylight",
"//ldml/dates/timeZoneNames/zone[@type=\"Etc/UTC\"]/long/standard",
"//ldml/dates/timeZoneNames/zone[@type=\"Etc/UTC\"]/short/standard",
// Person name paths
"//ldml/personNames/sampleName[@item=\"nativeG\"]/nameField[@type=\"given\"]",
"//ldml/personNames/sampleName[@item=\"nativeGS\"]/nameField[@type=\"given\"]",
"//ldml/personNames/sampleName[@item=\"nativeGS\"]/nameField[@type=\"surname\"]",
"//ldml/personNames/sampleName[@item=\"nativeGGS\"]/nameField[@type=\"given\"]",
"//ldml/personNames/sampleName[@item=\"nativeGGS\"]/nameField[@type=\"given2\"]",
"//ldml/personNames/sampleName[@item=\"nativeGGS\"]/nameField[@type=\"surname\"]",
"//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"title\"]",
"//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"given\"]",
"//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"given-informal\"]",
"//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"given2\"]",
"//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"surname-prefix\"]",
"//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"surname-core\"]",
"//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"surname2\"]",
"//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"generation\"]",
"//ldml/personNames/sampleName[@item=\"nativeFull\"]/nameField[@type=\"credentials\"]",
"//ldml/personNames/sampleName[@item=\"foreignG\"]/nameField[@type=\"given\"]",
"//ldml/personNames/sampleName[@item=\"foreignGS\"]/nameField[@type=\"given\"]",
"//ldml/personNames/sampleName[@item=\"foreignGS\"]/nameField[@type=\"surname\"]",
"//ldml/personNames/sampleName[@item=\"foreignGGS\"]/nameField[@type=\"given\"]",
"//ldml/personNames/sampleName[@item=\"foreignGGS\"]/nameField[@type=\"given2\"]",
"//ldml/personNames/sampleName[@item=\"foreignGGS\"]/nameField[@type=\"surname\"]",
"//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"title\"]",
"//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"given\"]",
"//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"given-informal\"]",
"//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"given2\"]",
"//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"surname-prefix\"]",
"//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"surname-core\"]",
"//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"surname2\"]",
"//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"generation\"]",
"//ldml/personNames/sampleName[@item=\"foreignFull\"]/nameField[@type=\"credentials\"]"));
}