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シートの {@カラム} 解析データを管理、収集する 雛形レイアウト管理クラスです。 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の {@カラム} 解析情報を設定します。 078 * 079 * 雛形EXCELは、HSSFListener を使用して、イベント駆動で取得します。その場合、 080 * {@カラム}を含むセルを見つける都度、このメソッドを呼び出して、{@カラム}の 081 * 位置(行列番号)を設定します。 082 * データEXCELからデータを読み出す場合は、ここで登録したカラムの行列より、読み込みます。 083 * 具体的には、HSSFListener#processRecord( Record )で、SSTRecord.sid の 情報をキープしておき、 084 * LabelSSTRecord.sid 毎に、{@カラム}を含むかチェックし、含む場合に、このメソッドに 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の {@カラム} 解析情報(ExcelLayoutData)を配列で取得します。 098 * 099 * 雛形EXCELは、イベント処理で取り込む為、すべての処理が終了してから、このメソッドで 100 * 処理結果を取り出す必要があります。 101 * 解析情報は、ExcelLayoutData オブジェクトにシート単位に保管されています。 102 * この ExcelLayoutData オブジェクト ひとつに、{@カラム} ひとつ、つまり、 103 * ある特定の行列番号を持っています。 104 * データEXCELを読取る場合、この ExcelLayoutData配列から、行列情報を取り出し、 105 * addData メソッドで、キー情報と関連付けて登録する為に、使用します。 106 * 107 * @param sheetNo シート番号 108 * @param loopClm 繰返必須カラム(なければ通常の1対1処理) 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 * 通常は、雛形とデータは1対1の関係で、雛形より多いデータは、読み取りませんし、 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; // 行番号の増加数(段組などの場合は、1以上となる) 342 private final int incSize ; // 行番号の増加数(段組などの場合は、1以上となる) 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でない場合、枝番が0のカラムを繰り返します。 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(ヘッダ)と、0のデータのみを使用する。枝番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}