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