/*
 * 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.plugin.io;

// import java.io.BufferedReader;
import java.io.File;										// 6.2.0.0 (2015/02/27)
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.Closer;					// 5.5.2.6 (2012/05/25)
// import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
// import org.opengion.hayabusa.db.DBTableModelUtil;
import org.opengion.hayabusa.io.AbstractTableReader;		// 6.2.0.0 (2015/02/27)
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import static org.opengion.fukurou.util.HybsConst.CR ;		// 6.2.2.0 (2015/03/27)

/**
 * XMLパーサによる、OpenOffice.org Calcの表計算ドキュメントファイルを読み取る実装クラスです。
 *
 * ①カラム名が指定されている場合
 *  #NAMEで始まる行を検索し、その行のそれぞれの値をカラム名として処理します。
 *  #NAMEで始まる行より以前の行については、全て無視されます。
 *  また、#NAMEより前のカラム及び、#NAMEの行の値がNULL(カラム名が設定されていない)カラムも
 *  無視します。
 *  読み飛ばされたカラム列に入力された値は取り込まれません。
 *  また、#NAME行以降の#で始まる行は、コメント行とみなされ処理されません。
 *
 * ②カラム名が指定されている場合
 *  指定されたカラム名に基づき、値を取り込みます。
 *  カラム名の順番と、シートに記述されている値の順番は一致している必要があります。
 *  指定されたカラム数を超える列の値については全て無視されます。
 *  #で始まる行は、コメント行とみなされ処理されません。
 *
 * また、いずれの場合も全くデータが存在していない行は読み飛ばされます。
 *
 * @og.group ファイル入力
 *
 * @version 4.0
 * @author Hiroki Nakamura
 * @since JDK5.0,
 */
// public class TableReader_Calc extends TableReader_Default {
public class TableReader_Calc extends AbstractTableReader {
	// * このプログラムのVERSION文字列を設定します。 {@value} */
	private static final String VERSION = "6.2.2.0 (2015/03/27)";

//	6.2.0.0 (2015/02/27) TableReader クラスの呼び出し元メソッドの共通化(EXCEL,TEXT)。廃止
//	private String		sheetName		;
//	private String		sheetNos		;		// 5.5.7.2 (2012/10/09)
//	private String		filename		;
//	private int			numberOfRows	;
	private int			firstClmIdx		;
	private int[]		valueClmIdx		;

	/**
	 * DBTableModel から 各形式のデータを作成して,BufferedReader より読み取ります。
	 * コメント/空行を除き、最初の行は、項目名が必要です。
	 * (但し、カラム名を指定することで、項目名を省略することができます)
	 * それ以降は、コメント/空行を除き、データとして読み込んでいきます。
	 * このメソッドは、Calc 読み込み時に使用します。
	 *
	 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加
	 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート
	 * @og.rev 6.2.0.0 (2015/02/27) TableReader クラスの呼び出し元メソッドの共通化(EXCEL,TEXT)。新規
	 *
	 * @param   file 読み取り元ファイル名
	 * @param   enc ファイルのエンコード文字列(未使用)
	 */
	@Override
//	public void readDBTable() {
	public void readDBTable( final File file , final String enc ) {

		ZipFile zipFile = null;
		boolean errFlag = false; 	// 5.0.0.1 (2009/08/15) finally ブロックの throw を避ける。
		try {
			// OpenOffice.org odsファイルを開く
			zipFile = new ZipFile( file );

			final ZipEntry entry = zipFile.getEntry( "content.xml" );
			if ( null == entry ) {
				final String errMsg = "ODSファイル中にファイルcontent.xmlが存在しません。";
				throw new HybsSystemException( errMsg );
			}

			// content.xmlをパースし、行、列単位のオブジェクトに分解します。
			final DomOdsParser odsParser = new DomOdsParser();
			odsParser.doParse( zipFile.getInputStream( entry ), sheetName , sheetNos );		// 5.5.7.2 (2012/10/09) sheetNos 対応
			final List<RowInfo> rowInfoList = odsParser.getRowInfoList();

	 		// 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加
			makeDBTableModel( rowInfoList.toArray( new RowInfo[rowInfoList.size()] ) );
		}
		catch ( IOException ex ) {
			final String errMsg = "ファイル読込みエラー[" + file + "]";
			throw new HybsSystemException( errMsg, ex );
		}
		finally {
			// 5.5.2.6 (2012/05/25) fukurou.util.Closer#zipClose( ZipFile ) を利用するように修正。
			errFlag = ! Closer.zipClose( zipFile );		// OK の場合、true なので、反転しておく。
		}

		if( errFlag ) {
			final String errMsg = "ODSファイルのクローズ中にエラーが発生しました[" + file + "]";
			throw new HybsSystemException ( errMsg );
		}
	}

//	/**
//	 * DBTableModel から 各形式のデータを作成して,BufferedReader より読み取ります。
//	 * このメソッドは、この実装クラスでは使用できません。
//	 *
//	 * @og.rev 6.2.0.0 (2015/02/27) TableReader クラスの呼び出し元メソッドの共通化(EXCEL,TEXT)。廃止
//	 *
//	 * @param reader 各形式のデータ(使用していません)
//	 */
//	@Override
//	public void readDBTable( final BufferedReader reader ) {
//		final String errMsg = "このクラスでは実装されていません。";
//		throw new UnsupportedOperationException( errMsg );
//	}

//	/**
//	 * DBTableModelのデータとしてCalcファイルを読み込むときのシート名を設定します。
//	 * これにより、複数の形式の異なるデータを順次読み込むことや、シートを指定して
//	 * 読み取ることが可能になります。
//	 * sheetNos と sheetName が同時に指定された場合は、sheetNos が優先されます。エラーにはならないのでご注意ください。
//	 * のでご注意ください。
//	 *
//	 * @og.rev 6.2.0.0 (2015/02/27) TableReader クラスの呼び出し元メソッドの共通化(EXCEL,TEXT)。廃止
//	 *
//	 * @param sheetName シート名
//	 */
//	@Override
//	public void setSheetName( final String sheetName ) {
//		this.sheetName = sheetName;
//	}

//	/**
//	 * Calcファイルを読み込むときのシート番号を指定します(初期値:0)。
//	 *
//	 * Calc読み込み時に複数シートをマージして取り込みます。
//	 * シート番号は、0 から始まる数字で表します。
//	 * ヘッダーは、最初のシートのカラム位置に合わせます。（ヘッダータイトルの自動認識はありません。）
//	 * よって、指定するシートは、すべて同一レイアウトでないと取り込み時にカラムのずれが発生します。
//	 * 
//	 * シート番号の指定は、CSV形式で、複数指定できます。また、N-M の様にハイフンで繋げることで、
//	 * N 番から、M 番のシート範囲を一括指定可能です。また、"*" による、全シート指定が可能です。
//	 * これらの組み合わせも可能です。（ 0,1,3,5-8,10-* ）
//	 * ただし、"*" に関しては例外的に、一文字だけで、すべてのシートを表すか、N-* を最後に指定するかの
//	 * どちらかです。途中には、"*" は、現れません。
//	 * シート番号は、重複(1,1,2,2)、逆転(3,2,1) での指定が可能です。これは、その指定順で、読み込まれます。
//	 * sheetNos と sheetName が同時に指定された場合は、sheetNos が優先されます。エラーにはならないのでご注意ください。
//	 * このメソッドは、isExcel() == true の場合のみ利用されます。
//	 * 
//	 * 初期値は、0（第一シート） です。
//	 *
//	 * ※ このクラスでは実装されていません。
//	 *
//	 * @og.rev 5.5.7.2 (2012/10/09) 新規追加
//	 * @og.rev 6.2.0.0 (2015/02/27) TableReader クラスの呼び出し元メソッドの共通化(EXCEL,TEXT)。廃止
//	 *
//	 * @param   sheetNos Calcファイルのシート番号（0から始まる）
//	 * @see		#setSheetName( String ) 
//	 */
//	@Override
//	public void setSheetNos( final String sheetNos ) {
//		this.sheetNos = sheetNos;
//	}

//	/**
//	 * このクラスが、EXCEL対応機能を持っているかどうかを返します。
//	 *
//	 * EXCEL対応機能とは、シート名のセット、読み込み元ファイルの Fileオブジェクト取得などの、特殊機能です。
//	 * 本来は、インターフェースを分けるべきと考えますが、taglib クラス等の 関係があり、問い合わせによる条件分岐で対応します。
//	 *
//	 * @og.rev 6.2.0.0 (2015/02/27) TableReader クラスの呼び出し元メソッドの共通化(EXCEL,TEXT)。廃止
//	 *
//	 * @return	EXCEL対応機能を持っているかどうか(常にtrue)
//	 */
//	@Override
//	public boolean isExcel() {
//		return true;
//	}

//	/**
//	 * 読み取り元ファイル名をセットします。(DIR + Filename) これは、OpenOffice.org
//	 * Calc追加機能として実装されています。
//	 *
//	 * @og.rev 6.2.0.0 (2015/02/27) TableReader クラスの呼び出し元メソッドの共通化(EXCEL,TEXT)。廃止
//	 *
//	 * @param filename 読み取り元ファイル名
//	 */
//	@Override
//	public void setFilename( final String filename ) {
//		this.filename = filename;
//		if ( filename == null ) {
//			final String errMsg = "ファイル名が指定されていません。";
//			throw new HybsSystemException( errMsg );
//		}
//	}

	/**
	 * ODSファイルをパースした結果からDBTableModelを生成します。
	 *
	 * @og.rev 5.1.6.0 (2010/05/01) skipRowCountの追加
	 *
	 * @param rowInfoList 行オブジェクトの配列(可変長引数)
	 */
//	private void makeDBTableModel( final RowInfo[] rowInfoList ) {
	private void makeDBTableModel( final RowInfo... rowInfoList ) {
		// カラム名が指定されている場合は、優先する。
		if( columns != null && columns.length() > 0 ) {
			makeHeaderFromClms();
		}

		final int skip = getSkipRowCount();						// 5.1.6.0 (2010/05/01)
		for( int row=skip; row<rowInfoList.length; row++ ) {
			final RowInfo rowInfo = rowInfoList[row];				// 5.1.6.0 (2010/05/01)
			if( valueClmIdx == null ) {
				makeHeader( rowInfo );
			}
			else {
				makeBody( rowInfo );
			}
		}

		// 最後まで、#NAME が見つから無かった場合
		if ( valueClmIdx == null ) {
			final String errMsg = "最後まで、#NAME が見つかりませんでした。" + CR
							 + "ファイル形式が異なるか、もしくは損傷している可能性があります。" + CR;
			throw new HybsSystemException( errMsg );
		}
	}

	/**
	 * 指定されたカラム一覧からヘッダー情報を生成します。
	 *
	 * @og.rev 5.1.6.0 (2010/05/01) useNumber の追加
	 * @og.rev 6.1.0.0 (2014/12/26) omitNames 属性を追加
	 * @og.rev 6.2.1.0 (2015/03/13) TableReaderModel を外部からセットします。
	 */
	private void makeHeaderFromClms() {
//		table = DBTableModelUtil.newDBTable();
		final String[] names = StringUtil.csv2Array( columns );
//		table.init( names.length );						// 6.1.0.0 (2014/12/26) 初期化は、setTableDBColumn で行う。
//		setTableDBColumn( names ) ;
		final int len = setTableDBColumn( names ) ;	// 6.1.0.0 (2014/12/26)
//		valueClmIdx = new int[names.length];
		valueClmIdx = new int[len];
		int adrs = isUseNumber() ? 1:0 ;		// useNumber =true の場合は、１件目(No)は読み飛ばす。
//		for( int i=0; i<names.length; i++ ) {
		for( int i=0; i<len; i++ ) {
			valueClmIdx[i] = adrs++;
		}
	}

	/**
	 * ヘッダー情報を読み取り、DBTableModelのオブジェクトを新規に作成します。
	 * ※ 他のTableReaderと異なり、#NAME が見つかるまで、読み飛ばす。
	 *
	 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加
	 * @og.rev 6.2.1.0 (2015/03/13) TableReaderModel を外部からセットします。
	 *
	 * @param rowInfo 行オブジェクト
	 */
	private void makeHeader( final RowInfo rowInfo ) {
		final CellInfo[] cellInfos = rowInfo.cellInfos;

		final int cellLen = cellInfos.length;
		int runPos = 0;
		ArrayList<String> nameList = null;
		ArrayList<Integer> posList = null;
		for ( int idx = 0; idx < cellLen; idx++ ) {
			// テーブルのヘッダ(#NAME)が見つかる前の行、列は全て無視される
			final CellInfo cellInfo = cellInfos[idx];
			final String text = cellInfo.text.trim();

			for ( int cellRep = 0; cellRep < cellInfo.colRepeat; cellRep++ ) {
				// 空白のヘッダは無視(その列にデータが入っていても読まない)
				if ( text.length() != 0 ) {
					if ( firstClmIdx == 0 && "#NAME".equalsIgnoreCase( text ) ) {
						nameList = new ArrayList<String>();
						posList = new ArrayList<Integer>();
//						table = DBTableModelUtil.newDBTable();		// 6.2.1.0 (2015/03/13) なぜあるのか不明
						firstClmIdx = idx;
					}
					else if ( nameList != null ) {
						nameList.add( text );
						posList.add( runPos );
					}
				}
				runPos++;
			}
		}

		if ( posList != null && ! posList.isEmpty() ) {
//			table = DBTableModelUtil.newDBTable();					// 6.2.1.0 (2015/03/13)
			// 4.3.5.0 (2009/02/01) サイズの初期値指定
			final int size = nameList.size();
			final String[] names = nameList.toArray( new String[size] );
	//		table.init( size );
			setTableDBColumn( names );

			valueClmIdx = new int[posList.size()];
			for( int i = 0; i<posList.size(); i++ ) {
				valueClmIdx[i] = posList.get( i ).intValue();
			}
		}
	}

	/**
	 * 行、列(セル)単位の情報を読み取り、DBTableModelに値をセットします。
	 *
	 * @og.rev 5.2.1.0 (2010/10/01) setTableColumnValues メソッドを経由して、テーブルにデータをセットする。
	 * @og.rev 6.2.1.0 (2015/03/13) setTableColumnValuesに、行番号を引数に追加
	 * @og.rev 6.2.2.0 (2015/03/27) Overflow処理(maxRowCount)は、Tag側に戻す。
	 *
	 * @param rowInfo 行オブジェクト
	 */
	private void makeBody( final RowInfo rowInfo ) {
		final int rowRepeat = rowInfo.rowRepeat;
		final CellInfo[] cellInfos = rowInfo.cellInfos;
		final int cellLen = cellInfos.length;
		boolean isExistData = false;

		final List<String> colData = new ArrayList<String>();
		for ( int cellIdx = 0; cellIdx < cellLen; cellIdx++ ) {
			final CellInfo cellInfo = cellInfos[cellIdx];
			for ( int cellRep = 0; cellRep < cellInfo.colRepeat; cellRep++ ) {
				colData.add( cellInfo.text );
				if( cellInfo.text.length() > 0 ) {
					isExistData = true;
				}
			}
		}

		if( isExistData ) {
			// 初めの列(#NAMEが記述されていた列)の値が#で始まっている場合は、コメント行とみなす。
			final String firstVal = colData.get( firstClmIdx );
//			if( firstVal.length() > 0 && firstVal.startsWith( "#" ) ) {
//			if( firstVal != null && firstVal.length() > 0 && firstVal.charAt(0) == '#' ) {	// 6.1.0.0 (2014/12/26) refactoring
			if( StringUtil.startsChar( firstVal , '#' ) ) {					// 6.2.0.0 (2015/02/27) １文字 String.startsWith
				return;
			}
			else {
				String[] vals = new String[valueClmIdx.length];
				for( int col = 0; col < valueClmIdx.length; col++ ) {
					vals[col] = colData.get( valueClmIdx[col] );
				}

				// 重複行の繰り返し処理
				for ( int rowIdx = 0; rowIdx < rowRepeat; rowIdx++ ) {
					// テーブルモデルにデータをセット
					// 6.2.2.0 (2015/03/27) Overflow処理(maxRowCount)は、Tag側に戻す。
//					if ( numberOfRows < getMaxRowCount() ) {
//						setTableColumnValues( vals );			// 5.2.1.0 (2010/10/01)
						setTableColumnValues( vals,rowIdx );	// 6.2.1.0 (2015/03/13)
//						numberOfRows++;
//					}
//					else {
//						table.setOverflow( true );
//					}
				}
			}
		}
		// 全くデータが存在しない行は読み飛ばし
		else {
			return;
		}
	}

	/**
	 * ODSファイルに含まれるcontent.xmlをDOMパーサーでパースし、行、列単位に
	 * オブジェクトに変換します。
	 *
	 */
	private static class DomOdsParser{

		// OpenOffice.org Calc tag Names
		private static final String TABLE_TABLE_ELEM = "table:table";
		private static final String TABLE_TABLE_ROW_ELEM = "table:table-row";
		private static final String TABLE_TABLE_CELL_ELEM = "table:table-cell";
		private static final String TEXT_P_ELEM = "text:p";

		// Sheet tag attributes
		private static final String TABLE_NAME_ATTR = "table:name";
		private static final String TABLE_NUMBER_ROWS_REPEATED_ATTR = "table:number-rows-repeated";
		private static final String TABLE_NUMBER_COLUMNS_REPEATED_ATTR = "table:number-columns-repeated";

		List<RowInfo> rowInfoList = new ArrayList<RowInfo>();
		/**
		 * DomパーサでXMLをパースする
		 *
		 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート
		 *
		 * @param inputStream InputStream
		 * @param sheetName String
		 * @param sheetNos  String
		 */
		public void doParse( final InputStream inputStream, final String sheetName, final String sheetNos ) {
			try {
				// ドキュメントビルダーファクトリを生成
				final DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
				dbFactory.setNamespaceAware( true );

				// ドキュメントビルダーを生成
				final DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
				// パースを実行してDocumentオブジェクトを取得
				final Document doc = dBuilder.parse( inputStream );
				processBook( doc, sheetName, sheetNos );			// 5.5.7.2 (2012/10/09) sheetNos 追加
			}
			catch ( ParserConfigurationException ex ) {
				throw new HybsSystemException( ex );
			}
			catch ( SAXException ex ) {
				final String errMsg = "ODSファイル中に含まれるcontent.xmlがXML形式ではありません。";
				throw new HybsSystemException( errMsg, ex );
			}
			catch ( IOException ex ) {
				throw new HybsSystemException( ex );
			}
		}

		/**
		 * 行オブジェクトのリストを返します。
		 *
		 * @return List<RowInfo>
		 */
		public List<RowInfo> getRowInfoList() {
			return rowInfoList;
		}

		/**
		 * ODSファイル全体のパースを行い、処理対象となるシートを検索します。
		 *
		 * @og.rev 5.5.7.2 (2012/10/09) sheetNos 追加による複数シートのマージ読み取りサポート
		 *
		 * @param doc Document
		 * @param sheetName String
		 * @param sheetNos  String
		 */
		private void processBook( final Document doc, final String sheetName, final String sheetNos ) {
			// table:tableを探す
			final NodeList nodetList = doc.getElementsByTagName( TABLE_TABLE_ELEM );
			final int listLen = nodetList.getLength();

			Element[] sheets = null ;			// 5.5.7.2 (2012/10/09)

			// 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。 sheetNos の指定が優先される。
			if( sheetNos != null && sheetNos.length() > 0 ) {
				final String[] sheetList = StringUtil.csv2ArrayExt( sheetNos , listLen-1 );	// 最大シート番号は、シート数-1
				sheets = new Element[sheetList.length];
				for( int i=0; i<sheetList.length; i++ ) {
					sheets[i] = (Element)nodetList.item( Integer.parseInt( sheetList[i] ) );
				}
			}
			else if( sheetName != null && sheetName.length() > 0 ) {
				Element sheet = null;
				for ( int idx = 0; idx < listLen; idx++ ) {
					final Element st = (Element)nodetList.item( idx );
					if ( sheetName.equals( st.getAttribute( TABLE_NAME_ATTR ) ) ) {
						sheet = st;
						break;
					}
				}
				if( sheet == null ) {
					final String errMsg = "対応するシートが存在しません。 sheetName=[" + sheetName + "]" ;
					throw new HybsSystemException( errMsg );
				}
				sheets = new Element[] { sheet };
			}
			else {
				final Element sheet = (Element)nodetList.item(0);
				sheets = new Element[] { sheet };
			}

			// 指定のシートがなければ、エラー
			// 6.0.2.5 (2014/10/31) null でないことがわかっている値の冗長な null チェックがあります。
//			if ( sheets == null ) {
//			final String errMsg = "対応するシートが存在しません。 sheetNos=[" + sheetNos + "] or sheetName=[" + sheetName + "]";
//				throw new HybsSystemException( errMsg );
//			}
//			else {
				// 5.5.7.2 (2012/10/09) 複数シートのマージ読み取り。
				for( int i=0; i<sheets.length; i++ ) {
					processSheet( sheets[i] );
				}
//			}
		}

		/**
		 * ODSファイルのシート単位のパースを行い、行単位のオブジェクトを生成します。
		 *
		 * @param sheet Element
		 */
		private void processSheet( final Element sheet ) {
			final NodeList rows = sheet.getElementsByTagName( TABLE_TABLE_ROW_ELEM );
			final int listLen = rows.getLength();
			int rowRepeat;
			for ( int idx = 0; idx < listLen; idx++ ) {
				final Element row = (Element)rows.item( idx );
				// 行の内容が全く同じ場合、table:number-rows-repeatedタグにより省略される。
				final String repeatStr = row.getAttribute( TABLE_NUMBER_ROWS_REPEATED_ATTR );
//				if ( repeatStr == null || repeatStr.length() == 0 ) {
				if ( repeatStr == null || repeatStr.isEmpty() ) {		// 6.1.0.0 (2014/12/26) refactoring
					rowRepeat = 1;
				}
				else {
					rowRepeat = Integer.parseInt( repeatStr, 10 );
				}

				processRow( row, rowRepeat );
			}
		}

		/**
		 * ODSファイルの行単位のパースを行い、カラム単位のオブジェクトを生成します。
		 *
		 * @og.rev 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加
		 * @og.rev 5.1.8.0 (2010/07/01) セル内で書式設定されている場合に、テキストデータが取得されないバグを修正
		 *
		 * @param row Element
		 * @param rowRepeat int
		 */
		private void processRow( final Element row, final int rowRepeat ) {
			final NodeList cells = row.getElementsByTagName( TABLE_TABLE_CELL_ELEM );
			final int listLen = cells.getLength();
			int colRepeat;
			String cellText;
			final ArrayList<CellInfo> cellInfoList = new ArrayList<CellInfo>();
			for ( int idx = 0; idx < listLen; idx++ ) {
				final Element cell = (Element)cells.item( idx );
				// カラムの内容が全く同じ場合、table:number-columns-repeatedタグにより省略される。
				final String repeatStr = cell.getAttribute( TABLE_NUMBER_COLUMNS_REPEATED_ATTR );
//				if ( repeatStr == null || repeatStr.length() == 0 ) {
				if ( repeatStr == null || repeatStr.isEmpty() ) {		// 6.1.0.0 (2014/12/26) refactoring
					colRepeat = 1;
				}
				else {
					colRepeat = Integer.parseInt( repeatStr, 10 );
				}

				// text:p
				final NodeList texts = cell.getElementsByTagName( TEXT_P_ELEM );
				if ( texts.getLength() == 0 ) {
					cellText = "";
				}
				else {
					// 5.1.8.0 (2010/07/01) セル内で書式設定されている場合に、テキストデータが取得されないバグを修正
					cellText = texts.item( 0 ).getTextContent();
				}
				cellInfoList.add( new CellInfo( colRepeat, cellText ) );
			}

			if ( ! cellInfoList.isEmpty() ) {
		 		// 4.3.5.0 (2009/02/01) toArray するときに、サイズの初期値指定を追加
				rowInfoList.add( new RowInfo( rowRepeat, cellInfoList.toArray( new CellInfo[cellInfoList.size()] ) ) );
			}
		}
	}

	/**
	 * ODSファイルの行情報を表す構造体
	 */
	private static final class RowInfo {
		public final int rowRepeat;
		public final CellInfo[] cellInfos;

		RowInfo( final int rep, final CellInfo[] cell ) {
			rowRepeat = rep;
			cellInfos = cell;
		}
	}

	/**
	 * ODSファイルのカラム情報を表す構造体
	 */
	private static final class CellInfo {
		public final int colRepeat;
		public final String text;

		CellInfo( final int rep, final String tx ) {
			colRepeat = rep;
			text = tx;
		}
	}
}
