/*
 * 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.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.nio.channels.FileChannel;
import java.nio.charset.CharacterCodingException;					// 6.3.1.0 (2015/06/28)
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.concurrent.ConcurrentMap;							// 6.4.3.3 (2016/03/04)
import java.util.concurrent.ConcurrentHashMap;						// 6.4.3.1 (2016/02/12) refactoring
import java.util.Locale;
import java.util.Set;

import org.opengion.fukurou.system.OgCharacterException ;			// 6.5.0.1 (2016/10/21)
import org.opengion.fukurou.model.NativeType;
import org.opengion.fukurou.util.StringUtil;						// 6.2.0.0 (2015/02/27)
import org.opengion.fukurou.system.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;
import org.opengion.hayabusa.db.DBTableModel;						// 6.1.1.0 (2015/01/17)
import static org.opengion.fukurou.system.HybsConst.CR ;			// 6.1.0.0 (2014/12/26)
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

/**
 * 指定されたパスに存在する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";

	/* 7.0.5.1 (2019/09/27) QRコードのﾊﾟﾗﾒｰﾀをｼｽﾃﾑﾘｿｰｽで設定できるようにします(ただし、staticとします) */
	private static final int	QR_VERSION		= HybsSystem.sysInt( "REPORT_QR_VERSION" );			// 7.0.5.1 (2019/09/27) バージョン
	private static final char	QR_ENCMODE_CH	= HybsSystem.sys( "REPORT_QR_ENCMODE" ).charAt(0);	// 7.0.5.1 (2019/09/27) エンコードモード
	private static final char	QR_ERRCRCT_CH	= HybsSystem.sys( "REPORT_QR_ERRCRCT" ).charAt(0);	// 7.0.5.1 (2019/09/27) エラー訂正レベル
	private static final String QR_IMAGE_TYPE	= "PNG";											// 7.0.5.1 (2019/09/27) 出力イメージのタイプ(PNG/JPEG)
	private static final int	QR_PIXEL		= HybsSystem.sysInt( "REPORT_QR_PIXEL" );			// 7.0.5.1 (2019/09/27) １セル辺りの塗りつぶしピクセル数
	private static final QrcodeImage.EncMode QR_ENCMODE = QrcodeImage.EncMode.get( QR_ENCMODE_CH );	// 7.0.5.1 (2019/09/27) 
	private static final QrcodeImage.ErrCrct QR_ERRCRCT = QrcodeImage.ErrCrct.get( QR_ERRCRCT_CH );	// 7.0.5.1 (2019/09/27) 

	/* 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 = "/>";					// XML なので、このまま。

	/* 数値タイプ置き換え用の文字列 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	;
	private int currentMaxRow	;

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

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

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

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

	/* シートブレイク対象かどうか 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	;

	/* ページ名のマッピング(元のシート名に対する新しいシート名) 5.2.2.0 (2010/11/01) */
	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
	private final ConcurrentMap<String,List<String>> pageNameMap = new ConcurrentHashMap<>();

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

	/* manifest.xmlに追加が必要なオブジェクトのマップ 5.3.1.0 (2011/01/01) */
	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
	private final ConcurrentMap<String,String> addObjMap = new ConcurrentHashMap<>();

	private final ExecQueue queue;
	private final String path;

	private final boolean useChangeType ;		// 6.8.3.1 (2017/12/01)

	/**
	 * コンストラクタ
	 *
	 * @og.rev 5.1.2.0 (2010/01/01) 処理した行数をQueueオブジェクトから取得(シート数が256を超えた場合の対応)
	 * @og.rev 6.8.3.1 (2017/12/01) ローカルリソースの文字型⇒数値型変換の処理の有無
	 *
	 * @param qu ExecQueueオブジェクト
	 * @param pt パス
	 */
	OdsContentParser( final ExecQueue qu, final String pt ) {
		queue = qu;
		path = pt;

		currentBaseRow = queue.getExecRowCnt();
		useChangeType = !queue.isFglocal() || HybsSystem.sysBool( "REPORT_USE_CHANGETYPE" );		// 6.8.3.1 (2017/12/01)
	}

	/**
	 * パース処理を実行します。
	 *
	 * @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( !addObjMap.isEmpty() ) {			// 6.1.1.0 (2015/01/17) refactoring
			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雛形への適用
	 * @og.rev 6.1.1.0 (2015/01/17) 内部ロジックの見直し。queue.getBody() を、ローカル変数で定義他。
	 * @og.rev 6.4.3.3 (2016/03/04) 気になったので、sheet.getOrigSheetName() を、変数に入れておきます。
	 */
	private void execContent() {
		final String fileName = path + "content.xml";
		final String content = readOOoXml( fileName );
		// ファイルの解析し、シート+行単位に分解
		final String[] tags = tag2Array( content, BODY_START_TAG, BODY_END_TAG );

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

		final String contentFooter = tags[1];
		final List<OdsSheet> firstSheets   = new ArrayList<>();
		final Map<String, OdsSheet> sheetMap = new HashMap<>();

		final DBTableModel bodyModel = queue.getBody();				// 6.1.1.0 (2015/01/17)
		final int rowCount = bodyModel.getRowCount();				// 6.1.1.0 (2015/01/17)

		OdsSheet defaultSheet = null;
		for( int i=2; i<tags.length; i++ ) {
			final OdsSheet sheet = new OdsSheet();
			// sheet.analyze( tags[i] );
			sheet.analyze( tags[i],rowCount );						// 6.1.1.0 (2015/01/17) ループから出す。
			// 5.1.7.0 (2010/06/01) 複数シート対応
			final String sheetName = sheet.getSheetName();
			if( sheetName.startsWith( FIRST_PAGE_NAME ) ) {
				firstSheets.add( sheet );
			}
			else {
				sheetMap.put( sheetName, sheet );
				// 一番初めに見つかった表紙以外のシートをデフォルトシートとして設定
				if( defaultSheet == null ) {
					defaultSheet = sheet;
				}
			}

			// 6.4.3.3 (2016/03/04) 気になったので、sheet.getOrigSheetName() を、変数に入れておきます。
			final String orgShtName = sheet.getOrigSheetName();

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

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

		// 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 = bodyModel.getColumnNo( PAGE_BREAK, false );			// 6.1.1.0 (2015/01/17)
			}

			// 表紙ページの出力
			if( queue.getExecPagesCnt() == 0 ) {
				for( final OdsSheet firstSheet : firstSheets ) {
					if( currentBaseRow >= rowCount ) {								// 6.1.1.0 (2015/01/17) ループから出す。
						break;
					}
					writeParsedSheet( firstSheet, bw );
				}
			}

			// 5.1.7.0 (2010/06/01) 複数シート対応
			sheetBreakClm = bodyModel.getColumnNo( SHEET_BREAK, false );			// 6.1.1.0 (2015/01/17)

			// 5.7.6.3 (2014/05/23) 表紙ページも、PAGEBREAKカラムの値を、シート名として使えるようにする。

			// 繰り返しページの出力
			while( currentBaseRow < rowCount ) {									// 6.1.1.0 (2015/01/17) ループから出す。
				// 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 ) {
					final String sheetName = bodyModel.getValue( currentBaseRow, sheetBreakClm );	// 6.1.1.0 (2015/01/17)
					if( sheetName != null && sheetName.length() > 0 ) {
						sheet = sheetMap.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( final IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:error occurer while write Parsed Sheet " + fileName + 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**** の場合、**** 部分をシート名に使う。
	 * @og.rev 6.4.3.3 (2016/03/04) 気になったので、sheet.getOrigSheetName() を、変数に入れておきます。
	 *
	 * @param sheet	シート
	 * @param bw	BufferedWriterオブジェクト
	 * @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 ) {
			final 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( StringUtil.startsChar( sheetName , '_' ) ) {		// 6.2.0.0 (2015/02/27) １文字 String.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 ;
			}
			else {
				outputSheetName = sheet.getConfSheetName() + ( queue.getExecPagesCnt() + pages + 1 ) ;
			}
		}
		// ページブレイク変数を初期化
		isPageBreak = false;

		// 6.4.3.3 (2016/03/04) 気になったので、sheet.getOrigSheetName() を、変数に入れておきます。
		final String orgShtName = sheet.getOrigSheetName();

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

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

		bw.write( headerStr );

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

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

		pages++;

		// 5.2.2.0 (2010/11/01) 条件付書式対応
		pageNameMap.get( orgShtName ).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) 条件付書式の属性終了文字対応
	 * @og.rev 6.8.3.1 (2017/12/01) ローカルリソースの文字型⇒数値型変換の処理の有無
	 *
	 * @param row				行データ
	 * @param bw				BufferedWriterオブジェクト
	 * @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() {
			/**
			 * 開始タグから終了タグまでの文字列の処理を定義します。
			 *
			 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
			 * @param buf 出力を行う文字列バッファ
			 * @param offset 終了タグのオフセット(ここでは使っていません)
			 */
			@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 + 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 は使われていないので、削除します。
					if( useChangeType ) {		// 6.8.3.1 (2017/12/01)
						// 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 ) ) ) {
		if( isPageEndCut && ( status == OVERFLOW || sheetBreakClm >= 0 && isPageBreak ) ) {				// 6.9.7.0 (2018/05/14) PMD Useless parentheses.
			// ページエンドカットの場合は、非表示状態にする。
			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( final 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			StringBuilderオブジェクト
	 */
	private void changeType( final String row, final int curOffset
							, final String val, final NativeType type, final StringBuilder sb ) {
		if( val == null || val.isEmpty() ) {
			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 ) {
			final int typeIdx = sb.lastIndexOf( TABLE_CELL_STRING_TYPE );
			final 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) のメソッドを使用するように変更。
	 * @og.rev 5.10.5.1 (2018/11/09) intだけでなくlongの0始まりも文字列として扱う
	 *
	 * @param key	キー
	 * @param val	文字列
	 *
	 * @return  NATIVEの型の識別コード
	 * @og.rtnNotNull
	 * @see org.opengion.fukurou.model.NativeType
	 */
	private NativeType getNativeType( final String key, final String val ) {
		if( val == null || val.isEmpty() ) {
			return NativeType.STRING;
		}

		NativeType type = null;
		if( queue.isFglocal() ) {
			String name = key;
			final int conOffset = key.lastIndexOf( VAR_CON );
			if( conOffset >= 0 ) {
				int rownum = -1;
				try {
					rownum = Integer.parseInt( name.substring( conOffset + VAR_CON.length(), name.length() ) );		// 6.0.2.4 (2014/10/17) メソッド間違い
				}
				// '_'以降の文字が数字でない場合は、'_'以降の文字もカラム名の一部として扱う
				catch( final NumberFormatException ex ) {
					// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid empty catch blocks
					final String errMsg = "'_'以降の文字をカラム名の一部として扱います。カラム名=[" + key + "]" + CR + ex.getMessage() ;
					System.err.println( errMsg );
				}
				if( rownum >= 0 ) {
					name = name.substring( 0, conOffset );
				}
			}
			final int col = queue.getBody().getColumnNo( name, false );
			if( col >= 0 ) {
				type = queue.getBody().getDBColumn( col ).getNativeType();
			}
		}

		if( type == null ) {
			// ,は削除した状態で判定
			final 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' ) {
			if( ( type == NativeType.INT || type == NativeType.LONG ) && repVal.length() >= 2 && repVal.charAt(0) == '0' ) { // 5.10.5.1 (2018/11/09) LONGを含む
				type = NativeType.STRING;
			}
		}

		return type;
	}

	/**
	 * コメント(アノテーションによる置き換え処理を行います)
	 * この処理では、offsetを進めるため、戻り値として処理後のoffsetを返します。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) 新規作成
	 * @og.rev 6.8.3.1 (2017/12/01) ローカルリソースの文字型⇒数値型変換の処理の有無
	 *
	 * @param row			行データ
	 * @param curOffset		オフセット
	 * @param key			キー
	 * @param sb			StringBuilderオブジェクト
	 *
	 * @return 処理後のオフセット
	 */
	private int parseByAnnotation( final String row, final int curOffset, final String key, final StringBuilder sb ) {
		int offset = curOffset;
		final boolean isCell = isCell( row, offset );

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

				final int floatStrIdx = sb.indexOf( TABLE_CELL_FLOAT_VAL_START, floatIdx );
				if( floatStrIdx >= 0 ) {
					final 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(), "" );
					}
				}
			}
		}

		// アノテーションの値から、セルの文字列部分を置き換え
		final 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定義など)を書き込み
					final 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タグなどを取り除いた状態の文字列
					final String textVal = TagParser.checkKey( row.substring( textStyleEnd, textEndIdx ), sb );
					// 取得したテキスト内にタグ文字が含まれている場合は、処理しない
					if( textVal.indexOf( '<' ) < 0 && textVal.indexOf( '>' ) < 0 ) {
						// <text:p xxxx>を書き出し
						final String val = getValue( key );
						if( useChangeType ) {		// 6.8.3.1 (2017/12/01)
			 				// 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 ) {
		final int drawStartOffset = row.lastIndexOf( DRAW_START_KEY, offset );
		if( drawStartOffset < 0 ) {
			return true;
		}
		else {
			final int cellStartOffset = row.lastIndexOf( TABLE_CELL_START_TAG, offset );
			if( drawStartOffset < cellStartOffset ) {
				return true;
			}
			else {
				final int drawEndOffset = row.indexOf( DRAW_END_KEY, offset );
				if( drawEndOffset < 0 ) {
					return true;
				}
				else {
					final 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に登録する
	 * @og.rev 7.0.5.1 (2019/09/27) QRコードのﾊﾟﾗﾒｰﾀをｼｽﾃﾑﾘｿｰｽで設定できるようにします(ただし、staticとします)
	 *
	 * @param row			行データ
	 * @param curOffset		オフセット
	 * @param key			キー
	 * @param sb			StringBuilderオブジェクト
	 *
	 * @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) ファイル名に処理行を付加
		final String fileName = IMG_DIR + '/' + key + "_" + currentBaseRow + QRCODE_FILETYPE;
		sb.append( fileName ).append( DRAW_IMG_HREF_END );

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

		// QRCODEの作成
		// 4.3.3.5 (2008/11/08) ファイル名に処理行を付加
		final 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) 存在チェック追加
		// 6.9.7.0 (2018/05/14) PMD These nested if statements could be combined
//		if( !new File( fileNameAbs ).getParentFile().exists() ) {
//			if( new File( fileNameAbs ).getParentFile().mkdirs() ) {
		if( !new File( fileNameAbs ).getParentFile().exists()
			&& new File( fileNameAbs ).getParentFile().mkdirs() ) {
				System.err.println( fileNameAbs + " の ディレクトリ作成に失敗しました。" );
//			}
		}

		final QrcodeImage qrImage = new QrcodeImage();
//		qrImage.init( value, fileNameAbs );
		qrImage.init( value, fileNameAbs, QR_VERSION, QR_ENCMODE, QR_ERRCRCT, QR_IMAGE_TYPE, QR_PIXEL );	// 7.0.5.1 (2019/09/27)
		qrImage.saveImage();

		// 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
		addObjMap.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			StringBuilderオブジェクト
	 *
	 * @return 処理後のオフセット
	 */
	private int changeImage( final String row, final int curOffset, final String key, final StringBuilder sb ) {
		int offset = curOffset;
		File imgFile = null;

		// 画像ファイルを読み込むパスを求める
		final 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();

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

			final String fileNameOutAbs =
				new File( path ).getAbsolutePath() + File.separator + IMG_DIR + File.separator + imgFile.getName();
			// 6.9.7.0 (2018/05/14) PMD These nested if statements could be combined
//			if( !new File( fileNameOutAbs ).getParentFile().exists() ) {
//				if( new File( fileNameOutAbs ).getParentFile().mkdirs() ) {
			if( !new File( fileNameOutAbs ).getParentFile().exists()
				&& 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に登録する
			addObjMap.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; }

		final String rowStr = new TagParser() {
			/**
			 * 開始タグから終了タグまでの文字列の処理を実行するかどうかを定義します。
			 *
			 * @param strOffset 開始タグのオフセット
			 * @param endOffset 終了タグのオフセット
			 *
			 * @return 処理を行うかどうか(true:処理を行う false:処理を行わない)
			 */
			@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 ;
			}

			/**
			 * 開始タグから終了タグまでの文字列の処理を定義します。
			 *
			 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
			 * @param buf 出力を行う文字列バッファ
			 * @param offset 終了タグのオフセット(ここでは使っていません)
			 */
			@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, "" );
				// 6.4.2.1 (2016/02/05) PMD refactoring.
				buf.append( functionStart ).append( "IF(ISERROR(" ).append( key ).append( ");&quot;&quot;;" ).append( key ).append( 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; }

		final String rowStr = new TagParser() {
			/**
			 * 開始タグから終了タグまでの文字列の処理を定義します。
			 *
			 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
			 * @param buf 出力を行う文字列バッファ
			 * @param offset 終了タグのオフセット(ここでは使っていません)
			 */
			@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に登録する
						addObjMap.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() {
				/**
				 * 開始タグから終了タグまでの文字列の処理を定義します。
				 *
				 * この実装では、何も処理を行いません。(切り出した文字列はアペンドされません)
				 * サブクラスでオーバーライドして実際の処理を実装して下さい。
				 *
				 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
				 * @param buf 出力を行う文字列バッファ
				 * @param offset 終了タグのオフセット(ここでは使っていません)
				 */
				@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() {
				/**
				 * 開始タグから終了タグまでの文字列の処理を定義します。
				 *
				 * この実装では、何も処理を行いません。(切り出した文字列はアペンドされません)
				 * サブクラスでオーバーライドして実際の処理を実装して下さい。
				 *
				 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
				 * @param buf 出力を行う文字列バッファ
				 * @param offset 終了タグのオフセット(ここでは使っていません)
				 */
				@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 ) {
		final int conOffset = key.lastIndexOf( VAR_CON );

		String value = null;

		if( conOffset < 0 ) {
			value = getHeaderFooterValue( key );
		}
		else {
			final String name = key.substring( 0, conOffset );
			int rownum = -1;
			try {
				rownum = Integer.parseInt( key.substring( conOffset + VAR_CON.length(), key.length() ) ) + currentBaseRow;	// 6.0.2.4 (2014/10/17) メソッド間違い
			}
			catch( final NumberFormatException ex ) {
				// 4.3.0.0 (2008/07/18) エラーが起きてもなにもしない。
				// queue.addMsg( "[ERROR]雛形の変数定義が誤っています。カラム名=" + name + CR );
				// throw new Exception( ex );
				// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid empty catch blocks
				final String errMsg = "雛形の変数定義で、行番号文字が取得できません。カラム名=[" + key + "]" + CR + ex.getMessage() ;
				System.err.println( errMsg );
			}

			// 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出力対応
	 * @og.rev 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
	 * @og.rev 6.1.1.0 (2015/01/17) 内部ロジックの見直し。queue.getFooter(),queue.getHeader() を、ローカル変数で定義。
	 *
	 * @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 );
		}
		// 6.1.1.0 (2015/01/17) 内部ロジックの見直し。queue.getFooter(),queue.getHeader() を、ローカル変数で定義。
		else {
			// 最後の行かオーバーフロー時はフッター。最後の行にきていない場合はヘッダー
			final DBTableModel headerFooterModel = status >= LASTROW ? queue.getFooter() : queue.getHeader() ;

			if( headerFooterModel != null ) {
				final int clmno = headerFooterModel.getColumnNo( key, false );
				if( clmno >= 0 ) {
					value = headerFooterModel.getValue( 0, clmno );
					// 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
					if( queue.isFglocal() ) {
						// 4.3.6.0 (2009/04/01)
						// 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
						value = headerFooterModel.getDBColumn( clmno ).getWriteValue( 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) 最終行で正しくシートブレイクされないバグを修正
	 * @og.rev 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
	 * @og.rev 6.1.1.0 (2015/01/17) 内部ロジックの見直し。queue.getBody() を、ローカル変数で定義他。
	 *
	 * @param key		キー
	 * @param rownum	行番号
	 *
	 * @return キーのボディー値
	 * @og.rtnNotNull
	 */
	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時バグ修正

		final DBTableModel bodyModel = queue.getBody();				// 6.1.1.0 (2015/01/17)

		final int clmno = bodyModel.getColumnNo( key, false );		// 6.1.1.0 (2015/01/17)
		if( clmno < 0 && !ROW_NO.equals( key ) ) { return ""; }

		// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
		final int rowCount = bodyModel.getRowCount();				// 6.1.1.0 (2015/01/17)
		// ページブレイク判定、先読みして判断
		if( PAGE_BREAK.equals( key ) ) {
			// 6.9.7.0 (2018/05/14) PMD These nested if statements could be combined
//			if( rownum < rowCount - 1 ) {							// 6.1.1.0 (2015/01/17)
//				if( !( bodyModel.getValue( rownum, clmno ).equals( bodyModel.getValue( rownum + 1, clmno ) ) ) ) {
			if( rownum < rowCount - 1							// 6.1.1.0 (2015/01/17)
				&& !( bodyModel.getValue( rownum, clmno ).equals( bodyModel.getValue( rownum + 1, clmno ) ) ) ) {
					isPageBreak = true;
//				}
			}
			return "";
		}

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

		if( rownum >= rowCount ) {									// 6.1.1.0 (2015/01/17)
			status = OVERFLOW;
			return "";
		}

		if( rownum == rowCount - 1 ) {								// 6.1.1.0 (2015/01/17)
			// 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 = bodyModel.getValue( rownum, clmno );			// 6.1.1.0 (2015/01/17)
			// 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
			if( queue.isFglocal() ) {
				// 4.3.6.0 (2009/04/01)
				// 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
				value = bodyModel.getDBColumn( clmno ).getWriteValue( value );	// 6.1.1.0 (2015/01/17)
			}
		}

		// 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 変換後の値
	 * @og.rtnNotNull
	 */
	private String checkValue( final String value ) {
		String rtn = value;

		// 5.0.2.0 (2009/11/01)
		if( queue.isFglocal() ) {
			// 6.0.2.5 (2014/10/31) refactoring
			final int idx = rtn.indexOf( "<span" );
			if( idx >= 0 ) {
				final 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 解析結果の配列
	 * @og.rtnNotNull
	 */
	private static String[] tag2Array( final String str, final String startTag, final String endTag ) {
		String header = null;
		String footer = null;
		final List<String> body = new ArrayList<>();

		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() {
		final String fileName = path + "styles.xml";
		String content = readOOoXml( fileName );

		if( content.indexOf( VAR_START ) < 0 ) { return; }

		content = new TagParser() {
			/**
			 * 開始タグから終了タグまでの文字列の処理を定義します。
			 *
			 * この実装では、何も処理を行いません。(切り出した文字列はアペンドされません)
			 * サブクラスでオーバーライドして実際の処理を実装して下さい。
			 *
			 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
			 * @param buf 出力を行う文字列バッファ
			 * @param offset 終了タグのオフセット(ここでは使っていません)
			 */
			@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() {
		final String fileName = path + "meta.xml";

		String meta = readOOoXml( fileName );

		// シート数書き換え
		// 5.1.6.0 (2010/05/01)
		if( meta.indexOf( TABLE_COUNT_START_TAG ) >=0 ){
			final 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 ){
			final 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 ){
			final 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 ) {
		final String[] tags = tag2Array( header, STYLE_START_TAG, STYLE_END_TAG );
		final Set<String> origNameSet = pageNameMap.keySet();
		for( int i=2; i<tags.length; i++ ) {
			for( final String origName : origNameSet ) {
				if( tags[i].indexOf( "=\"" + origName + "." ) >= 0 ) {
					final 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() {
		final String fileName = path + "content.xml";
		final String content = readOOoXml( fileName );

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

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

			// スタイル情報にシート依存の情報がある場合は、ページ分だけコピーする。
			// 6.3.9.0 (2015/11/06) entrySet イテレータではなく効率が悪い keySet イテレータを使用している
			for( int i=2; i<tags.length; i++ ) {
				boolean isReplace = false;
				for( final Map.Entry<String,List<String>> entry : pageNameMap.entrySet() ) {
					final String origName = entry.getKey();
					if( tags[i].indexOf( "=\"" + origName + "." ) >= 0 ) {
						for( final String newName : entry.getValue() ) {
							String styleStr = tags[i].replace( "=\"" + origName + "." , "=\"" + newName + "." );
							// シート名の書き換え
							final 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( final IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:error occurer while write ReParsed Sheet " + fileName + 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( final IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:error occurer while merge content.xml" + 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() {
		final String fileName = path + "META-INF" + File.separator + "manifest.xml";
		final String[] conArr = TagParser.tag2Array( readOOoXml( fileName ), MANIFEST_START_TAG, MANIFEST_END_TAG );

		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		buf.append( conArr[0] );
		for( int i=2; i<conArr.length; i++ ) {
			buf.append( conArr[i] );
		}
		for( final Map.Entry<String,String> entry : addObjMap.entrySet() ) {
			if( "graph".equals( entry.getValue() ) ) {
				buf.append( "<manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"" )
					.append( entry.getKey() )
					.append( "/content.xml\"/><manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"" )
					.append( entry.getKey() )
					.append( "/styles.xml\"/><manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"" )
					.append( entry.getKey() )
					.append( "/meta.xml\"/><manifest:file-entry manifest:media-type=\"application/vnd.oasis.opendocument.chart\" manifest:full-path=\"" )
					.append( entry.getKey() ).append( "/\"/>" );						// XML なので、このまま。
			}
			else {
				buf.append( "<manifest:file-entry manifest:media-type=\"image/" )
					.append( entry.getValue() ).append( "\" manifest:full-path=\"" )
					.append( entry.getKey() ).append( "\"/>" );							// XML なので、このまま。
			}
		}
		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でコンテンツの部分が改行されている場合があるため、ループを回して読込み
	 * @og.rev 6.2.0.0 (2015/02/27) new BufferedReader … を、FileUtil.getBufferedReader … に変更。
	 * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
	 * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。
	 *
	 * @param fileName	ファイル名
	 *
	 * @return 読み取った文字列
	 * @og.rtnNotNull
	 */
	private String readOOoXml( final String fileName ) {
		final File file = new File ( fileName );

		BufferedReader br = null;
		String tmp = null;
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		try {
			br = FileUtil.getBufferedReader( file, "UTF-8" );		// 6.2.0.0 (2015/02/27)
			xmlHeader = br.readLine();
			while( ( tmp = br.readLine() ) != null ) {				// 4.3.6.0 (2009/04/01)
				buf.append( tmp );
			}
		}
		// 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
		catch( final CharacterCodingException ex ) {
			final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
								+	"  ファイルのエンコードが指定のエンコードと異なります。" + CR
								+	" [" + fileName + "] , Encode=[UTF-8]" ;
			throw new OgCharacterException( errMsg,ex );	// 6.5.0.1 (2016/10/21)
		}
		catch( final IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:Failed to read " + fileName + CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( br );
		}

		final String str = buf.toString();
		if( xmlHeader == null || xmlHeader.isEmpty() || str == null || str.isEmpty() ) {
			queue.addMsg( "[ERROR]PARSE:Maybe " + fileName + " is Broken!" + 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( final IOException ex  ) {
			queue.addMsg( "[ERROR]PARSE:Failed to write " + fileName + CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( bw );
		}
	}

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

	/**
	 * XMLファイル書き込み用のライターを返します。
	 *
	 * @param fileName ファイル名
	 * @param append アベンドするか
	 *
	 * @return ライター
	 * @og.rtnNotNull
	 */
	private BufferedWriter getWriter( final String fileName, final boolean append ) {
		final File file = new File ( fileName );
		BufferedWriter bw;
		try {
			bw = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( file, append ), "UTF-8" ) );
		}
		catch( final UnsupportedEncodingException ex ) {
			queue.addMsg( "[ERROR] Input File is written by Unsupported Encoding" );
			throw new HybsSystemException( ex );
		}
		catch( final 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 ) {
			final int sufIdx = fileName.lastIndexOf( '.' );
			if( sufIdx >= 0 ) {
				suffix = fileName.substring( sufIdx + 1 ).toLowerCase( Locale.JAPAN );
			}
		}
		return suffix;
	}
}
