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