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 catch( final ParserConfigurationException ex ) { 325 throw new HybsSystemException( ex ); 326 } 327 catch( final SAXException ex ) { 328 final String errMsg = "ODSファイル中に含まれるcontent.xmlがXML形式ではありません。"; 329 throw new HybsSystemException( errMsg, ex ); 330 } 331 catch( final IOException ex ) { 332 throw new HybsSystemException( ex ); 333 } 334 } 335 336 /** 337 * 行オブジェクトのリストを返します。 338 * 339 * @og.rev 6.3.9.1 (2015/11/27) 修飾子を、public → なし に変更。 340 * 341 * @return 行オブジェクトのリスト 342 */ 343 /* default */ List<RowInfo> getRowInfoList() { 344 return rowInfoList; 345 } 346 347 /** 348 * ODSファイル全体のパースを行い、処理対象となるシートを検索します。 349 * 350 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート 351 * @og.rev 6.2.6.0 (2015/06/19) #csv2ArrayExt(String,int)の戻り値を、文字列配列から数字配列に変更。 352 * 353 * @param doc Documentオブジェクト 354 * @param sheetName シート名 355 * @param sheetNos シート番号 356 */ 357 private void processBook( final Document doc, final String sheetName, final String sheetNos ) { 358 // table:tableを探す 359 final NodeList nodetList = doc.getElementsByTagName( TABLE_TABLE_ELEM ); 360 final int listLen = nodetList.getLength(); 361 362 // 6.3.9.1 (2015/11/27) Found 'DD'-anomaly for variable(PMD) 363 final Element[] sheets ; // 5.5.7.2 (2012/10/09) 364 365 // 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。 sheetNos の指定が優先される。 366 if( sheetNos != null && sheetNos.length() > 0 ) { 367 final Integer[] sheetList = StringUtil.csv2ArrayExt( sheetNos , listLen-1 ); // 最大シート番号は、シート数-1 368 sheets = new Element[sheetList.length]; 369 for( int i=0; i<sheetList.length; i++ ) { 370 sheets[i] = (Element)nodetList.item( sheetList[i] ); 371 } 372 } 373 else if( sheetName != null && sheetName.length() > 0 ) { 374 Element sheet = null; 375 for( int idx=0; idx<listLen; idx++ ) { 376 final Element st = (Element)nodetList.item( idx ); 377 if( sheetName.equals( st.getAttribute( TABLE_NAME_ATTR ) ) ) { 378 sheet = st; 379 break; 380 } 381 } 382 if( sheet == null ) { 383 final String errMsg = "対応するシートが存在しません。 sheetName=[" + sheetName + "]" ; 384 throw new HybsSystemException( errMsg ); 385 } 386 sheets = new Element[] { sheet }; 387 } 388 else { 389 final Element sheet = (Element)nodetList.item(0); 390 sheets = new Element[] { sheet }; 391 } 392 393 // 指定のシートがなければ、エラー 394 // 6.0.2.5 (2014/10/31) null でないことがわかっている値の冗長な null チェックがあります。 395 // 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。 396 for( int i=0; i<sheets.length; i++ ) { 397 processSheet( sheets[i] ); 398 } 399 } 400 401 /** 402 * ODSファイルのシート単位のパースを行い、行単位のオブジェクトを生成します。 403 * 404 * @param sheet Elementオブジェクト 405 */ 406 private void processSheet( final Element sheet ) { 407 final NodeList rows = sheet.getElementsByTagName( TABLE_TABLE_ROW_ELEM ); 408 final int listLen = rows.getLength(); 409 int rowRepeat; 410 for( int idx=0; idx<listLen; idx++ ) { 411 final Element row = (Element)rows.item( idx ); 412 // 行の内容が全く同じ場合、table:number-rows-repeatedタグにより省略される。 413 final String repeatStr = row.getAttribute( TABLE_NUMBER_ROWS_REPEATED_ATTR ); 414 if( repeatStr == null || repeatStr.isEmpty() ) { // 6.1.0.0 (2014/12/26) refactoring 415 rowRepeat = 1; 416 } 417 else { 418 rowRepeat = Integer.parseInt( repeatStr, 10 ); 419 } 420 421 processRow( row, rowRepeat ); 422 } 423 } 424 425 /** 426 * ODSファイルの行単位のパースを行い、カラム単位のオブジェクトを生成します。 427 * 428 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 429 * @og.rev 5.1.8.0 (2010/07/01) セル内で書式設定されている場合に、テキストデータが取得されないバグを修正 430 * 431 * @param row Elementオブジェクト 432 * @param rowRepeat 繰り返し数 433 */ 434 private void processRow( final Element row, final int rowRepeat ) { 435 final NodeList cells = row.getElementsByTagName( TABLE_TABLE_CELL_ELEM ); 436 final int listLen = cells.getLength(); 437 int colRepeat; 438 String cellText; 439 final ArrayList<CellInfo> cellInfoList = new ArrayList<>(); 440 for( int idx=0; idx<listLen; idx++ ) { 441 final Element cell = (Element)cells.item( idx ); 442 // カラムの内容が全く同じ場合、table:number-columns-repeatedタグにより省略される。 443 final String repeatStr = cell.getAttribute( TABLE_NUMBER_COLUMNS_REPEATED_ATTR ); 444 if( repeatStr == null || repeatStr.isEmpty() ) { // 6.1.0.0 (2014/12/26) refactoring 445 colRepeat = 1; 446 } 447 else { 448 colRepeat = Integer.parseInt( repeatStr, 10 ); 449 } 450 451 // text:p 452 final NodeList texts = cell.getElementsByTagName( TEXT_P_ELEM ); 453 if( texts.getLength() == 0 ) { 454 cellText = ""; 455 } 456 else { 457 // 5.1.8.0 (2010/07/01) セル内で書式設定されている場合に、テキストデータが取得されないバグを修正 458 cellText = texts.item( 0 ).getTextContent(); 459 } 460 cellInfoList.add( new CellInfo( colRepeat, cellText ) ); 461 } 462 463 if( ! cellInfoList.isEmpty() ) { 464 // 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 465 rowInfoList.add( new RowInfo( rowRepeat, cellInfoList.toArray( new CellInfo[cellInfoList.size()] ) ) ); 466 } 467 } 468 } 469 470 /** 471 * ODSファイルの行情報を表す構造体 472 */ 473 private static final class RowInfo { 474 public final int rowRepeat; 475 public final CellInfo[] cellInfos; 476 477 /** 478 * 行の繰り返しとカラム情報の構造体配列を引数に取る、コンストラクター 479 * 480 * @param rep 行の繰り返し数 481 * @param cell カラム情報を表す構造体(CellInfoオブジェクト)の配列 482 */ 483 RowInfo( final int rep, final CellInfo[] cell ) { 484 rowRepeat = rep; 485 cellInfos = cell; 486 } 487 } 488 489 /** 490 * ODSファイルのカラム情報を表す構造体 491 */ 492 private static final class CellInfo { 493 public final int colRepeat; 494 public final String text; 495 496 /** 497 * 行の繰り返しとカラム情報を引数に取る、コンストラクター 498 * 499 * @param rep 列の繰り返し数 500 * @param tx カラム情報 501 */ 502 CellInfo( final int rep, final String tx ) { 503 colRepeat = rep; 504 text = tx; 505 } 506 } 507}