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}