1 /**
2 * Copyright 2005 Steve Molloy
3 *
4 * This file is part of OV4J.
5 *
6 * OV4J is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as
7 * published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version.
8 *
9 * OV4J is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
10 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
11 *
12 * You should have received a copy of the GNU General Public License along with OV4J; if not, write to the Free Software
13 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
14 *
15 */
16 package org.ov4j.cache;
17
18 import java.io.IOException;
19 import java.lang.reflect.Method;
20 import java.util.Arrays;
21 import java.util.TreeMap;
22 import java.util.Vector;
23 import java.util.logging.Level;
24 import java.util.logging.Logger;
25
26 import org.ov4j.Config;
27 import org.ov4j.Controler;
28 import org.ov4j.IContainer;
29 import org.ov4j.comp.ComparisonResult;
30 import org.ov4j.data.ClassComparable;
31 import org.ov4j.data.Version;
32
33 /**
34 * This class will handle caching results of calls.
35 *
36 * @author smolloy
37 *
38 */
39 public class CacheHandler {
40 /**
41 * Logger for this class
42 */
43 private static final Logger logger =
44 Logger
45 .getLogger(CacheHandler.class
46 .getName());
47
48 /** Author used when caching. */
49 protected static final String CACHE_AUTHOR =
50 Config
51 .getString("OV4J.cache.Author");
52
53 /** Comment on normal cache. */
54 protected static final String NORMAL_COMMENT =
55 Config
56 .getString("OV4J.cache.NormalComment");
57
58 /** Comment for timeouts */
59 protected static final String TIMEOUT_COMMENT =
60 Config
61 .getString("OV4J.cache.TimeoutComment");
62
63 /** Comment for error. */
64 protected static final String ERROR_COMMENT =
65 Config
66 .getString("OV4J.cache.ErrorComment");
67
68 /** Number of versions to keep in history. */
69 protected static final int CACHE_VERSION_ROLLOVER =
70 Integer
71 .parseInt(Config
72 .getString("OV4J.CacheVersionRollover"));
73
74 /** Controler used for caching. */
75 private final Controler<ClassComparable, CachedResultId> theCtrl;
76
77 /** Timeout before using cached result. */
78 private long timeout = 30000;
79
80 /** Timeout after which the call is considered to fail, even for caching. */
81 private long delayedCacheTimeout = 60000;
82
83 /** Current results. */
84 private final TreeMap<CachedResultId, Version<ClassComparable>> results =
85 new TreeMap<CachedResultId, Version<ClassComparable>>();
86
87 /** Listeners who want to receive notifications. */
88 private final Vector<ICacheHandlerListener> listeners =
89 new Vector<ICacheHandlerListener>();
90
91 /** Whether or not we are running offline. */
92 private boolean offline = false;
93
94 /**
95 * Constructor.
96 *
97 * @param cont
98 * Container to used for storing data.
99 */
100 public CacheHandler(final IContainer<ClassComparable, CachedResultId> cont) {
101 theCtrl = new Controler<ClassComparable, CachedResultId>(cont);
102 setVersionRollover(CacheHandler.CACHE_VERSION_ROLLOVER);
103 }
104
105 /**
106 * Register listener for notifications.
107 *
108 * @param listener
109 * Listener to be added.
110 * @return Whether or not the listener was added.
111 */
112 public boolean addListener(final ICacheHandlerListener listener) {
113 if (CacheHandler.logger.isLoggable(Level.FINEST)) {
114 CacheHandler.logger
115 .entering("CacheHandler", "addListener(ICacheHandlerListener=" + listener + ")", "start");
116 }
117
118 final boolean returnboolean = listeners.add(listener);
119
120 if (CacheHandler.logger.isLoggable(Level.FINEST)) {
121 CacheHandler.logger.exiting("CacheHandler", "addListener(ICacheHandlerListener=" + listener + ")",
122 "end - return value=" + returnboolean);
123 }
124 return returnboolean;
125 }
126
127 /**
128 * Add result to cache.
129 *
130 * @param id
131 * ID to use for storing result.
132 * @param result
133 * Result to be stored.
134 * @throws IOException
135 */
136 protected void addNCommit(final CachedResultId id, final Version<ClassComparable> result) throws IOException {
137 if (CacheHandler.logger.isLoggable(Level.FINER)) {
138 CacheHandler.logger.entering("CacheHandler", "addNCommit(CachedResultId=" + id
139 + ", Version<ClassComparable>=" + result + ")", "start");
140 }
141
142 if (!theCtrl.isClosed()) {
143 if (theCtrl.addNCommit(id, result)) {
144 theCtrl.saveChanges();
145 } else {
146 theCtrl.discardChanges();
147 }
148 }
149
150 if (CacheHandler.logger.isLoggable(Level.FINER)) {
151 CacheHandler.logger.exiting("CacheHandler", "addNCommit(CachedResultId=" + id
152 + ", Version<ClassComparable>=" + result + ")", "end");
153 }
154 }
155
156 /**
157 * Perform a cached call, will wait for a period of time before returning the result from the cache if it exists.
158 * After that period, a background thread will wait another period of time to see if the call makes it and if the
159 * cache can be updated.
160 *
161 * @param theMethod
162 * Method to be called.
163 * @param instance
164 * Object on which the method should be called.
165 * @param args
166 * Arguments to pass to the method call.
167 * @return The result of the call.
168 */
169 public Version<ClassComparable> cachedCall(final Method theMethod, final Object instance, final Object... args) {
170 if (CacheHandler.logger.isLoggable(Level.FINER)) {
171 CacheHandler.logger.entering("CacheHandler", "cachedCall(Method=" + theMethod + ", Object=" + instance
172 + ", Object=" + Arrays.toString(args) + ")", "start");
173 }
174
175 boolean newVersion = false;
176 final MethodCaller theCaller = new MethodCaller(theMethod, instance, args);
177 Version<ClassComparable> result = null;
178 final CachedResultId id = new CachedResultId();
179 id.setMethod(theMethod.toString());
180 if (args != null && args.length == 1 && args[0] instanceof Comparable) {
181 id.setArgs(args);
182 } else {
183 final ClassComparable cc = new ClassComparable();
184 cc.setTheObject(args);
185 id.setArgs(cc);
186 }
187 prefetchLatest(id);
188 theCaller.call(timeout);
189 if (theCaller.hasTimedout()) {
190 result = latest(id);
191 if (result != null) {
192 result.setComment(CacheHandler.TIMEOUT_COMMENT);
193 }
194 setOffline(true);
195 } else if (theCaller.hasFailed()) {
196 result = latest(id);
197 if (result != null) {
198 result.setComment(CacheHandler.ERROR_COMMENT + theCaller.getError().toString());
199 }
200 setOffline(true);
201 } else {
202 final Object res = theCaller.getTheResult();
203 if (res != null) {
204 ClassComparable theObject = null;
205 if (res instanceof ClassComparable) {
206 theObject = (ClassComparable) res;
207 } else {
208 theObject = new ClassComparable();
209 theObject.setTheObject(res);
210 }
211 if (theObject != null) {
212 result = latest(id);
213 if (result == null) {
214 result = new Version<ClassComparable>();
215 result.setAuthor(CacheHandler.CACHE_AUTHOR);
216 result.setVersionNumber(0);
217 newVersion = true;
218 }
219 result.setComment(CacheHandler.NORMAL_COMMENT);
220 result.setVersionedObject(theObject);
221 setOffline(false);
222 }
223 }
224 }
225 final DelayedCacher dc = new DelayedCacher(theCaller, result, delayedCacheTimeout, id, this, newVersion);
226 final Thread t = new Thread(dc);
227 t.setPriority(Thread.MIN_PRIORITY);
228 t.start();
229
230 if (CacheHandler.logger.isLoggable(Level.FINER)) {
231 CacheHandler.logger.exiting("CacheHandler", "cachedCall(Method=" + theMethod + ", Object=" + instance
232 + ", Object=" + Arrays.toString(args) + ")", "end - return value=" + result);
233 }
234 return result;
235 }
236
237 /**
238 * Clear the cache.
239 *
240 */
241 public void clear() {
242 if (CacheHandler.logger.isLoggable(Level.FINER)) {
243 CacheHandler.logger.entering("CacheHandler", "clear()", "start");
244 }
245
246 theCtrl.clear();
247
248 if (CacheHandler.logger.isLoggable(Level.FINER)) {
249 CacheHandler.logger.exiting("CacheHandler", "clear()", "end");
250 }
251 }
252
253 /**
254 * Close the cache.
255 *
256 */
257 public void close() {
258 if (CacheHandler.logger.isLoggable(Level.FINER)) {
259 CacheHandler.logger.entering("CacheHandler", "close()", "start");
260 }
261
262 theCtrl.close();
263
264 if (CacheHandler.logger.isLoggable(Level.FINER)) {
265 CacheHandler.logger.exiting("CacheHandler", "close()", "end");
266 }
267 }
268
269 /**
270 * Commit the result in cache.
271 *
272 * @param id
273 * ID to used for storing result.
274 * @param result
275 * Result to be stored.
276 * @throws InstantiationException
277 * @throws IllegalAccessException
278 * @throws ClassNotFoundException
279 * @throws IOException
280 */
281 protected void commit(final CachedResultId id, final Version<ClassComparable> result)
282 throws InstantiationException, IllegalAccessException, ClassNotFoundException, IOException {
283 if (CacheHandler.logger.isLoggable(Level.FINER)) {
284 CacheHandler.logger.entering("CacheHandler", "commit(CachedResultId=" + id + ", Version<ClassComparable>="
285 + result + ")", "start");
286 }
287
288 if (!theCtrl.isClosed()) {
289 if (theCtrl.commit(id, result, (Class<ComparisonResult<ClassComparable>>) Class
290 .forName("org.ov4j.comp.DummyComparisonResult"))) {
291 theCtrl.saveChanges();
292 } else {
293 theCtrl.discardChanges();
294 }
295 }
296
297 if (CacheHandler.logger.isLoggable(Level.FINER)) {
298 CacheHandler.logger.exiting("CacheHandler", "commit(CachedResultId=" + id + ", Version<ClassComparable>="
299 + result + ")", "end");
300 }
301 }
302
303 /**
304 * @return Returns the delayedCacheTimeout.
305 */
306 public long getDelayedCacheTimeout() {
307 return delayedCacheTimeout;
308 }
309
310 /**
311 * @return Returns the timeout.
312 */
313 public long getTimeout() {
314 return timeout;
315 }
316
317 /**
318 *
319 * @return The version rollover.
320 */
321 public int getVersionRollover() {
322 return theCtrl.getVersionRollover();
323 }
324
325 /**
326 * Fetch latest result for the given ID.
327 *
328 * @param id
329 * ID to be fetched.
330 * @return The latest result for the given ID.
331 */
332 protected Version<ClassComparable> latest(final CachedResultId id) {
333 if (CacheHandler.logger.isLoggable(Level.FINER)) {
334 CacheHandler.logger.entering("CacheHandler", "latest(CachedResultId=" + id + ")", "start");
335 }
336
337 while (!results.containsKey(id)) {
338 Thread.yield();
339 }
340 final Version<ClassComparable> returnVersion = results.get(id);
341
342 if (CacheHandler.logger.isLoggable(Level.FINER)) {
343 CacheHandler.logger.exiting("CacheHandler", "latest(CachedResultId=" + id + ")", "end - return value="
344 + returnVersion);
345 }
346 return returnVersion;
347 }
348
349 /**
350 * Starts a thread which will try fetching latest result in the background in case it is needed.
351 *
352 * @param id
353 * ID to prefetch.
354 */
355 private void prefetchLatest(final CachedResultId id) {
356 if (CacheHandler.logger.isLoggable(Level.FINER)) {
357 CacheHandler.logger.entering("CacheHandler", "prefetchLatest(CachedResultId=" + id + ")", "start");
358 }
359
360 final ResultFetcher t = new ResultFetcher(results, theCtrl, id);
361 t.start();
362
363 if (CacheHandler.logger.isLoggable(Level.FINER)) {
364 CacheHandler.logger.exiting("CacheHandler", "prefetchLatest(CachedResultId=" + id + ")", "end");
365 }
366 }
367
368 /**
369 * Unregister listener for notifications.
370 *
371 * @param listener
372 * Listener to be removed.
373 * @return Whether or not the listener was found and removed.
374 */
375 public boolean removeListener(final ICacheHandlerListener listener) {
376 if (CacheHandler.logger.isLoggable(Level.FINEST)) {
377 CacheHandler.logger.entering("CacheHandler", "removeListener(ICacheHandlerListener=" + listener + ")",
378 "start");
379 }
380
381 final boolean returnboolean = listeners.remove(listener);
382
383 if (CacheHandler.logger.isLoggable(Level.FINEST)) {
384 CacheHandler.logger.exiting("CacheHandler", "removeListener(ICacheHandlerListener=" + listener + ")",
385 "end - return value=" + returnboolean);
386 }
387 return returnboolean;
388 }
389
390 /**
391 * @param delayedCacheTimeout
392 * The delayedCacheTimeout to set.
393 */
394 public void setDelayedCacheTimeout(final long delayedCacheTimeout) {
395 this.delayedCacheTimeout = delayedCacheTimeout;
396 }
397
398 /**
399 * Set offline, will notify listeners.
400 *
401 * @param off
402 * Whether or not we are running offline (normally when last call timedout).
403 */
404 private void setOffline(final boolean off) {
405 if (CacheHandler.logger.isLoggable(Level.FINER)) {
406 CacheHandler.logger.entering("CacheHandler", "setOffline(boolean=" + off + ")", "start");
407 }
408
409 if (offline != off) {
410 offline = off;
411 for (int i = 0; i < listeners.size(); i++) {
412 listeners.elementAt(i).setOffline(offline);
413 }
414 }
415
416 if (CacheHandler.logger.isLoggable(Level.FINER)) {
417 CacheHandler.logger.exiting("CacheHandler", "setOffline(boolean=" + off + ")", "end");
418 }
419 }
420
421 /**
422 * @param timeout
423 * The timeout to set.
424 */
425 public void setTimeout(final long timeout) {
426 this.timeout = timeout;
427 }
428
429 /**
430 *
431 * @param versionRollover
432 * The version rollover to set.
433 */
434 public void setVersionRollover(final int versionRollover) {
435 theCtrl.setVersionRollover(versionRollover);
436 }
437 }