| /* |
| ****************************************************************************** |
| * Copyright (C) 2005-2011, International Business Machines Corporation and * |
| * others. All Rights Reserved. * |
| ****************************************************************************** |
| */ |
| |
| package org.unicode.cldr.util; |
| |
| import java.io.File; |
| import java.lang.ref.WeakReference; |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Date; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.LinkedHashMap; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.WeakHashMap; |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| import org.unicode.cldr.util.CLDRFile.DraftStatus; |
| import org.unicode.cldr.util.XPathParts.Comments; |
| import org.xml.sax.Locator; |
| |
| import com.google.common.collect.Iterators; |
| import com.ibm.icu.impl.Utility; |
| import com.ibm.icu.util.Freezable; |
| import com.ibm.icu.util.Output; |
| import com.ibm.icu.util.VersionInfo; |
| |
| /** |
| * Overall process is described in |
| * http://cldr.unicode.org/development/development-process/design-proposals/resolution-of-cldr-files |
| * Please update that document if major changes are made. |
| */ |
| public abstract class XMLSource implements Freezable<XMLSource>, Iterable<String> { |
| public static final String CODE_FALLBACK_ID = "code-fallback"; |
| public static final String ROOT_ID = "root"; |
| public static final boolean USE_PARTS_IN_ALIAS = false; |
| private static final String TRACE_INDENT = " "; // "\t" |
| private static Map<String, String> allowDuplicates = new HashMap<>(); |
| |
| private String localeID; |
| private boolean nonInheriting; |
| private TreeMap<String, String> aliasCache; |
| private LinkedHashMap<String, List<String>> reverseAliasCache; |
| protected boolean locked; |
| transient String[] fixedPath = new String[1]; |
| |
| /** |
| * This class represents a source location of an XPath. |
| * @see com.ibm.icu.dev.test.TestFmwk.SourceLocation |
| */ |
| public static class SourceLocation { |
| final static String FILE_PREFIX = "file://"; |
| private String system; |
| private int line; |
| private int column; |
| |
| /** |
| * Initialize from an XML Locator |
| * @param locator |
| */ |
| public SourceLocation(Locator locator) { |
| this(locator.getSystemId(), |
| locator.getLineNumber(), |
| locator.getColumnNumber()); |
| } |
| |
| public SourceLocation(String system, int line, int column) { |
| this.system = system.intern(); |
| this.line = line; |
| this.column = column; |
| } |
| |
| public String getSystem() { |
| // Trim prefix lazily. |
| if (system.startsWith(FILE_PREFIX)) { |
| return system.substring(FILE_PREFIX.length()); |
| } else { |
| return system; |
| } |
| } |
| |
| public int getLine() { |
| return line; |
| } |
| |
| public int getColumn() { |
| return column; |
| } |
| |
| /** |
| * The toString() format is suitable for printing to the command line |
| * and has the format 'file:line:column: ' |
| */ |
| @Override |
| public String toString() { |
| return toString(null); |
| } |
| |
| |
| /** |
| * The toString() format is suitable for printing to the command line |
| * and has the format 'file:line:column: ' |
| * A good leading base path might be CLDRPaths.BASE_DIRECTORY |
| * @param basePath path to trim |
| */ |
| public String toString(final String basePath) { |
| return getSystem(basePath) + ":" + getLine() + ":" + getColumn() + ": "; |
| } |
| |
| /** |
| * Format location suitable for GitHub annotations, skips leading base bath |
| * A good leading base path might be CLDRPaths.BASE_DIRECTORY |
| * @param basePath path to trim |
| * @return |
| */ |
| public String forGitHub(String basePath) { |
| return "file=" + getSystem(basePath) + ",line=" + getLine() + ",col=" + getColumn(); |
| } |
| |
| |
| /** |
| * Format location suitable for GitHub annotations |
| */ |
| public String forGitHub() { |
| return forGitHub(null); |
| } |
| |
| /** |
| * as with getSystem(), but skips the leading base path if identical. |
| * A good leading path might be CLDRPaths.BASE_DIRECTORY |
| * @param basePath path to trim |
| */ |
| public String getSystem(String basePath) { |
| String path = getSystem(); |
| if (basePath != null && !basePath.isEmpty() && path.startsWith(basePath)) { |
| path = path.substring(basePath.length()); |
| // Handle case where the path did NOT start with a slash |
| if (path.startsWith("/") && !basePath.endsWith("/")) { |
| path = path.substring(1); // skip leading / |
| } |
| } |
| return path; |
| } |
| } |
| |
| /* |
| * For testing, make it possible to disable multiple caches: |
| * getFullPathAtDPathCache, getSourceLocaleIDCache, aliasCache, reverseAliasCache |
| */ |
| protected boolean cachingIsEnabled = true; |
| |
| public void disableCaching() { |
| cachingIsEnabled = false; |
| } |
| |
| public static class AliasLocation { |
| public final String pathWhereFound; |
| public final String localeWhereFound; |
| |
| public AliasLocation(String pathWhereFound, String localeWhereFound) { |
| this.pathWhereFound = pathWhereFound; |
| this.localeWhereFound = localeWhereFound; |
| } |
| } |
| |
| // Listeners are stored using weak references so that they can be garbage collected. |
| private List<WeakReference<Listener>> listeners = new ArrayList<>(); |
| |
| public String getLocaleID() { |
| return localeID; |
| } |
| |
| public void setLocaleID(String localeID) { |
| if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); |
| this.localeID = localeID; |
| } |
| |
| /** |
| * Adds all the path,value pairs in tempMap. |
| * The paths must be Full Paths. |
| * |
| * @param tempMap |
| * @param conflict_resolution |
| */ |
| public void putAll(Map<String, String> tempMap, int conflict_resolution) { |
| for (Iterator<String> it = tempMap.keySet().iterator(); it.hasNext();) { |
| String path = it.next(); |
| if (conflict_resolution == CLDRFile.MERGE_KEEP_MINE && getValueAtPath(path) != null) continue; |
| putValueAtPath(path, tempMap.get(path)); |
| } |
| } |
| |
| /** |
| * Adds all the path, value pairs in otherSource. |
| * |
| * @param otherSource |
| * @param conflict_resolution |
| */ |
| public void putAll(XMLSource otherSource, int conflict_resolution) { |
| for (Iterator<String> it = otherSource.iterator(); it.hasNext();) { |
| String path = it.next(); |
| final String oldValue = getValueAtDPath(path); |
| if (conflict_resolution == CLDRFile.MERGE_KEEP_MINE && oldValue != null) { |
| continue; |
| } |
| final String newValue = otherSource.getValueAtDPath(path); |
| if (newValue.equals(oldValue)) { |
| continue; |
| } |
| String fullPath = putValueAtPath(otherSource.getFullPathAtDPath(path), newValue); |
| addSourceLocation(fullPath, otherSource.getSourceLocation(fullPath)); |
| } |
| } |
| |
| /** |
| * Removes all the paths in the collection. |
| * WARNING: must be distinguishedPaths |
| * |
| * @param xpaths |
| */ |
| public void removeAll(Collection<String> xpaths) { |
| for (Iterator<String> it = xpaths.iterator(); it.hasNext();) { |
| removeValueAtDPath(it.next()); |
| } |
| } |
| |
| /** |
| * Tests whether the full path for this dpath is draft or now. |
| * |
| * @param path |
| * @return |
| */ |
| public boolean isDraft(String path) { |
| String fullpath = getFullPath(path); |
| if (path == null) { |
| return false; |
| } |
| if (fullpath.indexOf("[@draft=") < 0) { |
| return false; |
| } |
| XPathParts parts = XPathParts.getFrozenInstance(fullpath); |
| return parts.containsAttribute("draft"); |
| } |
| |
| @Override |
| public boolean isFrozen() { |
| return locked; |
| } |
| |
| /** |
| * Adds the path,value pair. The path must be full path. |
| * |
| * @param xpath |
| * @param value |
| */ |
| public String putValueAtPath(String xpath, String value) { |
| if (locked) { |
| throw new UnsupportedOperationException("Attempt to modify locked object"); |
| } |
| String distinguishingXPath = CLDRFile.getDistinguishingXPath(xpath, fixedPath); |
| putValueAtDPath(distinguishingXPath, value); |
| if (!fixedPath[0].equals(distinguishingXPath)) { |
| clearCache(); |
| putFullPathAtDPath(distinguishingXPath, fixedPath[0]); |
| } |
| return distinguishingXPath; |
| } |
| |
| /** |
| * Gets those paths that allow duplicates |
| */ |
| public static Map<String, String> getPathsAllowingDuplicates() { |
| return allowDuplicates; |
| } |
| |
| /** |
| * A listener for XML source data. |
| */ |
| public static interface Listener { |
| /** |
| * Called whenever the source being listened to has a data change. |
| * |
| * @param xpath |
| * The xpath that had its value changed. |
| * @param source |
| * back-pointer to the source that changed |
| */ |
| public void valueChanged(String xpath, XMLSource source); |
| } |
| |
| /** |
| * Internal class. Immutable! |
| */ |
| public static final class Alias { |
| final private String newLocaleID; |
| final private String oldPath; |
| final private String newPath; |
| final private boolean pathsEqual; |
| static final Pattern aliasPattern = Pattern |
| .compile("(?:\\[@source=\"([^\"]*)\"])?(?:\\[@path=\"([^\"]*)\"])?(?:\\[@draft=\"([^\"]*)\"])?"); |
| // constant, so no need to sync |
| |
| public static Alias make(String aliasPath) { |
| int pos = aliasPath.indexOf("/alias"); |
| if (pos < 0) return null; // quickcheck |
| String aliasParts = aliasPath.substring(pos + 6); |
| String oldPath = aliasPath.substring(0, pos); |
| String newPath = null; |
| |
| return new Alias(pos, oldPath, newPath, aliasParts); |
| } |
| |
| /** |
| * @param newLocaleID |
| * @param oldPath |
| * @param aliasParts |
| * @param newPath |
| * @param pathsEqual |
| */ |
| private Alias(int pos, String oldPath, String newPath, String aliasParts) { |
| Matcher matcher = aliasPattern.matcher(aliasParts); |
| if (!matcher.matches()) { |
| throw new IllegalArgumentException("bad alias pattern for " + aliasParts); |
| } |
| String newLocaleID = matcher.group(1); |
| if (newLocaleID != null && newLocaleID.equals("locale")) { |
| newLocaleID = null; |
| } |
| String relativePath2 = matcher.group(2); |
| if (newPath == null) { |
| newPath = oldPath; |
| } |
| if (relativePath2 != null) { |
| newPath = addRelative(newPath, relativePath2); |
| } |
| |
| boolean pathsEqual = oldPath.equals(newPath); |
| |
| if (pathsEqual && newLocaleID == null) { |
| throw new IllegalArgumentException("Alias must have different path or different source. AliasPath: " |
| + aliasParts |
| + ", Alias: " + newPath + ", " + newLocaleID); |
| } |
| |
| this.newLocaleID = newLocaleID; |
| this.oldPath = oldPath; |
| this.newPath = newPath; |
| this.pathsEqual = pathsEqual; |
| } |
| |
| /** |
| * Create a new path from an old path + relative portion. |
| * Basically, each ../ at the front of the relative portion removes a trailing |
| * element+attributes from the old path. |
| * WARNINGS: |
| * 1. It could fail if an attribute value contains '/'. This should not be the |
| * case except in alias elements, but need to verify. |
| * 2. Also assumes that there are no extra /'s in the relative or old path. |
| * 3. If we verified that the relative paths always used " in place of ', |
| * we could also save a step. |
| * |
| * Maybe we could clean up #2 and #3 when reading in a CLDRFile the first time? |
| * |
| * @param oldPath |
| * @param relativePath |
| * @return |
| */ |
| static String addRelative(String oldPath, String relativePath) { |
| if (relativePath.startsWith("//")) { |
| return relativePath; |
| } |
| while (relativePath.startsWith("../")) { |
| relativePath = relativePath.substring(3); |
| // strip extra "/". Shouldn't occur, but just to be safe. |
| while (relativePath.startsWith("/")) { |
| relativePath = relativePath.substring(1); |
| } |
| // strip last element |
| oldPath = stripLastElement(oldPath); |
| } |
| return oldPath + "/" + relativePath.replace('\'', '"'); |
| } |
| |
| static final Pattern MIDDLE_OF_ATTRIBUTE_VALUE = PatternCache.get("[^\"]*\"\\]"); |
| |
| public static String stripLastElement(String oldPath) { |
| int oldPos = oldPath.lastIndexOf('/'); |
| // verify that we are not in the middle of an attribute value |
| Matcher verifyElement = MIDDLE_OF_ATTRIBUTE_VALUE.matcher(oldPath.substring(oldPos)); |
| while (verifyElement.lookingAt()) { |
| oldPos = oldPath.lastIndexOf('/', oldPos - 1); |
| // will throw exception if we didn't find anything |
| verifyElement.reset(oldPath.substring(oldPos)); |
| } |
| oldPath = oldPath.substring(0, oldPos); |
| return oldPath; |
| } |
| |
| @Override |
| public String toString() { |
| return |
| "newLocaleID: " + newLocaleID + ",\t" |
| + |
| "oldPath: " + oldPath + ",\n\t" |
| + |
| "newPath: " + newPath; |
| } |
| |
| /** |
| * This function is called on the full path, when we know the distinguishing path matches the oldPath. |
| * So we just want to modify the base of the path |
| * |
| * @param oldPath |
| * @param newPath |
| * @param result |
| * @return |
| */ |
| public String changeNewToOld(String fullPath, String newPath, String oldPath) { |
| // do common case quickly |
| if (fullPath.startsWith(newPath)) { |
| return oldPath + fullPath.substring(newPath.length()); |
| } |
| |
| // fullPath will be the same as newPath, except for some attributes at the end. |
| // add those attributes to oldPath, starting from the end. |
| XPathParts partsOld = XPathParts.getFrozenInstance(oldPath); |
| XPathParts partsNew = XPathParts.getFrozenInstance(newPath); |
| XPathParts partsFull = XPathParts.getFrozenInstance(fullPath); |
| Map<String, String> attributesFull = partsFull.getAttributes(-1); |
| Map<String, String> attributesNew = partsNew.getAttributes(-1); |
| Map<String, String> attributesOld = partsOld.getAttributes(-1); |
| for (Iterator<String> it = attributesFull.keySet().iterator(); it.hasNext();) { |
| String attribute = it.next(); |
| if (attributesNew.containsKey(attribute)) continue; |
| attributesOld.put(attribute, attributesFull.get(attribute)); |
| } |
| String result = partsOld.toString(); |
| return result; |
| } |
| |
| public String getOldPath() { |
| return oldPath; |
| } |
| |
| public String getNewLocaleID() { |
| return newLocaleID; |
| } |
| |
| public String getNewPath() { |
| return newPath; |
| } |
| |
| public String composeNewAndOldPath(String path) { |
| return newPath + path.substring(oldPath.length()); |
| } |
| |
| public String composeOldAndNewPath(String path) { |
| return oldPath + path.substring(newPath.length()); |
| } |
| |
| public boolean pathsEqual() { |
| return pathsEqual; |
| } |
| |
| public static boolean isAliasPath(String path) { |
| return path.contains("/alias"); |
| } |
| } |
| |
| /** |
| * This method should be overridden. |
| * |
| * @return a mapping of paths to their aliases. Note that since root is the |
| * only locale to have aliases, all other locales will have no mappings. |
| */ |
| protected synchronized TreeMap<String, String> getAliases() { |
| if (!cachingIsEnabled) { |
| /* |
| * Always create and return a new "aliasMap" instead of this.aliasCache |
| * Probably expensive! |
| */ |
| return loadAliases(); |
| } |
| |
| /* |
| * The cache assumes that aliases will never change over the lifetime of an XMLSource. |
| */ |
| if (aliasCache == null) { |
| aliasCache = loadAliases(); |
| } |
| return aliasCache; |
| } |
| |
| /** |
| * Look for aliases and create mappings for them. |
| * Aliases are only ever found in root. |
| * |
| * return aliasMap the new map |
| */ |
| private TreeMap<String, String> loadAliases() { |
| TreeMap<String, String> aliasMap = new TreeMap<>(); |
| for (String path : this) { |
| if (!Alias.isAliasPath(path)) { |
| continue; |
| } |
| String fullPath = getFullPathAtDPath(path); |
| Alias temp = Alias.make(fullPath); |
| if (temp == null) { |
| continue; |
| } |
| aliasMap.put(temp.getOldPath(), temp.getNewPath()); |
| } |
| return aliasMap; |
| } |
| |
| /** |
| * @return a reverse mapping of aliases |
| */ |
| private LinkedHashMap<String, List<String>> getReverseAliases() { |
| if (cachingIsEnabled && reverseAliasCache != null) { |
| return reverseAliasCache; |
| } |
| // Aliases are only ever found in root. |
| Map<String, String> aliases = getAliases(); |
| Map<String, List<String>> reverse = new HashMap<>(); |
| for (Map.Entry<String, String> entry : aliases.entrySet()) { |
| List<String> list = reverse.get(entry.getValue()); |
| if (list == null) { |
| list = new ArrayList<>(); |
| reverse.put(entry.getValue(), list); |
| } |
| list.add(entry.getKey()); |
| } |
| // Sort map. |
| LinkedHashMap<String, List<String>> reverseAliasMap = new LinkedHashMap<>(new TreeMap<>(reverse)); |
| if (cachingIsEnabled) { |
| reverseAliasCache = reverseAliasMap; |
| } |
| return reverseAliasMap; |
| } |
| |
| /** |
| * Clear "any internal caches" (or only aliasCache?) for this XMLSource. |
| * |
| * Called only by XMLSource.putValueAtPath and XMLSource.removeValueAtPath |
| * |
| * Note: this method does not affect other caches: reverseAliasCache, getFullPathAtDPathCache, getSourceLocaleIDCache |
| */ |
| private void clearCache() { |
| aliasCache = null; |
| } |
| |
| /** |
| * Return the localeID of the XMLSource where the path was found |
| * SUBCLASSING: must be overridden in a resolving locale |
| * |
| * @param path the given path |
| * @param status if not null, to have status.pathWhereFound filled in |
| * @return the localeID |
| */ |
| public String getSourceLocaleID(String path, CLDRFile.Status status) { |
| if (status != null) { |
| status.pathWhereFound = CLDRFile.getDistinguishingXPath(path, null); |
| } |
| return getLocaleID(); |
| } |
| |
| /** |
| * Same as getSourceLocaleID, with unused parameter skipInheritanceMarker. |
| * This is defined so that the version for ResolvingSource can be defined and called |
| * for a ResolvingSource that is declared as an XMLSource. |
| * |
| * @param path the given path |
| * @param status if not null, to have status.pathWhereFound filled in |
| * @param skipInheritanceMarker ignored |
| * @return the localeID |
| */ |
| public String getSourceLocaleIdExtended(String path, CLDRFile.Status status, |
| @SuppressWarnings("unused") boolean skipInheritanceMarker) { |
| return getSourceLocaleID(path, status); |
| } |
| |
| /** |
| * Remove the value. |
| * SUBCLASSING: must be overridden in a resolving locale |
| * |
| * @param xpath |
| */ |
| public void removeValueAtPath(String xpath) { |
| if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); |
| clearCache(); |
| removeValueAtDPath(CLDRFile.getDistinguishingXPath(xpath, null)); |
| } |
| |
| /** |
| * Get the value. |
| * SUBCLASSING: must be overridden in a resolving locale |
| * |
| * @param xpath |
| * @return |
| */ |
| public String getValueAtPath(String xpath) { |
| return getValueAtDPath(CLDRFile.getDistinguishingXPath(xpath, null)); |
| } |
| |
| /** |
| * Get the full path for a distinguishing path |
| * SUBCLASSING: must be overridden in a resolving locale |
| * |
| * @param xpath |
| * @return |
| */ |
| public String getFullPath(String xpath) { |
| return getFullPathAtDPath(CLDRFile.getDistinguishingXPath(xpath, null)); |
| } |
| |
| /** |
| * Put the full path for this distinguishing path |
| * The caller will have processed the path, and only call this with the distinguishing path |
| * SUBCLASSING: must be overridden |
| */ |
| abstract public void putFullPathAtDPath(String distinguishingXPath, String fullxpath); |
| |
| /** |
| * Put the distinguishing path, value. |
| * The caller will have processed the path, and only call this with the distinguishing path |
| * SUBCLASSING: must be overridden |
| */ |
| abstract public void putValueAtDPath(String distinguishingXPath, String value); |
| |
| /** |
| * Remove the path, and the full path, and value corresponding to the path. |
| * The caller will have processed the path, and only call this with the distinguishing path |
| * SUBCLASSING: must be overridden |
| */ |
| abstract public void removeValueAtDPath(String distinguishingXPath); |
| |
| /** |
| * Get the value at the given distinguishing path |
| * The caller will have processed the path, and only call this with the distinguishing path |
| * SUBCLASSING: must be overridden |
| */ |
| abstract public String getValueAtDPath(String path); |
| |
| public boolean hasValueAtDPath(String path) { |
| return (getValueAtDPath(path) != null); |
| } |
| |
| /** |
| * Get the Last-Change Date (if known) when the value was changed. |
| * SUBCLASSING: may be overridden. defaults to NULL. |
| * @return last change date (if known), else null |
| */ |
| public Date getChangeDateAtDPath(String path) { |
| return null; |
| } |
| |
| /** |
| * Get the full path at the given distinguishing path |
| * The caller will have processed the path, and only call this with the distinguishing path |
| * SUBCLASSING: must be overridden |
| */ |
| abstract public String getFullPathAtDPath(String path); |
| |
| /** |
| * Get the comments for the source. |
| * TODO: integrate the Comments class directly into this class |
| * SUBCLASSING: must be overridden |
| */ |
| abstract public Comments getXpathComments(); |
| |
| /** |
| * Set the comments for the source. |
| * TODO: integrate the Comments class directly into this class |
| * SUBCLASSING: must be overridden |
| */ |
| abstract public void setXpathComments(Comments comments); |
| |
| /** |
| * @return an iterator over the distinguished paths |
| */ |
| @Override |
| abstract public Iterator<String> iterator(); |
| |
| /** |
| * @return an iterator over the distinguished paths that start with the prefix. |
| * SUBCLASSING: Normally overridden for efficiency |
| */ |
| public Iterator<String> iterator(String prefix) { |
| if (prefix == null || prefix.length() == 0) return iterator(); |
| return Iterators.filter(iterator(), s -> s.startsWith(prefix)); |
| } |
| |
| public Iterator<String> iterator(Matcher pathFilter) { |
| if (pathFilter == null) return iterator(); |
| return Iterators.filter(iterator(), s -> pathFilter.reset(s).matches()); |
| } |
| |
| /** |
| * @return returns whether resolving or not |
| * SUBCLASSING: Only changed for resolving subclasses |
| */ |
| public boolean isResolving() { |
| return false; |
| } |
| |
| /** |
| * Returns the unresolved version of this XMLSource. |
| * SUBCLASSING: Override in resolving sources. |
| */ |
| public XMLSource getUnresolving() { |
| return this; |
| } |
| |
| /** |
| * SUBCLASSING: must be overridden |
| */ |
| @Override |
| public XMLSource cloneAsThawed() { |
| try { |
| XMLSource result = (XMLSource) super.clone(); |
| result.locked = false; |
| return result; |
| } catch (CloneNotSupportedException e) { |
| throw new InternalError("should never happen"); |
| } |
| } |
| |
| /** |
| * for debugging only |
| */ |
| @Override |
| public String toString() { |
| StringBuffer result = new StringBuffer(); |
| for (Iterator<String> it = iterator(); it.hasNext();) { |
| String path = it.next(); |
| String value = getValueAtDPath(path); |
| String fullpath = getFullPathAtDPath(path); |
| result.append(fullpath).append(" =\t ").append(value).append(CldrUtility.LINE_SEPARATOR); |
| } |
| return result.toString(); |
| } |
| |
| /** |
| * for debugging only |
| */ |
| public String toString(String regex) { |
| Matcher matcher = PatternCache.get(regex).matcher(""); |
| StringBuffer result = new StringBuffer(); |
| for (Iterator<String> it = iterator(matcher); it.hasNext();) { |
| String path = it.next(); |
| String value = getValueAtDPath(path); |
| String fullpath = getFullPathAtDPath(path); |
| result.append(fullpath).append(" =\t ").append(value).append(CldrUtility.LINE_SEPARATOR); |
| } |
| return result.toString(); |
| } |
| |
| /** |
| * @return returns whether supplemental or not |
| */ |
| public boolean isNonInheriting() { |
| return nonInheriting; |
| } |
| |
| /** |
| * @return sets whether supplemental. Normally only called internall. |
| */ |
| public void setNonInheriting(boolean nonInheriting) { |
| if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); |
| this.nonInheriting = nonInheriting; |
| } |
| |
| /** |
| * Internal class for doing resolution |
| * |
| * @author davis |
| * |
| */ |
| public static class ResolvingSource extends XMLSource implements Listener { |
| private XMLSource currentSource; |
| private LinkedHashMap<String, XMLSource> sources; |
| |
| @Override |
| public boolean isResolving() { |
| return true; |
| } |
| |
| @Override |
| public XMLSource getUnresolving() { |
| return sources.get(getLocaleID()); |
| } |
| |
| /* |
| * If there is an alias, then inheritance gets tricky. |
| * If there is a path //ldml/xyz/.../uvw/alias[@path=...][@source=...] |
| * then the parent for //ldml/xyz/.../uvw/abc/.../def/ |
| * is source, and the path to search for is really: //ldml/xyz/.../uvw/path/abc/.../def/ |
| */ |
| public static final boolean TRACE_VALUE = CldrUtility.getProperty("TRACE_VALUE", false); |
| |
| // Map<String,String> getValueAtDPathCache = new HashMap(); |
| |
| @Override |
| public String getValueAtDPath(String xpath) { |
| if (DEBUG_PATH != null && DEBUG_PATH.matcher(xpath).find()) { |
| System.out.println("Getting value for Path: " + xpath); |
| } |
| if (TRACE_VALUE) System.out.println("\t*xpath: " + xpath |
| + CldrUtility.LINE_SEPARATOR + "\t*source: " + currentSource.getClass().getName() |
| + CldrUtility.LINE_SEPARATOR + "\t*locale: " + currentSource.getLocaleID()); |
| String result = null; |
| AliasLocation fullStatus = getCachedFullStatus(xpath, true /* skipInheritanceMarker */); |
| if (fullStatus != null) { |
| if (TRACE_VALUE) { |
| System.out.println("\t*pathWhereFound: " + fullStatus.pathWhereFound); |
| System.out.println("\t*localeWhereFound: " + fullStatus.localeWhereFound); |
| } |
| result = getSource(fullStatus).getValueAtDPath(fullStatus.pathWhereFound); |
| } |
| if (TRACE_VALUE) System.out.println("\t*value: " + result); |
| return result; |
| } |
| |
| @Override |
| public SourceLocation getSourceLocation(String xpath) { |
| SourceLocation result = null; |
| final String dPath = CLDRFile.getDistinguishingXPath(xpath, null); |
| // getCachedFullStatus wants a dPath |
| AliasLocation fullStatus = getCachedFullStatus(dPath, true /* skipInheritanceMarker */); |
| if (fullStatus != null) { |
| result = getSource(fullStatus).getSourceLocation(xpath); // getSourceLocation wants fullpath |
| } |
| return result; |
| } |
| |
| public XMLSource getSource(AliasLocation fullStatus) { |
| XMLSource source = sources.get(fullStatus.localeWhereFound); |
| return source == null ? constructedItems : source; |
| } |
| |
| Map<String, String> getFullPathAtDPathCache = new HashMap<>(); |
| |
| @Override |
| public String getFullPathAtDPath(String xpath) { |
| String result = currentSource.getFullPathAtDPath(xpath); |
| if (result != null) { |
| return result; |
| } |
| // This is tricky. We need to find the alias location's path and full path. |
| // then we need to the the non-distinguishing elements from them, |
| // and add them into the requested path. |
| AliasLocation fullStatus = getCachedFullStatus(xpath, true /* skipInheritanceMarker */); |
| if (fullStatus != null) { |
| String fullPathWhereFound = getSource(fullStatus).getFullPathAtDPath(fullStatus.pathWhereFound); |
| if (fullPathWhereFound == null) { |
| result = null; |
| } else if (fullPathWhereFound.equals(fullStatus.pathWhereFound)) { |
| result = xpath; // no difference |
| } else { |
| result = getFullPath(xpath, fullStatus, fullPathWhereFound); |
| } |
| } |
| return result; |
| } |
| |
| @Override |
| public Date getChangeDateAtDPath(String xpath) { |
| Date result = currentSource.getChangeDateAtDPath(xpath); |
| if (result != null) { |
| return result; |
| } |
| AliasLocation fullStatus = getCachedFullStatus(xpath, true /* skipInheritanceMarker */); |
| if (fullStatus != null) { |
| result = getSource(fullStatus).getChangeDateAtDPath(fullStatus.pathWhereFound); |
| } |
| return result; |
| } |
| |
| private String getFullPath(String xpath, AliasLocation fullStatus, String fullPathWhereFound) { |
| String result = null; |
| if (this.cachingIsEnabled) { |
| result = getFullPathAtDPathCache.get(xpath); |
| } |
| if (result == null) { |
| // find the differences, and add them into xpath |
| // we do this by walking through each element, adding the corresponding attribute values. |
| // we add attributes FROM THE END, in case the lengths are different! |
| XPathParts xpathParts = XPathParts.getFrozenInstance(xpath).cloneAsThawed(); // not frozen, for putAttributeValue |
| XPathParts fullPathWhereFoundParts = XPathParts.getFrozenInstance(fullPathWhereFound); |
| XPathParts pathWhereFoundParts = XPathParts.getFrozenInstance(fullStatus.pathWhereFound); |
| int offset = xpathParts.size() - pathWhereFoundParts.size(); |
| |
| for (int i = 0; i < pathWhereFoundParts.size(); ++i) { |
| Map<String, String> fullAttributes = fullPathWhereFoundParts.getAttributes(i); |
| Map<String, String> attributes = pathWhereFoundParts.getAttributes(i); |
| if (!attributes.equals(fullAttributes)) { // add differences |
| for (String key : fullAttributes.keySet()) { |
| if (!attributes.containsKey(key)) { |
| String value = fullAttributes.get(key); |
| xpathParts.putAttributeValue(i + offset, key, value); |
| } |
| } |
| } |
| } |
| result = xpathParts.toString(); |
| if (cachingIsEnabled) { |
| getFullPathAtDPathCache.put(xpath, result); |
| } |
| } |
| return result; |
| } |
| |
| /** |
| * Return the "George Bailey" value, i.e., the value that would obtain if the value didn't exist (in the first source). |
| * Often the Bailey value comes from the parent locale (such as "fr") of a sublocale (such as "fr_CA"). |
| * Sometimes the Bailey value comes from an alias which may be a different path in the same locale. |
| * |
| * @param xpath the given path |
| * @param pathWhereFound if not null, to be filled in with the path where found |
| * @param localeWhereFound if not null, to be filled in with the locale where found |
| * @return the Bailey value |
| */ |
| @Override |
| public String getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound) { |
| AliasLocation fullStatus = getPathLocation(xpath, true /* skipFirst */, true /* skipInheritanceMarker */); |
| if (localeWhereFound != null) { |
| localeWhereFound.value = fullStatus.localeWhereFound; |
| } |
| if (pathWhereFound != null) { |
| pathWhereFound.value = fullStatus.pathWhereFound; |
| } |
| return getSource(fullStatus).getValueAtDPath(fullStatus.pathWhereFound); |
| } |
| |
| /** |
| * Get the AliasLocation that would be returned by getPathLocation (with skipFirst false), |
| * using a cache for efficiency |
| * |
| * @param xpath the given path |
| * @param skipInheritanceMarker if true, skip sources in which value is INHERITANCE_MARKER |
| * @return the AliasLocation |
| */ |
| private AliasLocation getCachedFullStatus(String xpath, boolean skipInheritanceMarker) { |
| /* |
| * Skip the cache in the special and relatively rare cases where skipInheritanceMarker is false. |
| * |
| * Note: we might consider using a cache also when skipInheritanceMarker is false. |
| * Can't use the same cache for skipInheritanceMarker true and false. |
| * Could use two caches, or add skipInheritanceMarker to the key (append 'T' or 'F' to xpath). |
| * The situation is complicated by use of getSourceLocaleIDCache also in valueChanged. |
| * |
| * There is no caching problem with skipFirst, since that is always false here -- though |
| * getBaileyValue could use a cache if there was one for skipFirst true. |
| */ |
| if (!skipInheritanceMarker || !cachingIsEnabled ) { |
| return getPathLocation(xpath, false /* skipFirst */, skipInheritanceMarker); |
| } |
| synchronized (getSourceLocaleIDCache) { |
| AliasLocation fullStatus = getSourceLocaleIDCache.get(xpath); |
| if (fullStatus == null) { |
| fullStatus = getPathLocation(xpath, false /* skipFirst */, skipInheritanceMarker); |
| getSourceLocaleIDCache.put(xpath, fullStatus); // cache copy |
| } |
| return fullStatus; |
| } |
| } |
| |
| @Override |
| public String getWinningPath(String xpath) { |
| String result = currentSource.getWinningPath(xpath); |
| if (result != null) return result; |
| AliasLocation fullStatus = getCachedFullStatus(xpath, true /* skipInheritanceMarker */); |
| if (fullStatus != null) { |
| result = getSource(fullStatus).getWinningPath(fullStatus.pathWhereFound); |
| } else { |
| result = xpath; |
| } |
| return result; |
| } |
| |
| private transient Map<String, AliasLocation> getSourceLocaleIDCache = new WeakHashMap<>(); |
| |
| /** |
| * Get the source locale ID for the given path, for this ResolvingSource. |
| * |
| * @param distinguishedXPath the given path |
| * @param status if not null, to have status.pathWhereFound filled in |
| * @return the localeID, as a string |
| */ |
| @Override |
| public String getSourceLocaleID(String distinguishedXPath, CLDRFile.Status status) { |
| return getSourceLocaleIdExtended(distinguishedXPath, status, true /* skipInheritanceMarker */); |
| } |
| |
| /** |
| * Same as ResolvingSource.getSourceLocaleID, with additional parameter skipInheritanceMarker, |
| * which is passed on to getCachedFullStatus and getPathLocation. |
| * |
| * @param distinguishedXPath the given path |
| * @param status if not null, to have status.pathWhereFound filled in |
| * @param skipInheritanceMarker if true, skip sources in which value is INHERITANCE_MARKER |
| * @return the localeID, as a string |
| */ |
| @Override |
| public String getSourceLocaleIdExtended(String distinguishedXPath, CLDRFile.Status status, boolean skipInheritanceMarker) { |
| AliasLocation fullStatus = getCachedFullStatus(distinguishedXPath, skipInheritanceMarker); |
| if (status != null) { |
| status.pathWhereFound = fullStatus.pathWhereFound; |
| } |
| return fullStatus.localeWhereFound; |
| } |
| |
| static final Pattern COUNT_EQUALS = PatternCache.get("\\[@count=\"[^\"]*\"]"); |
| |
| /** |
| * Get the AliasLocation, containing path and locale where found, for the given path, for this ResolvingSource. |
| * |
| * @param xpath the given path |
| * @param skipFirst true if we're getting the Bailey value (caller is getBaileyValue), |
| * else false (caller is getCachedFullStatus) |
| * @param skipInheritanceMarker if true, skip sources in which value is INHERITANCE_MARKER |
| * @return the AliasLocation |
| * |
| * skipInheritanceMarker must be true when the caller is getBaileyValue, so that the caller |
| * will not return INHERITANCE_MARKER as the George Bailey value. When the caller is getMissingStatus, |
| * we're not getting the Bailey value, and skipping INHERITANCE_MARKER here could take us up |
| * to "root", which getMissingStatus would misinterpret to mean the item should be listed under |
| * Missing in the Dashboard. Therefore skipInheritanceMarker needs to be false when getMissingStatus |
| * is the caller. Note that we get INHERITANCE_MARKER when there are votes for inheritance, but when |
| * there are no votes getValueAtDPath returns null so we don't get INHERITANCE_MARKER. |
| * |
| * Situation for CheckCoverage.handleCheck may be similar to getMissingStatus, see ticket 11720. |
| * |
| * For other callers, we stick with skipInheritanceMarker true for now, to retain |
| * the behavior before the skipInheritanceMarker parameter was added, but we should be alert for the |
| * possibility that skipInheritanceMarker should be false in some other cases |
| * |
| * References: https://unicode.org/cldr/trac/ticket/11765 |
| * https://unicode.org/cldr/trac/ticket/11720 |
| * https://unicode.org/cldr/trac/ticket/11103 |
| */ |
| private AliasLocation getPathLocation(String xpath, boolean skipFirst, boolean skipInheritanceMarker) { |
| for (XMLSource source : sources.values()) { |
| if (skipFirst) { |
| skipFirst = false; |
| continue; |
| } |
| String value = source.getValueAtDPath(xpath); |
| if (value != null) { |
| if (skipInheritanceMarker && CldrUtility.INHERITANCE_MARKER.equals(value)) { |
| continue; |
| } |
| return new AliasLocation(xpath, source.getLocaleID()); |
| } |
| } |
| // Path not found, check if an alias exists |
| TreeMap<String, String> aliases = sources.get("root").getAliases(); |
| String aliasedPath = aliases.get(xpath); |
| |
| if (aliasedPath == null) { |
| // Check if there is an alias for a subset xpath. |
| // If there are one or more matching aliases, lowerKey() will |
| // return the alias with the longest matching prefix since the |
| // hashmap is sorted according to xpath. |
| |
| // // The following is a work in progress |
| // // We need to recurse, since we might have a chain of aliases |
| // while (true) { |
| String possibleSubpath = aliases.lowerKey(xpath); |
| if (possibleSubpath != null && xpath.startsWith(possibleSubpath)) { |
| aliasedPath = aliases.get(possibleSubpath) + |
| xpath.substring(possibleSubpath.length()); |
| // xpath = aliasedPath; |
| // } else { |
| // break; |
| // } |
| } |
| } |
| |
| // alts are special; they act like there is a root alias to the path without the alt. |
| if (aliasedPath == null && xpath.contains("[@alt=")) { |
| aliasedPath = XPathParts.getPathWithoutAlt(xpath); |
| } |
| |
| // counts are special; they act like there is a root alias to 'other' |
| // and in the special case of currencies, other => null |
| // //ldml/numbers/currencies/currency[@type="BRZ"]/displayName[@count="other"] => //ldml/numbers/currencies/currency[@type="BRZ"]/displayName |
| if (aliasedPath == null && xpath.contains("[@count=")) { |
| aliasedPath = COUNT_EQUALS.matcher(xpath).replaceAll("[@count=\"other\"]"); |
| if (aliasedPath.equals(xpath)) { |
| if (xpath.contains("/displayName")) { |
| aliasedPath = COUNT_EQUALS.matcher(xpath).replaceAll(""); |
| if (aliasedPath.equals(xpath)) { |
| throw new RuntimeException("Internal error"); |
| } |
| } else { |
| aliasedPath = null; |
| } |
| } |
| } |
| |
| if (aliasedPath != null) { |
| // Call getCachedFullStatus recursively to avoid recalculating cached aliases. |
| return getCachedFullStatus(aliasedPath, skipInheritanceMarker); |
| } |
| |
| // Fallback location. |
| return new AliasLocation(xpath, CODE_FALLBACK_ID); |
| } |
| |
| /** |
| * We have to go through the source, add all the paths, then recurse to parents |
| * However, aliases are tricky, so watch it. |
| */ |
| static final boolean TRACE_FILL = CldrUtility.getProperty("TRACE_FILL", false); |
| static final String DEBUG_PATH_STRING = CldrUtility.getProperty("DEBUG_PATH", null); |
| static final Pattern DEBUG_PATH = DEBUG_PATH_STRING == null ? null : PatternCache.get(DEBUG_PATH_STRING); |
| static final boolean SKIP_FALLBACKID = CldrUtility.getProperty("SKIP_FALLBACKID", false); |
| |
| static final int MAX_LEVEL = 40; /* Throw an error if it goes past this. */ |
| |
| /** |
| * Initialises the set of xpaths that a fully resolved XMLSource contains. |
| * http://cldr.unicode.org/development/development-process/design-proposals/resolution-of-cldr-files. |
| * Information about the aliased path and source locale ID of each xpath |
| * is not precalculated here since it doesn't appear to improve overall |
| * performance. |
| */ |
| private Set<String> fillKeys() { |
| Set<String> paths = findNonAliasedPaths(); |
| // Find aliased paths and loop until no more aliases can be found. |
| Set<String> newPaths = paths; |
| int level = 0; |
| boolean newPathsFound = false; |
| do { |
| // Debugging code to protect against an infinite loop. |
| if (TRACE_FILL && DEBUG_PATH == null || level > MAX_LEVEL) { |
| System.out.println(Utility.repeat(TRACE_INDENT, level) + "# paths waiting to be aliased: " |
| + newPaths.size()); |
| System.out.println(Utility.repeat(TRACE_INDENT, level) + "# paths found: " + paths.size()); |
| } |
| if (level > MAX_LEVEL) throw new IllegalArgumentException("Stack overflow"); |
| |
| String[] sortedPaths = new String[newPaths.size()]; |
| newPaths.toArray(sortedPaths); |
| Arrays.sort(sortedPaths); |
| |
| newPaths = getDirectAliases(sortedPaths); |
| newPathsFound = paths.addAll(newPaths); |
| level++; |
| } while (newPathsFound); |
| return paths; |
| } |
| |
| /** |
| * Creates the set of resolved paths for this ResolvingSource while |
| * ignoring aliasing. |
| * |
| * @return |
| */ |
| private Set<String> findNonAliasedPaths() { |
| HashSet<String> paths = new HashSet<>(); |
| |
| // Get all XMLSources used during resolution. |
| List<XMLSource> sourceList = new ArrayList<>(sources.values()); |
| if (!SKIP_FALLBACKID) { |
| sourceList.add(constructedItems); |
| } |
| |
| // Make a pass through, filling all the direct paths, excluding aliases, and collecting others |
| for (XMLSource curSource : sourceList) { |
| for (String xpath : curSource) { |
| paths.add(xpath); |
| } |
| } |
| return paths; |
| } |
| |
| /** |
| * Takes in a list of xpaths and returns a new set of paths that alias |
| * directly to those existing xpaths. |
| * |
| * @param paths a sorted list of xpaths |
| * @return the new set of paths |
| */ |
| private Set<String> getDirectAliases(String[] paths) { |
| HashSet<String> newPaths = new HashSet<>(); |
| // Keep track of the current path index: since it's sorted, we |
| // never have to backtrack. |
| int pathIndex = 0; |
| LinkedHashMap<String, List<String>> reverseAliases = getReverseAliases(); |
| for (String subpath : reverseAliases.keySet()) { |
| // Find the first path that matches the current alias. |
| while (pathIndex < paths.length && |
| paths[pathIndex].compareTo(subpath) < 0) { |
| pathIndex++; |
| } |
| |
| // Alias all paths that match the current alias. |
| String xpath; |
| List<String> list = reverseAliases.get(subpath); |
| int endIndex = pathIndex; |
| int suffixStart = subpath.length(); |
| // Suffixes should always start with an element and not an |
| // attribute to prevent invalid aliasing. |
| while (endIndex < paths.length && |
| (xpath = paths[endIndex]).startsWith(subpath) && |
| xpath.charAt(suffixStart) == '/') { |
| String suffix = xpath.substring(suffixStart); |
| for (String reverseAlias : list) { |
| String reversePath = reverseAlias + suffix; |
| newPaths.add(reversePath); |
| } |
| endIndex++; |
| } |
| if (endIndex == paths.length) break; |
| } |
| return newPaths; |
| } |
| |
| private LinkedHashMap<String, List<String>> getReverseAliases() { |
| return sources.get("root").getReverseAliases(); |
| } |
| |
| private transient Set<String> cachedKeySet = null; |
| |
| /** |
| * @return an iterator over all the xpaths in this XMLSource. |
| */ |
| @Override |
| public Iterator<String> iterator() { |
| return getCachedKeySet().iterator(); |
| } |
| |
| private Set<String> getCachedKeySet() { |
| if (cachedKeySet == null) { |
| cachedKeySet = fillKeys(); |
| cachedKeySet = Collections.unmodifiableSet(cachedKeySet); |
| } |
| return cachedKeySet; |
| } |
| |
| @Override |
| public void putFullPathAtDPath(String distinguishingXPath, String fullxpath) { |
| throw new UnsupportedOperationException("Resolved CLDRFiles are read-only"); |
| } |
| |
| @Override |
| public void putValueAtDPath(String distinguishingXPath, String value) { |
| throw new UnsupportedOperationException("Resolved CLDRFiles are read-only"); |
| } |
| |
| @Override |
| public Comments getXpathComments() { |
| return currentSource.getXpathComments(); |
| } |
| |
| @Override |
| public void setXpathComments(Comments path) { |
| throw new UnsupportedOperationException("Resolved CLDRFiles are read-only"); |
| } |
| |
| @Override |
| public void removeValueAtDPath(String xpath) { |
| throw new UnsupportedOperationException("Resolved CLDRFiles are read-only"); |
| } |
| |
| @Override |
| public XMLSource freeze() { |
| return this; // No-op. ResolvingSource is already read-only. |
| } |
| |
| @Override |
| public void valueChanged(String xpath, XMLSource nonResolvingSource) { |
| if (!cachingIsEnabled) { |
| return; |
| } |
| synchronized (getSourceLocaleIDCache) { |
| AliasLocation location = getSourceLocaleIDCache.remove(xpath); |
| if (location == null) { |
| return; |
| } |
| // Paths aliasing to this path (directly or indirectly) may be affected, |
| // so clear them as well. |
| // There's probably a more elegant way to fix the paths than simply |
| // throwing everything out. |
| Set<String> dependentPaths = getDirectAliases(new String[] { xpath }); |
| if (dependentPaths.size() > 0) { |
| for (String path : dependentPaths) { |
| getSourceLocaleIDCache.remove(path); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Creates a new ResolvingSource with the given locale resolution chain. |
| * |
| * @param sourceList |
| * the list of XMLSources to look in during resolution, |
| * ordered from the current locale up to root. |
| */ |
| public ResolvingSource(List<XMLSource> sourceList) { |
| // Sanity check for root. |
| if (sourceList == null || !sourceList.get(sourceList.size() - 1).getLocaleID().equals("root")) { |
| throw new IllegalArgumentException("Last element should be root"); |
| } |
| currentSource = sourceList.get(0); // Convenience variable |
| sources = new LinkedHashMap<>(); |
| for (XMLSource source : sourceList) { |
| sources.put(source.getLocaleID(), source); |
| } |
| |
| // Add listeners to all locales except root, since we don't expect |
| // root to change programatically. |
| for (int i = 0, limit = sourceList.size() - 1; i < limit; i++) { |
| sourceList.get(i).addListener(this); |
| } |
| } |
| |
| @Override |
| public String getLocaleID() { |
| return currentSource.getLocaleID(); |
| } |
| |
| private static final String[] keyDisplayNames = { |
| "calendar", |
| "cf", |
| "collation", |
| "currency", |
| "hc", |
| "lb", |
| "ms", |
| "numbers" |
| }; |
| private static final String[][] typeDisplayNames = { |
| { "account", "cf" }, |
| { "ahom", "numbers" }, |
| { "arab", "numbers" }, |
| { "arabext", "numbers" }, |
| { "armn", "numbers" }, |
| { "armnlow", "numbers" }, |
| { "bali", "numbers" }, |
| { "beng", "numbers" }, |
| { "big5han", "collation" }, |
| { "brah", "numbers" }, |
| { "buddhist", "calendar" }, |
| { "cakm", "numbers" }, |
| { "cham", "numbers" }, |
| { "chinese", "calendar" }, |
| { "compat", "collation" }, |
| { "coptic", "calendar" }, |
| { "cyrl", "numbers" }, |
| { "dangi", "calendar" }, |
| { "deva", "numbers" }, |
| { "diak", "numbers" }, |
| { "dictionary", "collation" }, |
| { "ducet", "collation" }, |
| { "emoji", "collation" }, |
| { "eor", "collation" }, |
| { "ethi", "numbers" }, |
| { "ethiopic", "calendar" }, |
| { "ethiopic-amete-alem", "calendar" }, |
| { "fullwide", "numbers" }, |
| { "gb2312han", "collation" }, |
| { "geor", "numbers" }, |
| { "gong", "numbers" }, |
| { "gonm", "numbers" }, |
| { "gregorian", "calendar" }, |
| { "grek", "numbers" }, |
| { "greklow", "numbers" }, |
| { "gujr", "numbers" }, |
| { "guru", "numbers" }, |
| { "h11", "hc" }, |
| { "h12", "hc" }, |
| { "h23", "hc" }, |
| { "h24", "hc" }, |
| { "hanidec", "numbers" }, |
| { "hans", "numbers" }, |
| { "hansfin", "numbers" }, |
| { "hant", "numbers" }, |
| { "hantfin", "numbers" }, |
| { "hebr", "numbers" }, |
| { "hebrew", "calendar" }, |
| { "hmng", "numbers" }, |
| { "hmnp", "numbers" }, |
| { "indian", "calendar" }, |
| { "islamic", "calendar" }, |
| { "islamic-civil", "calendar" }, |
| { "islamic-rgsa", "calendar" }, |
| { "islamic-tbla", "calendar" }, |
| { "islamic-umalqura", "calendar" }, |
| { "iso8601", "calendar" }, |
| { "japanese", "calendar" }, |
| { "java", "numbers" }, |
| { "jpan", "numbers" }, |
| { "jpanfin", "numbers" }, |
| { "kali", "numbers" }, |
| { "kawi", "numbers" }, |
| { "khmr", "numbers" }, |
| { "knda", "numbers" }, |
| { "lana", "numbers" }, |
| { "lanatham", "numbers" }, |
| { "laoo", "numbers" }, |
| { "latn", "numbers" }, |
| { "lepc", "numbers" }, |
| { "limb", "numbers" }, |
| { "loose", "lb" }, |
| { "mathbold", "numbers" }, |
| { "mathdbl", "numbers" }, |
| { "mathmono", "numbers" }, |
| { "mathsanb", "numbers" }, |
| { "mathsans", "numbers" }, |
| { "metric", "ms" }, |
| { "mlym", "numbers" }, |
| { "modi", "numbers" }, |
| { "mong", "numbers" }, |
| { "mroo", "numbers" }, |
| { "mtei", "numbers" }, |
| { "mymr", "numbers" }, |
| { "mymrshan", "numbers" }, |
| { "mymrtlng", "numbers" }, |
| { "nagm", "numbers" }, |
| { "nkoo", "numbers" }, |
| { "normal", "lb" }, |
| { "olck", "numbers" }, |
| { "orya", "numbers" }, |
| { "osma", "numbers" }, |
| { "persian", "calendar" }, |
| { "phonebook", "collation" }, |
| { "pinyin", "collation" }, |
| { "reformed", "collation" }, |
| { "roc", "calendar" }, |
| { "rohg", "numbers" }, |
| { "roman", "numbers" }, |
| { "romanlow", "numbers" }, |
| { "saur", "numbers" }, |
| { "search", "collation" }, |
| { "searchjl", "collation" }, |
| { "shrd", "numbers" }, |
| { "sind", "numbers" }, |
| { "sinh", "numbers" }, |
| { "sora", "numbers" }, |
| { "standard", "cf" }, |
| { "standard", "collation" }, |
| { "strict", "lb" }, |
| { "stroke", "collation" }, |
| { "sund", "numbers" }, |
| { "takr", "numbers" }, |
| { "talu", "numbers" }, |
| { "taml", "numbers" }, |
| { "tamldec", "numbers" }, |
| { "tnsa", "numbers" }, |
| { "telu", "numbers" }, |
| { "thai", "numbers" }, |
| { "tibt", "numbers" }, |
| { "tirh", "numbers" }, |
| { "traditional", "collation" }, |
| { "unihan", "collation" }, |
| { "uksystem", "ms" }, |
| { "ussystem", "ms" }, |
| { "vaii", "numbers" }, |
| { "wara", "numbers" }, |
| { "wcho", "numbers" }, |
| { "zhuyin", "collation" } }; |
| |
| private static final boolean SKIP_SINGLEZONES = false; |
| private static XMLSource constructedItems = new SimpleXMLSource(CODE_FALLBACK_ID); |
| |
| static { |
| StandardCodes sc = StandardCodes.make(); |
| Map<String, Set<String>> countries_zoneSet = sc.getCountryToZoneSet(); |
| Map<String, String> zone_countries = sc.getZoneToCounty(); |
| |
| for (int typeNo = 0; typeNo <= CLDRFile.TZ_START; ++typeNo) { |
| String type = CLDRFile.getNameName(typeNo); |
| String type2 = (typeNo == CLDRFile.CURRENCY_SYMBOL) ? CLDRFile.getNameName(CLDRFile.CURRENCY_NAME) |
| : (typeNo >= CLDRFile.TZ_START) ? "tzid" |
| : type; |
| Set<String> codes = sc.getSurveyToolDisplayCodes(type2); |
| for (Iterator<String> codeIt = codes.iterator(); codeIt.hasNext();) { |
| String code = codeIt.next(); |
| String value = code; |
| if (typeNo == CLDRFile.TZ_EXEMPLAR) { // skip single-zone countries |
| if (SKIP_SINGLEZONES) { |
| String country = zone_countries.get(code); |
| Set<String> s = countries_zoneSet.get(country); |
| if (s != null && s.size() == 1) continue; |
| } |
| value = TimezoneFormatter.getFallbackName(value); |
| } else if (typeNo == CLDRFile.LANGUAGE_NAME) { |
| if (ROOT_ID.equals(value)) { |
| continue; |
| } |
| } |
| addFallbackCode(typeNo, code, value); |
| } |
| } |
| |
| String[] extraCodes = { |
| "ar_001", |
| "de_AT", "de_CH", |
| "en_AU", "en_CA", "en_GB", "en_US", "es_419", "es_ES", "es_MX", |
| "fa_AF", "fr_CA", "fr_CH", "frc", |
| "hi_Latn", |
| "lou", |
| "nds_NL", "nl_BE", |
| "pt_BR", "pt_PT", |
| "ro_MD", |
| "sw_CD", |
| "zh_Hans", "zh_Hant" |
| }; |
| for (String extraCode : extraCodes) { |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, extraCode, extraCode); |
| } |
| |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_GB", "en_GB", "short"); |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "en_US", "en_US", "short"); |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "az", "az", "short"); |
| |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "ckb", "ckb", "menu"); |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "ckb", "ckb", "variant"); |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "hi_Latn", "hi_Latn", "variant"); |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "yue", "yue", "menu"); |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "zh", "zh", "menu"); |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "zh_Hans", "zh", "long"); |
| addFallbackCode(CLDRFile.LANGUAGE_NAME, "zh_Hant", "zh", "long"); |
| |
| addFallbackCode(CLDRFile.SCRIPT_NAME, "Hans", "Hans", "stand-alone"); |
| addFallbackCode(CLDRFile.SCRIPT_NAME, "Hant", "Hant", "stand-alone"); |
| |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "GB", "GB", "short"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "HK", "HK", "short"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "MO", "MO", "short"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "PS", "PS", "short"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "US", "US", "short"); |
| |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "CD", "CD", "variant"); // add other geopolitical items |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "CG", "CG", "variant"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "CI", "CI", "variant"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "CZ", "CZ", "variant"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "FK", "FK", "variant"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "TL", "TL", "variant"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "SZ", "SZ", "variant"); |
| |
| // new alternate name |
| |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "NZ", "NZ", "variant"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "TR", "TR", "variant"); |
| |
| |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "XA", "XA"); |
| addFallbackCode(CLDRFile.TERRITORY_NAME, "XB", "XB"); |
| |
| addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraAbbr/era[@type=\"0\"]", "BCE", "variant"); |
| addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraAbbr/era[@type=\"1\"]", "CE", "variant"); |
| addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNames/era[@type=\"0\"]", "BCE", "variant"); |
| addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNames/era[@type=\"1\"]", "CE", "variant"); |
| addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNarrow/era[@type=\"0\"]", "BCE", "variant"); |
| addFallbackCode("//ldml/dates/calendars/calendar[@type=\"gregorian\"]/eras/eraNarrow/era[@type=\"1\"]", "CE", "variant"); |
| |
| for (int i = 0; i < keyDisplayNames.length; ++i) { |
| constructedItems.putValueAtPath( |
| "//ldml/localeDisplayNames/keys/key" + |
| "[@type=\"" + keyDisplayNames[i] + "\"]", |
| keyDisplayNames[i]); |
| } |
| for (int i = 0; i < typeDisplayNames.length; ++i) { |
| constructedItems.putValueAtPath( |
| "//ldml/localeDisplayNames/types/type" |
| + "[@key=\"" + typeDisplayNames[i][1] + "\"]" |
| + "[@type=\"" + typeDisplayNames[i][0] + "\"]", |
| typeDisplayNames[i][0]); |
| } |
| constructedItems.freeze(); |
| allowDuplicates = Collections.unmodifiableMap(allowDuplicates); |
| } |
| |
| private static void addFallbackCode(int typeNo, String code, String value) { |
| addFallbackCode(typeNo, code, value, null); |
| } |
| |
| private static void addFallbackCode(int typeNo, String code, String value, String alt) { |
| String fullpath = CLDRFile.getKey(typeNo, code); |
| String distinguishingPath = addFallbackCodeToConstructedItems(fullpath, value, alt); |
| if (typeNo == CLDRFile.LANGUAGE_NAME || typeNo == CLDRFile.SCRIPT_NAME || typeNo == CLDRFile.TERRITORY_NAME) { |
| allowDuplicates.put(distinguishingPath, code); |
| } |
| } |
| |
| private static void addFallbackCode(String fullpath, String value, String alt) { // assumes no allowDuplicates for this |
| addFallbackCodeToConstructedItems(fullpath, value, alt); // ignore unneeded return value |
| } |
| |
| private static String addFallbackCodeToConstructedItems(String fullpath, String value, String alt) { |
| if (alt != null) { |
| // Insert the @alt= string after the last occurrence of "]" |
| StringBuffer fullpathBuf = new StringBuffer(fullpath); |
| fullpath = fullpathBuf.insert(fullpathBuf.lastIndexOf("]") + 1, "[@alt=\"" + alt + "\"]").toString(); |
| } |
| return constructedItems.putValueAtPath(fullpath, value); |
| } |
| |
| @Override |
| public boolean isHere(String path) { |
| return currentSource.isHere(path); // only test one level |
| } |
| |
| @Override |
| public void getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result) { |
| // NOTE: No caching is currently performed here because the unresolved |
| // locales already cache their value-path mappings, and it's not |
| // clear yet how much further caching would speed this up. |
| |
| // Add all non-aliased paths with the specified value. |
| List<XMLSource> children = new ArrayList<>(); |
| Set<String> filteredPaths = new HashSet<>(); |
| for (XMLSource source : sources.values()) { |
| Set<String> pathsWithValue = new HashSet<>(); |
| source.getPathsWithValue(valueToMatch, pathPrefix, pathsWithValue); |
| // Don't add a path with the value if it is overridden by a child locale. |
| for (String pathWithValue : pathsWithValue) { |
| if (!sourcesHavePath(pathWithValue, children)) { |
| filteredPaths.add(pathWithValue); |
| } |
| } |
| children.add(source); |
| } |
| |
| // Find all paths that alias to the specified value, then filter by |
| // path prefix. |
| Set<String> aliases = new HashSet<>(); |
| Set<String> oldAliases = new HashSet<>(filteredPaths); |
| Set<String> newAliases; |
| do { |
| String[] sortedPaths = new String[oldAliases.size()]; |
| oldAliases.toArray(sortedPaths); |
| Arrays.sort(sortedPaths); |
| newAliases = getDirectAliases(sortedPaths); |
| oldAliases = newAliases; |
| aliases.addAll(newAliases); |
| } while (newAliases.size() > 0); |
| |
| // get the aliases, but only the ones that have values that match |
| String norm = null; |
| for (String alias : aliases) { |
| if (alias.startsWith(pathPrefix)) { |
| if (norm == null && valueToMatch != null) { |
| norm = SimpleXMLSource.normalize(valueToMatch); |
| } |
| String value = getValueAtDPath(alias); |
| if (value != null && SimpleXMLSource.normalize(value).equals(norm)) { |
| filteredPaths.add(alias); |
| } |
| } |
| } |
| |
| result.addAll(filteredPaths); |
| } |
| |
| private boolean sourcesHavePath(String xpath, List<XMLSource> sources) { |
| for (XMLSource source : sources) { |
| if (source.hasValueAtDPath(xpath)) return true; |
| } |
| return false; |
| } |
| |
| @Override |
| public VersionInfo getDtdVersionInfo() { |
| return currentSource.getDtdVersionInfo(); |
| } |
| } |
| |
| /** |
| * See CLDRFile isWinningPath for documentation |
| * |
| * @param path |
| * @return |
| */ |
| public boolean isWinningPath(String path) { |
| return getWinningPath(path).equals(path); |
| } |
| |
| /** |
| * See CLDRFile getWinningPath for documentation. |
| * Default implementation is that it removes draft and [@alt="...proposed..." if possible |
| * |
| * @param path |
| * @return |
| */ |
| public String getWinningPath(String path) { |
| String newPath = CLDRFile.getNondraftNonaltXPath(path); |
| if (!newPath.equals(path)) { |
| String value = getValueAtPath(newPath); // ensure that it still works |
| if (value != null) { |
| return newPath; |
| } |
| } |
| return path; |
| } |
| |
| /** |
| * Adds a listener to this XML source. |
| */ |
| public void addListener(Listener listener) { |
| listeners.add(new WeakReference<>(listener)); |
| } |
| |
| /** |
| * Notifies all listeners that the winning value for the given path has changed. |
| * |
| * @param xpath |
| * the xpath where the change occurred. |
| */ |
| public void notifyListeners(String xpath) { |
| int i = 0; |
| while (i < listeners.size()) { |
| Listener listener = listeners.get(i).get(); |
| if (listener == null) { // listener has been garbage-collected. |
| listeners.remove(i); |
| } else { |
| listener.valueChanged(xpath, this); |
| i++; |
| } |
| } |
| } |
| |
| /** |
| * return true if the path in this file (without resolution). Default implementation is to just see if the path has |
| * a value. |
| * The resolved source must just test the top level. |
| * |
| * @param path |
| * @return |
| */ |
| public boolean isHere(String path) { |
| return getValueAtPath(path) != null; |
| } |
| |
| /** |
| * Find all the distinguished paths having values matching valueToMatch, and add them to result. |
| * |
| * @param valueToMatch |
| * @param pathPrefix |
| * @param result |
| */ |
| public abstract void getPathsWithValue(String valueToMatch, String pathPrefix, Set<String> result); |
| |
| public VersionInfo getDtdVersionInfo() { |
| return null; |
| } |
| |
| @SuppressWarnings("unused") |
| public String getBaileyValue(String xpath, Output<String> pathWhereFound, Output<String> localeWhereFound) { |
| return null; // only a resolving xmlsource will return a value |
| } |
| |
| // HACK, should be field on XMLSource |
| public DtdType getDtdType() { |
| final Iterator<String> it = iterator(); |
| if (it.hasNext()) { |
| String path = it.next(); |
| return DtdType.fromPath(path); |
| } |
| return null; |
| } |
| |
| /** |
| * XMLNormalizingDtdType is set in XMLNormalizingHandler loading XML process |
| */ |
| private DtdType XMLNormalizingDtdType; |
| private static final boolean LOG_PROGRESS = false; |
| |
| public DtdType getXMLNormalizingDtdType() { |
| return this.XMLNormalizingDtdType; |
| } |
| |
| public void setXMLNormalizingDtdType(DtdType dtdType) { |
| this.XMLNormalizingDtdType = dtdType; |
| } |
| |
| /** |
| * Sets the initial comment, replacing everything that was there |
| * Use in XMLNormalizingHandler only |
| */ |
| public XMLSource setInitialComment(String comment) { |
| if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); |
| Log.logln(LOG_PROGRESS, "SET initial Comment: \t" + comment); |
| this.getXpathComments().setInitialComment(comment); |
| return this; |
| } |
| |
| /** |
| * Use in XMLNormalizingHandler only |
| */ |
| public XMLSource addComment(String xpath, String comment, Comments.CommentType type) { |
| if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); |
| Log.logln(LOG_PROGRESS, "ADDING Comment: \t" + type + "\t" + xpath + " \t" + comment); |
| if (xpath == null || xpath.length() == 0) { |
| this.getXpathComments().setFinalComment( |
| CldrUtility.joinWithSeparation(this.getXpathComments().getFinalComment(), XPathParts.NEWLINE, |
| comment)); |
| } else { |
| xpath = CLDRFile.getDistinguishingXPath(xpath, null); |
| this.getXpathComments().addComment(type, xpath, comment); |
| } |
| return this; |
| } |
| |
| /** |
| * Use in XMLNormalizingHandler only |
| */ |
| public String getFullXPath(String xpath) { |
| if (xpath == null) { |
| throw new NullPointerException("Null distinguishing xpath"); |
| } |
| String result = this.getFullPath(xpath); |
| return result != null ? result : xpath; // we can't add any non-distinguishing values if there is nothing there. |
| } |
| |
| /** |
| * Add a new element to a XMLSource |
| * Use in XMLNormalizingHandler only |
| */ |
| public XMLSource add(String currentFullXPath, String value) { |
| if (locked) throw new UnsupportedOperationException("Attempt to modify locked object"); |
| Log.logln(LOG_PROGRESS, "ADDING: \t" + currentFullXPath + " \t" + value + "\t" + currentFullXPath); |
| try { |
| this.putValueAtPath(currentFullXPath, value); |
| } catch (RuntimeException e) { |
| throw new IllegalArgumentException("failed adding " + currentFullXPath + ",\t" + value, e); |
| } |
| return this; |
| } |
| |
| /** |
| * Get frozen normalized XMLSource |
| * @param localeId |
| * @param dirs |
| * @param minimalDraftStatus |
| * @return XMLSource |
| */ |
| public static XMLSource getFrozenInstance(String localeId, List<File> dirs, DraftStatus minimalDraftStatus) { |
| return XMLNormalizingLoader.getFrozenInstance(localeId, dirs, minimalDraftStatus); |
| } |
| |
| /** |
| * Does the value in question either match or inherent the current value in this XMLSource? |
| * |
| * To match, the value in question and the current value must be non-null and equal. |
| * |
| * To inherit the current value, the value in question must be INHERITANCE_MARKER |
| * and the current value must equal the bailey value. |
| * |
| * @param value the value in question |
| * @param curValue the current value, that is, 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; |
| } |
| |
| /** |
| * Add a SourceLocation to this full XPath. |
| * Base implementation does nothing. |
| * @param currentFullXPath |
| * @param location |
| * @return |
| */ |
| public XMLSource addSourceLocation(String currentFullXPath, SourceLocation location) { |
| return this; |
| } |
| |
| /** |
| * Get the SourceLocation for a specific XPath. |
| * Base implementation always returns null. |
| * @param fullXPath |
| * @return |
| */ |
| public SourceLocation getSourceLocation(String fullXPath) { |
| return null; |
| } |
| } |