1 /* 2 * Copyright (C) 2017 The Libphonenumber Authors. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.google.i18n.phonenumbers.metadata.table; 17 18 import static com.google.common.base.CharMatcher.inRange; 19 import static com.google.common.base.CharMatcher.whitespace; 20 import static com.google.common.base.Preconditions.checkArgument; 21 import static java.lang.Boolean.FALSE; 22 import static java.lang.Boolean.TRUE; 23 24 import com.google.auto.value.AutoValue; 25 import com.google.common.base.CaseFormat; 26 import com.google.common.base.CharMatcher; 27 import com.google.common.collect.ImmutableMap; 28 import java.util.function.Function; 29 import javax.annotation.Nullable; 30 31 /** 32 * A column specifier which holds a set of values that are allowed with a column. 33 */ 34 @AutoValue 35 public abstract class Column<T extends Comparable<T>> { 36 private static final ImmutableMap<String, Boolean> BOOLEAN_MAP = 37 ImmutableMap.of("true", TRUE, "TRUE", TRUE, "false", FALSE, "FALSE", false); 38 private static final CharMatcher ASCII_LETTER_OR_DIGIT = 39 inRange('a', 'z').or(inRange('A', 'Z')).or(inRange('0', '9')); 40 private static final CharMatcher LOWER_ASCII_LETTER_OR_DIGIT = 41 inRange('a', 'z').or(inRange('0', '9')); 42 private static final CharMatcher LOWER_UNDERSCORE = 43 CharMatcher.is('_').or(LOWER_ASCII_LETTER_OR_DIGIT); 44 45 46 /** 47 * Returns a column for the specified type with a given parsing function. Use alternate helper 48 * methods for creating columns of common types. 49 */ create( Class<T> clazz, String name, T defaultValue, Function<String, T> parseFn)50 public static <T extends Comparable<T>> Column<T> create( 51 Class<T> clazz, String name, T defaultValue, Function<String, T> parseFn) { 52 return new AutoValue_Column<>( 53 checkName(name), clazz, parseFn, String::valueOf, defaultValue, null); 54 } 55 56 /** 57 * Returns a column for the specified enum type. The string representation of a value in this 58 * column is just the {@code toString()} value of the enum. 59 */ of(Class<T> clazz, String name, T defaultValue)60 public static <T extends Enum<T>> Column<T> of(Class<T> clazz, String name, T defaultValue) { 61 return create(clazz, name, defaultValue, s -> Enum.valueOf(clazz, toEnumName(s))); 62 } 63 64 /** 65 * Returns a column for strings. In there serialized form, strings do not preserve leading or 66 * trailing whitespace, unless surrounded by double-quotes (e.g. {@code " foo "}). The quotes are 67 * stripped on parsing and added back for any String value with leading/trailing whitespace. The 68 * default value is the empty string. 69 */ ofString(String name)70 public static Column<String> ofString(String name) { 71 return new AutoValue_Column<>( 72 checkName(name), String.class, Column::trimOrUnquote, Column::maybeQuote, "", null); 73 } 74 75 /** 76 * Returns a column for unsigned integers. The string representation of a value in this column 77 * matches the {@link Integer#toString(int)} value. The default value is {@code 0}. 78 */ ofUnsignedInteger(String name)79 public static Column<Integer> ofUnsignedInteger(String name) { 80 return create(Integer.class, name, 0, Integer::parseUnsignedInt); 81 } 82 83 /** 84 * Returns a column for booleans. The string representation of a value in this column can be any 85 * of "true", "false", "TRUE", "FALSE" (but not things like "True", "T" or "YES"). The default 86 * value is {@code false}. 87 */ ofBoolean(String name)88 public static Column<Boolean> ofBoolean(String name) { 89 return create(Boolean.class, name, false, BOOLEAN_MAP::get); 90 } 91 checkName(String name)92 private static String checkName(String name) { 93 checkArgument(name.indexOf(':') == -1, "invalid column name: %s", name); 94 return name; 95 } 96 97 // Converts to UPPER_UNDERSCORE naming for enums. toEnumName(String name)98 private static String toEnumName(String name) { 99 // Allow conversion for lower_underscore and lowerCamel, since UPPER_UNDERSCORE is so "LOUD". 100 // We can be sloppy with respect to errors here since all runtime exceptions are handled. 101 if (LOWER_ASCII_LETTER_OR_DIGIT.matches(name.charAt(0))) { 102 if (LOWER_UNDERSCORE.matchesAllOf(name)) { 103 name = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_UNDERSCORE, name); 104 } else if (ASCII_LETTER_OR_DIGIT.matchesAllOf(name)) { 105 name = CaseFormat.LOWER_CAMEL.to(CaseFormat.UPPER_UNDERSCORE, name); 106 } else { 107 // Message/type not important here since all exceptions are replaced anyway. 108 throw new IllegalArgumentException(); 109 } 110 } 111 return name; 112 } 113 114 // Trims whitespace from a serialize string, unless the value is surrounded by double-quotes (in 115 // which case the quotes are removed). This is done to permit the rare use of leading/trailing 116 // whitespace in data in a visually distinct and deliberate way. trimOrUnquote(String s)117 private static String trimOrUnquote(String s) { 118 if (s.length() >= 2 && s.startsWith("\"") && s.endsWith("\"")) { 119 return s.substring(1, s.length() - 1); 120 } 121 return whitespace().trimFrom(s); 122 } 123 124 // Surrounds any string with whitespace at either end with double quotes. maybeQuote(String s)125 private static String maybeQuote(String s) { 126 if (s.length() > 0 127 && (whitespace().matches(s.charAt(0)) || whitespace().matches(s.charAt(s.length() - 1)))) { 128 return '"' + s + '"'; 129 } 130 return s; 131 } 132 133 /** Returns the column name (which can be used as a human readable title if needed). */ getName()134 public abstract String getName(); 135 type()136 abstract Class<T> type(); 137 138 // The parsing function from a string to a value. parseFn()139 abstract Function<String, T> parseFn(); 140 // The serialization function from a value to a String. This must be the inverse of the parseFn. serializeFn()141 abstract Function<T, String> serializeFn(); 142 143 /** Default value for this column (inferred for unassigned ranges when a snapshot is built). */ defaultValue()144 public abstract T defaultValue(); 145 146 // This is very private and should only be used in this class. owningGroup()147 @Nullable abstract Column<T> owningGroup(); 148 149 /** Attempts to cast the given instance to the runtime type of this column. */ cast(@ullable Object value)150 @Nullable public final T cast(@Nullable Object value) { 151 return type().cast(value); 152 } 153 154 /** 155 * Returns the value of this column based on its serialized representation (which is not 156 * necessarily its {@code toString()} representation). 157 */ parse(String id)158 @Nullable public final T parse(String id) { 159 if (id.isEmpty()) { 160 return null; 161 } 162 try { 163 // TODO: Technically wrong, since for String columns this will unquote strings. 164 // Hopefully this won't be an issue, since quoting is really only likely to be used for 165 // preserving whitespace (which i 166 167 T value = parseFn().apply(id); 168 if (value != null) { 169 return value; 170 } 171 } catch (RuntimeException e) { 172 // fall through 173 } 174 throw new IllegalArgumentException( 175 String.format("unknown value '%s' in column '%s'", id, getName())); 176 } 177 178 /** 179 * Returns the serialized representation of a value in this column. This is the stored 180 * representation of the value, not the value itself. 181 */ serialize(@ullable Object value)182 public final String serialize(@Nullable Object value) { 183 return (value != null) ? serializeFn().apply(cast(value)) : ""; 184 } 185 186 // Only to be called by ColumnGroup. fromPrototype(String suffix)187 final Column<T> fromPrototype(String suffix) { 188 String name = getName() + ":" + checkName(suffix); 189 return new AutoValue_Column<T>(name, type(), parseFn(), serializeFn(), defaultValue(), this); 190 } 191 isIn(ColumnGroup<?, ?> group)192 final boolean isIn(ColumnGroup<?, ?> group) { 193 return group.prototype().equals(owningGroup()); 194 } 195 196 @Override toString()197 public final String toString() { 198 return "Column{'" + getName() + "'}"; 199 } 200 201 @Override equals(Object obj)202 public final boolean equals(Object obj) { 203 if (!(obj instanceof Column<?>)) { 204 return false; 205 } 206 Column<?> c = (Column<?>) obj; 207 return c.getName().equals(getName()) && c.type().equals(type()); 208 } 209 210 @Override hashCode()211 public final int hashCode() { 212 return getName().hashCode() ^ type().hashCode(); 213 } 214 215 // Visible only for AutoValue Column()216 Column() {} 217 } 218