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.fukurou.model; 017 018import org.opengion.fukurou.util.Closer; // 6.2.0.0 (2015/02/27) 019import static org.opengion.fukurou.util.HybsConst.CR; // 6.1.0.0 (2014/12/26) refactoring 020 021import java.io.InputStream; 022import java.io.File; // 6.2.0.0 (2015/02/27) 023import java.io.IOException; 024import java.util.List; // 6.0.3.0 (2014/11/13) XSSFイベントモデル 025import java.util.ArrayList; // 6.0.3.0 (2014/11/13) XSSFイベントモデル 026 027import org.apache.poi.xssf.eventusermodel.XSSFReader; 028import org.apache.poi.xssf.model.SharedStringsTable; 029import org.apache.poi.xssf.model.StylesTable; // 6.2.0.0 (2015/02/27) 030import org.apache.poi.xssf.usermodel.XSSFRichTextString; 031import org.apache.poi.openxml4j.opc.OPCPackage; 032import org.apache.poi.openxml4j.exceptions.InvalidFormatException ; 033import org.apache.poi.openxml4j.exceptions.OpenXML4JException ; // 6.1.0.0 (2014/12/26) findBugs 034import org.xml.sax.Attributes; 035import org.xml.sax.InputSource; 036import org.xml.sax.SAXException; 037import org.xml.sax.XMLReader; 038import org.xml.sax.helpers.DefaultHandler; 039import org.xml.sax.helpers.XMLReaderFactory; 040 041/** 042 * POI による、Excel(xlsx)の読み取りクラスです。 043 * 044 * xlsx形式のEXCELを、イベント方式でテキストデータを読み取ります。 045 * このクラスでは、XSSF(.xlsx)形式のファイルを、TableModelHelper を介したイベントで読み取ります。 046 * TableModelHelperイベントは、openGion形式のファイル読み取りに準拠した方法をサポートします。 047 * ※ openGion形式のEXCELファイルとは、#NAME 列に、カラム名があり、#で始まる 048 * レコードは、コメントとして判断し、読み飛ばす処理の事です。 049 * 050 * @og.rev 6.0.3.0 (2014/11/13) 新規作成 051 * @og.rev 6.2.0.0 (2015/02/27) パッケージ変更(util → model),クラス名変更(ExcelReader_XSSF → EventReader_XLSX) 052 * @og.group ファイル入力 053 * 054 * @version 6.0 055 * @author Kazuhiko Hasegawa 056 * @since JDK7.0, 057 */ 058public final class EventReader_XLSX implements EventReader { 059 /** このプログラムのVERSION文字列を設定します。 {@value} */ 060 private static final String VERSION = "6.2.0.0 (2015/02/27)" ; 061 062 /** XSSF(.xlsx)ファイル解析用の、SAXParser {@value} */ 063 public static final String SAX_PARSER = "org.apache.xerces.parsers.SAXParser" ; 064 065 /** 6.2.0.0 (2015/02/27) タイプのenum */ 066 private static enum XSSFDataType { 067 BOOL, 068 ERROR, 069 FORMULA, 070 INLINESTR, 071 SSTINDEX, 072 NUMBER, 073 } 074 075 /** 076 * 引数ファイル(Excel)を、XSSFイベントモデルを使用してテキスト化します。 077 * 078 * TableModelHelperは、EXCEL読み取り処理用の統一されたイベント処理クラスです。 079 * openGion特有のEXCEL処理方法(#NAME , 先頭行#コメントなど)を実装しています。 080 * これは、HSSFやXSSFの処理を、統一的なイベントモデルで扱うためです。 081 * SSモデルが良いのですが、巨大なXSSF(.xlsx)ファイルを解析すると、OutOfMemoryエラーが 082 * 発生する為、個々に処理する必要があります。 083 * あくまで、読み取り限定であれば、こちらのイベントモデルで十分です。 084 * 085 * @og.rev 6.0.3.0 (2014/11/13) 新規作成 086 * @og.rev 6.1.0.0 (2014/12/26) シートの数のイベント 087 * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更 088 * 089 * @param file 入力ファイル 090 * @param helper イベント処理するオブジェクト 091 */ 092 @Override 093 public void eventReader( final File file , final TableModelHelper helper ) { 094 OPCPackage pkg = null; 095 096 try { 097 // 6.2.0.0 (2015/02/27) TableModelHelper 変更に伴う修正 098 helper.startFile( file ); 099 pkg = OPCPackage.open( file ); // InvalidFormatException 100 final XSSFReader rd = new XSSFReader( pkg ); // IOException , OpenXML4JException 101 102 final XMLReader parser = XMLReaderFactory.createXMLReader( SAX_PARSER ); // SAXException 103 final List<SheetObj> shtList = getSheetList( rd,parser ); // SAXException , InvalidFormatException 104 helper.sheetSize( shtList.size() ); // 6.1.0.0 (2014/12/26) 105 106 final SharedStringsTable sst = rd.getSharedStringsTable(); // IOException , InvalidFormatException 107 final StylesTable styles = rd.getStylesTable(); 108 109 final SheetHandler handler = new SheetHandler( styles,sst,helper ); // ContentHandler のサブクラス 110 parser.setContentHandler( handler ); // ContentHandler のサブクラスを設定 111 112 // Iterator<InputStream> sheets = rd.getSheetsData(); 113 // while(sheets.hasNext()) { 114 // sheet = sheets.next(); 115 // ・・・・・ 116 // } 117 // 形式で、全シート対象に処理できますが、シート名が取り出せません。 118 119 InputStream sheet = null; 120 for( int i=0; i<shtList.size(); i++ ) { 121 final SheetObj sht = shtList.get(i); 122 123 if( helper.startSheet( sht.getName() , i ) ) { // イベント処理 124 try { 125 // シートIDは、rId# で取得できる。 126 sheet = rd.getSheet( sht.getRid() ); // IOException , InvalidFormatException 127 parser.parse( new InputSource( sheet ) ); // IOException 128 } 129 finally { 130 Closer.ioClose( sheet ); 131 } 132 } 133 helper.endSheet( i ); // イベント処理 134 } 135 } 136 // 6.1.0.0 (2014/12/26) findBugs: Bug type REC_CATCH_EXCEPTION (click for details) 137 // 例外がスローされないのに例外をキャッチしています。 138 catch( OpenXML4JException ex ) { // サブクラスの、InvalidFormatException も含まれます。 139 final String errMsg = ".xlsxのファイル解析に失敗しました。" 140 + " filename=" + file + CR 141 + ex.getMessage() ; 142 throw new RuntimeException( errMsg , ex ); 143 } 144 catch( SAXException ex ) { 145 final String errMsg = "SAX の一般的なエラーまたは警告が発生しました。" 146 + " filename=" + file + CR 147 + ex.getMessage() ; 148 throw new RuntimeException( errMsg , ex ); 149 } 150 catch( IOException ex ) { 151 final String errMsg = ".xlsxのファイルの読み取りに失敗しました。" 152 + " filename=" + file + CR 153 + ex.getMessage() ; 154 throw new RuntimeException( errMsg , ex ); 155 } 156 finally { 157 if( pkg != null ) { 158 pkg.revert(); // Close the package WITHOUT saving its content. 159 // Closer.ioClose( pkg ); // OPCPackage を close すると、書き戻しされる。 160 } 161 helper.endFile( file ); // 6.2.0.0 (2015/02/27) 162 } 163 } 164 165 /** 166 * この内部クラスは、XSSFイベントモデルに基づいた、xlsxファイルを SAX処理します。 167 * 168 * この処理のオリジナルは、https://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/xssf/eventusermodel/examples/FromHowTo.java です。 169 * 170 * また、日付変換で、StylesTable を使用するのは、http://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/xssf/eventusermodel/XLSX2CSV.java です。 171 * 172 * DefaultHandler を継承しており、xlsx の シート処理を行い、カラム番号と値を取得します。 173 * このクラス自体は、内部で使用されるため、TableModelHelper を引数に設定することで、 174 * 外部から、EXCELのセル情報の取得が可能です。 175 * 176 * @og.rev 6.0.3.0 (2014/11/13) 新規作成 177 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2) 178 * 179 * @see org.xml.sax.helpers.DefaultHandler 180 */ 181 private static final class SheetHandler extends DefaultHandler { 182 private final SharedStringsTable sst ; 183 private final TableModelHelper helper; 184 private final ExcelStyleFormat format; 185 186 private String lastContents ; 187 private XSSFDataType nextDataType = XSSFDataType.NUMBER; // 6.2.0.0 (2015/02/27) 初期化 188 private String cellStyleStr ; // 6.2.0.0 (2015/02/27) 初期化 189 190 private int rowNo = -1; // 現在の行番号 191 private int colNo = -1; // 現在の列番号 192 193 private boolean isRowSkip ; // 行の読み取りを行うかどうか 194 195 /** 196 * コンストラクター 197 * 198 * SharedStringsTable は、テキストの値を持っているオブジェクトです。 199 * ここで指定する TableModelHelper に対して、パーサー処理の結果がセットされます。 200 * 201 * @og.rev 6.0.3.0 (2014/11/13) 新規作成 202 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2) 203 * 204 * @param styles StylesTableオブジェクト 205 * @param sst SharedStringsTableオブジェクト 206 * @param helper イベント処理するオブジェクト 207 */ 208 public SheetHandler( final StylesTable styles , final SharedStringsTable sst , final TableModelHelper helper ) { 209 this.sst = sst; 210 this.helper = helper; 211 format = new ExcelStyleFormat( styles ); // 6.2.0.0 (2015/02/27) StylesTable 追加 212 } 213 214 /** 215 * 要素の開始通知を受け取ります。 216 * 217 * インタフェース ContentHandler 内の startElement メソッドをオーバーライドしています。 218 * パーサは XML 文書内の各要素の前でこのメソッドを呼び出します。 219 * 各 startElement イベントには対応する endElement イベントがあります。 220 * これは、要素が空である場合も変わりません。対応する endElement イベントの前に、 221 * 要素のコンテンツ全部が順番に報告されます。 222 * ここでは、タグがレベル3以上の場合は、上位タグの内容として取り扱います。よって、 223 * タグに名前空間が定義されている場合、その属性は削除します。 224 * 225 * @og.rev 6.0.3.0 (2014/11/13) 新規作成 226 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2) 227 * 228 * @param namespace 名前空間 URI 229 * @param localName 前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列 230 * @param qname 前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列 231 * @param attributes 要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト 232 * @see org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes ) 233 */ 234 @Override 235 public void startElement( final String namespace, final String localName, final String qname, final Attributes attributes ) { 236 if( "row".equals(qname) ) { // row 237 rowNo = Integer.parseInt( attributes.getValue("r") ) - 1; // 0 から始まる 238 isRowSkip = false; 239 } 240 else if( isRowSkip ) { return ; } 241 else if( "c".equals(qname) ) { // c => cell 242 final String kigo = attributes.getValue("r") ; // Excelの行列記号(A1 など) 243 final int[] rowCol = POIUtil.kigo2rowCol( kigo ); // Excelの行列記号を、行番号と列番号に分解します。 244 245 // rowNo = rowCol[0]; // 行番号・・・・ 246 colNo = rowCol[1]; // カラム番号 247 248 // 6.2.0.0 (2015/02/27) 日付型の処理 249 nextDataType = XSSFDataType.NUMBER; 250 cellStyleStr = attributes.getValue("s"); 251 // fmtIdx = -1; 252 // fmtStr = null; 253 254 final String cellType = attributes.getValue("t"); 255 if ( "b".equals(cellType) ) { nextDataType = XSSFDataType.BOOL; } 256 else if( "e".equals(cellType) ) { nextDataType = XSSFDataType.ERROR; } 257 else if( "inlineStr".equals(cellType) ) { nextDataType = XSSFDataType.INLINESTR; } 258 else if( "s".equals(cellType) ) { nextDataType = XSSFDataType.SSTINDEX; } 259 else if( "str".equals(cellType) ) { nextDataType = XSSFDataType.FORMULA; } 260 } 261 lastContents = ""; // なんでもクリアしておかないと、関数文字列を拾ってしまう。 262 } 263 264 /** 265 * 要素の終了通知を受け取ります。 266 * 267 * インタフェース ContentHandler 内の endElement メソッドをオーバーライドしています。 268 * SAX パーサは、XML 文書内の各要素の終わりにこのメソッドを呼び出します。 269 * 各 endElement イベントには対応する startElement イベントがあります。 270 * これは、要素が空である場合も変わりません。 271 * 272 * @og.rev 6.0.3.0 (2014/11/13) 新規作成 273 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2) 274 * @og.rev 5.9.27.0 (2017/12/01) 6.8.2.4分反映 POIで作成したEXCEL(XLSX)は、文字列を、inlineStr で持っている為、取り出し方が特殊になります。 275 * 276 * @param namespace 名前空間 URI 277 * @param localName 前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列 278 * @param qname 前置修飾子を持つ XML 1.0 修飾名。修飾名を使用できない場合は空文字列 279 * @see org.xml.sax.helpers.DefaultHandler#endElement(String , String , String ) 280 */ 281 @Override 282 public void endElement( final String namespace, final String localName, final String qname ) { 283 isRowSkip = helper.isSkip( rowNo ); // イベント 284 285 if( isRowSkip ) { return ; } 286 287 String thisStr = null; 288 289 // v は、値なので、空の場合は、イベントが発生しない。 290 if( "v".equals(qname) ) { // v の時の値出力を行う。 291 // Process the last contents as required. 292 // Do now, as characters() may be called more than once 293 switch( nextDataType ) { 294 case BOOL: 295 // 6.3.9.0 (2015/11/06) ゼロ文字列のチェックを追加 296 thisStr = lastContents.isEmpty() || lastContents.charAt(0) == '0' ? "FALSE" : "TRUE"; 297// final char first = lastContents.charAt(0); 298// thisStr = first == '0' ? "FALSE" : "TRUE"; 299 break; 300 301 case ERROR: 302 thisStr = "\"ERROR:" + lastContents + '"'; 303 break; 304 305 case FORMULA: 306 // A formula could result in a string value, 307 // so always add double-quote characters. 308 thisStr = '"' + lastContents + '"'; 309 break; 310 311 case INLINESTR: 312 // TODO: have seen an example of this, so it's untested. 313 thisStr = new XSSFRichTextString( lastContents ).toString(); 314 break; 315 316 case SSTINDEX: 317 final String sstIndex = lastContents; 318 try { 319 final int idx = Integer.parseInt( sstIndex ); 320 thisStr = new XSSFRichTextString( sst.getEntryAt(idx) ).toString(); 321 } 322 catch( final NumberFormatException ex ) { 323 final String errMsg = "Failed to parse SST index [" + sstIndex + "]: " + ex.toString() ; 324 System.out.println( errMsg ); 325 } 326 break; 327 328 case NUMBER: 329 thisStr = format.getNumberValue( cellStyleStr,lastContents ); 330 break; 331 332 default: 333 thisStr = "(TODO: Unexpected type: " + nextDataType + ")"; 334 break; 335 } 336// // v => contents of a cell 337// // Output after we've seen the string contents 338// // 文字列(値) 行 列 339// 340// helper.value( thisStr, rowNo , colNo ); 341 } 342 // 6.8.2.4 (2017/11/20) POIで作成したEXCEL(XLSX)は、文字列を、inlineStr で持っている為、取り出し方が特殊になります。 343 else if( "t".equals(qname) && nextDataType == XSSFDataType.INLINESTR ) { // t で、INLINESTR の時 344 // TODO: have seen an example of this, so it's untested. 345 thisStr = new XSSFRichTextString( lastContents ).toString(); 346 } 347 348 if( thisStr != null ) { 349 // v => contents of a cell 350 // Output after we've seen the string contents 351 // 文字列(値) 行 列 352 353 helper.value( thisStr, rowNo , colNo ); 354 } 355 } 356 357 /** 358 * 要素内の文字データの通知を受け取ります。 359 * 360 * インタフェース ContentHandler 内の characters メソッドをオーバーライドしています。 361 * 各文字データチャンクに対して特殊なアクション (ノードまたはバッファへのデータの追加、 362 * データのファイルへの出力など) を実行することができます。 363 * 364 * @og.rev 6.0.3.0 (2014/11/13) 新規作成 365 * 366 * @param buffer 文字データ配列 367 * @param start 配列内の開始位置 368 * @param length 配列から読み取られる文字数 369 * @see org.xml.sax.helpers.DefaultHandler#characters(char[] , int , int ) 370 */ 371 @Override 372 public void characters( final char[] buffer, final int start, final int length ) { 373 if( isRowSkip ) { return ; } 374 375 lastContents += new String( buffer, start, length ); // StringBuilder#append より速かった。 376 } 377 378 } 379 380 /** 381 * シート一覧を、XSSFReader から取得します。 382 * 383 * 取得元が、XSSFReader なので、xlsx 形式のみの対応です。 384 * 汎用的なメソッドではなく、大きな xlsx ファイルは、通常の DOM処理すると、 385 * 大量のメモリを消費する為、イベントモデルで処理する場合に、使います。 386 * 387 * EXCEL上のシート名を、配列で返します。 388 * 389 * @og.rev 6.0.3.0 (2014/11/13) 新規作成 390 * 391 * @param rd XSSFReaderオブジェクト 392 * @param parser XMLReaderオブジェクト 393 * @return シート名とシートIDを持つオブジェクトのリスト 394 * @throws SAXException SAX の一般的なエラーが発生 395 * @throws IOException SAXパース処理時のI/Oエラー 396 * @throws InvalidFormatException よみとったEXCEL ファイルのフォーマットが異なる。 397 */ 398 public static List<SheetObj> getSheetList( final XSSFReader rd, final XMLReader parser ) 399 throws SAXException,IOException,InvalidFormatException { 400 final List<SheetObj> shtList = new ArrayList(); 401 402 parser.setContentHandler( 403 new DefaultHandler() { 404 /** 405 * 要素の開始通知を受け取ります。 406 * 407 * @param uri 名前空間 URI 408 * @param localName 前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列 409 * @param name 前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列 410 * @param attributes 要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト 411 * @see org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes ) 412 */ 413 public void startElement( final String uri, final String localName, final String name, final Attributes attributes) { 414 if("sheet".equals(name)) { 415 final String shtNm = attributes.getValue("name"); // シート名 416 final String shtId = attributes.getValue("r:id"); // シートID( rId# #は、1から始まる ) 417 shtList.add( new SheetObj( shtNm,shtId ) ); 418 } 419 } 420 } 421 ); 422 423 InputStream workbk = null; 424 try { 425 workbk = rd.getWorkbookData(); // IOException,InvalidFormatException 426 parser.parse( new InputSource( workbk ) ); // IOException,SAXException 427 } 428 finally { 429 Closer.ioClose( workbk ); 430 } 431 432 return shtList; 433 } 434 435 /** 436 * シート名とシートIDを持つオブジェクトのインナークラス 437 * 438 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善 439 */ 440 private static final class SheetObj { 441 private final String name; 442 private final String rid ; 443 444 /** 445 * シート名とシートIDを引数に取るコンストラクター 446 * 447 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善 448 * 449 * @param name シート名 450 * @param rid シートID(rId# #は、1から始まる番号) 451 */ 452 public SheetObj( final String name , final String rid ) { 453 this.name = name; 454 this.rid = rid; 455 } 456 457 /** 458 * シート名を返します。 459 * 460 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善 461 * 462 * @return シート名 463 */ 464 public String getName() { return name ; } 465 466 /** 467 * シートIDを返します。 468 * 469 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善 470 * 471 * @return シートID(rId# #は、1から始まる番号) 472 */ 473 public String getRid() { return rid ; } 474 } 475 476 /** 477 * アプリケーションのサンプルです。 478 * 479 * 入力ファイル名 は必須で、第一引数固定です。 480 * 481 * Usage: java org.opengion.fukurou.model.EventReader_XLSX 入力ファイル名 482 * 483 * @og.rev 6.0.3.0 (2014/11/13) 新規作成 484 * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更 485 * 486 * @param args コマンド引数配列 487 */ 488 public static void main( final String[] args ) { 489 final String usageMsg = "Usage: java org.opengion.fukurou.model.EventReader_XLSX 入力ファイル名" ; 490 if( args.length == 0 ) { 491 System.err.println( usageMsg ); 492 return ; 493 } 494 495 final File file = new File( args[0] ); 496 final EventReader reader = new EventReader_XLSX(); 497 498 reader.eventReader( // 6.2.0.0 (2015/02/27) 499 file, 500 new TableModelHelper() { 501 /** 502 * シートの読み取り開始時にイベントが発生します。 503 * 504 * @param shtNm シート名 505 * @param shtNo シート番号(0〜) 506 * @return true:シートの読み取り処理を継続します/false:このシートは読み取りません。 507 */ 508 public boolean startSheet( final String shtNm,final int shtNo ) { 509 System.out.println( "S[" + shtNo + "]=" + shtNm ); 510 return super.startSheet( shtNm,shtNo ); 511 } 512 513 // public void columnNames( final String[] names ) { 514 // System.out.println( "NM=" + java.util.Arrays.toString( names ) ); 515 // } 516 517 // public void values( final String[] vals,final int rowNo ) { 518 // System.out.println( "V[" + rowNo + "]=" + java.util.Arrays.toString( vals ) ); 519 // } 520 521 // public boolean isSkip( final int rowNo ) { 522 // super.isSkip( rowNo ); 523 // return false; 524 // } 525 526 /** 527 * 読み取り状態の時に、rowNo,colNo にあるセルの値を引数にイベントが発生します。 528 * 529 * @param val 文字列値 530 * @param rowNo 行番号(0〜) 531 * @param colNo 列番号(0〜) 532 * @return 読み取りするかどうか(true:読み取りする/false:読み取りしない) 533 */ 534 public boolean value( final String val,final int rowNo,final int colNo ) { 535 System.out.println( "R[" + rowNo + "],C[" + colNo + "]=" + val ); 536 return super.value( val,rowNo,colNo ); 537 } 538 } 539 ); 540 } 541}