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 * "{@" + key + '_' と、'}' の間の文字列を返します。 328 * 329 * '_' を含まない場合は、ゼロ文字列を返します。 330 * ここでは、簡易的に処理しているため、タグ等の文字列が含まれる場合は、 331 * 上手くいかない可能性があります。 332 * 333 * "{@" + key + '_' と、'}' の間の文字列を返します。 334 * '_' が存在しない場合は、空文字列を返します。 335 * "{@" + key が存在しない場合は、null を返します。 336 * "{@" + 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 "{@" + 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}