    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
    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                 *
    180                 * @param record        イベント時に設定されるレコード
    181                 * @see org.apache.poi.hssf.eventusermodel.HSSFListener
    182                 */
    183                @Override
    184                @SuppressWarnings(value={"deprecation"})        // poi-3.15
    185                public void processRecord( final Record record ) {
    186                        if( record instanceof CellRecord ) {
    187                                final CellRecord crec = (CellRecord)record;
    188                                rowNo = crec.getRow() ;
    189                                if( helper.isSkip( rowNo ) ) { return; }                // 行のスキップ判定
    190                                colNo = crec.getColumn();
    191                        }
    192
    193                        String val = null;
    194                        switch( record.getSid() ) {
    195                                // the BOFRecord can represent either the beginning of a sheet or the workbook
    196                                case BOFRecord.sid:                             // Beginning Of File
    197                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    198                                        if( record instanceof BOFRecord && ((BOFRecord)record).getType() == BOFRecord.TYPE_WORKSHEET ) {
    199                                                // 6.1.0.0 (2014/12/26) シートの数のイベント
    200                                                // シート一覧の読み取り後、最初のレコードの判定に、shtNo を使います。
    201                                                if( shtNo < 0 ) { helper.sheetSize( shtNms.size() ); }
    202
    203                                                shtNo++ ;                                               // 現在のシート番号。初期値が、-1 してあるので、先に ＋＋ する。
    204                                                shtNm = shtNms.get( shtNo ) ;   // 現在のシート名。
    205                                                rcdLvl = 0;                                             // シートの開始
    206                                                isReadSheet = helper.startSheet( shtNm,shtNo );
    207                                                if( useDebug ) { System.out.println( "① BOFRecord:" + record ); }
    208                                        }
    209                                        else {
    210                                                rcdLvl++;                                               // シート以外の開始
    211                                        }
    212                                        break;
    213                                case EOFRecord.sid:                             // End Of File record
    214                                        if( rcdLvl == 0 ) {                                     // シートの終了
    215                                                helper.endSheet( shtNo );
    216                                                isReadSheet = true;
    217                                                if( useDebug ) { System.out.println( "② EOFRecord" + record ); }
    218                                        }
    219                                        else {
    220                                                rcdLvl--;                                               // シート以外の終了
    221                                        }
    222                                        break;
    223                                case BoundSheetRecord.sid:              // シート一覧(一括で最初にイベントが発生する)
    224                                        if( useDebug ) { System.out.println( "③ BoundSheetRecord" ); }
    225                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    226                                        if( record instanceof BoundSheetRecord ) {
    227                                                shtNms.add( ((BoundSheetRecord)record).getSheetname() );
    228                                        }
    229                                        break;
    230                                case SSTRecord.sid:                             // Static String Table Record
    231                                        if( useDebug ) { System.out.println( "④ SSTRecord" ); }
    232                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    233                                        if( record instanceof SSTRecord ) {
    234                                                sstrec = (SSTRecord)record;             // LabelSSTRecord のインデックスに対応した文字列配列
    235                                        }
    236                //                      for( int k = 0; k < sstrec.getNumUniqueStrings(); k++ ) {
    237                //                              System.out.println("table[" + k + "]=" + sstrec.getString(k));
    238                //                      }
    239                                        break;
    240                //              case RowRecord.sid:                             // stores the row information for the sheet
    241                //                      if( useDebug ) { System.out.println( "⑤ RowRecord" ); }
    242                //                      RowRecord rowrec = (RowRecord) record;
    243                //                      System.out.println("Row=[" + rowrec.getRowNumber() + "],Col=["
    244                //                                      + rowrec.getFirstCol() + "]-[" + rowrec.getLastCol() + "]" );
    245                //                      break;
    246
    247                                // NumberRecord の XFIndex が、ExtendedFormatRecord の 番号になり、その値が、FormatIndex = FormatRecordのIndexCode
    248                                case ExtendedFormatRecord.sid:
    249                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    250                                        if( record instanceof ExtendedFormatRecord ) {
    251                                                format.addExtFmtRec( (ExtendedFormatRecord)record );
    252                                        }
    253                                        break;
    254
    255                                // IndexCode をキーに、FormatString を取り出す。
    256                                case FormatRecord.sid:
    257                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    258                                        if( record instanceof FormatRecord ) {
    259                                                format.addFmtRec( (FormatRecord)record );
    260                                        }
    261                                        break;
    262
    263                                case NumberRecord.sid:                  // extend CellRecord
    264                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    265                                        if( isReadSheet && record instanceof NumberRecord ) {
    266                                                val = format.getNumberValue( (NumberRecord)record );
    267                                        }
    268                                        break;
    269                                // SSTRecords store a array of unique strings used in Excel.
    270                                case LabelSSTRecord.sid:                // extend CellRecord
    271                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    272                                        if( isReadSheet && record instanceof LabelSSTRecord ) {
    273                                                final LabelSSTRecord lrec = (LabelSSTRecord)record;
    274                                                val = sstrec.getString(lrec.getSSTIndex()).getString();
    275                                        }
    276                                        break;
    277                                case BoolErrRecord.sid:                 // extend CellRecord
    278                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    279                                        if( isReadSheet && record instanceof BoolErrRecord ) {
    280                                                final BoolErrRecord berec = (BoolErrRecord)record;
    281                                                final byte errVal = berec.getErrorValue();
    282                                                val = errVal == 0 ? Boolean.toString( berec.getBooleanValue() )
    283                                                                                        // 6.3.1.0 (2015/06/28)
    284                                                                                  : FormulaError.forInt( errVal ).getString();
    285                                        }
    286                                        break;
    287                                case FormulaRecord.sid:                 // extend CellRecord
    288                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    289                                        if( isReadSheet && record instanceof FormulaRecord ) {
    290                                                final FormulaRecord frec = (FormulaRecord)record;
    291                        //                      switch (frec.getCachedResultType()) {                                                   // 6.5.0.0 (2016/09/30) poi-3.12
    292                                                switch ( CellType.forInt( frec.getCachedResultType() ) ) {              // 6.5.0.0 (2016/09/30) poi-3.15
    293                        //                              case Cell.CELL_TYPE_NUMERIC:                            // 6.5.0.0 (2016/09/30) poi-3.12
    294                                                        case NUMERIC:                                                           // 6.5.0.0 (2016/09/30) poi-3.15
    295                                                                final double num = frec.getValue();
    296                                                                if( Double.isNaN(num) ) {
    297                                                                        // Formula result is a string
    298                                                                        // This is stored in the next record
    299                                                                        isNextRecord = true;
    300                                                                }
    301                                                                else {
    302                                                                        val = NumberToTextConverter.toText( num );
    303                                                                }
    304                                                                break;
    305                        //                              case Cell.CELL_TYPE_BOOLEAN:                            // 6.5.0.0 (2016/09/30) poi-3.12
    306                                                        case BOOLEAN:                                                           // 6.5.0.0 (2016/09/30) poi-3.15
    307                                                                val = Boolean.toString(frec.getCachedBooleanValue());
    308                                                                break;
    309                        //                              case Cell.CELL_TYPE_ERROR:                                      // 6.5.0.0 (2016/09/30) poi-3.12
    310                                                        case ERROR:                                                                     // 6.5.0.0 (2016/09/30) poi-3.15
    311                                                                // 6.3.1.0 (2015/06/28)
    312                                                                val = FormulaError.forInt( frec.getCachedErrorValue() ).getString();
    313                                                                break;
    314                        //                              case Cell.CELL_TYPE_STRING:                                     // 6.5.0.0 (2016/09/30) poi-3.12
    315                                                        case STRING:                                                            // 6.5.0.0 (2016/09/30) poi-3.15
    316                                                                isNextRecord = true;
    317                                                                break;
    318                                                        default : break;
    319                                                }
    320                                        }
    321                                        break;
    322                                case StringRecord.sid:                  // FormulaRecord の場合の次のレコードに値が設定されている
    323                                        // 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
    324                                        if( isReadSheet && isNextRecord && record instanceof StringRecord ) {
    325                                                        // String for formula
    326                                                        final StringRecord srec = (StringRecord)record;
    327                                                        val = srec.getString();
    328                                                        isNextRecord = false;
    329                                        }
    330                                        break;
    331                //              case TextObjectRecord.sid:              // 6.2.5.0 (2015/06/05) TextBox などの、非セルテキスト
    332                //                      if( isReadSheet ) {
    333                //                              if( useDebug ) { System.out.println( "⑥ TextObjectRecord" ); }
    334                //                              final TextObjectRecord txrec = (TextObjectRecord)record;
    335                //                              val = txrec.getStr().getString();
    336                //                      }
    337                //                      break;
    338                                default :
    339                                        break ;
    340                        }
    341                        if( val != null ) {
    342                                //           値   行(Row) 列(Col)
    343                                helper.value( val, rowNo,  colNo );                     // イベント処理
    344                        }
    345                }
    346        }
    347
    348        /**
    349         * アプリケーションのサンプルです。
    350         *
    351         * 入力ファイル名 は必須で、第一引数固定です。
    352         *
    353         * Usage: java org.opengion.fukurou.model.EventReader_XLS 入力ファイル名
    354         *
    355         * @og.rev 6.0.3.0 (2014/11/13) 新規作成
    356         * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更
    357         *
    358         * @param       args    コマンド引数配列
    359         */
    360        public static void main( final String[] args ) {
    361                final String usageMsg = "Usage: java org.opengion.fukurou.model.EventReader_XLS 入力ファイル名" ;
    362                if( args.length == 0 ) {
    363                        System.err.println( usageMsg );
    364                        return ;
    365                }
    366
    367                final File file = new File( args[0] );
    368                final EventReader reader = new EventReader_XLS();
    369
    370                reader.eventReader(                                     // 6.2.0.0 (2015/02/27)
    371                        file,
    372                        new TableModelHelper() {
    373                                /**
    374                                 * シートの読み取り開始時にイベントが発生します。
    375                                 *
    376                                 * @param   shtNm  シート名
    377                                 * @param   shtNo  シート番号(0～)
    378                                 * @return  true:シートの読み取り処理を継続します/false:このシートは読み取りません。
    379                                 */
    380                                public boolean startSheet( final String shtNm,final int shtNo ) {
    381                                        System.out.println( "S[" + shtNo + "]=" + shtNm );
    382                                        return super.startSheet( shtNm,shtNo );
    383                                }
    384
    385                //              public void columnNames( final String[] names ) {
    386                //                      System.out.println( "NM=" + java.util.Arrays.toString( names ) );
    387                //              }
    388
    389                //              public void values( final String[] vals,final int rowNo ) {
    390                //                      System.out.println( "V[" + rowNo + "]=" + java.util.Arrays.toString( vals ) );
    391                //              }
    392
    393                //              public boolean isSkip( final int rowNo ) {
    394                //                      super.isSkip( rowNo );
    395                //                      return false;
    396                //              }
    397
    398                                /**
    399                                 * 読み取り状態の時に、rowNo,colNo にあるセルの値を引数にイベントが発生します。
    400                                 *
    401                                 * @param   val     文字列値
    402                                 * @param   rowNo   行番号(0～)
    403                                 * @param   colNo   列番号(0～)
    404                                 * @return  読み取りするかどうか(true:読み取りする/false:読み取りしない)
    405                                 */
    406                                public boolean value( final String val,final int rowNo,final int colNo ) {
    407                                        System.out.println( "R[" + rowNo + "],C[" + colNo + "]=" + val );
    408                                        return super.value( val,rowNo,colNo );
    409                                }
    410                        }
    411                );
    412        }
    413}