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 * "{@" + key + '_' と、'}' の間の文字列を返します。 327 * 328 * '_' を含まない場合は、ゼロ文字列を返します。 329 * ここでは、簡易的に処理しているため、タグ等の文字列が含まれる場合は、 330 * 上手くいかない可能性があります。 331 * 332 * "{@" + key + '_' と、'}' の間の文字列を返します。 333 * '_' が存在しない場合は、空文字列を返します。 334 * "{@" + key が存在しない場合は、null を返します。 335 * "{@" + key が存在しており、'}' が存在しない場合は、Exception が throw されます。 336 * 337 * @og.rev 8.0.3.0 (2021/12/17) 新規追加 338 * 339 * @param row 検索元の文字列 340 * @param key 検索対象のキー 341 * 342 * @return "{@" + 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}