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