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         *
127         * @param con  コネクション
128         * @param query  SQL文字列
129         *
130         * @throws SQLException データベースアクセス時のエラー
131         * @see         org.jfree.data.jdbc.JDBCCategoryDataset#executeQuery( Connection,String )
132         * @see         org.opengion.fukurou.db.ResultSetValue
133         */
134        public void execute( final Connection con, final String query ) throws SQLException {
135
136                // Range を予め求めておきます。
137                double minimum = Double.POSITIVE_INFINITY;
138                double maximum = Double.NEGATIVE_INFINITY;
139                double sum     = 0.0d;                                  // 6.0.2.3 (2014/10/19) パレート図用合計
140
141                List<Color> colorList = null;                     // 6.0.2.2 (2014/10/03) カテゴリカラー
142
143                Statement statement = null;
144                ResultSet resultSet = null;
145                try {
146                        statement = con.createStatement();
147                        resultSet = statement.executeQuery(query);
148
149                        // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
150//                      ResultSetMetaData metaData = resultSet.getMetaData();
151                        final ResultSetValue rsv = new ResultSetValue( resultSet );
152
153//                      int dataSize = metaData.getColumnCount() -1;    // series の個数は、category 分を引いた数。
154                        int dataSize = rsv.getColumnCount() -1;                 // series の個数は、category 分を引いた数。
155                        if( isColorCategory ) {                                                 // ColorCategory使用時
156                                colorList       = new ArrayList<Color>();         // カテゴリカラー
157                                dataSize--;                                                                     // 最終カラムが Colorコードなので、マイナスする。
158                        }
159
160                        if( dataSize<1 ) {
161                                final String errMsg = "JDBCCategoryDataset.executeQuery() : insufficient columns "
162                                                        + "returned from the database. \n"
163                                                        + " SQL=" + query ;
164                                throw new SQLException( errMsg );
165                        }
166
167                        // 6.0.2.0 (2014/09/19) シリーズのラベル名配列を使うときは、シリーズ数必要。
168                        if( seriesLabels != null && seriesLabels.length < dataSize ) {
169                                final String errMsg = "seriesLabels を使用する場合は、必ずシリーズ数以上指定してください。"
170                                                                + CR
171                                                                + " seriesLabels=" + Arrays.toString( seriesLabels )
172                                                                + CR
173                                                                + " seriesLabels.length=" + seriesLabels.length
174                                                                + " dataSize=" + dataSize
175                                                                + CR ;
176                                throw new IllegalArgumentException( errMsg );
177                        }
178
179                        String[] series  = new String[dataSize];
180                        // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
181                        final String[] names   = rsv.getNames();
182//                      int[] columnType = new int[dataSize];
183                        // ORACLEの引数は、配列+1から始まるので、metaDataはi+2から取得。series と、seriesLabels は0から始まる。
184                        for( int i=0; i<dataSize; i++ ) {
185                                series[i] = seriesLabels != null && seriesLabels[i] != null
186                                                                        ? seriesLabels[i]
187//                                                                      : metaData.getColumnLabel(i+2).toUpperCase( Locale.JAPAN );
188                                                                        : names[i+1] ;
189//                              columnType[i] = metaData.getColumnType(i+2);
190                        }
191
192                        final List<Number[]> rowList = new ArrayList<Number[]>();
193                        // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。
194//                      while (resultSet.next()) {
195                        while (rsv.next()) {
196                                Number[] clmList = new Number[dataSize];
197                                // first column contains the row key...
198                                // 6.0.2.0 (2014/09/19) columnKeyは、series , rowKey は、category に変更する。
199//                              String category = resultSet.getString(1);                                       // 4.3.3.6 (2008/11/15) Generics警告対応
200                                final String category = uniqCategory( resultSet.getString(1) ); // 6.0.2.3 (2014/10/10) categoryの重複回避
201
202                                for( int i=0; i<dataSize; i++ ) {                                    // 6.0.2.2 (2014/10/03) dataSize 分回す。
203                                        Number value = null;
204                                        // 6.0.2.1 (2014/09/26) org.opengion.fukurou.db.DBUtil に、移動
205                                        try {
206                                                // JDBCのアドレス指定は、+2 する。(category 分と、アドレスが1から始まる為。)
207//                                              value = DBUtil.getNumber( columnType[i],resultSet.getObject(i+2) );
208                                                // ResultSetValueのカラム番号は、+1 する。(category 分があるため)
209                                                value = rsv.getNumber( i+1 );
210                                        }
211                                        catch( SQLException ex ) {              // 6.0.4.0 (2014/11/28) ResultSetValue を使用するので。
212                                                LogWriter.log( ex );
213                                        }
214                                        catch( 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//                                      String colStr = resultSet.getString(dataSize+2);                // 最後のカラム
236                                        final String colStr = rsv.getValue(dataSize+1);                         // 最後のカラム
237                                        final Color color   = ColorMap.getColorInstance( colStr );      // 6.0.2.1 (2014/09/26) StringUtil → ColorMap
238                                        colorList.add( color );
239                                }
240                        }
241                        numdata = rowList.toArray( new Number[dataSize][rowList.size()] );
242                }
243                finally {
244                        Closer.resultClose( resultSet );
245                        Closer.stmtClose( statement );
246                }
247
248                // colorList が null でないかどうかで判定。
249                if( isColorCategory && colorList != null ) {
250                        categoryColor = colorList.toArray( new Color[colorList.size()] );
251                }
252
253                // 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
254//              if( isParetoData ) { maximum = changeParetoData(); }
255                if( isParetoData ) {
256                        changeParetoData( sum );
257                        minimum = 0.0;
258                        maximum = 100.0;
259                }
260
261                range = new Range( minimum, maximum );
262        }
263
264        /**
265         * DBTableModelオブジェクトから、CategoryDataset のデータを作成します。
266         * openGionの独自処理メソッドです。
267         *
268         * このメソッドでは、先に #initParam(String[],boolean,isPareto) のパラメータを使用して
269         * 検索した結果のデータを加工、処理します。
270         * また、内部的に、データをキャッシュする事と、データ範囲を示す レンジオブジェクト を作成します。
271         *
272         * @og.rev 6.0.2.2 (2014/10/03) 新規追加
273         * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
274         *
275         * @param table DBTableModelオブジェクト
276         * @see         #execute( Connection,String )
277         */
278        public void execute( final DBTableModel table ) {
279                final int clmNo = table.getColumnCount();
280                final int rowNo = table.getRowCount();
281
282                // Range を予め求めておきます。
283                double minimum = Double.POSITIVE_INFINITY;
284                double maximum = Double.NEGATIVE_INFINITY;
285                double sum     = 0.0d;                                  // 6.0.2.3 (2014/10/19) パレート図用合計
286
287                int dataSize = clmNo -1;                                                // series の個数は、category 分を引いた数。
288                List<Color> colorList = null;                     // 6.0.2.2 (2014/10/03) カテゴリカラー
289                if( isColorCategory ) {                                                 // ColorCategory使用時
290                        colorList       = new ArrayList<Color>();         // カテゴリカラー
291                        dataSize--;                                                                     // 最終カラムが Colorコードなので、マイナスする。
292                }
293
294                numdata = new Number[rowNo][clmNo];
295
296                // ※ DBTableModel の row,col と、Dataset の row,col は、逆になっています。
297                for( int row=0; row<rowNo; row++ ) {
298//                      String   category = table.getValue( row,0 );                                    // 1番目(アドレス=0)はカラムの設定値
299                        final String   category = uniqCategory( table.getValue( row,0 ) );      // 6.0.2.3 (2014/10/10) categoryの重複回避
300                        final String[] vals     = table.getValues( row );
301                        for( int clm=0; clm<dataSize; clm++ ) {
302                                final String sval = vals[clm+1];                                // 2番目(アドレス=1)からカラムデータを取得
303                                final double val  = ( sval == null || sval.isEmpty() ) ? 0.0d : Double.parseDouble( sval ) ;
304
305                                addValue( val , seriesLabels[clm] , category );         // val,row,clm
306//                              numdata[row][clm] = new Double( val );
307                                numdata[row][clm] = Double.valueOf( val );                      // 6.0.2.4 (2014/10/17) 効率の悪いメソッド
308                                // Range 求め
309                                if( isParetoData ) {                                    // 6.0.2.3 (2014/10/19) パレート図用合計
310                                        sum += val ;
311                                } else {
312                                        if( val     < minimum ) { minimum = val; }
313                                        if( maximum < val     ) { maximum = val; }
314                                }
315                        }
316
317                        // 6.0.2.2 (2014/10/03) ColorCategory は、最後のカラム
318                        if( isColorCategory ) {
319                                final String colStr = vals[dataSize+1];                 // 最後のカラム
320                                final Color color   = ColorMap.getColorInstance( colStr );      // 6.0.2.1 (2014/09/26) StringUtil → ColorMap
321                                colorList.add( color );
322                        }
323                }
324
325                // colorList が null でないかどうかで判定。
326                if( isColorCategory && colorList != null ) {
327                        categoryColor = colorList.toArray( new Color[colorList.size()] );
328                }
329
330                // 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
331//              if( isParetoData ) { maximum = changeParetoData(); }
332                if( isParetoData ) {
333                        changeParetoData( sum );
334                        minimum = 0.0;
335                        maximum = 100.0;
336                }
337
338                range = new Range( minimum, maximum );
339        }
340
341        /**
342         * 指定された行列から、数字オブジェクトを取得します。
343         *
344         * @param       row     行番号(シリーズ:横持=clm相当)
345         * @param       column  カラム番号(カテゴリ:縦持ち=row相当)
346         *
347         * @return      指定の行列の値
348         */
349        @Override
350        public Number getValue( final int row, final int column ) {
351                // 注意:行列の順序が逆です。
352                return numdata[column][row];
353        }
354
355        /**
356         * レンジオブジェクトを取得します。(独自メソッド)
357         *
358         * @return      レンジオブジェクト
359         */
360        public Range getRange() {
361                return range;
362        }
363
364        /**
365         * パレート図用のDatasetに値を書き換えます。(独自メソッド)
366         *
367         * 色々と方法はあると思いますが、簡易的に、内部の Number配列を
368         * 積上げ計算して、パレート図用のデータを作成します。
369         * レンジオブジェクト も変更します。
370         *
371         * ※ 注意:親クラスの内部に持っている実データは変更されていないので、
372         * 場合によっては、おかしな動きをするかもしれません。
373         * その場合は、上位にもデータをセットするように変更する必要があります。
374         *
375         * なお、行列の順序が、イメージと異なりますので、注意願います。
376         * (columnは、series , row は、category で、シリーズを積み上げます)
377         *
378         * @og.rev 6.0.2.1 (2014/09/26) 新規追加
379         * @og.rev 6.0.2.2 (2014/10/03) HybsDataset i/f
380         * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。
381         *
382         * @param  sum データの合計
383         */
384//      private double changeParetoData() {
385        private void changeParetoData( final double sum ) {
386//              if( numdata == null || numdata.length == 0 || numdata[0].length == 0 ) { return 0.0d; }
387                if( numdata == null || numdata.length == 0 || numdata[0].length == 0 || sum == 0.0 ) { return ; }
388
389                final int rowCnt = numdata[0].length ;
390                final int clmCnt = numdata.length ;
391
392//              double maximum = Double.NEGATIVE_INFINITY;
393                for( int rowNo=0; rowNo<rowCnt; rowNo++ ) {                  // 行列が逆。
394                        double val = 0.0;               // 初期値
395                        for( int clmNo=0; clmNo<clmCnt; clmNo++ ) {          // 積上げ計算するカラムでループを回す。
396                                final Number v1Num = numdata[clmNo][rowNo];
397                                if(v1Num != null) {
398                                        val += v1Num.doubleValue();                             // 積上げ計算は、元の値のままにしておきます。
399                                }
400                                // データをセットするときに、100分率にします。
401//                              numdata[clmNo][rowNo] = new Double(val);
402                                numdata[clmNo][rowNo] = Double.valueOf( Math.round( val * 1000.0 / sum ) / 10.0 );
403        // きちんと計算するなら、BigDecimal で、スケールを指定して四捨五入すべき・・・かも
404        //                      java.math.BigDecimal bd = new BigDecimal( val * 100.0 / sum );
405        //                      numdata[clmNo][rowNo] = bd.setScale( 1, java.math.RoundingMode.HALF_UP );
406                        }
407//                      if( maximum < val ) { maximum = val; }                       // パレート図用の積上げなので、最大値は、最後のデータ
408                }
409
410//              return maximum;
411        }
412
413        /**
414         * categoryカラー配列を取得します。(独自メソッド)
415         *
416         * このクラスは、一番最後のカラムを、色文字列として処理し、categoryにColorを指定できます。
417         * select文で指定されていなかった場合は、null を返します。
418         *
419         * select category,series1,series2,series3,・・・,color from ・・・
420         *
421         * @og.rev 6.0.2.2 (2014/10/03) 新規追加
422         *
423         * なお、Colorコードは、このクラスで作成しますが、Renderer に与える必要があります。
424         * 通常のRenderer には、categoryにカラーを指定する機能がありませんので、HybsBarRenderer に
425         * setCategoryColor( Color[] ) メソッドを用意します。(正確には、HybsDrawItem インターフェース)
426         * このRenderer で、getItemPaint( int  , int )メソッドをオーバーライドすることで、カテゴリごとの
427         * 色を返します。
428         * この設定を行うと、シリーズは、カテゴリと同一色になります。
429         *
430         * @return      categoryカラー配列(なければ null)
431         */
432        public Color[] getCategoryColor() {
433                // 6.0.2.5 (2014/10/31) refactoring
434//              return categoryColor;
435                return ( categoryColor == null ) ? null : categoryColor.clone();
436        }
437
438        /**
439         * category の重複をさけて、必要であれば、新しいカテゴリ名を作成します。
440         *
441         * カテゴリが同じ場合、JFreeChartでは、表示されません。これは、同じカテゴリと認識され
442         * 値が上書きされるためです。
443         * この問題は、なかなか気づきにくく、デバッグ等に時間がかかってしまいます。
444         * 重複チェックを行い、警告してもよいのですが、ここでは、新しいカテゴリ名を作成することで
445         * エラーを回避しつつ、とりあえずグラフ表示をするようにします。
446         *
447         * @og.rev 6.0.2.3 (2014/10/10) 新規追加
448         *
449         * @param       category        元のカテゴリ名
450         * @return      新しい元のカテゴリ名
451         */
452        private String uniqCategory( final String category ) {
453                String newCate = category ;
454                int i = 0;
455                while( !cateCheck.add( newCate ) ) {    // すでに存在している場合。
456                        newCate = category + "(" + (i++) + ")" ;
457                }
458
459                return newCate ;
460        }
461
462        /**
463         * この文字列と指定されたオブジェクトを比較します。
464         *
465         * 親クラスで、equals メソッドが実装されているため、警告がでます。
466         *
467         * @og.rev 5.1.8.0 (2010/07/01) findbug対応
468         * @og.rev 5.1.9.0 (2010/08/01) findbug対応
469         *
470         * @param       object  比較するオブジェクト
471         *
472         * @return      Objectが等しい場合は true、そうでない場合は false
473         */
474        @Override
475        public boolean equals( final Object object ) {
476                if( super.equals( object ) ) {
477                        return hsCode == ((HybsCategoryDataset)object).hsCode;
478                }
479                return false;
480        }
481
482        /**
483         * このオブジェクトのハッシュコードを取得します。
484         *
485         * @og.rev 5.1.8.0 (2010/07/01) findbug対応
486         * @og.rev 5.1.9.0 (2010/08/01) findbug対応
487         *
488         * @return      ハッシュコード
489         */
490        @Override
491        public int hashCode() { return hsCode ; }
492}