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 import static com.google.common.base.Preconditions.checkState;
6 import static com.google.common.collect.ImmutableTable.toImmutableTable;
7 import static java.util.function.Function.identity;
8 import static org.unicode.cldr.util.DtdData.AttributeStatus.distinguished;
9 import static org.unicode.cldr.util.DtdData.AttributeStatus.value;
10 
11 import java.util.Arrays;
12 import java.util.List;
13 import java.util.Objects;
14 import java.util.Optional;
15 
16 import org.unicode.cldr.util.DtdData.Attribute;
17 
18 import com.google.common.base.Ascii;
19 import com.google.common.base.CharMatcher;
20 import com.google.common.base.Splitter;
21 import com.google.common.collect.ImmutableList;
22 import com.google.common.collect.ImmutableTable;
23 
24 /**
25  * Immutable identifier which holds both an attribute's name and the path element it is associated
26  * with. It is expected that key instances will be created as static constants in code rather than
27  * being generated each time they are used.
28  *
29  * <p>As well as providing a key for looking up attribute values from {@link CldrPath} or {@link
30  * CldrValue}, this class offers accessor methods to provide additional common semantics. This
31  * includes checking and parsing boolean values, and splitting lists. It is generally preferred to
32  * use the methods from this class rather than accessing the raw attribute value.
33  *
34  * <p>For example, prefer:
35  * <pre>{@code
36  *   // The attribute value cannot be null.
37  *   String attribute = REQUIRED_ATTRIBUTE_KEY.valueFrom(path);
38  * }</pre>
39  * to:
40  * <pre>{@code
41  *   // This could be null.
42  *   String attribute = path.get(REQUIRED_ATTRIBUTE_KEY);
43  * }</pre>
44  *
45  */
46 // Note: Using Guava's @AutoValue library would remove all this boiler-plate.
47 public final class AttributeKey {
48     // Unsorted cache of all possible known attribute keys (not including keys for elements in
49     // external namespaces (e.g. "icu:").
50     private static final ImmutableTable<String, String, AttributeKey> KNOWN_KEYS =
51         Arrays.stream(CldrDataType.values())
52             .flatMap(CldrDataType::getElements)
53             .flatMap(e -> e.getAttributes().keySet().stream()
54                 .filter(AttributeKey::isKnownAttribute)
55                 .map(a -> new AttributeKey(e.getName(), a.getName())))
56             .distinct()
57             .collect(toImmutableTable(
58                 AttributeKey::getElementName, AttributeKey::getAttributeName, identity()));
59 
isKnownAttribute(Attribute attr)60     private static boolean isKnownAttribute(Attribute attr) {
61         return !attr.isDeprecated() &&
62             (attr.attributeStatus == distinguished || attr.attributeStatus == value);
63     }
64 
65     private static final Splitter LIST_SPLITTER =
66         Splitter.on(CharMatcher.whitespace()).omitEmptyStrings();
67 
68     /**
69      * Common interface to permit both {@link CldrPath} and {@link CldrValue} to have attributes
70      * processed by the methods in this class.
71      */
72     interface AttributeSupplier {
73         /** Returns the raw attribute value, or null. */
get(AttributeKey k)74         /* @Nullable */ String get(AttributeKey k);
75 
76         /** Returns the data type of this supplier. */
getDataType()77         CldrDataType getDataType();
78     }
79 
80     /**
81      * Returns a key which identifies an attribute in either {@link CldrValue} or {@link CldrPath}.
82      *
83      * <p>It is expected that callers will typically store the keys for desired attributes as
84      * constant static fields rather than creating new keys each time they are needed.
85      *
86      * @param elementName the CLDR path element name.
87      * @param attributeName the CLDR attribute name in the specified element.
88      * @return a key to uniquely identify the specified attribute.
89      */
keyOf(String elementName, String attributeName)90     public static AttributeKey keyOf(String elementName, String attributeName) {
91         // No namespace for the element means that:
92         // 1) we don't expect the attribute name to have a namespace either,
93         // 2) the attribute key should be in our cache of known instances.
94         if (elementName.indexOf(':') == -1) {
95             checkArgument(attributeName.indexOf(':') == -1,
96                 "attributes in an external namespace cannot be present in elements in the default"
97                     + " namespace: %s:%s",
98                 elementName, attributeName);
99             return checkNotNull(KNOWN_KEYS.get(elementName, attributeName),
100                 "unknown attribute (was it deprecated?): %s:%s",
101                 elementName, attributeName);
102         }
103         // An element in an external namespace _can_ have an attribute in the default namespace!
104         // (e.g. <icu:dictionary type="Thai" icu:dependency="thaidict.dict"/>)
105         return new AttributeKey(elementName, attributeName);
106     }
107 
108     private final String elementName;
109     private final String attributeName;
110 
AttributeKey(String elementName, String attributeName)111     private AttributeKey(String elementName, String attributeName) {
112         this.elementName = checkValidLabel(elementName, "element name");
113         this.attributeName = checkValidLabel(attributeName, "attribute name");
114     }
115 
116     /** @return the non-empty element name of this key. */
getElementName()117     public String getElementName() {
118         return elementName;
119     }
120 
121     /** @return the non-empty attribute name of this key. */
getAttributeName()122     public String getAttributeName() {
123         return attributeName;
124     }
125 
126     /**
127      * Accessor for required attribute values on a {@link CldrPath} or {@link CldrValue}. Use this
128      * method in preference to the instance's own {@code get()} method in cases where the value is
129      * required or takes an implicit value.
130      *
131      * @param src the {@link CldrPath} or {@link CldrValue} from which the value is to be obtained.
132      * @return the attribute value or, if not present, the specified default.
133      * @throws IllegalStateException if this attribute is optional for the given supplier.
134      */
valueFrom(AttributeSupplier src)135     public String valueFrom(AttributeSupplier src) {
136         checkState(!src.getDataType().isOptionalAttribute(this),
137             "attribute %s is optional in %s, it should be accessed by an optional accessor",
138             this, src.getDataType());
139         // If this fails, it's a sign of an issue in the DTD and/or parser.
140         return checkNotNull(src.get(this), "missing required attribute: %s", this);
141     }
142 
143     /**
144      * Accessor for optional attribute values on a {@link CldrPath} or {@link CldrValue}. Use this
145      * method in preference to the instance's own {@code get()} method, unless efficiency is vital.
146      *
147      * @param src the {@link CldrPath} or {@link CldrValue} from which the value is to be obtained.
148      * @return the attribute value or, if not present, the specified default.
149      * @throws IllegalStateException if this attribute is not optional for the given supplier.
150      */
optionalValueFrom(AttributeSupplier src)151     public Optional<String> optionalValueFrom(AttributeSupplier src) {
152         checkState(src.getDataType().isOptionalAttribute(this),
153             "attribute %s is not optional in %s, it should not be accessed by an optional accessor",
154             this, src.getDataType());
155         return Optional.ofNullable(src.get(this));
156     }
157 
158     /**
159      * Accessor for attribute values on a {@link CldrPath} or {@link CldrValue}. Use this method
160      * in preference to the instance's own {@code get()} method in cases where a non-null value is
161      * required.
162      *
163      * @param src the {@link CldrPath} or {@link CldrValue} from which the value is to be obtained.
164      * @param defaultValue a non-null default returned if the value is not present.
165      * @return the attribute value or, if not present, the specified default.
166      * @throws IllegalStateException if this attribute is not optional for the given supplier.
167      */
valueFrom(AttributeSupplier src, String defaultValue)168     public String valueFrom(AttributeSupplier src, String defaultValue) {
169         checkState(src.getDataType().isOptionalAttribute(this),
170             "attribute %s is not optional in %s, it should not be accessed by an optional accessor",
171             this, src.getDataType());
172         checkNotNull(defaultValue, "default value must not be null");
173         String v = src.get(this);
174         return v != null ? v : defaultValue;
175     }
176 
177     /**
178      * Accessor for attribute values on a {@link CldrPath} or {@link CldrValue}. Use this method
179      * in preference to the instance's own {@code get()} method when an attribute is expected to
180      * only contain a legitimate boolean value.
181      *
182      * @param src the {@link CldrPath} or {@link CldrValue} from which the value is to be obtained.
183      * @param defaultValue a default returned if the value is not present.
184      * @return the attribute value or, if not present, the specified default.
185      */
186     // TODO: Enforce that this is only called for #ENUMERATION attributes with boolean values.
booleanValueFrom(AttributeSupplier src, boolean defaultValue)187     public boolean booleanValueFrom(AttributeSupplier src, boolean defaultValue) {
188         String v = src.get(this);
189         if (v == null) {
190             return defaultValue;
191         } else if (Ascii.equalsIgnoreCase(v, "true")) {
192             return true;
193         } else if (Ascii.equalsIgnoreCase(v, "false")) {
194             return false;
195         }
196         throw new IllegalArgumentException("value of attribute " + this + " is not boolean: " + v);
197     }
198 
199     /**
200      * Accessor for attribute values on a {@link CldrPath} or {@link CldrValue}. Use this method
201      * in preference to the instance's own {@code get()} method when an attribute is expected to
202      * contain a whitespace separated list of values.
203      *
204      * @param src the {@link CldrPath} or {@link CldrValue} from which values are to be obtained.
205      * @return a list of split attribute values, possible empty if the attribute does not exist.
206      */
listOfValuesFrom(AttributeSupplier src)207     public List<String> listOfValuesFrom(AttributeSupplier src) {
208         String v = src.get(this);
209         return v != null ? LIST_SPLITTER.splitToList(v) : ImmutableList.of();
210     }
211 
212     /**
213      * Accessor for attribute values on a {@link CldrPath} or {@link CldrValue} which map to a known
214      * enum. Use this method in preference to the instance's own {@code get()} method in cases where
215      * a non-null value is required.
216      *
217      * @param src the {@link CldrPath} or {@link CldrValue} from which the value is to be obtained.
218      * @param enumType the enum class type of the result.
219      * @return an enum value instance from the underlying attribute value by name.
220      */
221     // TODO: Handle optional enumerations (e.g. PluralRange#start/end).
valueFrom(AttributeSupplier src, Class<T> enumType)222     public <T extends Enum<T>> T valueFrom(AttributeSupplier src, Class<T> enumType) {
223         return Enum.valueOf(enumType, valueFrom(src));
224     }
225 
226     /** {@inheritDoc} */
227     @Override
equals(Object obj)228     public boolean equals(Object obj) {
229         if (obj == this) {
230             return true;
231         }
232         if (!(obj instanceof AttributeKey)) {
233             return false;
234         }
235         AttributeKey other = (AttributeKey) obj;
236         return this.elementName.equals(other.elementName)
237             && this.attributeName.equals(other.attributeName);
238     }
239 
240     /** {@inheritDoc} */
241     @Override
hashCode()242     public int hashCode() {
243         return Objects.hash(elementName, attributeName);
244     }
245 
246     /** Returns a debug-only representation of the qualified attribute key. */
247     @Override
toString()248     public String toString() {
249         return elementName + ":" + attributeName;
250     }
251 
252     // Note: This can be modified if necessary but care must be taken to never allow various
253     // meta-characters in element or attribute names (see CldrPath for the full list).
checkValidLabel(String value, String description)254     private static String checkValidLabel(String value, String description) {
255         checkArgument(!value.isEmpty(), "%s cannot be empty", description);
256         checkArgument(CharMatcher.ascii().matchesAllOf(value),
257             "non-ascii character in %s: %s", description, value);
258         return value;
259     }
260 }
261