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.plugin.io; 017 018import java.io.File; // 6.2.0.0 (2015/02/27) 019import java.io.IOException; 020import java.io.InputStream; 021import java.util.ArrayList; 022import java.util.List; 023import java.util.zip.ZipEntry; 024import java.util.zip.ZipFile; 025 026import javax.xml.parsers.DocumentBuilder; 027import javax.xml.parsers.DocumentBuilderFactory; 028import javax.xml.parsers.ParserConfigurationException; 029 030import org.opengion.fukurou.util.StringUtil; 031import org.opengion.fukurou.system.Closer; // 5.5.2.6 (2012/05/25) 032import org.opengion.hayabusa.common.HybsSystemException; 033import org.opengion.hayabusa.io.AbstractTableReader; // 6.2.0.0 (2015/02/27) 034import org.w3c.dom.Document; 035import org.w3c.dom.Element; 036import org.w3c.dom.NodeList; 037import org.xml.sax.SAXException; 038 039import static org.opengion.fukurou.system.HybsConst.CR ; // 6.2.2.0 (2015/03/27) 040 041/** 042 * XMLパーサによる、OpenOffice.org Calcの表計算ドキュメントファイルを読み取る実装クラスです。 043 * 044 * ①カラム名が指定されている場合 045 * #NAMEで始まる行を検索し、その行のそれぞれの値をカラム名として処理します。 046 * #NAMEで始まる行より以前の行については、全て無視されます。 047 * また、#NAMEより前のカラム及び、#NAMEの行の値がNULL(カラム名が設定されていない)カラムも 048 * 無視します。 049 * 読み飛ばされたカラム列に入力された値は取り込まれません。 050 * また、#NAME行以降の#で始まる行は、コメント行とみなされ処理されません。 051 * 052 * ②カラム名が指定されている場合 053 * 指定されたカラム名に基づき、値を取り込みます。 054 * カラム名の順番と、シートに記述されている値の順番は一致している必要があります。 055 * 指定されたカラム数を超える列の値については全て無視されます。 056 * #で始まる行は、コメント行とみなされ処理されません。 057 * 058 * また、いずれの場合も全くデータが存在していない行は読み飛ばされます。 059 * 060 * @og.group ファイル入力 061 * 062 * @version 4.0 063 * @author Hiroki Nakamura 064 * @since JDK5.0, 065 */ 066public class TableReader_Calc extends AbstractTableReader { 067 /** このプログラムのVERSION文字列を設定します。 {@value} */ 068 private static final String VERSION = "6.4.2.0 (2016/01/29)" ; 069 070 private int firstClmIdx ; 071 private int[] valueClmIdx ; 072 073 /** 074 * デフォルトコンストラクター 075 * 076 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor. 077 */ 078 public TableReader_Calc() { super(); } // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。 079 080 /** 081 * DBTableModel から 各形式のデータを作成して,BufferedReader より読み取ります。 082 * コメント/空行を除き、最初の行は、項目名が必要です。 083 * (但し、カラム名を指定することで、項目名を省略することができます) 084 * それ以降は、コメント/空行を除き、データとして読み込んでいきます。 085 * このメソッドは、Calc 読み込み時に使用します。 086 * 087 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 088 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート 089 * @og.rev 6.2.0.0 (2015/02/27) TableReader クラスの呼び出し元メソッドの共通化(EXCEL,TEXT)。新規 090 * 091 * @param file 読み取り元ファイル名 092 * @param enc ファイルのエンコード文字列(未使用) 093 */ 094 @Override 095 public void readDBTable( final File file , final String enc ) { 096 097 ZipFile zipFile = null; 098 boolean errFlag = false; // 5.0.0.1 (2009/08/15) finally ブロックの throw を避ける。 099 try { 100 // OpenOffice.org odsファイルを開く 101 zipFile = new ZipFile( file ); 102 103 final ZipEntry entry = zipFile.getEntry( "content.xml" ); 104 if( null == entry ) { 105 final String errMsg = "ODSファイル中にファイルcontent.xmlが存在しません。"; 106 throw new HybsSystemException( errMsg ); 107 } 108 109 // content.xmlをパースし、行、列単位のオブジェクトに分解します。 110 final DomOdsParser odsParser = new DomOdsParser(); 111 odsParser.doParse( zipFile.getInputStream( entry ), sheetName , sheetNos ); // 5.5.7.2 (2012/10/09) sheetNos 対応 112 final List<RowInfo> rowInfoList = odsParser.getRowInfoList(); 113 114 // 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 115 makeDBTableModel( rowInfoList.toArray( new RowInfo[rowInfoList.size()] ) ); 116 } 117 catch( final IOException ex ) { 118 final String errMsg = "ファイル読込みエラー[" + file + "]"; 119 throw new HybsSystemException( errMsg, ex ); 120 } 121 finally { 122 // 5.5.2.6 (2012/05/25) fukurou.system.Closer#zipClose( ZipFile ) を利用するように修正。 123 errFlag = ! Closer.zipClose( zipFile ); // OK の場合、true なので、反転しておく。 124 } 125 126 if( errFlag ) { 127 final String errMsg = "ODSファイルのクローズ中にエラーが発生しました[" + file + "]"; 128 throw new HybsSystemException ( errMsg ); 129 } 130 } 131 132 /** 133 * ODSファイルをパースした結果からDBTableModelを生成します。 134 * 135 * @og.rev 5.1.6.0 (2010/05/01) skipRowCountの追加 136 * 137 * @param rowInfoList 行オブジェクトの配列(可変長引数) 138 */ 139 private void makeDBTableModel( final RowInfo... rowInfoList ) { 140 // カラム名が指定されている場合は、優先する。 141 if( columns != null && columns.length() > 0 ) { 142 makeHeaderFromClms(); 143 } 144 145 final int skip = getSkipRowCount(); // 5.1.6.0 (2010/05/01) 146 for( int row=skip; row<rowInfoList.length; row++ ) { 147 final RowInfo rowInfo = rowInfoList[row]; // 5.1.6.0 (2010/05/01) 148 if( valueClmIdx == null ) { 149 makeHeader( rowInfo ); 150 } 151 else { 152 makeBody( rowInfo ); 153 } 154 } 155 156 // 最後まで、#NAME が見つから無かった場合 157 if( valueClmIdx == null ) { 158 final String errMsg = "最後まで、#NAME が見つかりませんでした。" + CR 159 + "ファイル形式が異なるか、もしくは損傷している可能性があります。" + CR; 160 throw new HybsSystemException( errMsg ); 161 } 162 } 163 164 /** 165 * 指定されたカラム一覧からヘッダー情報を生成します。 166 * 167 * @og.rev 5.1.6.0 (2010/05/01) useNumber の追加 168 * @og.rev 6.1.0.0 (2014/12/26) omitNames 属性を追加 169 * @og.rev 6.2.1.0 (2015/03/13) TableReaderModel を外部からセットします。 170 */ 171 private void makeHeaderFromClms() { 172 final String[] names = StringUtil.csv2Array( columns ); 173 final int len = setTableDBColumn( names ) ; // 6.1.0.0 (2014/12/26) 174 valueClmIdx = new int[len]; 175 int adrs = isUseNumber() ? 1:0 ; // useNumber =true の場合は、1件目(No)は読み飛ばす。 176 for( int i=0; i<len; i++ ) { 177 valueClmIdx[i] = adrs++; 178 } 179 } 180 181 /** 182 * ヘッダー情報を読み取り、DBTableModelのオブジェクトを新規に作成します。 183 * ※ 他のTableReaderと異なり、#NAME が見つかるまで、読み飛ばす。 184 * 185 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 186 * @og.rev 6.2.1.0 (2015/03/13) TableReaderModel を外部からセットします。 187 * 188 * @param rowInfo 行オブジェクト 189 */ 190 private void makeHeader( final RowInfo rowInfo ) { 191 final CellInfo[] cellInfos = rowInfo.cellInfos; 192 193 final int cellLen = cellInfos.length; 194 int runPos = 0; 195 ArrayList<String> nameList = null; 196 ArrayList<Integer> posList = null; 197 for( int idx=0; idx<cellLen; idx++ ) { 198 // テーブルのヘッダ(#NAME)が見つかる前の行、列は全て無視される 199 final CellInfo cellInfo = cellInfos[idx]; 200 final String text = cellInfo.text.trim(); 201 202 for( int cellRep=0; cellRep<cellInfo.colRepeat; cellRep++ ) { 203 // 空白のヘッダは無視(その列にデータが入っていても読まない) 204 if( text.length() != 0 ) { 205 if( firstClmIdx == 0 && "#NAME".equalsIgnoreCase( text ) ) { 206 nameList = new ArrayList<>(); 207 posList = new ArrayList<>(); 208 firstClmIdx = idx; 209 } 210 else if( nameList != null ) { 211 nameList.add( text ); 212 posList.add( runPos ); 213 } 214 } 215 runPos++; 216 } 217 } 218 219 if( posList != null && ! posList.isEmpty() ) { 220 // 4.3.5.0 (2009/02/01) サイズの初期値指定 221 final int size = nameList.size(); 222 final String[] names = nameList.toArray( new String[size] ); 223 // table.init( size ); 224 setTableDBColumn( names ); 225 226 valueClmIdx = new int[posList.size()]; 227 for( int i=0; i<posList.size(); i++ ) { 228 valueClmIdx[i] = posList.get( i ).intValue(); 229 } 230 } 231 } 232 233 /** 234 * 行、列(セル)単位の情報を読み取り、DBTableModelに値をセットします。 235 * 236 * @og.rev 5.2.1.0 (2010/10/01) setTableColumnValues メソッドを経由して、テーブルにデータをセットする。 237 * @og.rev 6.2.1.0 (2015/03/13) setTableColumnValuesに、行番号を引数に追加 238 * @og.rev 6.2.2.0 (2015/03/27) Overflow処理(maxRowCount)は、Tag側に戻す。 239 * 240 * @param rowInfo 行オブジェクト 241 */ 242 private void makeBody( final RowInfo rowInfo ) { 243 final CellInfo[] cellInfos = rowInfo.cellInfos; 244 final int cellLen = cellInfos.length; 245 boolean isExistData = false; 246 247 final List<String> colData = new ArrayList<>(); 248 for( int cellIdx=0; cellIdx<cellLen; cellIdx++ ) { 249 final CellInfo cellInfo = cellInfos[cellIdx]; 250 for( int cellRep=0; cellRep<cellInfo.colRepeat; cellRep++ ) { 251 colData.add( cellInfo.text ); 252 if( cellInfo.text.length() > 0 ) { 253 isExistData = true; 254 } 255 } 256 } 257 258 if( isExistData ) { 259 // 初めの列(#NAMEが記述されていた列)の値が#で始まっている場合は、コメント行とみなす。 260 final String firstVal = colData.get( firstClmIdx ); 261 // 6.3.9.1 (2015/11/27) A method should have only one exit point, and that should be the last statement in the method.(PMD) 262 if( !StringUtil.startsChar( firstVal , '#' ) ) { // 6.2.0.0 (2015/02/27) 1文字 String.startsWith 263 // 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD) 264 final String[] vals = new String[valueClmIdx.length]; 265 for( int col=0; col<valueClmIdx.length; col++ ) { 266 vals[col] = colData.get( valueClmIdx[col] ); 267 } 268 269 final int rowRepeat = rowInfo.rowRepeat; // 6.3.9.1 (2015/11/27) 使う直前に移動 270 271 // 重複行の繰り返し処理 272 for( int rowIdx=0; rowIdx<rowRepeat; rowIdx++ ) { 273 // テーブルモデルにデータをセット 274 // 6.2.2.0 (2015/03/27) Overflow処理(maxRowCount)は、Tag側に戻す。 275 setTableColumnValues( vals,rowIdx ); // 6.2.1.0 (2015/03/13) 276 } 277 } 278 } 279 } 280 281 /** 282 * ODSファイルに含まれるcontent.xmlをDOMパーサーでパースし、行、列単位に 283 * オブジェクトに変換します。 284 * 285 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、private static final class に変更。 286 */ 287 private static final class DomOdsParser{ 288 289 // OpenOffice.org Calc tag Names 290 private static final String TABLE_TABLE_ELEM = "table:table"; 291 private static final String TABLE_TABLE_ROW_ELEM = "table:table-row"; 292 private static final String TABLE_TABLE_CELL_ELEM = "table:table-cell"; 293 private static final String TEXT_P_ELEM = "text:p"; 294 295 // Sheet tag attributes 296 private static final String TABLE_NAME_ATTR = "table:name"; 297 private static final String TABLE_NUMBER_ROWS_REPEATED_ATTR = "table:number-rows-repeated"; 298 private static final String TABLE_NUMBER_COLUMNS_REPEATED_ATTR = "table:number-columns-repeated"; 299 300 private final List<RowInfo> rowInfoList = new ArrayList<>(); // 6.3.9.1 (2015/11/27) 301 302 /** 303 * DomパーサでXMLをパースする。 304 * 305 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート 306 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、public → なし に変更。 307 * 308 * @param inputStream 入力ストリーム 309 * @param sheetName シート名 310 * @param sheetNos シート番号 311 */ 312 /* default */ void doParse( final InputStream inputStream, final String sheetName, final String sheetNos ) { 313 try { 314 // ドキュメントビルダーファクトリを生成 315 final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); 316 dbFactory.setNamespaceAware( true ); 317 318 // ドキュメントビルダーを生成 319 final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); 320 // パースを実行してDocumentオブジェクトを取得 321 final Document doc = dBuilder.parse( inputStream ); 322 processBook( doc, sheetName, sheetNos ); // 5.5.7.2 (2012/10/09) sheetNos 追加 323 } 324 // 7.2.9.5 (2020/11/28) PMD:'catch' branch identical to 'ParserConfigurationException' branch 325 catch( final ParserConfigurationException | IOException ex ) { 326 throw new HybsSystemException( ex ); 327 } 328// catch( final ParserConfigurationException ex ) { 329// throw new HybsSystemException( ex ); 330// } 331 catch( final SAXException ex ) { 332 final String errMsg = "ODSファイル中に含まれるcontent.xmlがXML形式ではありません。"; 333 throw new HybsSystemException( errMsg, ex ); 334 } 335// catch( final IOException ex ) { 336// throw new HybsSystemException( ex ); 337// } 338 } 339 340 /** 341 * 行オブジェクトのリストを返します。 342 * 343 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、public → なし に変更。 344 * 345 * @return 行オブジェクトのリスト 346 */ 347 /* default */ List<RowInfo> getRowInfoList() { 348 return rowInfoList; 349 } 350 351 /** 352 * ODSファイル全体のパースを行い、処理対象となるシートを検索します。 353 * 354 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート 355 * @og.rev 6.2.6.0 (2015/06/19) #csv2ArrayExt(String,int)の戻り値を、文字列配列から数字配列に変更。 356 * 357 * @param doc Documentオブジェクト 358 * @param sheetName シート名 359 * @param sheetNos シート番号 360 */ 361 private void processBook( final Document doc, final String sheetName, final String sheetNos ) { 362 // table:tableを探す 363 final NodeList nodetList = doc.getElementsByTagName( TABLE_TABLE_ELEM ); 364 final int listLen = nodetList.getLength(); 365 366 // 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD) 367 final Element[] sheets ; // 5.5.7.2 (2012/10/09) 368 369 // 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。 sheetNos の指定が優先される。 370 if( sheetNos != null && sheetNos.length() > 0 ) { 371 final Integer[] sheetList = StringUtil.csv2ArrayExt( sheetNos , listLen-1 ); // 最大シート番号は、シート数-1 372 sheets = new Element[sheetList.length]; 373 for( int i=0; i<sheetList.length; i++ ) { 374 sheets[i] = (Element)nodetList.item( sheetList[i] ); 375 } 376 } 377 else if( sheetName != null && sheetName.length() > 0 ) { 378 Element sheet = null; 379 for( int idx=0; idx<listLen; idx++ ) { 380 final Element st = (Element)nodetList.item( idx ); 381 if( sheetName.equals( st.getAttribute( TABLE_NAME_ATTR ) ) ) { 382 sheet = st; 383 break; 384 } 385 } 386 if( sheet == null ) { 387 final String errMsg = "対応するシートが存在しません。 sheetName=[" + sheetName + "]" ; 388 throw new HybsSystemException( errMsg ); 389 } 390 sheets = new Element[] { sheet }; 391 } 392 else { 393 final Element sheet = (Element)nodetList.item(0); 394 sheets = new Element[] { sheet }; 395 } 396 397 // 指定のシートがなければ、エラー 398 // 6.0.2.5 (2014/10/31) null でないことがわかっている値の冗長な null チェックがあります。 399 // 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。 400 // 7.2.9.4 (2020/11/20) PMD:This for loop can be replaced by a foreach loop 401 for( final Element sheet : sheets ) { 402 processSheet( sheet ); 403 } 404// for( int i=0; i<sheets.length; i++ ) { 405// processSheet( sheets[i] ); 406// } 407 } 408 409 /** 410 * ODSファイルのシート単位のパースを行い、行単位のオブジェクトを生成します。 411 * 412 * @param sheet Elementオブジェクト 413 */ 414 private void processSheet( final Element sheet ) { 415 final NodeList rows = sheet.getElementsByTagName( TABLE_TABLE_ROW_ELEM ); 416 final int listLen = rows.getLength(); 417 int rowRepeat; 418 for( int idx=0; idx<listLen; idx++ ) { 419 final Element row = (Element)rows.item( idx ); 420 // 行の内容が全く同じ場合、table:number-rows-repeatedタグにより省略される。 421 final String repeatStr = row.getAttribute( TABLE_NUMBER_ROWS_REPEATED_ATTR ); 422 if( repeatStr == null || repeatStr.isEmpty() ) { // 6.1.0.0 (2014/12/26) refactoring 423 rowRepeat = 1; 424 } 425 else { 426 rowRepeat = Integer.parseInt( repeatStr, 10 ); 427 } 428 429 processRow( row, rowRepeat ); 430 } 431 } 432 433 /** 434 * ODSファイルの行単位のパースを行い、カラム単位のオブジェクトを生成します。 435 * 436 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 437 * @og.rev 5.1.8.0 (2010/07/01) セル内で書式設定されている場合に、テキストデータが取得されないバグを修正 438 * 439 * @param row Elementオブジェクト 440 * @param rowRepeat 繰り返し数 441 */ 442 private void processRow( final Element row, final int rowRepeat ) { 443 final NodeList cells = row.getElementsByTagName( TABLE_TABLE_CELL_ELEM ); 444 final int listLen = cells.getLength(); 445 int colRepeat; 446 String cellText; 447 final ArrayList<CellInfo> cellInfoList = new ArrayList<>(); 448 for( int idx=0; idx<listLen; idx++ ) { 449 final Element cell = (Element)cells.item( idx ); 450 // カラムの内容が全く同じ場合、table:number-columns-repeatedタグにより省略される。 451 final String repeatStr = cell.getAttribute( TABLE_NUMBER_COLUMNS_REPEATED_ATTR ); 452 if( repeatStr == null || repeatStr.isEmpty() ) { // 6.1.0.0 (2014/12/26) refactoring 453 colRepeat = 1; 454 } 455 else { 456 colRepeat = Integer.parseInt( repeatStr, 10 ); 457 } 458 459 // text:p 460 final NodeList texts = cell.getElementsByTagName( TEXT_P_ELEM ); 461 if( texts.getLength() == 0 ) { 462 cellText = ""; 463 } 464 else { 465 // 5.1.8.0 (2010/07/01) セル内で書式設定されている場合に、テキストデータが取得されないバグを修正 466 cellText = texts.item( 0 ).getTextContent(); 467 } 468 cellInfoList.add( new CellInfo( colRepeat, cellText ) ); 469 } 470 471 if( ! cellInfoList.isEmpty() ) { 472 // 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 473 rowInfoList.add( new RowInfo( rowRepeat, cellInfoList.toArray( new CellInfo[cellInfoList.size()] ) ) ); 474 } 475 } 476 } 477 478 /** 479 * ODSファイルの行情報を表す構造体 480 */ 481 private static final class RowInfo { 482 public final int rowRepeat; 483 public final CellInfo[] cellInfos; 484 485 /** 486 * 行の繰り返しとカラム情報の構造体配列を引数に取る、コンストラクター 487 * 488 * @param rep 行の繰り返し数 489 * @param cell カラム情報を表す構造体(CellInfoオブジェクト)の配列 490 */ 491 RowInfo( final int rep, final CellInfo[] cell ) { 492 rowRepeat = rep; 493 cellInfos = cell; 494 } 495 } 496 497 /** 498 * ODSファイルのカラム情報を表す構造体 499 */ 500 private static final class CellInfo { 501 public final int colRepeat; 502 public final String text; 503 504 /** 505 * 行の繰り返しとカラム情報を引数に取る、コンストラクター 506 * 507 * @param rep 列の繰り返し数 508 * @param tx カラム情報 509 */ 510 CellInfo( final int rep, final String tx ) { 511 colRepeat = rep; 512 text = tx; 513 } 514 } 515}