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