001    package org.apache.fulcrum.cache.impl;
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    
022    import java.io.ByteArrayOutputStream;
023    import java.io.IOException;
024    import java.io.ObjectOutputStream;
025    import java.util.ArrayList;
026    import java.util.Enumeration;
027    import java.util.Hashtable;
028    import java.util.Iterator;
029    import java.util.List;
030    
031    import org.apache.avalon.framework.activity.Disposable;
032    import org.apache.avalon.framework.activity.Initializable;
033    import org.apache.avalon.framework.configuration.Configurable;
034    import org.apache.avalon.framework.configuration.Configuration;
035    import org.apache.avalon.framework.configuration.ConfigurationException;
036    import org.apache.avalon.framework.logger.AbstractLogEnabled;
037    import org.apache.avalon.framework.thread.ThreadSafe;
038    import org.apache.fulcrum.cache.CachedObject;
039    import org.apache.fulcrum.cache.GlobalCacheService;
040    import org.apache.fulcrum.cache.ObjectExpiredException;
041    import org.apache.fulcrum.cache.RefreshableCachedObject;
042    
043    /**
044     * This Service functions as a Global Cache. A global cache is a good place to
045     * store items that you may need to access often but don't necessarily need (or
046     * want) to fetch from the database everytime. A good example would be a look up
047     * table of States that you store in a database and use throughout your
048     * application. Since information about States doesn't change very often, you
049     * could store this information in the Global Cache and decrease the overhead of
050     * hitting the database everytime you need State information.
051     * 
052     * @author <a href="mailto:mbryson@mont.mindspring.com">Dave Bryson</a>
053     * @author <a href="mailto:jon@clearink.com">Jon S. Stevens</a>
054     * @author <a href="mailto:john@zenplex.com">John Thorhauer</a>
055     * @author <a href="mailto:hps@intermeta.de">Henning P. Schmiedehausen</a>
056     * @author <a href="mailto:epugh@upstate.com">Eric Pugh</a>
057     * @author <a href="mailto:peter@courcoux.biz">Peter Courcoux</a>
058     * @version $Id: DefaultGlobalCacheService.java 927153 2010-03-24 18:55:08Z tv $
059     */
060    public class DefaultGlobalCacheService extends AbstractLogEnabled implements
061            GlobalCacheService, Runnable, Configurable, Initializable, Disposable,
062            ThreadSafe
063    {
064        /**
065         * Initial size of hash table Value must be > 0. Default = 20
066         */
067        public static final int DEFAULT_INITIAL_CACHE_SIZE = 20;
068    
069        /**
070         * The property for the InitalCacheSize
071         */
072        public static final String INITIAL_CACHE_SIZE = "cacheInitialSize";
073    
074        /**
075         * The property for the Cache check frequency
076         */
077        public static final String CACHE_CHECK_FREQUENCY = "cacheCheckFrequency";
078    
079        /**
080         * Cache check frequency in Millis (1000 Millis = 1 second). Value must be >
081         * 0. Default = 5 seconds
082         */
083        public static final long DEFAULT_CACHE_CHECK_FREQUENCY = 5000; // 5 seconds
084    
085        /** The cache. * */
086        protected Hashtable cache = null;
087    
088        /**
089         * cacheCheckFrequency (default - 5 seconds)
090         */
091        private long cacheCheckFrequency;
092    
093        /**
094         * cacheInitialSize (default - 20)
095         */
096        private int cacheInitialSize;
097    
098        /** thread for removing stale items from the cache */
099        private Thread housekeeping;
100    
101        /** flag to stop the housekeeping thread when the component is disposed. */
102        private boolean continueThread;
103    
104        /**
105         * Get the Cache Check Frequency in milliseconds
106         * 
107         * @return the time between two cache check runs in milliseconds
108         */
109        public long getCacheCheckFrequency()
110        {
111            return this.cacheCheckFrequency;
112        }
113    
114        /**
115         * Returns an item from the cache. /** Returns an item from the cache.
116         * RefreshableCachedObject will be refreshed if it is expired and not
117         * untouched.
118         * 
119         * @param id
120         *            The key of the stored object.
121         * @return The object from the cache.
122         * @exception ObjectExpiredException,
123         *                when either the object is not in the cache or it has
124         *                expired.
125         */
126        public CachedObject getObject(String id) throws ObjectExpiredException
127        {
128            CachedObject obj = null;
129            obj = (CachedObject) this.cache.get(id);
130            if (obj == null)
131            {
132                // Not in the cache.
133                throw new ObjectExpiredException();
134            }
135            if (obj.isStale())
136            {
137                if (obj instanceof RefreshableCachedObject)
138                {
139                    RefreshableCachedObject rco = (RefreshableCachedObject) obj;
140                    if (rco.isUntouched())
141                    {
142                        throw new ObjectExpiredException();
143                    }
144                    // Refresh Object
145                    rco.refresh();
146                    if (rco.isStale())
147                    {
148                        throw new ObjectExpiredException();
149                    }
150                }
151                else
152                {
153                    // Expired.
154                    throw new ObjectExpiredException();
155                }
156            }
157            if (obj instanceof RefreshableCachedObject)
158            {
159                // notify it that it's being accessed.
160                RefreshableCachedObject rco = (RefreshableCachedObject) obj;
161                rco.touch();
162            }
163            return obj;
164        }
165    
166        /**
167         * Adds an object to the cache.
168         * 
169         * @param id
170         *            The key to store the object by.
171         * @param o
172         *            The object to cache.
173         */
174        public void addObject(String id, CachedObject o)
175        {
176            // If the cache already contains the key, remove it and add
177            // the fresh one.
178            if (this.cache.containsKey(id))
179            {
180                this.cache.remove(id);
181            }
182            this.cache.put(id, o);
183        }
184    
185        /**
186         * Removes an object from the cache.
187         * 
188         * @param id
189         *            The String id for the object.
190         */
191        public void removeObject(String id)
192        {
193            this.cache.remove(id);
194        }
195    
196        /**
197         * Returns a copy of keys to objects in the cache as a list.
198         * 
199         * Note that keys to expired objects are not returned.
200         * 
201         * @return A List of <code>String</code>'s representing the keys to
202         *         objects in the cache.
203         */
204        public List getKeys()
205        {
206            ArrayList keys = new ArrayList(this.cache.size());
207            synchronized (this)
208            {
209                for (Iterator itr = this.cache.keySet().iterator(); itr.hasNext();)
210                {
211                    String key = (String) itr.next();
212                    try
213                    {
214                        /* CachedObject obj = */getObject(key);
215                    }
216                    catch (ObjectExpiredException oee)
217                    {
218                        // this is OK we just do not want this key
219                        continue;
220                    }
221                    keys.add(new String(key));
222                }
223            }
224            return keys;
225        }
226    
227        /**
228         * Returns a copy of the non-expired CachedObjects in the cache as a list.
229         * 
230         * @return A List of <code>CachedObject</code> objects held in the cache
231         */
232        public List getCachedObjects()
233        {
234            ArrayList objects = new ArrayList(this.cache.size());
235            synchronized (this)
236            {
237                for (Iterator itr = this.cache.keySet().iterator(); itr.hasNext();)
238                {
239                    String key = (String) itr.next();
240                    CachedObject obj = null;
241                    try
242                    {
243                        obj = getObject(key);
244                    }
245                    catch (ObjectExpiredException oee)
246                    {
247                        // this is OK we just do not want this object
248                        continue;
249                    }
250                    objects.add(obj);
251                }
252            }
253            return objects;
254        }
255    
256        /**
257         * Circle through the cache and remove stale objects. Frequency is
258         * determined by the cacheCheckFrequency property.
259         */
260        public void run()
261        {
262            while (this.continueThread)
263            {
264                // Sleep for amount of time set in cacheCheckFrequency -
265                // default = 5 seconds.
266                try
267                {
268                    Thread.sleep(this.cacheCheckFrequency);
269                }
270                catch (InterruptedException exc)
271                {
272                    if (!this.continueThread)
273                    {
274                        return;
275                    }
276                }
277    
278                clearCache();
279            }
280        }
281    
282        /**
283         * Iterate through the cache and remove or refresh stale objects.
284         */
285        public void clearCache()
286        {
287            List refreshThese = new ArrayList(20);
288            // Sync on this object so that other threads do not
289            // change the Hashtable while enumerating over it.
290            synchronized (this)
291            {
292                for (Enumeration e = this.cache.keys(); e.hasMoreElements();)
293                {
294                    String key = (String) e.nextElement();
295                    CachedObject co = (CachedObject) this.cache.get(key);
296                    if (co instanceof RefreshableCachedObject)
297                    {
298                        RefreshableCachedObject rco = (RefreshableCachedObject) co;
299                        if (rco.isUntouched())
300                        {
301                            this.cache.remove(key);
302                        }
303                        else if (rco.isStale())
304                        {
305                            // to prolong holding the lock on this object
306                            refreshThese.add(key);
307                        }
308                    }
309                    else if (co.isStale())
310                    {
311                        this.cache.remove(key);
312                    }
313                }
314            }
315            for (Iterator i = refreshThese.iterator(); i.hasNext();)
316            {
317                String key = (String) i.next();
318                CachedObject co = (CachedObject) this.cache.get(key);
319                RefreshableCachedObject rco = (RefreshableCachedObject) co;
320                rco.refresh();
321            }
322        }
323    
324        /**
325         * Returns the number of objects currently stored in the cache
326         * 
327         * @return int number of object in the cache
328         */
329        public int getNumberOfObjects()
330        {
331            return this.cache.size();
332        }
333    
334        /**
335         * Returns the current size of the cache.
336         * 
337         * @return int representing current cache size in number of bytes
338         */
339        public int getCacheSize() throws IOException
340        {
341            ByteArrayOutputStream baos = new ByteArrayOutputStream();
342            ObjectOutputStream out = new ObjectOutputStream(baos);
343            out.writeObject(this.cache);
344            out.flush();
345            //
346            // Subtract 4 bytes from the length, because the serialization
347            // magic number (2 bytes) and version number (2 bytes) are
348            // both written to the stream before the object
349            //
350            int objectsize = baos.toByteArray().length - 4;
351            return objectsize;
352        }
353    
354        /**
355         * Flush the cache of all objects.
356         */
357        public void flushCache()
358        {
359            synchronized (this)
360            {
361                for (Enumeration e = this.cache.keys(); e.hasMoreElements();)
362                {
363                    String key = (String) e.nextElement();
364                    this.cache.remove(key);
365                }
366            }
367        }
368    
369        // ---------------- Avalon Lifecycle Methods ---------------------
370        /**
371         * Avalon component lifecycle method
372         */
373        public void configure(Configuration conf) throws ConfigurationException
374        {
375            this.cacheCheckFrequency = conf.getAttributeAsLong(
376                    CACHE_CHECK_FREQUENCY, DEFAULT_CACHE_CHECK_FREQUENCY);
377            this.cacheInitialSize = conf.getAttributeAsInteger(INITIAL_CACHE_SIZE,
378                    DEFAULT_INITIAL_CACHE_SIZE);
379        }
380    
381        /**
382         * Avalon component lifecycle method
383         */
384        public void initialize() throws Exception
385        {
386            try
387            {
388                this.cache = new Hashtable(this.cacheInitialSize);
389                // Start housekeeping thread.
390                this.continueThread = true;
391                this.housekeeping = new Thread(this);
392                // Indicate that this is a system thread. JVM will quit only when
393                // there are no more active user threads. Settings threads spawned
394                // internally by Turbine as daemons allows commandline applications
395                // using Turbine to terminate in an orderly manner.
396                this.housekeeping.setDaemon(true);
397                this.housekeeping.start();
398            }
399            catch (Exception e)
400            {
401                throw new Exception(
402                        "DefaultGlobalCacheService failed to initialize", e);
403            }
404        }
405    
406        /**
407         * Avalon component lifecycle method
408         */
409        public void dispose()
410        {
411            this.continueThread = false;
412            this.housekeeping.interrupt();
413        }
414    
415        /**
416         * The name used to specify this component in TurbineResources.properties
417         * 
418         * @deprecated part of the pre-avalon compatibility layer
419         */
420        protected String getName()
421        {
422            return "GlobalCacheService";
423        }
424    }