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