    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}