1 package org.unicode.cldr.api; 2 3 import static com.google.common.base.Preconditions.checkArgument; 4 import static com.google.common.base.Preconditions.checkNotNull; 5 6 import java.util.LinkedHashMap; 7 import java.util.Map; 8 import java.util.Objects; 9 10 import org.unicode.cldr.api.AttributeKey.AttributeSupplier; 11 import org.unicode.cldr.util.CldrUtility; 12 13 import com.google.common.collect.ImmutableList; 14 import com.google.common.collect.ImmutableMap; 15 16 /** 17 * A CLDR element value and associated "value" attributes, along with its distinguishing {@link 18 * CldrPath}. 19 * 20 * <p>In CLDR, a path contains attributes that are one of three types; "distinguishing", "value" 21 * and "metadata", and a path can be parsed to extract value attributes. 22 * 23 * <p>CldrValue instance hold only the "value" attributes, with "distinguishing" attributes being 24 * held by the associated {@link CldrPath}, and "metadata" attributes being ignored completely 25 * since they are synthetic and internal to the core CLDR classes. 26 * 27 * <p>Note that while the ordering of "value" attributes is stable, it should not be relied upon. 28 * Unlike "distinguishing" attributes in CldrPath, "value" attributes don't conceptually form a 29 * sequence. It is expected that users will only lookup attribute values directly by their keys and 30 * never care about their order. 31 * 32 * <p>CldrValue is an immutable value type with efficient equality semantics. 33 * 34 * <p>See <a href="https://www.unicode.org/reports/tr35/#Definitions">the LDML specification</a> 35 * for more details. 36 */ 37 public final class CldrValue implements AttributeSupplier { 38 /** 39 * Parses a full CLDR path string, possibly containing "distinguishing", "value" and even 40 * private "metadata" attributes into a normalized CldrValue instance. Attributes will be parsed 41 * and handled according to their type: 42 * <ul> 43 * <li>Value attributes will be added to the returned CldrValue instance. 44 * <li>Distinguishing attributes will be added to the associated CldrPath instance. 45 * <li>Other non-public attributes will be ignored. 46 * </ul> 47 * 48 * <p>The path string must be structured correctly (e.g. "//ldml/foo[@bar="baz]") and must 49 * represent a known DTD type, based on the first path element (e.g. "//ldml/..."). 50 * 51 * @param fullPath the full path string, possibly containing all types of attribute. 52 * @param value the primary leaf value associated with the path (possibly empty). 53 * @return the parsed value instance, referencing the associated distinguishing path. 54 * @throws IllegalArgumentException if the path is not well formed. 55 */ parseValue(String fullPath, String value)56 public static CldrValue parseValue(String fullPath, String value) { 57 LinkedHashMap<AttributeKey, String> valueAttributes = new LinkedHashMap<>(); 58 CldrPath path = CldrPaths.processXPath(fullPath, ImmutableList.of(), valueAttributes::put); 59 return new CldrValue(value, valueAttributes, path); 60 } 61 62 /** 63 * Returns a value whose path has been replaced with the specified distinguished path. 64 * 65 * <p>In general, it is not safe to change paths arbitrarily. Care must be taken to ensure that 66 * the source and target paths are semantically interchangeable. 67 * 68 * <p>A very basic test is in place to prevent the most egregious errors, by ensuring that the 69 * replacement path has the same elements as the original, while allowing attributes and their 70 * values to be different. Do not, however, depend upon that test to catch all problems. 71 * 72 * @param path the new path for this value. 73 * @return a new value with the specified path (or the same value if the paths were identical). 74 */ replacePath(CldrPath path)75 public CldrValue replacePath(CldrPath path) { 76 if (this.path.equals(path)) { 77 return this; 78 } 79 checkArgument(hasSameElements(this.path, path), 80 "invalid replacement path '%s' for value: %s", path, this); 81 return new CldrValue(getValue(), attributes, path); 82 } 83 hasSameElements(CldrPath x, CldrPath y)84 private static boolean hasSameElements(CldrPath x, CldrPath y) { 85 if (x.getLength() != y.getLength()) { 86 return false; 87 } 88 do { 89 if (!x.getName().equals(y.getName())) { 90 return false; 91 } 92 x = x.getParent(); 93 y = y.getParent(); 94 } while (x != null); 95 return true; 96 } 97 98 // Note: If this is ever made public, it should be modified to enforce attribute order 99 // according to the DTD. It works now because the code calling it handles ordering correctly. create(String value, Map<AttributeKey, String> valueAttributes, CldrPath path)100 static CldrValue create(String value, Map<AttributeKey, String> valueAttributes, CldrPath path) { 101 return new CldrValue(value, valueAttributes, path); 102 } 103 104 private final String value; 105 private final ImmutableMap<AttributeKey, String> attributes; 106 private final CldrPath path; 107 // Cached to avoid repeated recalculation from the map (which cannot cache its hash code). 108 private final int hashCode; 109 CldrValue(String value, Map<AttributeKey, String> attributes, CldrPath path)110 private CldrValue(String value, Map<AttributeKey, String> attributes, CldrPath path) { 111 // Since early 2019 there's been the possibility of getting the inheritance marker as 112 // a value for a path. This indicates that the value does NOT actually exist for a 113 // locale and would be inherited. However everything that creates a CldrValue instance 114 // is expected to deal with this and we should never see inheritance markers here. 115 // Note: This also serves as a null check for values. 116 checkArgument(!value.equals(CldrUtility.INHERITANCE_MARKER), 117 "unexpected inheritance marker '%s' for path: %s", value, path); 118 this.value = checkNotNull(value); 119 this.attributes = checkAttributeMap(attributes); 120 this.path = checkNotNull(path); 121 this.hashCode = Objects.hash(value, this.attributes, path); 122 } 123 checkAttributeMap( Map<AttributeKey, String> attributes)124 private static ImmutableMap<AttributeKey, String> checkAttributeMap( 125 Map<AttributeKey, String> attributes) { 126 // Keys are checked on creation, but values need to be checked. 127 for (String v : attributes.values()) { 128 checkArgument(!v.contains("\""), "unsupported '\"' in attribute value: %s", v); 129 } 130 return ImmutableMap.copyOf(attributes); 131 } 132 133 /** 134 * Returns the primary (non-attribute) CLDR value associated with a distinguishing path. For a 135 * CLDR element with no explicitly associated value, an empty string is returned. 136 * 137 * @return the primary value of this CLDR value instance. 138 */ getValue()139 public String getValue() { 140 return value; 141 } 142 143 /** 144 * Returns the raw value of an attribute associated with this CLDR value or distinguishing 145 * path, or null if not present. For almost all use cases it is preferable to use the accessor 146 * methods on the {@link AttributeKey} class, which provide additional useful semantic checking 147 * and common type conversion. You should only use this method directly if there's a strong 148 * performance requirement. 149 * 150 * @param key the key identifying an attribute. 151 * @return the attribute value or {@code null} if not present. 152 * @see AttributeKey 153 */ 154 @Override get(AttributeKey key)155 /* @Nullable */ public String get(AttributeKey key) { 156 if (getPath().getDataType().isValueAttribute(key)) { 157 return attributes.get(key); 158 } 159 return getPath().get(key); 160 } 161 162 /** 163 * Returns the data type for this value, as defined by its path. 164 * 165 * @return the value's data type. 166 */ 167 @Override getDataType()168 public CldrDataType getDataType() { 169 return getPath().getDataType(); 170 } 171 172 /** 173 * Returns the "value" attributes associated with this value. Attribute ordering is stable, 174 * with attributes from earlier path elements preceding attributes for later ones. However it 175 * is recommended that callers avoid relying on specific ordering semantics and always look up 176 * attribute values by key if possible. 177 * 178 * @return a map of the value attributes for this CLDR value instance. 179 */ getValueAttributes()180 public ImmutableMap<AttributeKey, String> getValueAttributes() { 181 return attributes; 182 } 183 184 /** 185 * Returns the CldrPath associated with this value. All value instances are associated with 186 * a distinguishing path. 187 */ getPath()188 public CldrPath getPath() { 189 return path; 190 } 191 192 /** 193 * Returns a combined full path string in the XPath style {@code //foo/bar[@x="y"]/baz}, 194 * with value attributes inserted in correct DTD order for each path element. 195 * 196 * <p>Note that while in most cases the values attributes simply follow the path attributes on 197 * each element, this is not necessarily always true, and DTD ordering can place value 198 * attributes before path attributes in an element. 199 * 200 * @return the full XPath representation containing both distinguishing and value attributes. 201 */ getFullPath()202 public String getFullPath() { 203 return getPath().getFullPath(this); 204 } 205 206 /** {@inheritDoc} */ 207 @Override equals(Object obj)208 public boolean equals(Object obj) { 209 if (obj == this) { 210 return true; 211 } 212 if (!(obj instanceof CldrValue)) { 213 return false; 214 } 215 CldrValue other = (CldrValue) obj; 216 return this.path.equals(other.path) 217 && this.value.equals(other.value) 218 && this.attributes.equals(other.attributes); 219 } 220 221 /** {@inheritDoc} */ 222 @Override hashCode()223 public int hashCode() { 224 return hashCode; 225 } 226 227 /** @return a debug-only representation of this CLDR value. */ 228 @Override toString()229 public String toString() { 230 if (value.isEmpty()) { 231 return String.format("attributes=%s, path=%s", attributes, path); 232 } else if (attributes.isEmpty()) { 233 return String.format("value=\"%s\", path=%s", value, path); 234 } else { 235 return String.format("value=\"%s\", attributes=%s, path=%s", value, attributes, path); 236 } 237 } 238 } 239