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シートの {@カラム} 解析データを管理、収集する 雛形レイアウト管理クラスです。
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 繰返必須カラム(なければ通常の1対1処理)
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 * 通常は、雛形とデータは1対1の関係で、雛形より多いデータは、読み取りませんし、
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;    // 行番号の増加数(段組などの場合は、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でない場合、枝番が0のカラムを繰り返します。
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(ヘッダ)と、0のデータのみを使用する。枝番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}