    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.hayabusa.report;
    017
    018import org.opengion.fukurou.system.OgRuntimeException ;         // 6.4.2.0 (2016/01/29)
    019import org.opengion.hayabusa.common.HybsSystemException;
    020import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
    021
    022import java.util.Map;
    023import java.util.HashMap;
    024import java.util.List;
    025import java.util.ArrayList;
    026import java.util.Iterator ;
    027import java.util.NoSuchElementException;
    028import java.util.Arrays ;
    029
    030/**
    031 * 【EXCEL取込】雛形EXCELシートの {&#064;カラム} 解析データを管理、収集する 雛形レイアウト管理クラスです。
    032 * POIのHSSFListener などで、雛形情報を収集し、HSSFSheet などで、雛形情報のアドレス(行列)から
    033 * 必要な情報を取得し、このオブジェクトに設定しておきます。
    034 * EXCELシート毎に、INSERT文と、対応する文字列配列を取り出します。
    035 *
    036 * @og.rev 3.8.0.0 (2005/06/07) 新規追加
    037 * @og.group 帳票システム
    038 *
    039 * @version  4.0
    040 * @author   Kazuhiko Hasegawa
    041 * @since    JDK5.0,
    042 */
    043public class ExcelLayout {
    044
    045        /** 6.4.3.1 (2016/02/12) キー、値の null 許可のロジックを見直すまで、ConcurrentHashMap に置き換えできません。  */
    046        private final Map<String,String> headMap  = new HashMap<>();    // シート単位のヘッダーキーを格納します。
    047        /** 6.4.3.1 (2016/02/12) キー、値の null 許可のロジックを見直すまで、ConcurrentHashMap に置き換えできません。  */
    048        private final Map<String,String> bodyMap  = new HashMap<>();    // シート単位のボディーキーを格納します。
    049        /** 6.4.3.1 (2016/02/12) キー、値の null 許可のロジックを見直すまで、ConcurrentHashMap に置き換えできません。  */
    050        private final Map<Integer,Map<String,String>> dataMap  = new HashMap<>();       // シート単位のデータを格納するMapを格納します。(キーは、GEEDNO)
    051
    052        private final List<ExcelLayoutData>[] model  ;  // シート毎にExcelLayoutDataが格納されます。
    053
    054        private String loopClm  ;                                               // 繰返必須カラム(なければnull))
    055        private ExcelLayoutDataIterator iterator        ;       // ExcelLayoutData を返す、Iterator
    056
    057        /**
    058         * コンストラクター
    059         *
    060         * 雛形の最大シート数を設定します。
    061         * ここでは、連番で管理している為、その雛形シート番号が処理対象外であっても、
    062         * 雛形EXCEL上に存在するシート数を設定する必要があります。
    063         * 具体的には、HSSFListener#processRecord( Record )で、BoundSheetRecord.sid の
    064         * イベントの数を数えて設定します。
    065         *
    066         * @param       sheetSize       最大シート数
    067         */
    068        @SuppressWarnings(value={"unchecked","rawtypes"})
    069        public ExcelLayout( final int sheetSize ) {
    070                model = new ArrayList[sheetSize];
    071                for( int i=0; i<sheetSize; i++ ) {
    072                        model[i] = new ArrayList<>();
    073                }
    074        }
    075
    076        /**
    077         * 雛形EXCELの {&#064;カラム} 解析情報を設定します。
    078         *
    079         * 雛形EXCELは、HSSFListener を使用して、イベント駆動で取得します。その場合、
    080         * {&#064;カラム}を含むセルを見つける都度、このメソッドを呼び出して、{&#064;カラム}の
    081         * 位置(行列番号)を設定します。
    082         * データEXCELからデータを読み出す場合は、ここで登録したカラムの行列より、読み込みます。
    083         * 具体的には、HSSFListener#processRecord( Record )で、SSTRecord.sid の 情報をキープしておき、
    084         * LabelSSTRecord.sid 毎に、{&#064;カラム}を含むかチェックし、含む場合に、このメソッドに
    085         * 解析情報を設定します。
    086         *
    087         * @param       sheetNo シート番号
    088         * @param       key             処理カラム
    089         * @param       rowNo   行番号
    090         * @param       colNo   列番号
    091         */
    092        public void addModel( final int sheetNo, final String key, final int rowNo, final short colNo ) {
    093                model[sheetNo].add( new ExcelLayoutData( key,rowNo,colNo ) );
    094        }
    095
    096        /**
    097         * 雛形EXCELの {&#064;カラム} 解析情報(ExcelLayoutData)を配列で取得します。
    098         *
    099         * 雛形EXCELは、イベント処理で取り込む為、すべての処理が終了してから、このメソッドで
    100         * 処理結果を取り出す必要があります。
    101         * 解析情報は、ExcelLayoutData オブジェクトにシート単位に保管されています。
    102         * この ExcelLayoutData オブジェクト ひとつに、{&#064;カラム} ひとつ、つまり、
    103         * ある特定の行列番号を持っています。
    104         * データEXCELを読取る場合、この ExcelLayoutData配列から、行列情報を取り出し、
    105         * addData メソッドで、キー情報と関連付けて登録する為に、使用します。
    106         *
    107         * @param       sheetNo シート番号
    108         * @param       loopClm 繰返必須カラム(なければ通常の１対１処理)
    109         *
    110         * @return      ExcelLayoutData配列
    111         */
    112        public Iterator<ExcelLayoutData> getLayoutDataIterator( final int sheetNo, final String loopClm ) {
    113                this.loopClm = loopClm ;
    114                final ExcelLayoutData[] datas = model[sheetNo].toArray( new ExcelLayoutData[model[sheetNo].size()] );
    115                iterator = new ExcelLayoutDataIterator( datas,loopClm );
    116                return iterator ;
    117        }
    118
    119        /**
    120         * 解析情報(clm,edbn)と関連付けて、データEXCELの値を設定します。
    121         *
    122         * データEXCELは、雛形EXCELの解析情報を元に、行列番号から設定値を取り出します。
    123         * その設定値は、取りだした ExcelLayoutData の clm,edbn と関連付けて、このメソッドで登録します。
    124         * この処理は、シート毎に、初期化して使う必要があります。
    125         * 初期化メソッドする場合は、dataClear() を呼び出してください。
    126         *
    127         * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
    128         *
    129         * @param       clm             カラム名
    130         * @param       edbn    枝番
    131         * @param       value   データ値
    132         */
    133        public void addData( final String clm, final int edbn, final String value ) {
    134                if( loopClm != null && loopClm.equals( clm ) && edbn >= 0 && ( value == null || value.isEmpty() ) ) {
    135                        // 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
    136                        if( iterator == null ) {
    137                                final String errMsg = "#getLayoutDataIterator(int,String)を先に実行しておいてください。" ;
    138                                throw new OgRuntimeException( errMsg );
    139                        }
    140
    141                        iterator.setEnd();
    142                        final Integer edbnObj = Integer.valueOf( edbn );
    143                        dataMap.remove( edbnObj );              // 枝番単位のMapを削除
    144                        return ;
    145                }
    146
    147                final Integer edbnObj = Integer.valueOf( edbn );
    148                Map<String,String> map = dataMap.get( edbnObj );                // 枝番単位のMapを取得
    149                if( map == null ) { map = new HashMap<>(); }
    150                map.put( clm,value );                           // 枝番に含まれるキーと値をセット
    151                dataMap.put( edbnObj,map );                     // そのMapを枝番に登録
    152
    153                if( edbn < 0 ) {
    154                        headMap.put( clm,null );
    155                }
    156                else {
    157                        bodyMap.put( clm,null );
    158                }
    159        }
    160
    161        /**
    162         * データEXCELの設定情報を初期化します。
    163         *
    164         * データEXCELと、雛形EXCELの解析情報を関連付ける処理は、シート毎に行う必要があります。
    165         * 処理終了時(シート切り替え時)このメソッドを呼び出して、初期化しておく必要があります
    166         *
    167         */
    168        public void dataClear() {
    169                dataMap.clear();
    170                headMap.clear();
    171                bodyMap.clear();
    172        }
    173
    174        /**
    175         * ヘッダー情報のINSERT用Query文字列を取得します。
    176         *
    177         * シート単位に、データEXCELより、INSERT用のQuery文字列を作成します。
    178         * この、Query は、シート単位に登録したキー情報の最大数(使用されているすべてのキー)を
    179         * 元に、PreparedStatement で処理できる形の INSERT文を作成します。
    180         * シート単位に呼び出す必要があります。
    181         *
    182         * @param       table   ヘッダー情報を登録するデータベース名(HEADERDBID)
    183         *
    184         * @return      ヘッダー情報のINSERT用Query文字列
    185         */
    186        public String getHeaderInsertQuery( final String table ) {
    187                // 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
    188                return table == null || table.isEmpty() || headMap.isEmpty() ? null : makeQuery( table,headMap );
    189        }
    190
    191        /**
    192         * ボディ(明細)情報のINSERT用Query文字列を取得します。
    193         *
    194         * シート単位に、データEXCELより、INSERT用のQuery文字列を作成します。
    195         * この、Query は、シート単位に登録したキー情報の最大数(使用されているすべてのキー)を
    196         * 元に、PreparedStatement で処理できる形の INSERT文を作成します。
    197         * シート単位に呼び出す必要があります。
    198         *
    199         * @param       table   ボディ(明細)情報を登録するデータベース名(BODYDBID)
    200         *
    201         * @return      ボディ(明細)情報のINSERT用Query文字列
    202         */
    203        public String getBodyInsertQuery( final String table ) {
    204                // 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
    205                return table == null || table.isEmpty() || bodyMap.isEmpty() ? null : makeQuery( table,bodyMap );
    206        }
    207
    208        /**
    209         * ヘッダー情報のINSERT用Queryに対応する、データ配列を取得します。
    210         *
    211         * getHeaderInsertQuery( String ) で取りだした PreparedStatement に設定する値配列です。
    212         * シート単位に呼び出す必要があります。
    213         *
    214         * @param       systemId        システムID(SYSTEM_ID)
    215         * @param       ykno    要求番号(YKNO)
    216         * @param       sheetNo 登録するデータEXCELのシート番号(SHEETNO)
    217         *
    218         * @return      データ配列
    219         * @og.rtnNotNull
    220         */
    221        public String[] getHeaderInsertData( final String systemId,final int ykno,final int sheetNo ) {
    222                final String[] keys = headMap.keySet().toArray( new String[headMap.size()] );
    223                if( keys == null || keys.length == 0 ) { return new String[0]; }
    224
    225                final Integer edbnObj = Integer.valueOf( -1 );          // ヘッダー
    226                final Map<String,String> map = dataMap.get( edbnObj );
    227                if( map == null ) { return new String[0]; }
    228
    229                String[] rtnData = new String[keys.length+4];
    230
    231                rtnData[0] = systemId;
    232                rtnData[1] = String.valueOf( ykno );
    233                rtnData[2] = String.valueOf( sheetNo );
    234                rtnData[3] = String.valueOf( -1 );              // 枝番
    235
    236                for( int i=0; i<keys.length; i++ ) {
    237                        rtnData[i+4] = map.get( keys[i] );
    238                }
    239
    240                return rtnData;
    241        }
    242
    243        /**
    244         * ボディ(明細)情報のINSERT用Queryに対応する、データ配列のリスト(String[] のList)を取得します。
    245         *
    246         * getHeaderInsertQuery( String ) で取りだした PreparedStatement に設定する値配列です。
    247         * シート単位に呼び出す必要があります。
    248         *
    249         * @param       systemId        システムID(SYSTEM_ID)
    250         * @param       ykno    要求番号(YKNO)
    251         * @param       sheetNo 登録するデータEXCELのシート番号(SHEETNO)
    252         *
    253         * @return      データ配列のリスト
    254         */
    255        public List<String[]> getBodyInsertData( final String systemId,final int ykno,final int sheetNo ) {
    256                final String[] keys = bodyMap.keySet().toArray( new String[bodyMap.size()] );
    257                if( keys == null || keys.length == 0 ) { return null; }
    258
    259                final List<String[]> rtnList = new ArrayList<>();
    260
    261                final Integer[] edbnObjs = dataMap.keySet().toArray( new Integer[dataMap.size()] );
    262                for( int i=0; i<edbnObjs.length; i++ ) {
    263                        final int edbn = edbnObjs[i].intValue();
    264                        if( edbn < 0 ) { continue; }            // ヘッダーの場合は、読み直し
    265
    266                        String[] rtnData = new String[keys.length+4];   // 毎回、新規に作成する。
    267                        rtnData[0] = systemId;
    268                        rtnData[1] = String.valueOf( ykno );
    269                        rtnData[2] = String.valueOf( sheetNo );
    270                        rtnData[3] = String.valueOf( edbn );    // 枝番
    271
    272                        final Map<String,String> map = dataMap.get( edbnObjs[i] );
    273                        for( int j=0; j<keys.length; j++ ) {
    274                                rtnData[j+4] = map.get( keys[j] );
    275                        }
    276                        rtnList.add( rtnData );
    277                }
    278
    279                return rtnList;
    280        }
    281
    282        /**
    283         * 内部情報Mapより、INSERT用Query文字列を取得します。
    284         *
    285         * シート単位に、データEXCELより、INSERT用のQuery文字列を作成します。
    286         * この、Query は、シート単位に登録したキー情報の最大数(使用されているすべてのキー)を
    287         * 元に、PreparedStatement で処理できる形の INSERT文を作成します。
    288         * シート単位に呼び出す必要があります。
    289         *
    290         * @param       table   テーブル名
    291         * @param       map             ボディ(明細)情報を登録する内部情報Map
    292         *
    293         * @return      INSERT用Query文字列
    294         */
    295        private String makeQuery( final String table,final Map<String,String> map ) {
    296                final String[] keys = map.keySet().toArray( new String[map.size()] );
    297
    298                if( keys == null || keys.length == 0 ) { return null; }
    299
    300                final StringBuilder buf1 = new StringBuilder( BUFFER_MIDDLE )
    301                        .append( "INSERT INTO " ).append( table )
    302                        .append( " ( GESYSTEM_ID,GEYKNO,GESHEETNO,GEEDNO" );
    303
    304                final StringBuilder buf2 = new StringBuilder( BUFFER_MIDDLE )
    305                        .append( " ) VALUES (?,?,?,?" );
    306
    307                for( int i=0; i<keys.length; i++ ) {
    308                        buf1.append( ',' ).append( keys[i] );           // 6.0.2.5 (2014/10/31) char を append する。
    309                        buf2.append( ",?" );
    310                }
    311                buf2.append( ')' );                                                             // 6.0.2.5 (2014/10/31) char を append する。
    312                buf1.append( buf2 );
    313
    314                return buf1.toString();
    315        }
    316}
    317
    318/**
    319 * ExcelLayoutData (雛形解析結果)のシート毎のIteratorを返します。
    320 * ExcelLayout では、データEXCELは、シート毎に解析します。
    321 * 通常は、雛形とデータは１対１の関係で、雛形より多いデータは、読み取りませんし、
    322 * 少ないデータは、NULL値でデータ登録します。
    323 * ここで、繰返必須カラム(LOOPCLM)を指定することで、指定のカラムが必須であることを利用して、
    324 * データが少ない場合は、そこまでで処理を中止して、データが多い場合は、仮想的にカラムが
    325 * 存在すると仮定して、雛形に存在しない箇所のデータを読み取れるように、Iterator を返します。
    326 * データがオーバーする場合は、仮想的にカラムの存在するアドレスを求める必要があるため、
    327 * 最低 カラム_0 と カラム_1 が必要です。さらに、各カラムは、行方向に並んでおり、
    328 * 列方向は、同一であるという前提で、読み取るべき行列番号を作成します。
    329 *
    330 * @og.rev 3.8.0.0 (2005/06/07) 新規追加
    331 * @og.group 帳票システム
    332 *
    333 * @version  4.0
    334 * @author   Kazuhiko Hasegawa
    335 * @since    JDK5.0,
    336 */
    337class ExcelLayoutDataIterator implements Iterator<ExcelLayoutData> {
    338        private final ExcelLayoutData[] layoutDatas ;
    339        private final String    loopClm ;
    340        // 7.2.9.4 (2020/11/20) PMD:Private field 'incSize' could be made final; it is only initialized in the declaration or constructor.
    341//      private int             incSize = 1;    // 行番号の増加数(段組などの場合は、１以上となる)
    342        private final int incSize ;             // 行番号の増加数(段組などの場合は、１以上となる)
    343        private int             count   ;               // 現在処理中の行番号
    344        private int             edbnCnt ;               // 処理中の枝番に相当するカウント値
    345        private int             stAdrs  = -1;   // 繰返し処理を行う開始アドレス
    346        private int             edAdrs  = -1;   // 繰返し処理を行う終了アドレス
    347
    348        /**
    349         * ExcelLayoutData の配列を受け取って、初期情報を設定します。
    350         *
    351         * 繰返必須カラム(LOOPCLM)がnullでない場合、枝番が０のカラムを繰り返します。
    352         * 繰り返す場合、行番号と枝番を指定して、既存のExcelLayoutDataオブジェクトを作成し、
    353         * 仮想的に繰返します。
    354         *
    355         * ※ 設定する ExcelLayoutData の配列 は、そのまま、内部配列に設定されます。(コピーされません)
    356         *    よって、外部からパラメータに指定した ExcelLayoutData の配列を変更した場合の動作は保証されません。
    357         *    また、受け取った配列は、ExcelLayoutData の自然順序(枝番順)にソートされます。
    358         *
    359         * @param       datas   ExcelLayoutDataの配列
    360         * @param       lpClm   繰返必須カラム(LOOPCLM)
    361         */
    362        public ExcelLayoutDataIterator( final ExcelLayoutData[] datas,final String lpClm ) {
    363                layoutDatas = datas;
    364                loopClm     = lpClm;
    365
    366                final int size = layoutDatas.length;            // 配列の最大値
    367
    368                int iSize = 1;                                  // 7.2.9.4 (2020/11/20) PMD:
    369                Arrays.sort( layoutDatas );             // 枝番順にソートされます。
    370                // loopClm を使う場合は、枝番 -1(ヘッダ)と、０のデータのみを使用する。枝番１は、増加数の取得のみに用いる。
    371                if( loopClm != null ) {
    372                        int zeroRow = -1;
    373                        for( int i=0; i<size; i++ ) {
    374                                // System.out.println( "count=" + i + ":" + layoutDatas[i] );
    375                                final int edbn = layoutDatas[i].getEdbn();
    376                                if( stAdrs < 0 && edbn == 0 ) { stAdrs = i; }   // 初の枝番0アドレス＝開始(含む)
    377                                if( edAdrs < 0 && edbn == 1 ) { edAdrs = i; }   // 初の枝番1アドレス＝終了(含まない)
    378                                if( loopClm.equals( layoutDatas[i].getClm() ) ) {
    379                                        if( edbn == 0 ) {
    380                                                zeroRow = layoutDatas[i].getRowNo();    // loopClm の枝番0 の行番号
    381                                        }
    382                                        else if( edbn == 1 ) {
    383//                                              incSize = layoutDatas[i].getRowNo() - zeroRow;  // 増加数=枝番1-枝番0
    384                                                iSize = layoutDatas[i].getRowNo() - zeroRow;    // 増加数=枝番1-枝番0
    385                                                break;
    386                                        }
    387                                }
    388                        }
    389                        // 繰返がある場合(枝番が0以上)でloopClmが見つからない場合はエラー
    390                        if( zeroRow < 0 && stAdrs >= 0 ) {
    391                                final String errMsg = "繰返必須カラムがシート中に存在しません。[" + loopClm + "]";
    392                                throw new HybsSystemException( errMsg );
    393                        }
    394                }
    395                if( stAdrs < 0 ) { stAdrs = 0; }        // 開始(含む)
    396                if( edAdrs < 0 ) { edAdrs = size; }     // 終了(含まない)
    397                incSize = iSize ;                                       // 7.2.9.4 (2020/11/20) PMD:
    398//              System.out.println( "stAdrs=" + stAdrs + " , edAdrs=" + edAdrs  );
    399        }
    400
    401        /**
    402         * 繰り返し処理でさらに要素がある場合に true を返します。
    403         * つまり、next が例外をスローしないで要素を返す場合に true を返します。
    404         *
    405         * @return      反復子がさらに要素を持つ場合は true
    406         */
    407        public boolean hasNext() {
    408                if( loopClm != null && count == edAdrs ) {
    409                        count = stAdrs;
    410                        edbnCnt++;
    411                }
    412        //      System.out.print( "count=[" + count + "]:" );
    413                return count < edAdrs ;
    414        }
    415
    416        /**
    417         * 繰り返し処理で次の要素を返します。
    418         *
    419         * @return 繰り返し処理で次の要素
    420         * @throws NoSuchElementException 繰り返し処理でそれ以上要素がない場合
    421         */
    422        public ExcelLayoutData next() throws NoSuchElementException {
    423                if( layoutDatas == null || layoutDatas.length == count ) {
    424                        final String errMsg = "行番号がレイアウトデータをオーバーしました。" +
    425                                                " 行番号=[" + count + "]" ;
    426                        throw new NoSuchElementException( errMsg );
    427                }
    428
    429                ExcelLayoutData data = layoutDatas[count++];
    430
    431                if( edbnCnt > 0 ) {     // 繰返必須項目機能が働いているケース
    432                        final int rowNo = data.getRowNo() + edbnCnt * incSize ;
    433                        data = data.copy( rowNo,edbnCnt );
    434        //              System.out.println( "row,edbn=[" + rowNo + "," + edbnCnt + "]:" + data );
    435                }
    436
    437                return data;
    438        }
    439
    440        /**
    441         * このメソッドは、このクラスからは使用できません。
    442         * ※ このクラスでは実装されていません。
    443         * このメソッドでは、必ず、UnsupportedOperationException が、throw されます。
    444         */
    445        public void remove() {
    446                final String errMsg = "このメソッドは、このクラスからは使用できません。";
    447                throw new UnsupportedOperationException( errMsg );
    448        }
    449
    450        /**
    451         * 繰返し処理を終了させます。
    452         *
    453         */
    454        public void setEnd() {
    455                edAdrs = -1;
    456        }
    457}