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