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