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.BufferedReader; 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.util.Closer; // 5.5.2.6 (2012/05/25) 032import org.opengion.hayabusa.common.HybsSystem; 033import org.opengion.hayabusa.common.HybsSystemException; 034import org.opengion.hayabusa.db.DBTableModelUtil; 035import org.w3c.dom.Document; 036import org.w3c.dom.Element; 037import org.w3c.dom.NodeList; 038import org.xml.sax.SAXException; 039 040/** 041 * XMLパーサによる、OpenOffice.org Calcの表計算ドキュメントファイルを読み取る実装クラスです。 042 * 043 * ①カラム名が指定されている場合 044 * #NAMEで始まる行を検索し、その行のそれぞれの値をカラム名として処理します。 045 * #NAMEで始まる行より以前の行については、全て無視されます。 046 * また、#NAMEより前のカラム及び、#NAMEの行の値がNULL(カラム名が設定されていない)カラムも 047 * 無視します。 048 * 読み飛ばされたカラム列に入力された値は取り込まれません。 049 * また、#NAME行以降の#で始まる行は、コメント行とみなされ処理されません。 050 * 051 * ②カラム名が指定されている場合 052 * 指定されたカラム名に基づき、値を取り込みます。 053 * カラム名の順番と、シートに記述されている値の順番は一致している必要があります。 054 * 指定されたカラム数を超える列の値については全て無視されます。 055 * #で始まる行は、コメント行とみなされ処理されません。 056 * 057 * また、いずれの場合も全くデータが存在していない行は読み飛ばされます。 058 * 059 * @og.group ファイル入力 060 * 061 * @version 4.0 062 * @author Hiroki Nakamura 063 * @since JDK5.0, 064 */ 065public class TableReader_Calc extends TableReader_Default { 066 // * このプログラムのVERSION文字列を設定します。 {@value} */ 067 private static final String VERSION = "5.5.7.2 (2012/10/09)"; 068 069 private String sheetName = null; 070 private String sheetNos = null; // 5.5.7.2 (2012/10/09) 071 private String filename = null; 072 private int numberOfRows = 0; 073 private int firstClmIdx = 0; 074 private int[] valueClmIdx = null; 075 076 /** 077 * DBTableModel から 各形式のデータを作成して,BufferedReader より読み取ります。 078 * コメント/空行を除き、最初の行は、項目名が必要です。 079 * (但し、カラム名を指定することで、項目名を省略することができます) 080 * それ以降は、コメント/空行を除き、データとして読み込んでいきます。 081 * このメソッドは、Calc 読み込み時に使用します。 082 * 083 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 084 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート 085 * 086 * @see #isExcel() 087 */ 088 @Override 089 public void readDBTable() { 090 091 ZipFile zipFile = null; 092 boolean errFlag = false; // 5.0.0.1 (2009/08/15) finally ブロックの throw を避ける。 093 try { 094 // OpenOffice.org odsファイルを開く 095 zipFile = new ZipFile( filename ); 096 097 ZipEntry entry = zipFile.getEntry( "content.xml" ); 098 if ( null == entry ) { 099 String errMsg = "ODSファイル中にファイルcontent.xmlが存在しません。"; 100 throw new HybsSystemException( errMsg ); 101 } 102 103 // content.xmlをパースし、行、列単位のオブジェクトに分解します。 104 DomOdsParser odsParser = new DomOdsParser(); 105 odsParser.doParse( zipFile.getInputStream( entry ), sheetName , sheetNos ); // 5.5.7.2 (2012/10/09) sheetNos 対応 106 List<RowInfo> rowInfoList = odsParser.getRowInfoList(); 107 108 // 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 109 makeDBTableModel( rowInfoList.toArray( new RowInfo[rowInfoList.size()] ) ); 110 } 111 catch ( IOException ex ) { 112 String errMsg = "ファイル読込みエラー[" + filename + "]"; 113 throw new HybsSystemException( errMsg, ex ); 114 } 115 finally { 116 // 5.5.2.6 (2012/05/25) fukurou.util.Closer#zipClose( ZipFile ) を利用するように修正。 117 errFlag = ! Closer.zipClose( zipFile ); // OK の場合、true なので、反転しておく。 118 } 119 120 if( errFlag ) { 121 String errMsg = "ODSファイルのクローズ中にエラーが発生しました[" + filename + "]"; 122 throw new HybsSystemException ( errMsg ); 123 } 124 } 125 126 /** 127 * DBTableModel から 各形式のデータを作成して,BufferedReader より読み取ります。 128 * このメソッドは、この実装クラスでは使用できません。 129 * 130 * @param reader 各形式のデータ(使用していません) 131 */ 132 @Override 133 public void readDBTable( final BufferedReader reader ) { 134 String errMsg = "このクラスでは実装されていません。"; 135 throw new UnsupportedOperationException( errMsg ); 136 } 137 138 /** 139 * DBTableModelのデータとしてCalcファイルを読み込むときのシート名を設定します。 140 * これにより、複数の形式の異なるデータを順次読み込むことや、シートを指定して 141 * 読み取ることが可能になります。 142 * sheetNos と sheetName が同時に指定された場合は、sheetNos が優先されます。エラーにはならないのでご注意ください。 143 * のでご注意ください。 144 * 145 * @param sheetName シート名 146 */ 147 @Override 148 public void setSheetName( final String sheetName ) { 149 this.sheetName = sheetName; 150 } 151 152 /** 153 * Calcファイルを読み込むときのシート番号を指定します(初期値:0)。 154 * 155 * Calc読み込み時に複数シートをマージして取り込みます。 156 * シート番号は、0 から始まる数字で表します。 157 * ヘッダーは、最初のシートのカラム位置に合わせます。(ヘッダータイトルの自動認識はありません。) 158 * よって、指定するシートは、すべて同一レイアウトでないと取り込み時にカラムのずれが発生します。 159 * 160 * シート番号の指定は、カンマ区切りで、複数指定できます。また、N-M の様にハイフンで繋げることで、 161 * N 番から、M 番のシート範囲を一括指定可能です。また、"*" による、全シート指定が可能です。 162 * これらの組み合わせも可能です。( 0,1,3,5-8,10-* ) 163 * ただし、"*" に関しては例外的に、一文字だけで、すべてのシートを表すか、N-* を最後に指定するかの 164 * どちらかです。途中には、"*" は、現れません。 165 * シート番号は、重複(1,1,2,2)、逆転(3,2,1) での指定が可能です。これは、その指定順で、読み込まれます。 166 * sheetNos と sheetName が同時に指定された場合は、sheetNos が優先されます。エラーにはならないのでご注意ください。 167 * このメソッドは、isExcel() == true の場合のみ利用されます。 168 * 169 * 初期値は、0(第一シート) です。 170 * 171 * ※ このクラスでは実装されていません。 172 * 173 * @og.rev 5.5.7.2 (2012/10/09) 新規追加 174 * 175 * @param sheetNos Calcファイルのシート番号(0から始まる) 176 * @see #setSheetName( String ) 177 */ 178 @Override 179 public void setSheetNos( final String sheetNos ) { 180 this.sheetNos = sheetNos; 181 } 182 183 /** 184 * このクラスが、EXCEL対応機能を持っているかどうかを返します。 185 * 186 * EXCEL対応機能とは、シート名のセット、読み込み元ファイルの Fileオブジェクト取得などの、特殊機能です。 187 * 本来は、インターフェースを分けるべきと考えますが、taglib クラス等の 関係があり、問い合わせによる条件分岐で対応します。 188 * 189 * @return EXCEL対応機能を持っているかどうか(常にtrue) 190 */ 191 @Override 192 public boolean isExcel() { 193 return true; 194 } 195 196 /** 197 * 読み取り元ファイル名をセットします。(DIR + Filename) これは、OpenOffice.org 198 * Calc追加機能として実装されています。 199 * 200 * @param filename 読み取り元ファイル名 201 */ 202 @Override 203 public void setFilename( final String filename ) { 204 this.filename = filename; 205 if ( filename == null ) { 206 String errMsg = "ファイル名が指定されていません。"; 207 throw new HybsSystemException( errMsg ); 208 } 209 } 210 211 /** 212 * ODSファイルをパースした結果からDBTableModelを生成します。 213 * 214 * @og.rev 5.1.6.0 (2010/05/01) skipRowCountの追加 215 * 216 * @param rowInfoList 行オブジェクトの配列 217 */ 218 private void makeDBTableModel( final RowInfo[] rowInfoList ) { 219 // カラム名が指定されている場合は、優先する。 220 if( columns != null && columns.length() > 0 ) { 221 makeHeaderFromClms(); 222 } 223 224 int skip = getSkipRowCount(); // 5.1.6.0 (2010/05/01) 225 for( int row=skip; row<rowInfoList.length; row++ ) { 226 RowInfo rowInfo = rowInfoList[row]; // 5.1.6.0 (2010/05/01) 227 if( valueClmIdx == null ) { 228 makeHeader( rowInfo ); 229 } 230 else { 231 makeBody( rowInfo ); 232 } 233 } 234 235 // 最後まで、#NAME が見つから無かった場合 236 if ( valueClmIdx == null ) { 237 String errMsg = "最後まで、#NAME が見つかりませんでした。" + HybsSystem.CR + "ファイルが空か、もしくは損傷している可能性があります。" + HybsSystem.CR; 238 throw new HybsSystemException( errMsg ); 239 } 240 } 241 242 /** 243 * 指定されたカラム一覧からヘッダー情報を生成します。 244 * 245 * @og.rev 5.1.6.0 (2010/05/01) useNumber の追加 246 */ 247 private void makeHeaderFromClms() { 248 table = DBTableModelUtil.newDBTable(); 249 String[] names = StringUtil.csv2Array( columns ); 250 table.init( names.length ); 251 setTableDBColumn( names ) ; 252 valueClmIdx = new int[names.length]; 253 int adrs = isUseNumber() ? 1:0 ; // useNumber =true の場合は、1件目(No)は読み飛ばす。 254 for( int i=0; i<names.length; i++ ) { 255 valueClmIdx[i] = adrs++; 256 } 257 } 258 259 /** 260 * ヘッダー情報を読み取り、DBTableModelのオブジェクトを新規に作成します。 261 * ※ 他のTableReaderと異なり、#NAME が見つかるまで、読み飛ばす。 262 * 263 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 264 * 265 * @param rowInfo 行オブジェクト 266 */ 267 private void makeHeader( final RowInfo rowInfo ) { 268 CellInfo[] cellInfos = rowInfo.cellInfos; 269 270 int cellLen = cellInfos.length; 271 int runPos = 0; 272 ArrayList<String> nameList = null; 273 ArrayList<Integer> posList = null; 274 for ( int idx = 0; idx < cellLen; idx++ ) { 275 // テーブルのヘッダ(#NAME)が見つかる前の行、列は全て無視される 276 CellInfo cellInfo = cellInfos[idx]; 277 String text = cellInfo.text.trim(); 278 279 for ( int cellRep = 0; cellRep < cellInfo.colRepeat; cellRep++ ) { 280 // 空白のヘッダは無視(その列にデータが入っていても読まない) 281 if ( text.length() != 0 ) { 282 if ( firstClmIdx == 0 && "#NAME".equalsIgnoreCase( text ) ) { 283 nameList = new ArrayList<String>(); 284 posList = new ArrayList<Integer>(); 285 table = DBTableModelUtil.newDBTable(); 286 firstClmIdx = idx; 287 } 288 else if ( nameList != null ) { 289 nameList.add( text ); 290 posList.add( runPos ); 291 } 292 } 293 runPos++; 294 } 295 } 296 297 if ( posList != null && ! posList.isEmpty() ) { 298 table = DBTableModelUtil.newDBTable(); 299 // 4.3.5.0 (2009/02/01) サイズの初期値指定 300 int size = nameList.size(); 301 String[] names = nameList.toArray( new String[size] ); 302 table.init( size ); 303 setTableDBColumn( names ); 304 305 valueClmIdx = new int[posList.size()]; 306 for( int i = 0; i<posList.size(); i++ ) { 307 valueClmIdx[i] = posList.get( i ).intValue(); 308 } 309 } 310 } 311 312 /** 313 * 行、列(セル)単位の情報を読み取り、DBTableModelに値をセットします 314 * 315 * @og.rev 5.2.1.0 (2010/10/01) setTableColumnValues メソッドを経由して、テーブルにデータをセットする。 316 * 317 * @param rowInfo 行オブジェクト 318 */ 319 private void makeBody( final RowInfo rowInfo ) { 320 int rowRepeat = rowInfo.rowRepeat; 321 CellInfo[] cellInfos = rowInfo.cellInfos; 322 int cellLen = cellInfos.length; 323 boolean isExistData = false; 324 325 List<String> colData = new ArrayList<String>(); 326 for ( int cellIdx = 0; cellIdx < cellLen; cellIdx++ ) { 327 CellInfo cellInfo = cellInfos[cellIdx]; 328 for ( int cellRep = 0; cellRep < cellInfo.colRepeat; cellRep++ ) { 329 colData.add( cellInfo.text ); 330 if( cellInfo.text.length() > 0 ) { 331 isExistData = true; 332 } 333 } 334 } 335 336 if( isExistData ) { 337 // 初めの列(#NAMEが記述されていた列)の値が#で始まっている場合は、コメント行とみなす。 338 String firstVal = colData.get( firstClmIdx ); 339 if( firstVal.length() > 0 && firstVal.startsWith( "#" ) ) { 340 return; 341 } 342 else { 343 String[] vals = new String[valueClmIdx.length]; 344 for( int col = 0; col < valueClmIdx.length; col++ ) { 345 vals[col] = colData.get( valueClmIdx[col] ); 346 } 347 348 // 重複行の繰り返し処理 349 for ( int rowIdx = 0; rowIdx < rowRepeat; rowIdx++ ) { 350 // テーブルモデルにデータをセット 351 if ( numberOfRows < getMaxRowCount() ) { 352 setTableColumnValues( vals ); // 5.2.1.0 (2010/10/01) 353 numberOfRows++; 354 } 355 else { 356 table.setOverflow( true ); 357 } 358 } 359 } 360 } 361 // 全くデータが存在しない行は読み飛ばし 362 else { 363 return; 364 } 365 } 366 367 /** 368 * ODSファイルに含まれるcontent.xmlをDOMパーサーでパースし、行、列単位に 369 * オブジェクトに変換します。 370 * 371 */ 372 private static class DomOdsParser{ 373 374 // OpenOffice.org Calc tag Names 375 private static final String TABLE_TABLE_ELEM = "table:table"; 376 private static final String TABLE_TABLE_ROW_ELEM = "table:table-row"; 377 private static final String TABLE_TABLE_CELL_ELEM = "table:table-cell"; 378 private static final String TEXT_P_ELEM = "text:p"; 379 380 // Sheet tag attributes 381 private static final String TABLE_NAME_ATTR = "table:name"; 382 private static final String TABLE_NUMBER_ROWS_REPEATED_ATTR = "table:number-rows-repeated"; 383 private static final String TABLE_NUMBER_COLUMNS_REPEATED_ATTR = "table:number-columns-repeated"; 384 385 List<RowInfo> rowInfoList = new ArrayList<RowInfo>(); 386 /** 387 * DomパーサでXMLをパースする 388 * 389 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート 390 * 391 * @param inputStream InputStream 392 * @param sheetName String 393 * @param sheetNos String 394 */ 395 public void doParse( final InputStream inputStream, final String sheetName, final String sheetNos ) { 396 try { 397 // ドキュメントビルダーファクトリを生成 398 DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); 399 dbFactory.setNamespaceAware( true ); 400 401 // ドキュメントビルダーを生成 402 DocumentBuilder dBuilder = dbFactory.newDocumentBuilder(); 403 // パースを実行してDocumentオブジェクトを取得 404 Document doc = dBuilder.parse( inputStream ); 405 processBook( doc, sheetName, sheetNos ); // 5.5.7.2 (2012/10/09) sheetNos 追加 406 } 407 catch ( ParserConfigurationException ex ) { 408 throw new HybsSystemException( ex ); 409 } 410 catch ( SAXException ex ) { 411 String errMsg = "ODSファイル中に含まれるcontent.xmlがXML形式ではありません。"; 412 throw new HybsSystemException( errMsg, ex ); 413 } 414 catch ( IOException ex ) { 415 throw new HybsSystemException( ex ); 416 } 417 } 418 419 /** 420 * 行オブジェクトのリストを返します。 421 * 422 * @return List<RowInfo> 423 */ 424 public List<RowInfo> getRowInfoList() { 425 return rowInfoList; 426 } 427 428 /** 429 * ODSファイル全体のパースを行い、処理対象となるシートを検索します。 430 * 431 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート 432 * 433 * @param doc Document 434 * @param sheetName String 435 * @param sheetNos String 436 */ 437 private void processBook( final Document doc, final String sheetName, final String sheetNos ) { 438 // table:tableを探す 439 NodeList nodetList = doc.getElementsByTagName( TABLE_TABLE_ELEM ); 440 int listLen = nodetList.getLength(); 441 442 Element[] sheets = null ; // 5.5.7.2 (2012/10/09) 443 444 // 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。 sheetNos の指定が優先される。 445 if( sheetNos != null && sheetNos.length() > 0 ) { 446 String[] sheetList = StringUtil.csv2ArrayExt( sheetNos , listLen-1 ); // 最大シート番号は、シート数-1 447 sheets = new Element[sheetList.length]; 448 for( int i=0; i<sheetList.length; i++ ) { 449 sheets[i] = (Element)nodetList.item( Integer.parseInt( sheetList[i] ) ); 450 } 451 } 452 else if( sheetName != null && sheetName.length() > 0 ) { 453 Element sheet = null; 454 for ( int idx = 0; idx < listLen; idx++ ) { 455 Element st = (Element)nodetList.item( idx ); 456 if ( sheetName.equals( st.getAttribute( TABLE_NAME_ATTR ) ) ) { 457 sheet = st; 458 break; 459 } 460 } 461 if( sheet == null ) { 462 String errMsg = "対応するシートが存在しません。 sheetName=[" + sheetName + "]" ; 463 throw new HybsSystemException( errMsg ); 464 } 465 sheets = new Element[] { sheet }; 466 } 467 else { 468 Element sheet = (Element)nodetList.item(0); 469 sheets = new Element[] { sheet }; 470 } 471 472 // 指定のシートがなければ、エラー 473 if ( sheets == null ) { 474 String errMsg = "対応するシートが存在しません。 sheetNos=[" + sheetNos + "] or sheetName=[" + sheetName + "]"; 475 throw new HybsSystemException( errMsg ); 476 } 477 else { 478 // 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。 479 for( int i=0; i<sheets.length; i++ ) { 480 processSheet( sheets[i] ); 481 } 482 } 483 } 484 485 /** 486 * ODSファイルのシート単位のパースを行い、行単位のオブジェクトを生成します。 487 * 488 * @param sheet Element 489 */ 490 private void processSheet( final Element sheet ) { 491 NodeList rows = sheet.getElementsByTagName( TABLE_TABLE_ROW_ELEM ); 492 int listLen = rows.getLength(); 493 int rowRepeat; 494 for ( int idx = 0; idx < listLen; idx++ ) { 495 Element row = (Element)rows.item( idx ); 496 // 行の内容が全く同じ場合、table:number-rows-repeatedタグにより省略される。 497 String repeatStr = row.getAttribute( TABLE_NUMBER_ROWS_REPEATED_ATTR ); 498 if ( repeatStr == null || repeatStr.length() == 0 ) { 499 rowRepeat = 1; 500 } 501 else { 502 rowRepeat = Integer.parseInt( repeatStr, 10 ); 503 } 504 505 processRow( row, rowRepeat ); 506 } 507 } 508 509 /** 510 * ODSファイルの行単位のパースを行い、カラム単位のオブジェクトを生成します。 511 * 512 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 513 * @og.rev 5.1.8.0 (2010/07/01) セル内で書式設定されている場合に、テキストデータが取得されないバグを修正 514 * 515 * @param row Element 516 * @param rowRepeat int 517 */ 518 private void processRow( final Element row, final int rowRepeat ) { 519 NodeList cells = row.getElementsByTagName( TABLE_TABLE_CELL_ELEM ); 520 int listLen = cells.getLength(); 521 int colRepeat; 522 String cellText; 523 ArrayList<CellInfo> cellInfoList = new ArrayList<CellInfo>(); 524 for ( int idx = 0; idx < listLen; idx++ ) { 525 Element cell = (Element)cells.item( idx ); 526 // カラムの内容が全く同じ場合、table:number-columns-repeatedタグにより省略される。 527 String repeatStr = cell.getAttribute( TABLE_NUMBER_COLUMNS_REPEATED_ATTR ); 528 if ( repeatStr == null || repeatStr.length() == 0 ) { 529 colRepeat = 1; 530 } 531 else { 532 colRepeat = Integer.parseInt( repeatStr, 10 ); 533 } 534 535 // text:p 536 NodeList texts = cell.getElementsByTagName( TEXT_P_ELEM ); 537 if ( texts.getLength() == 0 ) { 538 cellText = ""; 539 } 540 else { 541 // 5.1.8.0 (2010/07/01) セル内で書式設定されている場合に、テキストデータが取得されないバグを修正 542 cellText = texts.item( 0 ).getTextContent(); 543 } 544 cellInfoList.add( new CellInfo( colRepeat, cellText ) ); 545 } 546 547 if ( ! cellInfoList.isEmpty() ) { 548 // 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加 549 rowInfoList.add( new RowInfo( rowRepeat, cellInfoList.toArray( new CellInfo[cellInfoList.size()] ) ) ); 550 } 551 } 552 } 553 554 /** 555 * ODSファイルの行情報を表す構造体 556 */ 557 private static final class RowInfo { 558 public final int rowRepeat; 559 public final CellInfo[] cellInfos; 560 561 RowInfo( final int rep, final CellInfo[] cell ) { 562 rowRepeat = rep; 563 cellInfos = cell; 564 } 565 } 566 567 /** 568 * ODSファイルのカラム情報を表す構造体 569 */ 570 private static final class CellInfo { 571 public final int colRepeat; 572 public final String text; 573 574 CellInfo( final int rep, final String tx ) { 575 colRepeat = rep; 576 text = tx; 577 } 578 } 579}