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.model;
017
018import org.opengion.fukurou.util.Closer;                                                // 6.2.0.0 (2015/02/27)
019import static org.opengion.fukurou.util.HybsConst.CR;                   // 6.1.0.0 (2014/12/26) refactoring
020
021import java.io.InputStream;
022import java.io.File;                                                    // 6.2.0.0 (2015/02/27)
023import java.io.IOException;
024import java.util.List;                                                  // 6.0.3.0 (2014/11/13) XSSFイベントモデル
025import java.util.ArrayList;                                             // 6.0.3.0 (2014/11/13) XSSFイベントモデル
026
027import org.apache.poi.xssf.eventusermodel.XSSFReader;
028import org.apache.poi.xssf.model.SharedStringsTable;
029import org.apache.poi.xssf.model.StylesTable;                                   // 6.2.0.0 (2015/02/27)
030import org.apache.poi.xssf.usermodel.XSSFRichTextString;
031import org.apache.poi.openxml4j.opc.OPCPackage;
032import org.apache.poi.openxml4j.exceptions.InvalidFormatException ;
033import org.apache.poi.openxml4j.exceptions.OpenXML4JException ;                 // 6.1.0.0 (2014/12/26) findBugs
034import org.xml.sax.Attributes;
035import org.xml.sax.InputSource;
036import org.xml.sax.SAXException;
037import org.xml.sax.XMLReader;
038import org.xml.sax.helpers.DefaultHandler;
039import org.xml.sax.helpers.XMLReaderFactory;
040
041/**
042 * POI による、Excel(xlsx)の読み取りクラスです。
043 *
044 * xlsx形式のEXCELを、イベント方式でテキストデータを読み取ります。
045 * このクラスでは、XSSF(.xlsx)形式のファイルを、TableModelHelper を介したイベントで読み取ります。
046 * TableModelHelperイベントは、openGion形式のファイル読み取りに準拠した方法をサポートします。
047 * ※ openGion形式のEXCELファイルとは、#NAME 列に、カラム名があり、#で始まる
048 *    レコードは、コメントとして判断し、読み飛ばす処理の事です。
049 *
050 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
051 * @og.rev 6.2.0.0 (2015/02/27) パッケージ変更(util → model),クラス名変更(ExcelReader_XSSF → EventReader_XLSX)
052 * @og.group ファイル入力
053 *
054 * @version  6.0
055 * @author   Kazuhiko Hasegawa
056 * @since    JDK7.0,
057 */
058public final class EventReader_XLSX implements EventReader {
059        /** このプログラムのVERSION文字列を設定します。   {@value} */
060        private static final String VERSION = "6.2.0.0 (2015/02/27)" ;
061
062        /** XSSF(.xlsx)ファイル解析用の、SAXParser {@value} */
063        public static final String SAX_PARSER = "org.apache.xerces.parsers.SAXParser" ;
064
065        /** 6.2.0.0 (2015/02/27) タイプのenum */
066        private static enum XSSFDataType {
067                BOOL,
068                ERROR,
069                FORMULA,
070                INLINESTR,
071                SSTINDEX,
072                NUMBER,
073        }
074
075        /**
076         * 引数ファイル(Excel)を、XSSFイベントモデルを使用してテキスト化します。
077         *
078         * TableModelHelperは、EXCEL読み取り処理用の統一されたイベント処理クラスです。
079         * openGion特有のEXCEL処理方法(#NAME , 先頭行#コメントなど)を実装しています。
080         * これは、HSSFやXSSFの処理を、統一的なイベントモデルで扱うためです。
081         * SSモデルが良いのですが、巨大なXSSF(.xlsx)ファイルを解析すると、OutOfMemoryエラーが
082         * 発生する為、個々に処理する必要があります。
083         * あくまで、読み取り限定であれば、こちらのイベントモデルで十分です。
084         *
085         * @og.rev 6.0.3.0 (2014/11/13) 新規作成
086         * @og.rev 6.1.0.0 (2014/12/26) シートの数のイベント
087         * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更
088         *
089         * @param       file 入力ファイル
090         * @param       helper イベント処理するオブジェクト
091         */
092        @Override
093        public void eventReader( final File file , final TableModelHelper helper ) {
094                OPCPackage      pkg  = null;
095
096                try {
097                        // 6.2.0.0 (2015/02/27) TableModelHelper 変更に伴う修正
098                        helper.startFile( file );
099                        pkg = OPCPackage.open( file );                                                          // InvalidFormatException
100                        final XSSFReader rd = new XSSFReader( pkg );                            // IOException , OpenXML4JException
101
102                        final XMLReader parser = XMLReaderFactory.createXMLReader( SAX_PARSER );        // SAXException
103                        final List<SheetObj> shtList = getSheetList( rd,parser );                                 // SAXException , InvalidFormatException
104                        helper.sheetSize( shtList.size() );                                                     // 6.1.0.0 (2014/12/26)
105
106                        final SharedStringsTable sst = rd.getSharedStringsTable();                      // IOException , InvalidFormatException
107                        final StylesTable styles = rd.getStylesTable();
108
109                        final SheetHandler handler = new SheetHandler( styles,sst,helper );             // ContentHandler のサブクラス
110                        parser.setContentHandler( handler );                                                                    // ContentHandler のサブクラスを設定
111
112                        // Iterator<InputStream> sheets = rd.getSheetsData();
113                        // while(sheets.hasNext()) {
114                        //     sheet = sheets.next();
115                        //     ・・・・・
116                        // }
117                        // 形式で、全シート対象に処理できますが、シート名が取り出せません。
118
119                        InputStream sheet = null;
120                        for( int i=0; i<shtList.size(); i++ ) {
121                                final SheetObj sht = shtList.get(i);
122
123                                if( helper.startSheet( sht.getName() , i ) ) {                          // イベント処理
124                                        try {
125                                                // シートIDは、rId# で取得できる。
126                                                sheet = rd.getSheet( sht.getRid() );                            // IOException , InvalidFormatException
127                                                parser.parse( new InputSource( sheet ) );                       // IOException
128                                        }
129                                        finally {
130                                                Closer.ioClose( sheet );
131                                        }
132                                }
133                                helper.endSheet( i );                                                                           // イベント処理
134                        }
135                }
136                // 6.1.0.0 (2014/12/26) findBugs: Bug type REC_CATCH_EXCEPTION (click for details)
137                // 例外がスローされないのに例外をキャッチしています。
138                catch( OpenXML4JException ex ) {                // サブクラスの、InvalidFormatException も含まれます。
139                        final String errMsg = ".xlsxのファイル解析に失敗しました。"
140                                                                + " filename=" + file + CR
141                                                                + ex.getMessage() ;
142                        throw new RuntimeException( errMsg , ex );
143                }
144                catch( SAXException ex ) {
145                        final String errMsg = "SAX の一般的なエラーまたは警告が発生しました。"
146                                                                + " filename=" + file + CR
147                                                                + ex.getMessage() ;
148                        throw new RuntimeException( errMsg , ex );
149                }
150                catch( IOException ex ) {
151                        final String errMsg = ".xlsxのファイルの読み取りに失敗しました。"
152                                                                + " filename=" + file + CR
153                                                                + ex.getMessage() ;
154                        throw new RuntimeException( errMsg , ex );
155                }
156                finally {
157                        if( pkg != null ) {
158                                pkg.revert();                                           // Close the package WITHOUT saving its content.
159        //                      Closer.ioClose( pkg );                          // OPCPackage を close すると、書き戻しされる。
160                        }
161                        helper.endFile( file );                                 // 6.2.0.0 (2015/02/27)
162                }
163        }
164
165        /**
166         * この内部クラスは、XSSFイベントモデルに基づいた、xlsxファイルを SAX処理します。
167         *
168         * この処理のオリジナルは、https://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/xssf/eventusermodel/examples/FromHowTo.java です。
169         *
170         * また、日付変換で、StylesTable を使用するのは、http://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/xssf/eventusermodel/XLSX2CSV.java です。
171         *
172         * DefaultHandler を継承しており、xlsx の シート処理を行い、カラム番号と値を取得します。
173         * このクラス自体は、内部で使用されるため、TableModelHelper を引数に設定することで、
174         * 外部から、EXCELのセル情報の取得が可能です。
175         *
176         * @og.rev 6.0.3.0 (2014/11/13) 新規作成
177         * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
178         *
179         * @see         org.xml.sax.helpers.DefaultHandler
180         */
181        private static final class SheetHandler extends DefaultHandler {
182                private final SharedStringsTable        sst  ;
183                private final TableModelHelper          helper;
184                private final ExcelStyleFormat          format;
185
186                private String  lastContents ;
187                private XSSFDataType nextDataType = XSSFDataType.NUMBER;                // 6.2.0.0 (2015/02/27) 初期化
188                private String       cellStyleStr ;                                                             // 6.2.0.0 (2015/02/27) 初期化
189
190                private int             rowNo = -1;             // 現在の行番号
191                private int             colNo = -1;             // 現在の列番号
192
193                private boolean isRowSkip       ;       // 行の読み取りを行うかどうか
194
195                /**
196                 * コンストラクター
197                 *
198                 * SharedStringsTable は、テキストの値を持っているオブジェクトです。
199                 * ここで指定する TableModelHelper に対して、パーサー処理の結果がセットされます。
200                 *
201                 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
202                 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
203                 *
204                 * @param       styles StylesTableオブジェクト
205                 * @param       sst    SharedStringsTableオブジェクト
206                 * @param       helper イベント処理するオブジェクト
207                 */
208                public SheetHandler( final StylesTable styles , final SharedStringsTable sst , final TableModelHelper helper ) {
209                        this.sst                = sst;
210                        this.helper             = helper;
211                        format                  = new ExcelStyleFormat( styles );               // 6.2.0.0 (2015/02/27) StylesTable 追加
212                }
213
214                /**
215                 * 要素の開始通知を受け取ります。
216                 *
217                 * インタフェース ContentHandler 内の startElement メソッドをオーバーライドしています。
218                 * パーサは XML 文書内の各要素の前でこのメソッドを呼び出します。
219                 * 各 startElement イベントには対応する endElement イベントがあります。
220                 * これは、要素が空である場合も変わりません。対応する endElement イベントの前に、
221                 * 要素のコンテンツ全部が順番に報告されます。
222                 * ここでは、タグがレベル3以上の場合は、上位タグの内容として取り扱います。よって、
223                 * タグに名前空間が定義されている場合、その属性は削除します。
224                 *
225                 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
226                 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
227                 *
228                 * @param       namespace       名前空間 URI
229                 * @param       localName       前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
230                 * @param       qname           前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列
231                 * @param       attributes      要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト
232                 * @see         org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes )
233                 */
234                @Override
235                public void startElement( final String namespace, final String localName, final String qname, final Attributes attributes ) {
236                        if( "row".equals(qname) ) {                     // row
237                                rowNo = Integer.parseInt( attributes.getValue("r") ) - 1;               // 0 から始まる
238                                isRowSkip = false;
239                        }
240                        else if( isRowSkip ) { return ; }
241                        else if( "c".equals(qname) ) {          // c => cell
242                                final String kigo  = attributes.getValue("r") ;                                 // Excelの行列記号(A1 など)
243                                final int[] rowCol = POIUtil.kigo2rowCol( kigo );                               // Excelの行列記号を、行番号と列番号に分解します。
244
245                //              rowNo = rowCol[0];                      // 行番号・・・・
246                                colNo = rowCol[1];                      // カラム番号
247
248                                // 6.2.0.0 (2015/02/27) 日付型の処理
249                                nextDataType = XSSFDataType.NUMBER;
250                                cellStyleStr = attributes.getValue("s");
251                        //      fmtIdx = -1;
252                        //      fmtStr = null;
253
254                                final String cellType = attributes.getValue("t");
255                                if (     "b".equals(cellType)                   ) { nextDataType = XSSFDataType.BOOL;           }
256                                else if( "e".equals(cellType)                   ) { nextDataType = XSSFDataType.ERROR;          }
257                                else if( "inlineStr".equals(cellType)   ) { nextDataType = XSSFDataType.INLINESTR;      }
258                                else if( "s".equals(cellType)                   ) { nextDataType = XSSFDataType.SSTINDEX;       }
259                                else if( "str".equals(cellType)                 ) { nextDataType = XSSFDataType.FORMULA;        }
260                        }
261                        lastContents = "";              // なんでもクリアしておかないと、関数文字列を拾ってしまう。
262                }
263
264                /**
265                 * 要素の終了通知を受け取ります。
266                 *
267                 * インタフェース ContentHandler 内の endElement メソッドをオーバーライドしています。
268                 * SAX パーサは、XML 文書内の各要素の終わりにこのメソッドを呼び出します。
269                 * 各 endElement イベントには対応する startElement イベントがあります。
270                 * これは、要素が空である場合も変わりません。
271                 *
272                 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
273                 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
274                 *
275                 * @param       namespace       名前空間 URI
276                 * @param       localName       前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
277                 * @param       qname           前置修飾子を持つ XML 1.0 修飾名。修飾名を使用できない場合は空文字列
278                 * @see org.xml.sax.helpers.DefaultHandler#endElement(String , String , String )
279                 */
280                @Override
281                public void endElement( final String namespace, final String localName, final String qname ) {
282                        isRowSkip = helper.isSkip( rowNo );                                                     // イベント
283
284                        if( isRowSkip ) { return ; }
285
286                        String thisStr = null;
287
288                        // v は、値なので、空の場合は、イベントが発生しない。
289                        if( "v".equals(qname) ) {               // v の時のみ、値出力を行う。
290                                // Process the last contents as required.
291                                // Do now, as characters() may be called more than once
292                                switch( nextDataType ) {
293                                        case BOOL:
294                                                final char first = lastContents.charAt(0);
295                                                thisStr = first == '0' ? "FALSE" : "TRUE";
296                                                break;
297
298                                        case ERROR:
299                                                thisStr = "\"ERROR:" + lastContents + '"';
300                                                break;
301
302                                        case FORMULA:
303                                                // A formula could result in a string value,
304                                                // so always add double-quote characters.
305                                                thisStr = '"' + lastContents + '"';
306                                                break;
307
308                                        case INLINESTR:
309                                                // TODO: have seen an example of this, so it's untested.
310                                                thisStr = new XSSFRichTextString( lastContents ).toString();
311                                                break;
312
313                                        case SSTINDEX:
314                                                final String sstIndex = lastContents;
315                                                try {
316                                                        final int idx = Integer.parseInt( sstIndex );
317                                                        thisStr = new XSSFRichTextString( sst.getEntryAt(idx) ).toString();
318                                                }
319                                                catch( NumberFormatException ex ) {
320                                                        final String errMsg = "Failed to parse SST index [" + sstIndex + "]: " + ex.toString() ;
321                                                        System.out.println( errMsg );
322                                                }
323                                                break;
324
325                                        case NUMBER:
326                                                thisStr = format.getNumberValue( cellStyleStr,lastContents );
327                                                break;
328
329                                        default:
330                                                thisStr = "(TODO: Unexpected type: " + nextDataType + ")";
331                                                break;
332                                }
333
334                                // v => contents of a cell
335                                // Output after we've seen the string contents
336                                //           文字列(値)    行      列
337//System.out.println(rowNo+"/"+colNo+"/"+thisStr);
338                                helper.value( thisStr, rowNo , colNo );
339                        }
340                }
341
342                /**
343                 * 要素内の文字データの通知を受け取ります。
344                 *
345                 * インタフェース ContentHandler 内の characters メソッドをオーバーライドしています。
346                 * 各文字データチャンクに対して特殊なアクション (ノードまたはバッファへのデータの追加、
347                 * データのファイルへの出力など) を実行することができます。
348                 *
349                 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
350                 *
351                 * @param       buffer  文字データ配列
352                 * @param       start   配列内の開始位置
353                 * @param       length  配列から読み取られる文字数
354                 * @see org.xml.sax.helpers.DefaultHandler#characters(char[] , int , int )
355                 */
356                @Override
357                public void characters( final char[] buffer, final int start, final int length ) {
358                        if( isRowSkip ) { return ; }
359
360                        lastContents += new String( buffer, start, length );            // StringBuilder#append より速かった。
361                }
362
363        }
364
365        /**
366         * シート一覧を、XSSFReader から取得します。
367         *
368         * 取得元が、XSSFReader なので、xlsx 形式のみの対応です。
369         * 汎用的なメソッドではなく、大きな xlsx ファイルは、通常の DOM処理すると、
370         * 大量のメモリを消費する為、イベントモデルで処理する場合に、使います。
371         *
372         * EXCEL上のシート名を、配列で返します。
373         *
374         * @og.rev 6.0.3.0 (2014/11/13) 新規作成
375         *
376         * @param       rd XSSFReaderオブジェクト
377         * @param       parser XMLReaderオブジェクト
378         * @return      シート名とシートIDを持つオブジェクトのリスト
379         * @throws      SAXException                    SAX の一般的なエラーが発生
380         * @throws      IOException                             SAXパース処理時のI/Oエラー
381         * @throws      InvalidFormatException  よみとったEXCEL ファイルのフォーマットが異なる。
382         */
383        public static List<SheetObj> getSheetList( final XSSFReader rd, final XMLReader parser )
384                                                                                                                throws SAXException,IOException,InvalidFormatException {
385                final List<SheetObj> shtList = new ArrayList();
386
387                parser.setContentHandler(
388                        new DefaultHandler() {
389                                /**
390                                 * 要素の開始通知を受け取ります。
391                                 *
392                                 * @param       uri                     名前空間 URI
393                                 * @param       localName       前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
394                                 * @param       name            前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列
395                                 * @param       attributes      要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト
396                                 * @see         org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes )
397                                 */
398                                public void startElement( final String uri, final String localName, final String name, final Attributes attributes) {
399                                        if("sheet".equals(name)) {
400                                                final String shtNm = attributes.getValue("name");               // シート名
401                                                final String shtId = attributes.getValue("r:id");               // シートID( rId#  #は、1から始まる )
402                                                shtList.add( new SheetObj( shtNm,shtId ) );
403                                        }
404                                }
405                        }
406                );
407
408                InputStream workbk = null;
409                try {
410                        workbk = rd.getWorkbookData();                                                                          // IOException,InvalidFormatException
411                        parser.parse( new InputSource( workbk ) );                                                      // IOException,SAXException
412                }
413                finally {
414                        Closer.ioClose( workbk );
415                }
416
417                return shtList;
418        }
419
420        /**
421         * シート名とシートIDを持つオブジェクトのインナークラス
422         *
423         * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善
424         */
425        private static final class SheetObj {
426                private final String name;
427                private final String rid ;
428
429                /**
430                 * シート名とシートIDを引数に取るコンストラクター
431                 *
432                 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善
433                 *
434                 * @param       name シート名
435                 * @param       rid  シートID(rId#  #は、1から始まる番号)
436                 */
437                public SheetObj( final String name , final String rid ) {
438                        this.name = name;
439                        this.rid  = rid;
440                }
441
442                /**
443                 * シート名を返します。
444                 *
445                 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善
446                 *
447                 * @return      シート名
448                 */
449                public String getName() { return name ; }
450
451                /**
452                 * シートIDを返します。
453                 *
454                 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善
455                 *
456                 * @return      シートID(rId#  #は、1から始まる番号)
457                 */
458                public String getRid()  { return rid ; }
459        }
460
461        /**
462         * アプリケーションのサンプルです。
463         *
464         * 入力ファイル名 は必須で、第一引数固定です。
465         *
466         * Usage: java org.opengion.fukurou.model.EventReader_XLSX 入力ファイル名
467         *
468         * @og.rev 6.0.3.0 (2014/11/13) 新規作成
469         * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更
470         *
471         * @param       args    コマンド引数配列
472         */
473        public static void main( final String[] args ) {
474                final String usageMsg = "Usage: java org.opengion.fukurou.model.EventReader_XLSX 入力ファイル名" ;
475                if( args.length == 0 ) {
476                        System.err.println( usageMsg );
477                        return ;
478                }
479
480                final File file = new File( args[0] );
481                final EventReader reader = new EventReader_XLSX();
482
483                reader.eventReader(                                     // 6.2.0.0 (2015/02/27)
484                        file,
485                        new TableModelHelper() {
486                                /**
487                                 * シートの読み取り開始時にイベントが発生します。
488                                 *
489                                 * @param   shtNm  シート名
490                                 * @param   shtNo  シート番号(0〜)
491                                 * @return  true:シートの読み取り処理を継続します/false:このシートは読み取りません。
492                                 */
493                                public boolean startSheet( final String shtNm,final int shtNo ) {
494                                        System.out.println( "S[" + shtNo + "]=" + shtNm );
495                                        return super.startSheet( shtNm,shtNo );
496                                }
497
498                //              public void columnNames( final String[] names ) {
499                //                      System.out.println( "NM=" + java.util.Arrays.toString( names ) );
500                //              }
501
502                //              public void values( final String[] vals,final int rowNo ) {
503                //                      System.out.println( "V[" + rowNo + "]=" + java.util.Arrays.toString( vals ) );
504                //              }
505
506                //              public boolean isSkip( final int rowNo ) {
507                //                      super.isSkip( rowNo );
508                //                      return false;
509                //              }
510
511                                /**
512                                 * 読み取り状態の時に、rowNo,colNo にあるセルの値を引数にイベントが発生します。
513                                 *
514                                 * @param   val     文字列値
515                                 * @param   rowNo   行番号(0〜)
516                                 * @param   colNo   列番号(0〜)
517                                 * @return  読み取りするかどうか(true:読み取りする/false:読み取りしない)
518                                 */
519                                public boolean value( final String val,final int rowNo,final int colNo ) {
520                                        System.out.println( "R[" + rowNo + "],C[" + colNo + "]=" + val );
521                                        return super.value( val,rowNo,colNo );
522                                }
523                        }
524                );
525        }
526}