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