001/* 002 * Copyright (c) 2017 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.fileexec; 017 018import java.io.IOException; 019import java.io.Reader; 020import java.sql.Struct; // 6.3.3.0 (2015/07/25) 021import java.sql.Clob; 022import java.sql.ResultSet; 023import java.sql.ResultSetMetaData; 024import java.sql.SQLException; 025import java.sql.Types; 026import java.sql.Date; 027import java.sql.Timestamp; 028import java.util.Locale; 029import java.util.List; // 6.3.3.0 (2015/07/25) 030import java.util.ArrayList; // 6.3.3.0 (2015/07/25) 031 032import oracle.jdbc.OracleStruct; // 6.3.8.0 (2015/09/11) 033import oracle.jdbc.OracleTypeMetaData; // 6.3.8.0 (2015/09/11) 034 035/** 036 * ResultSet のデータ処理をまとめたクラスです。 037 * ここでは、ResultSetMetaData から、カラム数、カラム名(NAME列)、 038 * Type属性を取得し、ResultSet で、値を求める時に、Object型の 039 * 処理を行います。 040 * Object型としては、CLOB、ROWID、TIMESTAMP 型のみ取り扱っています。 041 * STRUCTタイプもサポートしますが、1レベルのみとします。(6.3.3.0 (2015/07/25)) 042 * 043 * @og.rev 7.0.0.0 (2017/07/07) 新規作成 044 * 045 * @version 7.0 046 * @author Kazuhiko Hasegawa 047 * @since JDK1.8, 048 */ 049public class ResultSetValue implements AutoCloseable { 050 private static final XLogger LOGGER= XLogger.getLogger( ResultSetValue.class.getSimpleName() ); // ログ出力 051 052 private static final int BUFFER_MIDDLE = 10000; // 6.3.3.0 (2015/07/25) 053 054// /** システム依存の改行記号(String)。 */ 055// public static final String CR = System.getProperty("line.separator"); 056 057 private final ResultSet resultSet ; // 内部で管理する ResultSet オブジェクト 058 private final List<ColumnInfo> clmInfos ; 059 060 private boolean skipNext ; // STRUCT 使用時に、next() してデータ構造を取得するため。 061 private boolean firstNext ; // STRUCT 使用時に、next() してデータ構造を取得するため。 062 063 /** 064 * ResultSet を引数にとるコンストラクタ 065 * 066 * ここで、カラムサイズ、カラム名、java.sql.Types の定数定義 を取得します。 067 * STRUCTタイプもサポートしますが、1レベルのみとします。 068 * つまり、Object型のカラムに、Object型を定義した場合、ここでは取り出すことができません。 069 * また、Object型は、継承関係を構築できるため、個々のオブジェクトの要素数は異なります。 070 * 一番最初のレコードのオブジェクト数を元に、算出しますので、ご注意ください。 071 * 072 * @param res 内部で管理するResultSetオブジェクト 073 * @throws java.sql.SQLException データベース・アクセス・エラーが発生した場合 074 */ 075 public ResultSetValue( final ResultSet res ) throws SQLException { 076 resultSet = res; 077 078 final ResultSetMetaData metaData = resultSet.getMetaData(); 079 final int clmSize = metaData.getColumnCount(); 080 081 clmInfos = new ArrayList<>(); 082 083 for( int i=0; i<clmSize; i++ ) { 084 final int clmNo = i+1; 085 final int type = metaData.getColumnType( clmNo ); 086 final String name = metaData.getColumnLabel( clmNo ).toUpperCase(Locale.JAPAN) ; 087 if( type == Types.STRUCT && DBUtil.isOracle() ) { 088 if( !skipNext ) { // オブジェクト型を取得する為、データを取る必要がある。 089 skipNext = true; 090 firstNext = resultSet.next(); // 初めての next() の結果を保持(falseなら、データなし) 091 } 092 if( firstNext ) { 093 // 最初のオブジェクトのタイプを基準にする。 094 final Object obj = resultSet.getObject( clmNo ); 095 if( obj != null ) { 096 // 6.3.8.0 (2015/09/11) Oracle Database 12cリリース1 (12.1)以降、StructDescriptor は非推奨 097 final OracleTypeMetaData omd = ((OracleStruct)obj).getOracleMetaData(); 098 final ResultSetMetaData md = ((OracleTypeMetaData.Struct)omd).getMetaData(); 099 100 final int mdsize = md.getColumnCount(); 101 for( int j=0; j<mdsize; j++ ) { 102 final int objNo = j+1; 103 // カラム名.オブジェクトカラム名 104 final String name2 = name + '.' + md.getColumnLabel(objNo).toUpperCase(Locale.JAPAN); 105 final int type2 = md.getColumnType( objNo ); 106 final int size2 = md.getColumnDisplaySize( objNo ); 107 final boolean isWrit2 = md.isWritable( objNo ); 108 clmInfos.add( new ColumnInfo( name2,type2,size2,isWrit2,clmNo,j ) ); // ※ objNo でなく、「j」 109 } 110 } 111 } 112 } 113 else { 114 final int size = metaData.getColumnDisplaySize( clmNo ); 115 final boolean isWrit = metaData.isWritable( clmNo ); 116 clmInfos.add( new ColumnInfo( name,type,size,isWrit,clmNo,-1 ) ); // ※ objNo でなく、「-1」 117 } 118 } 119 } 120 121 /** 122 * ResultSetMetaData で求めた、カラム数を返します。 123 * 124 * @return カラム数(データの列数) 125 */ 126 public int getColumnCount() { 127 return clmInfos.size(); 128 } 129 130 /** 131 * カラム名配列を返します。 132 * 133 * 配列は、0から始まり、カラム数-1 までの文字型配列に設定されます。 134 * カラム名は、ResultSetMetaData#getColumnLabel(int) を toUpperCase した 135 * 大文字が返されます。 136 * 137 * @return カラム名配列 138 * @og.rtnNotNull 139 */ 140 public String[] getNames() { 141 return clmInfos.stream().map( info -> info.getName() ).toArray( String[]::new ); 142 } 143 144 /** 145 * 指定のカラム番号のカラム名を返します。 146 * 147 * カラム名を取得する、カラム番号は、0から始まり、カラム数-1 までの数字で指定します。 148 * データベース上の、1から始まる番号とは、異なります。 149 * カラム名は、ResultSetMetaData#getColumnLabel(int) を toUpperCase した 150 * 大文字が返されます。 151 * 152 * @param clmNo カラム番号 (0から始まり、カラム数-1までの数字) 153 * @return 指定のカラム番号のカラム名 154 */ 155 public String getColumnName( final int clmNo ) { 156 return clmInfos.get( clmNo ).name ; 157 } 158 159 /** 160 * 指定のカラム番号のサイズを返します。 161 * 162 * カラムのサイズは、ResultSetMetaData#getColumnDisplaySize(int) の値です。 163 * 164 * @param clmNo カラム番号 (0から始まり、カラム数-1までの数字) 165 * @return 指定のカラム番号のサイズ 166 */ 167 public int getColumnDisplaySize( final int clmNo ) { 168 return clmInfos.get( clmNo ).size ; 169 } 170 171 /** 172 * 指定の書き込み可能かどうかを返します。 173 * 174 * カラムの書き込み可能かどうかは、ResultSetMetaData#isWritable(int) の値です。 175 * 176 * @param clmNo カラム番号 (0から始まり、カラム数-1までの数字) 177 * @return 書き込み可能かどうか 178 */ 179 public boolean isWritable( final int clmNo ) { 180 return clmInfos.get( clmNo ).isWrit ; 181 } 182 183 /** 184 * カーソルを現在の位置から順方向に1行移動します。 185 * 186 * ResultSet#next() を呼び出しています。 187 * 結果は,すべて文字列に変換されて格納されます。 188 * 189 * @return 新しい現在の行が有効である場合はtrue、行がそれ以上存在しない場合はfalse 190 * @see java.sql.ResultSet#next() 191 * @throws java.sql.SQLException データベース・アクセス・エラーが発生した場合、またはこのメソッドがクローズされた結果セットで呼び出された場合 192 */ 193 public boolean next() throws SQLException { 194 if( skipNext ) { skipNext = false; return firstNext; } // STRUCTタイプ取得時に、一度 next() している。 195 return resultSet.next(); 196 } 197 198 /** 199 * 現在のカーソル位置にあるレコードのカラム番号のデータを取得します。 200 * 201 * ResultSet#getObject( clmNo+1 ) を呼び出しています。 202 * 引数のカラム番号は、0から始まりますが、ResultSet のカラム順は、1から始まります。 203 * 指定は、0から始まるカラム番号です。 204 * 結果は,すべて文字列に変換されて返されます。 205 * また、null オブジェクトの場合も、ゼロ文字列に変換されて返されます。 206 * 207 * @param clmNo カラム番号 (0から始まり、カラム数-1までの数字) 208 * @return 現在行のカラム番号のデータ(文字列) 209 * @throws java.sql.SQLException データベース・アクセス・エラーが発生した場合 210 * @og.rtnNotNull 211 */ 212 public String getValue( final int clmNo ) throws SQLException { 213 final ColumnInfo clmInfo = clmInfos.get( clmNo ) ; // 内部カラム番号に対応したObject 214 final int dbClmNo = clmInfo.clmNo ; // データベース上のカラム番号(+1済み) 215 216 final String val ; 217 final Object obj = resultSet.getObject( dbClmNo ); 218 219 if( obj == null ) { 220 val = ""; 221 } 222 else if( clmInfo.isStruct ) { 223 final Object[] attrs = ((Struct)obj).getAttributes(); 224 final int no = clmInfo.objNo; 225 val = no < attrs.length ? String.valueOf( attrs[no] ) : "" ; // 配列オーバーする場合は、""(ゼロ文字列) 226 } 227 else if( clmInfo.isObject ) { 228 switch( clmInfo.type ) { 229 case Types.CLOB : val = getClobData( (Clob)obj ) ; 230 break; 231 case Types.ROWID: val = resultSet.getString( dbClmNo ); 232 break; 233 case Types.TIMESTAMP : val = StringUtil.getTimeFormat( ((Timestamp)obj).getTime() , "yyyyMMddHHmmss" ); 234 break; 235 default : val = String.valueOf( obj ); 236 break; 237 } 238 } 239 else { 240 val = String.valueOf( obj ); 241 } 242 243 return val ; 244 } 245 246 /** 247 * 現在のカーソル位置にあるレコードの全カラムデータを取得します。 248 * 249 * 個々のカラムの値も、null を含みません。(ゼロ文字列になっています) 250 * 251 * #getValue( clmNo ) を、0から、カラム数-1 まで呼び出して求めた文字列配列を返します。 252 * 253 * @return 現在行の全カラムデータの文字列配列 254 * @throws java.sql.SQLException データベース・アクセス・エラーが発生した場合 255 * @og.rtnNotNull 256 */ 257 public String[] getValues() throws SQLException { 258 final String[] vals = new String[clmInfos.size()]; 259 260 for( int i=0; i<vals.length; i++ ) { 261 vals[i] = getValue( i ); 262 } 263 264 return vals ; 265 } 266 267 /** 268 * タイプに応じて変換された、Numberオブジェクトを返します。 269 * 270 * 条件に当てはまらない場合は、null を返します。 271 * org.opengion.hayabusa.io.HybsJDBCCategoryDataset2 から移動してきました。 272 * これは、検索結果をグラフ化する為の 値を取得する為のメソッドですので、 273 * 数値に変換できない場合は、エラーになります。 274 * 275 * @param clmNo カラム番号 (0から始まり、カラム数-1までの数字) 276 * @return Numberオブジェクト(条件に当てはまらない場合は、null) 277 * @see java.sql.Types 278 * @throws java.sql.SQLException データベース・アクセス・エラーが発生した場合 279 * @throws RuntimeException 数字変換できなかった場合。 280 */ 281 public Number getNumber( final int clmNo ) throws SQLException { 282 final ColumnInfo clmInfo = clmInfos.get( clmNo ) ; // 内部カラム番号に対応したObject 283 final int dbClmNo = clmInfo.clmNo ; // データベース上のカラム番号(+1済み) 284 285 Number value = null; 286 287 Object obj = resultSet.getObject( dbClmNo ); 288 if( obj != null ) { 289 if( clmInfo.isStruct ) { 290 final Object[] attrs = ((Struct)obj).getAttributes(); 291 final int no = clmInfo.objNo; 292 obj = no < attrs.length ? attrs[no] : null ; // 配列オーバーする場合は、null 293 if( obj == null ) { return value; } // 配列外 or 取出した結果が null の場合、処理を中止。 294 } 295 296 switch( clmInfo.type ) { 297 case Types.TINYINT: 298 case Types.SMALLINT: 299 case Types.INTEGER: 300 case Types.BIGINT: 301 case Types.FLOAT: 302 case Types.DOUBLE: 303 case Types.DECIMAL: 304 case Types.NUMERIC: 305 case Types.REAL: { 306 value = (Number)obj; 307 break; 308 } 309 case Types.DATE: 310 case Types.TIME: { 311 value = Long.valueOf( ((Date)obj).getTime() ); 312 break; 313 } 314 // 5.6.2.1 (2013/03/08) Types.DATE と Types.TIMESTAMP で処理を分けます。 315 case Types.TIMESTAMP: { 316 value = Long.valueOf( ((Timestamp)obj).getTime() ); 317 break; 318 } 319 case Types.CHAR: 320 case Types.VARCHAR: 321 case Types.LONGVARCHAR: { 322 final String str = (String)obj; 323 try { 324 value = Double.valueOf(str); 325 } 326 catch ( final NumberFormatException ex ) { 327// final String errMsg = "数字変換できませんでした。in=" + str 328// + CR + ex.getMessage() ; 329// throw new RuntimeException( errMsg,ex ); 330// // suppress (value defaults to null) 331 // MSG0031 = 数字変換できませんでした。\n\tメッセージ=[{0}] 332 final String errMsg = "in=" + str + " , clmNo=" + dbClmNo + " , CLM=" + clmInfo.getName() ; // 1.4.0 (2019/10/01) 333 throw MsgUtil.throwException( ex , "MSG0031" , errMsg ); 334 } 335 break; 336 } 337 default: 338 // not a value, can't use it (defaults to null) 339 break; 340 } 341 } 342 return value; 343 } 344 345 /** 346 * カラムのタイプを表現する文字列値を返します。 347 * 348 * この文字列を用いて、CCSファイルでタイプごとの表示方法を 349 * 指定することができます。 350 * 現時点では、VARCHAR2,LONG,NUMBER,DATE,CLOB,NONE のどれかにあてはめます。 351 * 352 * @param clmNo カラム番号 (0から始まり、カラム数-1までの数字) 353 * @return カラムのタイプを表現する文字列値 354 * @see java.sql.Types 355 * @og.rtnNotNull 356 */ 357 public String getClassName( final int clmNo ) { 358 final String rtn ; 359 360 switch( clmInfos.get( clmNo ).type ) { 361 case Types.CHAR: 362 case Types.VARCHAR: 363 case Types.BIT: 364 rtn = "VARCHAR2"; break; 365 case Types.LONGVARCHAR: 366 rtn = "LONG"; break; 367 case Types.TINYINT: 368 case Types.SMALLINT: 369 case Types.INTEGER: 370 case Types.NUMERIC: 371 case Types.BIGINT: 372 case Types.FLOAT: 373 case Types.DOUBLE: 374 case Types.REAL: 375 case Types.DECIMAL: 376 rtn = "NUMBER"; break; 377 case Types.DATE: 378 case Types.TIME: 379 case Types.TIMESTAMP: 380 rtn = "DATE"; break; 381 case Types.CLOB: 382 rtn = "CLOB"; break; 383 case Types.STRUCT: // 6.3.3.0 (2015/07/25) 内部分解されない2レベル以上の場合のみ 384 rtn = "STRUCT"; break; 385 default: 386 rtn = "NONE"; break; 387 } 388 389 return rtn; 390 } 391 392 /** 393 * try-with-resourcesブロックで、自動的に呼ばれる AutoCloseable の実装。 394 * 395 * コンストラクタで渡された ResultSet を close() します。 396 * 397 * @og.rev 6.4.2.1 (2016/02/05) 新規作成。try-with-resourcesブロックで、自動的に呼ばれる AutoCloseable の実装。 398 * 399 * @see java.lang.AutoCloseable#close() 400 */ 401 @Override // AutoCloseable 402 public void close() { 403 try { 404 if( resultSet != null ) { resultSet.close(); } 405 } 406 catch( final SQLException ex ) { 407 // MSG0020 = ResultSet を close することが出来ませんでした。{0} : {1} 408// MsgUtil.errPrintln( ex , "MSG0020" , ex.getSQLState() , ex.getMessage() ); 409 final String errMsg = "ResultSetValue#close : " + ex.getErrorCode() ; 410 LOGGER.warning( ex , "MSG0020" , ex.getSQLState() , errMsg ); 411 } 412 catch( final RuntimeException ex ) { 413 // MSG0021 = 予期せぬエラーが発生しました。\n\tメッセージ=[{0}] 414// MsgUtil.errPrintln( ex , "MSG0021" , ex.getMessage() ); 415 LOGGER.warning( ex , "MSG0021" , "ResultSetValue#close" ); 416 } 417 } 418 419 /** 420 * Clob オブジェクトから文字列を取り出します。 421 * 422 * @og.rev 6.0.4.0 (2014/11/28) 新規作成: org.opengion.hayabusa.db.DBUtil#getClobData( Clob ) から移動 423 * 424 * @param clobData Clobオブジェクト 425 * @return Clobオブジェクトから取り出した文字列 426 * @throws SQLException データベースアクセスエラー 427 * @throws RuntimeException 入出力エラーが発生した場合 428 * @og.rtnNotNull 429 */ 430 private String getClobData( final Clob clobData ) throws SQLException { 431 if( clobData == null ) { return ""; } 432 433 final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE ); 434 435 Reader reader = null; 436 try { 437 reader = clobData.getCharacterStream(); 438 final char[] ch = new char[BUFFER_MIDDLE]; // char配列とBuilderの初期値は無関係。 439 int len ; 440 while( (len = reader.read( ch )) >= 0 ) { 441 buf.append( ch,0,len ); 442 } 443 } 444 catch( final IOException ex ) { 445 // MSG0022 = CLOBデータの読み込みに失敗しました。メッセージ=[{0}] 446 throw MsgUtil.throwException( ex , "MSG0022" , ex.getMessage() ); 447 } 448 finally { 449 try { 450 if( reader != null ) { reader.close(); } 451 } 452 catch( final IOException ex ) { 453 // MSG0023 = ストリーム close 処理でエラーが発生しました。メッセージ=[{0}] 454// MsgUtil.errPrintln( ex , "MSG0023" , ex.getMessage() ); 455 LOGGER.warning( ex , "MSG0023" , "" ); 456 } 457 catch( final RuntimeException ex ) { 458 // MSG0021 = 予期せぬエラーが発生しました。\n\tメッセージ=[{0}] 459// MsgUtil.errPrintln( ex , "MSG0021" , ex.getMessage() ); 460 LOGGER.warning( ex , "MSG0021" , "ResultSetValue#getClobData" ); 461 } 462 } 463 464 return buf.toString(); 465 } 466 467 /** 468 * 各種カラム属性の管理を、クラスで行うようにします。 469 * 470 * @og.rev 6.3.3.0 (2015/07/25) STRUCTタイプの対応 471 * 472 * @param clobData Clobオブジェクト 473 * @return Clobオブジェクトから取り出した文字列 474 */ 475 private static final class ColumnInfo { 476 private final String name ; // カラム名(ResultSetMetaData#getColumnLabel(int) の toUpperCase) 477 private final int type ; // java.sql.Types の定数定義 478 private final int size ; // カラムサイズ(ResultSetMetaData#getColumnDisplaySize(int)) 479 private final boolean isWrit ; // 書き込み許可(ResultSetMetaData#isWritable(int)) 480 private final int clmNo ; // ResultSet での元のカラムNo( 1から始まる番号 ) 481 private final int objNo ; // STRUCT での配列番号( 0から始まる番号 ) 482 private final boolean isStruct ; // オリジナルのタイプが、Struct型 かどうか。 483 private final boolean isObject ; // タイプが、CLOB,ROWID,TIMESTAMP かどうか。 484 485 /** 486 * 引数付コンストラクター 487 * 488 * @og.rev 6.3.3.0 (2015/07/25) STRUCTタイプの対応 489 * 490 * @param name カラム名(ResultSetMetaData#getColumnLabel(int) の toUpperCase) 491 * @param type java.sql.Types の定数定義 492 * @param size カラムサイズ(ResultSetMetaData#getColumnDisplaySize(int)) 493 * @param isWrit 書き込み許可(ResultSetMetaData#isWritable(int)) 494 * @param clmNo ResultSet での元のカラムNo( 1から始まる番号 ) 495 * @param objNo STRUCT での配列番号( 0から始まる番号 ) 496 */ 497 ColumnInfo( final String name , final int type , final int size , final boolean isWrit , final int clmNo , final int objNo ) { 498 this.name = name ; 499 this.type = type ; 500 this.size = size ; 501 this.isWrit = isWrit; 502 this.clmNo = clmNo; 503 this.objNo = objNo; 504 isStruct = objNo >= 0; // Struct型かどうかは、配列番号で判定する。 505 isObject = type == Types.CLOB || type == Types.ROWID || type == Types.TIMESTAMP ; 506 } 507 508 /** 509 * カラム名を返します。 510 * 511 * @og.rev 6.3.3.0 (2015/07/25) STRUCTタイプの対応 512 * 513 * @return カラム名 514 */ 515 public String getName() { return name; } 516 } 517}