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;
020
021import org.opengion.hayabusa.common.HybsSystemException;
022import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;      // 6.1.0.0 (2014/12/26) refactoring
023import static org.opengion.fukurou.system.HybsConst.CR ;                        // 8.0.3.0 (2021/12/17)
024
025/**
026 * Calc帳票システムでタグのパースを行うためのクラスです。
027 *
028 * 主に開始タグ、終了タグを指定したパースのループ処理を行うための機能を提供します。
029 * 具体的には、{@link #doParse(String, String, String)}により、パース文字列、開始タグ、終了タグを
030 * 指定し、パースを行います。
031 * パース後の文字列は、{@link #doParse(String, String, String)}の戻り値になります。
032 *
033 * パース実行中に、発見された開始タグから終了タグまでの間の文字列の処理は、{@link #exec(String, StringBuilder, int)}を
034 * オーバーライドすることにより定義します。
035 *
036 * また、このクラスでは、パースに必要な各種ユーティリティメソッドについても同様に定義されています。
037 *
038 * @og.group 帳票システム
039 *
040 * @version  4.0
041 * @author   Hiroki.Nakamura
042 * @since    JDK1.6
043 */
044class TagParser {
045        private static final String VAR_START = "{@";           // 8.0.3.0 (2021/12/17) splitSufix で使います。
046        private static final char   VAR_END   = '}';            // 8.0.3.0 (2021/12/17) splitSufix で使います。
047        private static final char   VAR_CON   = '_';            // 8.0.3.0 (2021/12/17) splitSufix,SplitKey で使います。
048
049        private int preOffset   ;
050        private int curOffset   ;
051
052        /**
053         * パース処理を行います。
054         *
055         * パース中に取り出された開始タグから終了タグまでの文字列の処理は、
056         * {@link #exec(String, StringBuilder, int)}で定義します。
057         *
058         * また、isAddTagをtrueにした場合、{@link #exec(String, StringBuilder, int)}に渡される
059         * 文字列に、開始タグ、終了タグが含まれます。
060         * 逆にfalseにした場合は、開始タグ、終了タグを除き、{@link #exec(String, StringBuilder, int)}に渡されます。
061         *
062         * @og.rev 5.2.2.0 (2010/11/01) 読み飛ばしをした場合に、開始タグが書き込まれないバグを修正
063         *
064         * @param content パース対象文字列
065         * @param startTag 開始タグ
066         * @param endTag 終了タグ
067         * @param isAddTag 開始タグ・終了タグを含むか
068         *
069         * @return パース後の文字列
070         * @og.rtnNotNull
071         * @see #exec(String, StringBuilder, int)
072         */
073        public String doParse( final String content, final String startTag, final String endTag, final boolean isAddTag ) {
074                final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
075
076                while( ( preOffset = content.indexOf( startTag, Math.max( preOffset, curOffset ) ) ) >= 0 ) {
077                        buf.append( content.substring( curOffset, preOffset ) );
078                        curOffset = content.indexOf( endTag, preOffset + startTag.length() );
079
080                        if( checkIgnore( preOffset, curOffset ) ) {
081                                if( curOffset < 0 ){
082                                        final String errMsg = "[ERROR]PARSE:開始タグを終了タグの整合性が不正です。" + CR
083                                                                                + "[開始タグ=" + startTag + ":終了タグ=" + endTag + "]";
084                                        throw new HybsSystemException( errMsg );
085                                }
086                                preOffset += startTag.length();
087                                curOffset += endTag.length();
088
089                                String str = null;
090                                if( isAddTag ) {
091                                        str = content.substring( preOffset - startTag.length(), curOffset );
092                                }
093                                else {
094                                        str = content.substring( preOffset, curOffset - endTag.length() );
095                                }
096
097                                exec( str, buf, curOffset );
098                        }
099                        else {
100                                // 5.2.2.0 (2010/11/01) 開始タグが書き込まれないバグを修正
101                                buf.append( startTag );
102                                preOffset += startTag.length();
103                                curOffset = preOffset;
104                        }
105                }
106                buf.append( content.substring( curOffset, content.length() ) );
107
108                return buf.toString();
109        }
110
111        /**
112         * パース処理を行います。
113         *
114         * 詳細は、{@link #doParse(String, String, String, boolean)}のJavadocを参照して下さい。
115         *
116         * @param content パース対象文字列
117         * @param startTag 開始タグ
118         * @param endTag 終了タグ
119         *
120         * @return パース後の文字列
121         * @see #doParse(String, String, String, boolean)
122         */
123        public String doParse( final String content, final String startTag, final String endTag ) {
124                return doParse( content, startTag, endTag, true );
125        }
126
127        /**
128         * 開始タグから終了タグまでの文字列の処理を定義します。
129         *
130         * この実装では、何も処理を行いません。(切り出した文字列はアペンドされません)
131         * サブクラスでオーバーライドして実際の処理を実装して下さい。
132         *
133         * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
134         * @param buf 出力を行う文字列バッファ
135         * @param offset 終了タグのオフセット
136         */
137        protected void exec( final String str, final StringBuilder buf, final int offset ) {
138                // Document empty method 対策
139        }
140
141        /**
142         * 開始タグから終了タグまでの文字列の処理を実行するかどうかを定義します。
143         *
144         * falseが返された場合、何も処理されず({@link #exec(String, StringBuilder, int)}が実行されない)、
145         * 元の文字列がそのまま出力されます。
146         *
147         * @param strOffset 開始タグのオフセット
148         * @param endOffset 終了タグのオフセット
149         *
150         * @return 処理を行うかどうか(true:処理を行う false:処理を行わない)
151         */
152        protected boolean checkIgnore( final int strOffset, final int endOffset ) {
153                return true;
154        }
155
156        /**
157         * パース実行中のoffset値を外部からセットします。
158         *
159         * このメソッドは、{@link #exec(String, StringBuilder, int)}で、処理結果により、offset値を
160         * 進めておく必要がある場合に利用されます。(つまり通常は利用する必要はありません)
161         *
162         * @param offset オフセット
163         * @see #exec(String, StringBuilder, int)
164         */
165        public void setOffset( final int offset ) {
166                curOffset = offset;
167        }
168
169        /**
170         * 引数の文字列を指定された開始タグ、終了タグで解析し配列として返す、ユーティリティメソッドです。
171         *
172         * 開始タグより前の文字列は0番目に、終了タグより後の文字列は1番目に格納されます。
173         * 2番目以降に、開始タグ、終了タグの部分が格納されます。
174         *
175         * @param str           解析する文字列
176         * @param startTag      開始タグ
177         * @param endTag        終了タグ
178         *
179         * @return 解析結果の配列
180         */
181        public static String[] tag2Array( final String str, final String startTag, final String endTag ) {
182                String header = null;
183                String footer = null;
184                final List<String> body = new ArrayList<>();
185
186                int preOffset = -1;
187                int curOffset = 0;
188
189                while( true ) {
190                        curOffset = str.indexOf( startTag, preOffset + 1 );
191                        if( curOffset < 0 ) {
192                                curOffset = str.lastIndexOf( endTag ) + endTag.length();
193                                body.add( str.substring( preOffset, curOffset ) );
194
195                                footer = str.substring( curOffset );
196                                break;
197                        }
198                        else if( preOffset == -1 ) {
199                                header = str.substring( 0, curOffset );
200                        }
201                        else {
202                                body.add( str.substring( preOffset, curOffset ) );
203                        }
204                        preOffset = curOffset;
205                }
206
207                String[] arr = new String[body.size()+2];
208                arr[0] = header;
209                arr[1] = footer;
210                for( int i=0; i<body.size(); i++ ) {
211                        arr[i+2] = body.get(i);
212                }
213
214                return arr;
215        }
216
217        /**
218         * 引数の文字列の開始文字と終了文字の間の文字列を取り出す、ユーティリティメソッドです。
219         * ※返される文字列に、開始文字、終了文字は含まれません。
220         *
221         * @param str   解析する文字列
222         * @param start 開始文字列
223         * @param end   終了文字列
224         *
225         * @return 解析結果の文字
226         */
227        public static String getValueFromTag( final String str, final String start, final String end ) {
228                int startOffset = str.indexOf( start );
229                // 4.2.4.0 (2008/06/02) 存在しない場合はnullで返す
230                if( startOffset == -1 ) {
231                        return null;
232                }
233                startOffset += start.length();
234
235                final int endOffset = str.indexOf( end, startOffset );
236//              final String value = str.substring( startOffset, endOffset );
237
238                // 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX'
239                return str.substring( startOffset, endOffset );
240
241//              return value;
242        }
243
244        /**
245         * 引数のキーから不要なキーを取り除く、ユーティリティメソッドです。
246         *
247         * @og.rev 5.1.8.0 (2010/07/01) spanタグを削除
248         *
249         * @param key   オリジナルのキー
250         * @param sb    キーの外に含まれるaタグを削除するための、バッファ
251         *
252         * @return 削除後のキー
253         * @og.rtnNotNull
254         */
255        public static String checkKey( final String key, final StringBuilder sb ) {
256                if( key.indexOf( '<' ) < 0 && key.indexOf( '>' ) < 0 ) { return key; }
257
258                final StringBuilder rtn = new StringBuilder( key );
259                final String tagEnd = ">";
260                int rtnOffset = -1;
261
262                // <text:a ...>{@XXX</text:a>の不要タグを削除
263                final String delTagStart1 = "<text:a ";
264                final String delTagEnd1 = "</text:a>";
265                while( ( rtnOffset = rtn.lastIndexOf( delTagEnd1 ) ) >= 0 ) {
266                        boolean isDel = false;
267                        // キー自身に含まれるaタグを削除
268                        int startOffset = rtn.lastIndexOf( delTagStart1, rtnOffset );
269                        if( startOffset >= 0 ) {
270                                final int endOffset = rtn.indexOf( tagEnd, startOffset );
271                                if( endOffset >= 0 ) {
272                                        rtn.delete( rtnOffset, rtnOffset + delTagEnd1.length() );
273                                        rtn.delete( startOffset, endOffset + tagEnd.length() );
274                                        isDel = true;
275                                }
276                        }
277                        else {
278                                // キーの外に含まれるaタグを削除
279                                startOffset = sb.lastIndexOf( delTagStart1 );
280                                if( startOffset >= 0 ) {
281                                        final int endOffset = sb.indexOf( tagEnd, startOffset );
282                                        if( endOffset >= 0 ) {
283                                                rtn.delete( rtnOffset, rtnOffset + delTagEnd1.length() );
284                                                sb.delete( startOffset, endOffset + tagEnd.length() );
285                                                isDel = true;
286                                        }
287                                }
288                        }
289                        if( !isDel ) { break; }
290                }
291
292                // 5.1.8.0 (2010/07/01) spanタグを削除
293                final String delTagStart2 = "<text:span ";
294                final String delTagEnd2 = "</text:span>";
295                while( ( rtnOffset = rtn.lastIndexOf( delTagEnd2 ) ) >= 0 ) {
296                        boolean isDel = false;
297                        // キー自身に含まれるspanタグを削除
298                        int startOffset = rtn.lastIndexOf( delTagStart2, rtnOffset );
299                        if( startOffset >= 0 ) {
300                                final int endOffset = rtn.indexOf( tagEnd, startOffset );
301                                if( endOffset >= 0 ) {
302                                        rtn.delete( rtnOffset, rtnOffset + delTagEnd2.length() );
303                                        rtn.delete( startOffset, endOffset + tagEnd.length() );
304                                        isDel = true;
305                                }
306                        }
307                        else {
308                                // キーの外に含まれるspanタグを削除
309                                startOffset = sb.lastIndexOf( delTagStart2 );
310                                if( startOffset >= 0 ) {
311                                        final int endOffset = sb.indexOf( tagEnd, startOffset );
312                                        if( endOffset >= 0 ) {
313                                                rtn.delete( rtnOffset, rtnOffset + delTagEnd2.length() );
314                                                sb.delete( startOffset, endOffset + tagEnd.length() );
315                                                isDel = true;
316                                        }
317                                }
318                        }
319                        if( !isDel ) { break; }
320                }
321
322                return rtn.toString();
323        }
324
325        /**
326         * "{&#064;" + key + '_' と、'}' の間の文字列を返します。
327         *
328         * '_' を含まない場合は、ゼロ文字列を返します。
329         * ここでは、簡易的に処理しているため、タグ等の文字列が含まれる場合は、
330         * 上手くいかない可能性があります。
331         *
332         * "{&#064;" + key + '_' と、'}' の間の文字列を返します。
333         * '_' が存在しない場合は、空文字列を返します。
334         * "{&#064;" + key が存在しない場合は、null を返します。
335         * "{&#064;" + key が存在しており、'}' が存在しない場合は、Exception が throw されます。
336         *
337         * @og.rev 8.0.3.0 (2021/12/17) 新規追加
338         *
339         * @param row   検索元の文字列
340         * @param key   検索対象のキー
341         *
342         * @return "{&#064;" + key + '_' と、'}' の間の文字列を返します。
343         */
344        public static String splitSufix( final String row,final String key ) {
345                final int st1 = row.indexOf( VAR_START + key );                         // "{@" + key
346                if( st1 >= 0 ) {
347                        final int ed1 = row.indexOf( VAR_END,st1 );                             // '}' を探す
348                        if( ed1 >= 0 ) {
349                                final int st2 = row.lastIndexOf​( VAR_CON,ed1 );        // '_' を逆順で探す
350                                if( st2 < 0 ) {         // '_' が無い場合は、空文字列を返す。
351                                        return "";
352                                }
353                                else {
354                                        return row.substring( st2+1,ed1 );                              // '_'の次の文字から、'}' 手前まで
355                                }
356                        }
357                        else {
358                                final String errMsg = "[ERROR]SHEET:{@と}の整合性が不正です。" + CR
359                                                                        + "変数内の特定の文字列に書式設定がされている可能性があります。キー=" + key;
360                                throw new HybsSystemException( errMsg );
361                        }
362                }
363                return null;
364        }
365
366        /**
367         * アンダーバーで、キーと行番号の分離を行います。
368         *
369         * @og.rev 8.0.3.0 (2021/12/17) アンダーバーで、キーと行番号の分離を、インナークラス化します。
370         */
371        /* default */ static final class SplitKey {
372                /** 分割後のキー */
373                public final String name ;
374                /** 分割後の行番号 */
375                public final int        rownum ;
376
377                /**
378                 * コンストラクタで、分割、設定
379                 *
380                 * @param key 分割処理対象のキー
381                 */
382                public SplitKey( final String key ) {
383                        final int idx = key.lastIndexOf( VAR_CON );
384
385                        int num = -1;
386                        if( idx >= 0 ) {
387                                try {
388                                        num = Integer.parseInt( key.substring( idx+1 ) );
389                                }
390                                // '_'以降の文字が数字でない場合は、'_'以降の文字もカラム名の一部として扱う
391                                catch( final NumberFormatException ex ) {
392                                        // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid empty catch blocks
393                                        final String errMsg = "'_'以降の文字をカラム名の一部として扱います。" + CR
394                                                                                        + "カラム名=[" + key + "]" + CR
395                                                                                        + ex.getMessage() ;
396                                        System.err.println( errMsg );
397                                }
398                        }
399                        if( num >= 0 ) {
400                                name   = key.substring( 0, idx );
401                                rownum = num ;
402                        }
403                        else {
404                                name   = key;
405                                rownum = num ;
406                        }
407                }
408
409                /**
410                 * XXX_番号の番号部分を引数分追加して返します。
411                 * 番号部分が数字でない場合や、_が無い場合はそのまま返します。
412                 *
413                 * @param inc   カウンタ部
414                 *
415                 * @return 変更後キー
416                 */
417                public String incrementKey( final int inc ) {
418                        return rownum < 0 ? name : name + VAR_CON + (rownum + inc) ;
419//                      return new StringBuilder().append(name).append(VAR_CON).append( rownum+inc ).toString();
420                }
421
422//              /**
423//               * rownumが無効(-1)ならcntを、有効なら、rownumを返します。
424//               *
425//               * @param cnt   デフォルトのカウント値
426//               *
427//               * @return rownumか、引数のcntを返します。
428//               */
429//              public int count( final int cnt ) {
430//                      return rownum < 0 ? cnt : rownum;
431//              }
432        }
433}