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 */ 016 package org.opengion.hayabusa.report; 017 018 import org.opengion.hayabusa.common.HybsSystem; 019 import org.opengion.hayabusa.common.HybsSystemException; 020 import org.opengion.fukurou.util.LogWriter; 021 022 import org.opengion.fukurou.util.QrcodeImage; 023 import org.opengion.fukurou.util.ReplaceString; 024 025 import java.io.IOException; 026 import java.util.Map ; 027 import java.util.HashMap ; 028 import java.util.regex.Pattern; 029 import java.util.regex.Matcher ; 030 031 /** 032 * DBTableReport インターフェース を実?たHTMLをパースするクラスです? 033 * AbstractDBTableReport を継承して?す?で?writeReport() のみオーバ?ライドして?? 034 * 固定長?ファイルの出力機?を実現して?す? 035 * 036 * @og.group 帳票シス? 037 * 038 * @version 4.0 039 * @author Kazuhiko Hasegawa 040 * @since JDK5.0, 041 */ 042 public class DBTableReport_HTML extends AbstractDBTableReport { 043 private static final String TR_IN = "<tr" ; 044 private static final String TR_OUT = "</tr>" ; 045 private static final String TD_OUT = "</td>" ; // 3.5.5.9 (2004/06/07) 046 private static final String PAGE_BREAK = "page-break" ; 047 private static final String PAGE_END_CUT = "PAGE_END_CUT" ; // 3.6.0.0 (2004/09/17) 048 private static final String END_TAG = "</table></body></html>"; 049 private static final String CUT_TAG1 = "<span"; 050 private static final String CUT_TAG2 = "</span>"; 051 private static final String SPACE_ST = "<span style=\"mso-spacerun: yes\">"; // 3.6.0.0 (2004/09/17) 052 private static final String SPACE = " "; // 3.6.0.0 (2004/09/17) 053 private static final String SPACE_ED = " </span>"; // 3.6.0.0 (2004/09/17) 054 private static final String FRAMESET = "Excel Workbook Frameset" ; 055 056 private static final String CR = System.getProperty("line.separator"); 057 058 // <td xxx="yyy">zzzz</td> 形式とマッチし?zzzz< 部?前方参?します? 059 private static final Pattern PTN1 = Pattern.compile("<td[^>]*(>.*?<)/td>"); 060 // >aaaa<span bb="cc">dddd</span>eeee< 形式に?文字以上?スペ?スを含?ータと 061 // マッチし、aaaa,dddd,eeee を前方参?します? 062 private static final Pattern PTN2 = Pattern.compile("[^>]*>([^<]*? ++[^<]*?)<"); 063 // aa bb cc 形式とマッチし、各連続スペ?ス部?前方参?します? 064 private static final Pattern PTN3 = Pattern.compile("( +)"); 065 066 private boolean fileEnd = false; // ファイルの読み取り制御 067 068 // 3.6.1.0 (2005/01/05) QRコー??次?ーコー?用の出力ファイル管? 069 private Map<String,String> qrFileMap = null; 070 // <v:shape ??? alt="{@QRCODE.XXXX}" ???> 071 // <v:imagedata src="yyy" ???>???</v:shape>形式とマッチし? 072 // xxx 部?、yyy 部?前方参?します? 073 private static final Pattern IMGPTN1 = Pattern.compile("<v:shape [^>]*alt=\"\\{@QRCODE.([^\\}]*)\\}\"[^>]*>[^<]*<v:imagedata [^>]*src=\"([^\"]*)\"[^>]*>"); 074 // <img ??? src="yyy" ??? alt="{@QRCODE.XXXX}" ??? > 形式とマッチし? 075 // yyy 部?、xxx 部?前方参?します? 076 private static final Pattern IMGPTN2 = Pattern.compile("<img [^>]*src=\"([^\"]*)\"[^>]*alt=\"\\{@QRCODE.([^\\}]*)\\}\"[^>]*>"); 077 078 // 4.0.0 (2007/06/08) pageEndCut = true 時? LINE_COPY 機?の実? 079 private static final String LINE_COPY = "LINE_COPY" ; // 4.0.0 (2007/06/08) 080 private String lineCopy = null; 081 082 // 5.7.1.0 (2013/12/06) trueの場?PAGE_END_CUTの判定にdataOver フラグを使用。falseの場合?、rowOver を使用? 083 private boolean USE_DATAOVER = HybsSystem.sysBool( "COMPATIBLE_PAGE_END_CUT_RETRIEVAL" ); 084 085 /** 086 * 入力文字? を読み取って、?力します? 087 * tr タグを目印に??trタグ?ずつ取り出します? 088 * 読み取りを終?る?合?、null を返します? 089 * ?ブクラスで実?てください? 090 * 091 * @og.rev 3.0.0.1 (2003/02/14) ?もValueセ?して???に次ペ?ジ要求があった?合?、フォーマットがおかしい 092 * @og.rev 3.6.0.0 (2004/09/24) フォーマットエラーの判?formatErr)を?親クラスに移動します? 093 * 094 * @return 出力文字? 095 */ 096 @Override 097 protected String readLine() { 098 if( fileEnd ) { return null; } 099 100 // pageEndCut 時に、データがオーバ?して??のみ、lineCopy があれ?返す? 101 if( pageEndCut && !rowOver && lineCopy != null ) { 102 lineCopyCnt ++ ; // 雛形は、_0 のみが毎回返される為の、加? 103 return lineCopy ; 104 } 105 106 final StringBuilder buf ; 107 try { 108 String line = reader.readLine(); 109 if( line == null ) { 110 if( rowOver ) { 111 return null; 112 } 113 else { 114 initReader(); 115 initWriter(); 116 line = reader.readLine(); 117 if( line == null ) { return null; } 118 } 119 } 120 if( line.indexOf( FRAMESET ) >= 0 ) { 121 String errMsg = "HTML ファイルエラー :" + line + HybsSystem.CR 122 + "Excelファイル形式がフレー?なって?す?(?シートには未対?" ; 123 throw new HybsSystemException( errMsg ); 124 } 125 if( line.indexOf( TR_IN ) >= 0 ) { 126 buf = new StringBuilder( HybsSystem.BUFFER_MIDDLE ); 127 buf.append( line ); 128 int trLebel = 1; // 行を表?<tr> のレベル 129 while( trLebel != 0 ) { 130 line = reader.readLine(); 131 // 4.0.0 (2005/08/31) null 参?はずし対? 132 if( line != null ) { 133 if( line.indexOf( TR_IN ) >= 0 ) { trLebel++ ; } 134 if( line.indexOf( TR_OUT ) >= 0 ) { trLebel-- ; } 135 buf.append( CR ).append( line ); 136 } 137 else { 138 String errMsg = "HTML ファイルエラー :" + buf.toString() + HybsSystem.CR 139 + "?TR)の整合?が取れる前に、ファイルが終?ました? ; 140 throw new HybsSystemException( errMsg ); 141 } 142 } 143 } 144 else { 145 return line; 146 } 147 } catch(IOException ex) { 148 String errMsg = "HTML ファイル 読取時にエラーが発生しました? + reader; 149 throw new HybsSystemException( errMsg,ex ); // 3.5.5.4 (2004/04/15) 引数の並び?更 150 } 151 152 String rtnLine = buf.toString() ; 153 154 // lineCopy ??の取得? 155 if( pageEndCut && !rowOver ) { 156 // LINE_COPY は削除します?で、表示上見えるよ?しておいてください? 157 int adrs = rtnLine.indexOf( LINE_COPY ); 158 if( adrs >= 0 ) { 159 lineCopy = rtnLine.substring( 0,adrs ) 160 + rtnLine.substring( adrs + LINE_COPY.length() ) ; 161 rtnLine = lineCopy ; 162 } 163 } 164 165 return rtnLine ; 166 } 167 168 /** 169 * 入力文字? を加工して、?力します? 170 * {@XXXX} をテーブルモ?より読み取り、?をセ?します? 171 * ?ブクラスで実?てください? 172 * 173 * @og.rev 3.0.0.1 (2003/02/14) ?もValueセ?して???に次ペ?ジ要求があった?合?、フォーマットがおかしい 174 * @og.rev 3.0.0.2 (2003/02/20) {@XXXX}?が、EXCELに表示しきれな??合に挿入されるタグの削除処??変更? 175 * @og.rev 3.5.0.0 (2003/09/17) {@XXXX}??スペ?スを?&nbsp;と置き換えます? 176 * @og.rev 3.5.0.0 (2003/09/17) {@XXXX}?がアンバランス時にHybsSystemExceptionを発行する? 177 * @og.rev 3.5.5.9 (2004/06/07) {@XXXX}の連続???アドレス計算方法が?違って?したので修正します? 178 * @og.rev 3.6.0.0 (2004/09/17) pageEndCut ?true の場合?、PAGE_END_CUT ??のある行を削除します? 179 * @og.rev 3.6.0.0 (2004/09/24) フォーマットエラーの判?formatErr)を?親クラスに移動します? 180 * @og.rev 3.6.1.0 (2005/01/05) QRコー??次?ーコー?の機?追? 181 * @og.rev 3.8.1.2 (2005/12/19) PAGE_END_CUTの判定にdataOver フラグを使用? 182 * @og.rev 5.7.1.0 (2013/12/06) USE_DATAOVER ?trueの場?PAGE_END_CUTの判定にdataOver フラグを使用。falseの場合?、rowOver を使用 183 * 184 * @param inLine 入力文字? 185 * 186 * @return 出力文字? 187 */ 188 @Override 189 protected String changeData( final String inLine ) { 190 // rowOver で、かつ ペ?ジブレークか?ージエンドカ?の場合?処??? 191 if( rowOver && ( inLine.indexOf( PAGE_BREAK ) >= 0 ) ) { 192 fileEnd = true; 193 return END_TAG; 194 } 195 196 String chLine = changeHeaderFooterData( inLine ) ; 197 198 // 3.6.1.0 (2005/01/05) QRコー??次?ーコー?の機?追? 199 if( chLine.indexOf( "{@QRCODE." ) >= 0 ) { 200 chLine = qrcodeReplace( chLine ); 201 } 202 203 int st = chLine.indexOf( "{@" ); 204 // 3.8.1.2 (2005/12/19) {@XXXX}の存在しな?も PAGE_END_CUTの判定を行う? 205 206 StringBuilder buf = new StringBuilder( chLine ); 207 208 boolean spaceInFlag = false; // {@XXXX} 変数の??タにスペ?スを含?ど?チェ? 209 while( st >= 0 ) { 210 int end = buf.indexOf( "}",st+2 ); 211 212 // EXCELに表示しきれな?字?、CUT_TAG1,CUT_TAG2 が挿入されてしま?? 213 // 削除する?がある? 214 int cutSt1 = buf.indexOf( CUT_TAG1,st+2 ); 215 if( cutSt1 >= 0 && cutSt1 < end ) { 216 int cutEnd1 = buf.indexOf( ">",cutSt1 ); 217 218 int cutSt2 = buf.indexOf( CUT_TAG2,end ); 219 if( cutSt2 >= 0 ) { 220 buf.delete( cutSt2, cutSt2 + CUT_TAG2.length() ); 221 } 222 buf.delete( cutSt1, cutEnd1+1 ); 223 // 途中をカ?した為、も??計算しなおし? 224 end = buf.indexOf( "}",st+2 ); // 3.5.5.9 (2004/06/07) 225 } 226 227 // 3.5.5.9 (2004/06/07) 228 // 関数等を使用すると、{@XXXX} ??を直接?した??タが?力される? 229 // こ??されたデータは、HTML 表示に使用されるだけ?ため、削除します? 230 // 削除方法?、{@XXX</td> を想定して?為?{@ から </td> の間です? 231 int td_out = buf.indexOf( TD_OUT,st+2 ); 232 if( td_out >= 0 && td_out < end ) { 233 buf.delete( st, td_out ); 234 // {@XXXX} パラメータが消えた?で、次の計算を行います? 235 st = buf.indexOf( "{@",st+4 ); // 3.5.5.9 (2004/06/07) 236 continue ; 237 } 238 239 // 途中をカ?した為、も??計算しなおし? 240 // フォーマットがおかしい場合?処? 241 if( end < 0 ) { 242 String errMsg = "こ??プレートファイルの {@XXXX} が?フォーマットエラーです?" 243 + HybsSystem.CR 244 + chLine.substring( st ) ; 245 throw new HybsSystemException( errMsg ); 246 } 247 248 String key = buf.substring( st+2,end ); 249 250 String val = getValue( key ); 251 if( val.indexOf( " " ) >= 0 ) { spaceInFlag = true; } 252 253 // {@XXXX} ?実際の値と置き換える? 254 buf.replace( st,end+1,val ); 255 256 // {@ の 存在チェ?? 257 st = buf.indexOf( "{@",st-1 ); // 3.5.5.9 (2004/06/07) 258 } 259 260 // 3.6.0.0 (2004/09/17) pageEndCut ?true の場合?、PAGE_END_CUT ??のある行を削除します? 261 // ここで判定する?は、PAGE_END_CUT ?そのも?が??されて?可能性があるため? 262 String rtn = buf.toString(); 263 264 boolean flag = (USE_DATAOVER) ? dataOver : rowOver ; // 5.7.1.0 (2013/12/06) 265 266 // if( dataOver && pageEndCut ) { // 3.8.1.2 (2005/12/19) 267 if( flag && pageEndCut ) { // 5.7.1.0 (2013/12/06) 268 String temp = rtn.replaceAll( CUT_TAG1 + "[^>]*>" ,"" ); 269 if( temp.indexOf( PAGE_END_CUT ) >= 0 ) { 270 rtn = "" ; 271 } 272 } 273 else { 274 // 3.6.0.0 (2004/09/17) スペ?ス置き換え??td XXX>YYY</td> の YYYの?のみとする? 275 if( spaceInFlag ) { 276 rtn = spaceReplace( rtn ) ; 277 } 278 } 279 return rtn ; 280 } 281 282 /** 283 * ?殊??? 284 * EXCEL の ヘッ??/フッター部??、\{\@XXXX\} と、エスケープ文字が付加され? 285 * ので、この??を見つけたら?{@XXXX} に、戻して処?るよ?する? 286 * 287 * @param inLine 入力文字? 288 * 289 * @return 出力文字? 290 */ 291 private String changeHeaderFooterData( final String inLine ) { 292 int st = inLine.indexOf( "\\{\\@" ); 293 if( st < 0 ) { return inLine; } 294 295 StringBuilder buf = new StringBuilder( inLine ); 296 297 while( st >= 0 ) { 298 buf.deleteCharAt( st ); // 初めの '\' 299 buf.deleteCharAt( st+1 ); // ?文字削除して?為?1 番目を削除 300 int end = buf.indexOf( "\\}",st+2 ); 301 // フォーマットがおかしい場合?処? 302 if( end < 0 ) { 303 String errMsg = "こ??プレート? HeaderFooter 部?? {@XXXX} が?書式エラーです?" 304 + HybsSystem.CR 305 + inLine ; 306 throw new HybsSystemException( errMsg ); 307 } 308 buf.deleteCharAt( end ); // 初めの '\' 309 st = buf.indexOf( "\\{\\@",end + 1 ); 310 } 311 return buf.toString(); 312 } 313 314 /** 315 * 入力文字? を読み取って、?力します? 316 * ?ブクラスで実?てください? 317 * 318 * @param line 入力文字? 319 */ 320 @Override 321 protected void println( final String line ) { 322 writer.println( line ); 323 } 324 325 /** 326 * {@XXXX}?変換後?スペ?スを?&nbsp;と置き換えます? 327 * 328 * ただし?式などを使用すると、td タグの属???に{@XXXX}?が含ま? 329 * これに、EXCELのスペ?スである?lt;span style="mso-spacerun: 330 * yes">&nbsp;&nbsp;</span> 331 * と置き換えると、属?リスト中のタグと?入れ子状態が発生する為? 332 * これは、置き換えません? 333 * <td XXX>YYY</td> の YYYの? を置き換えることになります? 334 * 335 * ここでは?去の互換性を最大限確保する為に、特殊な方法で、??ます? 336 * 前後?スペ?スを取り除???で、かつ?つ以上?連続したスペ?ス? 337 * 存在する場合?み、trim して??続スペ?スを?&nbsp;と置き換えます? 338 * ??間に連続スペ?スがな??合?、前後?スペ?スも削除せずに? 339 * ????をそのまま返します? 340 * 前後?スペ?スを変換してしま?、数字型の場合に、EXCELでの計算式がエラーになります? 341 * 342 * @og.rev 3.5.0.0 (2003/09/17) 新規追? 343 * @og.rev 3.5.5.0 (2004/03/12) 連続スペ?スの処?EXCELの方式に合わせる 344 * @og.rev 3.6.0.0 (2004/09/17) スペ?ス置き換え??td XXX>YYY</td> の YYYの?のみとする? 345 * @og.rev 3.6.1.0 (2005/01/05) 置換ロジ?修正(ReplaceString クラスを使用) 346 * 347 * @param target ???? 348 * 349 * @return 置換えた文字? 350 */ 351 private String spaceReplace( final String target ) { 352 ReplaceString repData = new ReplaceString(); 353 354 Matcher match1 = PTN1.matcher( target ) ; 355 while( match1.find() ) { 356 int st1 = match1.start(1); 357 String grp1 = match1.group(1); 358 Matcher match2 = PTN2.matcher( grp1 ) ; 359 while( match2.find() ) { 360 int st2 = match2.start(1); 361 String grp2 = match2.group(1); 362 Matcher match3 = PTN3.matcher( grp2 ) ; 363 while( match3.find() ) { 364 365 int st = st1 + st2 + match3.start(1); 366 int ed = st1 + st2 + match3.end(1); 367 368 repData.add( st,ed,makeSpace( ed-st ) ); 369 } 370 } 371 } 372 373 String rtn = repData.replaceAll( target ); 374 375 return rtn ; 376 } 377 378 /** 379 * ??個数のスペ?ス?を表す?EXCEL の記号を作?します? 380 * 381 * EXCELでは、スペ?ス??以上を?lt;span style="mso-spacerun: yes">&nbsp;&nbsp;</span> 382 * 形式に置き換えます?これは、EXCELがHTML変換する時?ルールです? 383 * 384 * ここでは、スペ?スの個数-1 の &nbsp; を持つ、上記???を作?します? 385 * ???は、本物のスペ?ス記号を割り当てます? 386 * 387 * @og.rev 3.6.0.0 (2004/09/17) 新規追? 388 * 389 * @param cnt スペ?スの個数 390 * 391 * @return 置換えた文字? 392 */ 393 private String makeSpace( final int cnt ) { 394 StringBuilder buf = new StringBuilder( 40 + cnt * 6 ); 395 buf.append( SPACE_ST ); 396 for( int i=1; i<cnt; i++ ) { 397 buf.append( SPACE ); 398 } 399 buf.append( SPACE_ED ); 400 401 return buf.toString(); 402 } 403 404 /** 405 * {@QRCODE.XXXX} を含???の alt 属??src 属?にセ?します? 406 * 407 * QRコード?画像を入れ替えるため、alt属?に設定してある キー??を?に? 408 * ?次?ーコード画像を作?し?そ?ファイル名を、src 属?に設定することで? 409 * 動的に画像ファイルのリンクを作?します? 410 * 現在のEXCELでは、バージョンによって?種類?画像表示方法が存在するようで? 411 * ?画像に付き??の変更が?です?こ???は、変換方法が異なる為? 412 * 全く別の処?行う?があります? 413 * 414 * <v:shape ??? alt="{@QRCODE.XXXX}" ???> 415 * <v:imagedata src="yyy" ???>???</v:shape>形式とマッチし? 416 * xxx 部?、yyy 部?前方参?します? 417 * 418 * <img ??? src="yyy" ??? alt="{@QRCODE.XXXX}" ??? > 形式とマッチし? 419 * yyy 部?、xxx 部?前方参?します? 420 * 421 * 画像?エンコード?、alt属?に設定した?{@QRCODE.XXXX} ??の 422 * XXXX 部??カラ?ータ(通常、{@XXXX} で取得できる値)を使用します? 423 * ??タが存在しな??合?、src="yyy" 部を削除することで対応します? 424 * なお?後続???関係で、alt="{@QRCODE.XXXX}" ??は、削除します? 425 * 426 * @og.rev 3.6.1.0 (2005/01/05) 新規追? 427 * 428 * @param target ???? 429 * 430 * @return 置換えた文字? 431 */ 432 private String qrcodeReplace( final String target ) { 433 ReplaceString repData = new ReplaceString(); 434 435 Matcher match1 = IMGPTN1.matcher( target ) ; 436 while( match1.find() ) { 437 String altV = match1.group(1); 438 439 int stAlt = match1.start(1) - 9 ; // {@QRCODE. まで遡? 440 int edAlt = match1.end(1) + 1 ; // } を含める 441 repData.add( stAlt,edAlt,"" ); // {@QRCODE.XXXX} の部?除 442 443 int st = match1.start(2); 444 int ed = match1.end(2); 445 446 String msg = getValue( altV ); // QRコード変換する??の取? 447 if( msg != null && msg.length() > 0 ) { 448 String newStr = makeQrImage( altV,msg ); // 画像ファイルのファイル? 449 repData.add( st,ed,newStr ); 450 } 451 else { 452 repData.add( st-5,ed+1,"" ); // src="yyy" 部??み削除 453 } 454 } 455 456 Matcher match2 = IMGPTN2.matcher( target ) ; 457 while( match2.find() ) { 458 int st = match2.start(1); 459 int ed = match2.end(1); 460 461 String altV = match2.group(2); 462 int stAlt = match2.start(2) - 9 ; // {@QRCODE. まで遡? 463 int edAlt = match2.end(2) + 1 ; // } を含める 464 repData.add( stAlt,edAlt,"" ); // {@QRCODE.XXXX} の部?除 465 466 String msg = getValue( altV ); // QRコード変換する??の取? 467 if( msg != null && msg.length() > 0 ) { 468 String newStr = makeQrImage( altV,msg ); // 画像ファイルのファイル? 469 repData.add( st,ed,newStr ); 470 } 471 else { 472 repData.add( st-5,ed+1,"" ); // src="yyy" 部??み削除 473 } 474 } 475 476 String rtn = repData.replaceAll( target ) ; 477 478 return rtn ; 479 } 480 481 /** 482 * ??カラ?と、QRコード変換する??より、画像を作?します? 483 * 484 * 返り値は、作?した画像ファイルのファイル名です? 485 * これは、データが存在しな??合に、src="" を返す?があるため? 486 * (でな?、画像へのリンクが表示されてしま??) 487 * src="./帳票ID.files/image00x.png" と?画像ファイルのアドレス部? 488 * {@QRCODE_カラ?} 形式に変更しておく?があります? 489 * 490 * @og.rev 3.6.1.0 (2005/01/05) 新規追? 491 * 492 * @param key カラ? 493 * @param msg QRコード変換する?? 494 * 495 * @return 画像ファイルのファイル? 496 */ 497 private String makeQrImage( final String key, final String msg ) { 498 if( msg == null || msg.length() == 0 ) { return "" ; } 499 500 String realClmName = null ; 501 int sp = key.lastIndexOf( '_' ); 502 if( sp >= 0 ) { 503 try { 504 int row = Integer.parseInt( key.substring( sp+1 ) ); 505 int realRow = getRealRow( row ); 506 realClmName = key.substring( 0,sp ) + "_" + realRow ; 507 } 508 catch (NumberFormatException e) { // 4.0.0 (2005/01/31) 509 String errMsg = "警告:QRCODE名?ヘッ??に'_'カラ?が使用"; 510 LogWriter.log( errMsg ); 511 } 512 } 513 else { 514 realClmName = key ; 515 } 516 517 if( qrFileMap == null ) { qrFileMap = new HashMap<String,String>(); } 518 if( qrFileMap.containsKey( realClmName ) ) { // Map にすでに存在して?? 519 return qrFileMap.get( realClmName ); 520 } 521 522 // 帳票?? を?に、画像ファイルの保存フォル?求めます? 523 String filename = "./" + listId + ".files/" + realClmName + ".png"; 524 String fullAddress = htmlDir + filename ; 525 526 QrcodeImage qrImage = new QrcodeImage(); 527 qrImage.init( msg,fullAddress ); 528 qrImage.saveImage(); 529 530 qrFileMap.put( realClmName,filename ); 531 return filename; 532 } 533 }