    001/*
    002 * Copyright (c) 2009 The openGion Project.
    003 *
    004 * Licensed under the Apache License, Version 2.0 (the "License");
    005 * you may not use this file except in compliance with the License.
    006 * You may obtain a copy of the License at
    007 *
    008 *     http://www.apache.org/licenses/LICENSE-2.0
    009 *
    010 * Unless required by applicable law or agreed to in writing, software
    011 * distributed under the License is distributed on an "AS IS" BASIS,
    012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
    013 * either express or implied. See the License for the specific language
    014 * governing permissions and limitations under the License.
    015 */
    016package org.opengion.fukurou.util;
    017
    018import java.util.NoSuchElementException;
    019
    020/**
    021 * CSVTokenizer は、CSVファイルのデータを順次分割する StringTokenizer と非常に
    022 * 良く似たクラスです。
    023 *
    024 * StringTokenizer では、デリミタが連続する場合も、１つのデリミタとするため、
    025 * データが存在しない場合の表現が出来ませんでした。(例えば、AA,BB,,DD など)
    026 * また、デリミタをデータ中に含む場合の処理が出来ません。( AA,BB,"cc,dd",EE など)
    027 * この、CSVTokenizer クラスでは、データが存在しない場合もトークンとして返します。
    028 * また、ダブルコーテーション("")で囲まれた範囲のデリミタは無視します。
    029 * ただし、デリミタとしては、常に１種類の cher 文字 しか指定できません。
    030 *
    031 * @version  4.0
    032 * @author   Kazuhiko Hasegawa
    033 * @since    JDK5.0,
    034 */
    035public class CSVTokenizer {
    036        private int             currentPosition;
    037        private int             maxPosition;
    038        private String  str;
    039        private char    delimChar ;
    040        private boolean inQuoteFlag ;           // "" クオート処理を行うかどうか
    041
    042        /**
    043         * CSV 形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
    044         *
    045         * @param str CSV形式の文字列  改行コードを含まない。
    046         * @param delim  区切り文字(１文字のみ指定可)
    047         * @param inQuote  クオート処理を行うかどうか [true:行う/false:行わない]
    048         */
    049        public CSVTokenizer( final String str, final char delim, final boolean inQuote ) {
    050                currentPosition = 0;
    051                this.str        = str;
    052                maxPosition     = str.length();
    053                delimChar       = delim;
    054                inQuoteFlag     = inQuote;
    055        }
    056
    057        /**
    058         * CSV 形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
    059         *
    060         * @param str CSV形式の文字列  改行コードを含まない。
    061         * @param delim  区切り文字(１文字のみ指定可)
    062         */
    063        public CSVTokenizer( final String str, final char delim ) {
    064                this(str, delim, true);
    065        }
    066
    067        /**
    068         * CSV 形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
    069         *
    070         * @param str CSV形式の文字列  改行コードを含まない。
    071         */
    072        public CSVTokenizer( final String str ) {
    073                this(str, ',', true);
    074        }
    075
    076        /**
    077         * 次のカンマがある位置を返す。
    078         * カンマが残っていない場合は skipDelimiters() == maxPosition となる。
    079         * また最後の項目が空の場合も skipDelimiters() == maxPosition となる。
    080         *
    081         * @param startPos 検索を開始する位置
    082         *
    083         * @return 次のカンマがある位置。カンマがない場合は、文字列の
    084         *         長さの値となる。
    085         */
    086        private int skipDelimiters( final int startPos ) {
    087                boolean inquote = false;
    088                int position = startPos;
    089                while (position < maxPosition) {
    090                        char ch = str.charAt(position);
    091                        if(!inquote && ch == delimChar) {
    092                                break;
    093                        } else if('"' == ch && inQuoteFlag) {
    094                                inquote = !inquote;       // "" クオート処理を行う
    095                        }
    096                        position ++;
    097                }
    098                return position;
    099        }
    100
    101        /**
    102         * トークナイザの文字列で利用できるトークンがまだあるかどうかを判定します。
    103         * このメソッドが true を返す場合、それ以降の引数のない nextToken への
    104         * 呼び出しは適切にトークンを返します。
    105         *
    106         * @return  文字列内の現在の位置の後ろに 1 つ以上の
    107         *          トークンがある場合だけ true、そうでない場合は false
    108         */
    109        public boolean hasMoreTokens() {
    110                int newPosition = skipDelimiters(currentPosition);
    111                return newPosition <= maxPosition ;             // "<" だと末尾の項目を正しく処理できない
    112        }
    113
    114        /**
    115         * 文字列トークナイザから次のトークンを返します。
    116         *
    117         * @og.rev 5.2.0.0 (2010/09/01) トークンの前後が '"'である場合、"で囲われた文字列中の""は"に変換します。
    118         *
    119         * @return     文字列トークナイザからの次のトークン
    120         * @throws  NoSuchElementException トークナイザの文字列に
    121         *             トークンが残っていない場合
    122        */
    123        public String nextToken() {
    124                // ">=" では末尾の項目を正しく処理できない。
    125                // 末尾の項目が空(カンマで1行が終わる)場合、例外が発生して
    126                // しまうので。
    127                if(currentPosition > maxPosition) {
    128                        throw new NoSuchElementException(toString()+"#nextToken");
    129                }
    130
    131                // 3.5.4.7 (2004/02/06)
    132                int from = currentPosition;
    133                int to   = skipDelimiters(currentPosition);
    134                currentPosition = to + 1;
    135                // 5.2.0.0 (2010/09/01) トークンの前後が '"'である場合、"で囲われた文字列中の""は"に変換します。
    136                String rtn = null;
    137                // 3.5.5.8 (2004/05/20) トークンの前後が '"' なら、削除します。
    138                if( inQuoteFlag && from < maxPosition && from < to &&
    139                        str.charAt(from) == '"' && str.charAt(to-1) == '"' ) {
    140                        from++;
    141                        to--;
    142                        rtn = str.substring( from,to ).replace( "\"\"", "\"" );
    143                }
    144                else {
    145                        rtn = str.substring( from,to );
    146                }
    147
    148                return rtn;
    149        }
    150
    151        /**
    152         * 例外を生成せずにトークナイザの <code>nextToken</code> メソッドを呼び出せる
    153         * 回数を計算します。現在の位置は進みません。
    154         *
    155         * @return  現在の区切り文字を適用したときに文字列に残っているトークンの数
    156         * @see     CSVTokenizer#nextToken()
    157         */
    158        public int countTokens() {
    159                int count = 1;
    160                int currpos = 0;
    161                while ((currpos = skipDelimiters(currpos)) < maxPosition) {
    162                        currpos++;
    163                        count++;
    164                }
    165                return count;
    166        }
    167
    168        /**
    169         * インスタンスの文字列表現を返す。
    170         *
    171         * @return インスタンスの文字列表現。
    172         */
    173        @Override
    174        public String toString() {
    175                return "CSVTokenizer(" + str + ")";
    176        }
    177}