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.hayabusa.io;
017
018import static org.opengion.fukurou.system.HybsConst.CR ;                        // 6.1.0.0 (2014/12/26)
019import static org.opengion.fukurou.system.HybsConst.DB_FETCH_SIZE;      // 6.9.4.1 (2018/04/09)
020import org.opengion.fukurou.system.LogWriter;
021import org.opengion.fukurou.util.ColorMap;                      // 6.0.2.2 (2014/10/03)
022import org.opengion.fukurou.db.ResultSetValue;          // 6.0.4.0 (2014/11/28)
023import org.opengion.hayabusa.db.DBTableModel;
024// import org.opengion.hayabusa.common.HybsSystem;              // 6.9.3.0 (2018/03/26)6.9.3.0 (2018/03/26)
025
026import java.sql.Connection;
027import java.sql.ResultSet;
028import java.sql.SQLException;
029import java.sql.Statement;
030
031import java.util.List;
032import java.util.ArrayList;
033import java.util.Arrays;
034import java.util.Set;
035import java.util.HashSet;
036
037import java.awt.Color;                                                          // 6.0.2.2 (2014/10/03)
038
039import org.jfree.data.Range;
040import org.jfree.data.category.DefaultCategoryDataset;
041
042/**
043 * HybsCategoryDataset は、org.jfree.data.category.DefaultCategoryDataset を継承したサブクラスで、
044 * HybsDataset インターフェースの実装クラスになっています。
045 * これは、JDBCCategoryDatasetの データベース機能と、DBTableModel から Dataset を作成する機能を
046 * 兼ね備えています。
047 * HybsDataset インターフェースは、シリーズのラベル指定、カテゴリカラーバー、パレート図用積上げ
048 * 計算などの処理を行うための、インターフェースで、それらの処理も、HybsCategoryDataset に実装します。
049 *
050 * このクラスでは、検索結果を内部で持っておき、getValue(int row, int column)
051 * メソッドで直接値を返します。
052 *
053 * select category,series1,series2,series3,・・・ from ・・・
054 * series の横持ち(標準と同じ) 対応です。
055 * category カラムの値は、カテゴリのラベルになり、series1,2,3 のラベルがシリーズラベル、値が
056 * seriesの値になります。
057 *
058 * カテゴリのカラー名の指定を行う場合、最後のカラムが、カラー名の文字列になります。
059 * select category,series1,series2,series3,・・・,color from ・・・
060 * color文字列の検索結果は、Dataset には含まれません。
061 *
062 * その場合、color カラムがシリーズとして認識されない様に、ChartDatasetTag で、useCategoryColor="true"
063 * を指定しておく必要があります。このフラグは、HybsCategoryDataset を使う処理以外では効果が
064 * ありません(シリーズとして使用されてしまう)のでご注意ください。
065 * このフラグは、カテゴリカラーバーを使う場合には必要ですが、カテゴリカラーバーと(例えばパレート図)
066 * を合成する場合に、パレート図側にも useCategoryColor="true" を設定しておけば、同じSQL または、
067 * DBTableModel を使う事ができるというためのフラグです。
068 *
069 * なお、Colorコードは、このクラスで作成しますが、Renderer に与える必要があります。
070 * 通常のRenderer には、categoryにカラーを指定する機能がありませんので、HybsBarRenderer に
071 * setCategoryColor( Color[] ) メソッドを用意します。(正確には、HybsDrawItem インターフェース)
072 * このRenderer で、getItemPaint( int  , int )メソッドをオーバーライドすることで、カテゴリごとの
073 * 色を返します。
074 *
075 * @og.rev 6.0.2.2 (2014/10/03) 新規追加
076 *
077 * @version  6.0.2.2 (2014/10/03)
078 * @author   Kazuhiko Hasegawa
079 * @since    JDK1.6,
080 */
081public class HybsCategoryDataset extends DefaultCategoryDataset implements HybsDataset {
082        private static final long serialVersionUID = 602220141003L ;
083
084//      /** 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ  {@value} */
085//      private static final int DB_FETCH_SIZE = HybsSystem.sysInt( "DB_FETCH_SIZE" ) ;
086
087        private final Set<String> cateCheck     = new HashSet<>();                      // category の重複チェック
088        private final int       hsCode  = Long.valueOf( System.nanoTime() ).hashCode() ;        // 5.1.9.0 (2010/08/01) equals,hashCode
089
090        private String[]        seriesLabels            ;
091        private boolean         isColorCategory         ;                                               // 6.0.2.2 (2014/10/03)
092        private boolean         isParetoData            ;                                               // 6.0.2.2 (2014/10/03)
093
094        private Number[][]      numdata                         ;
095        private Color[]         categoryColor           ;
096        private Range           range                           = new Range( 0, 0 );    // 6.9.7.0 (2018/05/14) データ0件で、処理を抜けるので、初期値を与えておきます。
097
098        /**
099         * デフォルトコンストラクター
100         *
101         * @og.rev 6.4.2.0 (2016/01/29) PMD refactoring. Each class should declare at least one constructor.
102         */
103        public HybsCategoryDataset() { super(); }               // これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。
104
105        /**
106         * CategoryDataset を構築するに当たり、初期パラメータを設定します。
107         *
108         * @og.rev 6.0.2.2 (2014/10/03) 新規追加
109         *
110         * @param lbls  シリーズのラベル名配列
111         * @param isColCate  カテゴリのカラー名の指定有無(true:使用する)
112         * @param isPareto   パレート図用のDatasetとして処理するかどうか(true:処理する)
113         */
114        public void initParam( final String[] lbls , final boolean isColCate , final boolean isPareto ) {
115                // 6.0.2.5 (2014/10/31) refactoring
116                if( lbls != null ) { seriesLabels = lbls.clone(); }
117                isColorCategory = isColCate;
118                isParetoData    = isPareto;
119        }
120
121        /**
122         * コネクションと、SQL文字列から、CategoryDataset のデータを作成します。
123         * 元となる処理は、org.jfree.data.jdbc.JDBCCategoryDataset#executeQuery( Connection,String ) です。
124         *
125         * このメソッドでは、先に #initParam(String[],boolean,isPareto) のパラメータを使用して
126         * 検索した結果のデータを加工、処理します。
127         * また、内部的に、データをキャッシュする事と、データ範囲を示す レンジオブジェクト を作成します。
128         *
129         * @og.rev 6.0.2.2 (2014/10/03) 新規追加
130         * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
131         * @og.rev 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
132         * @og.rev 6.4.2.1 (2016/02/05) try-with-resources 文で記述。
133         * @og.rev 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズを設定。
134         * @og.rev 6.9.7.0 (2018/05/14) データ0件のときは、処理を中断します。
135         *
136         * @param conn  コネクション
137         * @param query  SQL文字列
138         *
139         * @throws SQLException データベースアクセス時のエラー
140         * @see         org.jfree.data.jdbc.JDBCCategoryDataset#executeQuery( Connection,String )
141         * @see         org.opengion.fukurou.db.ResultSetValue
142         */
143        public void execute( final Connection conn, final String query ) throws SQLException {
144
145                // Range を予め求めておきます。
146                double minimum = Double.POSITIVE_INFINITY;
147                double maximum = Double.NEGATIVE_INFINITY;
148                double sum     = 0.0d;                                  // 6.0.2.3 (2014/10/19) パレート図用合計
149
150                List<Color> colorList = null;                   // 6.0.2.2 (2014/10/03) カテゴリカラー
151
152                // 6.4.2.1 (2016/02/05) try-with-resources 文
153                try( Statement statement = conn.createStatement();
154                        final ResultSet resultSet = statement.executeQuery(query) ) {
155
156                        statement.setFetchSize( DB_FETCH_SIZE );                                // 6.9.3.0 (2018/03/26) データ検索時のフェッチサイズ
157
158                        // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
159                        final ResultSetValue rsv = new ResultSetValue( resultSet );
160
161                        int dataSize = rsv.getColumnCount() -1;                 // series の個数は、category 分を引いた数。
162                        if( isColorCategory ) {                                                 // ColorCategory使用時
163                                colorList       = new ArrayList<>();                    // カテゴリカラー
164                                dataSize--;                                                                     // 最終カラムが Colorコードなので、マイナスする。
165                        }
166
167                        if( dataSize<1 ) {
168                                final String errMsg = "JDBCCategoryDataset.executeQuery() : insufficient columns "
169                                                        + "returned from the database. \n"
170                                                        + " SQL=" + query ;
171                                throw new SQLException( errMsg );
172                        }
173
174                        // 6.0.2.0 (2014/09/19) シリーズのラベル名配列を使うときは、シリーズ数必要。
175                        if( seriesLabels != null && seriesLabels.length < dataSize ) {
176                                final String errMsg = "seriesLabels を使用する場合は、必ずシリーズ数以上指定してください。"
177                                                                + CR
178                                                                + " seriesLabels=" + Arrays.toString( seriesLabels )
179                                                                + CR
180                                                                + " seriesLabels.length=" + seriesLabels.length
181                                                                + " dataSize=" + dataSize
182                                                                + CR ;
183                                throw new IllegalArgumentException( errMsg );
184                        }
185
186                        String[] series  = new String[dataSize];
187                        // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
188                        final String[] names   = rsv.getNames();
189                        // ORACLEの引数は、配列+1から始まるので、metaDataはi+2から取得。series と、seriesLabels は0から始まる。
190                        for( int i=0; i<dataSize; i++ ) {
191                                // 6.4.1.1 (2016/01/16) PMD refactoring. Avoid if (x != y) ..; else ..;
192                                series[i] = seriesLabels == null || seriesLabels[i] == null
193                                                                        ? names[i+1]
194                                                                        : seriesLabels[i] ;
195                        }
196
197                        final List<Number[]> rowList = new ArrayList<>();
198                        // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
199                        while( rsv.next() ) {
200                                Number[] clmList = new Number[dataSize];
201                                // first column contains the row key...
202                                // 6.0.2.0 (2014/09/19) columnKeyは、series , rowKey は、category に変更する。
203                                final String category = uniqCategory( resultSet.getString(1) ); // 6.0.2.3 (2014/10/10) categoryの重複回避
204
205                                for( int i=0; i<dataSize; i++ ) {                                       // 6.0.2.2 (2014/10/03) dataSize 分回す。
206                                        Number value = null;
207                                        // 6.0.2.1 (2014/09/26) org.opengion.fukurou.db.DBUtil に、移動
208                                        try {
209                                                // JDBCのアドレス指定は、+2 する。(category 分と、アドレスが1から始まる為。)
210                                                // ResultSetValueのカラム番号は、+1 する。(category 分があるため)
211                                                value = rsv.getNumber( i+1 );
212                                        }
213                                        // 7.2.9.5 (2020/11/28) PMD:'catch' branch identical to 'SQLException' branch
214                                        catch( final SQLException | RuntimeException ex ) {             // 6.0.4.0 (2014/11/28) ResultSetValue を使用するので。
215                                                LogWriter.log( ex );
216                                        }
217//                                      catch( final SQLException ex ) {                // 6.0.4.0 (2014/11/28) ResultSetValue を使用するので。
218//                                              LogWriter.log( ex );
219//                                      }
220//                                      catch( final RuntimeException ex ) {
221//                                              LogWriter.log( ex );
222//                                      }
223
224                                        clmList[i] = value;
225                                        addValue(value, series[i], category);           // 6.0.2.0 (2014/09/19) columnKeyは、series , rowKey は、category に変更する。
226                                        // Range 求め
227                                        if( value != null ) {
228                                                final double dbl = value.doubleValue();
229                                                if( isParetoData ) {                                    // 6.0.2.3 (2014/10/19) パレート図用合計
230                                                        sum += dbl ;
231                                                } else {
232                                                        if( dbl     < minimum ) { minimum = dbl; }
233                                                        if( maximum < dbl     ) { maximum = dbl; }
234                                                }
235                                        }
236                                }
237                                rowList.add( clmList );
238                                // 6.0.2.2 (2014/10/03) ColorCategory は、最後のカラム
239                                if( isColorCategory ) {
240                                        // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
241                                        final String colStr = rsv.getValue(dataSize+1);                         // 最後のカラム
242                                        final Color color   = ColorMap.getColorInstance( colStr );      // 6.0.2.1 (2014/09/26) StringUtil → ColorMap
243                                        colorList.add( color );
244                                }
245                        }
246                        // 6.9.7.0 (2018/05/14) データ0件のときは、処理を中断します。
247                        if( rowList.isEmpty() ) { return; }
248
249                        numdata = rowList.toArray( new Number[dataSize][rowList.size()] );
250                }
251
252                // colorList が null でないかどうかで判定。
253                if( isColorCategory && colorList != null ) {
254                        categoryColor = colorList.toArray( new Color[colorList.size()] );
255                }
256
257                // 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
258                if( isParetoData ) {
259                        changeParetoData( sum );
260                        minimum = 0.0;
261                        maximum = 100.0;
262                }
263
264                range = new Range( minimum, maximum );
265        }
266
267        /**
268         * DBTableModelオブジェクトから、CategoryDataset のデータを作成します。
269         * openGionの独自処理メソッドです。
270         *
271         * このメソッドでは、先に #initParam(String[],boolean,isPareto) のパラメータを使用して
272         * 検索した結果のデータを加工、処理します。
273         * また、内部的に、データをキャッシュする事と、データ範囲を示す レンジオブジェクト を作成します。
274         *
275         * @og.rev 6.0.2.2 (2014/10/03) 新規追加
276         * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
277         * @og.rev 6.9.7.0 (2018/05/14) データ0件のときは、処理を中断します。
278         *
279         * @param table DBTableModelオブジェクト
280         * @see         #execute( Connection,String )
281         */
282        public void execute( final DBTableModel table ) {
283
284                // 6.9.7.0 (2018/05/14) データ0件のときは、処理を中断します。
285                if( table == null || table.getRowCount() == 0 ) { return; }
286
287                final int clmNo = table.getColumnCount();
288                final int rowNo = table.getRowCount();
289
290                // Range を予め求めておきます。
291                double minimum = Double.POSITIVE_INFINITY;
292                double maximum = Double.NEGATIVE_INFINITY;
293                double sum     = 0.0d;                                  // 6.0.2.3 (2014/10/19) パレート図用合計
294
295                int dataSize = clmNo -1;                                                // series の個数は、category 分を引いた数。
296                List<Color> colorList = null;                   // 6.0.2.2 (2014/10/03) カテゴリカラー
297                if( isColorCategory ) {                                                 // ColorCategory使用時
298                        colorList       = new ArrayList<>();            // カテゴリカラー
299                        dataSize--;                                                                     // 最終カラムが Colorコードなので、マイナスする。
300                }
301
302                numdata = new Number[rowNo][clmNo];
303
304                // ※ DBTableModel の row,col と、Dataset の row,col は、逆になっています。
305                for( int row=0; row<rowNo; row++ ) {
306                        final String   category = uniqCategory( table.getValue( row,0 ) );      // 6.0.2.3 (2014/10/10) categoryの重複回避
307                        final String[] vals     = table.getValues( row );
308                        for( int clm=0; clm<dataSize; clm++ ) {
309                                final String sval = vals[clm+1];                                // 2番目(アドレス=1)からカラムデータを取得
310                                final double val  = sval == null || sval.isEmpty() ? 0.0d : Double.parseDouble( sval ) ;                // 6.4.2.1 (2016/02/05) PMD refactoring. Useless parentheses.
311
312                                addValue( val , seriesLabels[clm] , category );         // val,row,clm
313                                numdata[row][clm] = Double.valueOf( val );                      // 6.0.2.4 (2014/10/17) 効率の悪いメソッド
314                                // Range 求め
315                                if( isParetoData ) {                                    // 6.0.2.3 (2014/10/19) パレート図用合計
316                                        sum += val ;
317                                } else {
318                                        if( val     < minimum ) { minimum = val; }
319                                        if( maximum < val     ) { maximum = val; }
320                                }
321                        }
322
323                        // 6.0.2.2 (2014/10/03) ColorCategory は、最後のカラム
324                        if( isColorCategory ) {
325                                final String colStr = vals[dataSize+1];                 // 最後のカラム
326                                final Color color   = ColorMap.getColorInstance( colStr );      // 6.0.2.1 (2014/09/26) StringUtil → ColorMap
327                                colorList.add( color );
328                        }
329                }
330
331                // colorList が null でないかどうかで判定。
332                if( isColorCategory && colorList != null ) {
333                        categoryColor = colorList.toArray( new Color[colorList.size()] );
334                }
335
336                // 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
337                if( isParetoData ) {
338                        changeParetoData( sum );
339                        minimum = 0.0;
340                        maximum = 100.0;
341                }
342
343                range = new Range( minimum, maximum );
344        }
345
346        /**
347         * 指定された行列から、数字オブジェクトを取得します。
348         *
349         * @param       row     行番号(シリーズ:横持=clm相当)
350         * @param       column  カラム番号(カテゴリ:縦持ち=row相当)
351         *
352         * @return      指定の行列の値
353         */
354        @Override
355        public Number getValue( final int row, final int column ) {
356                // 注意:行列の順序が逆です。
357                return numdata[column][row];
358        }
359
360        /**
361         * レンジオブジェクトを取得します。(独自メソッド)
362         *
363         * @return      レンジオブジェクト
364         */
365        public Range getRange() {
366                return range;
367        }
368
369        /**
370         * パレート図用のDatasetに値を書き換えます。(独自メソッド)
371         *
372         * 色々と方法はあると思いますが、簡易的に、内部の Number配列を
373         * 積上げ計算して、パレート図用のデータを作成します。
374         * レンジオブジェクト も変更します。
375         *
376         * ※ 注意:親クラスの内部に持っている実データは変更されていないので、
377         * 場合によっては、おかしな動きをするかもしれません。
378         * その場合は、上位にもデータをセットするように変更する必要があります。
379         *
380         * なお、行列の順序が、イメージと異なりますので、注意願います。
381         * (columnは、series , row は、category で、シリーズを積み上げます)
382         *
383         * @og.rev 6.0.2.1 (2014/09/26) 新規追加
384         * @og.rev 6.0.2.2 (2014/10/03) HybsDataset i/f
385         * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
386         *
387         * @param  sum データの合計
388         */
389        private void changeParetoData( final double sum ) {
390                if( numdata == null || numdata.length == 0 || numdata[0].length == 0 || sum == 0.0 ) { return ; }
391
392                final int rowCnt = numdata[0].length ;
393                final int clmCnt = numdata.length ;
394
395                for( int rowNo=0; rowNo<rowCnt; rowNo++ ) {                     // 行列が逆。
396                        double val = 0.0;               // 初期値
397                        for( int clmNo=0; clmNo<clmCnt; clmNo++ ) {             // 積上げ計算するカラムでループを回す。
398                                final Number v1Num = numdata[clmNo][rowNo];
399                                if( v1Num != null ) {
400                                        val += v1Num.doubleValue();                             // 積上げ計算は、元の値のままにしておきます。
401                                }
402                                // データをセットするときに、100分率にします。
403                                numdata[clmNo][rowNo] = Double.valueOf( Math.round( val * 1000.0 / sum ) / 10.0 );
404        // きちんと計算するなら、BigDecimal で、スケールを指定して四捨五入すべき・・・かも
405        //                      java.math.BigDecimal bd = new BigDecimal( val * 100.0 / sum );
406        //                      numdata[clmNo][rowNo] = bd.setScale( 1, java.math.RoundingMode.HALF_UP );
407                        }
408                }
409        }
410
411        /**
412         * categoryカラー配列を取得します。(独自メソッド)
413         *
414         * このクラスは、一番最後のカラムを、色文字列として処理し、categoryにColorを指定できます。
415         * select文で指定されていなかった場合は、null を返します。
416         *
417         * select category,series1,series2,series3,・・・,color from ・・・
418         *
419         * @og.rev 6.0.2.2 (2014/10/03) 新規追加
420         *
421         * なお、Colorコードは、このクラスで作成しますが、Renderer に与える必要があります。
422         * 通常のRenderer には、categoryにカラーを指定する機能がありませんので、HybsBarRenderer に
423         * setCategoryColor( Color[] ) メソッドを用意します。(正確には、HybsDrawItem インターフェース)
424         * このRenderer で、getItemPaint( int  , int )メソッドをオーバーライドすることで、カテゴリごとの
425         * 色を返します。
426         * この設定を行うと、シリーズは、カテゴリと同一色になります。
427         *
428         * @return      categoryカラー配列(なければ null)
429         */
430        public Color[] getCategoryColor() {
431                // 6.0.2.5 (2014/10/31) refactoring
432                return ( categoryColor == null ) ? null : categoryColor.clone();
433        }
434
435        /**
436         * category の重複をさけて、必要であれば、新しいカテゴリ名を作成します。
437         *
438         * カテゴリが同じ場合、JFreeChartでは、表示されません。これは、同じカテゴリと認識され
439         * 値が上書きされるためです。
440         * この問題は、なかなか気づきにくく、デバッグ等に時間がかかってしまいます。
441         * 重複チェックを行い、警告してもよいのですが、ここでは、新しいカテゴリ名を作成することで
442         * エラーを回避しつつ、とりあえずグラフ表示をするようにします。
443         *
444         * @og.rev 6.0.2.3 (2014/10/10) 新規追加
445         *
446         * @param       category        元のカテゴリ名
447         * @return      新しい元のカテゴリ名
448         */
449        private String uniqCategory( final String category ) {
450                String newCate = category ;
451                int i = 0;
452                while( !cateCheck.add( newCate ) ) {    // すでに存在している場合。
453                        newCate = category + "(" + (i++ ) + ")" ;
454                }
455
456                return newCate ;
457        }
458
459        /**
460         * この文字列と指定されたオブジェクトを比較します。
461         *
462         * 親クラスで、equals メソッドが実装されているため、警告がでます。
463         *
464         * @og.rev 5.1.8.0 (2010/07/01) findbug対応
465         * @og.rev 5.1.9.0 (2010/08/01) findbug対応
466         *
467         * @param       object  比較するオブジェクト
468         *
469         * @return      Objectが等しい場合は true、そうでない場合は false
470         */
471        @Override
472        public boolean equals( final Object object ) {
473                // 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
474                return super.equals( object ) && hsCode == ((HybsCategoryDataset)object).hsCode;
475        }
476
477        /**
478         * このオブジェクトのハッシュコードを取得します。
479         *
480         * @og.rev 5.1.8.0 (2010/07/01) findbug対応
481         * @og.rev 5.1.9.0 (2010/08/01) findbug対応
482         *
483         * @return      ハッシュコード
484         */
485        @Override
486        public int hashCode() { return hsCode ; }
487}