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;
019import java.util.Iterator;
020
021import static org.opengion.fukurou.system.HybsConst.CR;                         // 6.4.5.1 (2016/04/28)
022
023/**
024 * CSVTokenizer は、CSVファイルのデータを順次分割する StringTokenizer と非常に
025 * 良く似たクラスです。
026 *
027 * StringTokenizer では、デリミタが連続する場合も、1つのデリミタとするため、
028 * データが存在しない場合の表現が出来ませんでした。(例えば、AA,BB,,DD など)
029 * 現在では、StringTokenizerの代わりに、String#split(String)が推奨されており、
030 * これなら、区切り文字を正規表現で指定できるため、両方の形式に対応できます。
031 * ただし、ダブルクオート等で囲われたデータを塊として処理することが出来ません。
032 * ( AA,BB,"cc,dd",EE などを、「AA」「BB」「cc,dd」「EE」に分割したい、など)
033 *
034 * この、CSVTokenizer クラスでは、データが存在しない場合もトークンとして返します。
035 *
036 * また、ダブルコーテーション("")で囲まれた範囲のデリミタは無視します。
037 * ("") は、2重にすることで、エスケープできます。これにより、改行コードで分割した
038 * 1行の中に、("")が奇数の場合は、次の行は、続いていると判定できます。
039 * ただし、デリミタとしては、文字列は指定できず、cher 1文字 しか指定できません。
040 *
041 * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
042 *
043 * @version  4.0
044 * @author   Kazuhiko Hasegawa
045 * @since    JDK5.0,
046 */
047public class CSVTokenizer implements Iterator<String> {
048        private int             currentPosition;
049        private int             maxPosition;
050        private String  str;
051        private char    delimChar ;
052        private boolean inQuoteFlag ;           // "" クオート処理を行うかどうか
053
054        /**
055         * CSV形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
056         *
057         * @param str           CSV形式の文字列  改行コードを含まない。
058         * @param delim         区切り文字(1文字のみ指定可)
059         * @param inQuote       クオート処理を行うかどうか [true:行う/false:行わない]
060         */
061        public CSVTokenizer( final String str, final char delim, final boolean inQuote ) {
062                currentPosition = 0;
063                this.str        = str;
064                maxPosition     = str.length();
065                delimChar       = delim;
066                inQuoteFlag     = inQuote;
067        }
068
069        /**
070         * CSV形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
071         *
072         * inQuote=true を初期値設定されます。
073         *
074         * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
075         *
076         * @param str CSV形式の文字列  改行コードを含まない。
077         * @param delim  区切り文字(1文字のみ指定可)
078         */
079        public CSVTokenizer( final String str, final char delim ) {
080                this( str, delim, true );
081        }
082
083        /**
084         * CSV形式の 文字列を解析する CSVTokenizer のインスタンスを作成する。
085         *
086         * delim=',' , inQuote=true を初期値設定されます。
087         *
088         * @param str CSV形式の文字列  改行コードを含まない。
089         */
090        public CSVTokenizer( final String str ) {
091                this( str, ',', true );
092        }
093
094        /**
095         * トークナイザの文字列で利用できるトークンがまだあるかどうかを判定します。
096         * このメソッドが true を返す場合、それ以降の引数のない next() への
097         * 呼び出しは適切にトークンを返します。
098         *
099         * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
100         *
101         * @return  文字列内の現在の位置の後ろに 1 つ以上のトークンがある場合だけ true、そうでない場合は false
102         */
103        @Override       // Iterator
104        public boolean hasNext() {
105                final int newPosition = skipDelimiters(currentPosition);
106                return newPosition <= maxPosition ;             // "<" だと末尾の項目を正しく処理できない
107        }
108
109        /**
110         * 文字列トークナイザから次のトークンを返します。
111         *
112         * @og.rev 5.2.0.0 (2010/09/01) トークンの前後が '"'である場合、"で囲われた文字列中の""は"に変換します。
113         * @og.rev 6.4.5.1 (2016/04/28) Iteratorインタフェース化。
114         *
115         * @return      文字列トークナイザからの次のトークン
116         * @throws      NoSuchElementException トークナイザの文字列にトークンが残っていない場合
117        */
118        @Override       // Iterator
119        public String next() {
120                // ">=" では末尾の項目を正しく処理できない。
121                // 末尾の項目が空(カンマで1行が終わる)場合、例外が発生して
122                // しまうので。
123                if( currentPosition > maxPosition ) {
124                        final String errMsg = "currentPosition=[" + currentPosition + "] , maxPosition=[" + maxPosition + "]" + CR
125                                                                        + toString() ;
126                        throw new NoSuchElementException( errMsg );
127                }
128
129                // 3.5.4.7 (2004/02/06)
130                int from = currentPosition;
131                int to   = skipDelimiters( currentPosition );
132                currentPosition = to + 1;
133                // 5.2.0.0 (2010/09/01) トークンの前後が '"'である場合、"で囲われた文字列中の""は"に変換します。
134                String rtn = null;
135                // 3.5.5.8 (2004/05/20) トークンの前後が '"' なら、削除します。
136                if( inQuoteFlag && from < maxPosition && from < to &&
137                        str.charAt(from) == '"' && str.charAt(to-1) == '"' ) {
138                        from++;
139                        to--;
140                        rtn = str.substring( from,to ).replace( "\"\"", "\"" );
141                }
142                else {
143                        rtn = str.substring( from,to );
144                }
145
146                return rtn;
147        }
148
149        /**
150         * 例外を生成せずにトークナイザの <code>next()</code> メソッドを呼び出せる
151         * 回数を計算します。現在の位置は進みません。
152         *
153         * @return  現在の区切り文字を適用したときに文字列に残っているトークンの数
154         * @see     CSVTokenizer#next()
155         */
156        public int countTokens() {
157                int count = 1;
158                int currpos = 0;
159                while( (currpos = skipDelimiters(currpos)) < maxPosition ) {
160                        currpos++;
161                        count++;
162                }
163                return count;
164        }
165
166        /**
167         * 次のカンマがある位置を返す。
168         * カンマが残っていない場合は skipDelimiters() == maxPosition となる。
169         * また最後の項目が空の場合も skipDelimiters() == maxPosition となる。
170         *
171         * @param startPos 検索を開始する位置
172         *
173         * @return 次のカンマがある位置。カンマがない場合は、文字列の長さの値となる。
174         */
175        private int skipDelimiters( final int startPos ) {
176                boolean inquote = false;
177                int position = startPos;
178                while( position < maxPosition ) {
179                        final char ch = str.charAt(position);
180                        if( !inquote && ch == delimChar ) {
181                                break;
182                        } else if( '"' == ch && inQuoteFlag ) {
183                                inquote = !inquote;                             // "" クオート処理を行う
184                        }
185                        position++;
186                }
187                return position;
188        }
189
190        /**
191         * インスタンスの文字列表現を返す。
192         *
193         * @return インスタンスの文字列表現。
194         * @og.rtnNotNull
195         */
196        @Override               // Object
197        public String toString() {
198                return "CSVTokenizer(" + str + ")";
199        }
200}