/*
 * 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.hayabusa.report2;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.channels.FileChannel;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.opengion.fukurou.model.NativeType;
import org.opengion.fukurou.util.Closer;
import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.util.QrcodeImage;
import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;

/**
 * 指定されたパスに存在するODSの各XMLファイルをパースし、帳票定義及び
 * 帳票データから書き換えます。
 * 書き換えは読み取り先と同じファイルであるため、一旦読み取った各XMLを
 * メモリ上に格納したからパース後のXMLファイルの書き込みを行います。
 *
 * パース対象となるファイルは以下の3つです。
 *  content.xml シートの中身を定義
 *  meta.xml    メタデータを定義
 *  style.xml   帳票ヘッダーフッターを定義
 *
 * content.xmlのパース処理として、まずxmlファイルをシート+行単位に分解します。
 * その後、分解された行毎に帳票データを埋め込み、出力先のXMLに書き込みを行います。
 * 書き込みは行単位に行われます。
 *
 * また、Calcの特性として、関数の引数に不正な引数が指定された場合、(Text関数の
 * 引数にnullが指定された場合等)、エラー:XXXという文字が表示されます。
 * ここでは、これを回避するため、全ての関数にisError関数を埋め込み、エラー表示を
 * 行わないようにしています。
 *
 * @og.group 帳票システム
 *
 * @version  4.0
 * @author   Hiroki.Nakamura
 * @since    JDK1.6
 */
class OdsContentParser {

	//======== content.xmlのパースで使用 ========================================
	/* シートの開始終了タグ */
	private static final String BODY_START_TAG = "<table:table ";
	private static final String BODY_END_TAG = "</table:table>";

	/* 行の開始終了タグ */
	private static final String ROW_START_TAG = "<table:table-row ";

	/* ページエンドカットの際に、行を非表示にするためのテーブル宣言 */
	private static final String ROW_START_TAG_INVISIBLE = "<table:table-row table:visibility=\"collapse\" ";

	/* セルの開始タグ */
	private static final String TABLE_CELL_START_TAG = "<table:table-cell";
	private static final String TABLE_CELL_END_TAG = "</table:table-cell>";

	/* シート名を取得するための開始終了文字 */
	private static final String SHEET_NAME_START = "table:name=\"";

	/* オブジェクトの終了位置(シート名)を見つけるための開始文字 */
	private static final String OBJECT_SEARCH_STR = "table:end-cell-address=\"";

	/* 印刷範囲指定の開始終了文字 */
	// 4.3.3.5 (2008/11/08) 空白ページ対策で追加
	private static final String PRINT_RANGE_START = "table:print-ranges=\"";
	private static final String PRINT_RANGE_END = "\"";

	/* 表紙印刷用のページ名称 */
	private static final String FIRST_PAGE_NAME = "FIRST";

	/* シートブレイク用のキー 5.1.7.0 (2010/06/01) */
	private static final String SHEET_BREAK = "SHEETBREAK";

	/* 変数定義の開始終了文字及び区切り文字 */
	private static final String VAR_START = "{@";
	private static final String VAR_END = "}";
	private static final String VAR_CON = "_";

	/* ページエンドカットのカラム文字列 */
	private static final String PAGE_END_CUT = "PAGEENDCUT";

	/* ページブレイクのカラム文字列 */
	private static final String PAGE_BREAK = "PAGEBREAK";

	/* ページ番号出力用文字列 5.1.6.0 (2010/05/01) */
	private static final String PAGE_NO= "PAGENO";

	/* 行番号出力用文字列 5.1.6.0 (2010/05/01) */
	private static final String ROW_NO= "ROWNO";

	/* 画像のリンクを取得するための開始終了文字 */
	private static final String DRAW_IMG_START_TAG = "<draw:image xlink:href=\"";
	private static final String DRAW_IMG_END_TAG = "</draw:image>";
	private static final String DRAW_IMG_HREF_END = "\"";

	/* 画像ファイルを保存するためのパス */
	private static final String IMG_DIR = "Pictures";

	/* QRコードを処理するためのカラム名 */
	private static final String QRCODE_PREFIX = "QRCODE.";

	/* 作成したQRコードのフォルダ名及び拡張子 */
	private static final String QRCODE_FILETYPE = ".png";

	/* 4.3.3.5 (2008/11/08) 動的に画像を入れ替えるためのパスを記述するカラム名 */
	private static final String IMG_PREFIX = "IMG.";

	/* ファンクション定義を見つけるための開始終了文字 */
	private static final String OOOC_FUNCTION_START = "oooc:=";
	private static final String OOOC_FUNCTION_START_3 = "of:="; // 4.3.7.2 (2009/06/15) ODS仕様変更につき追加
	private static final String OOOC_FUNCTION_END = ")\" ";

	/* セル内の改行を定義する文字列 5.0.2.0 (2009/11/01) */
	private static final String OOO_CR = "</text:p><text:p>";

	/* グラフオブジェクトの書き換えを行うための開始終了文字 5.1.8.0 (2010/07/01) */
	private static final String GRAPH_START_TAG = "<draw:frame ";
	private static final String GRAPH_END_TAG = "</draw:frame>";
	/* グラフの範囲指定の書き換えを行うための開始終了文字 5.1.8.0 (2010/07/01) */
	private static final String GRAPH_UPDATE_RANGE_START = "draw:notify-on-update-of-ranges=\"";
	private static final String GRAPH_UPDATE_RANGE_END = "\"";
	/* グラフのオブジェクトへのリンクの書き換えを行うための開始終了文字 5.1.8.0 (2010/07/01) */
	private static final String GRAPH_HREF_START = "xlink:href=\"./";
	private static final String GRAPH_HREF_END = "\"";
	private static final String GRAPH_OBJREPL = "ObjectReplacements";
	/* グラフのオブジェクト毎のcontent.xmlに記述してあるシート名の書き換えを行うための開始終了文字 5.1.8.0 (2010/07/01) */
	private static final String GRAPH_CONTENT_START = "-address=\"";
	private static final String GRAPH_CONTENT_END = "\"";
	/* 生成したグラフのオブジェクトをMETA-INF/manifest.xmlに登録するための開始終了文字列 5.1.8.0 (2010/07/01) */
	private static final String MANIFEST_START_TAG = "<manifest:file-entry ";
	private static final String MANIFEST_END_TAG = "/>";

	/* 数値タイプ置き換え用の文字列 5.1.8.0 (2010/07/01) */
	private static final String TABLE_CELL_STRING_TYPE = "office:value-type=\"string\"";
	private static final String TABLE_CELL_FLOAT_TYPE = "office:value-type=\"float\"";
	private static final String TABLE_CELL_FLOAT_VAL_START = "office:value=\"";
	private static final String TABLE_CELL_FLOAT_VAL_END = "\"";

	/* テキスト文字列の開始終了文字列 5.1.8.0 (2010/07/01) */
	private static final String TEXT_START_TAG = "<text:p>";
	private static final String TEXT_END_TAG = "</text:p>";

	/* コメント(アノテーション)を処理するためのカラム名 5.1.8.0 (2010/07/01) */
	private static final String ANNOTATION_PREFIX = "ANO.";
	private static final String TEXT_START_ANO_TAG = "<text:p"; // アノテーションの場合の置き換えを
	private static final String TEXT_START_END_ANO_TAG = ">"; // アノテーションの場合の置き換えを

	/* コメント(アノテーション)の開始・終了タグ 5.1.8.0 (2010/07/01) */
	private static final String ANNOTATION_START_TAG = "<office:annotation";
	private static final String ANNOTATION_END_TAG = "</office:annotation>";

	/* オブジェクトを検索するための文字列 5.1.8.0 (2010/07/01) */
	private static final String DRAW_START_KEY = "<draw:";
	private static final String DRAW_END_KEY = "</draw:";

	/* シートの開始終了タグ 5.2.2.0 (2010/11/01) */
	private static final String STYLE_START_TAG = "<style:style ";
	private static final String STYLE_END_TAG = "</style:style>";

	/* シート名称 5.2.2.0 (2010/11/01) */
	private static final String STYLE_NAME_START_TAG = "style:name=\"";
	private static final String STYLE_NAME_END_TAG = "\"";

	/* テーブル内シート名称 5.2.2.0 (2010/11/01) */
	private static final String TABLE_STYLE_NAME_START_TAG = "table:style-name=\"";
	private static final String TABLE_STYLE_NAME_END_TAG = "\""; // 5.6.3.1 (2013/04/05)

	//===========================================================================

	//======== meta.xmlのパースで使用 ===========================================
	/* 総シートカウント数 */
	private static final String TABLE_COUNT_START_TAG = "meta:table-count=\"";
	private static final String TABLE_COUNT_END_TAG = "\"";

	/* 総セルカウント数 */
	private static final String CELL_COUNT_START_TAG = "meta:cell-count=\"";
	private static final String CELL_COUNT_END_TAG = "\"";

	/* 総オブジェクトカウント数 */
	private static final String OBJECT_COUNT_START_TAG = "meta:object-count=\"";
	private static final String OBJECT_COUNT_END_TAG = "\"";
	//===========================================================================

	/*
	 * 処理中の行番号の状態
	 * NORMAL : 通常
	 * LASTROW : 最終行
	 * OVERFLOW : 終了
	 */
	private static final int NORMAL = 0;
	private static final int LASTROW = 1;
	private static final int OVERFLOW = 2;
	private int status = NORMAL;

	/*
	 * 各雛形ファイルを処理する際の基準となる行数
	 * 初期>0 2行({&#064;XXX_1}まで)処理後>2 ・・・
	 * 各雛形で定義されている行番号 + [baseRow] の値がDBTableModel上の行番号に相当する
	 * currentMaxRowは各シート処理後の[baseRow]と同じ
	 */
	private int currentBaseRow = 0;
	private int currentMaxRow = 0;

	/* 処理したページ数 */
	private int pages = 0;

	/* 処理行がページエンドカットの対象かどうか */
	private boolean isPageEndCut = false;			// 4.3.1.1 (2008/08/23) ローカル変数化

	/* ページブレイクの処理中かどうか */
	private boolean isPageBreak = false;

	/* XML宣言の文字列。各XMLで共通なのでクラス変数として定義 */
	private String xmlHeader = null;

	/* シートブレイク対象かどうか 5.1.7.0 (2010/06/01) */
	private int sheetBreakClm = -1;

	/* シート名カラム 5.7.6.2 (2014/05/16) */
	private int sheetNameClm = -1;						// 今は、ページブレイクカラムと同じカラムを使用しています。

	/* シートのヘッダー部分の再パースを行うかどうか  5.2.2.0 (2010/11/01) */
	private boolean isNeedsReparse = false;

	/* ページ名のマッピング(元のシート名に対する新しいシート名) 5.2.2.0 (2010/11/01) */
	private final Map<String,List<String>> pageNameMap = new HashMap<String,List<String>>();

	/* ページ名に依存しているスタイル名称のリスト 5.2.2.0 (2010/11/01) */
	private final List<String> repStyleList = new ArrayList<String>();

	/* manifest.xmlに追加が必要なオブジェクトのマップ 5.3.1.0 (2011/01/01) */
	private final Map<String,String> addObjs = new HashMap<String,String>();

	private final ExecQueue queue;
	private final String path;

	/**
	 * コンストラクタ
	 *
	 * @og.rev 5.1.2.0 (2010/01/01) 処理した行数をQueueオブジェクトから取得(シート数が256を超えた場合の対応)
	 *
	 * @param qu ExecQueue
	 * @param pt String
	 */
	OdsContentParser( final ExecQueue qu, final String pt ) {
		queue = qu;
		path = pt;

		currentBaseRow = queue.getExecRowCnt();
	}

	/**
	 * パース処理を実行します
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
	 * @og.rev 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
	 */
	public void exec() {
		/*
		 * 雛形ヘッダーフッターの定義
		 * OOoではページ毎にヘッダーフッターが設定できないよう。
		 * なので、全てヘッダー扱いで処理
		 */
		execStyles();

		/* 中身の変換 */
		execContent();

		/* ヘッダー部分にシート情報がある場合に書き換え */
		if( isNeedsReparse ) {
			/* ヘッダーファイルの再パース */
			execContentHeader();
			/* ヘッダーファイルとそれ以降のファイルの連結 */
			execMergeContent();
		}

		/* メタデータの変換 */
		execMeta();

		// 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
		/* 追加した画像、オブジェクトをmanifest.xmlに追加 */
		if( addObjs.size() > 0 ) {
			execManifest();
		}
	}

	/**
	 * 帳票処理キューを元に、content.xmlを書き換えます。
	 * まず、XMLを一旦メモリ上に展開した後、シート単位に分解し、データの埋め込みを行います。
	 *
	 * @og.rev 4.3.0.0 (2008/07/18) ページ数が256を超えた場合のエラー処理
	 * @og.rev 5.0.0.2 (2009/09/15) LINECOPY機能追加
	 * @og.rev 5.1.2.0 (2010/01/01) 処理したページ数、行数をQueueオブジェクトにセット(シート数が256を超えた場合の対応)
	 * @og.rev 5.1.7.0 (2010/06/01) 複数シート対応
	 * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
	 * @og.rev 5.7.6.2 (2014/05/16) PAGEBREAKカラムの値を、シート名として使う場合の処理追加
	 * @og.rev 5.7.6.3 (2014/05/23) PAGEBREAKカラムの値を、シート名として使う場合の、FIRST雛形への適用
	 */
	private void execContent() {
		String fileName = path + "content.xml";
		String content = readOOoXml( fileName );
		// ファイルの解析し、シート+行単位に分解
		String[] tags = tag2Array( content, BODY_START_TAG, BODY_END_TAG );

		// 5.2.2.0 (2010/11/01) 条件付書式対応
		// content.xmlのヘッダー部分のみ書き出し
		String contentHeader = tags[0];
		BufferedWriter bw = null;
		try {
			bw = getWriter( fileName );
			bw.write( xmlHeader );
			bw.write( '\n' );
			bw.write( contentHeader );
			bw.flush();
		}
		catch ( IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:error occurer while content.xml(header) " + fileName + HybsSystem.CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( bw );
			bw = null;
		}

		String contentFooter = tags[1];
		List<OdsSheet> firstSheets = new ArrayList<OdsSheet>();
		Map<String, OdsSheet> sheets = new HashMap<String,OdsSheet>();
		OdsSheet defaultSheet = null;
		for( int i = 2; i < tags.length; i++ ) {
			OdsSheet sheet = new OdsSheet();
			// sheet.analyze( tags[i] );
			sheet.analyze( tags[i],queue.getBody().getRowCount() ); // 5.0.0.2 (2009/09/15)
			// 5.1.7.0 (2010/06/01) 複数シート対応
			String sheetName = sheet.getSheetName();
			if( sheetName.startsWith( FIRST_PAGE_NAME ) ) {
				firstSheets.add( sheet );
			}
			else {
				sheets.put( sheetName, sheet );
				// 一番初めに見つかった表紙以外のシートをデフォルトシートとして設定
				if( defaultSheet == null ) {
					defaultSheet = sheet;
				}
			}

			// 5.2.2.0 (2010/11/01) 条件付書式対応
			if( !isNeedsReparse && contentHeader.indexOf( "=\"" + sheet.getOrigSheetName() + "." ) >= 0 ) {
				isNeedsReparse = true;
			}

			// 5.2.2.0 (2010/11/01) 条件付書式対応
			pageNameMap.put( sheet.getOrigSheetName(), new ArrayList<String>() );
		}

		// content.xmlの書き出し
		try {
			// 5.2.2.0 (2010/11/01) 条件付書式対応
			if( isNeedsReparse ) {
				// ヘッダーを再パースする場合は、ボディ部分を
				// content.xml.tmpに書き出して、後でマージする
				bw = getWriter( fileName + ".tmp" );
				getRepStyleList( contentHeader );
			}
			else {
				// ヘッダーを再パースすしない場合は、ボディ部分を
				// content.xml追加モードで書き込みする
				bw = getWriter( fileName, true );
			}

			// 5.7.6.3 (2014/05/23) PAGEBREAKカラムの値を、シート名として使うかどうか。
			if( queue.isUseSheetName() ) {
				sheetNameClm = queue.getBody().getColumnNo( PAGE_BREAK, false );
			}

			// 表紙ページの出力
			if( queue.getExecPagesCnt() == 0 ) {
				for ( OdsSheet firstSheet : firstSheets ) {
					if ( currentBaseRow >= queue.getBody().getRowCount() ) {
						break;
					}
					writeParsedSheet( firstSheet, bw );
				}
			}

			// 5.1.7.0 (2010/06/01) 複数シート対応
			sheetBreakClm = queue.getBody().getColumnNo( SHEET_BREAK, false );

			// 5.7.6.3 (2014/05/23) 表紙ページも、PAGEBREAKカラムの値を、シート名として使えるようにする。
//			// 5.7.6.2 (2014/05/16) PAGEBREAKカラムの値を、シート名として使うかどうか。
//			if( queue.isUseSheetName() ) {
//				sheetNameClm = queue.getBody().getColumnNo( PAGE_BREAK, false );
//			}

			// 繰り返しページの出力
			while ( currentBaseRow < queue.getBody().getRowCount() ) {
				// 4.3.0.0 (2008/07/18) ページ数が256を超えた場合にエラーとする
				// 5.1.2.0 (2010/01/01) 256シートを超えた場合の対応
				if( pages >= ExecQueue.MAX_SHEETS_PER_FILE ) {
					queue.setEnd( false );
					break;
				}

				OdsSheet sheet = null;
				if( sheetBreakClm >= 0 ) {
					String sheetName = queue.getBody().getValue( currentBaseRow, sheetBreakClm );
					if( sheetName != null && sheetName.length() > 0 ) {
						sheet = sheets.get( sheetName );
					}
				}
				if( sheet == null ) { sheet = defaultSheet; }

				writeParsedSheet( sheet, bw );
			}

			// 5.1.2.0 (2010/01/01) 256シートを超えた場合の対応
			queue.addExecPageCnt( pages );
			queue.setExecRowCnt( currentBaseRow );

			// フッター
			bw.write( contentFooter );
			bw.flush();
		}
		catch ( IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:error occurer while write Parsed Sheet " + fileName + HybsSystem.CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( bw );
		}
	}

	/**
	 * シート単位にパースされた文書データを書き込みます
	 * 出力されるシート名には、ページ番号と基底となる行番号をセットします。
	 *
	 * @og.rev 4.2.4.0 (2008/07/04) 行単位にファイルに書き込むように変更
	 * @og.rev 5.2.0.0 (2010/09/01) 表紙の場合は、BODY部分のデータが含まれていなくてもOK
	 * @og.rev 5.2.1.0 (2010/10/01) シート名定義対応
	 * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
	 * @og.rev 5.7.6.2 (2014/05/16) PAGEBREAKカラムの値を、シート名として使う場合の処理追加
	 * @og.rev 5.7.6.3 (2014/05/23) FIRST雛形シート名が、FIRST**** の場合、**** 部分をシート名に使う。
	 *
	 * @param sheet
	 * @param bw
	 * @throws IOException 書き込みに失敗した場合
	 */
	private void writeParsedSheet( final OdsSheet sheet, final BufferedWriter bw ) throws IOException {
		// シート名
		String outputSheetName = null;

		// 5.7.6.2 (2014/05/16) PAGEBREAKカラムの値を、シート名として使うかどうか。
		if( sheetNameClm >= 0 ) {
			String sheetName = queue.getBody().getValue( currentBaseRow, sheetNameClm );
			if( sheetName != null ) {
				outputSheetName = sheetName;
			}
		}

		// 5.7.6.3 (2014/05/23) FIRST雛形シート名が、FIRST**** の場合、**** 部分をシート名に使う。
		if( outputSheetName == null ) {
			String sheetName = sheet.getSheetName();
			if( sheetName.startsWith( FIRST_PAGE_NAME ) ) {
				sheetName = sheetName.substring( FIRST_PAGE_NAME.length() ).trim();
				// 小細工。"FIRST_****" の場合は、"_" を外す。長さ０判定の前に行う。
				if( sheetName.startsWith( "_" ) ) { sheetName = sheetName.substring( 1 ); }
				// 長さ０の場合（例えば、FIRSTだけとか）は、設定しない。
				if( sheetName.length() > 0 ) { outputSheetName = sheetName; }
			}
		}

		// 従来からあるシート名の値
		if( outputSheetName == null ) {
			if( sheet.getConfSheetName() == null ) {
//				outputSheetName = "Page" + ( queue.getExecPagesCnt() + pages ) + "_" + "Row" + currentBaseRow + "";
				outputSheetName = "Page" + ( queue.getExecPagesCnt() + pages ) + "_Row" + currentBaseRow ;
			}
			else {
//				outputSheetName = sheet.getConfSheetName() + queue.getExecPagesCnt() + pages + 1 ;
				outputSheetName = sheet.getConfSheetName() + ( queue.getExecPagesCnt() + pages + 1 ) ;
			}
		}
		// ページブレイク変数を初期化
		isPageBreak = false;

		// シートのヘッダー部分を書き込み(シート名も書き換え)
		String headerStr = sheet.getHeader().replace( SHEET_NAME_START + sheet.getOrigSheetName(), SHEET_NAME_START + outputSheetName );

		// 印刷範囲指定部分のシート名を変更
		// 4.3.3.5 (2008/11/08) 空白ページ出力の対策。印刷範囲のシート名書き換えを追加
		int printRangeStart = headerStr.indexOf( PRINT_RANGE_START );
		if( printRangeStart >= 0 ) {
			int printRangeEnd = headerStr.indexOf( PRINT_RANGE_END, printRangeStart + PRINT_RANGE_START.length() );
			String rangeStr = headerStr.substring( printRangeStart, printRangeEnd );
			rangeStr = rangeStr.replace( sheet.getOrigSheetName(), outputSheetName );
			headerStr = headerStr.substring( 0, printRangeStart ) + rangeStr + headerStr.substring( printRangeEnd );
		}

		bw.write( headerStr );

		// シートのボディ部分を書き込み
		String[] rows = sheet.getRows();
		for( int i = 0; i < rows.length; i++ ) {
			// 4.3.4.4 (2009/01/01)
			writeParsedRow( rows[i], bw, sheet.getOrigSheetName(), outputSheetName );
		}
		// {@XXXX}が埋め込まれていない場合はエラー
		// 5.2.0.0 (2010/09/01) 表紙の場合は、BODY部分のデータが含まれていなくてもOK
		if( currentBaseRow == currentMaxRow && !sheet.getOrigSheetName().startsWith( FIRST_PAGE_NAME ) ) {
			queue.addMsg( "[ERROR]PARSE:No Data defined on Template ODS(" + queue.getListId() + ")" + HybsSystem.CR );
			throw new HybsSystemException();
		}
		currentBaseRow = currentMaxRow;

		// シートのフッター部分を書き込み
		bw.write( sheet.getFooter() );

		pages++;

		// 5.2.2.0 (2010/11/01) 条件付書式対応
		pageNameMap.get( sheet.getOrigSheetName() ).add( outputSheetName );
	}

	/**
	 * 行単位にパースされた文書データを書き込みます
	 *
	 * @og.rev 4.2.3.1 (2008/06/19) 関数エラーを表示させないため、ISERROR関数を埋め込み
	 * @og.rev 4.2.4.0 (2008/07/04) 行単位にファイルに書き込むように変更
	 * @og.rev 4.3.0.0 (2008/07/17) ｛＠と｝の整合性チェック追加
	 * @og.rev 4.3.0.0 (2008/07/22) 行最後の｛＠｝整合性エラーハンドリング追加
	 * @og.rev 4.3.3.5 (2008/11/08) 画像の動的な入れ替えに対応
	 * @og.rev 5.1.8.0 (2010/07/01) パース方法の内部実装変更
	 * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
	 * @og.rev 5.4.2.0 (2011/12/01) ページブレイク、シートブレイク中でもページエンドカットが適用されるようにする。
	 * @og.rev 5.6.3.1 (2013/04/05) 条件付書式の属性終了文字対応
	 *
	 * @param row
	 * @param bw
	 * @param sheetNameOrig
	 * @param sheetNameNew
	 * @throws IOException 書き込みに失敗した場合
	 */
	private void writeParsedRow( final String row, final BufferedWriter bw, final String sheetNameOrig, final String sheetNameNew ) throws IOException {
		isPageEndCut = false;

		String rowStr = new TagParser() {
			@Override
			protected void exec( final String str, final StringBuilder buf, final int offset ) {
				String key = TagParser.checkKey( str, buf );

				// 4.3.0.0 (2008/07/15) "<"が入っていた場合には{@不整合}エラー
				if( key.indexOf( '<' ) >= 0 ){
					queue.addMsg( "[ERROR]PARSE:{@と}の整合性が不正です。変数内の特定の文字列に書式設定がされている可能性があります。キー=" + key + HybsSystem.CR );
					throw new HybsSystemException();
				}

				// QRコードの処理、処理後はoffsetが進むため、offsetを再セット
				if( key.startsWith( QRCODE_PREFIX ) ) {
					setOffset( makeQRImage( row, offset, key.substring( QRCODE_PREFIX.length() ), buf ) );
				}
				// 画像置き換えの処理、処理後はoffsetが進むため、offsetを再セット
				else if( key.startsWith( IMG_PREFIX  ) ) {
					setOffset( changeImage( row, offset, key.substring( IMG_PREFIX.length() ), buf ) );
				}
				// コメント(アノテーション)による置き換え処理、処理後はoffsetが進むため、offsetを再セット
				else if( key.startsWith( ANNOTATION_PREFIX ) ) {
					setOffset( parseByAnnotation( row, offset, key.substring( ANNOTATION_PREFIX.length() ), buf ) );
				}
				else {
					String val = getValue( key );
	 				// 5.5.2.4 (2012/05/16) String key は使われていないので、削除します。
					changeType( row, offset, val, getNativeType( key, val ), buf );
					buf.append( val );
				}

				// 処理行がページエンドカットの対象か
				if( queue.isFgcut() && PAGE_END_CUT.equals( key ) ) {
					isPageEndCut = true;
				}
			}
		}.doParse( row, VAR_START, VAR_END, false );

		//==== ここからは後処理 =========================================================
		/*
		 * ページエンドカットの判定は最後で処理する。
		 * {&#064;PAGEENDCUT}が行の最初に書かれている場合は、OVERFLOWになっていない可能性が
		 * あるため行の途中では判断できない
		 */
		// 5.4.2.0 (2011/12/01) シートブレイク中でもページエンドカットが適用されるようにする。
		// (通常のページブレイクは先読み判定のためページエンドカットすると、ブレイク発生行自身が
		//  削除されてしまうため現時点では未対応)
		if( isPageEndCut && ( status == OVERFLOW || ( sheetBreakClm >= 0 && isPageBreak ) ) ) {
			// ページエンドカットの場合は、非表示状態にする。
			rowStr = rowStr.replace( ROW_START_TAG, ROW_START_TAG_INVISIBLE ) ;
		}

		/*
		 * オブジェクトで定義されているテーブル名を変更
		 */
		if( rowStr.indexOf( OBJECT_SEARCH_STR ) >= 0 ) {
			rowStr = rowStr.replace( OBJECT_SEARCH_STR + sheetNameOrig, OBJECT_SEARCH_STR + sheetNameNew );
		}

		/*
		 * 関数エラーを表示されないため、ISERROR関数を埋め込み 4.2.3.1 (2008/06/19)
		 */
		rowStr = replaceOoocError( rowStr );

		/*
		 * グラフをシート毎にコピー 5.1.8.0 (2010/07/01)
		 */
		rowStr = replaceGraphInfo( rowStr, sheetNameOrig, sheetNameNew );

		/*
		 * アノテーション(コメント)を削除 5.1.8.0 (2010/07/01)
		 * (コメントが存在すると起動が異常に遅くなる)
		 */
		if( rowStr.indexOf( ANNOTATION_START_TAG ) >= 0 ) {
			rowStr = new TagParser() {}.doParse( rowStr, ANNOTATION_START_TAG, ANNOTATION_END_TAG );
		}

		/*
		 * 条件付書式対応 5.2.2.0 (2010/11/01)
		 * テーブル内に存在するスタイル名称を書き換え
		 */
		if( isNeedsReparse ) {
			for( String name : repStyleList ) {
				// 5.6.3.1 (2013/04/05) 属性終了追加
				if( rowStr.indexOf( TABLE_STYLE_NAME_START_TAG + name + TABLE_STYLE_NAME_END_TAG ) >= 0 ) {
					rowStr = rowStr.replace( TABLE_STYLE_NAME_START_TAG + name + TABLE_STYLE_NAME_END_TAG, TABLE_STYLE_NAME_START_TAG + name + "_" + sheetNameNew + TABLE_STYLE_NAME_END_TAG );
				}
			}

		}
		//==============================================================================

		bw.write( rowStr );
	}

	/**
	 * 帳票データに応じて、カラムの属性を変更(文字型⇒数値型)に変更します。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) 新規作成
	 * @og.rev 5.5.2.4 (2012/05/16) String key は使われていないので、削除します。
	 *
	 * @param row
	 * @param curOffset
	 * @param val
	 * @param type
	 * @param sb
	 */
	private void changeType( final String row, final int curOffset
							, final String val, final NativeType type, final StringBuilder sb ) {
		if( val == null || val.length() == 0 ) {
			return;
		}
		// 書き換え対象は数値型のみ
		if( type != NativeType.INT && type != NativeType.LONG && type != NativeType.DOUBLE ) {
			return;
		}
		// 処理対象がセルでない(オブジェクト)は書き換えしない
		if( !isCell( row, curOffset ) ) {
			return;
		}

		// セルの文字が{@xxxx_n}のみであった場合だけ、数値定義の判定を行う。
		// (関数内に{@xxxx_n}等があった場合は、判定しない(<text:p>{@xxxx_n}</text:p>の場合のみ))
		if( sb.lastIndexOf( TEXT_START_TAG ) + TEXT_START_TAG.length() == sb.length()
			&& row.indexOf( TEXT_END_TAG, curOffset ) == curOffset ) {
			int typeIdx = sb.lastIndexOf( TABLE_CELL_STRING_TYPE );
			int cellIdx = sb.lastIndexOf( TABLE_CELL_START_TAG );
			if( typeIdx >= 0 && cellIdx >= 0 && typeIdx > cellIdx ) {
				// office:value-type="string" を office:value-type="float" office:value="xxx" に変換
				sb.replace( typeIdx, typeIdx + TABLE_CELL_STRING_TYPE.length()
					,TABLE_CELL_FLOAT_TYPE + " " + TABLE_CELL_FLOAT_VAL_START + val + TABLE_CELL_FLOAT_VAL_END );
			}
		}
	}

	/**
	 * 引数に指定された文字列のNativeタイプを返します。
	 *
	 * リソース使用時は、各DBTypeで定義されたNativeタイプを、
	 * 未使用時は、値からNativeタイプを取得して返します。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) NativeType#getType(String) のメソッドを使用するように変更。
	 *
	 * @param key
	 * @param val
	 *
	 * @return  NATIVEの型の識別コード
	 * @see org.opengion.fukurou.model.NativeType
	 */
	private NativeType getNativeType( final String key, final String val ) {
		if( val == null || val.length() == 0 ) {
			return NativeType.STRING;
		}

		NativeType type = null;
		if( queue.isFglocal() ) {
			String name = key;
			int conOffset = key.lastIndexOf( VAR_CON );
			if( conOffset >= 0 ) {
				int rownum = -1;
				try {
					rownum = Integer.valueOf( name.substring( conOffset + VAR_CON.length(), name.length() ) );
				}
				// '_'以降の文字が数字でない場合は、'_'以降の文字もカラム名の一部として扱う
				catch ( NumberFormatException ex ) {}
				if( rownum >= 0 ) {
					name = name.substring( 0, conOffset );
				}
			}
			int col = queue.getBody().getColumnNo( name, false );
			if( col >= 0 ) {
				type = queue.getBody().getDBColumn( col ).getNativeType();
			}
		}

		if( type == null ) {
			// ,は削除した状態で判定
			String repVal = val.replace( ",", "" );
			type = NativeType.getType( repVal );			// 5.1.8.0 (2010/07/01) NativeType#getType(String) のメソッドを使用
			// 整数型で、0nnnとなっている場合は、文字列をして扱う
			if( type == NativeType.INT && repVal.length() >= 2 && repVal.charAt(0) == '0' ) {
				type = NativeType.STRING;
			}
		}

		return type;
	}

	/**
	 * コメント(アノテーションによる置き換え処理を行います)
	 * この処理では、offsetを進めるため、戻り値として処理後のoffsetを返します。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) 新規作成
	 *
	 * @param row
	 * @param curOffset
	 * @param key
	 * @param sb
	 *
	 * @return 処理後のオフセット
	 */
	private int parseByAnnotation( final String row, final int curOffset, final String key, final StringBuilder sb ) {
		int offset = curOffset;
		boolean isCell = isCell( row, offset );

		// セルの場合のみ置き換えの判定を行う(オブジェクトの場合は判定しない)
		if( isCell ) {
			int cellStrIdx = sb.lastIndexOf( TABLE_CELL_START_TAG, offset );
			// office:value-type="float" office:value="xxx" を office:value-type="string" に変換
			// 数値型の場合は、後で再度変換を行う。
			// (文字型に変換しておかないと、値がnullの場合でも"0"が表示されてしまうため)
			int floatIdx = sb.indexOf( TABLE_CELL_FLOAT_TYPE, cellStrIdx );
			if( floatIdx >= 0 ) {
				sb.replace( floatIdx, floatIdx + TABLE_CELL_FLOAT_TYPE.length(), TABLE_CELL_STRING_TYPE );

				int floatStrIdx = sb.indexOf( TABLE_CELL_FLOAT_VAL_START, floatIdx );
				if( floatStrIdx >= 0 ) {
					int floatEndIdx = sb.indexOf( TABLE_CELL_FLOAT_VAL_END, floatStrIdx + TABLE_CELL_FLOAT_VAL_START.length() );
					if( floatEndIdx >= 0 ) {
						sb.replace( floatStrIdx, floatEndIdx + TABLE_CELL_FLOAT_VAL_END.length(), "" );
					}
				}
			}
		}

		// アノテーションの値から、セルの文字列部分を置き換え
		int endIdx = isCell ? row.indexOf( TABLE_CELL_END_TAG, offset ) : row.indexOf( DRAW_END_KEY, offset );
		if( endIdx >= 0 ) {
			int textStrIdx = row.indexOf( TEXT_START_ANO_TAG, offset );
			// セルのコメントの場合、<text:pで検索すると、オブジェクトのテキストが検索されている可能性がある。
			// このため、セルの<text:pが見つかるまで検索を繰り返す
			if( isCell ) {
				while( !isCell( row, textStrIdx ) && textStrIdx >= 0 ) {
					textStrIdx = row.indexOf( TEXT_START_ANO_TAG, textStrIdx + 1 );
				}
			}
			if( textStrIdx >= 0 && textStrIdx < endIdx ) {
				// セルのコメントの場合、</text:p>で検索すると、オブジェクトのテキストが検索されている可能性がある。
				// このため、セルの</text:p>が見つかるまで検索を繰り返す
				int textEndIdx = row.lastIndexOf( TEXT_END_TAG, endIdx );
				if( isCell ) {
					while( !isCell( row, textEndIdx ) && textEndIdx >= 0  ) {
						textEndIdx = row.lastIndexOf( TEXT_END_TAG, textEndIdx - 1 );
					}
				}
				if( textEndIdx >= 0 && textStrIdx < textEndIdx && textEndIdx < endIdx ) {
					// <text:p xxxx> の xxxx> の部分(style定義など)を書き込み
					int textStyleEnd = row.indexOf( TEXT_START_END_ANO_TAG, textStrIdx + TEXT_START_ANO_TAG.length() ) + TEXT_START_END_ANO_TAG.length();
					sb.append( row.substring( offset, textStyleEnd ) );

					// <text:pの中身(spanタグなどを取り除いた状態の文字列
					String textVal = TagParser.checkKey( row.substring( textStyleEnd, textEndIdx ), sb );
					// 取得したテキスト内にタグ文字が含まれている場合は、処理しない
					if( textVal.indexOf( '<' ) < 0 && textVal.indexOf( '>' ) < 0 ) {
						// <text:p xxxx>を書き出し
						String val = getValue( key );
		 				// 5.5.2.4 (2012/05/16) String key は使われていないので、削除します。
						changeType( row, textEndIdx, val, getNativeType( key, textVal ), sb );
						sb.append( val );
					}
					offset = textEndIdx;
				}
			}
		}

		return offset;
	}

	/**
	 * 現在のオフセットがセルかどうかを返します。
	 *
	 * trueの場合はセルを、falseの場合はオブジェクトを意味します。
	 *
	 * セルとして判定されるための条件は以下の通りです。
	 *  現在のoffsetを基準として、
	 *  ①前に<draw:(オブジェクトの開始)が見つからない
	 *  ②前に<table:table-cell(セルの始まり)が<draw:(オブジェクトの始まり)より後方にある
	 *  ③後に</draw:(オブジェクトの終わり)が見つからない
	 *  ④後に</draw:(オブジェクトの終わり)が</table:table-cell>(セルの終わり)より後方にある
	 *
	 * @param row
	 * @param offset
	 *
	 * @return 現在のオフセットがセルかどうか(falseの場合はオブジェクト)
	 */
	private boolean isCell( final String row, final int offset ) {
		int drawStartOffset = row.lastIndexOf( DRAW_START_KEY, offset );
		if( drawStartOffset < 0 ) {
			return true;
		}
		else {
			int cellStartOffset = row.lastIndexOf( TABLE_CELL_START_TAG, offset );
			if( drawStartOffset < cellStartOffset ) {
				return true;
			}
			else {
				int drawEndOffset = row.indexOf( DRAW_END_KEY, offset );
				if( drawEndOffset < 0 ) {
					return true;
				}
				else {
					int cellEndOffset = row.indexOf( TABLE_CELL_END_TAG, offset );
					// 5.1.8.0 (2010/07/01) Avoid unnecessary if..then..else statements when returning a boolean
					return cellEndOffset >= 0 && cellEndOffset < drawEndOffset ;
				}
			}
		}
	}

	/**
	 * QRコードを作成します。
	 * この処理では、offsetを進めるため、戻り値として処理後のoffsetを返します。
	 *
	 * @og.rev 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
	 * @og.rev 4.3.3.5 (2008/11/08) ↑の判定は存在チェックを行ってから処理する。ファイル名に処理行を付加
	 * @og.rev 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
	 *
	 * @param row
	 * @param curOffset
	 * @param key
	 * @param sb
	 *
	 * @return 処理後のオフセット
	 */
	private int makeQRImage( final String row, final int curOffset, final String key, final StringBuilder sb ) {
		int offset = curOffset;

		// {@QRCODE.XXXX}から実際に画像のパスが書かれている部分までを書き込む
		offset = row.indexOf( DRAW_IMG_START_TAG, offset ) + DRAW_IMG_START_TAG.length();
		sb.append( row.substring( curOffset, offset ) );
		// 画像のパスの終了インデックスを求める
		offset = row.indexOf( DRAW_IMG_HREF_END, offset ) + DRAW_IMG_HREF_END.length();

		// QRCODEの画像ファイル名を求め書き込む
		// 4.3.3.5 (2008/11/08) ファイル名に処理行を付加
		String fileName = IMG_DIR + '/' + key + "_" + currentBaseRow + QRCODE_FILETYPE;
		sb.append( fileName ).append( DRAW_IMG_HREF_END );

		// QRCODEに書き込む値を求める
		String value = getValue( key );

		// QRCODEの作成
		// 4.3.3.5 (2008/11/08) ファイル名に処理行を付加
		String fileNameAbs =
			new File( path ).getAbsolutePath() + File.separator + IMG_DIR + File.separator + key + "_" + currentBaseRow + QRCODE_FILETYPE;

		// 画像リンクが無効となっている場合は、Picturesのフォルダが作成されていない可能性がある
		// 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
		// 4.3.3.5 (2008/11/08) 存在チェック追加
		if( !new File( fileNameAbs ).getParentFile().exists() ) {
			if( new File( fileNameAbs ).getParentFile().mkdirs() ) {
				System.err.println( fileNameAbs + " の ディレクトリ作成に失敗しました。" );
			}
		}

		QrcodeImage qrImage = new QrcodeImage();
		qrImage.init( value, fileNameAbs );
		qrImage.saveImage();

		// 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
		addObjs.put( fileName, QRCODE_FILETYPE.substring( 1 ) );

		// 読み込みOffsetを返します
		return offset;
	}

	/**
	 * DBTableModelに設定されたパスから画像データを取得し、内部に取り込みます
	 * この処理では、offsetを進めるため、戻り値として処理後のoffsetを返します。
	 *
	 * @og.rev 4.3.3.5 (2008/11/08) 新規追加
	 * @og.rev 4.3.3.6 (2008/11/15) 画像パスが存在しない場合は、リンクタグ(draw:image)自体を削除
	 * @og.rev 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
	 *
	 * @param row
	 * @param curOffset
	 * @param key
	 * @param sb
	 *
	 * @return 処理後のオフセット
	 */
	private int changeImage( final String row, final int curOffset, final String key, final StringBuilder sb ) {
		int offset = curOffset;
		File imgFile = null;

		// 画像ファイルを読み込むパスを求める
		String value = getValue( key );

		if( value != null && value.length() > 0 ) {
			imgFile = new File( HybsSystem.url2dir( value ) );
		}

		// 画像ファイルのパスが入っていて、実際に画像が存在する場合
		if( imgFile != null && imgFile.exists() ) {
			// {@IMG.XXXX}から実際に画像のパスが書かれている部分までを書き込む
			offset = row.indexOf( DRAW_IMG_START_TAG, offset ) + DRAW_IMG_START_TAG.length();
			sb.append( row.substring( curOffset, offset ) );

			// 画像のパスの終了インデックスを求める
			offset = row.indexOf( DRAW_IMG_HREF_END, offset ) + DRAW_IMG_HREF_END.length();

			String fileNameOut = IMG_DIR + '/' + imgFile.getName();
			sb.append( fileNameOut ).append( DRAW_IMG_HREF_END );

			String fileNameOutAbs =
				new File( path ).getAbsolutePath() + File.separator + IMG_DIR + File.separator + imgFile.getName();
			if( !new File( fileNameOutAbs ).getParentFile().exists() ) {
				if( new File( fileNameOutAbs ).getParentFile().mkdirs() ) {
					System.err.println( fileNameOutAbs + " の ディレクトリ作成に失敗しました。" );
				}
			}
			FileUtil.copy( imgFile, new File( fileNameOutAbs ) );

			// 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
			addObjs.put( fileNameOut, getSuffix( imgFile.getName() ) );
		}
		// 画像パスが設定されていない、又は画像が存在しない場合
		else {
			// {@IMG.XXXX}から見て、<draw:image> ... </draw:image>までをスキップする
			offset = row.indexOf( DRAW_IMG_START_TAG, offset );
			sb.append( row.substring( curOffset, offset ) );

			offset = row.indexOf( DRAW_IMG_END_TAG, offset ) + DRAW_IMG_END_TAG.length();
		}

		// 読み込みOffsetを返します
		return offset;
	}

	/**
	 * 変換後の行データで定義されている関数にISERROR関数を埋め込みます。
	 *
	 * これは、OOoの関数の動作として、不正な引数等が入力された場合(null値など)に、
	 * エラー:xxxと表示されてしまうため、これを防ぐために関数エラーのハンドリングを行い、
	 * エラーの場合は、空白文字を返すようにします。
	 *
	 * @og.rev 4.3.7.2 (2009/06/15) 開始文字が変更になったため対応
	 * @og.rev 5.0.2.0 (2009/11/01) 関数内の"(quot)は、メタ文字に変換する
	 * @og.rev 5.1.7.0 (2010/06/01) 関数の終わりが)出ない場合にエラーとなるバグを修正
	 * @og.rev 5.1.8.0 (2010/07/01) パース方法の内部実装変更
	 *
	 * @param row
	 *
	 * @return 変換後の行データ
	 */
	private String replaceOoocError( final String row ) {
		// 4.3.7.2 (2009/06/15) OOOC_FUNCTION_START3の条件判定追加。どちらか分からないので変数で受ける。
		final String functionStart;
		if( row.indexOf( OOOC_FUNCTION_START_3 ) >= 0 )		{ functionStart = OOOC_FUNCTION_START_3; }
		else if( row.indexOf( OOOC_FUNCTION_START ) >= 0 )	{ functionStart = OOOC_FUNCTION_START; }
		else { return row; }

		String rowStr = new TagParser() {
			@Override
			protected boolean checkIgnore( final int strOffset, final int endOffset ) {
				// 5.1.7.0 (2010/06/01) 関数の終わりが)出ない場合にエラーとなるバグを修正
				// 単なる行参照でも、of:=で始まるがこの場合は、関数でないため終わりが)でない
				// このため、)が見つからないまたは、タグの終わり(>)が先に見つかった場合は、エラー関数を
				// 埋め込まないようにする。
				int tmpOffset = row.indexOf( ">", strOffset + 1 );
				return endOffset >= 0 && endOffset < tmpOffset ;
			}

			@Override
			protected void exec( final String str, final StringBuilder buf, final int offset ) {
				String key = str.substring( functionStart.length(), str.length() - OOOC_FUNCTION_END.length() ) + ")";
				key = key.replace( "\"", "&quot;&quot;" ).replace( OOO_CR, "" );
				buf.append( functionStart + "IF(ISERROR(" + key + ");&quot;&quot;;" + key + OOOC_FUNCTION_END );
			}
		}.doParse( row, functionStart, OOOC_FUNCTION_END );

		return rowStr;
	}

	/**
	 * グラフ表示データ部分を更新します。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) 新規作成
	 * @og.rev 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
	 *
	 * @param row
	 * @param sheetOrig
	 * @param sheetNew
	 *
	 * @return 変換後の行データ
	 */
	private String replaceGraphInfo( final String row, final String sheetOrig, final String sheetNew  ) {
		if( row.indexOf( GRAPH_START_TAG ) < 0 || row.indexOf( GRAPH_UPDATE_RANGE_START ) < 0 ) { return row; }

		String rowStr = new TagParser() {
			@Override
			protected void exec( final String str, final StringBuilder buf, final int offset ) {
				// <draw:object ... /> の部分
				String graphTag = str;

				if( graphTag.indexOf( GRAPH_UPDATE_RANGE_START ) >= 0 ) {
					String nameOrig = TagParser.getValueFromTag( graphTag, GRAPH_HREF_START, GRAPH_HREF_END );
					if( new File( path + nameOrig ).exists() ) {
						String nameNew = nameOrig + "_" + pages;

						// グラフオブジェクトの定義ファイルをコピー(./Object X/* ⇒ ./Object X_n/*)
						FileUtil.copyDirectry( path + nameOrig, path + nameNew );
						graphTag = graphTag.replace( GRAPH_HREF_START + nameOrig, GRAPH_HREF_START + nameNew );

						// グラフオブジェクトの画像イメージをコピー(./ObjectReplacements/Object X ⇒ ./ObjectReplacements/Object X_n)
						// ※実体はコピーしない(リンクの参照を無効にしておくことで、次回起動時にグラフの再描画が行われる)
						graphTag = graphTag.replace( GRAPH_HREF_START + GRAPH_OBJREPL + "/" + nameOrig, GRAPH_HREF_START + GRAPH_OBJREPL + "/" + nameNew );

						// OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
						addObjs.put( nameNew, "graph" );

						// グラフオブジェクトの定義ファイルに記述されている定義ファイルをパースし、シート名と{@XXXX}を置き換え
						parseGraphContent( path + nameNew + File.separator + "content.xml", sheetOrig, sheetNew );

						// グラフの参照範囲のシート名を置き換え
						String range = TagParser.getValueFromTag( str, GRAPH_UPDATE_RANGE_START, GRAPH_UPDATE_RANGE_END );
						graphTag = graphTag.replace( GRAPH_UPDATE_RANGE_START + range, GRAPH_UPDATE_RANGE_START + range.replace( sheetOrig, sheetNew ) );
					}
				}

				buf.append( graphTag );
			}
		}.doParse( row, GRAPH_START_TAG, GRAPH_END_TAG );

		return rowStr;
	}

	/**
	 * グラフデータのcontent.xmlをパースします。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) 新規作成
	 *
	 * @param fileName
	 * @param sheetOrig
	 * @param sheetNew
	 */
	private void parseGraphContent( final String fileName, final String sheetOrig, final String sheetNew  ) {
		String graphContent = readOOoXml( fileName );

		// シート名の置き換え
		if( graphContent.indexOf( GRAPH_CONTENT_START ) >= 0 ) {
			graphContent = new TagParser() {
				@Override
				protected void exec( final String str, final StringBuilder buf, final int offset ) {
					buf.append( str.replace( sheetOrig, sheetNew ) );
				}
			}.doParse( graphContent, GRAPH_CONTENT_START, GRAPH_CONTENT_END );
		}

		// {@XXXX}の置き換え
		if( graphContent.indexOf( VAR_START ) >= 0 ) {
			graphContent = new TagParser() {
				@Override
				public void exec( final String str, final StringBuilder buf, final int offset ) {
					buf.append( getHeaderFooterValue( str ) );
				}
			}.doParse( graphContent, VAR_START, VAR_END, false );
		}

		writeOOoXml( fileName, graphContent );
	}

	/**
	 * 指定されたキーの値を返します。
	 *
	 * @og.rev 4.3.0.0 (2008/07/18) アンダースコアの処理変更
 	 * @og.rev 4.3.5.0 (2008/02/01) カラム名と行番号文字の位置は最後から検索する 4.3.3.4 (2008/11/01) 修正分
	 *
	 * @param key
	 *
	 * @return 値
	 */
	private String getValue( final String key ) {
		int conOffset = key.lastIndexOf( VAR_CON );

		String value = null;

		if( conOffset < 0 ) {
			value = getHeaderFooterValue( key );
		}
		else {
			String name = key.substring( 0, conOffset );
			int rownum = -1;
			try {
				rownum = Integer.valueOf( key.substring( conOffset + VAR_CON.length(), key.length() ) ) + currentBaseRow;
			}
			catch ( NumberFormatException ex ) {
				// 4.3.0.0 (2008/07/18) エラーが起きてもなにもしない。
				// queue.addMsg( "[ERROR]雛形の変数定義が誤っています。カラム名=" + name + HybsSystem.CR );
				// throw new Exception( ex );
			}

			// 4.3.0.0 (2008/07/18) アンダースコア後が数字に変換できない場合はヘッダフッタとして認識
			if( rownum < 0 ){
				value = getHeaderFooterValue( key );
			}
			else{
				value = getBodyValue( name, rownum );
			}
		}

		return checkValue( value );
	}

	/**
	 * 指定されたキーのヘッダー、フッター値を返します。
	 *
	 * @og.rev 4.3.6.0 (2009/04/01) レンデラー適用されていないバグを修正
	 * @og.rev 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
	 * @og.rev 5.1.6.0 (2010/05/01) ページNO出力対応
	 *
	 * @param key
	 *
	 * @return 値
	 */
	private String getHeaderFooterValue( final String key ) {
		String value = "";

		// 5.1.6.0 (2010/05/01) ページNO出力対応
		if( PAGE_NO.equals( key ) ) {
			value = String.valueOf( pages + 1 );
		}
		// 最後の行かオーバーフロー時はフッター
		else if( status >= LASTROW ) {
			if( queue.getFooter() != null ) {
				int clmno = queue.getFooter().getColumnNo( key, false );
				if( clmno >= 0 ) {
					value = queue.getFooter().getValue( 0, clmno );
					// 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
					if( queue.isFglocal() ) {
						// 4.3.6.0 (2009/04/01)
						value = queue.getFooter().getDBColumn( clmno ).getRendererValue( value );
					}
				}
			}
		}
		// 最後の行にきていない場合はヘッダー
		else {
			if( queue.getHeader() != null ) {
				int clmno = queue.getHeader().getColumnNo( key, false );
				if( clmno >= 0 ) {
					value = queue.getHeader().getValue( 0, clmno );
					// 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
					if( queue.isFglocal() ) {
						// 4.3.6.0 (2009/04/01)
						value = queue.getHeader().getDBColumn( clmno ).getRendererValue( value );
					}
				}
			}
		}

		return value;
	}

	/**
	 * 指定された行番号、キーのボディー値を返します。
	 *
	 * @og.rev 4.3.6.0 (2009/04/01) レンデラー適用されていないバグを修正
	 * @og.rev 4.3.6.2 (2009/04/15) 行番号のより小さいカラム定義を読んだ際に、内部カウンタがクリアされてしまうバグを修正
	 * @og.rev 4.3.6.2 (2009/04/15) 一度オーバーフローした場合に移行が全て空文字で返ってしまうバグを修正
	 * @og.rev 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
	 * @og.rev 5.1.6.0 (2010/05/01) 行番号出力対応
	 * @og.rev 5.1.7.0 (2010/06/01) 複数シート対応
	 * @og.rev 5.1.9.0 (2010/08/01) 最終行で正しくシートブレイクされないバグを修正
	 *
	 * @param key
	 * @param rownum
	 *
	 * @return 値
	 */
	private String getBodyValue( final String key, final int rownum ) {
		// if( status == OVERFLOW || isPageBreak ) { return ""; }
		if( isPageBreak ) { return ""; } // 4.3.6.2 (2009/04/15) OVERFLOW時バグ修正

		int clmno = queue.getBody().getColumnNo( key, false );
		if( clmno < 0 && !ROW_NO.equals( key ) ) { return ""; }

		// ページブレイク判定、先読みして判断
		if( PAGE_BREAK.equals( key ) ) {
			if( rownum < queue.getBody().getRowCount() - 1 ) {
				if( !( queue.getBody().getValue( rownum, clmno ).equals( queue.getBody().getValue( rownum + 1, clmno ) ) ) ) {
					isPageBreak = true;
				}
			}
			return "";
		}

		// 5.1.7.0 (2010/06/01) 複数シート対応
		// シートブレイクは後読みして判断(前の行と異なっていた場合にブレイク)
		if( sheetBreakClm >= 0 ) {
			// 5.1.9.0 (2010/08/01) 最終行で正しくシートブレイクされないバグを修正
			if( rownum < queue.getBody().getRowCount() && currentBaseRow != rownum ) {
				if( !( queue.getBody().getValue( currentBaseRow, sheetBreakClm ).equals( queue.getBody().getValue( rownum, sheetBreakClm ) ) ) ) {
					isPageBreak = true;
					return "";
				}
			}
		}

		if( rownum >= queue.getBody().getRowCount() ) {
			status = OVERFLOW;
			return "";
		}

		if( rownum == queue.getBody().getRowCount() - 1 ) {
			// status = LASTROW;
			status = Math.max( LASTROW, status ); // 4.3.6.2 (2009/04/15) 自身のステータスと比べて大きい方を返す
		}

		String value = null;
		// 5.1.6.0 (2010/05/01) ページNO出力対応
		if( ROW_NO.equals( key ) ) {
			value = String.valueOf( rownum + 1 );
		}
		else {
			value = queue.getBody().getValue( rownum, clmno );
			// 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
			if( queue.isFglocal() ) {
				// 4.3.6.0 (2009/04/01)
				value = queue.getBody().getDBColumn( clmno ).getRendererValue( value );
			}
		}

		// 4.3.6.2 (2009/04/15)
		if( currentMaxRow < rownum + 1 ) {
			currentMaxRow = rownum + 1;
		}

		return value;
	}

	/**
	 * 値に'<'や'>','&'が含まれていた場合にメタ文字に変換します。
	 *
	 * @og.rev 5.0.2.0 (2009/11/01) 改行Cの変換ロジックを追加
	 * @og.rev 5.0.2.0 (2009/11/01) リソース変換時のspanタグを除去
	 *
	 * @param value
	 *
	 * @return 変換後の値
	 */
	private String checkValue( final String value ) {
		String rtn = value;

		// 5.0.2.0 (2009/11/01)
		if( queue.isFglocal() ) {
			int idx = -1;
			if( ( idx = rtn.indexOf( "<span" ) ) >= 0 ) {
				String spanStart = rtn.substring( idx, rtn.indexOf( '>', idx ) + 1 );
				rtn = rtn.replace( spanStart, "" ).replace( "</span>", "" );
			}
		}

		if( rtn.indexOf( '&' ) >= 0 ) {
			rtn = rtn.replace( "&", "&amp;" );
		}
		if( rtn.indexOf( '<' ) >= 0 ) {
			rtn = rtn.replace( "<", "&lt;" );
		}
		if( rtn.indexOf( '>' ) >= 0 ) {
			rtn = rtn.replace( ">", "&gt;" );
		}
		if( rtn.indexOf( '\n' ) >= 0 ) {
			rtn = rtn.replace( "\r\n", "\n" ).replace( "\n", OOO_CR );
		}

		return rtn;
	}

	/**
	 * 引数の文字列を指定された開始タグ、終了タグで解析し配列として返します。
	 * 開始タグより前の文字列は0番目に、終了タグより後の文字列は1番目に格納されます。
	 * 2番目以降に、開始タグ、終了タグの部分が格納されます。
	 *
	 * @param str
	 * @param startTag
	 * @param endTag
	 *
	 * @return 解析結果の配列
	 */
	private static String[] tag2Array( final String str, final String startTag, final String endTag ) {
		String header = null;
		String footer = null;
		List<String> body = new ArrayList<String>();

		int preOffset = -1;
		int curOffset = 0;

		while( true ) {
			curOffset = str.indexOf( startTag, preOffset + 1 );
			if( curOffset < 0 ) {
				curOffset = str.lastIndexOf( endTag ) + endTag.length();
				body.add( str.substring( preOffset, curOffset ) );

				footer = str.substring( curOffset );
				break;
			}
			else if( preOffset == -1 ) {
				header = str.substring( 0, curOffset );
			}
			else {
				body.add( str.substring( preOffset, curOffset ) );
			}
			preOffset = curOffset;
		}

		String[] arr = new String[body.size()+2];
		arr[0] = header;
		arr[1] = footer;
		for( int i=0; i<body.size(); i++ ) {
			arr[i+2] = body.get(i);
		}

		return arr;
	}

	/**
	 * 帳票処理キューを元に、style.xml(ヘッダー、フッター)を書き換えます。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) パース方法の内部実装変更
	 */
	private void execStyles() {
		String fileName = path + "styles.xml";
		String content = readOOoXml( fileName );

		if( content.indexOf( VAR_START ) < 0 ) { return; }
		content = new TagParser() {
			@Override
			public void exec( final String str, final StringBuilder buf, final int offset ) {
				buf.append( getHeaderFooterValue( str ) );
			}
		}.doParse( readOOoXml( fileName ), VAR_START, VAR_END, false );

		writeOOoXml( fileName, content );
	}

	/**
	 * 帳票処理キューを元に、meta.xmlを書き換えます。
	 *
	 * @og.rev 5.1.6.0 (2010/05/01) 画面帳票作成機能対応(API経由では出力されないことがある)
	 */
	private void execMeta() {
		String fileName = path + "meta.xml";

		String meta = readOOoXml( fileName );

		// シート数書き換え
		// 5.1.6.0 (2010/05/01)
		if( meta.indexOf( TABLE_COUNT_START_TAG ) >=0 ){
			String tableCount = TagParser.getValueFromTag( meta, TABLE_COUNT_START_TAG, TABLE_COUNT_END_TAG );
			meta = meta.replace( TABLE_COUNT_START_TAG + tableCount, TABLE_COUNT_START_TAG + pages );
		}

		// セル数書き換え
		// 5.1.6.0 (2010/05/01)
		if( meta.indexOf( CELL_COUNT_START_TAG ) >=0 ){
			String cellCount = TagParser.getValueFromTag( meta, CELL_COUNT_START_TAG, CELL_COUNT_END_TAG );
			meta = meta.replace( CELL_COUNT_START_TAG + cellCount, CELL_COUNT_START_TAG + ( Integer.parseInt( cellCount ) * pages ) );
		}

		// オブジェクト数書き換え
		// 5.1.6.0 (2010/05/01)
		if( meta.indexOf( OBJECT_COUNT_START_TAG ) >= 0 ){
			String objectCount = TagParser.getValueFromTag( meta, OBJECT_COUNT_START_TAG, OBJECT_COUNT_END_TAG );
			//4.2.4.0 (2008/06/02) 存在しない場合はnullで帰ってくるので無視する
			if( objectCount != null){
				meta = meta.replace( OBJECT_COUNT_START_TAG + objectCount, OBJECT_COUNT_START_TAG + ( Integer.parseInt( objectCount ) * pages ) );
			}
		}

		writeOOoXml( fileName, meta );
	}

	/**
	 * 書き換え対象のスタイルリストを取得します。
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
	 *
	 * @param header content.xmlのヘッダー
	 */
	private void getRepStyleList( final String header ) {
		String[] tags = tag2Array( header, STYLE_START_TAG, STYLE_END_TAG );
		Set<String> origNameSet = pageNameMap.keySet();
		for( int i=2; i<tags.length; i++ ) {
			for( String origName : origNameSet ) {
				if( tags[i].indexOf( "=\"" + origName + "." ) >= 0 ) {
					String styleName = TagParser.getValueFromTag( tags[i], STYLE_NAME_START_TAG, STYLE_NAME_END_TAG );
					repStyleList.add( styleName );
					break;
				}
			}
		}
	}

	/**
	 * 帳票処理キューを元に、content.xmlを書き換えます。
	 * まず、XMLを一旦メモリ上に展開した後、シート単位に分解し、データの埋め込みを行います。
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
	 */
	private void execContentHeader() {
		String fileName = path + "content.xml";
		String content = readOOoXml( fileName );

		// ファイルの解析し、シート+行単位に分解
		String[] tags = tag2Array( content, STYLE_START_TAG, STYLE_END_TAG );
		String header = tags[0];
		String footer = tags[1];

		BufferedWriter bw = null;
		try {
			bw = getWriter( fileName );
			bw.write( xmlHeader );
			bw.write( '\n' );
			bw.write( header );

			// スタイル情報にシート依存の情報がある場合は、ページ分だけコピーする。
			Set<String> origNameSet = pageNameMap.keySet();
			for( int i=2; i<tags.length; i++ ) {
				boolean isReplace = false;
				for( String origName : origNameSet ) {
					if( tags[i].indexOf( "=\"" + origName + "." ) >= 0 ) {
						List<String> newNames = pageNameMap.get( origName );
						for( String newName : newNames ) {
							String styleStr = tags[i].replace( "=\"" + origName + "." , "=\"" + newName + "." );
							// シート名の書き換え
							String styleName = TagParser.getValueFromTag( styleStr, STYLE_NAME_START_TAG, STYLE_NAME_END_TAG );
							styleStr = styleStr.replace( STYLE_NAME_START_TAG + styleName, STYLE_NAME_START_TAG + styleName + "_" + newName );
							bw.write( styleStr );
							isReplace = true;
						}
						break;
					}
				}

				if( !isReplace ) {
					bw.write( tags[i] );
				}
			}

			bw.write( footer );
			bw.flush();
		}
		catch ( IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:error occurer while write ReParsed Sheet " + fileName + HybsSystem.CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( bw );
		}
	}

	/**
	 * content.xmlのヘッダー部分を出力したcontent.xmlに、ヘッダー部分以降を出力した
	 * content.xml.bakをマージします。
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
	 */
	private void execMergeContent() {
		FileChannel srcChannel = null;
		FileChannel destChannel = null;
		try {
			srcChannel = new FileInputStream( path + "content.xml.tmp" ).getChannel();
			destChannel = new FileOutputStream( path + "content.xml", true ).getChannel();
			srcChannel.transferTo(0, srcChannel.size(), destChannel);
		}
		catch ( IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:error occurer while merge content.xml" + HybsSystem.CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( srcChannel );
			Closer.ioClose( destChannel );
		}
		FileUtil.deleteFiles( new File( path + "content.xml.tmp" ) );
	}

	/**
	 * META-INF/manifest.xmlに、追加したオブジェクト(グラフ、画像)を登録します。
	 *
	 * @og.rev 5.3.1.0 (2011/12/01) 新規作成
	 */
	private void execManifest() {
		String fileName = path + "META-INF" + File.separator + "manifest.xml";
		String[] conArr = TagParser.tag2Array( readOOoXml( fileName ), MANIFEST_START_TAG, MANIFEST_END_TAG );

		StringBuilder buf = new StringBuilder();
		buf.append( conArr[0] );
		for( int i=2; i<conArr.length; i++ ) {
			buf.append( conArr[i] );
		}
		for( Map.Entry<String,String> entry : addObjs.entrySet() ) {
			if( "graph".equals( entry.getValue() ) ) {
				buf.append( "<manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"" + entry.getKey() + "/content.xml\"/>" );
				buf.append( "<manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"" + entry.getKey() + "/styles.xml\"/>" );
				buf.append( "<manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"" + entry.getKey() + "/meta.xml\"/>" );
				buf.append( "<manifest:file-entry manifest:media-type=\"application/vnd.oasis.opendocument.chart\" manifest:full-path=\"" + entry.getKey() + "/\"/>" );
			}
			else {
				buf.append( "<manifest:file-entry manifest:media-type=\"image/" + entry.getValue() + "\" manifest:full-path=\"" + entry.getKey() + "\"/>" );
			}
		}
		buf.append( conArr[1] );

		writeOOoXml( fileName, buf.toString() );
	}

	/**
	 * XMLファイルを読み取り、結果を返します。
	 * OOoのXMLファイルは全て1行めがxml宣言で、2行目が内容全体という形式であるため、
	 * ここでは、2行目の内容部分を返します。
	 *
	 * @og.rev 4.3.6.0 (2009/04/01) meta.xmlでコンテンツの部分が改行されている場合があるため、ループを回して読込み
	 *
	 * @param fileName
	 *
	 * @return 読み取った文字列
	 */
	private String readOOoXml( final String fileName ) {
		File file = new File ( fileName );

		BufferedReader br = null;
		String tmp = null;
		StringBuilder buf = new StringBuilder();
		try {
			br = new BufferedReader( new InputStreamReader( new FileInputStream( file ), "UTF-8" ) );
			xmlHeader = br.readLine();
			while( ( tmp = br.readLine() ) != null ) { // 4.3.6.0 (2009/04/01)
				buf.append( tmp );
			}
		}
		catch( IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:Failed to read " + fileName + HybsSystem.CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( br );
		}

		String str = buf.toString();
		if( xmlHeader == null || xmlHeader.length() == 0 || str == null || str.length() == 0 ) {
			queue.addMsg( "[ERROR]PARSE:Maybe " + fileName + " is Broken!" + HybsSystem.CR );
			throw new HybsSystemException();
		}

		return str;
	}

	/**
	 * XMLファイルを書き込みます。
	 * OOoのXMLファイルは全て1行めがxml宣言で、2行目が内容全体という形式であるため、
	 * ここでは、2行目の内容部分を渡すことで、XMLファイルを作成します。
	 *
	 * @param fileName 書き込むXMLファイル名
	 * @param str 書き込む文字列
	 */
	private void writeOOoXml( final String fileName, final String str ) {
		BufferedWriter bw = null;
		try {
			bw = getWriter( fileName );
			bw.write( xmlHeader );
			bw.write( '\n' );
			bw.write( str );
			bw.flush();
		}
		catch( IOException ex  ) {
			queue.addMsg( "[ERROR]PARSE:Failed to write " + fileName + HybsSystem.CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( bw );
		}
	}

	/**
	 * XMLファイル書き込み用のライターを返します。
	 *
	 * @param fileName ファイル名
	 *
	 * @return ライター
	 */
	private BufferedWriter getWriter( final String fileName ) {
		return getWriter( fileName, false );
	}

	/**
	 * XMLファイル書き込み用のライターを返します。
	 *
	 * @param fileName ファイル名
	 * @param append アベンドするか
	 *
	 * @return ライター
	 */
	private BufferedWriter getWriter( final String fileName, final boolean append ) {
		File file = new File ( fileName );
		BufferedWriter bw;
		try {
			bw = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( file, append ), "UTF-8" ) );
		}
		catch ( UnsupportedEncodingException ex ) {
			queue.addMsg( "[ERROR] Input File is written by Unsupported Encoding" );
			throw new HybsSystemException( ex );
		}
		catch ( FileNotFoundException ex ) {
			queue.addMsg( "[ERROR] File not Found" );
			throw new HybsSystemException( ex );
		}
		return bw;
	}

	/**
	 * ファイル名から拡張子(小文字)を求めます。
	 *
	 * @param fileName 拡張子を取得する為のファイル名
	 *
	 * @return 拡張子(小文字)
	 */
	public static String getSuffix( final String fileName ) {
		String suffix = null;
		if( fileName != null ) {
			int sufIdx = fileName.lastIndexOf( '.' );
			if( sufIdx >= 0 ) {
				suffix = fileName.substring( sufIdx + 1 ).toLowerCase( Locale.JAPAN );
			}
		}
		return suffix;
	}
}

