    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) 内部分解されない２レベル以上の場合のみ
    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}