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