001/*
002 * Copyright (c) 2009 The openGion Project.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
013 * either express or implied. See the License for the specific language
014 * governing permissions and limitations under the License.
015 */
016package org.opengion.hayabusa.report2;
017
018import java.io.BufferedReader;
019import java.io.BufferedWriter;
020import java.io.File;
021import java.io.FileInputStream;
022import java.io.FileNotFoundException;
023import java.io.FileOutputStream;
024import java.io.IOException;
025import java.io.OutputStreamWriter;
026import java.io.UnsupportedEncodingException;
027import java.nio.channels.FileChannel;
028import java.nio.charset.CharacterCodingException;                                       // 6.3.1.0 (2015/06/28)
029import java.util.List;
030import java.util.ArrayList;
031import java.util.Map;
032import java.util.HashMap;
033import java.util.concurrent.ConcurrentMap;                                                      // 6.4.3.3 (2016/03/04)
034import java.util.concurrent.ConcurrentHashMap;                                          // 6.4.3.1 (2016/02/12) refactoring
035import java.util.Locale;
036import java.util.Set;
037
038import org.opengion.fukurou.system.OgCharacterException ;                       // 6.5.0.1 (2016/10/21)
039import org.opengion.fukurou.model.NativeType;
040import org.opengion.fukurou.util.StringUtil;                                            // 6.2.0.0 (2015/02/27)
041import org.opengion.fukurou.system.Closer;
042import org.opengion.fukurou.util.FileUtil;
043import org.opengion.fukurou.util.QrcodeImage;
044import org.opengion.hayabusa.common.HybsSystem;
045import org.opengion.hayabusa.common.HybsSystemException;
046import org.opengion.hayabusa.db.DBTableModel;                                           // 6.1.1.0 (2015/01/17)
047import static org.opengion.fukurou.system.HybsConst.CR ;                        // 6.1.0.0 (2014/12/26)
048import static org.opengion.fukurou.system.HybsConst.FS ;                        // 8.0.3.0 (2021/12/17)
049import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
050
051import org.opengion.hayabusa.report2.TagParser.SplitKey;                        // 8.0.3.0 (2021/12/17)
052
053/**
054 * 指定されたパスに存在するODSの各XMLファイルをパースし、帳票定義及び
055 * 帳票データから書き換えます。
056 * 書き換えは読み取り先と同じファイルであるため、一旦読み取った各XMLを
057 * メモリ上に格納したからパース後のXMLファイルの書き込みを行います。
058 *
059 * パース対象となるファイルは以下の3つです。
060 *  content.xml シートの中身を定義
061 *  meta.xml    メタデータを定義
062 *  style.xml   帳票ヘッダーフッターを定義
063 *
064 * content.xmlのパース処理として、まずxmlファイルをシート+行単位に分解します。
065 * その後、分解された行毎に帳票データを埋め込み、出力先のXMLに書き込みを行います。
066 * 書き込みは行単位に行われます。
067 *
068 * また、Calcの特性として、関数の引数に不正な引数が指定された場合、(Text関数の
069 * 引数にnullが指定された場合等)、エラー:XXXという文字が表示されます。
070 * ここでは、これを回避するため、全ての関数にisError関数を埋め込み、エラー表示を
071 * 行わないようにしています。
072 *
073 * @og.group 帳票システム
074 *
075 * @version  4.0
076 * @author   Hiroki.Nakamura
077 * @since    JDK1.6
078 */
079class OdsContentParser {
080
081        //======== content.xmlのパースで使用 ========================================
082        /* シートの開始終了タグ */
083        private static final String BODY_START_TAG = "<table:table ";
084        private static final String BODY_END_TAG = "</table:table>";
085
086        /* 行の開始終了タグ */
087        private static final String ROW_START_TAG = "<table:table-row ";
088
089        /* ページエンドカットの際に、行を非表示にするためのテーブル宣言 */
090        private static final String ROW_START_TAG_INVISIBLE = "<table:table-row table:visibility=\"collapse\" ";
091
092        /* セルの開始タグ */
093        private static final String TABLE_CELL_START_TAG = "<table:table-cell";
094        private static final String TABLE_CELL_END_TAG = "</table:table-cell>";
095
096        /* シート名を取得するための開始終了文字 */
097        private static final String SHEET_NAME_START = "table:name=\"";
098
099        /* オブジェクトの終了位置(シート名)を見つけるための開始文字 */
100        private static final String OBJECT_SEARCH_STR = "table:end-cell-address=\"";
101
102        /* 印刷範囲指定の開始終了文字 */
103        // 4.3.3.5 (2008/11/08) 空白ページ対策で追加
104        private static final String PRINT_RANGE_START = "table:print-ranges=\"";
105//      private static final String PRINT_RANGE_END = "\"";
106        private static final String END_KEY = "\"";                             // 8.0.3.0 (2021/12/17)
107
108        /* 表紙印刷用のページ名称 */
109        private static final String FIRST_PAGE_NAME = "FIRST";
110
111        /* シートブレイク用のキー 5.1.7.0 (2010/06/01) */
112        private static final String SHEET_BREAK = "SHEETBREAK";
113
114        /* 変数定義の開始終了文字及び区切り文字 */
115        private static final String VAR_START = "{@";
116        private static final String VAR_END = "}";
117//      private static final String VAR_CON = "_";              // 8.0.3.0 (2021/12/17) '_' で、キーと行番号の分離を、インナークラス化します。
118
119        /* ページエンドカットのカラム文字列 */
120        private static final String PAGE_END_CUT = "PAGEENDCUT";
121
122        /* ページブレイクのカラム文字列 */
123        private static final String PAGE_BREAK = "PAGEBREAK";
124
125        /* ページ番号出力用文字列 5.1.6.0 (2010/05/01) */
126        private static final String PAGE_NO= "PAGENO";
127
128        /* 行番号出力用文字列 5.1.6.0 (2010/05/01) */
129        private static final String ROW_NO= "ROWNO";
130
131        /* 画像のリンクを取得するための開始終了文字 */
132        private static final String DRAW_IMG_START_TAG = "<draw:image xlink:href=\"";
133        private static final String DRAW_IMG_END_TAG = "</draw:image>";
134//      private static final String DRAW_IMG_HREF_END = "\"";
135
136        /* 画像ファイルを保存するためのパス */
137        private static final String IMG_DIR = "Pictures";
138
139        /* QRコードを処理するためのカラム名 */
140        private static final String QRCODE_PREFIX = "QRCODE.";
141
142        /* 作成したQRコードのフォルダ名及び拡張子 */
143        private static final String QRCODE_FILETYPE = ".png";
144
145        /* 7.0.5.1 (2019/09/27) QRコードのパラメータをシステムリソースで設定できるようにします(ただし、staticとします) */
146        private static final int        QR_VERSION              = HybsSystem.sysInt( "REPORT_QR_VERSION" );                     // 7.0.5.1 (2019/09/27) バージョン
147        private static final char       QR_ENCMODE_CH   = HybsSystem.sys( "REPORT_QR_ENCMODE" ).charAt(0);      // 7.0.5.1 (2019/09/27) エンコードモード
148        private static final char       QR_ERRCRCT_CH   = HybsSystem.sys( "REPORT_QR_ERRCRCT" ).charAt(0);      // 7.0.5.1 (2019/09/27) エラー訂正レベル
149        private static final String QR_IMAGE_TYPE       = "PNG";                                                                                        // 7.0.5.1 (2019/09/27) 出力イメージのタイプ(PNG/JPEG)
150        private static final int        QR_PIXEL                = HybsSystem.sysInt( "REPORT_QR_PIXEL" );                       // 7.0.5.1 (2019/09/27) 1セル辺りの塗りつぶしピクセル数
151        private static final QrcodeImage.EncMode QR_ENCMODE = QrcodeImage.EncMode.get( QR_ENCMODE_CH ); // 7.0.5.1 (2019/09/27)
152        private static final QrcodeImage.ErrCrct QR_ERRCRCT = QrcodeImage.ErrCrct.get( QR_ERRCRCT_CH ); // 7.0.5.1 (2019/09/27)
153        private static final String     QR_TXT_ENC              = HybsSystem.sys( "REPORT_QR_TEXT_ENCODE" );            // 7.2.3.0 (2020/04/10) 帳票出力のQRコード作成時のテキストのエンコード指定
154
155        /* 4.3.3.5 (2008/11/08) 動的に画像を入れ替えるためのパスを記述するカラム名 */
156        private static final String IMG_PREFIX = "IMG.";
157
158        /* ファンクション定義を見つけるための開始終了文字 */
159        private static final String OOOC_FUNCTION_START = "oooc:=";
160        private static final String OOOC_FUNCTION_START_3 = "of:="; // 4.3.7.2 (2009/06/15) ODS仕様変更につき追加
161        private static final String OOOC_FUNCTION_END = ")\" ";
162
163        /* セル内の改行を定義する文字列 5.0.2.0 (2009/11/01) */
164        private static final String OOO_CR = "</text:p><text:p>";
165
166        /* グラフオブジェクトの書き換えを行うための開始終了文字 5.1.8.0 (2010/07/01) */
167        private static final String GRAPH_START_TAG = "<draw:frame ";
168        private static final String GRAPH_END_TAG = "</draw:frame>";
169
170        /* グラフの範囲指定の書き換えを行うための開始終了文字 5.1.8.0 (2010/07/01) */
171        private static final String GRAPH_UPDATE_RANGE_START = "draw:notify-on-update-of-ranges=\"";
172//      private static final String GRAPH_UPDATE_RANGE_END = "\"";
173
174        /* グラフのオブジェクトへのリンクの書き換えを行うための開始終了文字 5.1.8.0 (2010/07/01) */
175        private static final String GRAPH_HREF_START = "xlink:href=\"./";
176//      private static final String GRAPH_HREF_END = "\"";
177        private static final String GRAPH_OBJREPL = "ObjectReplacements";
178
179        /* グラフのオブジェクト毎のcontent.xmlに記述してあるシート名の書き換えを行うための開始終了文字 5.1.8.0 (2010/07/01) */
180        private static final String GRAPH_CONTENT_START = "-address=\"";
181//      private static final String GRAPH_CONTENT_END = "\"";
182
183        /* 生成したグラフのオブジェクトをMETA-INF/manifest.xmlに登録するための開始終了文字列 5.1.8.0 (2010/07/01) */
184        private static final String MANIFEST_START_TAG = "<manifest:file-entry ";
185        private static final String MANIFEST_END_TAG = "/>";                                    // XML なので、このまま。
186
187        /* 数値タイプ置き換え用の文字列 5.1.8.0 (2010/07/01) */
188        private static final String TABLE_CELL_STRING_TYPE = "office:value-type=\"string\"";
189        private static final String TABLE_CELL_FLOAT_TYPE = "office:value-type=\"float\"";
190        private static final String TABLE_CELL_FLOAT_VAL_START = "office:value=\"";
191//      private static final String TABLE_CELL_FLOAT_VAL_END = "\"";
192
193        /* テキスト文字列の開始終了文字列 5.1.8.0 (2010/07/01) */
194        private static final String TEXT_START_TAG = "<text:p>";
195        private static final String TEXT_END_TAG = "</text:p>";
196
197        /* コメント(アノテーション)を処理するためのカラム名 5.1.8.0 (2010/07/01) */
198        private static final String ANNOTATION_PREFIX = "ANO.";
199        private static final String TEXT_START_ANO_TAG = "<text:p"; // アノテーションの場合の置き換えを
200        private static final String TEXT_START_END_ANO_TAG = ">"; // アノテーションの場合の置き換えを
201
202        /* コメント(アノテーション)の開始・終了タグ 5.1.8.0 (2010/07/01) */
203        private static final String ANNOTATION_START_TAG = "<office:annotation";
204        private static final String ANNOTATION_END_TAG = "</office:annotation>";
205
206        /* オブジェクトを検索するための文字列 5.1.8.0 (2010/07/01) */
207//      private static final String DRAW_START_KEY = "<draw:";
208//      private static final String DRAW_END_KEY = "</draw:";
209        private static final String DRAW_START_TAG = "<draw:";
210        private static final String DRAW_END_TAG = "</draw:";
211
212        /* シートの開始終了タグ 5.2.2.0 (2010/11/01) */
213        private static final String STYLE_START_TAG = "<style:style ";
214        private static final String STYLE_END_TAG = "</style:style>";
215
216        /* シート名称 5.2.2.0 (2010/11/01) */
217        private static final String STYLE_NAME_START_TAG = "style:name=\"";
218//      private static final String STYLE_NAME_END_TAG = "\"";
219
220        /* テーブル内シート名称 5.2.2.0 (2010/11/01) */
221        private static final String TABLE_STYLE_NAME_START_TAG = "table:style-name=\"";
222//      private static final String TABLE_STYLE_NAME_END_TAG = "\""; // 5.6.3.1 (2013/04/05)
223
224        /* LibreOffice対応(数字が文字扱いの対応) 8.1.2.1 (2022/03/25) */
225        private static final String XMLNS_CALCEXT = "xmlns:calcext=\"";
226        //private static final String TABLE_CELL_CALCEXT = "calcext:value-type=\"";     // 一時的にコメント化
227
228        //===========================================================================
229
230        //======== meta.xmlのパースで使用 ===========================================
231        /* 総シートカウント数 */
232        private static final String TABLE_COUNT_START_TAG = "meta:table-count=\"";
233//      private static final String TABLE_COUNT_END_TAG = "\"";
234
235        /* 総セルカウント数 */
236        private static final String CELL_COUNT_START_TAG = "meta:cell-count=\"";
237//      private static final String CELL_COUNT_END_TAG = "\"";
238
239        /* 総オブジェクトカウント数 */
240        private static final String OBJECT_COUNT_START_TAG = "meta:object-count=\"";
241//      private static final String OBJECT_COUNT_END_TAG = "\"";
242        //===========================================================================
243
244        /*
245         * 処理中の行番号の状態
246         * NORMAL : 通常
247         * LASTROW : 最終行
248         * OVERFLOW : 終了
249         */
250        private static final int NORMAL   = 0;
251        private static final int LASTROW  = 1;
252        private static final int OVERFLOW = 2;
253        private int status = NORMAL;
254
255        /*
256         * 各雛形ファイルを処理する際の基準となる行数
257         * 初期>0 2行({&#064;XXX_1}まで)処理後>2 ・・・
258         * 各雛形で定義されている行番号 + [baseRow] の値がDBTableModel上の行番号に相当する
259         * currentMaxRowは各シート処理後の[baseRow]と同じ
260         */
261        private int currentBaseRow      ;
262        private int currentMaxRow       ;
263
264        /* 処理したページ数 */
265        private int pages       ;
266
267        /* 処理行がページエンドカットの対象かどうか */
268        private boolean isPageEndCut    ;                       // 4.3.1.1 (2008/08/23) ローカル変数化
269
270        /* ページブレイクの処理中かどうか */
271        private boolean isPageBreak             ;
272
273        /* XML宣言の文字列。各XMLで共通なのでクラス変数として定義 */
274        private String xmlHeader                ;
275
276        /* シートブレイク対象かどうか 5.1.7.0 (2010/06/01) */
277        private int sheetBreakClm = -1;
278
279        /* シート名カラム 5.7.6.2 (2014/05/16) */
280        private int sheetNameClm = -1;                                          // 今は、ページブレイクカラムと同じカラムを使用しています。
281
282        /* シートのヘッダー部分の再パースを行うかどうか  5.2.2.0 (2010/11/01) */
283        private boolean isNeedsReparse  ;
284
285        /* ページ名のマッピング(元のシート名に対する新しいシート名) 5.2.2.0 (2010/11/01) */
286        /** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
287        private final ConcurrentMap<String,List<String>> pageNameMap = new ConcurrentHashMap<>();
288
289        /* ページ名に依存しているスタイル名称のリスト 5.2.2.0 (2010/11/01) */
290        private final List<String> repStyleList = new ArrayList<>();
291
292        /* manifest.xmlに追加が必要なオブジェクトのマップ 5.3.1.0 (2011/01/01) */
293        /** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
294        private final ConcurrentMap<String,String> addObjMap = new ConcurrentHashMap<>();
295
296        private final ExecQueue queue;
297        private final String path;
298
299        private final boolean useChangeType ;           // 6.8.3.1 (2017/12/01)
300
301//      /* 8.0.3.0 (2021/12/17) ods→xlsx変換時のシート毎の行数 */
302//      【保留】private final List<Integer> sheetRows = new ArrayList<>();
303
304        /**
305         * コンストラクタ
306         *
307         * @og.rev 5.1.2.0 (2010/01/01) 処理した行数をQueueオブジェクトから取得(シート数が256を超えた場合の対応)
308         * @og.rev 6.8.3.1 (2017/12/01) ローカルリソースの文字型⇒数値型変換の処理の有無
309         *
310         * @param qu ExecQueueオブジェクト
311         * @param pt パス
312         */
313        OdsContentParser( final ExecQueue qu, final String pt ) {
314                queue = qu;
315                path = pt;
316
317                currentBaseRow = queue.getExecRowCnt();
318                useChangeType = !queue.isFglocal() || HybsSystem.sysBool( "REPORT_USE_CHANGETYPE" );            // 6.8.3.1 (2017/12/01)
319        }
320
321        /**
322         * パース処理を実行します。(1)
323         *
324         * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
325         * @og.rev 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
326         */
327        public void exec() {
328                /*
329                 * 雛形ヘッダーフッターの定義
330                 * OOoではページ毎にヘッダーフッターが設定できないよう。
331                 * なので、全てヘッダー扱いで処理
332                 */
333                execStyles();                                   // (2)
334
335                /* 中身の変換 */
336                execContent();                                  // (3)
337
338                /* ヘッダー部分にシート情報がある場合に書き換え */
339                if( isNeedsReparse ) {
340                        /* ヘッダーファイルの再パース */
341                        execContentHeader();            // (4)
342                        /* ヘッダーファイルとそれ以降のファイルの連結 */
343                        execMergeContent();                     // (5)
344                }
345
346                /* メタデータの変換 */
347                execMeta();                                             // (6)
348
349                // 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
350                /* 追加した画像、オブジェクトをmanifest.xmlに追加 */
351                if( !addObjMap.isEmpty() ) {                    // 6.1.1.0 (2015/01/17) refactoring
352                        execManifest();                         // (7)
353                }
354        }
355
356//      /**
357//       * 【保留】シート毎の行数をリストで返します。
358//       *
359//       * @og.rev 8.0.3.0 (2021/12/17) ods→xlsx変換時のシート毎の行数
360//       *
361//       * @return  シート毎の行数リスト
362//       */
363//      public List<Integer> getSheetRowsList() {
364//              return sheetRows;
365//      }
366
367        /**
368         * 帳票処理キューを元に、content.xmlを書き換えます。(3)
369         * まず、XMLを一旦メモリ上に展開した後、シート単位に分解し、データの埋め込みを行います。
370         *
371         * @og.rev 4.3.0.0 (2008/07/18) ページ数が256を超えた場合のエラー処理
372         * @og.rev 5.0.0.2 (2009/09/15) LINECOPY機能追加
373         * @og.rev 5.1.2.0 (2010/01/01) 処理したページ数、行数をQueueオブジェクトにセット(シート数が256を超えた場合の対応)
374         * @og.rev 5.1.7.0 (2010/06/01) 複数シート対応
375         * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
376         * @og.rev 5.7.6.2 (2014/05/16) PAGEBREAKカラムの値を、シート名として使う場合の処理追加
377         * @og.rev 5.7.6.3 (2014/05/23) PAGEBREAKカラムの値を、シート名として使う場合の、FIRST雛形への適用
378         * @og.rev 6.1.1.0 (2015/01/17) 内部ロジックの見直し。queue.getBody() を、ローカル変数で定義他。
379         * @og.rev 6.4.3.3 (2016/03/04) 気になったので、sheet.getOrigSheetName() を、変数に入れておきます。
380         * @og.rev 8.0.3.0 (2021/12/17) TagParser.tag2Array を使用します。
381         * @og.rev 8.1.2.1 (2022/03/25) LibreOffice対応(数字が文字扱いの対応)
382         */
383        private void execContent() {
384                final String fileName = path + "content.xml";
385                final String content = readOOoXml( fileName );
386                // ファイルを解析し、シート+行単位に分解
387//              final String[] tags = tag2Array( content, BODY_START_TAG, BODY_END_TAG );
388                final String[] tags = TagParser.tag2Array( content, BODY_START_TAG, BODY_END_TAG );             //
389
390                // 5.2.2.0 (2010/11/01) 条件付書式対応
391                // content.xmlのヘッダー部分のみ書き出し
392//              final String contentHeader = tags[0];                                                                   // 8.1.2.1 (2022/03/25) Modify
393                String contentHeader = tags[0];
394
395                // LibreOffice対応 8.1.2.1 (2022/03/25)
396                final int calcSt = contentHeader.indexOf( XMLNS_CALCEXT );
397                if( calcSt >= 0 ) {
398                        final int calcEd = contentHeader.indexOf( "\"", calcSt + XMLNS_CALCEXT.length() );
399                        contentHeader = contentHeader.replace( contentHeader.substring( calcSt - 1, calcEd + 1 ) , "" );
400                }
401
402                BufferedWriter bw = null;
403                try {
404                        bw = getWriter( fileName );
405                        bw.write( xmlHeader );
406                        bw.write( '\n' );
407                        bw.write( contentHeader );
408                        bw.flush();
409                }
410                catch( final IOException ex ) {
411                        queue.addMsg( "[ERROR]PARSE:error occurer while content.xml(header) " + fileName );
412                        throw new HybsSystemException( ex );
413                }
414                finally {
415                        Closer.ioClose( bw );
416                        bw = null;
417                }
418
419                final String contentFooter = tags[1];
420
421                final List<OdsSheet> firstSheets   = new ArrayList<>();
422                final Map<String, OdsSheet> sheetMap = new HashMap<>();
423
424                final DBTableModel bodyModel = queue.getBody();                         // 6.1.1.0 (2015/01/17)
425//              final int rowCount = bodyModel.getRowCount();                           // 6.1.1.0 (2015/01/17)
426
427                OdsSheet defaultSheet = null;
428                for( int i=2; i<tags.length; i++ ) {
429                        final OdsSheet sheet = new OdsSheet();
430
431                        // sheet.analyze( tags[i] );
432//                      sheet.analyze( tags[i],rowCount );                                              // 6.1.1.0 (2015/01/17) ループから出す。
433                        final String[] bodyTypes = queue.getBodyTypes();                // 8.0.3.0 (2021/12/17)
434                        sheet.analyze( tags[i],bodyTypes );                                             // 8.0.3.0 (2021/12/17)
435                        // 5.1.7.0 (2010/06/01) 複数シート対応
436                        final String sheetName = sheet.getSheetName();
437                        if( sheetName.startsWith( FIRST_PAGE_NAME ) ) {
438                                firstSheets.add( sheet );
439                        }
440                        else {
441                                sheetMap.put( sheetName, sheet );
442                                // 一番初めに見つかった表紙以外のシートをデフォルトシートとして設定
443                                if( defaultSheet == null ) {
444                                        defaultSheet = sheet;
445                                }
446                        }
447
448                        // 6.4.3.3 (2016/03/04) 気になったので、sheet.getOrigSheetName() を、変数に入れておきます。
449                        final String orgShtName = sheet.getOrigSheetName();
450
451                        // 5.2.2.0 (2010/11/01) 条件付書式対応
452                        if( !isNeedsReparse && contentHeader.indexOf( "=\"" + orgShtName + "." ) >= 0 ) {
453                                isNeedsReparse = true;
454                        }
455
456                        // 5.2.2.0 (2010/11/01) 条件付書式対応
457                        pageNameMap.put( orgShtName, new ArrayList<>() );
458                }
459
460                // content.xmlの書き出し
461                try {
462                        // 5.2.2.0 (2010/11/01) 条件付書式対応
463                        if( isNeedsReparse ) {
464                                // ヘッダーを再パースする場合は、ボディ部分を
465                                // content.xml.tmpに書き出して、後でマージする
466                                bw = getWriter( fileName + ".tmp" );
467                                getRepStyleList( contentHeader );
468                        }
469                        else {
470                                // ヘッダーを再パースしない場合は、ボディ部分を
471                                // content.xml追加モードで書き込みする
472                                bw = getWriter( fileName, true );
473                        }
474
475                        // 5.7.6.3 (2014/05/23) PAGEBREAKカラムの値を、シート名として使うかどうか。
476                        if( queue.isUseSheetName() ) {
477                                sheetNameClm = bodyModel.getColumnNo( PAGE_BREAK, false );      // 6.1.1.0 (2015/01/17)
478                        }
479
480                        final int rowCount = bodyModel.getRowCount();                                   // 6.1.1.0 (2015/01/17)
481
482                        // 表紙ページの出力
483                        if( queue.getExecPagesCnt() == 0 ) {
484                                for( final OdsSheet firstSheet : firstSheets ) {
485                                        if( currentBaseRow >= rowCount ) {                                              // 6.1.1.0 (2015/01/17) ループから出す。
486                                                break;
487                                        }
488                                        writeParsedSheet( firstSheet, bw );
489                                }
490                        }
491
492                        // 5.1.7.0 (2010/06/01) 複数シート対応
493                        sheetBreakClm = bodyModel.getColumnNo( SHEET_BREAK, false );    // 6.1.1.0 (2015/01/17)
494
495                        // 5.7.6.3 (2014/05/23) 表紙ページも、PAGEBREAKカラムの値を、シート名として使えるようにする。
496
497                        // 繰り返しページの出力
498                        while( currentBaseRow < rowCount ) {                                                    // 6.1.1.0 (2015/01/17) ループから出す。
499                                // 4.3.0.0 (2008/07/18) ページ数が256を超えた場合にエラーとする
500                                // 5.1.2.0 (2010/01/01) 256シートを超えた場合の対応
501                                if( pages >= ExecQueue.MAX_SHEETS_PER_FILE ) {
502                                        queue.setEnd( false );
503                                        break;
504                                }
505
506                                OdsSheet sheet = null;
507                                if( sheetBreakClm >= 0 ) {
508                                        final String sheetName = bodyModel.getValue( currentBaseRow, sheetBreakClm );   // 6.1.1.0 (2015/01/17)
509                                        if( sheetName != null && sheetName.length() > 0 ) {
510                                                sheet = sheetMap.get( sheetName );
511                                        }
512                                }
513                                if( sheet == null ) { sheet = defaultSheet; }
514
515                                writeParsedSheet( sheet, bw );
516                        }
517
518                        // 5.1.2.0 (2010/01/01) 256シートを超えた場合の対応
519                        queue.addExecPageCnt( pages );
520                        queue.setExecRowCnt( currentBaseRow );
521
522                        // フッター
523                        bw.write( contentFooter );
524                        bw.flush();
525                }
526                catch( final IOException ex ) {
527                        queue.addMsg( "[ERROR]PARSE:error occurer while write Parsed Sheet " + fileName );
528                        throw new HybsSystemException( ex );
529                }
530                finally {
531                        Closer.ioClose( bw );
532                }
533        }
534
535        /**
536         * シート単位にパースされた文書データを書き込みます
537         * 出力されるシート名には、ページ番号と基底となる行番号をセットします。
538         *
539         * @og.rev 4.2.4.0 (2008/07/04) 行単位にファイルに書き込むように変更
540         * @og.rev 5.2.0.0 (2010/09/01) 表紙の場合は、BODY部分のデータが含まれていなくてもOK
541         * @og.rev 5.2.1.0 (2010/10/01) シート名定義対応
542         * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
543         * @og.rev 5.7.6.2 (2014/05/16) PAGEBREAKカラムの値を、シート名として使う場合の処理追加
544         * @og.rev 5.7.6.3 (2014/05/23) FIRST雛形シート名が、FIRST**** の場合、**** 部分をシート名に使う。
545         * @og.rev 6.4.3.3 (2016/03/04) 気になったので、sheet.getOrigSheetName() を、変数に入れておきます。
546         * @og.rev 7.3.0.1 (2021/01/22) 画像ファイルの置き方によって、ヘッダー部に {@IMG.XXX} が書かれることがある。
547         * @og.rev 8.0.3.0 (2021/12/17) COPY_LINE機能の追加
548         * @og.rev 8.3.0.2 (2022/08/20) 条件付書式のシート名変換対応(ただし、xmlns:calcext 定義なので、現在未使用)(問合・トラブル 43100-220819-01)
549         *
550         * @param sheet シート
551         * @param bw    BufferedWriterオブジェクト
552         * @throws IOException 書き込みに失敗した場合
553         */
554        private void writeParsedSheet( final OdsSheet sheet, final BufferedWriter bw ) throws IOException {
555                // シート名
556                String outputSheetName = null;
557
558                // 5.7.6.2 (2014/05/16) PAGEBREAKカラムの値を、シート名として使うかどうか。
559                if( sheetNameClm >= 0 ) {
560                        final String sheetName = queue.getBody().getValue( currentBaseRow, sheetNameClm );
561                        if( sheetName != null ) {
562                                outputSheetName = sheetName;
563                        }
564                }
565
566                // 5.7.6.3 (2014/05/23) FIRST雛形シート名が、FIRST**** の場合、**** 部分をシート名に使う。
567                if( outputSheetName == null ) {
568                        String sheetName = sheet.getSheetName();
569                        if( sheetName.startsWith( FIRST_PAGE_NAME ) ) {
570                                sheetName = sheetName.substring( FIRST_PAGE_NAME.length() ).trim();
571                                // 小細工。"FIRST_****" の場合は、"_" を外す。長さ0判定の前に行う。
572                                if( StringUtil.startsChar( sheetName , '_' ) ) {                // 6.2.0.0 (2015/02/27) 1文字 String.startsWith
573                                        sheetName = sheetName.substring( 1 );
574                                }
575
576                                // 長さ0の場合(例えば、FIRSTだけとか)は、設定しない。
577                                if( sheetName.length() > 0 ) { outputSheetName = sheetName; }
578                        }
579                }
580
581                // 従来からあるシート名の値
582                if( outputSheetName == null ) {
583                        if( sheet.getConfSheetName() == null ) {
584                                outputSheetName = "Page" + ( queue.getExecPagesCnt() + pages ) + "_Row" + currentBaseRow ;
585                        }
586                        else {
587                                outputSheetName = sheet.getConfSheetName() + ( queue.getExecPagesCnt() + pages + 1 ) ;
588                        }
589                }
590                // ページブレイク変数を初期化
591                isPageBreak = false;
592
593                // 6.4.3.3 (2016/03/04) 気になったので、sheet.getOrigSheetName() を、変数に入れておきます。
594                final String orgShtName = sheet.getOrigSheetName();
595
596                // シートのヘッダー部分を書き込み(シート名も書き換え)
597                String headerStr = sheet.getHeader().replace( SHEET_NAME_START + orgShtName, SHEET_NAME_START + outputSheetName );
598                // 印刷範囲指定部分のシート名を変更
599                // 4.3.3.5 (2008/11/08) 空白ページ出力の対策。印刷範囲のシート名書き換えを追加
600                final int printRangeStart = headerStr.indexOf( PRINT_RANGE_START );
601                if( printRangeStart >= 0 ) {
602//                      final int printRangeEnd = headerStr.indexOf( PRINT_RANGE_END, printRangeStart + PRINT_RANGE_START.length() );
603                        final int printRangeEnd = headerStr.indexOf( END_KEY, printRangeStart + PRINT_RANGE_START.length() );
604                        String rangeStr = headerStr.substring( printRangeStart, printRangeEnd );
605                        rangeStr = rangeStr.replace( orgShtName, outputSheetName );
606                        headerStr = headerStr.substring( 0, printRangeStart ) + rangeStr + headerStr.substring( printRangeEnd );
607                }
608
609                // 7.3.0.1 (2021/01/22) 画像ファイルの置き方によって、ヘッダー部に {@IMG.XXX} が書かれることがある。
610                writeParsedRow( headerStr, bw, orgShtName, outputSheetName );
611//              bw.write( headerStr );
612
613                // 8.0.3.0 (2021/12/17) COPYLINE機能の追加 シートのボディ部分を書き込み
614        //      【保留】sheetRows.add( sheet.getRowCnt() );
615                for( int i=0; i<sheet.getRowCnt(); i++ ) {                                                      // i はシートの行数
616                        final String row = sheet.getRow( i,currentBaseRow );
617                        writeParsedRow( row, bw, orgShtName, outputSheetName );
618                }
619
620//              // シートのボディ部分を書き込み
621//              for( final String row : sheet.getRows() ) {
622//                      writeParsedRow( row, bw, orgShtName, outputSheetName );
623//              }
624
625//              final String[] rows = sheet.getRows();
626//              for( int i=0; i<rows.length; i++ ) {
627//                      // 4.3.4.4 (2009/01/01)
628//                      writeParsedRow( rows[i], bw, orgShtName, outputSheetName );
629//              }
630                // {@XXXX}が埋め込まれていない場合はエラー
631                // 5.2.0.0 (2010/09/01) 表紙の場合は、BODY部分のデータが含まれていなくてもOK
632                if( currentBaseRow == currentMaxRow && !orgShtName.startsWith( FIRST_PAGE_NAME ) ) {
633                        queue.addMsg( "[ERROR]PARSE:No Data defined on Template ODS(" + queue.getListId() + ")" );
634                        throw new HybsSystemException();
635                }
636                currentBaseRow = currentMaxRow;
637
638                // 8.3.0.2 (2022/08/20) 条件付書式のシート名変換対応(ただし、xmlns:calcext 定義なので、現在未使用)
639                // シートのフッター部分を書き込み
640//              bw.write( sheet.getFooter() );
641                String footerStr = sheet.getFooter();
642                final String sheetNew = outputSheetName;                // final 化しないと、無名クラスで使えない。
643
644                // シート名の置き換えの検索に、GRAPH_CONTENT_START を使っているが、"-address=" を使いたかっただけ。
645                if( footerStr.indexOf( GRAPH_CONTENT_START ) >= 0 ) {
646                        footerStr = new TagParser() {
647                                /**
648                                 * 開始タグから終了タグまでの文字列の処理を定義します。
649                                 *
650                                 * この実装では、何も処理を行いません。(切り出した文字列はアペンドされません)
651                                 * サブクラスでオーバーライドして実際の処理を実装して下さい。
652                                 *
653                                 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
654                                 * @param buf 出力を行う文字列バッファ
655                                 * @param offset 終了タグのオフセット(ここでは使っていません)
656                                 */
657                                @Override
658                                protected void exec( final String str, final StringBuilder buf, final int offset ) {
659                                        buf.append( str.replace( orgShtName, sheetNew ) );
660                                }
661                        }.doParse( footerStr, GRAPH_CONTENT_START, END_KEY );
662                }
663
664                bw.write( footerStr );
665
666                pages++;
667
668                // 5.2.2.0 (2010/11/01) 条件付書式対応
669                pageNameMap.get( orgShtName ).add( outputSheetName );
670        }
671
672        /**
673         * 行単位にパースされた文書データを書き込みます。
674         *
675         * @og.rev 4.2.3.1 (2008/06/19) 関数エラーを表示させないため、ISERROR関数を埋め込み
676         * @og.rev 4.2.4.0 (2008/07/04) 行単位にファイルに書き込むように変更
677         * @og.rev 4.3.0.0 (2008/07/17) {@と}の整合性チェック追加
678         * @og.rev 4.3.0.0 (2008/07/22) 行最後の{@}整合性エラーハンドリング追加
679         * @og.rev 4.3.3.5 (2008/11/08) 画像の動的な入れ替えに対応
680         * @og.rev 5.1.8.0 (2010/07/01) パース方法の内部実装変更
681         * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
682         * @og.rev 5.4.2.0 (2011/12/01) ページブレイク、シートブレイク中でもページエンドカットが適用されるようにする。
683         * @og.rev 5.6.3.1 (2013/04/05) 条件付書式の属性終了文字対応
684         * @og.rev 6.8.3.1 (2017/12/01) ローカルリソースの文字型⇒数値型変換の処理の有無
685         * @og.rev 8.0.3.0 (2021/12/17) "\n" 文字列を実際の改行コードに置換して設定します。
686         *
687         * @param row                           行データ
688         * @param bw                            BufferedWriterオブジェクト
689         * @param sheetNameOrig         元シート名
690         * @param sheetNameNew          新シート名
691         * @throws IOException 書き込みに失敗した場合
692         */
693        private void writeParsedRow( final String row, final BufferedWriter bw, final String sheetNameOrig, final String sheetNameNew ) throws IOException {
694                isPageEndCut = false;
695
696                String rowStr = new TagParser() {
697                        /**
698                         * 開始タグから終了タグまでの文字列の処理を定義します。
699                         *
700                         * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
701                         * @param buf 出力を行う文字列バッファ
702                         * @param offset 終了タグのオフセット(ここでは使っていません)
703                         */
704                        @Override
705                        protected void exec( final String str, final StringBuilder buf, final int offset ) {
706                                final String key = TagParser.checkKey( str, buf );
707
708                                // 4.3.0.0 (2008/07/15) "<"が入っていた場合には{@不整合}エラー
709                                if( key.indexOf( '<' ) >= 0 ){
710                                        queue.addMsg( "[ERROR]PARSE:{@と}の整合性が不正です。" + CR
711                                                        + "変数内の特定の文字列に書式設定がされている可能性があります。キー=" + key );
712                                        throw new HybsSystemException();
713                                }
714
715                                // QRコードの処理、処理後はoffsetが進むため、offsetを再セット
716                                if( key.startsWith( QRCODE_PREFIX ) ) {
717                                        setOffset( makeQRImage( row, offset, key.substring( QRCODE_PREFIX.length() ), buf ) );
718                                }
719                                // 画像置き換えの処理、処理後はoffsetが進むため、offsetを再セット
720                                else if( key.startsWith( IMG_PREFIX  ) ) {
721                                        setOffset( changeImage( row, offset, key.substring( IMG_PREFIX.length() ), buf ) );
722                                }
723                                // コメント(アノテーション)による置き換え処理、処理後はoffsetが進むため、offsetを再セット
724                                else if( key.startsWith( ANNOTATION_PREFIX ) ) {
725                                        setOffset( parseByAnnotation( row, offset, key.substring( ANNOTATION_PREFIX.length() ), buf ) );
726                                }
727                                else {
728                                        String val = getValue( key );
729                                        // 5.5.2.4 (2012/05/16) String key は使われていないので、削除します。
730                                        if( useChangeType ) {           // 6.8.3.1 (2017/12/01)
731                                                // 5.5.2.4 (2012/05/16) String key は使われていないので、削除します。
732                                                changeType( row, offset, val, getNativeType( key, val ), buf );
733                                        }
734                                        // 8.0.3.0 (2021/12/17) "\n" 文字列を実際の改行コードに置換して設定します。
735                                        if( val.indexOf( "\\n" ) >= 0 ) {
736                                                val = val.replace( "\\n" , "\n" );
737                                        }
738                                        buf.append( val );
739                                }
740
741                                // 処理行がページエンドカットの対象か
742                                if( queue.isFgcut() && PAGE_END_CUT.equals( key ) ) {
743                                        isPageEndCut = true;
744                                }
745                        }
746                }.doParse( row, VAR_START, VAR_END, false );
747
748                //==== ここからは後処理 =========================================================
749                /*
750                 * ページエンドカットの判定は最後で処理する。
751                 * {&#064;PAGEENDCUT}が行の最初に書かれている場合は、OVERFLOWになっていない可能性が
752                 * あるため行の途中では判断できない
753                 */
754                // 5.4.2.0 (2011/12/01) シートブレイク中でもページエンドカットが適用されるようにする。
755                // (通常のページブレイクは先読み判定のためページエンドカットすると、ブレイク発生行自身が
756                //  削除されてしまうため現時点では未対応)
757//              if( isPageEndCut && ( status == OVERFLOW || ( sheetBreakClm >= 0 && isPageBreak ) ) ) {
758                if( isPageEndCut && ( status == OVERFLOW || sheetBreakClm >= 0 && isPageBreak ) ) {                             // 6.9.7.0 (2018/05/14) PMD Useless parentheses.
759                        // ページエンドカットの場合は、非表示状態にする。
760                        rowStr = rowStr.replace( ROW_START_TAG, ROW_START_TAG_INVISIBLE ) ;
761                }
762
763                /*
764                 * オブジェクトで定義されているテーブル名を変更
765                 */
766                if( rowStr.indexOf( OBJECT_SEARCH_STR ) >= 0 ) {
767                        rowStr = rowStr.replace( OBJECT_SEARCH_STR + sheetNameOrig, OBJECT_SEARCH_STR + sheetNameNew );
768                }
769
770                /*
771                 * 関数エラーを表示されないため、ISERROR関数を埋め込み 4.2.3.1 (2008/06/19)
772                 */
773                rowStr = replaceOoocError( rowStr );
774
775                /*
776                 * グラフをシート毎にコピー 5.1.8.0 (2010/07/01)
777                 */
778                rowStr = replaceGraphInfo( rowStr, sheetNameOrig, sheetNameNew );
779
780                /*
781                 * アノテーション(コメント)を削除 5.1.8.0 (2010/07/01)
782                 * (コメントが存在すると起動が異常に遅くなる)
783                 */
784                if( rowStr.indexOf( ANNOTATION_START_TAG ) >= 0 ) {
785                        rowStr = new TagParser() {}.doParse( rowStr, ANNOTATION_START_TAG, ANNOTATION_END_TAG );
786                }
787
788                /*
789                 * 条件付書式対応 5.2.2.0 (2010/11/01)
790                 * テーブル内に存在するスタイル名称を書き換え
791                 */
792                if( isNeedsReparse ) {
793                        for( final String name : repStyleList ) {
794                                // 5.6.3.1 (2013/04/05) 属性終了追加
795                                final String from = TABLE_STYLE_NAME_START_TAG + name + END_KEY ;
796                                final String to   = TABLE_STYLE_NAME_START_TAG + name + "_" + sheetNameNew + END_KEY ;
797
798//                              if( rowStr.indexOf( TABLE_STYLE_NAME_START_TAG + name + TABLE_STYLE_NAME_END_TAG ) >= 0 ) {
799//                                      rowStr = rowStr.replace( TABLE_STYLE_NAME_START_TAG + name + TABLE_STYLE_NAME_END_TAG,
800//                                                                                       TABLE_STYLE_NAME_START_TAG + name + "_" + sheetNameNew + TABLE_STYLE_NAME_END_TAG );
801//                              }
802
803                                if( rowStr.indexOf( from ) >= 0 ) {
804                                        rowStr = rowStr.replace( from, to );
805                                }
806                        }
807                }
808                //==============================================================================
809
810                bw.write( rowStr );
811        }
812
813        /**
814         * 帳票データに応じて、カラムの属性を変更(文字型⇒数値型)に変更します。
815         *
816         * @og.rev 5.1.8.0 (2010/07/01) 新規作成
817         * @og.rev 5.5.2.4 (2012/05/16) String key は使われていないので、削除します。
818         * @og.rev 8.1.2.1 (2022/03/25) LibreOffice対応(数字が文字扱いの対応)
819         *
820         * @param row                   行データ
821         * @param curOffset             オフセット
822         * @param val                   設定値
823         * @param type                  ネイティブタイプ
824         * @param sb                    StringBuilderオブジェクト
825         */
826        private void changeType( final String row, final int curOffset
827                                                        , final String val, final NativeType type, final StringBuilder sb ) {
828                if( val == null || val.isEmpty() ) {
829                        return;
830                }
831                // 書き換え対象は数値型のみ
832                if( type != NativeType.INT && type != NativeType.LONG && type != NativeType.DOUBLE ) {
833                        return;
834                }
835                // 処理対象がセルでない(オブジェクト)は書き換えしない
836                if( !isCell( row, curOffset ) ) {
837                        return;
838                }
839
840                // セルの文字が{@xxxx_n}のみであった場合だけ、数値定義の判定を行う。
841                // (関数内に{@xxxx_n}等があった場合は、判定しない(<text:p>{@xxxx_n}</text:p>の場合のみ))
842                if( sb.lastIndexOf( TEXT_START_TAG ) + TEXT_START_TAG.length() == sb.length()
843                        && row.indexOf( TEXT_END_TAG, curOffset ) == curOffset ) {
844                        final int typeIdx = sb.lastIndexOf( TABLE_CELL_STRING_TYPE );
845                        final int cellIdx = sb.lastIndexOf( TABLE_CELL_START_TAG );
846                        if( typeIdx >= 0 && cellIdx >= 0 && typeIdx > cellIdx ) {
847                                // office:value-type="string" を office:value-type="float" office:value="xxx" に変換
848                                final int endIdx    = typeIdx + TABLE_CELL_STRING_TYPE.length() ;
849//                              final String repStr = TABLE_CELL_FLOAT_TYPE + " " + TABLE_CELL_FLOAT_VAL_START + val + END_KEY ;        // 8.1.2.1 (2022/03/25) Modify
850                                final String valNew = val.replace( ",", "" );                                   // カンマ除去 8.1.2.1 (2022/03/25) Add
851                                final String repStr = TABLE_CELL_FLOAT_TYPE + " " + TABLE_CELL_FLOAT_VAL_START + valNew + END_KEY ;
852
853//                              sb.replace( typeIdx, typeIdx + TABLE_CELL_STRING_TYPE.length()
854//                                      ,TABLE_CELL_FLOAT_TYPE + " " + TABLE_CELL_FLOAT_VAL_START + val + TABLE_CELL_FLOAT_VAL_END );
855                                sb.replace( typeIdx, endIdx, repStr );
856
857                        //      // LibreOffice対応 8.1.2.1 (2022/03/25) 一時的にコメント化
858                        //      final int calcSt = sb.lastIndexOf( TABLE_CELL_CALCEXT );
859                        //      if( calcSt >= 0 ) {
860                        //              final int calcEd = sb.indexOf( "\"", calcSt + TABLE_CELL_CALCEXT.length() );
861                        //              sb.delete( calcSt - 1, calcEd + 1 );
862                        //      }
863                        }
864                }
865        }
866
867        /**
868         * 引数に指定された文字列のNativeタイプを返します。
869         *
870         * リソース使用時は、各DBTypeで定義されたNativeタイプを、
871         * 未使用時は、値からNativeタイプを取得して返します。
872         *
873         * @og.rev 5.1.8.0 (2010/07/01) NativeType#getType(String) のメソッドを使用するように変更。
874         * @og.rev 5.10.5.1 (2018/11/09) intだけでなくlongの0始まりも文字列として扱う
875         * @og.rev 8.0.3.0 (2021/12/17) アンダーバーで、キーと行番号の分離を、インナークラス化します。
876         *
877         * @param key   キー
878         * @param val   文字列
879         *
880         * @return  NATIVEの型の識別コード
881         * @og.rtnNotNull
882         * @see org.opengion.fukurou.model.NativeType
883         */
884        private NativeType getNativeType( final String key, final String val ) {
885                if( val == null || val.isEmpty() ) {
886                        return NativeType.STRING;
887                }
888
889                NativeType type = null;
890                if( queue.isFglocal() ) {
891//                      String name = key;
892//                      final int conOffset = key.lastIndexOf( VAR_CON );
893//                      if( conOffset >= 0 ) {
894//                              int rownum = -1;
895//                              try {
896//                                      rownum = Integer.parseInt( name.substring( conOffset + VAR_CON.length(), name.length() ) );             // 6.0.2.4 (2014/10/17) メソッド間違い
897//                              }
898//                              // '_'以降の文字が数字でない場合は、'_'以降の文字もカラム名の一部として扱う
899//                              catch( final NumberFormatException ex ) {
900//                                      // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid empty catch blocks
901//                                      final String errMsg = "'_'以降の文字をカラム名の一部として扱います。カラム名=[" + key + "]" + CR + ex.getMessage() ;
902//                                      System.err.println( errMsg );
903//                              }
904//                              if( rownum >= 0 ) {
905//                                      name = name.substring( 0, conOffset );
906//                              }
907//                      }
908//                      final int col = queue.getBody().getColumnNo( name, false );
909                        final SplitKey spKey = new SplitKey( key );             // 8.0.3.0 (2021/12/17)
910                        final int col = queue.getBody().getColumnNo( spKey.name, false );
911                        if( col >= 0 ) {
912                                type = queue.getBody().getDBColumn( col ).getNativeType();
913                        }
914                }
915
916                if( type == null ) {
917                        // ,は削除した状態で判定
918                        final String repVal = val.replace( ",", "" );
919                        type = NativeType.getType( repVal );                    // 5.1.8.0 (2010/07/01) NativeType#getType(String) のメソッドを使用
920                        // 整数型で、0nnnとなっている場合は、文字列をして扱う
921//                      if( type == NativeType.INT && repVal.length() >= 2 && repVal.charAt(0) == '0' ) {
922                        if( ( type == NativeType.INT || type == NativeType.LONG ) && repVal.length() >= 2 && repVal.charAt(0) == '0' ) { // 5.10.5.1 (2018/11/09) LONGを含む
923                                type = NativeType.STRING;
924                        }
925                }
926
927                return type;
928        }
929
930        /**
931         * コメント(アノテーションによる置き換え処理を行います)
932         * この処理では、offsetを進めるため、戻り値として処理後のoffsetを返します。
933         *
934         * @og.rev 5.1.8.0 (2010/07/01) 新規作成
935         * @og.rev 6.8.3.1 (2017/12/01) ローカルリソースの文字型⇒数値型変換の処理の有無
936         *
937         * @param row                   行データ
938         * @param curOffset             オフセット
939         * @param key                   キー
940         * @param sb                    StringBuilderオブジェクト
941         *
942         * @return 処理後のオフセット
943         */
944        private int parseByAnnotation( final String row, final int curOffset, final String key, final StringBuilder sb ) {
945                int offset = curOffset;
946                final boolean isCell = isCell( row, offset );
947
948                // セルの場合のみ置き換えの判定を行う(オブジェクトの場合は判定しない)
949                if( isCell ) {
950                        final int cellStrIdx = sb.lastIndexOf( TABLE_CELL_START_TAG, offset );
951                        // office:value-type="float" office:value="xxx" を office:value-type="string" に変換
952                        // 数値型の場合は、後で再度変換を行う。
953                        // (文字型に変換しておかないと、値がnullの場合でも"0"が表示されてしまうため)
954                        final int floatIdx = sb.indexOf( TABLE_CELL_FLOAT_TYPE, cellStrIdx );
955                        if( floatIdx >= 0 ) {
956                                sb.replace( floatIdx, floatIdx + TABLE_CELL_FLOAT_TYPE.length(), TABLE_CELL_STRING_TYPE );
957
958                                final int floatStrIdx = sb.indexOf( TABLE_CELL_FLOAT_VAL_START, floatIdx );
959                                if( floatStrIdx >= 0 ) {
960//                                      final int floatEndIdx = sb.indexOf( TABLE_CELL_FLOAT_VAL_END, floatStrIdx + TABLE_CELL_FLOAT_VAL_START.length() );
961                                        final int floatEndIdx = sb.indexOf( END_KEY, floatStrIdx + TABLE_CELL_FLOAT_VAL_START.length() );
962                                        if( floatEndIdx >= 0 ) {
963//                                              sb.replace( floatStrIdx, floatEndIdx + TABLE_CELL_FLOAT_VAL_END.length(), "" );
964                                                sb.replace( floatStrIdx, floatEndIdx + 1, "" );
965
966                                        //      // LibreOffice対応 8.1.2.1 (2022/03/25)
967                                        //      final int calcSt = sb.indexOf( TABLE_CELL_CALCEXT, floatEndIdx );
968                                        //      if( calcSt >= 0 ) {
969                                        //              final int calcEd = sb.indexOf( "\"", calcSt + TABLE_CELL_CALCEXT.length() );
970                                        //              sb.delete( calcSt - 1, calcEd + 1 );
971                                        //      }
972                                        }
973                                }
974                        }
975                }
976
977                // アノテーションの値から、セルの文字列部分を置き換え
978                final int endIdx = isCell ? row.indexOf( TABLE_CELL_END_TAG, offset ) : row.indexOf( DRAW_END_TAG, offset );
979                if( endIdx >= 0 ) {
980                        int textStrIdx = row.indexOf( TEXT_START_ANO_TAG, offset );
981                        // セルのコメントの場合、<text:pで検索すると、オブジェクトのテキストが検索されている可能性がある。
982                        // このため、セルの<text:pが見つかるまで検索を繰り返す
983                        if( isCell ) {
984                                while( !isCell( row, textStrIdx ) && textStrIdx >= 0 ) {
985                                        textStrIdx = row.indexOf( TEXT_START_ANO_TAG, textStrIdx + 1 );
986                                }
987                        }
988                        if( textStrIdx >= 0 && textStrIdx < endIdx ) {
989                                // セルのコメントの場合、</text:p>で検索すると、オブジェクトのテキストが検索されている可能性がある。
990                                // このため、セルの</text:p>が見つかるまで検索を繰り返す
991                                int textEndIdx = row.lastIndexOf( TEXT_END_TAG, endIdx );
992                                if( isCell ) {
993                                        while( !isCell( row, textEndIdx ) && textEndIdx >= 0  ) {
994                                                textEndIdx = row.lastIndexOf( TEXT_END_TAG, textEndIdx - 1 );
995                                        }
996                                }
997                                if( textEndIdx >= 0 && textStrIdx < textEndIdx && textEndIdx < endIdx ) {
998                                        // <text:p xxxx> の xxxx> の部分(style定義など)を書き込み
999                                        final int textStyleEnd = row.indexOf( TEXT_START_END_ANO_TAG, textStrIdx + TEXT_START_ANO_TAG.length() ) + TEXT_START_END_ANO_TAG.length();
1000                                        sb.append( row.substring( offset, textStyleEnd ) );
1001
1002                                        // <text:pの中身(spanタグなどを取り除いた状態の文字列
1003                                        final String textVal = TagParser.checkKey( row.substring( textStyleEnd, textEndIdx ), sb );
1004                                        // 取得したテキスト内にタグ文字が含まれている場合は、処理しない
1005                                        if( textVal.indexOf( '<' ) < 0 && textVal.indexOf( '>' ) < 0 ) {
1006                                                // <text:p xxxx>を書き出し
1007                                                final String val = getValue( key );
1008                                                if( useChangeType ) {           // 6.8.3.1 (2017/12/01)
1009                                                        // 5.5.2.4 (2012/05/16) String key は使われていないので、削除します。
1010                                                        changeType( row, textEndIdx, val, getNativeType( key, textVal ), sb );
1011                                                }
1012                                                sb.append( val );
1013                                        }
1014                                        offset = textEndIdx;
1015                                }
1016                        }
1017                }
1018
1019                return offset;
1020        }
1021
1022        /**
1023         * 現在のオフセットがセルかどうかを返します。
1024         *
1025         * trueの場合はセルを、falseの場合はオブジェクトを意味します。
1026         *
1027         * セルとして判定されるための条件は以下の通りです。
1028         *  現在のoffsetを基準として、
1029         *  ①前に<draw:(オブジェクトの開始)が見つからない
1030         *  ②前に<table:table-cell(セルの始まり)が<draw:(オブジェクトの始まり)より後方にある
1031         *  ③後に</draw:(オブジェクトの終わり)が見つからない
1032         *  ④後に</draw:(オブジェクトの終わり)が</table:table-cell>(セルの終わり)より後方にある
1033         *
1034         * @param row           行データ
1035         * @param offset        オフセット
1036         *
1037         * @return 現在のオフセットがセルかどうか(falseの場合はオブジェクト)
1038         */
1039        private boolean isCell( final String row, final int offset ) {
1040                final int drawStartOffset = row.lastIndexOf( DRAW_START_TAG, offset );
1041                if( drawStartOffset < 0 ) {
1042                        return true;
1043                }
1044                else {
1045                        final int cellStartOffset = row.lastIndexOf( TABLE_CELL_START_TAG, offset );
1046                        if( drawStartOffset < cellStartOffset ) {
1047                                return true;
1048                        }
1049                        else {
1050                                final int drawEndOffset = row.indexOf( DRAW_END_TAG, offset );
1051                                if( drawEndOffset < 0 ) {
1052                                        return true;
1053                                }
1054                                else {
1055                                        final int cellEndOffset = row.indexOf( TABLE_CELL_END_TAG, offset );
1056                                        // 5.1.8.0 (2010/07/01) Avoid unnecessary if..then..else statements when returning a boolean
1057                                        return cellEndOffset >= 0 && cellEndOffset < drawEndOffset ;
1058                                }
1059                        }
1060                }
1061        }
1062
1063        /**
1064         * QRコードを作成します。
1065         * この処理では、offsetを進めるため、戻り値として処理後のoffsetを返します。
1066         *
1067         * @og.rev 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
1068         * @og.rev 4.3.3.5 (2008/11/08) ↑の判定は存在チェックを行ってから処理する。ファイル名に処理行を付加
1069         * @og.rev 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
1070         * @og.rev 7.0.5.1 (2019/09/27) QRコードのパラメータをシステムリソースで設定できるようにします(ただし、staticとします)
1071         * @og.rev 7.2.3.0 (2020/04/10) QRコードのパラメータをシステムリソースで設定(帳票出力のQRコード作成時のテキストのエンコード指定)
1072         * @og.rev 8.3.0.0 (2022/08/01) '<','>','&'のメタ文字変換をするかどうかを指定します。
1073         *
1074         * @param row                   行データ
1075         * @param curOffset             オフセット
1076         * @param key                   キー
1077         * @param sb                    StringBuilderオブジェクト
1078         *
1079         * @return 処理後のオフセット
1080         */
1081        private int makeQRImage( final String row, final int curOffset, final String key, final StringBuilder sb ) {
1082                int offset = curOffset;
1083
1084                // {@QRCODE.XXXX}から実際に画像のパスが書かれている部分までを書き込む
1085                offset = row.indexOf( DRAW_IMG_START_TAG, offset ) + DRAW_IMG_START_TAG.length();
1086                sb.append( row.substring( curOffset, offset ) );
1087                // 画像のパスの終了インデックスを求める
1088//              offset = row.indexOf( DRAW_IMG_HREF_END, offset ) + DRAW_IMG_HREF_END.length();
1089                offset = row.indexOf( END_KEY, offset ) + 1;
1090
1091                // QRCODEの画像ファイル名を求め書き込む
1092                // 4.3.3.5 (2008/11/08) ファイル名に処理行を付加
1093                final String fileName = IMG_DIR + '/' + key + "_" + currentBaseRow + QRCODE_FILETYPE;
1094//              sb.append( fileName ).append( DRAW_IMG_HREF_END );
1095                sb.append( fileName ).append( END_KEY );
1096
1097//              // QRCODEに書き込む値を求める             … 8.3.0.0 (2022/08/01) QrcodeImage 作成直前に移動
1098//              final String value = getValue( key );
1099
1100                // QRCODEの作成
1101                // 4.3.3.5 (2008/11/08) ファイル名に処理行を付加
1102                final String fileNameAbs =
1103//                      new File( path ).getAbsolutePath() + File.separator + IMG_DIR + File.separator + key + "_" + currentBaseRow + QRCODE_FILETYPE;
1104                        new File( path ).getAbsolutePath() + FS + IMG_DIR + FS + key + "_" + currentBaseRow + QRCODE_FILETYPE;
1105
1106                // 画像リンクが無効となっている場合は、Picturesのフォルダが作成されていない可能性がある
1107                // 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
1108                // 4.3.3.5 (2008/11/08) 存在チェック追加
1109                // 6.9.7.0 (2018/05/14) PMD These nested if statements could be combined
1110//              if( !new File( fileNameAbs ).getParentFile().exists() ) {
1111//                      if( new File( fileNameAbs ).getParentFile().mkdirs() ) {
1112                if( !new File( fileNameAbs ).getParentFile().exists()
1113                        && new File( fileNameAbs ).getParentFile().mkdirs() ) {
1114                                System.err.println( fileNameAbs + " の ディレクトリ作成に失敗しました。" );
1115//                      }
1116                }
1117
1118                // QRCODEに書き込む値を求める
1119                final String value = getValue( key,false );             // 8.3.0.0 (2022/08/01) メタ文字変換 しない
1120
1121                final QrcodeImage qrImage = new QrcodeImage();
1122//              qrImage.init( value, fileNameAbs );
1123//              qrImage.init( value, fileNameAbs, QR_VERSION, QR_ENCMODE, QR_ERRCRCT, QR_IMAGE_TYPE, QR_PIXEL );                                // 7.0.5.1 (2019/09/27)
1124                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)
1125                qrImage.saveImage();
1126
1127                // 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
1128                addObjMap.put( fileName, QRCODE_FILETYPE.substring( 1 ) );
1129
1130                // 読み込みOffsetを返します
1131                return offset;
1132        }
1133
1134        /**
1135         * DBTableModelに設定されたパスから画像データを取得し、内部に取り込みます
1136         * この処理では、offsetを進めるため、戻り値として処理後のoffsetを返します。
1137         *
1138         * @og.rev 4.3.3.5 (2008/11/08) 新規追加
1139         * @og.rev 4.3.3.6 (2008/11/15) 画像パスが存在しない場合は、リンクタグ(draw:image)自体を削除
1140         * @og.rev 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
1141         * @og.rev 7.3.0.1 (2021/01/22) 画像ファイル名が漢字の場合、うまくいかないので、置き換える。
1142         *
1143         * @param row                   行データ
1144         * @param curOffset             オフセット
1145         * @param key                   キー
1146         * @param sb                    StringBuilderオブジェクト
1147         *
1148         * @return 処理後のオフセット
1149         */
1150        private int changeImage( final String row, final int curOffset, final String key, final StringBuilder sb ) {
1151                int offset = curOffset;
1152                File imgFile = null;
1153
1154                // 画像ファイルを読み込むパスを求める
1155                final String value = getValue( key );
1156
1157                if( value != null && value.length() > 0 ) {
1158                        imgFile = new File( HybsSystem.url2dir( value ) );
1159                }
1160
1161                // 画像ファイルのパスが入っていて、実際に画像が存在する場合
1162                if( imgFile != null && imgFile.exists() ) {
1163                        // {@IMG.XXXX}から実際に画像のパスが書かれている部分までを書き込む
1164                        offset = row.indexOf( DRAW_IMG_START_TAG, offset ) + DRAW_IMG_START_TAG.length();
1165                        sb.append( row.substring( curOffset, offset ) );
1166
1167                        // 画像のパスの終了インデックスを求める
1168//                      offset = row.indexOf( DRAW_IMG_HREF_END, offset ) + DRAW_IMG_HREF_END.length();
1169                        offset = row.indexOf( END_KEY, offset ) + 1;
1170
1171                        // 7.3.0.1 (2021/01/22) 画像ファイル名が漢字の場合、うまくいかないので、置き換える。
1172//                      final String fileNameOut = IMG_DIR + '/' + imgFile.getName();
1173                        final String extension = value.substring( value.lastIndexOf('.') );             // 7.3.0.1 (2021/01/22) 拡張子( .付き )
1174                        // 7.3.0.1 (2021/01/22)  同一ファイルは同一名にしておきます。マイナスが気持ち悪いのでハッシュ値は絶対値にしておきます。
1175                        // 8.0.0.0 (2021/07/31) spotbugs:ハッシュコードが Integer.MIN_VALUE なら結果は同様に負です (Math.abs(Integer.MIN_VALUE) == Integer.MIN_VALUE なので)。
1176//                      final String fileNameOut = IMG_DIR + '/' + Math.abs( imgFile.hashCode() ) + extension;
1177                        final String fileNameOut = IMG_DIR + '/' + Integer.toUnsignedString( imgFile.hashCode() ) + extension;
1178
1179//                      sb.append( fileNameOut ).append( DRAW_IMG_HREF_END );
1180                        sb.append( fileNameOut ).append( END_KEY );
1181
1182                        final File fileOutAbs = new File( path,fileNameOut );
1183                        if( !fileOutAbs.getParentFile().exists() && fileOutAbs.getParentFile().mkdirs() ) {
1184                                System.err.println( fileOutAbs + " の ディレクトリ作成に失敗しました。" );
1185                        }
1186
1187        //              final String fileNameOutAbs =
1188//                              new File( path ).getAbsolutePath() + File.separator + IMG_DIR + File.separator + imgFile.getName();
1189        //                      new File( path ).getAbsolutePath() + '/' + fileNameOut;
1190        //              // 6.9.7.0 (2018/05/14) PMD These nested if statements could be combined
1191//                      if( !new File( fileNameOutAbs ).getParentFile().exists() ) {
1192//                              if( new File( fileNameOutAbs ).getParentFile().mkdirs() ) {
1193        //              if( !new File( fileNameOutAbs ).getParentFile().exists()
1194        //                      && new File( fileNameOutAbs ).getParentFile().mkdirs() ) {
1195        //                              System.err.println( fileNameOutAbs + " の ディレクトリ作成に失敗しました。" );
1196//                              }
1197        //              }
1198        //              FileUtil.copy( imgFile, new File( fileNameOutAbs ) );           // imgFile → fileNameOutAbs copy
1199                        FileUtil.copy( imgFile, fileOutAbs );                                           // imgFile → fileOutAbs copy
1200
1201                        // 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
1202                        addObjMap.put( fileNameOut, getSuffix( imgFile.getName() ) );
1203                }
1204                // 画像パスが設定されていない、又は画像が存在しない場合
1205                else {
1206                        // {@IMG.XXXX}から見て、<draw:image> ... </draw:image>までをスキップする
1207                        offset = row.indexOf( DRAW_IMG_START_TAG, offset );
1208                        sb.append( row.substring( curOffset, offset ) );
1209
1210                        offset = row.indexOf( DRAW_IMG_END_TAG, offset ) + DRAW_IMG_END_TAG.length();
1211                }
1212
1213                // 読み込みOffsetを返します
1214                return offset;
1215        }
1216
1217        /**
1218         * 変換後の行データで定義されている関数にISERROR関数を埋め込みます。
1219         *
1220         * これは、OOoの関数の動作として、不正な引数等が入力された場合(null値など)に、
1221         * エラー:xxxと表示されてしまうため、これを防ぐために関数エラーのハンドリングを行い、
1222         * エラーの場合は、空白文字を返すようにします。
1223         *
1224         * @og.rev 4.3.7.2 (2009/06/15) 開始文字が変更になったため対応
1225         * @og.rev 5.0.2.0 (2009/11/01) 関数内の"(quot)は、メタ文字に変換する
1226         * @og.rev 5.1.7.0 (2010/06/01) 関数の終わりが)出ない場合にエラーとなるバグを修正
1227         * @og.rev 5.1.8.0 (2010/07/01) パース方法の内部実装変更
1228         *
1229         * @param row   行データ
1230         *
1231         * @return 変換後の行データ
1232         */
1233        private String replaceOoocError( final String row ) {
1234                // 4.3.7.2 (2009/06/15) OOOC_FUNCTION_START3の条件判定追加。どちらか分からないので変数で受ける。
1235                final String functionStart;
1236                if( row.indexOf( OOOC_FUNCTION_START_3 ) >= 0 )         { functionStart = OOOC_FUNCTION_START_3; }
1237                else if( row.indexOf( OOOC_FUNCTION_START ) >= 0 )      { functionStart = OOOC_FUNCTION_START; }
1238                else { return row; }
1239
1240                final String rowStr = new TagParser() {
1241                        /**
1242                         * 開始タグから終了タグまでの文字列の処理を実行するかどうかを定義します。
1243                         *
1244                         * @param strOffset 開始タグのオフセット
1245                         * @param endOffset 終了タグのオフセット
1246                         *
1247                         * @return 処理を行うかどうか(true:処理を行う false:処理を行わない)
1248                         */
1249                        @Override
1250                        protected boolean checkIgnore( final int strOffset, final int endOffset ) {
1251                                // 5.1.7.0 (2010/06/01) 関数の終わりが)出ない場合にエラーとなるバグを修正
1252                                // 単なる行参照でも、of:=で始まるがこの場合は、関数でないため終わりが)でない
1253                                // このため、)が見つからないまたは、タグの終わり(>)が先に見つかった場合は、エラー関数を
1254                                // 埋め込まないようにする。
1255                                int tmpOffset = row.indexOf( ">", strOffset + 1 );
1256                                return endOffset >= 0 && endOffset < tmpOffset ;
1257                        }
1258
1259                        /**
1260                         * 開始タグから終了タグまでの文字列の処理を定義します。
1261                         *
1262                         * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
1263                         * @param buf 出力を行う文字列バッファ
1264                         * @param offset 終了タグのオフセット(ここでは使っていません)
1265                         */
1266                        @Override
1267                        protected void exec( final String str, final StringBuilder buf, final int offset ) {
1268                                String key = str.substring( functionStart.length(), str.length() - OOOC_FUNCTION_END.length() ) + ")";
1269                                key = key.replace( "\"", "&quot;&quot;" ).replace( OOO_CR, "" );
1270                                // 6.4.2.1 (2016/02/05) PMD refactoring.
1271                                buf.append( functionStart ).append( "IF(ISERROR(" ).append( key )
1272                                        .append( ");&quot;&quot;;" ).append( key ).append( OOOC_FUNCTION_END );
1273                        }
1274                }.doParse( row, functionStart, OOOC_FUNCTION_END );
1275
1276                return rowStr;
1277        }
1278
1279        /**
1280         * グラフ表示データ部分を更新します。
1281         *
1282         * @og.rev 5.1.8.0 (2010/07/01) 新規作成
1283         * @og.rev 5.3.1.0 (2011/01/01) OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
1284         *
1285         * @param row           行データ
1286         * @param sheetOrig     元シート
1287         * @param sheetNew      新シート
1288         *
1289         * @return 変換後の行データ
1290         */
1291        private String replaceGraphInfo( final String row, final String sheetOrig, final String sheetNew  ) {
1292                if( row.indexOf( GRAPH_START_TAG ) < 0 || row.indexOf( GRAPH_UPDATE_RANGE_START ) < 0 ) { return row; }
1293
1294                // 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX'
1295                return new TagParser() {
1296//              final String rowStr = new TagParser() {
1297                        /**
1298                         * 開始タグから終了タグまでの文字列の処理を定義します。
1299                         *
1300                         * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
1301                         * @param buf 出力を行う文字列バッファ
1302                         * @param offset 終了タグのオフセット(ここでは使っていません)
1303                         */
1304                        @Override
1305                        protected void exec( final String str, final StringBuilder buf, final int offset ) {
1306                                // <draw:object ... /> の部分
1307                                String graphTag = str;
1308
1309                                if( graphTag.indexOf( GRAPH_UPDATE_RANGE_START ) >= 0 ) {
1310//                                      final String nameOrig = TagParser.getValueFromTag( graphTag, GRAPH_HREF_START, GRAPH_HREF_END );
1311                                        final String nameOrig = TagParser.getValueFromTag( graphTag, GRAPH_HREF_START, END_KEY );       // 8.0.3.0 (2021/12/17)
1312                                        if( new File( path + nameOrig ).exists() ) {
1313                                                final String nameNew = nameOrig + "_" + pages;
1314
1315                                                // グラフオブジェクトの定義ファイルをコピー(./Object X/* ⇒ ./Object X_n/*)
1316                                                FileUtil.copyDirectry( path + nameOrig, path + nameNew );
1317                                                graphTag = graphTag.replace( GRAPH_HREF_START + nameOrig, GRAPH_HREF_START + nameNew );
1318
1319                                                // グラフオブジェクトの画像イメージをコピー(./ObjectReplacements/Object X ⇒ ./ObjectReplacements/Object X_n)
1320                                                // ※実体はコピーしない(リンクの参照を無効にしておくことで、次回起動時にグラフの再描画が行われる)
1321                                                graphTag = graphTag.replace( GRAPH_HREF_START + GRAPH_OBJREPL + "/" + nameOrig, GRAPH_HREF_START + GRAPH_OBJREPL + "/" + nameNew );
1322
1323                                                // OpenOffice3.2対応 追加した画像をmanifest.xmlに登録する
1324                                                addObjMap.put( nameNew, "graph" );
1325
1326                                                // グラフオブジェクトの定義ファイルに記述されている定義ファイルをパースし、シート名と{@XXXX}を置き換え
1327//                                              parseGraphContent( path + nameNew + File.separator + "content.xml", sheetOrig, sheetNew );
1328                                                parseGraphContent( path + nameNew + FS + "content.xml", sheetOrig, sheetNew );
1329
1330                                                // グラフの参照範囲のシート名を置き換え
1331//                                              final String range = TagParser.getValueFromTag( str, GRAPH_UPDATE_RANGE_START, GRAPH_UPDATE_RANGE_END );
1332                                                final String range = TagParser.getValueFromTag( str, GRAPH_UPDATE_RANGE_START, END_KEY );       // 8.0.3.0 (2021/12/17)
1333                                                graphTag = graphTag.replace( GRAPH_UPDATE_RANGE_START + range, GRAPH_UPDATE_RANGE_START + range.replace( sheetOrig, sheetNew ) );
1334                                        }
1335                                }
1336
1337                                buf.append( graphTag );
1338                        }
1339                }.doParse( row, GRAPH_START_TAG, GRAPH_END_TAG );
1340
1341//              return rowStr;
1342        }
1343
1344        /**
1345         * グラフデータのcontent.xmlをパースします。
1346         *
1347         * @og.rev 5.1.8.0 (2010/07/01) 新規作成
1348         *
1349         * @param fileName      ファイル名
1350         * @param sheetOrig     元シート
1351         * @param sheetNew      新シート
1352         */
1353        private void parseGraphContent( final String fileName, final String sheetOrig, final String sheetNew  ) {
1354                String graphContent = readOOoXml( fileName );
1355
1356                // シート名の置き換え
1357                if( graphContent.indexOf( GRAPH_CONTENT_START ) >= 0 ) {
1358                        graphContent = new TagParser() {
1359                                /**
1360                                 * 開始タグから終了タグまでの文字列の処理を定義します。
1361                                 *
1362                                 * この実装では、何も処理を行いません。(切り出した文字列はアペンドされません)
1363                                 * サブクラスでオーバーライドして実際の処理を実装して下さい。
1364                                 *
1365                                 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
1366                                 * @param buf 出力を行う文字列バッファ
1367                                 * @param offset 終了タグのオフセット(ここでは使っていません)
1368                                 */
1369                                @Override
1370                                protected void exec( final String str, final StringBuilder buf, final int offset ) {
1371                                        buf.append( str.replace( sheetOrig, sheetNew ) );
1372                                }
1373//                      }.doParse( graphContent, GRAPH_CONTENT_START, GRAPH_CONTENT_END );
1374                        }.doParse( graphContent, GRAPH_CONTENT_START, END_KEY );
1375                }
1376
1377                // {@XXXX}の置き換え
1378                if( graphContent.indexOf( VAR_START ) >= 0 ) {
1379                        graphContent = new TagParser() {
1380                                /**
1381                                 * 開始タグから終了タグまでの文字列の処理を定義します。
1382                                 *
1383                                 * この実装では、何も処理を行いません。(切り出した文字列はアペンドされません)
1384                                 * サブクラスでオーバーライドして実際の処理を実装して下さい。
1385                                 *
1386                                 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
1387                                 * @param buf 出力を行う文字列バッファ
1388                                 * @param offset 終了タグのオフセット(ここでは使っていません)
1389                                 */
1390                                @Override
1391                                public void exec( final String str, final StringBuilder buf, final int offset ) {
1392                                        buf.append( getHeaderFooterValue( str ) );
1393                                }
1394                        }.doParse( graphContent, VAR_START, VAR_END, false );
1395                }
1396
1397                writeOOoXml( fileName, graphContent );
1398        }
1399
1400        /**
1401         * 指定されたキーの値を返します。
1402         *
1403         * @og.rev 4.3.0.0 (2008/07/18) アンダースコアの処理変更
1404         * @og.rev 4.3.5.0 (2008/02/01) カラム名と行番号文字の位置は最後から検索する 4.3.3.4 (2008/11/01) 修正分
1405         * @og.rev 8.0.3.0 (2021/12/17) アンダーバーで、キーと行番号の分離を、インナークラス化します。
1406         * @og.rev 8.3.0.0 (2022/08/01) '<','>','&'のメタ文字変換をデフォルト実行します。
1407         *
1408         * @param key   キー
1409         *
1410         * @return 値
1411         */
1412        private String getValue( final String key ) {
1413                return getValue( key,true );
1414        }
1415
1416        /**
1417         * 指定されたキーの値を返します。
1418         *
1419         * @og.rev 4.3.0.0 (2008/07/18) アンダースコアの処理変更
1420         * @og.rev 4.3.5.0 (2008/02/01) カラム名と行番号文字の位置は最後から検索する 4.3.3.4 (2008/11/01) 修正分
1421         * @og.rev 8.0.3.0 (2021/12/17) アンダーバーで、キーと行番号の分離を、インナークラス化します。
1422         * @og.rev 8.3.0.0 (2022/08/01) '<','>','&'のメタ文字変換をするかどうかを指定します。
1423         *
1424         * @param key   キー
1425         * @param useChange     タ文字変換するかどうか[true:する]
1426         *
1427         * @return 値
1428         */
1429//      private String getValue( final String key ) {
1430        private String getValue( final String key,final boolean useChange ) {
1431//              final int conOffset = key.lastIndexOf( VAR_CON );
1432
1433//              String value = null;
1434                final String value ;
1435
1436//              if( conOffset < 0 ) {
1437                final SplitKey spKey = new SplitKey( key );             // 8.0.3.0 (2021/12/17)
1438                if( spKey.rownum < 0 ) {
1439                        value = getHeaderFooterValue( key );
1440                }
1441                else {
1442//                      final String name = key.substring( 0, conOffset );
1443//                      int rownum = -1;
1444//                      try {
1445//                              rownum = Integer.parseInt( key.substring( conOffset + VAR_CON.length(), key.length() ) ) + currentBaseRow;      // 6.0.2.4 (2014/10/17) メソッド間違い
1446//                      }
1447//                      catch( final NumberFormatException ex ) {
1448//                              // 4.3.0.0 (2008/07/18) エラーが起きてもなにもしない。
1449//                              // queue.addMsg( "[ERROR]雛形の変数定義が誤っています。カラム名=" + name + CR );
1450//                              // throw new Exception( ex );
1451//                              // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid empty catch blocks
1452//                              final String errMsg = "雛形の変数定義で、行番号文字が取得できません。カラム名=[" + key + "]" + CR + ex.getMessage() ;
1453//                              System.err.println( errMsg );
1454//                      }
1455//
1456//                      // 4.3.0.0 (2008/07/18) アンダースコア後が数字に変換できない場合はヘッダフッタとして認識
1457//                      if( rownum < 0 ){
1458//                              value = getHeaderFooterValue( key );
1459//                      }
1460//                      else{
1461//                              value = getBodyValue( name, rownum );
1462                                value = getBodyValue( spKey.name, spKey.rownum + currentBaseRow );
1463//                      }
1464                }
1465
1466//              return checkValue( value );
1467                return checkValue( value,useChange );                   // 8.3.0.0 (2022/08/01)
1468        }
1469
1470        /**
1471         * 指定されたキーのヘッダー、フッター値を返します。
1472         *
1473         * @og.rev 4.3.6.0 (2009/04/01) レンデラー適用されていないバグを修正
1474         * @og.rev 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
1475         * @og.rev 5.1.6.0 (2010/05/01) ページNO出力対応
1476         * @og.rev 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
1477         * @og.rev 6.1.1.0 (2015/01/17) 内部ロジックの見直し。queue.getFooter(),queue.getHeader() を、ローカル変数で定義。
1478         *
1479         * @param key   キー
1480         *
1481         * @return 値
1482         */
1483        private String getHeaderFooterValue( final String key ) {
1484                String value = "";
1485
1486                // 5.1.6.0 (2010/05/01) ページNO出力対応
1487                if( PAGE_NO.equals( key ) ) {
1488                        value = String.valueOf( pages + 1 );
1489                }
1490                // 6.1.1.0 (2015/01/17) 内部ロジックの見直し。queue.getFooter(),queue.getHeader() を、ローカル変数で定義。
1491                else {
1492                        // 最後の行かオーバーフロー時はフッター。最後の行にきていない場合はヘッダー
1493                        final DBTableModel headerFooterModel = status >= LASTROW ? queue.getFooter() : queue.getHeader() ;
1494
1495                        if( headerFooterModel != null ) {
1496                                final int clmno = headerFooterModel.getColumnNo( key, false );
1497                                if( clmno >= 0 ) {
1498                                        value = headerFooterModel.getValue( 0, clmno );
1499                                        // 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
1500                                        if( queue.isFglocal() ) {
1501                                                // 4.3.6.0 (2009/04/01)
1502                                                // 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
1503                                                value = headerFooterModel.getDBColumn( clmno ).getWriteValue( value );
1504                                        }
1505                                }
1506                        }
1507                }
1508
1509                return value;
1510        }
1511
1512        /**
1513         * 指定された行番号、キーのボディー値を返します。
1514         *
1515         * @og.rev 4.3.6.0 (2009/04/01) レンデラー適用されていないバグを修正
1516         * @og.rev 4.3.6.2 (2009/04/15) 行番号のより小さいカラム定義を読んだ際に、内部カウンタがクリアされてしまうバグを修正
1517         * @og.rev 4.3.6.2 (2009/04/15) 一度オーバーフローした場合に移行が全て空文字で返ってしまうバグを修正
1518         * @og.rev 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
1519         * @og.rev 5.1.6.0 (2010/05/01) 行番号出力対応
1520         * @og.rev 5.1.7.0 (2010/06/01) 複数シート対応
1521         * @og.rev 5.1.9.0 (2010/08/01) 最終行で正しくシートブレイクされないバグを修正
1522         * @og.rev 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
1523         * @og.rev 6.1.1.0 (2015/01/17) 内部ロジックの見直し。queue.getBody() を、ローカル変数で定義他。
1524         *
1525         * @param key           キー
1526         * @param rownum        行番号
1527         *
1528         * @return キーのボディー値
1529         * @og.rtnNotNull
1530         */
1531        private String getBodyValue( final String key, final int rownum ) {
1532                // if( status == OVERFLOW || isPageBreak ) { return ""; }
1533                if( isPageBreak ) { return ""; } // 4.3.6.2 (2009/04/15) OVERFLOW時バグ修正
1534
1535                final DBTableModel bodyModel = queue.getBody();                         // 6.1.1.0 (2015/01/17)
1536
1537                final int clmno = bodyModel.getColumnNo( key, false );          // 6.1.1.0 (2015/01/17)
1538                if( clmno < 0 && !ROW_NO.equals( key ) ) { return ""; }
1539
1540                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid declaring a variable if it is unreferenced before a possible exit point.
1541                final int rowCount = bodyModel.getRowCount();                           // 6.1.1.0 (2015/01/17)
1542                // ページブレイク判定、先読みして判断
1543                if( PAGE_BREAK.equals( key ) ) {
1544                        // 6.9.7.0 (2018/05/14) PMD These nested if statements could be combined
1545//                      if( rownum < rowCount - 1 ) {                                                   // 6.1.1.0 (2015/01/17)
1546//                              if( !( bodyModel.getValue( rownum, clmno ).equals( bodyModel.getValue( rownum + 1, clmno ) ) ) ) {
1547                        if( rownum < rowCount - 1                                                       // 6.1.1.0 (2015/01/17)
1548                                && !( bodyModel.getValue( rownum, clmno ).equals( bodyModel.getValue( rownum + 1, clmno ) ) ) ) {
1549                                        isPageBreak = true;
1550//                              }
1551                        }
1552                        return "";
1553                }
1554
1555                // 5.1.7.0 (2010/06/01) 複数シート対応
1556                // シートブレイクは後読みして判断(前の行と異なっていた場合にブレイク)
1557                if( sheetBreakClm >= 0 ) {
1558                        // 5.1.9.0 (2010/08/01) 最終行で正しくシートブレイクされないバグを修正
1559                        // 6.9.7.0 (2018/05/14) PMD These nested if statements could be combined
1560//                      if( rownum < rowCount && currentBaseRow != rownum ) {
1561//                              if( !( bodyModel.getValue( currentBaseRow, sheetBreakClm ).equals( bodyModel.getValue( rownum, sheetBreakClm ) ) ) ) {
1562                        if( rownum < rowCount && currentBaseRow != rownum
1563                                && !( bodyModel.getValue( currentBaseRow, sheetBreakClm ).equals( bodyModel.getValue( rownum, sheetBreakClm ) ) ) ) {
1564                                        isPageBreak = true;
1565                                        return "";
1566//                              }
1567                        }
1568                }
1569
1570                if( rownum >= rowCount ) {                                                                      // 6.1.1.0 (2015/01/17)
1571                        status = OVERFLOW;
1572                        return "";
1573                }
1574
1575                if( rownum == rowCount - 1 ) {                                                          // 6.1.1.0 (2015/01/17)
1576                        // status = LASTROW;
1577                        status = Math.max( LASTROW, status ); // 4.3.6.2 (2009/04/15) 自身のステータスと比べて大きい方を返す
1578                }
1579
1580                String value = null;
1581                // 5.1.6.0 (2010/05/01) ページNO出力対応
1582                if( ROW_NO.equals( key ) ) {
1583                        value = String.valueOf( rownum + 1 );
1584                }
1585                else {
1586                        value = bodyModel.getValue( rownum, clmno );                    // 6.1.1.0 (2015/01/17)
1587                        // 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
1588                        if( queue.isFglocal() ) {
1589                                // 4.3.6.0 (2009/04/01)
1590                                // 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
1591                                value = bodyModel.getDBColumn( clmno ).getWriteValue( value );  // 6.1.1.0 (2015/01/17)
1592                        }
1593                }
1594
1595                // 4.3.6.2 (2009/04/15)
1596                if( currentMaxRow < rownum + 1 ) {
1597                        currentMaxRow = rownum + 1;
1598                }
1599
1600                return value;
1601        }
1602
1603        /**
1604         * 値に'<'や'>','&'が含まれていた場合にメタ文字に変換します。
1605         *
1606         * @og.rev 5.0.2.0 (2009/11/01) 改行Cの変換ロジックを追加
1607         * @og.rev 5.0.2.0 (2009/11/01) リソース変換時のspanタグを除去
1608         * @og.rev 8.3.0.0 (2022/08/01) '<','>','&'のメタ文字変換をするかどうかを指定します。
1609         *
1610         * @param value 変換前の値
1611         * @param useChange     タ文字変換するかどうか[true:する]
1612         *
1613         * @return 変換後の値
1614         * @og.rtnNotNull
1615         */
1616//      private String checkValue( final String value ) {
1617        private String checkValue( final String value,final boolean useChange ) {
1618                String rtn = value;
1619
1620                // 5.0.2.0 (2009/11/01)
1621                if( queue.isFglocal() ) {
1622                        // 6.0.2.5 (2014/10/31) refactoring
1623                        final int idx = rtn.indexOf( "<span" );
1624                        if( idx >= 0 ) {
1625                                final String spanStart = rtn.substring( idx, rtn.indexOf( '>', idx ) + 1 );
1626                                rtn = rtn.replace( spanStart, "" ).replace( "</span>", "" );
1627                        }
1628                }
1629
1630                if( useChange ) {                                                                       // 8.3.0.0 (2022/08/01)
1631                        if( rtn.indexOf( '&' ) >= 0 ) {
1632                                rtn = rtn.replace( "&", "&amp;" );
1633                        }
1634                        if( rtn.indexOf( '<' ) >= 0 ) {
1635                                rtn = rtn.replace( "<", "&lt;" );
1636                        }
1637                        if( rtn.indexOf( '>' ) >= 0 ) {
1638                                rtn = rtn.replace( ">", "&gt;" );
1639                        }
1640                        if( rtn.indexOf( '\n' ) >= 0 ) {
1641                                rtn = rtn.replace( "\r\n", "\n" ).replace( "\n", OOO_CR );
1642                        }
1643                }
1644
1645                return rtn;
1646        }
1647
1648//      /**
1649//       * 引数の文字列を指定された開始タグ、終了タグで解析し配列として返します。
1650//       * 開始タグより前の文字列は0番目に、終了タグより後の文字列は1番目に格納されます。
1651//       * 2番目以降に、開始タグ、終了タグの部分が格納されます。
1652//       *
1653//       * @og.rev 8.0.3.0 (2021/12/17) TagParser.tag2Array を使用します。
1654//       *
1655//       * @param str           文字列
1656//       * @param startTag      開始タグ
1657//       * @param endTag        終了タグ
1658//       *
1659//       * @return 解析結果の配列
1660//       * @og.rtnNotNull
1661//       */
1662//      private static String[] tag2Array( final String str, final String startTag, final String endTag ) {
1663//              String header = null;
1664//              String footer = null;
1665//              final List<String> body = new ArrayList<>();
1666//
1667//              int preOffset = -1;
1668//              int curOffset = 0;
1669//
1670//              while( true ) {
1671//                      curOffset = str.indexOf( startTag, preOffset + 1 );
1672//                      if( curOffset < 0 ) {
1673//                              curOffset = str.lastIndexOf( endTag ) + endTag.length();
1674//                              body.add( str.substring( preOffset, curOffset ) );
1675//
1676//                              footer = str.substring( curOffset );
1677//                              break;
1678//                      }
1679//                      else if( preOffset == -1 ) {
1680//                              header = str.substring( 0, curOffset );
1681//                      }
1682//                      else {
1683//                              body.add( str.substring( preOffset, curOffset ) );
1684//                      }
1685//                      preOffset = curOffset;
1686//              }
1687//
1688//              String[] arr = new String[body.size()+2];
1689//              arr[0] = header;
1690//              arr[1] = footer;
1691//              for( int i=0; i<body.size(); i++ ) {
1692//                      arr[i+2] = body.get(i);
1693//              }
1694//
1695//              return arr;
1696//      }
1697
1698        /**
1699         * 帳票処理キューを元に、style.xml(ヘッダー、フッター)を書き換えます。(2)
1700         *
1701         * @og.rev 5.1.8.0 (2010/07/01) パース方法の内部実装変更
1702         */
1703        private void execStyles() {
1704                final String fileName = path + "styles.xml";
1705                String content = readOOoXml( fileName );
1706
1707                if( content.indexOf( VAR_START ) < 0 ) { return; }
1708
1709                content = new TagParser() {
1710                        /**
1711                         * 開始タグから終了タグまでの文字列の処理を定義します。
1712                         *
1713                         * この実装では、何も処理を行いません。(切り出した文字列はアペンドされません)
1714                         * サブクラスでオーバーライドして実際の処理を実装して下さい。
1715                         *
1716                         * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
1717                         * @param buf 出力を行う文字列バッファ
1718                         * @param offset 終了タグのオフセット(ここでは使っていません)
1719                         */
1720                        @Override
1721                        public void exec( final String str, final StringBuilder buf, final int offset ) {
1722                                buf.append( getHeaderFooterValue( str ) );
1723                        }
1724                }.doParse( readOOoXml( fileName ), VAR_START, VAR_END, false );
1725
1726                writeOOoXml( fileName, content );
1727        }
1728
1729        /**
1730         * 帳票処理キューを元に、meta.xmlを書き換えます。(6)
1731         *
1732         * @og.rev 5.1.6.0 (2010/05/01) 画面帳票作成機能対応(API経由では出力されないことがある)
1733         */
1734        private void execMeta() {
1735                final String fileName = path + "meta.xml";
1736
1737                String meta = readOOoXml( fileName );
1738
1739                // シート数書き換え
1740                // 5.1.6.0 (2010/05/01)
1741                if( meta.indexOf( TABLE_COUNT_START_TAG ) >=0 ){
1742//                      final String tableCount = TagParser.getValueFromTag( meta, TABLE_COUNT_START_TAG, TABLE_COUNT_END_TAG );
1743                        final String tableCount = TagParser.getValueFromTag( meta, TABLE_COUNT_START_TAG, END_KEY );    // 8.0.3.0 (2021/12/17)
1744                        meta = meta.replace( TABLE_COUNT_START_TAG + tableCount, TABLE_COUNT_START_TAG + pages );
1745                }
1746
1747                // セル数書き換え
1748                // 5.1.6.0 (2010/05/01)
1749                if( meta.indexOf( CELL_COUNT_START_TAG ) >=0 ){
1750//                      final String cellCount = TagParser.getValueFromTag( meta, CELL_COUNT_START_TAG, CELL_COUNT_END_TAG );
1751                        final String cellCount = TagParser.getValueFromTag( meta, CELL_COUNT_START_TAG, END_KEY );      // 8.0.3.0 (2021/12/17)
1752                        meta = meta.replace( CELL_COUNT_START_TAG + cellCount, CELL_COUNT_START_TAG + ( Integer.parseInt( cellCount ) * pages ) );
1753                }
1754
1755                // オブジェクト数書き換え
1756                // 5.1.6.0 (2010/05/01)
1757                if( meta.indexOf( OBJECT_COUNT_START_TAG ) >= 0 ){
1758//                      final String objectCount = TagParser.getValueFromTag( meta, OBJECT_COUNT_START_TAG, OBJECT_COUNT_END_TAG );
1759                        final String objectCount = TagParser.getValueFromTag( meta, OBJECT_COUNT_START_TAG, END_KEY );  // 8.0.3.0 (2021/12/17)
1760                        //4.2.4.0 (2008/06/02) 存在しない場合はnullで帰ってくるので無視する
1761                        if( objectCount != null){
1762                                meta = meta.replace( OBJECT_COUNT_START_TAG + objectCount, OBJECT_COUNT_START_TAG + ( Integer.parseInt( objectCount ) * pages ) );
1763                        }
1764                }
1765
1766                writeOOoXml( fileName, meta );
1767        }
1768
1769        /**
1770         * 書き換え対象のスタイルリストを取得します。
1771         *
1772         * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
1773         * @og.rev 8.0.3.0 (2021/12/17) TagParser.tag2Array を使用します。
1774         *
1775         * @param header content.xmlのヘッダー
1776         */
1777        private void getRepStyleList( final String header ) {
1778//              final String[] tags = tag2Array( header, STYLE_START_TAG, STYLE_END_TAG );
1779                final String[] tags =TagParser.tag2Array( header, STYLE_START_TAG, STYLE_END_TAG );
1780                final Set<String> origNameSet = pageNameMap.keySet();
1781                for( int i=2; i<tags.length; i++ ) {
1782                        for( final String origName : origNameSet ) {
1783                                if( tags[i].indexOf( "=\"" + origName + "." ) >= 0 ) {
1784//                                      final String styleName = TagParser.getValueFromTag( tags[i], STYLE_NAME_START_TAG, STYLE_NAME_END_TAG );
1785                                        final String styleName = TagParser.getValueFromTag( tags[i], STYLE_NAME_START_TAG, END_KEY );   // 8.0.3.0 (2021/12/17)
1786                                        repStyleList.add( styleName );
1787                                        break;
1788                                }
1789                        }
1790                }
1791        }
1792
1793        /**
1794         * 帳票処理キューを元に、content.xmlを書き換えます。(4)
1795         * まず、XMLを一旦メモリ上に展開した後、シート単位に分解し、データの埋め込みを行います。
1796         *
1797         * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
1798         * @og.rev 8.0.3.0 (2021/12/17) TagParser.tag2Array を使用します。
1799         */
1800        private void execContentHeader() {
1801                final String fileName = path + "content.xml";
1802                final String content = readOOoXml( fileName );
1803
1804                // ファイルの解析し、シート+行単位に分解
1805//              final String[] tags = tag2Array( content, STYLE_START_TAG, STYLE_END_TAG );
1806                final String[] tags = TagParser.tag2Array( content, STYLE_START_TAG, STYLE_END_TAG );
1807                final String header = tags[0];
1808                final String footer = tags[1];
1809
1810                BufferedWriter bw = null;
1811                try {
1812                        bw = getWriter( fileName );
1813                        bw.write( xmlHeader );
1814                        bw.write( '\n' );
1815                        bw.write( header );
1816
1817                        // スタイル情報にシート依存の情報がある場合は、ページ分だけコピーする。
1818                        // 6.3.9.0 (2015/11/06) entrySet イテレータではなく効率が悪い keySet イテレータを使用している
1819                        for( int i=2; i<tags.length; i++ ) {
1820                                boolean isReplace = false;
1821                                for( final Map.Entry<String,List<String>> entry : pageNameMap.entrySet() ) {
1822                                        final String origName = entry.getKey();
1823                                        if( tags[i].indexOf( "=\"" + origName + "." ) >= 0 ) {
1824                                                for( final String newName : entry.getValue() ) {
1825                                                        String styleStr = tags[i].replace( "=\"" + origName + "." , "=\"" + newName + "." );
1826                                                        // シート名の書き換え
1827//                                                      final String styleName = TagParser.getValueFromTag( styleStr, STYLE_NAME_START_TAG, STYLE_NAME_END_TAG );
1828                                                        final String styleName = TagParser.getValueFromTag( styleStr, STYLE_NAME_START_TAG, END_KEY );  // 8.0.3.0 (2021/12/17)
1829                                                        styleStr = styleStr.replace( STYLE_NAME_START_TAG + styleName, STYLE_NAME_START_TAG + styleName + "_" + newName );
1830                                                        bw.write( styleStr );
1831                                                        isReplace = true;
1832                                                }
1833                                                break;
1834                                        }
1835                                }
1836                                if( !isReplace ) {
1837                                        bw.write( tags[i] );
1838                                }
1839                        }
1840
1841                        bw.write( footer );
1842                        bw.flush();
1843                }
1844                catch( final IOException ex ) {
1845                        queue.addMsg( "[ERROR]PARSE:error occurer while write ReParsed Sheet " + fileName );
1846                        throw new HybsSystemException( ex );
1847                }
1848                finally {
1849                        Closer.ioClose( bw );
1850                }
1851        }
1852
1853        /**
1854         * content.xmlのヘッダー部分を出力したcontent.xmlに、ヘッダー部分以降を出力した content.xml.bakをマージします。(5)
1855         *
1856         * @og.rev 5.2.2.0 (2010/11/01) 条件付書式対応
1857         */
1858        private void execMergeContent() {
1859                FileChannel srcChannel = null;
1860                FileChannel destChannel = null;
1861                try {
1862                        srcChannel = new FileInputStream( path + "content.xml.tmp" ).getChannel();
1863                        destChannel = new FileOutputStream( path + "content.xml", true ).getChannel();
1864                        srcChannel.transferTo(0, srcChannel.size(), destChannel);
1865                }
1866                catch( final IOException ex ) {
1867                        queue.addMsg( "[ERROR]PARSE:error occurer while merge content.xml" );
1868                        throw new HybsSystemException( ex );
1869                }
1870                finally {
1871                        Closer.ioClose( srcChannel );
1872                        Closer.ioClose( destChannel );
1873                }
1874                FileUtil.deleteFiles( new File( path + "content.xml.tmp" ) );
1875        }
1876
1877        /**
1878         * META-INF/manifest.xmlに、追加したオブジェクト(グラフ、画像)を登録します。(7)
1879         *
1880         * @og.rev 5.3.1.0 (2011/12/01) 新規作成
1881         */
1882        private void execManifest() {
1883//              final String fileName = path + "META-INF" + File.separator + "manifest.xml";
1884                final String fileName = path + "META-INF" + FS + "manifest.xml";        // 8.0.3.0 (2021/12/17)
1885                final String[] conArr = TagParser.tag2Array( readOOoXml( fileName ), MANIFEST_START_TAG, MANIFEST_END_TAG );
1886
1887                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
1888                buf.append( conArr[0] );
1889                for( int i=2; i<conArr.length; i++ ) {
1890                        buf.append( conArr[i] );
1891                }
1892                for( final Map.Entry<String,String> entry : addObjMap.entrySet() ) {
1893                        if( "graph".equals( entry.getValue() ) ) {
1894                                buf.append( "<manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"" )
1895                                        .append( entry.getKey() )
1896                                        .append( "/content.xml\"/><manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"" )
1897                                        .append( entry.getKey() )
1898                                        .append( "/styles.xml\"/><manifest:file-entry manifest:media-type=\"text/xml\" manifest:full-path=\"" )
1899                                        .append( entry.getKey() )
1900                                        .append( "/meta.xml\"/><manifest:file-entry manifest:media-type=\"application/vnd.oasis.opendocument.chart\" manifest:full-path=\"" )
1901                                        .append( entry.getKey() ).append( "/\"/>" );                                            // XML なので、このまま。
1902                        }
1903                        else {
1904                                buf.append( "<manifest:file-entry manifest:media-type=\"image/" )
1905                                        .append( entry.getValue() ).append( "\" manifest:full-path=\"" )
1906                                        .append( entry.getKey() ).append( "\"/>" );                                                     // XML なので、このまま。
1907                        }
1908                }
1909                buf.append( conArr[1] );
1910
1911                writeOOoXml( fileName, buf.toString() );
1912        }
1913
1914        /**
1915         * XMLファイルを読み取り、結果を返します。
1916         * OOoのXMLファイルは全て1行めがxml宣言で、2行目が内容全体という形式であるため、
1917         * ここでは、2行目の内容部分を返します。
1918         *
1919         * @og.rev 4.3.6.0 (2009/04/01) meta.xmlでコンテンツの部分が改行されている場合があるため、ループを回して読込み
1920         * @og.rev 6.2.0.0 (2015/02/27) new BufferedReader … を、FileUtil.getBufferedReader … に変更。
1921         * @og.rev 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
1922         * @og.rev 6.5.0.1 (2016/10/21) CharacterCodingException は、OgCharacterException に変換する。
1923         *
1924         * @param fileName      ファイル名
1925         *
1926         * @return 読み取った文字列
1927         * @og.rtnNotNull
1928         */
1929        private String readOOoXml( final String fileName ) {
1930                final File file = new File ( fileName );
1931
1932                BufferedReader br = null;
1933                String tmp = null;
1934                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
1935                try {
1936                        br = FileUtil.getBufferedReader( file, "UTF-8" );               // 6.2.0.0 (2015/02/27)
1937                        xmlHeader = br.readLine();
1938                        while( ( tmp = br.readLine() ) != null ) {                              // 4.3.6.0 (2009/04/01)
1939                                buf.append( tmp );
1940                        }
1941                }
1942                // 6.3.1.0 (2015/06/28) nioを使用すると UTF-8とShuft-JISで、エラーになる。
1943                catch( final CharacterCodingException ex ) {
1944                        final String errMsg = "文字のエンコード・エラーが発生しました。" + CR
1945                                                                +       "  ファイルのエンコードが指定のエンコードと異なります。" + CR
1946                                                                +       " [" + fileName + "] , Encode=[UTF-8]" ;
1947                        throw new OgCharacterException( errMsg,ex );    // 6.5.0.1 (2016/10/21)
1948                }
1949                catch( final IOException ex ) {
1950                        queue.addMsg( "[ERROR]PARSE:Failed to read " + fileName );
1951                        throw new HybsSystemException( ex );
1952                }
1953                finally {
1954                        Closer.ioClose( br );
1955                }
1956
1957                final String str = buf.toString();
1958                if( xmlHeader == null || xmlHeader.isEmpty() || str == null || str.isEmpty() ) {
1959                        queue.addMsg( "[ERROR]PARSE:Maybe " + fileName + " is Broken!" );
1960                        throw new HybsSystemException();
1961                }
1962
1963                return str;
1964        }
1965
1966        /**
1967         * XMLファイルを書き込みます。
1968         * OOoのXMLファイルは全て1行めがxml宣言で、2行目が内容全体という形式であるため、
1969         * ここでは、2行目の内容部分を渡すことで、XMLファイルを作成します。
1970         *
1971         * @param fileName 書き込むXMLファイル名
1972         * @param str 書き込む文字列
1973         */
1974        private void writeOOoXml( final String fileName, final String str ) {
1975                BufferedWriter bw = null;
1976                try {
1977                        bw = getWriter( fileName );
1978                        bw.write( xmlHeader );
1979                        bw.write( '\n' );
1980                        bw.write( str );
1981                        bw.flush();
1982                }
1983                catch( final IOException ex  ) {
1984                        queue.addMsg( "[ERROR]PARSE:Failed to write " + fileName );
1985                        throw new HybsSystemException( ex );
1986                }
1987                finally {
1988                        Closer.ioClose( bw );
1989                }
1990        }
1991
1992        /**
1993         * XMLファイル書き込み用のライターを返します。
1994         *
1995         * @param fileName ファイル名
1996         *
1997         * @return ライター
1998         * @og.rtnNotNull
1999         */
2000        private BufferedWriter getWriter( final String fileName ) {
2001                return getWriter( fileName, false );
2002        }
2003
2004        /**
2005         * XMLファイル書き込み用のライターを返します。
2006         *
2007         * @param fileName ファイル名
2008         * @param append アベンドするか
2009         *
2010         * @return ライター
2011         * @og.rtnNotNull
2012         */
2013        private BufferedWriter getWriter( final String fileName, final boolean append ) {
2014                final File file = new File ( fileName );
2015                BufferedWriter bw;
2016                try {
2017                        bw = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( file, append ), "UTF-8" ) );
2018                }
2019                catch( final UnsupportedEncodingException ex ) {
2020                        queue.addMsg( "[ERROR]PARSE:Input File is written by Unsupported Encoding" );
2021                        throw new HybsSystemException( ex );
2022                }
2023                catch( final FileNotFoundException ex ) {
2024                        queue.addMsg( "[ERROR]PARSE:File not Found" );
2025                        throw new HybsSystemException( ex );
2026                }
2027                return bw;
2028        }
2029
2030        /**
2031         * ファイル名から拡張子(小文字)を求めます。
2032         *
2033         * @param fileName 拡張子を取得する為のファイル名
2034         *
2035         * @return 拡張子(小文字)
2036         */
2037        public static String getSuffix( final String fileName ) {
2038                String suffix = null;
2039                if( fileName != null ) {
2040                        final int sufIdx = fileName.lastIndexOf( '.' );
2041                        if( sufIdx >= 0 ) {
2042                                suffix = fileName.substring( sufIdx + 1 ).toLowerCase( Locale.JAPAN );
2043                        }
2044                }
2045                return suffix;
2046        }
2047}