    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;   // 最初に見つけたときに、＋＋ するので初期値は －１にしておく
    145                private String  shtNm                   ;               // BOFRecord でキャッシュしておきます。
    146                private boolean isNextRecord    ;               // FormulaRecord で、次のレコードに値があるかどうかの判定
    147                private boolean isReadSheet             = true; // シートの読み取りを行うかどうか
    148
    149                private int             rcdLvl  ;                               // BOFRecord で＋１、EOFRecord で－１ して、シートの 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}