/* * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.httprpc.io; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.NoSuchElementException; import java.util.stream.Stream; import java.util.stream.StreamSupport; /** * CSV decoder. */ public class CSVDecoder extends Decoder { /** * CSV cursor. */ public static class Cursor implements Iterable<Map<String, String>> { private Reader reader; private char delimiter; private StringBuilder valueBuilder = new StringBuilder(); private ArrayList<String> keys = new ArrayList<>(); private ArrayList<String> values = new ArrayList<>(); private Iterator<Map<String, String>> iterator = new Iterator<Map<String, String>>() { private Boolean hasNext = null; @Override public boolean hasNext() { if (hasNext == null) { try { values.clear(); readValues(reader, values, delimiter); } catch (IOException exception) { throw new RuntimeException(exception); } hasNext = !values.isEmpty(); } return hasNext; } @Override public Map<String, String> next() { if (!hasNext()) { throw new NoSuchElementException(); } LinkedHashMap<String, String> row = new LinkedHashMap<>(); for (int i = 0, n = Math.min(keys.size(), values.size()); i < n; i++) { String key = keys.get(i); if (key.isEmpty()) { continue; } String value = values.get(i); if (value.isEmpty()) { continue; } row.put(key, value); } hasNext = null; return row; } }; private static final int EOF = -1; private Cursor(Reader reader, char delimiter) throws IOException { this.reader = reader; this.delimiter = delimiter; readValues(reader, keys, delimiter); } private void readValues(Reader reader, ArrayList<String> values, char delimiter) throws IOException { int c = reader.read(); while (c != '\r' && c != '\n' && c != EOF) { valueBuilder.setLength(0); boolean quoted = false; if (c == '"') { quoted = true; c = reader.read(); } while ((quoted || (c != delimiter && c != '\r' && c != '\n')) && c != EOF) { valueBuilder.append((char)c); c = reader.read(); if (c == '"') { c = reader.read(); if (c != '"') { quoted = false; } } } if (quoted) { throw new IOException("Unterminated quoted value."); } values.add(valueBuilder.toString()); if (c == delimiter) { c = reader.read(); } } if (c == '\r') { c = reader.read(); if (c != '\n') { throw new IOException("Improperly terminated record."); } } } @Override public Iterator<Map<String, String>> iterator() { return iterator; } /** * Returns a stream over the results. * * @return * A stream over the results. */ public Stream<Map<String, String>> stream() { return StreamSupport.stream(spliterator(), false); } } private char delimiter; /** * Constructs a new CSV decoder. */ public CSVDecoder() { this(','); } /** * Constructs a new CSV decoder. * * @param delimiter * The character to use as a field delimiter. */ public CSVDecoder(char delimiter) { super(StandardCharsets.ISO_8859_1); this.delimiter = delimiter; } @Override @SuppressWarnings("unchecked") public Cursor read(InputStream inputStream) throws IOException { return super.read(inputStream); } @Override @SuppressWarnings("unchecked") public Cursor read(Reader reader) throws IOException { return new Cursor(new BufferedReader(reader), delimiter); } }