CSV処理

まあ特別なことはこれといってしていない。
ヘッダーの有無とか読み飛ばしとか必要かな・・・?

/**
 * CSVの解析ロジック
 */
public interface Tokenizer {
    /**
     * 1つの文字列を複数の文字列に分割する
     * @param text
     * @return
     */
    String[] toToken(String text);
    /**
     * 複数の文字列を1つの文字列に結合する
     * @param tokens
     * @return
     */
    String toLine(String[] tokens);
    /**
     * 分割する際に次の行と合わせて1つの文字列とする必要があるか否かを返す
     * @param text
     * @return
     */
    boolean hasNextLine(String text);
}
/**
 * 単純に1つの文字を区切り文字として結合・分割する解析ロジック
 */
public class SimpleTokenizer implements Tokenizer {

    // 区切り文字
    // デフォルトは,
    private char delimiter = ',';

    /**
     * 区切り文字を,として解析を行う
     */
    public SimpleTokenizer() {}

    /**
     * 指定した区切り文字で解析を行う
     * @param delimiter
     */
    public SimpleTokenizer(char delimiter) {
        this.delimiter = delimiter;
    }

    /**
     * 配列内の文字列を区切り文字で結合して1つの文字列とする
     */
    @Override
    public String toLine(String[] tokens) {
        return StringUtils.join(tokens, delimiter);
    }

    /**
     * 区切り文字を引数として{@link String#split(String)}を呼び出す
     */
    @Override
    public String[] toToken(String text) {
        return text.split("\\" + String.valueOf(delimiter));
    }

    /**
     * 常にfalseを返す
     */
    @Override
    public boolean hasNextLine(String text) {
        return false;
    }
    
}
import java.util.ArrayList;
import java.util.Arrays;

/**
 * CSVの行を扱うクラス
 */
public class CsvLine extends ArrayList<String> {

    // CSVの解析ロジック
    private Tokenizer tokenizer;

    /**
     * デフォルトの解析ロジックを使用する。
     */
    public CsvLine() {
        this(new SimpleTokenizer());
    }

    /**
     * デフォルトの解析ロジックを使用してtextを配列に分割して保持する。
     * @param text
     */
    public CsvLine(String text) {
        this(text, new SimpleTokenizer());
    }

    /**
     * 指定した解析ロジックを使用する
     * @param tokenizer
     */
    public CsvLine(Tokenizer tokenizer) {
        if (tokenizer == null) {
            throw new IllegalArgumentException("tokenizer must not null");
        }
        this.tokenizer = tokenizer;
    }

    /**
     * 指定した解析ロジックを使用してtextを配列に分割して保持する。
     * @param text
     * @param tokenizer
     */
    public CsvLine(String text, Tokenizer tokenizer) {
        if (text == null) {
            throw new IllegalArgumentException("text must not null");
        }
        if (tokenizer == null) {
            throw new IllegalArgumentException("tokenizer must not null");
        }
        this.tokenizer = tokenizer;
        if (text.length() == 0) {
            return;
        }
        addAll(Arrays.asList(tokenizer.toToken(text)));
    }

    /**
     * デフォルトの解析ロジックを使用して配列を格納する。
     * @param array
     */
    public CsvLine(String[] array) {
        this(array, new SimpleTokenizer());
    }

    /**
     * 指定した解析ロジックを使用して配列を格納する。
     * @param array
     * @param tokenizer
     */
    public CsvLine(String[] array, Tokenizer tokenizer) {
        if (tokenizer == null) {
            throw new IllegalArgumentException("tokenizer must not null");
        }
        addAll(Arrays.asList(array));
        this.tokenizer = tokenizer;
    }

    public void SetTokenizer(Tokenizer tokenizer) {
        if (tokenizer == null) {
            throw new NullPointerException();
        }
        this.tokenizer = tokenizer;
    }

    public Tokenizer getTokenizer() {
        return tokenizer;
    }

    @Override
    public String toString() {
        return tokenizer.toLine(toArray(new String[size()]));
    }

}
import java.io.BufferedReader;
import java.io.IOException;
import java.io.Reader;
import java.util.NoSuchElementException;

/**
 * CSVを1行ずつ読むクラス
 */
public class CsvReader {

    public static final String LINE_SEPARATOR = System.getProperty("line.separator");

    // ベースとなるReader
    private BufferedReader reader;
    // 使用する解析ロジック
    private Tokenizer tokenizer;
    // 現在読込み中の行
    private String currentLine;
    // hasNext()を複数回続けて呼ばれるとデータを読み飛ばしてしまうので、2回目以降は前回の結果を返すだけにする
    private boolean hasNextCalled;
    private boolean hasNextResult;

    /**
     * デフォルトの解析ロジックを使用してCSVを読み込む
     * @param reader
     */
    public CsvReader(Reader reader) {
        this(reader, new SimpleTokenizer());
    }

    /**
     * 指定した解析ロジックを使用してCSVを読み込む
     * @param reader
     * @param tokenizer
     */
    public CsvReader(Reader reader, Tokenizer tokenizer) {
        if (reader == null) {
            throw new IllegalArgumentException("reader must not null");
        }
        if (tokenizer == null) {
            throw new IllegalArgumentException("tokenizer must not null");
        }
        this.reader = new BufferedReader(reader);
        this.tokenizer = tokenizer;
    }

    /**
     * 次の行が存在するか否かを返す
     * @return
     * @throws IOException
     */
    public boolean hasNext() throws IOException {
        if (hasNextCalled) {
            return hasNextResult;
        }
        hasNextResult = false;
        hasNextCalled = false;
        currentLine = null;
        String text = "";
        // Readerから1行読み込む
        while ((text = reader.readLine()) != null) {
            if (currentLine == null) {
                currentLine = "";
            }
            if (currentLine.length() > 0) {
                currentLine += LINE_SEPARATOR;
            }
            currentLine += text;
            // 解析ロジックが次の行に続くと判断すれば、次の行もつなげて1つの行とする
            if (!tokenizer.hasNextLine(currentLine)) {
                hasNextResult = true;
                break;
            }
        }
        hasNextCalled = true;
        return hasNextResult;
    }

    /**
     * 次の行を返す。
     * @return
     */
    public CsvLine next() {
        if (currentLine == null) {
            throw new NoSuchElementException();
        }
        hasNextCalled = false;
        return new CsvLine(currentLine, tokenizer);
    }

}
import java.io.IOException;
import java.io.StringWriter;
import java.io.Writer;

/**
 * CSVを書き込むクラス
 */
public class CsvWriter {

    // 書き込み先のWriter
    private Writer writer;
    // 使用する解析ロジック
    private Tokenizer tokenizer;

    /**
     * デフォルトの解析ロジックを使用してメモリ内のWriterに書き込む。<br/>
     * 結果は{@link #toString()}で取得する。
     */
    public CsvWriter() {
        this(new StringWriter());
    }

    /**
     * デフォルトの解析ロジックを使用して指定したWriterに書き込む。
     * @param writer
     */
    public CsvWriter(Writer writer) {
        this(writer, new SimpleTokenizer());
    }

    /**
     * 指定した解析ロジックを使用して指定したWriterに書き込む
     * @param writer
     * @param tokenizer
     */
    public CsvWriter(Writer writer, Tokenizer tokenizer) {
        if (writer == null) {
            throw new NullPointerException("writer must not null");
        }
        if (tokenizer == null) {
            throw new NullPointerException("tokenizer must not null");
        }
        this.writer = writer;
        this.tokenizer = tokenizer;
    }

    /**
     * CSVを1行書き込む。<br/>
     * 解析ロジックはlineが保持しているものを使用する。
     * @param line
     * @throws IOException
     */
    public void write(CsvLine line) throws IOException {
        writer.write(line.toString());
        writer.write(CsvReader.LINE_SEPARATOR);
    }

    /**
     * CSVを1行書き込む
     * @param array
     * @throws IOException
     */
    public void write(String[] array) throws IOException {
        writer.write(tokenizer.toLine(array));
        writer.write(CsvReader.LINE_SEPARATOR);
    }

    public String toString() {
        return writer.toString();
    }

}
import java.io.IOException;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;

/**
 * CSVファイルを読み書きするクラス
 */
public class CsvFile extends ArrayList<CsvLine> {

    // CSVの解析ロジック
    private Tokenizer tokenizer;
    // adjustの有効/無効フラグ
    private boolean isAdjust = true;

    /**
     * デフォルトの解析ロジックを使用する
     */
    public CsvFile() {
        this(new SimpleTokenizer());
    }

    /**
     * 指定した解析ロジックを使用する
     * @param tokenizer
     */
    public CsvFile(Tokenizer tokenizer) {
        if (tokenizer == null) {
            throw new IllegalArgumentException("tokenizer must not null");
        }
        this.tokenizer = tokenizer;
    }

    /**
     * デフォルトの解析ロジックを使用してCSVデータを読み込む
     * @param source
     * @throws IOException
     */
    public CsvFile(Reader source) throws IOException {
        this(source, new SimpleTokenizer());
    }

    /**
     * 指定した解析ロジックを使用してCSVデータを読み込む
     * @param source
     * @param tokenizer
     * @throws IOException
     */
    public CsvFile(Reader source, Tokenizer tokenizer) throws IOException {
        if (tokenizer == null) {
            throw new IllegalArgumentException("tokenizer must not null");
        }
        this.tokenizer = tokenizer;
        for (CsvReader r = new CsvReader(source, tokenizer); r.hasNext();) {
            add(r.next());
        }
    }

    /**
     * CSVの内容をライタに書き込む。
     * 解析ロジックは自分のメンバを使用する。
     * @param writer
     * @throws IOException
     */
    public void write(Writer writer) throws IOException {
        // 出力前に項目数を合わせる
        adjustTokens();
        for (CsvLine line : this) {
            writer.write(tokenizer.toLine(line.toArray(new String[line.size()])));
            writer.write(CsvReader.LINE_SEPARATOR);
        }
    }

    /**
     * 項目数が異なるアイテムがあった場合に、最大項目数に合わせて空白を追加する
     */
    private void adjustTokens() {
        // 複数の項目を追加する場合など、フラグの値に応じて処理を行ったり行わなかったり。
        if (!isAdjust) {
            return;
        }
        int maxTokens = 0;
        for (CsvLine line : this) {
            maxTokens = Math.max(maxTokens, line.size());
        }
        for (CsvLine line : this) {
            while (line.size() < maxTokens) {
                line.add("");
            }
        }
    }

    @Override
    public boolean add(CsvLine e) {
        boolean ret = super.add(e);
        adjustTokens();
        return ret;
    }

    public boolean add(String e) {
        return add(new CsvLine(e, tokenizer));
    }

    @Override
    public void add(int index, CsvLine element) {
        super.add(index, element);
        adjustTokens();
    }

    public void add(int index, String element) {
        add(index, new CsvLine(element, tokenizer));
    }

    @Override
    public boolean addAll(Collection<? extends CsvLine> c) {
        boolean ret = false;
        isAdjust = false;
        for (CsvLine line : c) {
            if (add(line)) {
                ret = true;
            }
        }
        isAdjust = true;
        adjustTokens();
        return ret;
    }

    @Override
    public boolean addAll(int index, Collection<? extends CsvLine> c) {
        isAdjust = false;
        for (CsvLine line : c) {
            add(index++, line);
        }
        isAdjust = true;
        adjustTokens();
        return true;
    }

    @Override
    public CsvLine set(int index, CsvLine element) {
        CsvLine ret = super.set(index, element);
        adjustTokens();
        return ret;
    }

    public CsvLine set(int index, String element) {
        return set(index, new CsvLine(element, tokenizer));
    };

    @Override
    public String toString() {
        StringWriter writer = new StringWriter();
        try {
            write(writer);
        } catch (IOException e) {
        }
        return writer.toString();
    }

    public void sort() {
        sort(new ArrayComparator<String>(new NumberStringComparator()));
    }

    public void sort(final ArrayComparator<String> comparator) {
        Collections.sort(this, new Comparator<CsvLine>() {
            @Override
            public int compare(CsvLine o1, CsvLine o2) {
                return comparator.compare(o1.toArray(new String[o1.size()]), o2.toArray(new String[o2.size()]));
            }
        });
    }

}
import java.util.ArrayList;
import java.util.List;

/**
 * 引用符、および引用符・区切り文字を含む文字列の解析に対応したロジック
 */
public class QuotableTokenizer implements Tokenizer {

    // 区切り文字
    // デフォルトは,
    private char delimiter = ',';
    // 引用符
    // デフォルトは"
    private char quote = '"';
    // 結合の際、常に引用符をつけるか否かのフラグ
    private boolean allQuote = true;

    /**
     * {@link #QuotableTokenizer(char, char, boolean) QuotableTokenizer(',', '"', true)}と同じ
     */
    public QuotableTokenizer() {}

    /**
     * 区切り文字、引用符などを指定する
     * @param delimiter 区切り文字として使用する文字
     * @param quote 引用符として使用する文字
     * @param allQuote {@link #toLine(String[])}の際、常に引用符をつけるか否かのフラグ。falseにすると文字列の中に引用符・区切り文字・改行を含む場合のみ引用符がつく
     */
    public QuotableTokenizer(char delimiter, char quote, boolean allQuote) {
        this.delimiter = delimiter;
        this.quote = quote;
        this.allQuote = allQuote;
    }

    @Override
    public String toLine(String[] tokens) {
        if (tokens == null) {
            return "";
        }
        StringBuffer ret = new StringBuffer();
        for (int i = 0; i < tokens.length; i++) {
            if (i > 0) {
                ret.append(delimiter);
            }
            ret.append(appendQuote(tokens[i], delimiter, quote, allQuote));
        }
        return ret.toString();
    }

    @Override
    public String[] toToken(String text) {
        return split(text, delimiter, quote);
    }
    
    @Override
    public boolean hasNextLine(String text) {
        return (StringUtils.countChar(text, quote) % 2) != 0;
    }

    public static String appendQuote(String text, char delimiter, char quote, boolean all) {
        if (all || StringUtils.indexAny(text, new char[] {delimiter, quote, '\r', '\n'}) >= 0) {
            text = text.replaceAll("\\" + quote, "$0$0");
            text = quote + text + quote;
        }
        return text;
    }

    public static String[] split(String text, char delimiter, char quote) {
        List<String> ret = new ArrayList<String>();
        String item = "";
        int quoteCount = 0;
        for (int i = 0; i < text.length(); i++) {
            char c = text.charAt(i);
            if (c == quote) {
                quoteCount++;
                item += c;
            } else if (c == delimiter) {
                if ((quoteCount % 2) == 0) {
                    ret.add(trimQuote(item, quote));
                    item = "";
                } else {
                    item += c;
                }
            } else {
                item += c;
            }
        }
        if (item.length() > 0) {
            ret.add(trimQuote(item, quote));
        }
        return ret.toArray(new String[0]);
    }

    public static String trimQuote(String text, char quote) {
        if (text == null) {
            return null;
        }
        if (text.length() >= 2 && text.startsWith(String.valueOf(quote)) && text.endsWith(String.valueOf(quote))) {
            text = text.substring(1, text.length() - 1);
            text = text.replaceAll("\\" + quote + "\\" + quote, "\\" + quote);
        }
        return text;
    }

}