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.fukurou.db; 017 018import org.opengion.fukurou.util.StringUtil; 019import org.opengion.fukurou.system.OgRuntimeException ; // 6.4.2.0 (2016/01/29) 020import org.opengion.fukurou.system.Closer; 021import org.opengion.fukurou.model.Formatter; 022import org.opengion.fukurou.model.ArrayDataModel; 023import org.opengion.fukurou.model.DataModel; // 8.2.1.0 (2022/07/15) 024import static org.opengion.fukurou.system.HybsConst.CR; // 6.1.0.0 (2014/12/26) refactoring 025import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE; // 6.1.0.0 (2014/12/26) refactoring 026 027import java.sql.Connection; 028import java.sql.PreparedStatement; 029import java.sql.ParameterMetaData; 030import java.sql.SQLException; 031 032import java.util.Arrays; 033 034/** 035 * DBTableModel インターフェースを継承した TableModel の実装クラスです。 036 * sql文を execute( query ) する事により,データベースを検索した結果を 037 * DBTableModel に割り当てます。 038 * 039 * メソッドを宣言しています 040 * DBTableModel インターフェースは,データベースの検索結果(Resultset)をラップする 041 * インターフェースとして使用して下さい。 042 * 043 * @og.rev 5.2.2.0 (2010/11/01) パッケージ移動(hayabusa.db ⇒ fukurou.db) 044 * @og.group DB/Shell制御 045 * 046 * @version 4.0 047 * @author Kazuhiko Hasegawa 048 * @since JDK5.0, 049 */ 050public class DBSimpleTable { 051 052 private final String[] names ; // データ配列に対応するカラム配列(names) 053 private String[] keys ; // 登録に使用するカラムキー配列(keys) 054 private int[] keysNo ; // 登録に使用するカラムキー配列番号 055 private String table ; // 登録テーブル名 056 private String where ; // where 条件式[カラム名]を含む 057 private int[] whereNo ; // [カラム名]に対応するデータ配列番号 058 private String[] constrain ; // key に対応した制約条件 059 060 private String connID ; // 登録に使用するコネクションID 061 private boolean useWhere ; // where が設定されると true にセットされます。 062 063 private Connection conn ; 064 private PreparedStatement pstmt ; 065 private ParameterMetaData pMeta ; // 5.1.2.0 (2010/01/01) setObject に、Type を渡す。(PostgreSQL対応) 066 private String query ; // エラーメッセージ用の変数 067 private int execCnt ; 068 private ApplicationInfo appInfo ; // 3.8.7.0 (2006/12/15) 069 private boolean useParamMetaData; // 5.1.2.0 (2010/01/01) setObject に、Type を渡す。(PostgreSQL対応) 070 071 /** 072 * データ配列のカラム名称配列を指定してオブジェクトを構築します。 073 * 074 * @param nm カラム名称配列 075 * @throws RuntimeException tbl が null の場合 076 */ 077 public DBSimpleTable( final String[] nm ) { 078 if( nm == null ) { 079 final String errMsg = "データ配列のカラム名称に null は設定できません。"; 080 throw new OgRuntimeException( errMsg ); 081 } 082 083 names = new String[nm.length]; 084 System.arraycopy( nm,0,names,0,names.length ); 085 } 086 087 /** 088 * 登録に使用するカラムキー配列(keys)を登録します。 089 * 090 * 引数のkey配列が null の場合は、names と同じカラム名称配列(names)が使用されます。 091 * キー配列(keys)は、一度しか登録できません。また、addConstrain等のメソッド 092 * 呼び出しを先に実行すると、カラム名称配列(names)が設定されてしまう為、 093 * その後にこのメソッドを呼び出すとエラーが発生します。 094 * 095 * @param key 登録カラム名称配列(可変長引数) 096 * @see #addConstrain( String ,String ) 097 * @throws RuntimeException すでに キー配列(keys)が登録済み/作成済みの場合 098 */ 099 public void setKeys( final String... key ) { 100 if( keys != null ) { 101 final String errMsg = "すでに キー配列(keys)が登録済みです。"; 102 throw new OgRuntimeException( errMsg ); 103 } 104 105 if( key != null && key.length > 0 ) { // 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。 106 final int size = key.length; 107 keys = new String[size]; 108 System.arraycopy( key,0,keys,0,size ); 109 110 constrain = new String[size]; 111 Arrays.fill( constrain,"?" ); 112 113 keysNo = new int[size]; 114 for( int i=0; i<size; i++ ) { 115 final int address = findAddress( names,keys[i] ); 116 if( address >= 0 ) { 117 keysNo[i] = address; 118 } 119 else { 120 final String errMsg = "指定の key は、カラム配列(names)に存在しません" 121 + " key[" + i + "]=" + key[i] 122 + " names=" + StringUtil.array2csv( names ) ; 123 throw new OgRuntimeException( errMsg ); 124 } 125 } 126 } 127 } 128 129 /** 130 * カラム名称配列(names)と同じキー配列(keys)を作成します。 131 * 132 * これは、キー配列(keys) が作成されなかった場合の処理です。 133 * keys が null の場合のみ、処理を実行します。 134 * 135 * @see #setKeys( String[] ) 136 */ 137 private void makeKeys() { 138 // キー配列(keys) が未設定の場合は、カラム名称配列(names)が設定されます。 139 if( keys == null ) { 140 keys = names; 141 final int size = keys.length; 142 143 constrain = new String[size]; 144 Arrays.fill( constrain,"?" ); 145 146 keysNo = new int[size]; 147 for( int i=0; i<size; i++ ) { 148 keysNo[i] = i; 149 } 150 } 151 } 152 153 /** 154 * Insert/Update/Delete 時の登録するテーブル名。 155 * 156 * @param tbl テーブル名 157 * @throws RuntimeException tbl が null の場合 158 */ 159 public void setTable( final String tbl ) { 160 if( tbl == null ) { 161 final String errMsg = "table に null は設定できません。"; // 5.1.8.0 (2010/07/01) errMsg 修正 162 throw new OgRuntimeException( errMsg ); 163 } 164 165 table = tbl; 166 } 167 168 /** 169 * データベースの接続先IDを設定します。 170 * 171 * @param conn 接続先ID 172 */ 173 public void setConnectionID( final String conn ) { 174 connID = conn; 175 } 176 177 /** 178 * アクセスログ取得の為,ApplicationInfoオブジェクトを設定します。 179 * 180 * @og.rev 3.8.7.0 (2006/12/15) 新規追加 181 * 182 * @param appInfo アプリ情報オブジェクト 183 */ 184 public void setApplicationInfo( final ApplicationInfo appInfo ) { 185 this.appInfo = appInfo; 186 } 187 188 /** 189 * Insert/Update/Delete 時の PreparedStatement の引数(クエスチョンマーク)制約。 190 * 191 * 制約条件(val)は、そのまま引数に使用されます。通常、? で表される 192 * パラメータに、文字長を制限する場合、SUBSTRB( ?,1,100 ) という 193 * val 変数を与えます。 194 * また、キー一つに対して、値を複数登録したい場合にも、使用できます。 195 * 例えば、NVAL( ?,? ) のような場合、キー一つに値2つを割り当てます。 196 * 値配列の並び順は、キー配列(keys)に対する(?の個数)に対応します。 197 * 注意:カラム名称配列(names)ではありません。また、先にキー配列(keys)を登録 198 * しておかないと、キー配列登録時にエラーが発生します。 199 * 制約条件は、処理するQUERYに対して適用されますので、 200 * key または、val が null の場合は、RuntimeException を Throwします。 201 * 202 * @param key 制約をかけるキー 203 * @param val 制約条件式 204 * @see #setKeys( String[] ) 205 * @throws RuntimeException key または、val が null の場合 206 */ 207 public void addConstrain( final String key,final String val ) { 208 if( key == null || val == null ) { 209 final String errMsg = "key または、val に null は設定できません。" 210 + " key=[" + key + "] , val=[" + val + "]" ; 211 throw new OgRuntimeException( errMsg ); 212 } 213 214 // キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。 215 if( keys == null ) { makeKeys(); } 216 217 // 制約条件のアドレスは、カラム名称配列(names)でなく、キー配列(keys)を使用します。 218 final int address = findAddress( keys,key ); 219 if( address >= 0 ) { 220 constrain[address] = val; 221 } 222 else { 223 final String errMsg = "指定の key は、キー配列(keys)に存在しません" 224 + " key=[" + key + "] , val=[" + val + "]" 225 + " keys=" + StringUtil.array2csv( keys ) ; 226 throw new OgRuntimeException( errMsg ); 227 } 228 } 229 230 /** 231 * Update/Delete 時のキーとなるWHERE 条件のカラム名を設定します。 232 * 233 * 通常の WHERE 句の書き方と同じで、カラム配列(names)に対応する設定値(values)の値を 234 * 割り当てたい箇所に[カラム名] を記述します。文字列の場合、設定値をセットする 235 * ときに、シングルコーテーションを使用しますが、[カラム名]で指定する場合は、 236 * その前後に、(')シングルコーテーションは、不要です。 237 * WHERE条件は、登録に使用するキー配列(keys)に現れない条件で行を特定することがありますので 238 * カラム名称配列(names)を元にカラム名のアドレスを求めます。 239 * [カラム名]は、? に置き換えて、PreparedStatement として、実行される形式に変換されます。 240 * 例:FGJ='1' and CLM=[CLM] and SYSTEM_ID in ([SYSID],'**') 241 * 242 * @og.rev 4.3.4.0 (2008/12/01) キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てる 243 * @og.rev 5.0.2.0 (2009/11/01) バグ修正(keysはデータセットのキーなので、where句のカラムに含まれて入いるわけではない) 244 * @og.rev 6.4.1.2 (2016/01/22) PMD refactoring. where は、null をセットするのではなく、useWhere の設定で判定する。 245 * @og.rev 6.4.3.4 (2016/03/11) Formatterに新しいコンストラクターを追加する。 246 * @og.rev 6.9.5.0 (2018/04/23) カラム名が存在しない場合に、Exception を throw するかどうかを指定可能にする。 247 * 248 * @param wh WHERE条件のカラム名 249 * @throws RuntimeException [カラム名]がカラム配列(names)に存在しない場合 250 */ 251 public void setWhere( final String wh ) { 252 253 // 6.4.1.2 (2016/01/22) PMD refactoring. 254 if( wh == null || wh.isEmpty() ) { 255 useWhere= false; 256 } 257 else { 258 // キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。 259 // 5.0.2.0 (2009/11/01) 260// final ArrayDataModel data = new ArrayDataModel( names ); 261// final ArrayDataModel data = new ArrayDataModel( names,true ); // 6.9.5.0 (2018/04/23) カラム名が存在しない場合に、Exception を throw する 262 final DataModel<String> data = new ArrayDataModel( names,true ); // 8.2.1.0 (2022/07/15) 263 final Formatter format = new Formatter( data,wh ); // 6.4.3.4 (2016/03/11) 264 where = format.getQueryFormatString(); 265 whereNo = format.getClmNos(); 266 useWhere= true; 267 } 268 } 269 270 /** 271 * データをインサートする場合に使用するSQL文を作成します。 272 * 273 * @og.rev 6.2.3.0 (2015/05/01) CSV形式の作成を、String#join( CharSequence , CharSequence... )を使用。 274 * 275 * @return インサートSQL 276 * @og.rtnNotNull 277 */ 278 private String getInsertSQL() { 279 // キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。 280 if( keys == null ) { makeKeys(); } 281 282 // 6.2.3.0 (2015/05/01) CSV形式の作成を、String#join( CharSequence , CharSequence... )を使用。 283 final StringBuilder sql = new StringBuilder( BUFFER_MIDDLE ) 284 .append( "INSERT INTO " ).append( table ) 285 .append( " ( " ) 286 .append( String.join( "," , keys ) ) // 6.2.3.0 (2015/05/01) 287 .append( " ) VALUES ( " ) 288 .append( String.join( "," , constrain ) ) // 6.2.3.0 (2015/05/01) 289 .append( " )" ); 290 291 useWhere = false; 292 293 return sql.toString(); 294 } 295 296 /** 297 * データをアップデートする場合に使用するSQL文を作成します。 298 * 299 * @og.rev 6.4.1.2 (2016/01/22) PMD refactoring. where は、null をセットするのではなく、useWhere の設定で判定する。 300 * 301 * @return アップデートSQL 302 * @og.rtnNotNull 303 */ 304 private String getUpdateSQL() { 305 // キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。 306 if( keys == null ) { makeKeys(); } 307 308 final StringBuilder sql = new StringBuilder( BUFFER_MIDDLE ) 309 .append( "UPDATE " ).append( table ).append( " SET " ) 310 .append( keys[0] ).append( " = " ).append( constrain[0] ); 311 312 for( int i=1; i<keys.length; i++ ) { 313 sql.append( " , " ) 314 .append( keys[i] ).append( " = " ).append( constrain[i] ); 315 } 316 317 // 6.4.1.2 (2016/01/22) PMD refactoring. 318 if( useWhere ) { 319 sql.append( " WHERE " ).append( where ); 320 } 321 322 return sql.toString(); 323 } 324 325 /** 326 * データをデリートする場合に使用するSQL文を作成します。 327 * 328 * @og.rev 5.0.2.0 (2009/11/01) バグ修正(削除時はkeysは必要ない) 329 * @og.rev 6.4.1.2 (2016/01/22) PMD refactoring. where は、null をセットするのではなく、useWhere の設定で判定する。 330 * 331 * @return デリートSQL 332 * @og.rtnNotNull 333 */ 334 private String getDeleteSQL() { 335 // キー配列(keys)が未設定(null)の場合は、カラム名称配列(names)を割り当てます。 336 // 5.0.2.0 (2009/11/01) 337 keys = new String[0]; 338 339 final StringBuilder sql = new StringBuilder( BUFFER_MIDDLE ) 340 .append( "DELETE FROM " ).append( table ); 341 342 // 6.4.1.2 (2016/01/22) PMD refactoring. 343 if( useWhere ) { 344 sql.append( " WHERE " ).append( where ); 345 } 346 347 return sql.toString(); 348 } 349 350 /** 351 * Insert 処理の開始を宣言します。 352 * 内部的に、コネクションを接続して、PreparedStatementオブジェクトを作成します。 353 * このメソッドと、close() メソッドは必ずセットで処理してください。 354 * 355 * @og.rev 3.8.7.0 (2006/12/15) アクセスログ取得の為,ApplicationInfoオブジェクトを設定 356 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応) 357 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を ConnectionFactory経由で取得。(PostgreSQL対応) 358 * 359 * @throws SQLException Connection のオープンに失敗した場合 360 */ 361 public void startInsert() throws SQLException { 362 execCnt = 0; 363 query = getInsertSQL(); 364 conn = ConnectionFactory.connection( connID,appInfo ); 365 pstmt = conn.prepareStatement( query ); 366 // 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応) 367 useParamMetaData = ConnectionFactory.useParameterMetaData( connID ); // 5.3.8.0 (2011/08/01) 368 if( useParamMetaData ) { 369 pMeta = pstmt.getParameterMetaData(); 370 } 371 } 372 373 /** 374 * Update 処理の開始を宣言します。 375 * 内部的に、コネクションを接続して、PreparedStatementオブジェクトを作成します。 376 * このメソッドと、close() メソッドは必ずセットで処理してください。 377 * 378 * @og.rev 3.8.7.0 (2006/12/15) アクセスログ取得の為,ApplicationInfoオブジェクトを設定 379 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応) 380 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を ConnectionFactory経由で取得。(PostgreSQL対応) 381 * 382 * @throws SQLException Connection のオープンに失敗した場合 383 */ 384 public void startUpdate() throws SQLException { 385 execCnt = 0; 386 query = getUpdateSQL(); 387 conn = ConnectionFactory.connection( connID,appInfo ); 388 pstmt = conn.prepareStatement( query ); 389 // 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応) 390 useParamMetaData = ConnectionFactory.useParameterMetaData( connID ); // 5.3.8.0 (2011/08/01) 391 if( useParamMetaData ) { 392 pMeta = pstmt.getParameterMetaData(); 393 } 394 } 395 396 /** 397 * Delete 処理の開始を宣言します。 398 * 内部的に、コネクションを接続して、PreparedStatementオブジェクトを作成します。 399 * このメソッドと、close() メソッドは必ずセットで処理してください。 400 * 401 * @og.rev 3.8.7.0 (2006/12/15) アクセスログ取得の為,ApplicationInfoオブジェクトを設定 402 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応) 403 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData を ConnectionFactory経由で取得。(PostgreSQL対応) 404 * 405 * @throws SQLException Connection のオープンに失敗した場合 406 */ 407 public void startDelete() throws SQLException { 408 execCnt = 0; 409 query = getDeleteSQL(); 410 conn = ConnectionFactory.connection( connID,appInfo ); 411 pstmt = conn.prepareStatement( query ); 412 // 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応) 413 useParamMetaData = ConnectionFactory.useParameterMetaData( connID ); // 5.3.8.0 (2011/08/01) 414 if( useParamMetaData ) { 415 pMeta = pstmt.getParameterMetaData(); 416 } 417 } 418 419 /** 420 * データ配列を渡して実際のDB処理を実行します。 421 * 422 * この処理の前に、startXXXX をコールしておき、INSER,UPDATE,DELETEのどの 423 * 処理を行うか、宣言しておく必要があります。 424 * 戻り値は、この処理での処理件数です。 425 * 最終件数は、close( boolean ) 時に取得します。 426 * 427 * @og.rev 4.0.0.0 (2007/11/28) SQLException をきちんと伝播させます。 428 * @og.rev 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応) 429 * @og.rev 5.3.8.0 (2011/08/01) useParamMetaData 時の setNull 対応(PostgreSQL対応) 430 * 431 * @param values カラム配列(names)に対応する設定値配列(可変長引数) 432 * 433 * @return ここでの処理件数 434 * 435 * @see #close( boolean ) 436 * @throws SQLException Connection のクロースに失敗した場合 437 * @throws RuntimeException Connection DB処理の実行に失敗した場合 438 */ 439 public int execute( final String... values ) throws SQLException { 440 final int cnt; 441 try { 442 int clmNo = 1; // JDBC のカラム番号は、1から始まる。 443 444 // 5.1.2.0 (2010/01/01) setObject に ParameterMetaData の getParameterType を渡す。(PostgreSQL対応) 445 if( useParamMetaData ) { 446 // keys に値を割り当てます。 447 for( int i=0; i<keys.length; i++ ) { 448 final int type = pMeta.getParameterType( clmNo ); 449 // 5.3.8.0 (2011/08/01) setNull 対応 450 final String val = values[keysNo[i]]; 451 if( val == null || val.isEmpty() ) { 452 pstmt.setNull( clmNo++, type ); 453 } 454 else { 455 pstmt.setObject( clmNo++,val,type ); 456 } 457 } 458 459 // where 条件を使用する場合は、値を割り当てます。 460 if( useWhere ) { 461 for( int i=0; i<whereNo.length; i++ ) { 462 final int type = pMeta.getParameterType( clmNo ); 463 // 5.3.8.0 (2011/08/01) setNull 対応 464 final String val = values[whereNo[i]]; 465 if( val == null || val.isEmpty() ) { 466 pstmt.setNull( clmNo++, type ); 467 } 468 else { 469 pstmt.setObject( clmNo++,val,type ); 470 } 471 } 472 } 473 } 474 else { 475 // keys に値を割り当てます。 476 for( int i=0; i<keys.length; i++ ) { 477 pstmt.setObject( clmNo++,values[keysNo[i]] ); 478 } 479 480 // where 条件を使用する場合は、値を割り当てます。 481 if( useWhere ) { 482 for( int i=0; i<whereNo.length; i++ ) { 483 pstmt.setObject( clmNo++,values[whereNo[i]] ); 484 } 485 } 486 } 487 488 cnt = pstmt.executeUpdate(); 489 execCnt += cnt; 490 } 491 catch( final SQLException ex) { 492 Closer.stmtClose( pstmt ); 493 pMeta = null; // 5.1.2.0 (2010/01/01) 494 if( conn != null ) { 495 conn.rollback(); 496 ConnectionFactory.remove( conn,connID ); 497 conn = null; 498 } 499 final String errMsg = "DB処理の実行に失敗しました。" + CR 500 + " query=[" + query + "]" + CR 501 + " values=" + StringUtil.array2csv( values ); 502 throw new OgRuntimeException( errMsg ,ex ); 503 } 504 return cnt; 505 } 506 507 /** 508 * DB処理をクロースします。 509 * 510 * 引数には、commit させる場合は、true を、rollback させる場合は、false をセットします。 511 * 戻り値は、今まで処理された合計データ件数です。 512 * この処理は、SQLException を内部で RuntimeException に変換している為、catch 節は 513 * 不要ですが、必ず finally 節で呼び出してください。そうしないと、リソースリークの 514 * 原因になります。 515 * 516 * @og.rev 5.1.2.0 (2010/01/01) pMeta のクリア 517 * 518 * @param commitFlag コミットフラグ [true:commitする/false:rollbacする] 519 * 520 * @return 今までの合計処理件数 521 */ 522 public int close( final boolean commitFlag ) { 523 try { 524// if( conn != null ) { // 8.1.1.2 (2022/02/25) Modify 525 if( conn != null && !conn.isClosed() ) { 526 if( commitFlag ) { conn.commit(); } 527 else { conn.rollback(); } 528 } 529 } 530 catch( final SQLException ex) { 531 ConnectionFactory.remove( conn,connID ); 532 conn = null; 533 final String errMsg = "DB処理を確定(COMMIT)できませんでした。" + CR 534 + " query=[" + query + "]" + CR ; 535 throw new OgRuntimeException( errMsg,ex ); 536 } 537 finally { 538 Closer.stmtClose( pstmt ); 539 pMeta = null; // 5.1.2.0 (2010/01/01) 540 ConnectionFactory.close( conn,connID ); 541 conn = null; 542 } 543 544 return execCnt; 545 } 546 547 /** 548 * 文字列配列中の値とマッチするアドレスを検索します。 549 * 文字列配列がソートされていない為、バイナリサーチが使えません。よって、 550 * 総当りでループ検索しています。 551 * 総数が多い場合は、遅くなる為、マップにセットして使用することを検討ください。 552 * 553 * @param data ターゲットの文字列配列中 554 * @param key 検索する文字列 555 * 556 * @return ターゲットの添え字(存在しない場合は、-1) 557 */ 558 private int findAddress( final String[] data,final String key ) { 559 int address = -1; 560 if( data != null && key != null ) { 561 for( int i=0; i<data.length; i++ ) { 562 if( key.equalsIgnoreCase( data[i] ) ) { 563 address = i; 564 break; 565 } 566 } 567 } 568 return address; 569 } 570}