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( final 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 catch( final SQLException ex ) { // 6.0.4.0 (2014/11/28) ResultSetValue を使用するので。 214 LogWriter.log( ex ); 215 } 216 catch( final RuntimeException ex ) { 217 LogWriter.log( ex ); 218 } 219 220 clmList[i] = value; 221 addValue(value, series[i], category); // 6.0.2.0 (2014/09/19) columnKeyは、series , rowKey は、category に変更する。 222 // Range 求め 223 if( value != null ) { 224 final double dbl = value.doubleValue(); 225 if( isParetoData ) { // 6.0.2.3 (2014/10/19) パレート図用合計 226 sum += dbl ; 227 } else { 228 if( dbl < minimum ) { minimum = dbl; } 229 if( maximum < dbl ) { maximum = dbl; } 230 } 231 } 232 } 233 rowList.add( clmList ); 234 // 6.0.2.2 (2014/10/03) ColorCategory は、最後のカラム 235 if( isColorCategory ) { 236 // 6.0.4.0 (2014/11/28) ResultSetValue を使用するように変更。 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 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 ) { 255 changeParetoData( sum ); 256 minimum = 0.0; 257 maximum = 100.0; 258 } 259 260 range = new Range( minimum, maximum ); 261 } 262 263 /** 264 * DBTableModelオブジェクトから、CategoryDataset のデータを作成します。 265 * openGionの独自処理メソッドです。 266 * 267 * このメソッドでは、先に #initParam(String[],boolean,isPareto) のパラメータを使用して 268 * 検索した結果のデータを加工、処理します。 269 * また、内部的に、データをキャッシュする事と、データ範囲を示す レンジオブジェクト を作成します。 270 * 271 * @og.rev 6.0.2.2 (2014/10/03) 新規追加 272 * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。 273 * @og.rev 6.9.7.0 (2018/05/14) データ0件のときは、処理を中断します。 274 * 275 * @param table DBTableModelオブジェクト 276 * @see #execute( Connection,String ) 277 */ 278 public void execute( final DBTableModel table ) { 279 280 // 6.9.7.0 (2018/05/14) データ0件のときは、処理を中断します。 281 if( table == null || table.getRowCount() == 0 ) { return; } 282 283 final int clmNo = table.getColumnCount(); 284 final int rowNo = table.getRowCount(); 285 286 // Range を予め求めておきます。 287 double minimum = Double.POSITIVE_INFINITY; 288 double maximum = Double.NEGATIVE_INFINITY; 289 double sum = 0.0d; // 6.0.2.3 (2014/10/19) パレート図用合計 290 291 int dataSize = clmNo -1; // series の個数は、category 分を引いた数。 292 List<Color> colorList = null; // 6.0.2.2 (2014/10/03) カテゴリカラー 293 if( isColorCategory ) { // ColorCategory使用時 294 colorList = new ArrayList<>(); // カテゴリカラー 295 dataSize--; // 最終カラムが Colorコードなので、マイナスする。 296 } 297 298 numdata = new Number[rowNo][clmNo]; 299 300 // ※ DBTableModel の row,col と、Dataset の row,col は、逆になっています。 301 for( int row=0; row<rowNo; row++ ) { 302 final String category = uniqCategory( table.getValue( row,0 ) ); // 6.0.2.3 (2014/10/10) categoryの重複回避 303 final String[] vals = table.getValues( row ); 304 for( int clm=0; clm<dataSize; clm++ ) { 305 final String sval = vals[clm+1]; // 2番目(アドレス=1)からカラムデータを取得 306 final double val = sval == null || sval.isEmpty() ? 0.0d : Double.parseDouble( sval ) ; // 6.4.2.1 (2016/02/05) PMD refactoring. Useless parentheses. 307 308 addValue( val , seriesLabels[clm] , category ); // val,row,clm 309 numdata[row][clm] = Double.valueOf( val ); // 6.0.2.4 (2014/10/17) 効率の悪いメソッド 310 // Range 求め 311 if( isParetoData ) { // 6.0.2.3 (2014/10/19) パレート図用合計 312 sum += val ; 313 } else { 314 if( val < minimum ) { minimum = val; } 315 if( maximum < val ) { maximum = val; } 316 } 317 } 318 319 // 6.0.2.2 (2014/10/03) ColorCategory は、最後のカラム 320 if( isColorCategory ) { 321 final String colStr = vals[dataSize+1]; // 最後のカラム 322 final Color color = ColorMap.getColorInstance( colStr ); // 6.0.2.1 (2014/09/26) StringUtil → ColorMap 323 colorList.add( color ); 324 } 325 } 326 327 // colorList が null でないかどうかで判定。 328 if( isColorCategory && colorList != null ) { 329 categoryColor = colorList.toArray( new Color[colorList.size()] ); 330 } 331 332 // 6.0.2.3 (2014/10/19) パレート図は、100分率にする。 333 if( isParetoData ) { 334 changeParetoData( sum ); 335 minimum = 0.0; 336 maximum = 100.0; 337 } 338 339 range = new Range( minimum, maximum ); 340 } 341 342 /** 343 * 指定された行列から、数字オブジェクトを取得します。 344 * 345 * @param row 行番号(シリーズ:横持=clm相当) 346 * @param column カラム番号(カテゴリ:縦持ち=row相当) 347 * 348 * @return 指定の行列の値 349 */ 350 @Override 351 public Number getValue( final int row, final int column ) { 352 // 注意:行列の順序が逆です。 353 return numdata[column][row]; 354 } 355 356 /** 357 * レンジオブジェクトを取得します。(独自メソッド) 358 * 359 * @return レンジオブジェクト 360 */ 361 public Range getRange() { 362 return range; 363 } 364 365 /** 366 * パレート図用のDatasetに値を書き換えます。(独自メソッド) 367 * 368 * 色々と方法はあると思いますが、簡易的に、内部の Number配列を 369 * 積上げ計算して、パレート図用のデータを作成します。 370 * レンジオブジェクト も変更します。 371 * 372 * ※ 注意:親クラスの内部に持っている実データは変更されていないので、 373 * 場合によっては、おかしな動きをするかもしれません。 374 * その場合は、上位にもデータをセットするように変更する必要があります。 375 * 376 * なお、行列の順序が、イメージと異なりますので、注意願います。 377 * (columnは、series , row は、category で、シリーズを積み上げます) 378 * 379 * @og.rev 6.0.2.1 (2014/09/26) 新規追加 380 * @og.rev 6.0.2.2 (2014/10/03) HybsDataset i/f 381 * @og.rev 6.0.2.3 (2014/10/19) パレート図は、100分率にする。 382 * 383 * @param sum データの合計 384 */ 385 private void changeParetoData( final double sum ) { 386 if( numdata == null || numdata.length == 0 || numdata[0].length == 0 || sum == 0.0 ) { return ; } 387 388 final int rowCnt = numdata[0].length ; 389 final int clmCnt = numdata.length ; 390 391 for( int rowNo=0; rowNo<rowCnt; rowNo++ ) { // 行列が逆。 392 double val = 0.0; // 初期値 393 for( int clmNo=0; clmNo<clmCnt; clmNo++ ) { // 積上げ計算するカラムでループを回す。 394 final Number v1Num = numdata[clmNo][rowNo]; 395 if( v1Num != null ) { 396 val += v1Num.doubleValue(); // 積上げ計算は、元の値のままにしておきます。 397 } 398 // データをセットするときに、100分率にします。 399 numdata[clmNo][rowNo] = Double.valueOf( Math.round( val * 1000.0 / sum ) / 10.0 ); 400 // きちんと計算するなら、BigDecimal で、スケールを指定して四捨五入すべき・・・かも 401 // java.math.BigDecimal bd = new BigDecimal( val * 100.0 / sum ); 402 // numdata[clmNo][rowNo] = bd.setScale( 1, java.math.RoundingMode.HALF_UP ); 403 } 404 } 405 } 406 407 /** 408 * categoryカラー配列を取得します。(独自メソッド) 409 * 410 * このクラスは、一番最後のカラムを、色文字列として処理し、categoryにColorを指定できます。 411 * select文で指定されていなかった場合は、null を返します。 412 * 413 * select category,series1,series2,series3,・・・,color from ・・・ 414 * 415 * @og.rev 6.0.2.2 (2014/10/03) 新規追加 416 * 417 * なお、Colorコードは、このクラスで作成しますが、Renderer に与える必要があります。 418 * 通常のRenderer には、categoryにカラーを指定する機能がありませんので、HybsBarRenderer に 419 * setCategoryColor( Color[] ) メソッドを用意します。(正確には、HybsDrawItem インターフェース) 420 * このRenderer で、getItemPaint( int , int )メソッドをオーバーライドすることで、カテゴリごとの 421 * 色を返します。 422 * この設定を行うと、シリーズは、カテゴリと同一色になります。 423 * 424 * @return categoryカラー配列(なければ null) 425 */ 426 public Color[] getCategoryColor() { 427 // 6.0.2.5 (2014/10/31) refactoring 428 return ( categoryColor == null ) ? null : categoryColor.clone(); 429 } 430 431 /** 432 * category の重複をさけて、必要であれば、新しいカテゴリ名を作成します。 433 * 434 * カテゴリが同じ場合、JFreeChartでは、表示されません。これは、同じカテゴリと認識され 435 * 値が上書きされるためです。 436 * この問題は、なかなか気づきにくく、デバッグ等に時間がかかってしまいます。 437 * 重複チェックを行い、警告してもよいのですが、ここでは、新しいカテゴリ名を作成することで 438 * エラーを回避しつつ、とりあえずグラフ表示をするようにします。 439 * 440 * @og.rev 6.0.2.3 (2014/10/10) 新規追加 441 * 442 * @param category 元のカテゴリ名 443 * @return 新しい元のカテゴリ名 444 */ 445 private String uniqCategory( final String category ) { 446 String newCate = category ; 447 int i = 0; 448 while( !cateCheck.add( newCate ) ) { // すでに存在している場合。 449 newCate = category + "(" + (i++ ) + ")" ; 450 } 451 452 return newCate ; 453 } 454 455 /** 456 * この文字列と指定されたオブジェクトを比較します。 457 * 458 * 親クラスで、equals メソッドが実装されているため、警告がでます。 459 * 460 * @og.rev 5.1.8.0 (2010/07/01) findbug対応 461 * @og.rev 5.1.9.0 (2010/08/01) findbug対応 462 * 463 * @param object 比較するオブジェクト 464 * 465 * @return Objectが等しい場合は true、そうでない場合は false 466 */ 467 @Override 468 public boolean equals( final Object object ) { 469 // 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 470 return super.equals( object ) && hsCode == ((HybsCategoryDataset)object).hsCode; 471 } 472 473 /** 474 * このオブジェクトのハッシュコードを取得します。 475 * 476 * @og.rev 5.1.8.0 (2010/07/01) findbug対応 477 * @og.rev 5.1.9.0 (2010/08/01) findbug対応 478 * 479 * @return ハッシュコード 480 */ 481 @Override 482 public int hashCode() { return hsCode ; } 483}