/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package org.opengion.fukurou.model;

import org.opengion.fukurou.util.Closer;						// 6.2.0.0 (2015/02/27)
import static org.opengion.fukurou.util.HybsConst.CR;			// 6.1.0.0 (2014/12/26) refactoring

import java.io.InputStream;
import java.io.File;							// 6.2.0.0 (2015/02/27)
import java.io.IOException;
import java.util.List;							// 6.0.3.0 (2014/11/13) XSSFイベントモデル
import java.util.ArrayList;						// 6.0.3.0 (2014/11/13) XSSFイベントモデル

// 6.0.3.0 (2014/11/13) XSSFイベントモデル
import org.apache.poi.xssf.eventusermodel.XSSFReader;
import org.apache.poi.xssf.model.SharedStringsTable;
import org.apache.poi.xssf.model.StylesTable;					// 6.2.0.0 (2015/02/27)
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException ;
import org.apache.poi.openxml4j.exceptions.OpenXML4JException ;			// 6.1.0.0 (2014/12/26) findBugs
import org.xml.sax.Attributes;
// import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;
import org.xml.sax.helpers.XMLReaderFactory;

/**
 * POI による、Excel(xlsx)の読み取りクラスです。
 *
 * xlsx形式のEXCELを、イベント方式でテキストデータを読み取ります。
 * このクラスでは、XSSF(.xlsx)形式のファイルを、TableModelHelper を介したイベントで読み取ります。
 * TableModelHelperイベントは、openGion形式のファイル読み取りに準拠した方法をサポートします。
 * ※ openGion形式のEXCELファイルとは、#NAME 列に、カラム名があり、#で始まる
 *    レコードは、コメントとして判断し、読み飛ばす処理の事です。
 *
 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
 * @og.rev 6.2.0.0 (2015/02/27) パッケージ変更(util → model),クラス名変更(ExcelReader_XSSF → EventReader_XLSX)
 * @og.group ファイル入力
 *
 * @version  6.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK7.0,
 */
// public final class ExcelReader_XSSF {
// public final class ExcelReader_XSSF implements ExcelReader {
public final class EventReader_XLSX implements EventReader {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.2.0.0 (2015/02/27)" ;

//	private static final String CR = System.getProperty("line.separator");

	/** XSSF(.xlsx)ファイル解析用の、SAXParser {@value} */
	public static final String SAX_PARSER = "org.apache.xerces.parsers.SAXParser" ;

	/** 6.2.0.0 (2015/02/27) タイプのenum */
	private static enum XSSFDataType {
		BOOL,
		ERROR,
		FORMULA,
		INLINESTR,
		SSTINDEX,
		NUMBER,
	}

	/**
	 * 引数ファイル(Excel)を、XSSFイベントモデルを使用してテキスト化します。
	 *
	 * TableModelHelperは、EXCEL読み取り処理用の統一されたイベント処理クラスです。
	 * openGion特有のEXCEL処理方法(#NAME , 先頭行#コメントなど)を実装しています。
	 * これは、HSSFやXSSFの処理を、統一的なイベントモデルで扱うためです。
	 * SSモデルが良いのですが、巨大なXSSF(.xlsx)ファイルを解析すると、OutOfMemoryエラーが
	 * 発生する為、個々に処理する必要があります。
	 * あくまで、読み取り限定であれば、こちらのイベントモデルで十分です。
	 *
	 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
	 * @og.rev 6.1.0.0 (2014/12/26) シートの数のイベント
	 * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更
	 *
	 * @param	file 入力ファイル
	 * @param	helper イベント処理するオブジェクト
	 */
	@Override
//	public static void excelReader( final String fname , final TableModelHelper helper ) {
	public void eventReader( final File file , final TableModelHelper helper ) {
		OPCPackage	pkg  = null;
//		File		file = null;		// 6.2.0.0 (2015/02/27)

		try {
			// 6.2.0.0 (2015/02/27) TableModelHelper 変更に伴う修正
//			file = new File( fname );
			helper.startFile( file );
//			pkg = OPCPackage.open( fname );								// InvalidFormatException
			pkg = OPCPackage.open( file );								// InvalidFormatException
			final XSSFReader rd = new XSSFReader( pkg );				// IOException , OpenXML4JException

			final XMLReader parser = XMLReaderFactory.createXMLReader( SAX_PARSER );	// SAXException
			final List<SheetObj> shtList = getSheetList( rd,parser );					// SAXException , InvalidFormatException
			helper.sheetSize( shtList.size() );							// 6.1.0.0 (2014/12/26)

			final SharedStringsTable sst = rd.getSharedStringsTable();			// IOException , InvalidFormatException
//			final SheetHandler handler = new SheetHandler( sst,helper );		// ContentHandler のサブクラス
//			parser.setContentHandler(handler);
			final StylesTable styles = rd.getStylesTable();

//			parser.setContentHandler( new SheetHandler( sst,helper ) );				// ContentHandler のサブクラスを設定

//			parser.setContentHandler( new SheetHandler( styles,sst,helper ) );		// ContentHandler のサブクラスを設定
			final SheetHandler handler = new SheetHandler( styles,sst,helper );		// ContentHandler のサブクラス
			parser.setContentHandler( handler );									// ContentHandler のサブクラスを設定

			// Iterator<InputStream> sheets = rd.getSheetsData();
			// while(sheets.hasNext()) {
			//     sheet = sheets.next();
			//     ・・・・・
			// }
			// 形式で、全シート対象に処理できますが、シート名が取り出せません。

			InputStream sheet = null;
			for( int i=0; i<shtList.size(); i++ ) {
				final SheetObj sht = shtList.get(i);

				if( helper.startSheet( sht.getName() , i ) ) {				// イベント処理
					try {
						// シートIDは、rId# で取得できる。
						sheet = rd.getSheet( sht.getRid() );				// IOException , InvalidFormatException
						parser.parse( new InputSource( sheet ) );			// IOException
					}
					finally {
						Closer.ioClose( sheet );
					}
				}
				helper.endSheet( i );										// イベント処理
			}
		}
		// 6.1.0.0 (2014/12/26) findBugs: Bug type REC_CATCH_EXCEPTION (click for details)
		// 例外がスローされないのに例外をキャッチしています。
		catch( OpenXML4JException ex ) {		// サブクラスの、InvalidFormatException も含まれます。
			final String errMsg = ".xlsxのファイル解析に失敗しました。"
								+ " filename=" + file + CR
								+ ex.getMessage() ;
			throw new RuntimeException( errMsg , ex );
		}
		catch( SAXException ex ) {
			final String errMsg = "SAX の一般的なエラーまたは警告が発生しました。"
								+ " filename=" + file + CR
								+ ex.getMessage() ;
			throw new RuntimeException( errMsg , ex );
		}
		catch( IOException ex ) {
			final String errMsg = ".xlsxのファイルの読み取りに失敗しました。"
								+ " filename=" + file + CR
								+ ex.getMessage() ;
			throw new RuntimeException( errMsg , ex );
		}
		finally {
			if( pkg != null ) {
				pkg.revert();						// Close the package WITHOUT saving its content.
	//			Closer.ioClose( pkg );				// OPCPackage を close すると、書き戻しされる。
			}
			helper.endFile( file );					// 6.2.0.0 (2015/02/27)
		}
	}

	/**
	 * この内部クラスは、XSSFイベントモデルに基づいた、xlsxファイルを SAX処理します。
	 *
	 * この処理のオリジナルは、https://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/xssf/eventusermodel/examples/FromHowTo.java です。
	 *
	 * また、日付変換で、StylesTable を使用するのは、http://svn.apache.org/repos/asf/poi/trunk/src/examples/src/org/apache/poi/xssf/eventusermodel/XLSX2CSV.java です。
	 *
	 * DefaultHandler を継承しており、xlsx の シート処理を行い、カラム番号と値を取得します。
	 * このクラス自体は、内部で使用されるため、TableModelHelper を引数に設定することで、
	 * 外部から、EXCELのセル情報の取得が可能です。
	 *
	 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 *
	 * @see		org.xml.sax.helpers.DefaultHandler
	 */
	private static final class SheetHandler extends DefaultHandler {
		private final SharedStringsTable	sst  ;
//		private final TableModelHelper		helper;
		private final TableModelHelper		helper;
		private final ExcelStyleFormat		format;

		private String  lastContents ;
//		private boolean isSST;			// SharedStringsTable のケース
//		private int     dtType = -1;	// 6.2.0.0 (2015/02/27) 日付型の処理(DATE=1,DATETIME=2,TIME=3)
		private XSSFDataType nextDataType = XSSFDataType.NUMBER;		// 6.2.0.0 (2015/02/27) 初期化
		private String       cellStyleStr ;								// 6.2.0.0 (2015/02/27) 初期化

//		// 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
//		private static final String[] DATE_TYPE = new String[] { "yyyyMMdd" , "yyyyMMddHHmmss" , "HHmmss" };
//		private final DateFormat[]	dtFormat = new DateFormat[DATE_TYPE.length];
//		private final StylesTable	stylesTable ;
//
//		private short	fmtIdx = -1;
//		private String	fmtStr = null;

//		private String shtNm = null ;
//		private int    shtNo = -1;

		private int		rowNo = -1;		// 現在の行番号
		private int		colNo = -1;		// 現在の列番号

		private boolean isRowSkip	;	// 行の読み取りを行うかどうか

		/**
		 * コンストラクター
		 *
		 * SharedStringsTable は、テキストの値を持っているオブジェクトです。
		 * ここで指定する TableModelHelper に対して、パーサー処理の結果がセットされます。
		 *
		 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
		 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
		 *
		 * @param	styles StylesTableオブジェクト
		 * @param	sst    SharedStringsTableオブジェクト
		 * @param	helper イベント処理するオブジェクト
		 */
//		private SheetHandler( final SharedStringsTable sst , final TableModelHelper helper ) {
		private SheetHandler( final StylesTable styles , final SharedStringsTable sst , final TableModelHelper helper ) {
//			stylesTable		= styles;
			this.sst		= sst;
			this.helper		= helper;
			format			= new ExcelStyleFormat( styles );		// 6.2.0.0 (2015/02/27) StylesTable 追加
		}

		/**
		 * 要素の開始通知を受け取ります。
		 *
		 * インタフェース ContentHandler 内の startElement メソッドをオーバーライドしています。
		 * パーサは XML 文書内の各要素の前でこのメソッドを呼び出します。
		 * 各 startElement イベントには対応する endElement イベントがあります。
		 * これは、要素が空である場合も変わりません。対応する endElement イベントの前に、
		 * 要素のコンテンツ全部が順番に報告されます。
		 * ここでは、タグがレベル３以上の場合は、上位タグの内容として取り扱います。よって、
		 * タグに名前空間が定義されている場合、その属性は削除します。
		 *
		 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
		 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
		 *
		 * @param	namespace	名前空間 ＵＲＩ
		 * @param	localName	前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
		 * @param	qname		前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列
		 * @param	attributes	要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト
		 * @see		org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes )
		 */
		@Override
		public void startElement( final String namespace, final String localName, final String qname, final Attributes attributes ) {
			if( "row".equals(qname) ) {			// row
				rowNo = Integer.parseInt( attributes.getValue("r") ) - 1;		// 0 から始まる
				isRowSkip = false;
			}
			else if( isRowSkip ) { return ; }
			else if( "c".equals(qname) ) {		// c => cell
				final String kigo  = attributes.getValue("r") ;					// Excelの行列記号(A1 など)
				final int[] rowCol = POIUtil.kigo2rowCol( kigo );				// Excelの行列記号を、行番号と列番号に分解します。

		//		rowNo = rowCol[0];			// 行番号････
				colNo = rowCol[1];			// カラム番号

//				// Figure out if the value is an index in the SST
//				isSST = "s".equals( attributes.getValue("t") );					// cellType
//				final String cSo = attributes.getValue("s");					// cellSomething

				// 6.2.0.0 (2015/02/27) 日付型の処理
				nextDataType = XSSFDataType.NUMBER;
				cellStyleStr = attributes.getValue("s");
			//	fmtIdx = -1;
			//	fmtStr = null;

				final String cellType = attributes.getValue("t");
				if (     "b".equals(cellType)			) { nextDataType = XSSFDataType.BOOL;		}
				else if( "e".equals(cellType)			) { nextDataType = XSSFDataType.ERROR;		}
				else if( "inlineStr".equals(cellType)	) { nextDataType = XSSFDataType.INLINESTR;	}
				else if( "s".equals(cellType)			) { nextDataType = XSSFDataType.SSTINDEX;	}
				else if( "str".equals(cellType)			) { nextDataType = XSSFDataType.FORMULA;	}
//				else if( cellStyleStr != null ) {
//					fmtStr = format.getFormat( cellStyleStr );
//					int stIdx = Integer.parseInt(cellStyleStr);
//					XSSFCellStyle style = stylesTable.getStyleAt( stIdx );
//			//		fmtIdx = style.getDataFormat();
//					fmtStr = style.getDataFormatString();
//			//		if( fmtStr == null ) {
//			//			fmtStr = BuiltinFormats.getBuiltinFormat( fmtIdx );
//			//		}
//					// 特殊処理：General(標準)は、除外します。
//					if( "General".equalsIgnoreCase( fmtStr ) ) { fmtStr = null; }
//				}
			}
			lastContents = "";		// なんでもクリアしておかないと、関数文字列を拾ってしまう。
		}

		/**
		 * 要素の終了通知を受け取ります。
		 *
		 * インタフェース ContentHandler 内の endElement メソッドをオーバーライドしています。
		 * SAX パーサは、XML 文書内の各要素の終わりにこのメソッドを呼び出します。
		 * 各 endElement イベントには対応する startElement イベントがあります。
		 * これは、要素が空である場合も変わりません。
		 *
		 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
		 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
		 *
		 * @param	namespace	名前空間 URI
		 * @param	localName	前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
		 * @param	qname		前置修飾子を持つ XML 1.0 修飾名。修飾名を使用できない場合は空文字列
		 * @see org.xml.sax.helpers.DefaultHandler#endElement(String , String , String )
		 */
		@Override
		public void endElement( final String namespace, final String localName, final String qname ) {
			isRowSkip = helper.isSkip( rowNo );							// イベント

			if( isRowSkip ) { return ; }

			String thisStr = null;

			// v は、値なので、空の場合は、イベントが発生しない。
			if( "v".equals(qname) ) {		// v の時のみ、値出力を行う。
				// Process the last contents as required.
				// Do now, as characters() may be called more than once
				switch( nextDataType ) {
					case BOOL:
						final char first = lastContents.charAt(0);
						thisStr = first == '0' ? "FALSE" : "TRUE";
						break;

					case ERROR:
						thisStr = "\"ERROR:" + lastContents + '"';
						break;

					case FORMULA:
						// A formula could result in a string value,
						// so always add double-quote characters.
						thisStr = '"' + lastContents + '"';
						break;

					case INLINESTR:
						// TODO: have seen an example of this, so it's untested.
						thisStr = new XSSFRichTextString( lastContents ).toString();
						break;

					case SSTINDEX:
						final String sstIndex = lastContents;
						try {
							final int idx = Integer.parseInt( sstIndex );
							thisStr = new XSSFRichTextString( sst.getEntryAt(idx) ).toString();
						}
						catch( NumberFormatException ex ) {
							final String errMsg = "Failed to parse SST index [" + sstIndex + "]: " + ex.toString() ;
							System.out.println( errMsg );
						}
						break;

					case NUMBER:
						thisStr = format.getNumberValue( cellStyleStr,lastContents );
//						thisStr = lastContents;
//						if( isDateFormat( fmtStr ) ) {			// 日付
//							thisStr = dateFormat( thisStr );
//						}
//						else {									// 数字
//				//			thisStr = org.apache.poi.ss.util.NumberToTextConverter.toText( Double.parseDouble( lastContents ) );
//							if( thisStr.endsWith( ".0" ) ) { thisStr = thisStr.substring( 0,thisStr.length()-2 ); }
//						}
						break;

					default:
						thisStr = "(TODO: Unexpected type: " + nextDataType + ")";
						break;
				}

//				if( isSST ) {
//					final int idx = Integer.parseInt(lastContents);
//					lastContents = new XSSFRichTextString(sst.getEntryAt(idx)).toString();
//					isSST = false;			// 初期化
//				}
//				// 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
//				else if( dtType >= 0 ) {
//					DateFormat dtf = dtFormat[dtType];
//					if( dtf == null ) {
//						dtf = new SimpleDateFormat( DATE_TYPE[dtType] , Locale.JAPAN );
//						dtFormat[dtType] = dtf;
//					}
//					Date dt = DateUtil.getJavaDate( Double.parseDouble( lastContents ) );
//					lastContents = dtf.format( dt );
//					dtType = -1;			// 初期化
//				}
//				// 数字の場合、EXCELの値と変換が必要
//				else {
//				//	lastContents = org.apache.poi.ss.util.NumberToTextConverter.toText( Double.parseDouble(lastContents) );
//					if( lastContents.endsWith( ".0" ) ) { lastContents = lastContents.substring( 0,lastContents.length()-2 ); }
//				}

				// v => contents of a cell
				// Output after we've seen the string contents
				//           文字列(値)    行      列

//				helper.value( lastContents, rowNo , colNo );
				helper.value( thisStr, rowNo , colNo );
			}
		}

		/**
		 * 要素内の文字データの通知を受け取ります。
		 *
		 * インタフェース ContentHandler 内の characters メソッドをオーバーライドしています。
		 * 各文字データチャンクに対して特殊なアクション (ノードまたはバッファへのデータの追加、
		 * データのファイルへの出力など) を実行することができます。
		 *
		 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
		 *
		 * @param	buffer	文字データ配列
		 * @param	start	配列内の開始位置
		 * @param	length	配列から読み取られる文字数
		 * @see org.xml.sax.helpers.DefaultHandler#characters(char[] , int , int )
		 */
		@Override
		public void characters( final char[] buffer, final int start, final int length ) {
			if( isRowSkip ) { return ; }

			lastContents += new String( buffer, start, length );		// StringBuilder#append より速かった。
		}

//		/**
//		 * フォーマット文字列から、日付型フォーマットかどうかの判定を行います。
//		 *
//		 * org.apache.poi.ss.usermodel.DateUtil#isInternalDateFormat( int ) で判定できそうですが、
//		 * 引数の formatId に何を渡せばよいのかわからないため、使えません。
//		 * ※ ただしく処理できるのかも、良く判りません。
//		 * ここでは、日本式のフォーマットをベースに、判定しています。
//		 * 以下の文字列を含む場合は、true を返し、それ以外は、false です。
//		 *   "年","月","日","yy","y/m","m/d","h:m"
//		 *
//		 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
//		 *
//		 * @param	fmt	フォーマット文字列
//		 * @return	判定結果 [true:日付型/false:それ以外]
//		 */
//		private boolean isDateFormat( final String fmt ) {
//			boolean isDf = false;
//			if( fmt != null && !fmt.isEmpty() ) {
//				// これ以外のパターンもあるかも。逆に、日付フォームでない別のフォームと一致するかも。
//				if( fmt.contains( "年" ) || fmt.contains( "月"  ) || fmt.contains( "日"  ) ||
//					fmt.contains( "yy" ) || fmt.contains( "y/m" ) || fmt.contains( "m/d" ) ||
//					fmt.contains( "h:m" ) ) {
//						isDf = true;
//				}
//			}
//
//			return isDf;
//		}

//		/**
//		 * 日付型の値を、最適なフォーマットで変換して返します。
//		 *
//		 * 日付データは、(DATE=0,DATETIME=1,TIME=2) に分類できます。
//		 * DATE とは、日付のみの状態で、引数の val は、整数に変換できます。
//		 * その場合、"yyyyMMdd" フォーマットで変換します。
//		 * DATETIME とは、日付＋時刻なので、"yyyyMMddHHmmss" に変換します。
//		 * TIME は、日付情報を持っていないため、"HHmmss" に変換します。
//		 *
//		 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
//		 *
//		 * @param	val	日付型の値
//		 * @return	日付型の変換結果
//		 */
//		private String dateFormat( final String val ) {
//			int dtType = 0;		// 日付型の処理(DATE=0,DATETIME=1,TIME=2)
//			final double tmp = Double.parseDouble( val );
//			if( tmp < 1.0d ) {												// 日付部が無い → TIME=2
//				dtType = 2;
//			}
//			else if( Double.compare( tmp , Math.floor( tmp ) ) != 0 ) {		// 整数でない → DATETIME=1
//				dtType = 1;
//			}
//			else {															// Long 相当 → DATE=0
//				dtType = 0;
//			}
//
//			DateFormat dtf = dtFormat[dtType];			// 各タイプ別にキャッシュしている。
//			if( dtf == null ) {
//				dtf = new SimpleDateFormat( DATE_TYPE[dtType] , Locale.JAPAN );
//				dtFormat[dtType] = dtf;
//			}
//			Date dt = DateUtil.getJavaDate( tmp );
//			return dtf.format( dt );
//		}
	}

	/**
	 * シート一覧を、XSSFReader から取得します。
	 *
	 * 取得元が、XSSFReader なので、xlsx 形式のみの対応です。
	 * 汎用的なメソッドではなく、大きな xlsx ファイルは、通常の DOM処理すると、
	 * 大量のメモリを消費する為、イベントモデルで処理する場合に、使います。
	 *
	 * EXCEL上のシート名を、配列で返します。
	 *
	 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
	 *
	 * @param	rd XSSFReaderオブジェクト
	 * @param	parser XMLReaderオブジェクト
	 * @return	シート名とシートIDを持つオブジェクトのリスト
	 * @throws	SAXException			SAX の一般的なエラーが発生
	 * @throws	IOException				SAXパース処理時のI/Oエラー
	 * @throws	InvalidFormatException	よみとったEXCEL ファイルのフォーマットが異なる。
	 */
	public static List<SheetObj> getSheetList( final XSSFReader rd, final XMLReader parser )
														throws SAXException,IOException,InvalidFormatException {
		final List<SheetObj> shtList = new ArrayList<SheetObj>();

		parser.setContentHandler(
			new DefaultHandler() {
				/**
				 * 要素の開始通知を受け取ります。
				 *
				 * @param	uri			名前空間 ＵＲＩ
				 * @param	localName	前置修飾子を含まないローカル名。名前空間処理が行われない場合は空文字列
				 * @param	name		前置修飾子を持つ修飾名。修飾名を使用できない場合は空文字列
				 * @param	attributes	要素に付加された属性。属性が存在しない場合、空の Attributesオブジェクト
				 * @see		org.xml.sax.helpers.DefaultHandler#startElement(String , String , String , Attributes )
				 */
				public void startElement( final String uri, final String localName, final String name, final Attributes attributes) {
					if("sheet".equals(name)) {
						final String shtNm = attributes.getValue("name");		// シート名
						final String shtId = attributes.getValue("r:id");		// シートID( rId#  #は、１から始まる )
						shtList.add( new SheetObj( shtNm,shtId ) );
					}
				}
			}
		);

		InputStream workbk = null;
		try {
			workbk = rd.getWorkbookData();										// IOException,InvalidFormatException
			parser.parse( new InputSource( workbk ) );							// IOException,SAXException
		}
		finally {
			Closer.ioClose( workbk );
		}

		return shtList;
	}

	/**
	 * シート名とシートIDを持つオブジェクトのインナークラス
	 *
	 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善
	 */
	private static final class SheetObj {
		private final String name;
		private final String rid ;

		/**
		 * シート名とシートIDを引数に取るコンストラクター
		 *
		 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善
		 *
		 * @param	name シート名
		 * @param	rid  シートID(rId#  #は、１から始まる番号)
		 */
		public SheetObj( final String name , final String rid ) {
			this.name = name;
			this.rid  = rid;
		}

		/**
		 * シート名を返します。
		 *
		 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善
		 *
		 * @return	シート名
		 */
		public String getName() { return name ; }

		/**
		 * シートIDを返します
		 *
		 * @og.rev 6.1.0.0 (2014/12/26) Excel関係改善
		 *
		 * @return	シートID(rId#  #は、１から始まる番号)
		 */
		public String getRid()  { return rid ; }
	}

	/**
	 * アプリケーションのサンプルです。
	 *
	 * 入力ファイル名 は必須で、第一引数固定です。
	 *
	 * Usage: java org.opengion.fukurou.model.EventReader_XLSX 入力ファイル名
	 *
	 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
	 * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {
		final String usageMsg = "Usage: java org.opengion.fukurou.model.EventReader_XLSX 入力ファイル名" ;
		if( args.length == 0 ) {
			System.err.println( usageMsg );
			return ;
		}

		final File file = new File( args[0] );
//		final ExcelReader reader = new ExcelReader_XSSF();
		final EventReader reader = new EventReader_XLSX();

//		ExcelReader_XSSF.excelReader(
		reader.eventReader(					// 6.2.0.0 (2015/02/27)
			file,
//			new TableModelHelper() {
			new TableModelHelper() {
				/**
				 * シートの読み取り開始時にイベントが発生します。
				 *
				 * @param   shtNm  シート名
				 * @param   shtNo  シート番号(0～)
				 * @return  true:シートの読み取り処理を継続します/false:このシートは読み取りません。
				 */
				public boolean startSheet( final String shtNm,final int shtNo ) {
					System.out.println( "S[" + shtNo + "]=" + shtNm );
					return super.startSheet( shtNm,shtNo );
				}

		//		public void columnNames( final String[] names ) {
		//			System.out.println( "NM=" + java.util.Arrays.toString( names ) );
		//		}

		//		public void values( final String[] vals,final int rowNo ) {
		//			System.out.println( "V[" + rowNo + "]=" + java.util.Arrays.toString( vals ) );
		//		}

		//		public boolean isSkip( final int rowNo ) {
		//			super.isSkip( rowNo );
		//			return false;
		//		}

				/**
				 * 読み取り状態の時に、rowNo,colNo にあるセルの値を引数にイベントが発生します。
				 *
				 * @param   val     文字列値
				 * @param   rowNo   行番号(0～)
				 * @param   colNo   列番号(0～)
				 * @return  読み取りするかどうか(true:読み取りする/false:読み取りしない)
				 */
				public boolean value( final String val,final int rowNo,final int colNo ) {
					System.out.println( "R[" + rowNo + "],C[" + colNo + "]=" + val );
					return super.value( val,rowNo,colNo );
				}
			}
		);
	}
}
