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.db; 017 018import org.opengion.fukurou.system.HybsConst ; // 6.1.0.0 (2014/12/26) 019import org.opengion.fukurou.util.ErrorMessage; 020import org.opengion.fukurou.db.Transaction; 021import org.opengion.hayabusa.common.HybsSystemException; 022import org.opengion.hayabusa.resource.ResourceManager; 023import org.opengion.hayabusa.html.ViewMarker; // 8.0.0.0 (2021/09/30) 024 025import java.util.Locale; 026import java.util.Map; 027import java.util.LinkedHashMap ; 028import java.util.concurrent.ConcurrentMap; // 6.4.3.3 (2016/03/04) 029import java.util.concurrent.ConcurrentHashMap; // 6.4.3.1 (2016/02/12) refactoring 030import java.util.Collections; // 6.4.3.1 (2016/02/12) refactoring 031import java.util.List; // 8.2.1.0 (2022/07/15) 032import java.util.ArrayList; // 8.2.1.0 (2022/07/15) 033 034/** 035 * AbstractTableFilter は、TableUpda インターフェースを継承した、DBTableModel 処理用の 036 * Abstract実装クラスです。 037 * 038 * @og.rev 5.5.2.6 (2012/05/25) protected変数をprivateに変更。インターフェースにメソッド追加 039 * @og.rev 6.4.1.1 (2016/01/16) keysMap を、サブクラスから設定させるように変更。 040 * 041 * @version 0.9.0 2000/10/17 042 * @author Kazuhiko Hasegawa 043 * @since JDK1.1, 044 */ 045public abstract class AbstractTableFilter implements TableFilter { 046 /** システムの改行コードを設定します。*/ 047 protected static final String CR = HybsConst.CR; // 6.1.0.0 (2014/12/26) refactoring 048 /** StringBilderなどの初期値を設定します。 {@value} */ 049 protected static final int BUFFER_MIDDLE = HybsConst.BUFFER_MIDDLE; // 6.1.0.0 (2014/12/26) refactoring 050 051 // 5.5.2.6 (2012/05/25) protected変数をprivateに変更。インターフェースにメソッド追加 052 private DBTableModel table ; 053 private String modifyType ; 054 private int[] rowNo ; 055 private boolean useDebug ; // 6.0.2.5 (2014/10/31) refactoring メソッドと同じなので名称変更 056 private Transaction tran ; // 5.1.9.0 (2010/08/01) 追加 057 private String sql ; // 4.2.4.0 (2008/06/23) 058 private String dbid ; // 4.2.4.0 (2008/06/23) 059 private ResourceManager resource ; // 4.3.7.4 (2009/07/01) 060 private ViewMarker viewMarker ; // 8.0.0.0 (2021/09/30) 061 062 private int errCode = ErrorMessage.OK; 063 private ErrorMessage errMessage ; 064 065 // 8.2.1.0 (2022/07/15) TableFilter_MAPCLM 専用のMapを管理するList 066 private List<Map<String,String>> mapList ; 067 068 /** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。 */ 069 private final ConcurrentMap<String,String> keyValMap = new ConcurrentHashMap<>(); 070 071 /** 7.4.0.1 (2021/04/16) 値を返すための変数 */ 072 private final ConcurrentMap<String,String> rtnValMap = new ConcurrentHashMap<>(); 073 074 // 5.6.6.0 (2013/07/05) keys の整合性チェックを行います。 075 // 6.4.1.1 (2016/01/16) keysMap を、サブクラスから設定させるように変更 076 /** 6.4.3.1 (2016/02/12) Collections.synchronizedMap で同期処理を行います。 */ 077 private final Map<String,String> keysMap = Collections.synchronizedMap( new LinkedHashMap<>() ) ; 078 079 // 6.0.2.3 (2014/10/10) plugin.table.TableFilter_XXXX から移動 080 /** XML宣言定数 */ 081 protected static final String XML_START_TAG = "<?xml version='1.0' encoding='UTF-8'?>" + CR + "<ROWSET tableName='xxx'>"; 082 /** ROWSET 終了TAG {@value} */ 083 protected static final String XML_END_TAG = "</ROWSET>"; 084 /** EXEC_SQL 開始TAG {@value} */ 085 protected static final String EXEC_START_TAG= "<EXEC_SQL>"; 086 /** EXEC_SQL exists="0" 開始TAG {@value} */ 087 protected static final String EXEC_EXISTS_0_TAG= "<EXEC_SQL exists=\"0\">"; // 8.1.0.3 (2022/01/21) 088 /** EXEC_SQL 終了TAG {@value} */ 089 protected static final String EXEC_END_TAG = "</EXEC_SQL>"; 090 091 // 6.0.2.3 (2014/10/10) isXml で、CR + EXEC_END_TAG のキャッシュを作成します。 092 /** XML形式かどうか */ 093 protected boolean isXml ; // 6.0.2.3 (2014/10/10) 094 /** exists判定に使用するSQL文 */ 095 protected String execExistsSQL; // 8.1.0.3 (2022/01/21) 096 /** 終了タグ */ 097 protected String execEndTag ; // 6.0.2.3 (2014/10/10) 098 099 /** 100 * デフォルトコンストラクター 101 * 102 * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor. 103 */ 104 protected AbstractTableFilter() { super(); } // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。 105 106 /** 107 * keys の整合性チェックを行うための初期設定を行います。 108 * サブクラスのコンストラクタ内で、設定するようにしてください。 109 * 110 * @og.rev 6.4.1.1 (2016/01/16) keys の整合性チェック対応 111 * @og.rev 6.4.3.1 (2016/02/12) ConcurrentMap 系は、key,val ともに not null 制限です。 112 * 113 * @param key 整合性チェックを行うための keysMap に設定するキー 114 * @param cmnt 整合性チェックを行うための キー の説明 115 */ 116 protected void initSet( final String key , final String cmnt ) { 117 if( key != null && cmnt != null ) { 118 keysMap.put( key , cmnt ); 119 } 120 } 121 122 /** 123 * DBTableModel をセットします。 124 * 125 * @param table DBTableModelオブジェクト 126 */ 127 @Override // TableFilter 128 public void setDBTableModel( final DBTableModel table ) { 129 this.table = table; 130 } 131 132 /** 133 * DBTableModel を取得します。 134 * 135 * @og.rev 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加 136 * 137 * @return 内部のDBTableModel 138 */ 139 @Override // TableFilter 140 public DBTableModel getDBTableModel() { 141 return table; 142 } 143 144 /** 145 * データ処理の方法(A:追加 C:更新 D:削除)を指定します。 146 * 147 * 通常は、DBTableModel に自動設定されている modifyType を元に、データ処理方法を 148 * 選別します。(A:追加 C:更新 D:削除) 149 * この場合、行単位で modifyType の値を取得して判別する必要がありますが、一般には 150 * 処理対象は、全件おなじ modifyType である可能性が高いです。 151 * また、selectedAll などで強制的に全件処理対象とする場合は、modifyType に値が 152 * 設定さていません。その様な場合に外部より modifyType を指定します。 153 * 初期値は、自動判定 です。 154 * 155 * @og.rev 5.5.2.6 (2012/05/25) 廃止 156 * 157 * @param type データ処理の方法(A:追加 C:更新 D:削除) 158 */ 159 @Override // TableFilter 160 public void setModifyType( final String type ) { 161 modifyType = type; 162 } 163 164 /** 165 * データ処理の方法(A:追加 C:更新 D:削除)を取得します。 166 * 167 * 初期値は、自動判定 です。 168 * 169 * @og.rev 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加 170 * 171 * @return データ処理の方法(A:追加 C:更新 D:削除) 172 */ 173 @Override // TableFilter 174 public String getModifyType() { 175 return modifyType ; 176 } 177 178 /** 179 * キーと値のペアの変数配列を受け取ります。 180 * 181 * ここでは、この方式以外に、パラメーターMapを受け取る方法もあります。 182 * この受け取る時に、キーを大文字化します。TableFilter の keys は、 183 * 大文字のみで定義しておくことで、HTMLやWindows世代の曖昧な表記方法に 184 * 対応しています。(unixやxmlのような厳格な方が好きですけど) 185 * 186 * keys,vals とパラメーターMapを同時に指定した場合は、両方とも有効です。 187 * ただし、キーが重複した場合は、不定と考えてください。 188 * 189 * @og.rev 5.6.6.0 (2013/07/05) keys の整合性チェックを行います。 190 * 191 * @param keys キー配列 192 * @param vals 値配列 193 * @see #setParamMap( ConcurrentMap ) 194 */ 195 @Override // TableFilter 196 public void setKeysVals( final String[] keys,final String[] vals ) { 197 if( keys != null && vals != null ) { 198 for( int i=0; i<keys.length; i++ ) { 199 // 5.6.6.0 (2013/07/05) 共通のセッターメソッド経由で登録します。 200 setKeyVal( keys[i],vals[i] ); 201 } 202 } 203 } 204 205 /** 206 * キーと値のペアを受け取り、内部の keyValMap マップに追加します。 207 * 208 * キーか値のどちらかが null の場合は、何もしません。つまり、val に 209 * null をセットすることはできません。 210 * 211 * このメソッドは、setKeysVals( String[] ,String[] ) メソッドと、 212 * setParamMap( Map<String,String> ) メソッドの両方から、使用します。 213 * 処理を行うに当たり、下記の処理を行います。 214 * 1.キーを大文字化します。 215 * 2.各クラスの keys と整合性チェックを行います。 216 * 217 * ただし、setKeysVals と setParamMap の登録順は、不定と考えてください。 218 * 両方に同じキーを指定すると、どちらの値がセットされたかは、不定です。 219 * 220 * @og.rev 5.6.6.0 (2013/07/05) keys の整合性チェックを行います。 221 * @og.rev 6.4.3.4 (2016/03/12) Map#forEach で対応する。 222 * @og.rev 6.7.9.1 (2017/05/19) keysMap が、空の場合も、keyValMap に登録する。(initSet 未登録時) 223 * @og.rev 7.0.1.0 (2018/10/15) XHTML → HTML5 対応(空要素の、"/>" 止めを、">" に変更します)。 224 * @og.rev 8.0.0.0 (2021/09/30) brタグを止めてを、preタグに変更します。 225 * 226 * @param key キー文字列(null の場合は、処理しない) 227 * @param val 値文字列(null の場合は、処理しない) 228 * @see #setKeysVals( String[] ,String[] ) 229 * @see #setParamMap( ConcurrentMap ) 230 */ 231 private void setKeyVal( final String key,final String val ) { 232 // key か val かどちらかが null の場合は、処理を行わない。 233 if( key == null || val == null ) { return; } 234 235 final String upKey = key.toUpperCase(Locale.JAPAN); 236 237 // 6.7.9.1 (2017/05/19) keysMap が、空の場合も、keyValMap に登録する。(initSet 未登録時) 238 if( keysMap.isEmpty() || keysMap.containsKey( upKey ) ) { // keysMap は、各サブクラスで定義 239 keyValMap.put( upKey,val ); 240 } 241 else { 242// final String BR = "<br />" + CR ; 243// final String BR = "<br>" + CR ; // 7.0.1.0 (2018/10/15) 244 final String BR = CR ; // 8.0.0.0 (2021/09/30) 245 final StringBuilder errMsg = new StringBuilder( BUFFER_MIDDLE ); 246 // 6.0.2.5 (2014/10/31) char を append する。 247// errMsg.append( BR ) 248 errMsg.append( "<pre>" ).append( BR ) 249 .append( "指定のキーは、この tableFilter では、使用できません。" ).append( BR ) 250 .append( " class=[" ).append( getClass().getName() ).append( ']' ).append( BR ) 251 .append( " key =[" ).append( key ).append( ']' ).append( BR ) 252 .append( " ======== usage keys ======== " ).append( BR ) ; 253 // 6.4.3.4 (2016/03/12) Map#forEach で対応する。 254 keysMap.forEach( (k,v) -> { errMsg.append( ' ' ).append( k ).append( ':' ).append( v ).append( BR ); } ); 255 errMsg.append( " ============================ " ).append( BR ).append( "</pre>" ); 256 257 throw new HybsSystemException( errMsg.toString() ); 258 } 259 } 260 261 /** 262 * 選択された行番号の配列をセットします。 263 * 264 * 表示データの HybsSystem.ROW_SELECTED_KEY を元に、選ばれた 行を 265 * 処理の対象とします。 266 * 267 * @param rowNoTmp 行番号配列(可変長引数) 268 */ 269 @Override // TableFilter 270 public void setParameterRows( final int... rowNoTmp ) { 271 if( rowNoTmp != null && rowNoTmp.length > 0 ) { // 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。 272 final int size = rowNoTmp.length ; 273 rowNo = new int[size]; 274 System.arraycopy( rowNoTmp,0,rowNo,0,size ); 275 } 276 } 277 278 /** 279 * 選択された行番号の配列を取得します。 280 * 281 * 表示データの HybsSystem.ROW_SEL_KEY を元に、選ばれた 行を 282 * 処理の対象とします。 283 * 284 * @og.rev 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加 285 * @og.rev 6.0.2.5 (2014/10/31) null ではなく、サイズ0の配列を返すように変更。 286 * 287 * @return 行番号の配列(選ばれていない場合は、サイズ0の配列を返す) 288 * @og.rtnNotNull 289 */ 290 @Override // TableFilter 291 public int[] getParameterRows() { 292 // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..; 293 return rowNo == null ? new int[0] : rowNo.clone() ; 294 } 295 296 /** 297 * アクセスログ取得の為,Transactionオブジェクトを設定します。 298 * 299 * @og.rev 5.1.9.0 (2010/08/01) Transaction 対応(新規追加) 300 * 301 * @param tran Transactionオブジェクト 302 */ 303 @Override // TableFilter 304 public void setTransaction( final Transaction tran ) { 305 this.tran = tran; 306 } 307 308 /** 309 * アクセスログ取得の為,Transactionオブジェクトを取得します。 310 * 311 * @og.rev 5.1.9.0 (2010/08/01) Transaction 対応(新規追加) 312 * @og.rev 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加 313 * 314 * @return Transactionオブジェクト 315 */ 316 @Override // TableFilter 317 public Transaction getTransaction() { 318 return tran; 319 } 320 321 /** 322 * DBIDを指定します。 323 * 324 * @og.rev 4.2.4.0 (2008/06/23) 新規追加 325 * 326 * @param dbid 接続先ID 327 */ 328 @Override // TableFilter 329 public void setDbid( final String dbid ) { 330 this.dbid = dbid; 331 } 332 333 /** 334 * DBIDを取得します。 335 * 336 * @og.rev 4.2.4.0 (2008/06/23) 新規追加 337 * @og.rev 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加 338 * 339 * @return DBID(接続先情報) 340 */ 341 @Override // TableFilter 342 public String getDbid() { 343 return dbid; 344 } 345 346 /** 347 * ボディー部分のSQLを指定します。 348 * 349 * @og.rev 4.2.4.0 (2008/06/23) 新規追加 350 * 351 * @param sql ボディー部分のSQL 352 */ 353 @Override // TableFilter 354 public void setSql( final String sql ) { 355 this.sql = sql; 356 } 357 358 /** 359 * ボディー部分のSQLを取得します。 360 * 361 * @og.rev 4.2.4.0 (2008/06/23) 新規追加 362 * @og.rev 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加 363 * 364 * @return ボディー部分のSQL 365 */ 366 @Override // TableFilter 367 public String getSql() { 368 return sql; 369 } 370 371 /** 372 * パラメーターMapを指定します。 373 * 374 * keys,vals と パラメーターMapを同時に指定した場合は、両方とも有効です。 375 * ただし、キーが重複した場合は、不定と考えてください。 376 * 377 * この受け取る時に、キーを大文字化します。TableFilter の keys は、 378 * 大文字のみで定義しておくことで、HTMLやWindows世代の曖昧な表記方法に 379 * 対応しています。(unixやxmlのような厳格な方が好きですけど) 380 * 381 * @og.rev 5.6.5.2 (2013/06/21) 新規追加 382 * @og.rev 5.6.6.0 (2013/07/05) keys の整合性チェックを行います。 383 * @og.rev 6.4.3.3 (2016/03/04) ConcurrentHashMap を受け取ることを明確にするため、I/FをConcurrentMapに変更します。 384 * 385 * @param paramMap パラメーターMap 386 * @see #setKeysVals( String[] ,String[] ) 387 */ 388 @Override // TableFilter 389 public void setParamMap( final ConcurrentMap<String,String> paramMap ) { 390 // 6.4.3.3 (2016/03/04) Map#forEach に変更 391 if( paramMap != null ) { 392 paramMap.forEach( (k,v) -> setKeyVal( k,v ) ); 393 } 394 } 395 396 /** 397 * リソースオブジェクトを指定します。 398 * 399 * @og.rev 4.3.7.4 (2009/07/01) 新規追加 400 * 401 * @param resource リソースオブジェクト 402 */ 403 @Override // TableFilter 404 public void setResource( final ResourceManager resource ) { 405 this.resource = resource; 406 } 407 408 /** 409 * リソースオブジェクトを取得します。 410 * 411 * @og.rev 4.3.7.4 (2009/07/01) 新規追加 412 * @og.rev 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加 413 * 414 * @return リソースオブジェクト 415 */ 416 @Override // TableFilter 417 public ResourceManager getResource() { 418 return resource; 419 } 420 421 /** 422 * viewMarkerオブジェクトを指定します。 423 * 424 * @og.rev 8.0.0.0 (2021/09/30) viewMarker追加 425 * 426 * @param marker viewMarkerオブジェクト 427 */ 428 @Override // TableFilter 429 public void setViewMarker( final ViewMarker marker ) { 430 viewMarker = marker; 431 } 432 433 /** 434 * viewMarkerオブジェクトを取得します。 435 * 436 * @og.rev 8.0.0.0 (2021/09/30) viewMarker追加 437 * 438 * @return viewMarkerオブジェクト 439 */ 440 @Override // TableFilter 441 public ViewMarker getViewMarker() { 442 return viewMarker; 443 } 444 445 /** 446 * 値を返すためのMapを返します。 447 * 448 * Mapそのものを返しますので、中身の書き換えは行わないでください。 449 * 450 * @og.rev 7.4.0.1 (2021/04/16) 値を返すための変数 451 * 452 * @return Mapオブジェクト 453 */ 454 @Override // TableFilter 455 public Map<String,String> getReturnMap() { 456 return rtnValMap; 457 } 458 459 /** 460 * TableFilter_MAPCLM 専用のMapを管理するListを設定します。 461 * 462 * @og.rev 8.2.1.0 (2022/07/15) TableFilter_MAPCLM 専用のMapを管理するList 463 * 464 * @param valList Mapオブジェクトのリスト 465 */ 466 @Override // TableFilter 467 public void setMapList( final List<Map<String,String>> valList ) { 468 mapList = valList ; 469 } 470 471 /** 472 * TableFilter_MAPCLM 専用のMapを管理するListを返します。 473 * 474 * リストの登録順は、vals で指定されたMapオブジェクトの登録順です。 475 * 指定がない場合は、null が返ります。 476 * 477 * @og.rev 8.2.1.0 (2022/07/15) TableFilter_MAPCLM 専用のMapを管理するList 478 * 479 * @return Mapオブジェクト 480 */ 481 protected List<Map<String,String>> getMapList() { 482 return mapList; 483 } 484 485 /** 486 * デバッグ情報を出力するかどうか[true:する/false:しない]を指定します。 487 * true でデバッグ情報を表示します。 488 * 489 * @param flag デバッグ出力するか [true:する/false:しない] 490 */ 491 @Override // TableFilter 492 public void setDebug( final boolean flag ) { 493 useDebug = flag; // 6.0.2.5 (2014/10/31) refactoring メソッドと同じなので名称変更 494 } 495 496 /** 497 * デバッグ情報を出力するかどうか[true:する/false:しない]を取得します。 498 * true でデバッグ情報を表示します。 499 * 500 * @og.rev 5.5.2.6 (2012/05/25) インターフェースにgetterメソッド追加 501 * 502 * @return デバッグ出力 [true:する/false:しない] 503 */ 504 @Override // TableFilter 505 public boolean isDebug() { 506 return useDebug ; // 6.0.2.5 (2014/10/31) refactoring メソッドと同じなので名称変更 507 } 508 509 /** 510 * エラーコード を取得します。 511 * エラーコード は、ErrorMessage クラスで規定されているコードです。 512 * 513 * @return エラーコード 514 */ 515 @Override // TableFilter 516 public int getErrorCode() { 517 return errCode; 518 } 519 520 /** 521 * エラーメッセージオブジェクト を取得します。 522 * 523 * @return エラーメッセージオブジェクト 524 */ 525 @Override // TableFilter 526 public ErrorMessage getErrorMessage() { 527 return errMessage; 528 } 529 530 /** 531 * タイトルとエラーコードを指定して、エラーメッセージオブジェクト を作成します。 532 * すでに、作成済みの場合は、作成済みのオブジェクトを、まだ、未作成の場合は、 533 * 新規に作成します。 534 * 535 * @param title タイトル 536 * @param code エラーコード 537 * 538 * @return エラーメッセージオブジェクト 539 */ 540 protected ErrorMessage makeErrorMessage( final String title,final int code ) { 541 if( errMessage == null ) { 542 errMessage = new ErrorMessage( title ); 543 } 544 if( errCode < code ) { errCode = code; } 545 return errMessage; 546 } 547 548 /** 549 * カラム名配列(String[])より、対応するカラムNo配列(int[])を作成します。 550 * 551 * @param nameArray カラム名配列 552 * 553 * @return カラムNo配列(可変長引数) 554 */ 555 protected int[] getTableColumnNo( final String... nameArray ) { 556 int[] clmNo = new int[nameArray.length]; 557 for( int i=0; i<clmNo.length; i++ ) { 558 clmNo[i] = table.getColumnNo( nameArray[i] ); 559 } 560 return clmNo; 561 } 562 563 /** 564 * 設定されたパラメータキーに対する値を取得します。 565 * 引数、および、パラメータが null の場合は、 null を返します。 566 * 567 * @og.rev 6.4.3.3 (2016/03/04) ConcurrentHashMap の not null制限のチェック追加 568 * 569 * @param key パラメータキー 570 * 571 * @return パラメータ値 572 */ 573 protected String getValue( final String key ) { 574 return key == null ? null : keyValMap.get( key ); 575 } 576 577 /** 578 * フィルターからtaglibのリクエスト変数に値を書き戻したい場合に、key と val をセットします。 579 * 引数、および、パラメータが null の場合は、何もしません。 580 * 581 * @og.rev 7.4.0.1 (2021/04/16) 値を返すための変数セット 582 * 583 * @param key 戻しキー 584 * @param val 戻し値 585 */ 586 protected void setValue( final String key ,final String val ) { 587 if( key != null && val != null ) { 588 rtnValMap.put( key,val ); 589 } 590 } 591 592 /** 593 * keyValMapに持っているキーの配列を取得します。 594 * これは、サブクラスで、initSet(String,String) を行わない場合、keys には 595 * 値を自由に設定できます。 596 * その値を取り出すためです。 597 * 598 * @og.rev 6.7.9.1 (2017/05/19) 新規追加 599 * 600 * @return キー値の配列(keyValMapに持っているキー) 601 */ 602 protected String[] getKeys() { 603 return keyValMap.keySet().toArray( new String[keyValMap.size()] ); 604 } 605}