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