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