View Javadoc
1   /*
2    * Copyright 2004-2023 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *    https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package com.ibatis.sqlmap.engine.cache;
17  
18  import com.ibatis.common.logging.Log;
19  import com.ibatis.common.logging.LogFactory;
20  import com.ibatis.sqlmap.engine.mapping.statement.ExecuteListener;
21  import com.ibatis.sqlmap.engine.mapping.statement.MappedStatement;
22  
23  import java.io.*;
24  import java.util.HashSet;
25  import java.util.Iterator;
26  import java.util.Properties;
27  import java.util.Set;
28  
29  /**
30   * Wrapper for Caches.
31   */
32  public class CacheModel implements ExecuteListener {
33  
34    /** The Constant log. */
35    private static final Log log = LogFactory.getLog(CacheModel.class);
36  
37    /** The Constant MAX_OBJECT_LOG_SIZE. */
38    private static final int MAX_OBJECT_LOG_SIZE = 200;
39  
40    /**
41     * This is used to represent null objects that are returned from the cache so that they can be cached, too.
42     */
43    public static final Object NULL_OBJECT = "SERIALIZABLE_NULL_OBJECT";
44  
45    /** The requests. */
46    private int requests = 0;
47  
48    /** The hits. */
49    private int hits = 0;
50  
51    /** Constant to turn off periodic cache flushes. */
52    private static final long NO_FLUSH_INTERVAL = -99999;
53  
54    /** The id. */
55    private String id;
56  
57    /** The read only. */
58    private boolean readOnly;
59  
60    /** The serialize. */
61    private boolean serialize;
62  
63    /** The last flush. */
64    private long lastFlush;
65  
66    /** The flush interval. */
67    private long flushInterval;
68  
69    /** The flush interval seconds. */
70    private long flushIntervalSeconds;
71  
72    /** The flush trigger statements. */
73    private Set flushTriggerStatements;
74  
75    /** The controller. */
76    private CacheController controller;
77  
78    /** The resource. */
79    private String resource;
80  
81    /**
82     * Default constructor.
83     */
84    public CacheModel() {
85      this.flushInterval = NO_FLUSH_INTERVAL;
86      this.flushIntervalSeconds = NO_FLUSH_INTERVAL;
87      this.lastFlush = System.currentTimeMillis();
88      this.flushTriggerStatements = new HashSet();
89    }
90  
91    /**
92     * Getter for the cache model's id.
93     *
94     * @return the id
95     */
96    public String getId() {
97      return id;
98    }
99  
100   /**
101    * Setter for the cache model's id.
102    *
103    * @param id
104    *          - the new id
105    */
106   public void setId(String id) {
107     this.id = id;
108   }
109 
110   /**
111    * Getter for read-only property.
112    *
113    * @return true if a read-only model
114    */
115   public boolean isReadOnly() {
116     return readOnly;
117   }
118 
119   /**
120    * Setter for read-only property.
121    *
122    * @param readOnly
123    *          - the new setting
124    */
125   public void setReadOnly(boolean readOnly) {
126     this.readOnly = readOnly;
127   }
128 
129   /**
130    * Getter to tell if the cache serializes.
131    *
132    * @return true if the cache model serializes objects
133    */
134   public boolean isSerialize() {
135     return serialize;
136   }
137 
138   /**
139    * Setter to tell the cache to serialize objects.
140    *
141    * @param serialize
142    *          - if the cache model is to serialize objects
143    */
144   public void setSerialize(boolean serialize) {
145     this.serialize = serialize;
146   }
147 
148   /**
149    * Getter for resource property.
150    *
151    * @return the value of the resource property
152    */
153   public String getResource() {
154     return resource;
155   }
156 
157   /**
158    * Setter for resource property.
159    *
160    * @param resource
161    *          - the new value
162    */
163   public void setResource(String resource) {
164     this.resource = resource;
165   }
166 
167   /**
168    * Sets up the controller for the cache model.
169    *
170    * @param controller
171    *          the new cache controller
172    *
173    * @throws ClassNotFoundException
174    *           - if the class cannot be found
175    * @throws InstantiationException
176    *           - if the class cannot be instantiated
177    * @throws IllegalAccessException
178    *           - if the classes constructor is not accessible
179    */
180   public void setCacheController(CacheController controller)
181       throws ClassNotFoundException, InstantiationException, IllegalAccessException {
182     this.controller = controller;
183   }
184 
185   /**
186    * Getter for flushInterval property.
187    *
188    * @return The flushInterval (in milliseconds)
189    */
190   public long getFlushInterval() {
191     return flushInterval;
192   }
193 
194   /**
195    * Getter for flushInterval property.
196    *
197    * @return The flushInterval (in milliseconds)
198    */
199   public long getFlushIntervalSeconds() {
200     return flushIntervalSeconds;
201   }
202 
203   /**
204    * Setter for flushInterval property.
205    *
206    * @param flushInterval
207    *          The new flushInterval (in milliseconds)
208    */
209   public void setFlushInterval(long flushInterval) {
210     this.flushInterval = flushInterval;
211     this.flushIntervalSeconds = flushInterval / 1000;
212   }
213 
214   /**
215    * Adds a flushTriggerStatment. When a flushTriggerStatment is executed, the cache is flushed (cleared).
216    *
217    * @param statementName
218    *          The statement to add.
219    */
220   public void addFlushTriggerStatement(String statementName) {
221     flushTriggerStatements.add(statementName);
222   }
223 
224   /**
225    * Gets an Iterator containing all flushTriggerStatment objects for this cache.
226    *
227    * @return The Iterator
228    */
229   public Iterator getFlushTriggerStatementNames() {
230     return flushTriggerStatements.iterator();
231   }
232 
233   /**
234    * ExecuteListener event. This will be called by a MappedStatement for which this cache is registered as a
235    * ExecuteListener. It will be called each time an executeXXXXXX method is called. In the case of the Cache class, it
236    * is registered in order to flush the cache whenever a certain statement is executed. (i.e. the flushOnExecute cache
237    * policy)
238    *
239    * @param statement
240    *          The statement to execute
241    */
242   public void onExecuteStatement(MappedStatement statement) {
243     flush();
244   }
245 
246   /**
247    * Returns statistical information about the cache.
248    *
249    * @return the number of cache hits divided by the total requests
250    */
251   public double getHitRatio() {
252     return (double) hits / (double) requests;
253   }
254 
255   /**
256    * Configures the cache.
257    *
258    * @param props
259    *          the props
260    */
261   public void configure(Properties props) {
262     controller.setProperties(props);
263   }
264 
265   /**
266    * Clears the cache.
267    */
268   public void flush() {
269     synchronized (this) {
270       controller.flush(this);
271       lastFlush = System.currentTimeMillis();
272       if (log.isDebugEnabled()) {
273         log("flushed", false, null);
274       }
275     }
276   }
277 
278   /**
279    * Get an object out of the cache. A side effect of this method is that is may clear the cache if it has not been
280    * cleared in the flushInterval.
281    *
282    * @param key
283    *          The key of the object to be returned
284    *
285    * @return The cached object (or null)
286    */
287   public Object getObject(CacheKey key) {
288     Object value = null;
289     synchronized (this) {
290       if (flushInterval != NO_FLUSH_INTERVAL && System.currentTimeMillis() - lastFlush > flushInterval) {
291         flush();
292       }
293 
294       value = controller.getObject(this, key);
295       if (serialize && !readOnly && (value != NULL_OBJECT && value != null)) {
296         try {
297           ByteArrayInputStream bis = new ByteArrayInputStream((byte[]) value);
298           ObjectInputStream ois = new ObjectInputStream(bis);
299           value = ois.readObject();
300           ois.close();
301         } catch (Exception e) {
302           throw new RuntimeException("Error caching serializable object.  Be sure you're not attempting to use "
303               + "a serialized cache for an object that may be taking advantage of lazy loading.  Cause: " + e, e);
304         }
305       }
306       requests++;
307       if (value != null) {
308         hits++;
309       }
310       if (log.isDebugEnabled()) {
311         if (value != null) {
312           log("retrieved object", true, value);
313         } else {
314           log("cache miss", false, null);
315         }
316       }
317     }
318     return value;
319   }
320 
321   /**
322    * Add an object to the cache.
323    *
324    * @param key
325    *          The key of the object to be cached
326    * @param value
327    *          The object to be cached
328    */
329   public void putObject(CacheKey key, Object value) {
330     if (null == value)
331       value = NULL_OBJECT;
332     synchronized (this) {
333       if (serialize && !readOnly && value != NULL_OBJECT) {
334         try {
335           ByteArrayOutputStream bos = new ByteArrayOutputStream();
336           ObjectOutputStream oos = new ObjectOutputStream(bos);
337           oos.writeObject(value);
338           oos.flush();
339           oos.close();
340           value = bos.toByteArray();
341         } catch (IOException e) {
342           throw new RuntimeException("Error caching serializable object.  Cause: " + e, e);
343         }
344       }
345       controller.putObject(this, key, value);
346       if (log.isDebugEnabled()) {
347         log("stored object", true, value);
348       }
349     }
350   }
351 
352   /**
353    * Get the maximum size of an object in the log output.
354    *
355    * @return Maximum size of a logged object in the output
356    */
357   protected int getMaxObjectLogSize() {
358     return MAX_OBJECT_LOG_SIZE;
359   }
360 
361   /**
362    * Log a cache action. Since this method is pretty heavy weight, it's best to enclose it with a log.isDebugEnabled()
363    * when called.
364    *
365    * @param action
366    *          String to output
367    * @param addValue
368    *          Add the value being cached to the log
369    * @param cacheValue
370    *          The value being logged
371    */
372   protected void log(String action, boolean addValue, Object cacheValue) {
373     StringBuilder output = new StringBuilder("Cache '");
374     output.append(getId());
375     output.append("': ");
376     output.append(action);
377     if (addValue) {
378       String cacheObjectStr = (cacheValue == null ? "null" : cacheValue.toString());
379       output.append(" '");
380       if (cacheObjectStr.length() < getMaxObjectLogSize()) {
381         output.append(cacheObjectStr);
382       } else {
383         output.append(cacheObjectStr.substring(1, getMaxObjectLogSize()));
384         output.append("...");
385       }
386       output.append("'");
387     }
388     log.debug(output.toString());
389   }
390 
391   /**
392    * Sets the controller properties.
393    *
394    * @param cacheProps
395    *          the new controller properties
396    */
397   public void setControllerProperties(Properties cacheProps) {
398     controller.setProperties(cacheProps);
399   }
400 }