1 package org.apache.jcs.auxiliary.disk.jdbc;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22 import java.io.IOException;
23 import java.io.Serializable;
24 import java.sql.Connection;
25 import java.sql.Date;
26 import java.sql.PreparedStatement;
27 import java.sql.ResultSet;
28 import java.sql.SQLException;
29 import java.sql.Statement;
30 import java.util.ArrayList;
31 import java.util.Arrays;
32 import java.util.List;
33 import java.util.Set;
34
35 import org.apache.commons.logging.Log;
36 import org.apache.commons.logging.LogFactory;
37 import org.apache.jcs.auxiliary.AuxiliaryCacheAttributes;
38 import org.apache.jcs.auxiliary.disk.AbstractDiskCache;
39 import org.apache.jcs.engine.CacheConstants;
40 import org.apache.jcs.engine.behavior.ICacheElement;
41 import org.apache.jcs.engine.behavior.IElementSerializer;
42 import org.apache.jcs.engine.stats.StatElement;
43 import org.apache.jcs.engine.stats.behavior.IStatElement;
44 import org.apache.jcs.engine.stats.behavior.IStats;
45 import org.apache.jcs.utils.serialization.StandardSerializer;
46
47 /***
48 * This is the jdbc disk cache plugin.
49 * <p>
50 * It expects a table created by the following script. The table name is
51 * configurable.
52 * <p>
53 *
54 * <pre>
55 * drop TABLE JCS_STORE;
56 *
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 DATE,
63 * CREATE_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 *
71 * <p>
72 * The cleanup thread will delete non eternal items where (now - create time) >
73 * max life seconds * 1000
74 * <p>
75 * To speed up the deletion the SYSTEM_EXPIRE_TIME_SECONDS is used instead. It
76 * is recommended that an index be created on this column is you will have over
77 * a million records.
78 * <p>
79 * @author Aaron Smuts
80 */
81 public class JDBCDiskCache
82 extends AbstractDiskCache
83 {
84 private final static Log log = LogFactory.getLog( JDBCDiskCache.class );
85
86 private static final long serialVersionUID = -7169488308515823492L;
87
88 private IElementSerializer elementSerializer = new StandardSerializer();
89
90 private JDBCDiskCacheAttributes jdbcDiskCacheAttributes;
91
92 private int updateCount = 0;
93
94 private int getCount = 0;
95
96
97 private static final int LOG_INTERVAL = 100;
98
99 private JDBCDiskCachePoolAccess poolAccess = null;
100
101 private TableState tableState;
102
103 /***
104 * Constructs a JDBC Disk Cache for the provided cache attributes. The table
105 * state object is used to mark deletions.
106 * <p>
107 * @param cattr
108 * @param tableState
109 */
110 public JDBCDiskCache( JDBCDiskCacheAttributes cattr, TableState tableState )
111 {
112 super( cattr );
113
114 this.setTableState( tableState );
115
116 setJdbcDiskCacheAttributes( cattr );
117
118 if ( log.isInfoEnabled() )
119 {
120 log.info( "jdbcDiskCacheAttributes = " + getJdbcDiskCacheAttributes() );
121 }
122
123 /***
124 * This initializes the pool access.
125 */
126 initializePoolAccess( cattr );
127
128
129 alive = true;
130 }
131
132 /***
133 * Registers the driver and creates a poolAccess class.
134 * @param cattr
135 */
136 protected void initializePoolAccess( JDBCDiskCacheAttributes cattr )
137 {
138 try
139 {
140 try
141 {
142
143 Class.forName( cattr.getDriverClassName() );
144 }
145 catch ( ClassNotFoundException e )
146 {
147 log.error( "Couldn't find class for driver [" + cattr.getDriverClassName() + "]", e );
148 }
149
150 poolAccess = new JDBCDiskCachePoolAccess( cattr.getName() );
151
152 poolAccess.setupDriver( cattr.getUrl() + cattr.getDatabase(), cattr.getUserName(), cattr.getPassword(),
153 cattr.getMaxActive() );
154
155 poolAccess.logDriverStats();
156 }
157 catch ( Exception e )
158 {
159 log.error( "Problem getting connection.", e );
160 }
161 }
162
163
164
165
166
167 public void doUpdate( ICacheElement ce )
168 {
169 incrementUpdateCount();
170
171 if ( log.isDebugEnabled() )
172 {
173 log.debug( "updating, ce = " + ce );
174 }
175
176 Connection con;
177 try
178 {
179 con = poolAccess.getConnection();
180 }
181 catch ( SQLException e )
182 {
183 log.error( "Problem getting conenction.", e );
184 return;
185 }
186
187 try
188 {
189
190 Statement sStatement = null;
191 try
192 {
193 sStatement = con.createStatement();
194 alive = true;
195 }
196 catch ( SQLException e )
197 {
198 log.error( "Problem creating statement.", e );
199 alive = false;
200 }
201 finally
202 {
203 try
204 {
205 sStatement.close();
206 }
207 catch ( SQLException e )
208 {
209 log.error( "Problem closing statement.", e );
210 }
211 }
212
213 if ( !alive )
214 {
215 if ( log.isInfoEnabled() )
216 {
217 log.info( "Disk is not alive, aborting put." );
218 }
219 return;
220 }
221
222 if ( log.isDebugEnabled() )
223 {
224 log.debug( "Putting [" + ce.getKey() + "] on disk." );
225 }
226
227 byte[] element;
228
229 try
230 {
231 element = serialize( ce );
232 }
233 catch ( IOException e )
234 {
235 log.error( "Could not serialize element", e );
236 return;
237 }
238
239 boolean exists = false;
240
241
242 if ( this.getJdbcDiskCacheAttributes().isTestBeforeInsert() )
243 {
244 exists = doesElementExist( ce );
245 }
246
247
248 if ( !exists )
249 {
250 try
251 {
252 String sqlI = "insert into "
253 + getJdbcDiskCacheAttributes().getTableName()
254 + " (CACHE_KEY, REGION, ELEMENT, MAX_LIFE_SECONDS, IS_ETERNAL, CREATE_TIME, CREATE_TIME_SECONDS, SYSTEM_EXPIRE_TIME_SECONDS) "
255 + " values (?, ?, ?, ?, ?, ?, ?, ?)";
256
257 PreparedStatement psInsert = con.prepareStatement( sqlI );
258 psInsert.setString( 1, (String) ce.getKey() );
259 psInsert.setString( 2, this.getCacheName() );
260 psInsert.setBytes( 3, element );
261 psInsert.setLong( 4, ce.getElementAttributes().getMaxLifeSeconds() );
262 if ( ce.getElementAttributes().getIsEternal() )
263 {
264 psInsert.setString( 5, "T" );
265 }
266 else
267 {
268 psInsert.setString( 5, "F" );
269 }
270 Date createTime = new Date( ce.getElementAttributes().getCreateTime() );
271 psInsert.setDate( 6, createTime );
272
273 long now = System.currentTimeMillis() / 1000;
274 psInsert.setLong( 7, now );
275
276 long expireTime = now + ce.getElementAttributes().getMaxLifeSeconds();
277 psInsert.setLong( 8, expireTime );
278
279 psInsert.execute();
280 psInsert.close();
281 }
282 catch ( SQLException e )
283 {
284 if ( e.toString().indexOf( "Violation of unique index" ) != -1
285 || e.getMessage().indexOf( "Violation of unique index" ) != -1
286 || e.getMessage().indexOf( "Duplicate entry" ) != -1 )
287 {
288 exists = true;
289 }
290 else
291 {
292 log.error( "Could not insert element", e );
293 }
294
295
296 if ( !exists && !this.getJdbcDiskCacheAttributes().isTestBeforeInsert() )
297 {
298 exists = doesElementExist( ce );
299 }
300 }
301 }
302
303
304 if ( exists )
305 {
306 String sqlU = null;
307 try
308 {
309 sqlU = "update " + getJdbcDiskCacheAttributes().getTableName()
310 + " set ELEMENT = ?, CREATE_TIME = ?, CREATE_TIME_SECONDS = ?, "
311 + " SYSTEM_EXPIRE_TIME_SECONDS = ? " + " where CACHE_KEY = ? and REGION = ?";
312 PreparedStatement psUpdate = con.prepareStatement( sqlU );
313 psUpdate.setBytes( 1, element );
314
315 Date createTime = new Date( ce.getElementAttributes().getCreateTime() );
316 psUpdate.setDate( 2, createTime );
317
318 long now = System.currentTimeMillis() / 1000;
319 psUpdate.setLong( 3, now );
320
321 long expireTime = now + ce.getElementAttributes().getMaxLifeSeconds();
322 psUpdate.setLong( 4, expireTime );
323
324 psUpdate.setString( 5, (String) ce.getKey() );
325 psUpdate.setString( 6, this.getCacheName() );
326 psUpdate.execute();
327 psUpdate.close();
328
329 if ( log.isDebugEnabled() )
330 {
331 log.debug( "ran update " + sqlU );
332 }
333 }
334 catch ( SQLException e2 )
335 {
336 log.error( "e2 sql [" + sqlU + "] Exception: ", e2 );
337 }
338 }
339 }
340 finally
341 {
342 try
343 {
344 con.close();
345 }
346 catch ( SQLException e )
347 {
348 log.error( "Problem closing connection.", e );
349 }
350 }
351
352 if ( log.isInfoEnabled() )
353 {
354 if ( updateCount % LOG_INTERVAL == 0 )
355 {
356
357 log.info( "Update Count [" + updateCount + "]" );
358 }
359 }
360 }
361
362 /***
363 * Does an element exist for this key?
364 * <p>
365 * @param ce
366 * @return boolean
367 */
368 protected boolean doesElementExist( ICacheElement ce )
369 {
370 boolean exists = false;
371
372 Connection con;
373 try
374 {
375 con = poolAccess.getConnection();
376 }
377 catch ( SQLException e )
378 {
379 log.error( "Problem getting conenction.", e );
380 return exists;
381 }
382
383 PreparedStatement psSelect = null;
384 try
385 {
386
387 String sqlS = "select CACHE_KEY from " + getJdbcDiskCacheAttributes().getTableName()
388 + " where REGION = ? and CACHE_KEY = ?";
389
390 psSelect = con.prepareStatement( sqlS );
391 psSelect.setString( 1, this.getCacheName() );
392 psSelect.setString( 2, (String) ce.getKey() );
393
394 ResultSet rs = psSelect.executeQuery();
395
396 if ( rs.next() )
397 {
398 exists = true;
399 }
400
401 if ( log.isDebugEnabled() )
402 {
403 log.debug( "[" + ce.getKey() + "] existing status is " + exists );
404 }
405
406 rs.close();
407 }
408 catch ( SQLException e )
409 {
410 log.error( "Problem looking for item before insert.", e );
411 }
412 finally
413 {
414 try
415 {
416 if ( psSelect != null )
417 {
418 psSelect.close();
419 }
420 psSelect.close();
421 }
422 catch ( SQLException e1 )
423 {
424 log.error( "Problem closing statement.", e1 );
425 }
426
427 try
428 {
429 con.close();
430 }
431 catch ( SQLException e )
432 {
433 log.error( "Problem closing connection.", e );
434 }
435 }
436
437 return exists;
438 }
439
440 /***
441 * Queries the database for the value. If it gets a result, the value is
442 * deserialized.
443 * <p>
444 * @see org.apache.jcs.auxiliary.disk.AbstractDiskCache#doGet(java.io.Serializable)
445 */
446 public ICacheElement doGet( Serializable key )
447 {
448 incrementGetCount();
449
450 if ( log.isDebugEnabled() )
451 {
452 log.debug( "Getting " + key + " from disk" );
453 }
454
455 if ( !alive )
456 {
457 return null;
458 }
459
460 ICacheElement obj = null;
461
462 byte[] data = null;
463 try
464 {
465
466 String selectString = "select ELEMENT from " + getJdbcDiskCacheAttributes().getTableName()
467 + " where REGION = ? and CACHE_KEY = ?";
468
469 Connection con = poolAccess.getConnection();
470 try
471 {
472 PreparedStatement psSelect = null;
473 try
474 {
475 psSelect = con.prepareStatement( selectString );
476 psSelect.setString( 1, this.getCacheName() );
477 psSelect.setString( 2, key.toString() );
478
479 ResultSet rs = psSelect.executeQuery();
480 try
481 {
482 if ( rs.next() )
483 {
484 data = rs.getBytes( 1 );
485 }
486 if ( data != null )
487 {
488 try
489 {
490
491 obj = (ICacheElement) getElementSerializer().deSerialize( data );
492 }
493 catch ( IOException ioe )
494 {
495 log.error( ioe );
496 }
497 catch ( Exception e )
498 {
499 log.error( "Problem getting item.", e );
500 }
501 }
502 }
503 finally
504 {
505 if ( rs != null )
506 {
507 rs.close();
508 }
509 rs.close();
510 }
511 }
512 finally
513 {
514 if ( psSelect != null )
515 {
516 psSelect.close();
517 }
518 psSelect.close();
519 }
520 }
521 finally
522 {
523 if ( con != null )
524 {
525 con.close();
526 }
527 }
528 }
529 catch ( SQLException sqle )
530 {
531 log.error( sqle );
532 }
533
534 if ( log.isInfoEnabled() )
535 {
536 if ( getCount % LOG_INTERVAL == 0 )
537 {
538
539 log.info( "Get Count [" + getCount + "]" );
540 }
541 }
542
543 return obj;
544 }
545
546 /***
547 * Returns true if the removal was succesful; or false if there is nothing
548 * to remove. Current implementation always result in a disk orphan.
549 * <p>
550 * @param key
551 * @return boolean
552 */
553 public boolean doRemove( Serializable key )
554 {
555
556 String sql = "delete from " + getJdbcDiskCacheAttributes().getTableName()
557 + " where REGION = ? and CACHE_KEY = ?";
558
559 try
560 {
561 boolean partial = false;
562 if ( key instanceof String && key.toString().endsWith( CacheConstants.NAME_COMPONENT_DELIMITER ) )
563 {
564
565 sql = "delete from " + getJdbcDiskCacheAttributes().getTableName()
566 + " where REGION = ? and CACHE_KEY like ?";
567 partial = true;
568 }
569 Connection con = poolAccess.getConnection();
570 PreparedStatement psSelect = null;
571 try
572 {
573 psSelect = con.prepareStatement( sql );
574 psSelect.setString( 1, this.getCacheName() );
575 if ( partial )
576 {
577 psSelect.setString( 2, key.toString() + "%" );
578 }
579 else
580 {
581 psSelect.setString( 2, key.toString() );
582 }
583
584 psSelect.executeUpdate( );
585
586 alive = true;
587 }
588 catch ( SQLException e )
589 {
590 log.error( "Problem creating statement. sql [" + sql + "]", e );
591 alive = false;
592 }
593 finally
594 {
595 try
596 {
597 if ( psSelect != null )
598 {
599 psSelect.close();
600 }
601 con.close();
602 }
603 catch ( SQLException e1 )
604 {
605 log.error( "Problem closing statement.", e1 );
606 }
607 }
608
609 }
610 catch ( Exception e )
611 {
612 log.error( "Problem updating cache.", e );
613 reset();
614 }
615 return false;
616 }
617
618 /*** This should remove all elements. */
619 public void doRemoveAll()
620 {
621
622 if ( this.jdbcDiskCacheAttributes.isAllowRemoveAll() )
623 {
624 try
625 {
626 String sql = "delete from " + getJdbcDiskCacheAttributes().getTableName() + " where REGION = ?";
627 Connection con = poolAccess.getConnection();
628 PreparedStatement psDelete = null;
629 try
630 {
631 psDelete = con.prepareStatement( sql );
632 psDelete.setString( 1, this.getCacheName() );
633 alive = true;
634 psDelete.executeUpdate( );
635 }
636 catch ( SQLException e )
637 {
638 log.error( "Problem creating statement.", e );
639 alive = false;
640 }
641 finally
642 {
643 try
644 {
645 if ( psDelete != null )
646 {
647 psDelete.close();
648 }
649 con.close();
650 }
651 catch ( SQLException e1 )
652 {
653 log.error( "Problem closing statement.", e1 );
654 }
655 }
656 }
657 catch ( Exception e )
658 {
659 log.error( "Problem removing all.", e );
660 reset();
661 }
662 }
663 else
664 {
665 if ( log.isInfoEnabled() )
666 {
667 log.info( "RemoveAll was requested but the request was not fulfilled: allowRemoveAll is set to false." );
668 }
669 }
670 }
671
672 /***
673 * Removed the expired. (now - create time) > max life seconds * 1000
674 * <p>
675 * @return the number deleted
676 */
677 protected int deleteExpired()
678 {
679 int deleted = 0;
680
681 try
682 {
683 getTableState().setState( TableState.DELETE_RUNNING );
684
685 long now = System.currentTimeMillis() / 1000;
686
687
688
689
690
691
692
693 String sql = "delete from " + getJdbcDiskCacheAttributes().getTableName()
694 + " where IS_ETERNAL = ? and REGION = ? and ? > SYSTEM_EXPIRE_TIME_SECONDS";
695
696 Connection con = poolAccess.getConnection();
697 PreparedStatement psDelete = null;
698 try
699 {
700 psDelete = con.prepareStatement( sql );
701 psDelete.setString( 1, "F" );
702 psDelete.setString( 2, this.getCacheName() );
703 psDelete.setLong( 3, now );
704
705 alive = true;
706
707 deleted = psDelete.executeUpdate( );
708 }
709 catch ( SQLException e )
710 {
711 log.error( "Problem creating statement.", e );
712 alive = false;
713 }
714 finally
715 {
716 try
717 {
718 if ( psDelete != null )
719 {
720 psDelete.close();
721 }
722 con.close();
723 }
724 catch ( SQLException e1 )
725 {
726 log.error( "Problem closing statement.", e1 );
727 }
728 }
729 }
730 catch ( Exception e )
731 {
732 log.error( "Problem removing expired elements from the table.", e );
733 reset();
734 }
735 finally
736 {
737 getTableState().setState( TableState.FREE );
738 }
739
740 return deleted;
741 }
742
743 /***
744 * Typically this is used to handle errors by last resort, force content
745 * update, or removeall
746 */
747 public void reset()
748 {
749
750 }
751
752 /*** Shuts down the pool */
753 public void doDispose()
754 {
755 try
756 {
757 poolAccess.shutdownDriver();
758 }
759 catch ( Exception e )
760 {
761 log.error( "Problem shutting down.", e );
762 }
763 }
764
765 /***
766 * Returns the current cache size. Just does a count(*) for the region.
767 * <p>
768 * @return The size value
769 */
770 public int getSize()
771 {
772 int size = 0;
773
774
775 String selectString = "select count(*) from " + getJdbcDiskCacheAttributes().getTableName()
776 + " where REGION = ?";
777
778 Connection con;
779 try
780 {
781 con = poolAccess.getConnection();
782 }
783 catch ( SQLException e1 )
784 {
785 log.error( "Problem getting conenction.", e1 );
786 return size;
787 }
788 try
789 {
790 PreparedStatement psSelect = null;
791 try
792 {
793 psSelect = con.prepareStatement( selectString );
794 psSelect.setString( 1, this.getCacheName() );
795 ResultSet rs = null;
796
797 rs = psSelect.executeQuery();
798 try
799 {
800 if ( rs.next() )
801 {
802 size = rs.getInt( 1 );
803 }
804 }
805 finally
806 {
807 if ( rs != null )
808 {
809 rs.close();
810 }
811 rs.close();
812 }
813 }
814 finally
815 {
816 if ( psSelect != null )
817 {
818 psSelect.close();
819 }
820 psSelect.close();
821 }
822 }
823 catch ( SQLException e )
824 {
825 log.error( "Problem getting size.", e );
826 }
827 finally
828 {
829 try
830 {
831 con.close();
832 }
833 catch ( SQLException e )
834 {
835 log.error( "Problem closing connection.", e );
836 }
837 }
838 return size;
839 }
840
841 /***
842 * Returns the serialized form of the given object in a byte array.
843 * <p>
844 * @param obj
845 * @return byte[]
846 * @throws IOException
847 */
848 protected byte[] serialize( Serializable obj )
849 throws IOException
850 {
851 return getElementSerializer().serialize( obj );
852 }
853
854 /***
855 * @param groupName
856 * @return
857 */
858 public Set getGroupKeys( String groupName )
859 {
860 if ( true )
861 {
862 throw new UnsupportedOperationException( "Groups not implemented." );
863 }
864 return null;
865 }
866
867 /***
868 * @param elementSerializer
869 * The elementSerializer to set.
870 */
871 public void setElementSerializer( IElementSerializer elementSerializer )
872 {
873 this.elementSerializer = elementSerializer;
874 }
875
876 /***
877 * @return Returns the elementSerializer.
878 */
879 public IElementSerializer getElementSerializer()
880 {
881 return elementSerializer;
882 }
883
884 /*** safely increment */
885 private synchronized void incrementUpdateCount()
886 {
887 updateCount++;
888 }
889
890 /*** safely increment */
891 private synchronized void incrementGetCount()
892 {
893 getCount++;
894 }
895
896 /***
897 * @param jdbcDiskCacheAttributes
898 * The jdbcDiskCacheAttributes to set.
899 */
900 protected void setJdbcDiskCacheAttributes( JDBCDiskCacheAttributes jdbcDiskCacheAttributes )
901 {
902 this.jdbcDiskCacheAttributes = jdbcDiskCacheAttributes;
903 }
904
905 /***
906 * @return Returns the jdbcDiskCacheAttributes.
907 */
908 protected JDBCDiskCacheAttributes getJdbcDiskCacheAttributes()
909 {
910 return jdbcDiskCacheAttributes;
911 }
912
913 /***
914 * @return Returns the AuxiliaryCacheAttributes.
915 */
916 public AuxiliaryCacheAttributes getAuxiliaryCacheAttributes()
917 {
918 return this.getJdbcDiskCacheAttributes();
919 }
920
921 /***
922 * Extends the parent stats.
923 */
924 public IStats getStatistics()
925 {
926 IStats stats = super.getStatistics();
927 stats.setTypeName( "JDBC/Abstract Disk Cache" );
928 stats.getStatElements();
929
930 ArrayList elems = new ArrayList();
931
932 IStatElement se = null;
933
934 se = new StatElement();
935 se.setName( "Update Count" );
936 se.setData( "" + updateCount );
937 elems.add( se );
938
939 se = new StatElement();
940 se.setName( "Get Count" );
941 se.setData( "" + getCount );
942 elems.add( se );
943
944 se = new StatElement();
945 se.setName( "Size" );
946 se.setData( "" + getSize() );
947 elems.add( se );
948
949 se = new StatElement();
950 se.setName( "Active DB Connections" );
951 se.setData( "" + poolAccess.getNumActiveInPool() );
952 elems.add( se );
953
954 se = new StatElement();
955 se.setName( "Idle DB Connections" );
956 se.setData( "" + poolAccess.getNumIdleInPool() );
957 elems.add( se );
958
959 se = new StatElement();
960 se.setName( "DB URL" );
961 se.setData( this.jdbcDiskCacheAttributes.getUrl() );
962 elems.add( se );
963
964
965
966 IStatElement[] eqSEs = stats.getStatElements();
967 List eqL = Arrays.asList( eqSEs );
968 elems.addAll( eqL );
969
970
971 IStatElement[] ses = (IStatElement[]) elems.toArray( new StatElement[0] );
972 stats.setStatElements( ses );
973
974 return stats;
975 }
976
977 /***
978 * Returns the name of the table.
979 * <p>
980 * @return the table name or UNDEFINED
981 */
982 protected String getTableName()
983 {
984 String name = "UNDEFINED";
985 if ( this.getJdbcDiskCacheAttributes() != null )
986 {
987 name = this.getJdbcDiskCacheAttributes().getTableName();
988 }
989 return name;
990 }
991
992 /***
993 * @param tableState The tableState to set.
994 */
995 public void setTableState( TableState tableState )
996 {
997 this.tableState = tableState;
998 }
999
1000 /***
1001 * @return Returns the tableState.
1002 */
1003 public TableState getTableState()
1004 {
1005 return tableState;
1006 }
1007
1008 /***
1009 * For debugging.
1010 */
1011 public String toString()
1012 {
1013 return this.getStats();
1014 }
1015 }