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