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.system.OgRuntimeException ;                 // 6.4.2.0 (2016/01/29)
019import org.opengion.fukurou.system.Closer;                                              // 6.2.0.0 (2015/02/27)
020import static org.opengion.fukurou.system.HybsConst.CR;                 // 6.1.0.0 (2014/12/26) refactoring
021
022import java.io.File;                                                                                    // 6.2.0.0 (2015/02/27)
023import java.io.InputStream;
024import java.io.FileInputStream;
025import java.io.BufferedInputStream;
026import java.io.IOException;
027
028import java.util.List;
029import java.util.ArrayList;
030
031import org.apache.poi.hssf.record.Record;
032import org.apache.poi.hssf.record.CellRecord;
033import org.apache.poi.hssf.record.SSTRecord;
034import org.apache.poi.hssf.record.BOFRecord;
035import org.apache.poi.hssf.record.EOFRecord;
036import org.apache.poi.hssf.record.BoundSheetRecord;
037import org.apache.poi.hssf.record.LabelSSTRecord;
038import org.apache.poi.hssf.record.NumberRecord;
039import org.apache.poi.hssf.record.BoolErrRecord;
040import org.apache.poi.hssf.record.FormulaRecord;
041import org.apache.poi.hssf.record.StringRecord;
042import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
043import org.apache.poi.hssf.eventusermodel.HSSFListener;
044import org.apache.poi.hssf.eventusermodel.HSSFRequest;
045
046import org.apache.poi.hssf.record.ExtendedFormatRecord;                 // 6.2.0.0 (2015/02/27)
047import org.apache.poi.hssf.record.FormatRecord;                                 // 6.2.0.0 (2015/02/27)
048
049import org.apache.poi.ss.usermodel.CellType;                                    // 6.5.0.0 (2016/09/30) poi-3.15
050import org.apache.poi.ss.usermodel.FormulaError;                                // 6.3.1.0 (2015/06/28)
051import org.apache.poi.ss.util.NumberToTextConverter;
052import org.apache.poi.poifs.filesystem.POIFSFileSystem;
053
054/**
055 * POI による、Excel(xls)の読み取りクラスです。
056 *
057 * xls形式のEXCELを、イベント方式でテキストデータを読み取ります。
058 * このクラスでは、HSSF(.xls)形式のファイルを、TableModelHelper を介したイベントで読み取ります。
059 * TableModelHelperイベントは、openGion形式のファイル読み取りに準拠した方法をサポートします。
060 * ※ openGion形式のEXCELファイルとは、#NAME 列に、カラム名があり、#で始まる
061 *    レコードは、コメントとして判断し、読み飛ばす処理の事です。
062 *
063 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
064 * @og.rev 6.2.0.0 (2015/02/27) パッケージ変更(util → model),クラス名変更(ExcelReader_XSSF → EventReader_XLSX)
065 * @og.group ファイル入力
066 *
067 * @version  6.0
068 * @author   Kazuhiko Hasegawa
069 * @since    JDK7.0,
070 */
071public final class EventReader_XLS implements EventReader {
072        /** このプログラムのVERSION文字列を設定します。   {@value} */
073        private static final String VERSION = "6.2.0.0 (2015/02/27)" ;
074
075        /**
076         * 引数ファイル(Excel)を、HSSFイベントモデルを使用してテキスト化します。
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.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更
087         *
088         * @param       file 入力ファイル
089         * @param       helper イベント処理するオブジェクト
090         */
091        @Override       // EventReader
092        public void eventReader( final File file , final TableModelHelper helper ) {
093                InputStream fin  = null;
094                InputStream din  = null;
095
096                try {
097                        // 6.2.0.0 (2015/02/27) TableModelHelper 変更に伴う修正
098                        helper.startFile( file );
099
100                        fin = new BufferedInputStream( new FileInputStream( file ) );           // 6.2.0.0 (2015/02/27)
101                        final POIFSFileSystem poifs = new POIFSFileSystem( fin );
102
103                        din = poifs.createDocumentInputStream( "Workbook" );
104
105                        final HSSFRequest req = new HSSFRequest();
106                        req.addListenerForAllRecords( new ExcelListener( helper ) );
107                        final HSSFEventFactory factory = new HSSFEventFactory();
108
109                        factory.processEvents( req, din );
110                }
111                catch( final IOException ex ) {
112                        final String errMsg = "ファイルの読取処理に失敗しました。"
113                                                                + " filename=" + file + CR
114                                                                + ex.getMessage() ;
115                        throw new OgRuntimeException( errMsg , ex );
116                }
117                finally {
118                        Closer.ioClose( din );
119                        Closer.ioClose( fin );
120                        helper.endFile( file );                                 // 6.2.0.0 (2015/02/27)
121                }
122        }
123
124        /**
125         * HSSF(.xls)処理に特化したイベント処理を行う、HSSFListener の実装内部クラス。
126         *
127         * HSSFListener のイベント処理を、TableModelHelper に変換します。
128         * これは、HSSFやXSSFの処理を、統一的なイベントモデルで扱うためです。
129         * SSモデルが良いのですが、巨大なXSSF(.xlsx)ファイルを解析すると、OutOfMemoryエラーが
130         * 発生する為、個々に処理する必要があります。
131         * あくまで、読み取り限定であれば、こちらのイベントモデルで十分です。
132         *
133         * 読み書きも含めた EXCEL処理を行うには、ExcelModel クラスが別にあります。
134         *
135         * @og.rev 6.0.3.0 (2014/11/13) 新規作成
136         */
137        private static final class ExcelListener implements HSSFListener {
138                private final TableModelHelper  helper;
139                private final ExcelStyleFormat  format;
140
141                private final List<String> shtNms = new ArrayList<>();  // シート名の一括登録
142                private SSTRecord sstrec;                               // LabelSSTRecord のインデックスに対応した文字列配列
143
144                private int             shtNo                   = -1;   // 最初に見つけたときに、++ するので初期値は -1にしておく
145                private String  shtNm                   ;               // BOFRecord でキャッシュしておきます。
146                private boolean isNextRecord    ;               // FormulaRecord で、次のレコードに値があるかどうかの判定
147                private boolean isReadSheet             = true; // シートの読み取りを行うかどうか
148
149                private int             rcdLvl  ;                               // BOFRecord で+1、EOFRecord で-1 して、シートの EOFRecord の判定に使う。
150
151                private int             rowNo   ;                               // 処理中の行番号(0~)
152                private int             colNo   ;                               // 処理中の列番号(0~)
153
154                private final boolean   useDebug        ;       // デバッグフラグ
155
156                /**
157                 * TableModelHelper を引数に取るコンストラクタ
158                 *
159                 * HSSFListener のイベント処理を、TableModelHelper に変換します。
160                 *
161                 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
162                 * @og.rev 6.2.0.0 (2015/02/27) デバッグ情報の出力するかどうか。新規追加
163                 *
164                 * @param       helper イベント処理するオブジェクト
165                 */
166                public ExcelListener( final TableModelHelper helper ) {
167                        this.helper = helper ;
168                        useDebug        = helper.isDebug();                                             // 6.2.0.0 (2015/02/27) デバッグ情報の出力
169                        format          = new ExcelStyleFormat();                               // 6.2.0.0 (2015/02/27) StylesTable 追加
170                }
171
172                /**
173                 * HSSFListener のイベントを受け取るメソッド。
174                 *
175                 * @og.rev 6.1.0.0 (2014/12/26) シートの数のイベント
176                 * @og.rev 6.3.1.0 (2015/06/28) ErrorConstants のDeprecated に伴う、FormulaError への置き換え。
177                 * @og.rev 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)。
178                 * @og.rev 6.5.0.0 (2016/09/30) poi-3.15 対応(Cell.CELL_TYPE_XXXX → CellType.XXXX)
179                 * @og.rev 8.2.1.0 (2022/07/15) poi-5.0.0 対応(CellType.forInt( FormulaRecord#getCachedResultType() ) → FormulaRecord#getCachedResultTypeEnum() )
180                 *
181                 * @param record        イベント時に設定されるレコード
182                 * @see org.apache.poi.hssf.eventusermodel.HSSFListener
183                 */
184                @Override       // HSSFListener
185//              @SuppressWarnings(value={"deprecation"})        // poi-3.15
186                public void processRecord( final Record record ) {
187                        if( record instanceof CellRecord ) {
188                                final CellRecord crec = (CellRecord)record;
189                                rowNo = crec.getRow() ;
190                                if( helper.isSkip( rowNo ) ) { return; }                // 行のスキップ判定
191                                colNo = crec.getColumn();
192                        }
193
194                        String val = null;
195                        switch( record.getSid() ) {
196                                // the BOFRecord can represent either the beginning of a sheet or the workbook
197                                case BOFRecord.sid:                             // Beginning Of File
198                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
199                                        if( record instanceof BOFRecord && ((BOFRecord)record).getType() == BOFRecord.TYPE_WORKSHEET ) {
200                                                // 6.1.0.0 (2014/12/26) シートの数のイベント
201                                                // シート一覧の読み取り後、最初のレコードの判定に、shtNo を使います。
202                                                if( shtNo < 0 ) { helper.sheetSize( shtNms.size() ); }
203
204                                                shtNo++ ;                                               // 現在のシート番号。初期値が、-1 してあるので、先に ++ する。
205                                                shtNm = shtNms.get( shtNo ) ;   // 現在のシート名。
206                                                rcdLvl = 0;                                             // シートの開始
207                                                isReadSheet = helper.startSheet( shtNm,shtNo );
208                                                if( useDebug ) { System.out.println( "① BOFRecord:" + record ); }
209                                        }
210                                        else {
211                                                rcdLvl++;                                               // シート以外の開始
212                                        }
213                                        break;
214                                case EOFRecord.sid:                             // End Of File record
215                                        if( rcdLvl == 0 ) {                                     // シートの終了
216                                                helper.endSheet( shtNo );
217                                                isReadSheet = true;
218                                                if( useDebug ) { System.out.println( "② EOFRecord" + record ); }
219                                        }
220                                        else {
221                                                rcdLvl--;                                               // シート以外の終了
222                                        }
223                                        break;
224                                case BoundSheetRecord.sid:              // シート一覧(一括で最初にイベントが発生する)
225                                        if( useDebug ) { System.out.println( "③ BoundSheetRecord" ); }
226                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
227                                        if( record instanceof BoundSheetRecord ) {
228                                                shtNms.add( ((BoundSheetRecord)record).getSheetname() );
229                                        }
230                                        break;
231                                case SSTRecord.sid:                             // Static String Table Record
232                                        if( useDebug ) { System.out.println( "④ SSTRecord" ); }
233                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
234                                        if( record instanceof SSTRecord ) {
235                                                sstrec = (SSTRecord)record;             // LabelSSTRecord のインデックスに対応した文字列配列
236                                        }
237                //                      for( int k = 0; k < sstrec.getNumUniqueStrings(); k++ ) {
238                //                              System.out.println("table[" + k + "]=" + sstrec.getString(k));
239                //                      }
240                                        break;
241                //              case RowRecord.sid:                             // stores the row information for the sheet
242                //                      if( useDebug ) { System.out.println( "⑤ RowRecord" ); }
243                //                      RowRecord rowrec = (RowRecord) record;
244                //                      System.out.println("Row=[" + rowrec.getRowNumber() + "],Col=["
245                //                                      + rowrec.getFirstCol() + "]-[" + rowrec.getLastCol() + "]" );
246                //                      break;
247
248                                // NumberRecord の XFIndex が、ExtendedFormatRecord の 番号になり、その値が、FormatIndex = FormatRecordのIndexCode
249                                case ExtendedFormatRecord.sid:
250                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
251                                        if( record instanceof ExtendedFormatRecord ) {
252                                                format.addExtFmtRec( (ExtendedFormatRecord)record );
253                                        }
254                                        break;
255
256                                // IndexCode をキーに、FormatString を取り出す。
257                                case FormatRecord.sid:
258                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
259                                        if( record instanceof FormatRecord ) {
260                                                format.addFmtRec( (FormatRecord)record );
261                                        }
262                                        break;
263
264                                case NumberRecord.sid:                  // extend CellRecord
265                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
266                                        if( isReadSheet && record instanceof NumberRecord ) {
267                                                val = format.getNumberValue( (NumberRecord)record );
268                                        }
269                                        break;
270                                // SSTRecords store a array of unique strings used in Excel.
271                                case LabelSSTRecord.sid:                // extend CellRecord
272                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
273                                        if( isReadSheet && record instanceof LabelSSTRecord ) {
274                                                final LabelSSTRecord lrec = (LabelSSTRecord)record;
275                                                val = sstrec.getString(lrec.getSSTIndex()).getString();
276                                        }
277                                        break;
278                                case BoolErrRecord.sid:                 // extend CellRecord
279                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
280                                        if( isReadSheet && record instanceof BoolErrRecord ) {
281                                                final BoolErrRecord berec = (BoolErrRecord)record;
282                                                final byte errVal = berec.getErrorValue();
283                                                val = errVal == 0 ? Boolean.toString( berec.getBooleanValue() )
284                                                                                        // 6.3.1.0 (2015/06/28)
285                                                                                  : FormulaError.forInt( errVal ).getString();
286                                        }
287                                        break;
288                                case FormulaRecord.sid:                 // extend CellRecord
289                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
290                                        if( isReadSheet && record instanceof FormulaRecord ) {
291                                                final FormulaRecord frec = (FormulaRecord)record;
292                        //                      switch (frec.getCachedResultType()) {                                                   // 6.5.0.0 (2016/09/30) poi-3.12
293//                                              switch ( CellType.forInt( frec.getCachedResultType() ) ) {              // 6.5.0.0 (2016/09/30) poi-3.15
294                                                switch ( frec.getCachedResultTypeEnum() ) {                                             // 8.2.1.0 (2022/07/15) poi-5.0.0
295                        //                              case Cell.CELL_TYPE_NUMERIC:                            // 6.5.0.0 (2016/09/30) poi-3.12
296                                                        case NUMERIC:                                                           // 6.5.0.0 (2016/09/30) poi-3.15
297                                                                final double num = frec.getValue();
298                                                                if( Double.isNaN(num) ) {
299                                                                        // Formula result is a string
300                                                                        // This is stored in the next record
301                                                                        isNextRecord = true;
302                                                                }
303                                                                else {
304                                                                        val = NumberToTextConverter.toText( num );
305                                                                }
306                                                                break;
307                        //                              case Cell.CELL_TYPE_BOOLEAN:                            // 6.5.0.0 (2016/09/30) poi-3.12
308                                                        case BOOLEAN:                                                           // 6.5.0.0 (2016/09/30) poi-3.15
309                                                                val = Boolean.toString(frec.getCachedBooleanValue());
310                                                                break;
311                        //                              case Cell.CELL_TYPE_ERROR:                                      // 6.5.0.0 (2016/09/30) poi-3.12
312                                                        case ERROR:                                                                     // 6.5.0.0 (2016/09/30) poi-3.15
313                                                                // 6.3.1.0 (2015/06/28)
314                                                                val = FormulaError.forInt( frec.getCachedErrorValue() ).getString();
315                                                                break;
316                        //                              case Cell.CELL_TYPE_STRING:                                     // 6.5.0.0 (2016/09/30) poi-3.12
317                                                        case STRING:                                                            // 6.5.0.0 (2016/09/30) poi-3.15
318                                                                isNextRecord = true;
319                                                                break;
320                                                        default : break;
321                                                }
322                                        }
323                                        break;
324                                case StringRecord.sid:                  // FormulaRecord の場合の次のレコードに値が設定されている
325                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
326                                        if( isReadSheet && isNextRecord && record instanceof StringRecord ) {
327                                                        // String for formula
328                                                        final StringRecord srec = (StringRecord)record;
329                                                        val = srec.getString();
330                                                        isNextRecord = false;
331                                        }
332                                        break;
333                //              case TextObjectRecord.sid:              // 6.2.5.0 (2015/06/05) TextBox などの、非セルテキスト
334                //                      if( isReadSheet ) {
335                //                              if( useDebug ) { System.out.println( "⑥ TextObjectRecord" ); }
336                //                              final TextObjectRecord txrec = (TextObjectRecord)record;
337                //                              val = txrec.getStr().getString();
338                //                      }
339                //                      break;
340                                default :
341                                        break ;
342                        }
343                        if( val != null ) {
344                                //           値   行(Row) 列(Col)
345                                helper.value( val, rowNo,  colNo );                     // イベント処理
346                        }
347                }
348        }
349
350        /**
351         * アプリケーションのサンプルです。
352         *
353         * 入力ファイル名 は必須で、第一引数固定です。
354         *
355         * Usage: java org.opengion.fukurou.model.EventReader_XLS 入力ファイル名
356         *
357         * @og.rev 6.0.3.0 (2014/11/13) 新規作成
358         * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更
359         *
360         * @param       args    コマンド引数配列
361         */
362        public static void main( final String[] args ) {
363                final String usageMsg = "Usage: java org.opengion.fukurou.model.EventReader_XLS 入力ファイル名" ;
364                if( args.length == 0 ) {
365                        System.err.println( usageMsg );
366                        return ;
367                }
368
369                final File file = new File( args[0] );
370                final EventReader reader = new EventReader_XLS();
371
372                reader.eventReader(                                     // 6.2.0.0 (2015/02/27)
373                        file,
374                        new TableModelHelper() {
375                                /**
376                                 * シートの読み取り開始時にイベントが発生します。
377                                 *
378                                 * @param   shtNm  シート名
379                                 * @param   shtNo  シート番号(0~)
380                                 * @return  true:シートの読み取り処理を継続します/false:このシートは読み取りません。
381                                 */
382                                public boolean startSheet( final String shtNm,final int shtNo ) {
383                                        System.out.println( "S[" + shtNo + "]=" + shtNm );
384                                        return super.startSheet( shtNm,shtNo );
385                                }
386
387                //              public void columnNames( final String[] names ) {
388                //                      System.out.println( "NM=" + java.util.Arrays.toString( names ) );
389                //              }
390
391                //              public void values( final String[] vals,final int rowNo ) {
392                //                      System.out.println( "V[" + rowNo + "]=" + java.util.Arrays.toString( vals ) );
393                //              }
394
395                //              public boolean isSkip( final int rowNo ) {
396                //                      super.isSkip( rowNo );
397                //                      return false;
398                //              }
399
400                                /**
401                                 * 読み取り状態の時に、rowNo,colNo にあるセルの値を引数にイベントが発生します。
402                                 *
403                                 * @param   val     文字列値
404                                 * @param   rowNo   行番号(0~)
405                                 * @param   colNo   列番号(0~)
406                                 * @return  読み取りするかどうか(true:読み取りする/false:読み取りしない)
407                                 */
408                                public boolean value( final String val,final int rowNo,final int colNo ) {
409                                        System.out.println( "R[" + rowNo + "],C[" + colNo + "]=" + val );
410                                        return super.value( val,rowNo,colNo );
411                                }
412                        }
413                );
414        }
415}