• 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.Preconditions.checkArgument;
19 import static com.google.common.base.Preconditions.checkNotNull;
20 import static com.google.common.collect.ImmutableList.toImmutableList;
21 import static com.google.common.collect.ImmutableSet.toImmutableSet;
22 import static com.google.common.collect.ImmutableSortedSet.toImmutableSortedSet;
23 import static com.google.i18n.phonenumbers.metadata.table.DiffKey.Status.LHS_CHANGED;
24 import static com.google.i18n.phonenumbers.metadata.table.DiffKey.Status.LHS_ONLY;
25 import static com.google.i18n.phonenumbers.metadata.table.DiffKey.Status.RHS_CHANGED;
26 import static com.google.i18n.phonenumbers.metadata.table.DiffKey.Status.RHS_ONLY;
27 import static com.google.i18n.phonenumbers.metadata.table.DiffKey.Status.UNCHANGED;
28 
29 import com.google.auto.value.AutoValue;
30 import com.google.common.base.CharMatcher;
31 import com.google.common.collect.ImmutableList;
32 import com.google.common.collect.ImmutableMap;
33 import com.google.common.collect.ImmutableSet;
34 import com.google.common.collect.ImmutableSortedSet;
35 import com.google.common.collect.Maps;
36 import com.google.common.collect.Ordering;
37 import com.google.common.collect.Sets;
38 import com.google.common.collect.Table;
39 import com.google.common.collect.Tables;
40 import com.google.common.collect.TreeBasedTable;
41 import com.google.common.escape.CharEscaperBuilder;
42 import com.google.common.escape.Escaper;
43 import java.io.BufferedReader;
44 import java.io.IOException;
45 import java.io.Reader;
46 import java.io.StringWriter;
47 import java.io.Writer;
48 import java.util.Arrays;
49 import java.util.Collections;
50 import java.util.LinkedHashMap;
51 import java.util.List;
52 import java.util.Map;
53 import java.util.Objects;
54 import java.util.Optional;
55 import java.util.Set;
56 import java.util.TreeMap;
57 import java.util.function.Consumer;
58 import java.util.function.Predicate;
59 import java.util.function.Supplier;
60 import java.util.stream.Stream;
61 import javax.annotation.Nullable;
62 
63 /**
64  * A general tabular representation of {@link Column} based data, which can include range data
65  * (via {@link RangeTable}) or other tabular data using a specified row key implementation.
66  *
67  * @param <K> the row key type.
68  */
69 @AutoValue
70 public abstract class CsvTable<K> {
71   // Trim whitespace (since CSV files may be textually aligned) but don't allow multiline values
72   // (we handle that by JSON style escaping to keep the "one row per line" assumption true).
73   public static final String DEFAULT_DELIMETER = ";";
74   private static final CsvParser CSV_PARSER =
75       CsvParser.withSeparator(DEFAULT_DELIMETER.charAt(0)).trimWhitespace();
76 
77   /**
78    * Mode to control how diffs are generated. If a diff table, rows have an additional
79    * {@code Status} applied to describe whether they are unchanged, modified or exclusive (i.e.
80    * exist only in one of the source tables).
81    */
82   public enum DiffMode {
83     /** Include all rows in the "diff table" (unchanged, modified or exclusive). */
84     ALL,
85     /** Include only changed rows in the "diff table" (modified or exclusive). */
86     CHANGES,
87     /** Include only left-hand-side rows in the "diff table" (unchanged, modified or exclusive). */
88     LHS,
89     /** Include only right-hand-side rows in the "diff table" (unchanged, modified or exclusive). */
90     RHS,
91   }
92 
93   /** A simple builder for programmatic generation of CSV tables. */
94   public static final class Builder<T> {
95     private final CsvSchema<T> schema;
96     private final Table<T, Column<?>, Object> table;
97 
Builder(CsvSchema<T> schema)98     private Builder(CsvSchema<T> schema) {
99       this.schema = checkNotNull(schema);
100 
101       // Either use insertion order or sorted order for rows (depends on schema).
102       if (schema.rowOrdering().isPresent()) {
103         this.table = TreeBasedTable.create(schema.rowOrdering().get(), schema.columnOrdering());
104       } else {
105         this.table = Tables.newCustomTable(
106             new LinkedHashMap<>(),
107             () -> new TreeMap<>(schema.columnOrdering()));
108       }
109     }
110 
111     /**
112      * Puts a row into the table using the specific mappings (potentially overwriting any existing
113      * row).
114      */
putRow(T key, Map<Column<?>, ?> row)115     public Builder<T> putRow(T key, Map<Column<?>, ?> row) {
116       table.rowMap().remove(key);
117       return addRow(key, row);
118     }
119 
120     /**
121      * Adds a new row to the table using the specific mappings (the row must not already be
122      * present).
123      */
addRow(T key, Map<Column<?>, ?> row)124     public Builder<T> addRow(T key, Map<Column<?>, ?> row) {
125       checkArgument(!table.containsRow(key), "row '%s' already added\n%s", key, this);
126       row.forEach((c, v) -> table.put(key, c, v));
127       return this;
128     }
129 
130     /**
131      * Adds a new row to the table using the specific mappings (the row must not already be
132      * present).
133      */
addRow(T key, List<Assignment<?>> row)134     public Builder<T> addRow(T key, List<Assignment<?>> row) {
135       checkArgument(!table.containsRow(key), "row '%s' already added\n%s", key, this);
136       put(key, row);
137       return this;
138     }
139 
140     /** Puts (overwrites) a single value in the table. */
put(T key, Column<V> c, @Nullable V v)141     public <V extends Comparable<V>> Builder<T> put(T key, Column<V> c, @Nullable V v) {
142       schema.columns().checkColumn(c);
143       if (v != null) {
144         table.put(key, c, c.cast(v));
145       } else {
146         table.remove(key, c);
147       }
148       return this;
149     }
150 
151     /** Puts (overwrites) a sequence of values in the table. */
put(T key, Iterable<Assignment<?>> assign)152     public Builder<T> put(T key, Iterable<Assignment<?>> assign) {
153       for (Assignment<?> a : assign) {
154         if (a.value().isPresent()) {
155           table.put(key, a.column(), a.value().get());
156         } else {
157           table.remove(key, a.column());
158         }
159       }
160       return this;
161     }
162 
163     /** Puts (overwrites) a sequence of values in the table. */
put(T key, Assignment<?>... assign)164     public Builder<T> put(T key, Assignment<?>... assign) {
165       return put(key, Arrays.asList(assign));
166     }
167 
168     /** Returns an unmodifiable view of the keys for the table. */
getKeys()169     public Set<T> getKeys() {
170       return Collections.unmodifiableSet(table.rowKeySet());
171     }
172 
173     /** Gets a single value in the table (or null). */
get(T key, Column<V> c)174     public <V extends Comparable<V>> V get(T key, Column<V> c) {
175       return c.cast(table.get(key, c));
176     }
177 
178     /** Removes an entire row from the table (does nothing if the row did no exist). */
removeRow(T key)179     public Builder<T> removeRow(T key) {
180       table.rowKeySet().remove(key);
181       return this;
182     }
183 
184     /** Filters the rows of a table, keeping those which match the given predicate. */
filterRows(Predicate<T> predicate)185     public Builder<T> filterRows(Predicate<T> predicate) {
186       Set<T> rows = table.rowKeySet();
187       // Copy to avoid concurrent modification exception.
188       for (T key : ImmutableSet.copyOf(table.rowKeySet())) {
189         if (!predicate.test(key)) {
190           rows.remove(key);
191         }
192       }
193       return this;
194     }
195 
196     /** Filters the columns of a table, keeping only those which match the given predicate. */
filterColumns(Predicate<Column<?>> predicate)197     public Builder<T> filterColumns(Predicate<Column<?>> predicate) {
198       Set<Column<?>> toRemove =
199           table.columnKeySet().stream().filter(predicate.negate()).collect(toImmutableSet());
200       table.columnKeySet().removeAll(toRemove);
201       return this;
202     }
203 
204     /** Builds the immutable CSV table. */
build()205     public CsvTable<T> build() {
206       return from(schema, table);
207     }
208 
209     @Override
toString()210     public String toString() {
211       return build().toString();
212     }
213   }
214 
215   /** Returns a builder for a CSV table with the expected key and column semantics. */
builder(CsvSchema<K> schema)216   public static <K> Builder<K> builder(CsvSchema<K> schema) {
217     return new Builder<>(schema);
218   }
219 
220   /** Returns a CSV table based on the given table with the expected key and column semantics. */
from(CsvSchema<K> schema, Table<K, Column<?>, Object> table)221   public static <K> CsvTable<K> from(CsvSchema<K> schema, Table<K, Column<?>, Object> table) {
222     ImmutableSet<Column<?>> columns = table.columnKeySet().stream()
223         .sorted(schema.columnOrdering())
224         .collect(toImmutableSet());
225     columns.forEach(schema.columns()::checkColumn);
226     return new AutoValue_CsvTable<>(
227         schema,
228         ImmutableMap.copyOf(Maps.transformValues(table.rowMap(), ImmutableMap::copyOf)),
229         columns);
230   }
231 
232   /**
233    * Imports a semicolon separated CSV file. The CSV file needs to have the following layout:
234    * <pre>
235    * Key1 ; Key2 ; Column1 ; Column2 ; Column3
236    * k1   ; k2   ; OTHER   ; "Text"  ; true
237    * ...
238    * </pre>
239    * Where the first {@code N} columns represent the row key (as encapsulated by the key
240    * {@link CsvKeyMarshaller}) and the remaining columns correspond to the given {@link Schema}
241    * via the column names.
242    * <p>
243    * Column values are represented in a semi-typed fashion according to the associated column (some
244    * columns require values to be escaped, others do not). Note that it's the column that defines
245    * whether the value needs escaping, not the content of the value itself (all values in a String
246    * column are required to be quoted).
247    */
importCsv(CsvSchema<K> schema, Reader csv)248   public static <K> CsvTable<K> importCsv(CsvSchema<K> schema, Reader csv) throws IOException {
249     return importCsv(schema, csv, CSV_PARSER);
250   }
251 
252   /** Imports a CSV file using a specified parser. */
importCsv(CsvSchema<K> schema, Reader csv, CsvParser csvParser)253   public static <K> CsvTable<K> importCsv(CsvSchema<K> schema, Reader csv, CsvParser csvParser)
254       throws IOException {
255     TableParser<K> parser = new TableParser<>(schema);
256     try (BufferedReader r = new BufferedReader(csv)) {
257       csvParser.parse(
258           r.lines(),
259           row -> parser.accept(
260               row.map(CsvTable::unescapeSingleLineCsvText).collect(toImmutableList())));
261     }
262     return parser.done();
263   }
264 
265   /**
266    * Imports a sequence of rows to create a CSV table. The values in the rows are unescaped and
267    * require no explicit parsing.
268    */
importRows(CsvSchema<K> schema, Supplier<List<String>> rows)269   public static <K> CsvTable<K> importRows(CsvSchema<K> schema, Supplier<List<String>> rows) {
270     TableParser<K> parser = new TableParser<>(schema);
271     List<String> row;
272     while ((row = rows.get()) != null) {
273       parser.accept(row);
274     }
275     return parser.done();
276   }
277   /**
278    * Creates a "diff table" based on the given left and right table inputs. The resulting table
279    * has a new key column which indicates (via the {@code Status} enum) how rows difference between
280    * the left and right tables.
281    */
diff(CsvTable<K> lhs, CsvTable<K> rhs, DiffMode mode)282   public static <K> CsvTable<DiffKey<K>> diff(CsvTable<K> lhs, CsvTable<K> rhs, DiffMode mode) {
283     checkArgument(lhs.getSchema().equals(rhs.getSchema()), "Cannot diff with different schemas");
284     checkNotNull(mode, "Must specify a diff mode");
285 
286     CsvKeyMarshaller<DiffKey<K>> marshaller = DiffKey.wrap(lhs.getSchema().keyMarshaller());
287     CsvSchema<DiffKey<K>> diffSchema = CsvSchema.of(marshaller, lhs.getSchema().columns());
288 
289     Builder<DiffKey<K>> diff = CsvTable.builder(diffSchema);
290     if (mode != DiffMode.RHS) {
291       Sets.difference(lhs.getKeys(), rhs.getKeys())
292           .forEach(k -> diff.addRow(DiffKey.of(LHS_ONLY, k), lhs.getRow(k)));
293     }
294     if (mode != DiffMode.LHS) {
295       Sets.difference(rhs.getKeys(), lhs.getKeys())
296           .forEach(k -> diff.addRow(DiffKey.of(RHS_ONLY, k), rhs.getRow(k)));
297     }
298     for (K key : Sets.intersection(lhs.getKeys(), rhs.getKeys())) {
299       Map<Column<?>, Object> lhsRow = lhs.getRow(key);
300       Map<Column<?>, Object> rhsRow = rhs.getRow(key);
301       if (lhsRow.equals(rhsRow)) {
302         if (mode != DiffMode.CHANGES) {
303           diff.addRow(DiffKey.of(UNCHANGED, key), lhsRow);
304         }
305       } else {
306         if (mode != DiffMode.RHS) {
307           diff.addRow(DiffKey.of(LHS_CHANGED, key), lhsRow);
308         }
309         if (mode != DiffMode.LHS) {
310           diff.addRow(DiffKey.of(RHS_CHANGED, key), rhsRow);
311         }
312       }
313     }
314     return diff.build();
315   }
316 
317   /** Returns the schema for this table. */
getSchema()318   public abstract CsvSchema<K> getSchema();
319 
320   /** Returns the rows of the table (not public to avoid access to untyped access). */
321   // Note that this cannot easily be replaced by ImmutableTable (as of Jan 2019) because
322   // ImmutableTable has severe limitations on how row/column ordering is handled that make the
323   // row/column ordering required in CsvTable currently impossible.
getRows()324   abstract ImmutableMap<K, ImmutableMap<Column<?>, Object>> getRows();
325 
326   /**
327    * Returns the set of columns for the table (excluding the synthetic key columns, which are
328    * handled by the marshaller).
329    */
getColumns()330   public abstract ImmutableSet<Column<?>> getColumns();
331 
332   /** Returns whether a row is in the table. */
isEmpty()333   public boolean isEmpty() {
334     return getRows().isEmpty();
335   }
336 
337   /** Returns the set of keys for the table. */
getKeys()338   public ImmutableSet<K> getKeys() {
339     return getRows().keySet();
340   }
341 
342   /** Returns a single row as a map of column assignments. */
getRow(K rowKey)343   public ImmutableMap<Column<?>, Object> getRow(K rowKey) {
344     ImmutableMap<Column<?>, Object> row = getRows().get(rowKey);
345     return row != null ? row : ImmutableMap.of();
346   }
347 
348   /** Returns whether a row is in the table. */
containsRow(K rowKey)349   public boolean containsRow(K rowKey) {
350     return getKeys().contains(rowKey);
351   }
352 
toBuilder()353   public Builder<K> toBuilder() {
354     Builder<K> builder = builder(getSchema());
355     getRows().forEach(builder::putRow);
356     return builder;
357   }
358 
359   /** Returns the table column names, including the key columns, in schema order. */
getCsvHeader()360   public Stream<String> getCsvHeader() {
361     return Stream.concat(
362             getSchema().keyMarshaller().getColumns().stream(),
363             getColumns().stream().map(Column::getName));
364   }
365 
366   /** Returns the unescaped CSV values for the specified row, in order. */
getCsvRow(K key)367   public Stream<String> getCsvRow(K key) {
368     checkArgument(getKeys().contains(key), "no such row: %s", key);
369     // Note that we pass the raw value (possibly null) to serialize so that we don't conflate
370     // missing and default values.
371     return Stream.concat(
372         getSchema().keyMarshaller().serialize(key),
373         getColumns().stream().map(c -> c.serialize(getOrNull(key, c))));
374   }
375 
376   /**
377    * Exports the given table by writing its values as semicolon separated "CSV", with or without
378    * alignment. For example (with alignment):
379    *
380    * <pre>
381    * Key1 ; Key2 ; Column1 ; Column2 ; Column3
382    * k1   ; k2   ; OTHER   ; "Text"  ; true
383    * ...
384    * </pre>
385    *
386    * Where the first {@code N} columns represent the row key (as encapsulated by the key {@link
387    * CsvKeyMarshaller}) and the remaining columns correspond to the given {@link Schema} via the
388    * column names.
389    */
exportCsv(Writer writer, boolean align)390   public boolean exportCsv(Writer writer, boolean align) {
391     return exportCsvHelper(writer, align, getColumns());
392   }
393 
394   /**
395    * Exports the given table by writing its values as semicolon separated "CSV", with or without
396    * alignment. For example (with alignment):
397    *
398    * <pre>
399    * Key1 ; Key2 ; Column1 ; Column2 ; Column3
400    * k1   ; k2   ; OTHER   ; "Text"  ; true
401    * ...
402    * </pre>
403    *
404    * Where the first {@code N} columns represent the row key (as encapsulated by the key {@link
405    * CsvKeyMarshaller}) and the remaining columns correspond to the given {@link Schema} via the
406    * column names. This will add columns that are part of the schema for the given table but have no
407    * assigned values.
408    */
exportCsvWithEmptyColumnsPresent(Writer writer, boolean align)409   public boolean exportCsvWithEmptyColumnsPresent(Writer writer, boolean align) {
410 
411     return exportCsvHelper(
412         writer,
413         align,
414         Stream.concat(getSchema().columns().getColumns().stream(), getColumns().stream())
415             .collect(ImmutableSet.toImmutableSet()));
416   }
417 
exportCsvHelper( Writer writer, boolean align, ImmutableSet<Column<?>> columnsToExport)418   private boolean exportCsvHelper(
419       Writer writer, boolean align, ImmutableSet<Column<?>> columnsToExport) {
420 
421     if (isEmpty()) {
422       // Exit for empty tables (CSV file is truncated). The caller may then delete the empty file.
423       return false;
424     }
425     CsvTableCollector collector = new CsvTableCollector(align);
426     collector.accept(
427         Stream.concat(
428                 getSchema().keyMarshaller().getColumns().stream(),
429                 columnsToExport.stream().map(Column::getName))
430             .distinct());
431     for (K k : getKeys()) {
432       // Format raw values (possibly null) to avoid default values everywhere.
433       collector.accept(
434           Stream.concat(
435               getSchema().keyMarshaller().serialize(k),
436               columnsToExport.stream().map(c -> formatValue(c, getOrNull(k, c)))));
437     }
438     collector.writeCsv(writer);
439     return true;
440   }
441 
getOrNull(K rowKey, Column<T> column)442   @Nullable private <T extends Comparable<T>> T getOrNull(K rowKey, Column<T> column) {
443     return column.cast(getRow(rowKey).get(column));
444   }
445 
446   /**
447    * Returns the value from the underlying table for the given row and column if present.
448    */
get(K rowKey, Column<T> column)449   public <T extends Comparable<T>> Optional<T> get(K rowKey, Column<T> column) {
450     return Optional.ofNullable(getOrNull(rowKey, column));
451   }
452 
453   /**
454    * Returns the value from the underlying table for the given row and column, or the (non-null)
455    * default value.
456    */
getOrDefault(K rowKey, Column<T> column)457   public <T extends Comparable<T>> T getOrDefault(K rowKey, Column<T> column) {
458     T value = getOrNull(rowKey, column);
459     return value != null ? value : column.defaultValue();
460   }
461 
462   /**
463    * Returns the set of unique values in the given column. Note that if some rows do not have a
464    * value, then this will NOT result in the column default value being in the returned set. An
465    * empty column will result in an empty set being returned here.
466    */
getValues(Column<T> column)467   public <T extends Comparable<T>> ImmutableSortedSet<T> getValues(Column<T> column) {
468     return getKeys().stream()
469         .map(k -> getOrNull(k, column))
470         .filter(Objects::nonNull)
471         .collect(toImmutableSortedSet(Ordering.natural()));
472   }
473 
474   @Override
toString()475   public final String toString() {
476     StringWriter w = new StringWriter();
477     exportCsv(w, true);
478     return w.toString();
479   }
480 
481   /** Parses CSV data on per-row basis, deserializing keys and adding values to a table. */
482   static class TableParser<K> implements Consumer<List<String>> {
483     private final Builder<K> table;
484     // Set when the header row is processed.
485     private ImmutableList<Column<?>> columns = null;
486 
TableParser(CsvSchema<K> schema)487     TableParser(CsvSchema<K> schema) {
488       this.table = builder(schema);
489     }
490 
491     @Override
accept(List<String> row)492     public void accept(List<String> row) {
493       if (columns == null) {
494         columns = table.schema.parseHeader(row);
495       } else {
496         table.schema.parseRow(columns, row, table::addRow);
497       }
498     }
499 
done()500     public CsvTable<K> done() {
501       return table.build();
502     }
503   }
504 
505   // Newlines can, in theory, be emitted "raw" in the CSV output inside a quoted string, but
506   // this breaks all sorts of nice properties of CSV files, since there's no longer one row per
507   // line. This export process escapes literal newlines and other control characters into Json
508   // like escape sequences ('\n', '\t', '\\' etc...). Unlike Json however, any double-quotes are
509   // _not_ escaped via '\' since the CSV way to escape those is via doubling. We leave other
510   // non-ASCII characters as-is, since this is meant to be as human readable as possible.
511   private static final Escaper ESCAPER = new CharEscaperBuilder()
512       .addEscape('\n', "\\n")
513       .addEscape('\r', "\\r")
514       .addEscape('\t', "\\t")
515       .addEscape('\\', "\\\\")
516       // This is a special case only required when writing CSV file (since the parser handles
517       // unescaping quotes when they are read back in). In theory it should be part of a separate
518       // step during CSV writing, but it's not worth splitting it out. This is not considered an
519       // unsafe char (since it definitely does appear).
520       .addEscape('"', "\"\"")
521       .toEscaper();
522 
523   private static final CharMatcher ESCAPED_CHARS = CharMatcher.anyOf("\n\r\t\\");
524   private static final CharMatcher UNSAFE_CHARS =
525       CharMatcher.javaIsoControl().and(ESCAPED_CHARS.negate());
526 
formatValue(Column<?> column, @Nullable Object value)527   private static String formatValue(Column<?> column, @Nullable Object value) {
528     String unescaped = column.serialize(value);
529     if (unescaped.isEmpty()) {
530       return unescaped;
531     }
532     // Slightly risky with enums, since an enum could have ';' in its toString() representation.
533     // However since columns and their semantics are tightly controlled, this should never happen.
534     if (Number.class.isAssignableFrom(column.type())
535         || column.type() == Boolean.class
536         || column.type().isEnum()) {
537       checkArgument(ESCAPED_CHARS.matchesNoneOf(unescaped), "Bad 'safe' value: %s", unescaped);
538       return unescaped;
539     }
540     return escapeForSingleLineCsv(unescaped);
541   }
542 
543   /**
544    * Escapes and quotes an arbitrary text string, ensuring it is safe for use as a single-line CSV
545    * value. Newlines, carriage returns and tabs are backslash escaped (as is backslash itself) and
546    * other ISO control characters are not permitted.
547    *
548    * <p>The purpose of this method is to make arbitrary Unicode text readable in a single line of
549    * a CSV file so that we can rely on per-line processing tools, such as "grep" or "sed" if needed
550    * without requiring expensive conversion to/from a spreadsheet.
551    */
escapeForSingleLineCsv(String unescaped)552   public static String escapeForSingleLineCsv(String unescaped) {
553     checkArgument(UNSAFE_CHARS.matchesNoneOf(unescaped), "Bad string value: %s", unescaped);
554     return '"' + ESCAPER.escape(unescaped) + '"';
555   }
556 
557   /**
558    * Unescapes a line of text escaped by {@link #escapeForSingleLineCsv(String)} to restore literal
559    * newlines and other backslash-escaped characters. Note that if the given string already has
560    * newlines present, they are preserved but will then be escaped if the text is re-escaped later.
561    */
unescapeSingleLineCsvText(String s)562   public static String unescapeSingleLineCsvText(String s) {
563     int i = s.indexOf('\\');
564     if (i == -1) {
565       return s;
566     }
567     StringBuilder out = new StringBuilder();
568     int start = 0;
569     do {
570       out.append(s, start, i);
571       char c = s.charAt(++i);
572       out.append(checkNotNull(UNESCAPE.get(c), "invalid escape sequence: \\%s", c));
573       start = i + 1;
574       i = s.indexOf('\\', start);
575     } while (i != -1);
576     return out.append(s, start, s.length()).toString();
577   }
578 
579   private static final ImmutableMap<Character, Character> UNESCAPE =
580       ImmutableMap.<Character, Character>builder()
581           .put('n', '\n')
582           .put('r', '\r')
583           .put('t', '\t')
584           .put('\\', '\\')
585           .build();
586 
587   // Visible for AutoValue only.
CsvTable()588   CsvTable() {}
589 }
590