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