• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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