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.util.ArrayList;
019import java.util.List;
020import java.util.Map;                                                                                           // 8.0.3.0 (2021/12/17)
021import java.util.HashMap;                                                                                       // 8.0.3.0 (2021/12/17)
022
023import org.opengion.hayabusa.common.HybsSystemException;
024import static org.opengion.fukurou.system.HybsConst.CR ;                        // 8.0.3.0 (2021/12/17)
025// import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;   // 8.0.3.0 (2021/12/17)
026
027import org.opengion.hayabusa.report2.TagParser.SplitKey;
028
029/**
030 * シート単位のcontent.xmlを管理するためのクラスです。
031 * シートのヘッダー、行の配列、フッター及びシート名を管理します。
032 *
033 * 7.0.1.5 (2018/12/10)
034 *   FORMAT_LINEでは、同一行をコピーするため、セルの選択行(A5とか$C7とか)までコピーされ
035 *   行レベルの計算が出来ません。その場合は、INDIRECT(ADDRESS(ROW();列番号))関数を
036 *   使用することでセルのアドレスが指定可能です。
037 *   列番号は、A=1 です。
038 *   ※ OpenOffice系は、区切り文字が『;』 EXCELの場合は、『,』 要注意
039 *
040 * ※ 繰り返しを使用する場合で、ヘッダー部分の印刷領域を繰り返したい場合は、
041 *    1.「書式(O)」-「印刷範囲(N)」-「編集(E)」で「印刷範囲の編集」ダイアログを表示
042 *    2.「繰り返す行」の右の方にある矢印のついたボタンをクリック
043 *    3.ページごとに表示したい行をドラックして指定する
044 *        (テキストボックスに$1:$2などと直接入力しても良い->$1:$2の場合であれば1-2行目が繰り返し印刷される)
045 *
046 * 8.0.3.0 (2021/12/17)
047 *    {@FORMATLINE} を指定した行は、BODY(GE51)行のフォーマットを指定できます。
048 *    {@FORMATLINE_1}で、GE51のKBTEXT=B1 で指定した行をひな形にします。
049 *    {@FORMATLINE_2}で、GE51のKBTEXT=B2 です。
050 *    引数の数字を指定しない場合は、KBTEXT=B です。
051 *    {@DUMMYLINE} は、先のフォーマット行をその行と交換して出力します。
052 *    ただし、データが存在しない場合は、このDUMMYLINEそのものが使用されます。
053 *    {@COPYLINE} は、先のフォーマット行をデータの数だけ繰り返しその場にコピーします。
054 *    イメージ的には、DUMMYLINE は、1ページ分のフォーマットを指定する場合、COPYLINE は
055 *    無制限の連続帳票を想定しています。
056 *
057 * @og.group 帳票システム
058 *
059 * @version  4.0
060 * @author   Hiroki.Nakamura
061 * @since    JDK1.6
062 */
063class OdsSheet {
064
065        //======== content.xmlのパースで使用 ========================================
066
067        /* 行の開始終了タグ */
068        private static final String ROW_START_TAG = "<table:table-row ";
069        private static final String ROW_END_TAG = "</table:table-row>";
070
071        /* シート名を取得するための開始終了文字 */
072        private static final String SHEET_NAME_START = "table:name=\"";
073//      private static final String SHEET_NAME_END = "\"";
074        private static final String END_KEY = "\"";                             // 8.0.3.0 (2021/12/17)
075
076        /* 変数定義の開始終了文字及び区切り文字 */
077        private static final String VAR_START = "{@";
078        private static final String VAR_END = "}";
079//      private static final String VAR_CON = "_";
080
081        /* フォーマットライン文字列 5.0.0.2 (2009/09/15) */
082        private static final String FORMAT_LINE = "FORMATLINE"; // 8.0.3.0 (2021/12/17)
083
084        /* ダミーライン文字列 8.0.3.0 (2021/12/17) */
085        private static final String DUMMY_LINE = "DUMMYLINE";   // 8.0.3.0 (2021/12/17)
086
087        /* コピーライン文字列 8.0.3.0 (2021/12/17) */
088        private static final String COPY_LINE = "COPYLINE";             // 8.0.3.0 (2021/12/17)
089
090        private final List<String>                      sheetRows       = new ArrayList<>();
091        private final Map<String,String>        rowsMap         = new HashMap<>();
092        private int                     offsetCnt = -1;         // 8.0.3.0 (2021/12/17) FORMAT_LINE が最初に現れた番号
093        private String[]        bodyTypes ;                     // 8.0.3.0 (2021/12/17) 行番号に対応した、ボディタイプ(KBTEXT)配列
094
095        private String          sheetHeader;
096        private String          sheetFooter;
097        private String          sheetName;
098        private String          origSheetName;
099        private String          confSheetName;
100
101        /**
102         * デフォルトコンストラクター
103         *
104         * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
105         */
106        public OdsSheet() { super(); }          // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
107
108        /**
109         * シートを行単位に分解します。
110         *
111         * @og.rev 5.0.0.2 (2009/09/15) ボディ部のカウントを引数に追加し、LINECOPY機能実装。
112         * @og.rev 5.2.1.0 (2010/10/01) シート名定義対応
113         * @og.rev 8.0.3.0 (2021/12/17) COPY_LINE機能の追加
114         *
115         * @param sheet シート名
116         * @param bodyTypes 行番号に対応した、ボディタイプ(KBTEXT)配列
117         */
118//      public void analyze( final String sheet, final int bodyRowCount ) {
119        public void analyze( final String sheet, final String[] bodyTypes ) {
120                this.bodyTypes = bodyTypes ;
121
122                final String[] tags = TagParser.tag2Array( sheet, ROW_START_TAG, ROW_END_TAG );
123                sheetHeader = tags[0];
124                sheetFooter = tags[1];
125                for( int i=2; i<tags.length; i++ ) {
126//                      sheetRows.add( tags[i] );
127//                      lineCopy( tags[i], bodyRowCount );      // 5.0.0.2 (2009/09/15)
128                        lineCopy( tags[i] );                            // 8.0.3.0 (2021/12/17)
129                }
130
131//              sheetName = TagParser.getValueFromTag( sheetHeader, SHEET_NAME_START, SHEET_NAME_END );
132                sheetName = TagParser.getValueFromTag( sheetHeader, SHEET_NAME_START, END_KEY );        // 8.0.3.0 (2021/12/17)
133                origSheetName = sheetName;
134
135                confSheetName = null;
136                if( sheetName != null ) {
137                        final int cnfIdx = sheetName.indexOf( "__" );
138                        if( cnfIdx > 0 && !sheetName.endsWith( "__" ) ) {
139                                confSheetName = sheetName.substring( cnfIdx + 2 );
140                                sheetName = sheetName.substring( 0, cnfIdx );
141                        }
142                }
143        }
144
145        /**
146         * ラインコピーに関する処理を行います。
147         *
148         * {&#064;LINECOPY}が存在した場合に、テーブルモデル分だけ
149         * 行をコピーします。
150         * その際、{&#064;XXX_番号}の番号をカウントアップしてコピーします。
151         *
152         * 整合性等のエラーハンドリングはこのメソッドでは行わず、
153         * 実際のパース処理中で行います。
154         *
155         * 7.0.1.5 (2018/12/10)
156         *   LINECOPYでは、同一行をコピーするため、セルの選択行(A5とか$C7とか)までコピーされ
157         *   行レベルの計算が出来ません。その場合は、INDIRECT(ADDRESS(ROW(),列番号))関数を
158         *   使用することでセルのアドレスが指定可能です。
159         *   列番号は、A=1 です。
160         *
161         * @og.rev 5.0.0.2 (2009/09/15) 追加
162         * @og.rev 5.1.8.0 (2010/07/01) パース処理の内部実装を変更
163         * @og.rev 7.0.1.5 (2018/12/10) LINECOPYでの注意(JavaDocのみ追記)
164         * @og.rev 8.0.3.0 (2021/12/17) TagParser.SplitKey#incrementKey(int) に処理を移します。
165         * @og.rev 8.1.1.1 (2022/02/18) FORMAT_LINEは、無視します。
166         *
167         * @param rowStr        行データ
168         * @param rowCount      カウンタ
169         */
170//      private void lineCopy( final String rowStr, final int rowCount ) {
171        private void lineCopy( final String rowStr ) {
172                // FORMAT_LINE を見つけて、引数をキーにマップに登録します。
173                final String cpLin = TagParser.splitSufix( rowStr,FORMAT_LINE );
174                if( cpLin != null ) {
175                        if( offsetCnt < 0 ) { offsetCnt = sheetRows.size(); }   // 初めてあらわれた位置
176                        final String tmp = rowsMap.get( "B" + cpLin );
177                        if( tmp == null ) {
178                                rowsMap.put( "B" + cpLin , rowStr );            // フォーマットのキーは、"B" + サフィックス
179        //                      sheetRows.add( rowStr );                                        // 8.1.1.1 (2022/02/18) FORMAT_LINEは、無視します。
180                        }
181                        else {
182                                // セル結合時に、複数行を1行に再設定する。
183                                rowsMap.put( "B" + cpLin , tmp + rowStr );      // フォーマットのキーは、"B" + サフィックス
184                        }
185                        return;
186                }
187
188                // DUMMY_LINE を見つける。
189                final int st1 = rowStr.indexOf( VAR_START + DUMMY_LINE );
190                if( st1 >= 0 ) {                                                // キーが見つかった場合
191                        if( offsetCnt < 0 ) { offsetCnt = sheetRows.size(); }   // 初めてあらわれた位置
192                        sheetRows.add( rowStr );                        // DUMMY_LINE を登録
193                        return ;
194                }
195
196                // COPY_LINE を見つける。
197                final int st2 = rowStr.indexOf( VAR_START + COPY_LINE );
198                if( st2 >= 0 ) {                                                // キーが見つかった場合
199                        if( offsetCnt < 0 ) { offsetCnt = sheetRows.size(); }   // 初めてあらわれた位置
200
201                        // COPY_LINEは、その場に全件コピーします(行数を確保するため)
202                        for( int row=0; row<bodyTypes.length; row++ ) {
203                                sheetRows.add( rowStr );                // COPY_LINE を登録
204                        }
205                        return ;
206                }
207
208                sheetRows.add( rowStr );                                // rowStr を登録(通常行)
209
210//              // この段階で存在しなければ即終了
211//              final int lcStr = row.indexOf( VAR_START + LINE_COPY );
212////            if( lcStrOffset < 0 ) { return; }
213//              if( lcStr < 0 ) { sheetRows.add( row ); return 1; }
214//              final int lcEnd = row.indexOf( VAR_END, lcStr );
215////            if( lcEndOffset < 0 ) { return; }
216//              if( lcEnd < 0 ) { sheetRows.add( row ); return 1; }
217//
218//              final StringBuilder lcStrBuf = new StringBuilder( row );
219//              final String lcKey = TagParser.checkKey( row.substring( lcStr + VAR_START.length(), lcEnd ), lcStrBuf );
220////            if( lcKey == null || !LINE_COPY.equals( lcKey ) ) { return; }
221//              final SplitKey cpKey = new SplitKey( lcKey );           // 8.0.3.0 (2021/12/17)
222//              final int copyCnt = cpKey.count( rowCount );
223
224//              // 存在すればテーブルモデル行数-1回ループ(自身を除くため)
225//              for( int i=1; i<rowCount; i++ ) {
226//              // 存在すればテーブルモデル行数回ループ(自身も含める必要がある)
227//              for( int i=0; i<copyCnt; i++ ) {                                        // {@LINECOPY_回数} で、繰り返し回数指定
228//                      final int cRow = i;                                                             // final 宣言しないと無名クラスに設定できない。
229//                      final String rowStr = new TagParser() {
230//                              /**
231//                               * 開始タグから終了タグまでの文字列の処理を定義します。
232//                               *
233//                               * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
234//                               * @param buf 出力を行う文字列バッファ
235//                               * @param offset 終了タグのオフセット(ここでは使っていません)
236//                               */
237//                              @Override
238//                              protected void exec( final String str, final StringBuilder buf, final int offset ) {
239//                                      String key = TagParser.checkKey( str, buf );
240//                                      if( key.indexOf( '<' ) >= 0 ){
241//                                              final String errMsg = "[ERROR]SHEET:{@と}の整合性が不正です。" + CR
242//                                                                                      + "変数内の特定の文字列に書式設定がされている可能性があります。キー=" + key;
243//                                              throw new HybsSystemException( errMsg );
244//                                      }
245//                                      final SplitKey spKey = new SplitKey( key );             // 8.0.3.0 (2021/12/17)
246////                                    buf.append( VAR_START ).append( incrementKey( key, cRow ) ).append( VAR_END );
247//                                      buf.append( VAR_START ).append( spKey.incrementKey( cRow ) ).append( VAR_END );
248//                              }
249//                      }.doParse( lcStrBuf.toString(), VAR_START, VAR_END, false );
250//
251//                      sheetRows.add( rowStr );
252//              }
253//              return copyCnt;
254        }
255
256//      /**
257//       * XXX_番号の番号部分を引数分追加して返します。
258//       * 番号部分が数字でない場合や、_が無い場合はそのまま返します。
259//       *
260//       * @og.rev 5.0.0.2 LINE_COPYで利用するために追加
261//       * @og.rev 8.0.3.0 (2021/12/17) TagParser.SplitKey#incrementKey(int) に処理を移します。
262//       *
263//       * @param key   キー文字列
264//       * @param inc   カウンタ部
265//       *
266//       * @return 変更後キー
267//       */
268//      private String incrementKey( final String key, final int inc ) {
269//              final int conOffset = key.lastIndexOf( VAR_CON );
270//              if( conOffset < 0 ) { return key; }
271//
272//              final String name = key.substring( 0, conOffset );
273//              int rownum = -1;
274//              try {
275//                      rownum = Integer.parseInt( key.substring( conOffset + VAR_CON.length(), key.length() ) );               // 6.0.2.4 (2014/10/17) メソッド間違い
276//              }
277//              // エラーが起きてもなにもしない。
278//              catch( final NumberFormatException ex ) {
279//                      // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid empty catch blocks
280//                      final String errMsg = "Not incrementKey. KEY=[" + key + "] " + ex.getMessage() ;
281//                      System.err.println( errMsg );
282//              }
283//
284//              // アンダースコア後が数字に変換できない場合はヘッダフッタとして認識
285//              if( rownum < 0 ){ return key; }
286//              else                    { return name + VAR_CON + (rownum + inc) ; }
287//      }
288
289        /**
290         * シートのヘッダー部分を返します。
291         *
292         * @return ヘッダー
293         */
294        public String getHeader() {
295                return sheetHeader;
296        }
297
298        /**
299         * シートのフッター部分を返します。
300         *
301         * @return フッター
302         */
303        public String getFooter() {
304                return sheetFooter;
305        }
306
307        /**
308         * シート名称を返します。
309         *
310         * @return シート名称
311         */
312        public String getSheetName() {
313                return sheetName;
314        }
315
316        /**
317         * 定義済シート名称を返します。
318         *
319         * @og.rev 5.2.1.0 (2010/10/01) シート名定義対応
320         *
321         * @return 定義済シート名称
322         */
323        public String getConfSheetName() {
324                return confSheetName;
325        }
326
327        /**
328         * 定義名変換前のシート名称を返します。
329         *
330         * @og.rev 5.2.1.0 (2010/10/01) シート名定義対応
331         *
332         * @return 定義済シート名称
333         */
334        public String getOrigSheetName() {
335                return origSheetName;
336        }
337
338//      /**
339//       * シートの各行を配列で返します。
340//       *
341//       * @og.rev 4.3.1.1 (2008/08/23) あらかじめ、必要な配列の長さを確保しておきます。
342//       * @og.rev 8.0.3.0 (2021/12/17) 廃止
343//       *
344//       * @return シートの各行の配列
345//       * @og.rtnNotNull
346//       */
347//      public String[] getRows() {
348//              return sheetRows.toArray( new String[sheetRows.size()] );
349//      }
350
351        /**
352         * シートの行を返します。
353         *
354         * @og.rev 8.0.3.0 (2021/12/17) 新規追加
355         * @og.rev 8.1.1.1 (2022/02/18) FORMAT_LINEは、無視します。
356         *
357         * @param idx           シート内での行番号
358         * @param baseRow       TableModelのベース行番号
359         *
360         * @return シートの行
361         * @og.rtnNotNull
362         */
363        public String getRow( final int idx, final int baseRow ) {
364                final String rowStr = sheetRows.get( idx );
365
366//              8.1.1.1 (2022/02/18) FORMAT_LINEは、無視します。
367//              final boolean useFmt = rowStr.contains( VAR_START + FORMAT_LINE )
368//                                                      || rowStr.contains( VAR_START + DUMMY_LINE )
369//                                                      || rowStr.contains( VAR_START + COPY_LINE ) ;
370
371                final boolean useFmt = rowStr.contains( VAR_START + DUMMY_LINE )
372                                                        || rowStr.contains( VAR_START + COPY_LINE ) ;
373
374                if( useFmt ) {                          // キーが見つかった場合
375                        final int row = idx-offsetCnt+baseRow;
376
377                        final String dummy = row < bodyTypes.length                     // 配列overチェック
378                                                        ? rowsMap.getOrDefault( bodyTypes[row],rowStr ) // 存在しなかった場合の処置
379                                                        : rowStr ;
380
381                        return new TagParser() {
382                                /**
383                                 * 開始タグから終了タグまでの文字列の処理を定義します。
384                                 *
385                                 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
386                                 * @param buf 出力を行う文字列バッファ
387                                 * @param offset 終了タグのオフセット(ここでは使っていません)
388                                 */
389                                @Override
390                                protected void exec( final String str, final StringBuilder buf, final int offset ) {
391                                        final String key = TagParser.checkKey( str, buf );
392                                        if( key.indexOf( '<' ) >= 0 ){
393                                                final String errMsg = "[ERROR]SHEET:{@と}の整合性が不正です。" + CR
394                                                                                        + "変数内の特定の文字列に書式設定がされている可能性があります。キー=" + key;
395                                                throw new HybsSystemException( errMsg );
396                                        }
397                                        final SplitKey spKey = new SplitKey( key );             // 8.0.3.0 (2021/12/17)
398                                        buf.append( VAR_START ).append( spKey.incrementKey( idx-offsetCnt ) ).append( VAR_END );
399                                }
400                        }.doParse( dummy, VAR_START, VAR_END, false );
401                }
402                return rowStr;
403        }
404
405        /**
406         * シートに含まれている行数を返します。
407         *
408         * @og.rev 8.0.3.0 (2021/12/17) 新規追加
409         *
410         * @return シートに含まれている行数
411         */
412        public int getRowCnt() {
413                return sheetRows.size();
414        }
415}