View Javadoc
1   package org.apache.commons.jcs3.auxiliary.disk.jdbc;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.IOException;
23  import java.sql.Connection;
24  import java.sql.DatabaseMetaData;
25  import java.sql.PreparedStatement;
26  import java.sql.ResultSet;
27  import java.sql.SQLException;
28  import java.sql.Timestamp;
29  import java.util.HashMap;
30  import java.util.List;
31  import java.util.Map;
32  import java.util.Set;
33  import java.util.concurrent.atomic.AtomicInteger;
34  
35  import javax.sql.DataSource;
36  
37  import org.apache.commons.jcs3.auxiliary.AuxiliaryCacheAttributes;
38  import org.apache.commons.jcs3.auxiliary.disk.AbstractDiskCache;
39  import org.apache.commons.jcs3.auxiliary.disk.jdbc.dsfactory.DataSourceFactory;
40  import org.apache.commons.jcs3.engine.behavior.ICache;
41  import org.apache.commons.jcs3.engine.behavior.ICacheElement;
42  import org.apache.commons.jcs3.engine.logging.behavior.ICacheEvent;
43  import org.apache.commons.jcs3.engine.logging.behavior.ICacheEventLogger;
44  import org.apache.commons.jcs3.engine.stats.StatElement;
45  import org.apache.commons.jcs3.engine.stats.behavior.IStatElement;
46  import org.apache.commons.jcs3.engine.stats.behavior.IStats;
47  import org.apache.commons.jcs3.log.Log;
48  import org.apache.commons.jcs3.log.LogManager;
49  
50  /**
51   * This is the jdbc disk cache plugin.
52   * <p>
53   * It expects a table created by the following script. The table name is configurable.
54   * </p>
55   * <pre>
56   *                       drop TABLE JCS_STORE;
57   *                       CREATE TABLE JCS_STORE
58   *                       (
59   *                       CACHE_KEY                  VARCHAR(250)          NOT NULL,
60   *                       REGION                     VARCHAR(250)          NOT NULL,
61   *                       ELEMENT                    BLOB,
62   *                       CREATE_TIME                TIMESTAMP,
63   *                       UPDATE_TIME_SECONDS        BIGINT,
64   *                       MAX_LIFE_SECONDS           BIGINT,
65   *                       SYSTEM_EXPIRE_TIME_SECONDS BIGINT,
66   *                       IS_ETERNAL                 CHAR(1),
67   *                       PRIMARY KEY (CACHE_KEY, REGION)
68   *                       );
69   * </pre>
70   * <p>
71   * The cleanup thread will delete non eternal items where (now - create time) &gt; max life seconds *
72   * 1000
73   * </p>
74   * <p>
75   * To speed up the deletion the SYSTEM_EXPIRE_TIME_SECONDS is used instead. It is recommended that
76   * an index be created on this column is you will have over a million records.
77   * </p>
78   */
79  public class JDBCDiskCache<K, V>
80      extends AbstractDiskCache<K, V>
81  {
82      /** The local logger. */
83      private static final Log log = LogManager.getLog( JDBCDiskCache.class );
84  
85      /** configuration */
86      private JDBCDiskCacheAttributes jdbcDiskCacheAttributes;
87  
88      /** # of times update was called */
89      private final AtomicInteger updateCount = new AtomicInteger(0);
90  
91      /** # of times get was called */
92      private final AtomicInteger getCount = new AtomicInteger(0);
93  
94      /** # of times getMatching was called */
95      private final AtomicInteger getMatchingCount = new AtomicInteger(0);
96  
97      /** db connection pool */
98      private final DataSourceFactory dsFactory;
99  
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) &gt; 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 }