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