/*
 * 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.FS ;			// 8.0.3.0 (2021/12/17)
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

import org.opengion.hayabusa.report2.TagParser.SplitKey;			// 8.0.3.0 (2021/12/17)

/**
 * 指定されたﾊﾟｽに存在する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 END_KEY = "\"";				// 8.0.3.0 (2021/12/17)

	/* 表紙印刷用のﾍﾟｰｼﾞ名称 */
	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 = "_";		// 8.0.3.0 (2021/12/17) '_' で､ｷｰと行番号の分離を､ｲﾝﾅｰｸﾗｽ化します｡

	/* ﾍﾟｰｼﾞｴﾝﾄﾞｶｯﾄのｶﾗﾑ文字列 */
	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)
	private static final String	QR_TXT_ENC		= HybsSystem.sys( "REPORT_QR_TEXT_ENCODE" );		// 7.2.3.0 (2020/04/10) 帳票出力のQRｺｰﾄﾞ作成時のﾃｷｽﾄのｴﾝｺｰﾄﾞ指定

	/* 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:";
	private static final String DRAW_START_TAG = "<draw:";
	private static final String DRAW_END_TAG = "</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)

//	/* 8.0.3.0 (2021/12/17) ods→xlsx変換時のｼｰﾄ毎の行数 */
//	【保留】private final List<Integer> sheetRows = new ArrayList<>();

	/**
	 * ｺﾝｽﾄﾗｸﾀ
	 *
	 * @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)
	}

	/**
	 * ﾊﾟｰｽ処理を実行します｡(1)
	 *
	 * @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();					// (2)

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

		/* ﾍｯﾀﾞｰ部分にｼｰﾄ情報がある場合に書き換え */
		if( isNeedsReparse ) {
			/* ﾍｯﾀﾞｰﾌｧｲﾙの再ﾊﾟｰｽ */
			execContentHeader();		// (4)
			/* ﾍｯﾀﾞｰﾌｧｲﾙとそれ以降のﾌｧｲﾙの連結 */
			execMergeContent();			// (5)
		}

		/* ﾒﾀﾃﾞｰﾀの変換 */
		execMeta();						// (6)

		// 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();				// (7)
		}
	}

//	/**
//	 * 【保留】ｼｰﾄ毎の行数をﾘｽﾄで返します。
//	 *
//	 * @og.rev 8.0.3.0 (2021/12/17) ods→xlsx変換時のｼｰﾄ毎の行数
//	 *
//	 * @return  ｼｰﾄ毎の行数ﾘｽﾄ
//	 */
//	public List<Integer> getSheetRowsList() {
//		return sheetRows;
//	}

	/**
	 * 帳票処理ｷｭｰを元に､content.xmlを書き換えます｡(3)
	 * まず､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() を､変数に入れておきます｡
	 * @og.rev 8.0.3.0 (2021/12/17) TagParser.tag2Array を使用します｡
	 */
	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 );
		final String[] tags = TagParser.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 );
			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) ﾙｰﾌﾟから出す｡
			final String[] bodyTypes = queue.getBodyTypes();		// 8.0.3.0 (2021/12/17)
			sheet.analyze( tags[i],bodyTypes );						// 8.0.3.0 (2021/12/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)
			}

			final int rowCount = bodyModel.getRowCount();					// 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 );
			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() を､変数に入れておきます｡
	 * @og.rev 7.3.0.1 (2021/01/22) 画像ﾌｧｲﾙの置き方によって､ﾍｯﾀﾞｰ部に {@IMG.XXX} が書かれることがある｡
	 * @og.rev 8.0.3.0 (2021/12/17) COPY_LINE機能の追加
	 *
	 * @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() );
			final int printRangeEnd = headerStr.indexOf( END_KEY, 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 );
		}

		// 7.3.0.1 (2021/01/22) 画像ﾌｧｲﾙの置き方によって､ﾍｯﾀﾞｰ部に {@IMG.XXX} が書かれることがある｡
		writeParsedRow( headerStr, bw, orgShtName, outputSheetName );
//		bw.write( headerStr );

		// 8.0.3.0 (2021/12/17) COPYLINE機能の追加 ｼｰﾄのﾎﾞﾃﾞｨ部分を書き込み
	//	【保留】sheetRows.add( sheet.getRowCnt() );
		for( int i=0; i<sheet.getRowCnt(); i++ ) {							// i はシートの行数
			final String row = sheet.getRow( i,currentBaseRow );
			writeParsedRow( row, bw, orgShtName, outputSheetName );
		}

//		// ｼｰﾄのﾎﾞﾃﾞｨ部分を書き込み
//		for( final String row : sheet.getRows() ) {
//			writeParsedRow( row, bw, orgShtName, outputSheetName );
//		}

//		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() + ")" );
			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) ﾛｰｶﾙﾘｿｰｽの文字型⇒数値型変換の処理の有無
	 * @og.rev 8.0.3.0 (2021/12/17) "\n" 文字列を実際の改行ｺｰﾄﾞに置換して設定します｡
	 *
	 * @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 ) {
				final String key = TagParser.checkKey( str, buf );

				// 4.3.0.0 (2008/07/15) "<"が入っていた場合には{@不整合}ｴﾗｰ
				if( key.indexOf( '<' ) >= 0 ){
					queue.addMsg( "[ERROR]PARSE:{@と}の整合性が不正です｡" + CR
							+ "変数内の特定の文字列に書式設定がされている可能性があります｡ｷｰ=" + key );
					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 );
					}
					// 8.0.3.0 (2021/12/17) "\n" 文字列を実際の改行ｺｰﾄﾞに置換して設定します｡
					if( val.indexOf( "\\n" ) >= 0 ) {
						val = val.replace( "\\n" , "\n" );
					}
					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) 属性終了追加
				final String from = TABLE_STYLE_NAME_START_TAG + name + END_KEY ;
				final String to   = TABLE_STYLE_NAME_START_TAG + name + "_" + sheetNameNew + END_KEY ;

//				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 );
//				}

				if( rowStr.indexOf( from ) >= 0 ) {
					rowStr = rowStr.replace( from, to );
				}
			}
		}
		//==============================================================================

		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" に変換
				final int endIdx    = typeIdx + TABLE_CELL_STRING_TYPE.length() ;
				final String repStr = TABLE_CELL_FLOAT_TYPE + " " + TABLE_CELL_FLOAT_VAL_START + val + END_KEY ;

//				sb.replace( typeIdx, typeIdx + TABLE_CELL_STRING_TYPE.length()
//					,TABLE_CELL_FLOAT_TYPE + " " + TABLE_CELL_FLOAT_VAL_START + val + TABLE_CELL_FLOAT_VAL_END );
				sb.replace( typeIdx, endIdx, repStr );
			}
		}
	}

	/**
	 * 引数に指定された文字列の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始まりも文字列として扱う
 	 * @og.rev 8.0.3.0 (2021/12/17) ｱﾝﾀﾞｰﾊﾞｰで､ｷｰと行番号の分離を､ｲﾝﾅｰｸﾗｽ化します｡
	 *
	 * @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 );
			final SplitKey spKey = new SplitKey( key );		// 8.0.3.0 (2021/12/17)
			final int col = queue.getBody().getColumnNo( spKey.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() );
					final int floatEndIdx = sb.indexOf( END_KEY, floatStrIdx + TABLE_CELL_FLOAT_VAL_START.length() );
					if( floatEndIdx >= 0 ) {
//						sb.replace( floatStrIdx, floatEndIdx + TABLE_CELL_FLOAT_VAL_END.length(), "" );
						sb.replace( floatStrIdx, floatEndIdx + 1, "" );
					}
				}
			}
		}

		// ｱﾉﾃｰｼｮﾝの値から､ｾﾙの文字列部分を置き換え
		final int endIdx = isCell ? row.indexOf( TABLE_CELL_END_TAG, offset ) : row.indexOf( DRAW_END_TAG, 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_TAG, 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_TAG, 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とします)
	 * @og.rev 7.2.3.0 (2020/04/10) QRｺｰﾄﾞのﾊﾟﾗﾒｰﾀをｼｽﾃﾑﾘｿｰｽで設定(帳票出力のQRｺｰﾄﾞ作成時のﾃｷｽﾄのｴﾝｺｰﾄﾞ指定)
	 *
	 * @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();
		offset = row.indexOf( END_KEY, offset ) + 1;

		// 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 );
		sb.append( fileName ).append( END_KEY );

		// 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;
			new File( path ).getAbsolutePath() + FS + IMG_DIR + FS + 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.init( value, fileNameAbs, QR_VERSION, QR_ENCMODE, QR_ERRCRCT, QR_IMAGE_TYPE, QR_PIXEL, QR_TXT_ENC );	// 7.2.3.0 (2020/04/10)
		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に登録する
	 * @og.rev 7.3.0.1 (2021/01/22) 画像ﾌｧｲﾙ名が漢字の場合､うまくいかないので､置き換える｡
	 *
	 * @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();
			offset = row.indexOf( END_KEY, offset ) + 1;

			// 7.3.0.1 (2021/01/22) 画像ﾌｧｲﾙ名が漢字の場合､うまくいかないので､置き換える｡
//			final String fileNameOut = IMG_DIR + '/' + imgFile.getName();
			final String extension = value.substring( value.lastIndexOf('.') );		// 7.3.0.1 (2021/01/22) 拡張子( .付き )
			// 7.3.0.1 (2021/01/22)  同一ﾌｧｲﾙは同一名にしておきます｡ﾏｲﾅｽが気持ち悪いのでﾊｯｼｭ値は絶対値にしておきます｡
			// 8.0.0.0 (2021/07/31) spotbugs:ﾊｯｼｭｺｰﾄﾞが Integer.MIN_VALUE なら結果は同様に負です (Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE なので)｡
//			final String fileNameOut = IMG_DIR + '/' + Math.abs( imgFile.hashCode() ) + extension;
			final String fileNameOut = IMG_DIR + '/' + Integer.toUnsignedString( imgFile.hashCode() ) + extension;

//			sb.append( fileNameOut ).append( DRAW_IMG_HREF_END );
			sb.append( fileNameOut ).append( END_KEY );

			final File fileOutAbs = new File( path,fileNameOut );
			if( !fileOutAbs.getParentFile().exists() && fileOutAbs.getParentFile().mkdirs() ) {
				System.err.println( fileOutAbs + " の ﾃﾞｨﾚｸﾄﾘ作成に失敗しました｡" );
			}

	//		final String fileNameOutAbs =
//				new File( path ).getAbsolutePath() + File.separator + IMG_DIR + File.separator + imgFile.getName();
	//			new File( path ).getAbsolutePath() + '/' + fileNameOut;
	//		// 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 ) );		// imgFile → fileNameOutAbs copy
			FileUtil.copy( imgFile, fileOutAbs );						// imgFile → fileOutAbs copy

			// 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; }

		// 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX'
		return new TagParser() {
//		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 ) {
//					final String nameOrig = TagParser.getValueFromTag( graphTag, GRAPH_HREF_START, GRAPH_HREF_END );
					final String nameOrig = TagParser.getValueFromTag( graphTag, GRAPH_HREF_START, END_KEY );	// 8.0.3.0 (2021/12/17)
					if( new File( path + nameOrig ).exists() ) {
						final 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 );
						parseGraphContent( path + nameNew + FS + "content.xml", sheetOrig, sheetNew );

						// ｸﾞﾗﾌの参照範囲のｼｰﾄ名を置き換え
//						final String range = TagParser.getValueFromTag( str, GRAPH_UPDATE_RANGE_START, GRAPH_UPDATE_RANGE_END );
						final String range = TagParser.getValueFromTag( str, GRAPH_UPDATE_RANGE_START, END_KEY );	// 8.0.3.0 (2021/12/17)
						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 );
			}.doParse( graphContent, GRAPH_CONTENT_START, END_KEY );
		}

		// {@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) 修正分
 	 * @og.rev 8.0.3.0 (2021/12/17) ｱﾝﾀﾞｰﾊﾞｰで､ｷｰと行番号の分離を､ｲﾝﾅｰｸﾗｽ化します｡
	 *
	 * @param key	ｷｰ
	 *
	 * @return 値
	 */
	private String getValue( final String key ) {
//		final int conOffset = key.lastIndexOf( VAR_CON );

//		String value = null;
		final String value ;

//		if( conOffset < 0 ) {
		final SplitKey spKey = new SplitKey( key );		// 8.0.3.0 (2021/12/17)
		if( spKey.rownum < 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 );
				value = getBodyValue( spKey.name, spKey.rownum + currentBaseRow );
//			}
		}

		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番目以降に､開始ﾀｸﾞ､終了ﾀｸﾞの部分が格納されます｡
//	 *
//	 * @og.rev 8.0.3.0 (2021/12/17) TagParser.tag2Array を使用します｡
//	 *
//	 * @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(ﾍｯﾀﾞｰ､ﾌｯﾀｰ)を書き換えます｡(2)
	 *
	 * @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を書き換えます｡(6)
	 *
	 * @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 );
			final String tableCount = TagParser.getValueFromTag( meta, TABLE_COUNT_START_TAG, END_KEY );	// 8.0.3.0 (2021/12/17)
			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 );
			final String cellCount = TagParser.getValueFromTag( meta, CELL_COUNT_START_TAG, END_KEY );	// 8.0.3.0 (2021/12/17)
			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 );
			final String objectCount = TagParser.getValueFromTag( meta, OBJECT_COUNT_START_TAG, END_KEY );	// 8.0.3.0 (2021/12/17)
			//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) 条件付書式対応
	 * @og.rev 8.0.3.0 (2021/12/17) TagParser.tag2Array を使用します｡
	 *
	 * @param header content.xmlのﾍｯﾀﾞｰ
	 */
	private void getRepStyleList( final String header ) {
//		final String[] tags = tag2Array( header, STYLE_START_TAG, STYLE_END_TAG );
		final String[] tags =TagParser.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 );
					final String styleName = TagParser.getValueFromTag( tags[i], STYLE_NAME_START_TAG, END_KEY );	// 8.0.3.0 (2021/12/17)
					repStyleList.add( styleName );
					break;
				}
			}
		}
	}

	/**
	 * 帳票処理ｷｭｰを元に､content.xmlを書き換えます｡(4)
	 * まず､XMLを一旦ﾒﾓﾘ上に展開した後､ｼｰﾄ単位に分解し､ﾃﾞｰﾀの埋め込みを行います｡
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
	 * @og.rev 8.0.3.0 (2021/12/17) TagParser.tag2Array を使用します｡
	 */
	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[] tags = TagParser.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 );
							final String styleName = TagParser.getValueFromTag( styleStr, STYLE_NAME_START_TAG, END_KEY );	// 8.0.3.0 (2021/12/17)
							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 );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( bw );
		}
	}

	/**
	 * content.xmlのﾍｯﾀﾞｰ部分を出力したcontent.xmlに､ﾍｯﾀﾞｰ部分以降を出力した content.xml.bakをﾏｰｼﾞします｡(5)
	 *
	 * @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" );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( srcChannel );
			Closer.ioClose( destChannel );
		}
		FileUtil.deleteFiles( new File( path + "content.xml.tmp" ) );
	}

	/**
	 * META-INF/manifest.xmlに､追加したｵﾌﾞｼﾞｪｸﾄ(ｸﾞﾗﾌ､画像)を登録します｡(7)
	 *
	 * @og.rev 5.3.1.0 (2011/12/01) 新規作成
	 */
	private void execManifest() {
//		final String fileName = path + "META-INF" + File.separator + "manifest.xml";
		final String fileName = path + "META-INF" + FS + "manifest.xml";	// 8.0.3.0 (2021/12/17)
		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 );
			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!" );
			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 );
			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]PARSE:Input File is written by Unsupported Encoding" );
			throw new HybsSystemException( ex );
		}
		catch( final FileNotFoundException ex ) {
			queue.addMsg( "[ERROR]PARSE: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;
	}
}
