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 では、デリミタが連続する場合も、1つのデリミタとするため、
025 * データが存在しない場合の表現が出来ませんでした。(例えば、AA,BB,,DD など)
026 * また、デリミタをデータ中に含む場合の処理が出来ません。( AA,BB,"cc,dd",EE など)
027 * この、CSVTokenizer クラスでは、データが存在しない場合もトークンとして返します。
028 * また、ダブルコーテーション("")で囲まれた範囲のデリミタは無視します。
029 * ただし、デリミタとしては、常に1種類の 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  区切り文字(1文字のみ指定可)
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  区切り文字(1文字のみ指定可)
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//              return str.substring( from,to );
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}