001package org.apache.commons.jcs3.auxiliary.disk.jdbc; 002 003/* 004 * Licensed to the Apache Software Foundation (ASF) under one 005 * or more contributor license agreements. See the NOTICE file 006 * distributed with this work for additional information 007 * regarding copyright ownership. The ASF licenses this file 008 * to you under the Apache License, Version 2.0 (the 009 * "License"); you may not use this file except in compliance 010 * with the License. You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, 015 * software distributed under the License is distributed on an 016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 017 * KIND, either express or implied. See the License for the 018 * specific language governing permissions and limitations 019 * under the License. 020 */ 021 022import java.io.IOException; 023import java.sql.Connection; 024import java.sql.DatabaseMetaData; 025import java.sql.PreparedStatement; 026import java.sql.ResultSet; 027import java.sql.SQLException; 028import java.sql.Timestamp; 029import java.util.HashMap; 030import java.util.List; 031import java.util.Map; 032import java.util.Set; 033import java.util.concurrent.atomic.AtomicInteger; 034 035import javax.sql.DataSource; 036 037import org.apache.commons.jcs3.auxiliary.AuxiliaryCacheAttributes; 038import org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache; 039import org.apache.commons.jcs3.auxiliary.disk.jdbc.dsfactory.DataSourceFactory; 040import org.apache.commons.jcs3.engine.behavior.ICache; 041import org.apache.commons.jcs3.engine.behavior.ICacheElement; 042import org.apache.commons.jcs3.engine.logging.behavior.ICacheEvent; 043import org.apache.commons.jcs3.engine.logging.behavior.ICacheEventLogger; 044import org.apache.commons.jcs3.engine.stats.StatElement; 045import org.apache.commons.jcs3.engine.stats.behavior.IStatElement; 046import org.apache.commons.jcs3.engine.stats.behavior.IStats; 047import org.apache.commons.jcs3.log.Log; 048import org.apache.commons.jcs3.log.LogManager; 049 050/** 051 * This is the jdbc disk cache plugin. 052 * <p> 053 * It expects a table created by the following script. The table name is configurable. 054 * </p> 055 * <pre> 056 * drop TABLE JCS_STORE; 057 * CREATE TABLE JCS_STORE 058 * ( 059 * CACHE_KEY VARCHAR(250) NOT NULL, 060 * REGION VARCHAR(250) NOT NULL, 061 * ELEMENT BLOB, 062 * CREATE_TIME TIMESTAMP, 063 * UPDATE_TIME_SECONDS BIGINT, 064 * MAX_LIFE_SECONDS BIGINT, 065 * SYSTEM_EXPIRE_TIME_SECONDS BIGINT, 066 * IS_ETERNAL CHAR(1), 067 * PRIMARY KEY (CACHE_KEY, REGION) 068 * ); 069 * </pre> 070 * <p> 071 * The cleanup thread will delete non eternal items where (now - create time) > max life seconds * 072 * 1000 073 * </p> 074 * <p> 075 * To speed up the deletion the SYSTEM_EXPIRE_TIME_SECONDS is used instead. It is recommended that 076 * an index be created on this column is you will have over a million records. 077 * </p> 078 */ 079public class JDBCDiskCache<K, V> 080 extends AbstractDiskCache<K, V> 081{ 082 /** The local logger. */ 083 private static final Log log = LogManager.getLog( JDBCDiskCache.class ); 084 085 /** configuration */ 086 private JDBCDiskCacheAttributes jdbcDiskCacheAttributes; 087 088 /** # of times update was called */ 089 private final AtomicInteger updateCount = new AtomicInteger(0); 090 091 /** # of times get was called */ 092 private final AtomicInteger getCount = new AtomicInteger(0); 093 094 /** # of times getMatching was called */ 095 private final AtomicInteger getMatchingCount = new AtomicInteger(0); 096 097 /** db connection pool */ 098 private final DataSourceFactory dsFactory; 099 100 /** tracks optimization */ 101 private TableState tableState; 102 103 /** 104 * Constructs a JDBC Disk Cache for the provided cache attributes. The table state object is 105 * used to mark deletions. 106 * <p> 107 * @param cattr the configuration object for this cache 108 * @param dsFactory the DataSourceFactory for this cache 109 * @param tableState an object to track table operations 110 */ 111 public JDBCDiskCache(final JDBCDiskCacheAttributes cattr, final DataSourceFactory dsFactory, final TableState tableState) 112 { 113 super( cattr ); 114 115 setTableState( tableState ); 116 setJdbcDiskCacheAttributes( cattr ); 117 118 log.info( "jdbcDiskCacheAttributes = {0}", this::getJdbcDiskCacheAttributes); 119 120 // This initializes the pool access. 121 this.dsFactory = dsFactory; 122 123 // Initialization finished successfully, so set alive to true. 124 setAlive(true); 125 } 126 127 /** 128 * Inserts or updates. By default it will try to insert. If the item exists we will get an 129 * error. It will then update. This behavior is configurable. The cache can be configured to 130 * check before inserting. 131 * <p> 132 * @param ce 133 */ 134 @Override 135 protected void processUpdate( final ICacheElement<K, V> ce ) 136 { 137 updateCount.incrementAndGet(); 138 139 log.debug( "updating, ce = {0}", ce ); 140 141 try (Connection con = getDataSource().getConnection()) 142 { 143 log.debug( "Putting [{0}] on disk.", ce::getKey); 144 145 try 146 { 147 final byte[] element = getElementSerializer().serialize( ce ); 148 insertOrUpdate( ce, con, element ); 149 } 150 catch ( final IOException e ) 151 { 152 log.error( "Could not serialize element", e ); 153 } 154 } 155 catch ( final SQLException e ) 156 { 157 log.error( "Problem getting connection.", e ); 158 } 159 } 160 161 /** 162 * If test before insert it true, we check to see if the element exists. If the element exists 163 * we will update. Otherwise, we try inserting. If this fails because the item exists, we will 164 * update. 165 * <p> 166 * @param ce 167 * @param con 168 * @param element 169 */ 170 private void insertOrUpdate( final ICacheElement<K, V> ce, final Connection con, final byte[] element ) 171 { 172 boolean exists = false; 173 174 // First do a query to determine if the element already exists 175 if ( this.getJdbcDiskCacheAttributes().isTestBeforeInsert() ) 176 { 177 exists = doesElementExist( ce, con ); 178 } 179 180 // If it doesn't exist, insert it, otherwise update 181 if ( !exists ) 182 { 183 exists = insertRow( ce, con, element ); 184 } 185 186 // update if it exists. 187 if ( exists ) 188 { 189 updateRow( ce, con, element ); 190 } 191 } 192 193 /** 194 * This inserts a new row in the database. 195 * <p> 196 * @param ce 197 * @param con 198 * @param element 199 * @return true if the insertion fails because the record exists. 200 */ 201 private boolean insertRow( final ICacheElement<K, V> ce, final Connection con, final byte[] element ) 202 { 203 boolean exists = false; 204 final String sqlI = String.format("insert into %s" 205 + " (CACHE_KEY, REGION, ELEMENT, MAX_LIFE_SECONDS, IS_ETERNAL, CREATE_TIME, UPDATE_TIME_SECONDS," 206 + " SYSTEM_EXPIRE_TIME_SECONDS) " 207 + " values (?, ?, ?, ?, ?, ?, ?, ?)", getJdbcDiskCacheAttributes().getTableName()); 208 209 try (PreparedStatement psInsert = con.prepareStatement( sqlI )) 210 { 211 psInsert.setString( 1, ce.getKey().toString() ); 212 psInsert.setString( 2, this.getCacheName() ); 213 psInsert.setBytes( 3, element ); 214 psInsert.setLong( 4, ce.getElementAttributes().getMaxLife() ); 215 psInsert.setString( 5, ce.getElementAttributes().getIsEternal() ? "T" : "F" ); 216 217 final Timestamp createTime = new Timestamp( ce.getElementAttributes().getCreateTime() ); 218 psInsert.setTimestamp( 6, createTime ); 219 220 final long now = System.currentTimeMillis() / 1000; 221 psInsert.setLong( 7, now ); 222 223 final long expireTime = now + ce.getElementAttributes().getMaxLife(); 224 psInsert.setLong( 8, expireTime ); 225 226 psInsert.execute(); 227 } 228 catch ( final SQLException e ) 229 { 230 if ("23000".equals(e.getSQLState())) 231 { 232 exists = true; 233 } 234 else 235 { 236 log.error( "Could not insert element", e ); 237 } 238 239 // see if it exists, if we didn't already 240 if ( !exists && !this.getJdbcDiskCacheAttributes().isTestBeforeInsert() ) 241 { 242 exists = doesElementExist( ce, con ); 243 } 244 } 245 246 return exists; 247 } 248 249 /** 250 * This updates a row in the database. 251 * <p> 252 * @param ce 253 * @param con 254 * @param element 255 */ 256 private void updateRow( final ICacheElement<K, V> ce, final Connection con, final byte[] element ) 257 { 258 final String sqlU = String.format("update %s" 259 + " set ELEMENT = ?, CREATE_TIME = ?, UPDATE_TIME_SECONDS = ?, " + " SYSTEM_EXPIRE_TIME_SECONDS = ? " 260 + " where CACHE_KEY = ? and REGION = ?", getJdbcDiskCacheAttributes().getTableName()); 261 262 try (PreparedStatement psUpdate = con.prepareStatement( sqlU )) 263 { 264 psUpdate.setBytes( 1, element ); 265 266 final Timestamp createTime = new Timestamp( ce.getElementAttributes().getCreateTime() ); 267 psUpdate.setTimestamp( 2, createTime ); 268 269 final long now = System.currentTimeMillis() / 1000; 270 psUpdate.setLong( 3, now ); 271 272 final long expireTime = now + ce.getElementAttributes().getMaxLife(); 273 psUpdate.setLong( 4, expireTime ); 274 275 psUpdate.setString( 5, (String) ce.getKey() ); 276 psUpdate.setString( 6, this.getCacheName() ); 277 psUpdate.execute(); 278 279 log.debug( "ran update {0}", sqlU ); 280 } 281 catch ( final SQLException e ) 282 { 283 log.error( "Error executing update sql [{0}]", sqlU, e ); 284 } 285 } 286 287 /** 288 * Does an element exist for this key? 289 * <p> 290 * @param ce the cache element 291 * @param con a database connection 292 * @return boolean 293 */ 294 protected boolean doesElementExist( final ICacheElement<K, V> ce, final Connection con ) 295 { 296 boolean exists = false; 297 // don't select the element, since we want this to be fast. 298 final String sqlS = String.format("select CACHE_KEY from %s where REGION = ? and CACHE_KEY = ?", 299 getJdbcDiskCacheAttributes().getTableName()); 300 301 try (PreparedStatement psSelect = con.prepareStatement( sqlS )) 302 { 303 psSelect.setString( 1, this.getCacheName() ); 304 psSelect.setString( 2, (String) ce.getKey() ); 305 306 try (ResultSet rs = psSelect.executeQuery()) 307 { 308 exists = rs.next(); 309 } 310 311 log.debug( "[{0}] existing status is {1}", ce.getKey(), exists ); 312 } 313 catch ( final SQLException e ) 314 { 315 log.error( "Problem looking for item before insert.", e ); 316 } 317 318 return exists; 319 } 320 321 /** 322 * Queries the database for the value. If it gets a result, the value is deserialized. 323 * <p> 324 * @param key 325 * @return ICacheElement 326 * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#get(Object) 327 */ 328 @Override 329 protected ICacheElement<K, V> processGet( final K key ) 330 { 331 getCount.incrementAndGet(); 332 333 log.debug( "Getting [{0}] from disk", key ); 334 335 if ( !isAlive() ) 336 { 337 return null; 338 } 339 340 ICacheElement<K, V> obj = null; 341 342 // region, key 343 final String selectString = String.format("select ELEMENT from %s where REGION = ? and CACHE_KEY = ?", 344 getJdbcDiskCacheAttributes().getTableName()); 345 346 try (Connection con = getDataSource().getConnection()) 347 { 348 try (PreparedStatement psSelect = con.prepareStatement( selectString )) 349 { 350 psSelect.setString( 1, this.getCacheName() ); 351 psSelect.setString( 2, key.toString() ); 352 353 try (ResultSet rs = psSelect.executeQuery()) 354 { 355 byte[] data = null; 356 357 if ( rs.next() ) 358 { 359 data = rs.getBytes( 1 ); 360 } 361 362 if ( data != null ) 363 { 364 try 365 { 366 // USE THE SERIALIZER 367 obj = getElementSerializer().deSerialize( data, null ); 368 } 369 catch ( final IOException | ClassNotFoundException e ) 370 { 371 log.error( "Problem getting item for key [{0}]", key, e ); 372 } 373 } 374 } 375 } 376 } 377 catch ( final SQLException sqle ) 378 { 379 log.error( "Caught a SQL exception trying to get the item for key [{0}]", 380 key, sqle ); 381 } 382 383 return obj; 384 } 385 386 /** 387 * This will run a like query. It will try to construct a usable query but different 388 * implementations will be needed to adjust the syntax. 389 * <p> 390 * @param pattern 391 * @return key,value map 392 */ 393 @Override 394 protected Map<K, ICacheElement<K, V>> processGetMatching( final String pattern ) 395 { 396 getMatchingCount.incrementAndGet(); 397 398 log.debug( "Getting [{0}] from disk", pattern); 399 400 if ( !isAlive() ) 401 { 402 return null; 403 } 404 405 final Map<K, ICacheElement<K, V>> results = new HashMap<>(); 406 407 // region, key 408 final String selectString = String.format("select ELEMENT from %s where REGION = ? and CACHE_KEY like ?", 409 getJdbcDiskCacheAttributes().getTableName()); 410 411 try (Connection con = getDataSource().getConnection()) 412 { 413 try (PreparedStatement psSelect = con.prepareStatement( selectString )) 414 { 415 psSelect.setString( 1, this.getCacheName() ); 416 psSelect.setString( 2, constructLikeParameterFromPattern( pattern ) ); 417 418 try (ResultSet rs = psSelect.executeQuery()) 419 { 420 while ( rs.next() ) 421 { 422 final byte[] data = rs.getBytes(1); 423 if ( data != null ) 424 { 425 try 426 { 427 // USE THE SERIALIZER 428 final ICacheElement<K, V> value = getElementSerializer().deSerialize( data, null ); 429 results.put( value.getKey(), value ); 430 } 431 catch ( final IOException | ClassNotFoundException e ) 432 { 433 log.error( "Problem getting items for pattern [{0}]", pattern, e ); 434 } 435 } 436 } 437 } 438 } 439 } 440 catch ( final SQLException sqle ) 441 { 442 log.error( "Caught a SQL exception trying to get items for pattern [{0}]", 443 pattern, sqle ); 444 } 445 446 return results; 447 } 448 449 /** 450 * @param pattern 451 * @return String to use in the like query. 452 */ 453 public String constructLikeParameterFromPattern( final String pattern ) 454 { 455 String likePattern = pattern.replace( ".+", "%" ); 456 likePattern = likePattern.replace( ".", "_" ); 457 458 log.debug( "pattern = [{0}]", likePattern ); 459 460 return likePattern; 461 } 462 463 /** 464 * Returns true if the removal was successful; or false if there is nothing to remove. Current 465 * implementation always results in a disk orphan. 466 * <p> 467 * @param key 468 * @return boolean 469 */ 470 @Override 471 protected boolean processRemove( final K key ) 472 { 473 // remove single item. 474 final String sqlSingle = String.format("delete from %s where REGION = ? and CACHE_KEY = ?", 475 getJdbcDiskCacheAttributes().getTableName()); 476 // remove all keys of the same name group. 477 final String sqlPartial = String.format("delete from %s where REGION = ? and CACHE_KEY like ?", 478 getJdbcDiskCacheAttributes().getTableName()); 479 480 try (Connection con = getDataSource().getConnection()) 481 { 482 boolean partial = key.toString().endsWith(ICache.NAME_COMPONENT_DELIMITER); 483 String sql = partial ? sqlPartial : sqlSingle; 484 485 try (PreparedStatement psSelect = con.prepareStatement(sql)) 486 { 487 psSelect.setString( 1, this.getCacheName() ); 488 if ( partial ) 489 { 490 psSelect.setString( 2, key.toString() + "%" ); 491 } 492 else 493 { 494 psSelect.setString( 2, key.toString() ); 495 } 496 497 psSelect.executeUpdate(); 498 499 setAlive(true); 500 } 501 catch ( final SQLException e ) 502 { 503 log.error( "Problem creating statement. sql [{0}]", sql, e ); 504 setAlive(false); 505 } 506 } 507 catch ( final SQLException e ) 508 { 509 log.error( "Problem updating cache.", e ); 510 reset(); 511 } 512 return false; 513 } 514 515 /** 516 * This should remove all elements. The auxiliary can be configured to forbid this behavior. If 517 * remove all is not allowed, the method balks. 518 */ 519 @Override 520 protected void processRemoveAll() 521 { 522 // it should never get here from the abstract disk cache. 523 if ( this.jdbcDiskCacheAttributes.isAllowRemoveAll() ) 524 { 525 final String sql = String.format("delete from %s where REGION = ?", 526 getJdbcDiskCacheAttributes().getTableName()); 527 528 try (Connection con = getDataSource().getConnection()) 529 { 530 try (PreparedStatement psDelete = con.prepareStatement( sql )) 531 { 532 psDelete.setString( 1, this.getCacheName() ); 533 setAlive(true); 534 psDelete.executeUpdate(); 535 } 536 catch ( final SQLException e ) 537 { 538 log.error( "Problem creating statement.", e ); 539 setAlive(false); 540 } 541 } 542 catch ( final SQLException e ) 543 { 544 log.error( "Problem removing all.", e ); 545 reset(); 546 } 547 } 548 else 549 { 550 log.info( "RemoveAll was requested but the request was not fulfilled: " 551 + "allowRemoveAll is set to false." ); 552 } 553 } 554 555 /** 556 * Removed the expired. (now - create time) > max life seconds * 1000 557 * <p> 558 * @return the number deleted 559 */ 560 protected int deleteExpired() 561 { 562 int deleted = 0; 563 564 try (Connection con = getDataSource().getConnection()) 565 { 566 // The shrinker thread might kick in before the table is created 567 // So check if the table exists first 568 final DatabaseMetaData dmd = con.getMetaData(); 569 final ResultSet result = dmd.getTables(null, null, 570 getJdbcDiskCacheAttributes().getTableName(), null); 571 572 if (result.next()) 573 { 574 getTableState().setState( TableState.DELETE_RUNNING ); 575 final long now = System.currentTimeMillis() / 1000; 576 577 final String sql = String.format("delete from %s where IS_ETERNAL = ? and REGION = ?" 578 + " and ? > SYSTEM_EXPIRE_TIME_SECONDS", getJdbcDiskCacheAttributes().getTableName()); 579 580 try (PreparedStatement psDelete = con.prepareStatement( sql )) 581 { 582 psDelete.setString( 1, "F" ); 583 psDelete.setString( 2, this.getCacheName() ); 584 psDelete.setLong( 3, now ); 585 586 setAlive(true); 587 588 deleted = psDelete.executeUpdate(); 589 } 590 catch ( final SQLException e ) 591 { 592 log.error( "Problem creating statement.", e ); 593 setAlive(false); 594 } 595 596 logApplicationEvent( getAuxiliaryCacheAttributes().getName(), "deleteExpired", 597 "Deleted expired elements. URL: " + getDiskLocation() ); 598 } 599 else 600 { 601 log.warn( "Trying to shrink non-existing table [{0}]", 602 getJdbcDiskCacheAttributes().getTableName() ); 603 } 604 } 605 catch ( final SQLException e ) 606 { 607 logError( getAuxiliaryCacheAttributes().getName(), "deleteExpired", 608 e.getMessage() + " URL: " + getDiskLocation() ); 609 log.error( "Problem removing expired elements from the table.", e ); 610 reset(); 611 } 612 finally 613 { 614 getTableState().setState( TableState.FREE ); 615 } 616 617 return deleted; 618 } 619 620 /** 621 * Typically this is used to handle errors by last resort, force content update, or removeall 622 */ 623 public void reset() 624 { 625 // nothing 626 } 627 628 /** Shuts down the pool */ 629 @Override 630 public void processDispose() 631 { 632 final ICacheEvent<K> cacheEvent = createICacheEvent( getCacheName(), null, ICacheEventLogger.DISPOSE_EVENT ); 633 634 try 635 { 636 dsFactory.close(); 637 } 638 catch ( final SQLException e ) 639 { 640 log.error( "Problem shutting down.", e ); 641 } 642 finally 643 { 644 logICacheEvent( cacheEvent ); 645 } 646 } 647 648 /** 649 * Returns the current cache size. Just does a count(*) for the region. 650 * <p> 651 * @return The size value 652 */ 653 @Override 654 public int getSize() 655 { 656 int size = 0; 657 658 // region, key 659 final String selectString = String.format("select count(*) from %s where REGION = ?", 660 getJdbcDiskCacheAttributes().getTableName()); 661 662 try (Connection con = getDataSource().getConnection()) 663 { 664 try (PreparedStatement psSelect = con.prepareStatement( selectString )) 665 { 666 psSelect.setString( 1, this.getCacheName() ); 667 668 try (ResultSet rs = psSelect.executeQuery()) 669 { 670 if ( rs.next() ) 671 { 672 size = rs.getInt( 1 ); 673 } 674 } 675 } 676 } 677 catch ( final SQLException e ) 678 { 679 log.error( "Problem getting size.", e ); 680 } 681 682 return size; 683 } 684 685 /** 686 * Return the keys in this cache. 687 * <p> 688 * @see org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache#getKeySet() 689 */ 690 @Override 691 public Set<K> getKeySet() throws IOException 692 { 693 throw new UnsupportedOperationException( "Groups not implemented." ); 694 // return null; 695 } 696 697 /** 698 * @param jdbcDiskCacheAttributes The jdbcDiskCacheAttributes to set. 699 */ 700 protected void setJdbcDiskCacheAttributes( final JDBCDiskCacheAttributes jdbcDiskCacheAttributes ) 701 { 702 this.jdbcDiskCacheAttributes = jdbcDiskCacheAttributes; 703 } 704 705 /** 706 * @return Returns the jdbcDiskCacheAttributes. 707 */ 708 protected JDBCDiskCacheAttributes getJdbcDiskCacheAttributes() 709 { 710 return jdbcDiskCacheAttributes; 711 } 712 713 /** 714 * @return Returns the AuxiliaryCacheAttributes. 715 */ 716 @Override 717 public AuxiliaryCacheAttributes getAuxiliaryCacheAttributes() 718 { 719 return this.getJdbcDiskCacheAttributes(); 720 } 721 722 /** 723 * Extends the parent stats. 724 * <p> 725 * @return IStats 726 */ 727 @Override 728 public IStats getStatistics() 729 { 730 final IStats stats = super.getStatistics(); 731 stats.setTypeName( "JDBC/Abstract Disk Cache" ); 732 733 final List<IStatElement<?>> elems = stats.getStatElements(); 734 735 elems.add(new StatElement<>( "Update Count", updateCount ) ); 736 elems.add(new StatElement<>( "Get Count", getCount ) ); 737 elems.add(new StatElement<>( "Get Matching Count", getMatchingCount ) ); 738 elems.add(new StatElement<>( "DB URL", getJdbcDiskCacheAttributes().getUrl()) ); 739 740 stats.setStatElements( elems ); 741 742 return stats; 743 } 744 745 /** 746 * Returns the name of the table. 747 * <p> 748 * @return the table name or UNDEFINED 749 */ 750 protected String getTableName() 751 { 752 String name = "UNDEFINED"; 753 if ( this.getJdbcDiskCacheAttributes() != null ) 754 { 755 name = this.getJdbcDiskCacheAttributes().getTableName(); 756 } 757 return name; 758 } 759 760 /** 761 * @param tableState The tableState to set. 762 */ 763 public void setTableState( final TableState tableState ) 764 { 765 this.tableState = tableState; 766 } 767 768 /** 769 * @return Returns the tableState. 770 */ 771 public TableState getTableState() 772 { 773 return tableState; 774 } 775 776 /** 777 * This is used by the event logging. 778 * <p> 779 * @return the location of the disk, either path or ip. 780 */ 781 @Override 782 protected String getDiskLocation() 783 { 784 return this.jdbcDiskCacheAttributes.getUrl(); 785 } 786 787 /** 788 * Public so managers can access it. 789 * @return the dsFactory 790 * @throws SQLException if getting a data source fails 791 */ 792 public DataSource getDataSource() throws SQLException 793 { 794 return dsFactory.getDataSource(); 795 } 796 797 /** 798 * For debugging. 799 * <p> 800 * @return this.getStats(); 801 */ 802 @Override 803 public String toString() 804 { 805 return this.getStats(); 806 } 807}