001package 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
022import java.io.ByteArrayOutputStream;
023import java.io.IOException;
024import java.io.ObjectOutputStream;
025import java.io.Serializable;
026import java.util.ArrayList;
027import java.util.List;
028import java.util.Set;
029
030import org.apache.avalon.framework.activity.Disposable;
031import org.apache.avalon.framework.activity.Initializable;
032import org.apache.avalon.framework.configuration.Configurable;
033import org.apache.avalon.framework.configuration.Configuration;
034import org.apache.avalon.framework.configuration.ConfigurationException;
035import org.apache.avalon.framework.logger.AbstractLogEnabled;
036import org.apache.avalon.framework.thread.ThreadSafe;
037import org.apache.commons.jcs3.JCS;
038import org.apache.commons.jcs3.access.GroupCacheAccess;
039import org.apache.commons.jcs3.access.exception.CacheException;
040import org.apache.commons.jcs3.engine.ElementAttributes;
041import org.apache.commons.jcs3.engine.control.CompositeCacheManager;
042import org.apache.fulcrum.cache.CachedObject;
043import org.apache.fulcrum.cache.GlobalCacheService;
044import org.apache.fulcrum.cache.ObjectExpiredException;
045import org.apache.fulcrum.cache.RefreshableCachedObject;
046
047/**
048 * Default implementation of JCSCacheService
049 *
050 * @author <a href="mailto:tv@apache.org">Thomas Vandahl</a>
051 * @version $Id$
052 */
053public class JCSCacheService extends AbstractLogEnabled implements
054        GlobalCacheService, Runnable, Configurable, Disposable, Initializable,
055        ThreadSafe
056{
057    /**
058     * Cache check frequency in Millis (1000 Millis = 1 second). Value must be &gt;
059     * 0. Default = 5 seconds
060     */
061    public static final long DEFAULT_CACHE_CHECK_FREQUENCY = 5000; // 5 seconds
062
063    /**
064     * cacheCheckFrequency (default - 5 seconds)
065     */
066    private long cacheCheckFrequency;
067
068    /**
069     * Instance of the JCS cache
070     */
071    private GroupCacheAccess<String, CachedObject<?>> cacheManager;
072
073    /**
074     * JCS region to use
075     */
076    private String region;
077
078    /**
079     * Path name of the JCS configuration file
080     */
081    private String configFile;
082
083    /**
084     * Constant value which provides a group name
085     */
086    private static String group = "default_group";
087
088    /** thread for refreshing stale items in the cache */
089    private Thread refreshing;
090
091    /** flag to stop the housekeeping thread when the component is disposed. */
092    private boolean continueThread;
093    
094    public JCSCacheService()
095    {
096        
097    }
098
099    // ---------------- Avalon Lifecycle Methods ---------------------
100
101    /**
102     * @see org.apache.avalon.framework.configuration.Configurable#configure(org.apache.avalon.framework.configuration.Configuration)
103     */
104    @Override
105        public void configure(Configuration config) throws ConfigurationException
106    {
107        this.cacheCheckFrequency = config.getChild("cacheCheckFrequency")
108                .getValueAsLong(DEFAULT_CACHE_CHECK_FREQUENCY);
109        this.region = config.getChild("region").getValue("fulcrum");
110        this.configFile = config.getChild("configurationFile").getValue(
111                "/cache.ccf");
112    }
113
114    /**
115     * @see org.apache.avalon.framework.activity.Initializable#initialize()
116     */
117    @Override
118        public void initialize() throws Exception
119    {
120        JCS.setConfigFilename(this.configFile);
121        this.cacheManager = JCS.getGroupCacheInstance(this.region);
122
123        // Start housekeeping thread.
124        this.continueThread = true;
125        this.refreshing = new Thread(this);
126
127        // Indicate that this is a system thread. JVM will quit only when
128        // there are no more active user threads. Settings threads spawned
129        // internally by Turbine as daemons allows commandline applications
130        // using Turbine to terminate in an orderly manner.
131        this.refreshing.setDaemon(true);
132        this.refreshing.setName("JCSCacheService Refreshing");
133        this.refreshing.start();
134
135        getLogger().debug("JCSCacheService started.");
136    }
137
138    /**
139     * @see org.apache.avalon.framework.activity.Disposable#dispose()
140     */
141    @Override
142        public void dispose()
143    {
144        this.continueThread = false;
145        this.refreshing.interrupt();
146
147        this.cacheManager.dispose();
148        this.cacheManager = null;
149        CompositeCacheManager.getInstance().shutDown();
150
151        getLogger().debug("JCSCacheService stopped.");
152    }
153
154    /**
155     * @see org.apache.fulcrum.cache.GlobalCacheService#getObject(java.lang.String)
156     */
157    @Override
158        public <T> CachedObject<T> getObject(final String objectId) throws ObjectExpiredException
159    {
160        @SuppressWarnings("unchecked")
161        CachedObject<T> cachedObject = (CachedObject<T>)this.cacheManager.getFromGroup(objectId, group);
162
163        if (cachedObject == null)
164        {
165            // Not in the cache.
166            throw new ObjectExpiredException();
167        }
168
169        if (cachedObject.isStale())
170        {
171            if (cachedObject instanceof RefreshableCachedObject)
172            {
173                RefreshableCachedObject<?> refreshableObject = (RefreshableCachedObject<?>) cachedObject;
174                if (refreshableObject.isUntouched())
175                {
176                    // Do not refresh an object that has exceeded TimeToLive
177                    removeObject(objectId);
178                    throw new ObjectExpiredException();
179                }
180
181                // Refresh Object
182                refreshableObject.refresh();
183                if (refreshableObject.isStale())
184                {
185                    // Object is Expired, remove it from cache.
186                    removeObject(objectId);
187                    throw new ObjectExpiredException();
188                }
189            }
190            else
191            {
192                // Expired.
193                removeObject(objectId);
194                throw new ObjectExpiredException();
195            }
196        }
197
198        if (cachedObject instanceof RefreshableCachedObject)
199        {
200            // notify it that it's being accessed.
201            RefreshableCachedObject<?> refreshableCachedObject = (RefreshableCachedObject<?>) cachedObject;
202            refreshableCachedObject.touch();
203        }
204
205        return cachedObject;
206    }
207
208    /**
209     * @see org.apache.fulcrum.cache.GlobalCacheService#addObject(java.lang.String,
210     *      org.apache.fulcrum.cache.CachedObject)
211     */
212    @Override
213        public <T> void addObject(String objectId, CachedObject<T> cachedObject)
214    {
215        try
216        {
217            if (!(cachedObject.getContents() instanceof Serializable))
218            {
219                getLogger()
220                        .warn(
221                                "Object (contents) with id ["
222                                        + objectId
223                                        + "] is not serializable. Expect problems with auxiliary caches: " + 
224                                        cachedObject.getContents().getClass().getSimpleName());
225            }
226
227            ElementAttributes attrib = (ElementAttributes) this.cacheManager.getDefaultElementAttributes();
228
229            if (cachedObject instanceof RefreshableCachedObject)
230            {
231                attrib.setIsEternal(true);
232            }
233            else
234            {
235                attrib.setIsEternal(false);
236                // expires in millis, maxlife in seconds
237                double tmp0 = ((double) (cachedObject.getExpires() + 500)) / 1000;
238                getLogger().debug( "setting maxlife seconds (minimum 1sec) from expiry + 0.5s: " + (int)tmp0 );
239                attrib.setMaxLife( tmp0 > 0 ? (int) Math.floor( tmp0 ) : 1 );
240            }
241
242            attrib.setLastAccessTimeNow();
243            attrib.setCreateTime();
244
245            this.cacheManager.putInGroup(objectId, group, cachedObject, attrib);
246        }
247        catch (CacheException e)
248        {
249            getLogger().error("Could not add object " + objectId + " to cache", e);
250        }
251    }
252
253    /**
254     * @see org.apache.fulcrum.cache.GlobalCacheService#removeObject(java.lang.String)
255     */
256    @Override
257        public void removeObject(String objectId)
258    {
259        this.cacheManager.removeFromGroup(objectId, group);
260    }
261
262    /**
263     * @see org.apache.fulcrum.cache.GlobalCacheService#getKeys()
264     */
265    @Override
266        public List<String> getKeys()
267    {
268        ArrayList<String> keys = new ArrayList<>();
269        keys.addAll(this.cacheManager.getGroupKeys(group));
270        return keys;
271    }
272
273    /**
274     * @see org.apache.fulcrum.cache.GlobalCacheService#getCachedObjects()
275     */
276    @Override
277        public List<CachedObject<?>> getCachedObjects()
278    {
279        ArrayList<CachedObject<?>> values = new ArrayList<>();
280        for (String key : this.cacheManager.getGroupKeys(group))
281        {
282            CachedObject<?> cachedObject = this.cacheManager.getFromGroup(key, group);
283            if (cachedObject != null)
284            {
285                values.add(cachedObject);
286            }
287        }
288
289        return values;
290    }
291
292    /**
293     * Circle through the cache and refresh stale objects. Frequency is
294     * determined by the cacheCheckFrequency property.
295     */
296    @Override
297        public void run()
298    {
299        while (this.continueThread)
300        {
301            // Sleep for amount of time set in cacheCheckFrequency -
302            // default = 5 seconds.
303            try
304            {
305                Thread.sleep(this.cacheCheckFrequency);
306            }
307            catch (InterruptedException exc)
308            {
309                if (!this.continueThread)
310                {
311                    return;
312                }
313            }
314
315            for (String key : this.cacheManager.getGroupKeys(group))
316            {
317                CachedObject<?> cachedObject = this.cacheManager.getFromGroup(key, group);
318                if (cachedObject == null)
319                {
320                    removeObject(key);
321                }
322                else
323                {
324                    if (cachedObject instanceof RefreshableCachedObject)
325                    {
326                        RefreshableCachedObject<?> refreshableObject = (RefreshableCachedObject<?>) cachedObject;
327                        if (refreshableObject.isUntouched())
328                        {
329                            this.cacheManager.removeFromGroup(key, group);
330                        }
331                        else if (refreshableObject.isStale())
332                        {
333                            refreshableObject.refresh();
334                        }
335                    }
336                }
337            }
338        }
339    }
340
341    /**
342     * @see org.apache.fulcrum.cache.GlobalCacheService#getCacheSize()
343     */
344    @Override
345        public int getCacheSize() throws IOException
346    {
347        // This is evil!
348        ByteArrayOutputStream baos = new ByteArrayOutputStream();
349        ObjectOutputStream out = new ObjectOutputStream(baos);
350        Set<String> keys = this.cacheManager.getGroupKeys(group);
351
352        for (String key : keys)
353        {
354            out.writeObject(this.cacheManager.getFromGroup(key, group));
355        }
356
357        out.flush();
358
359        //
360        // Subtract 4 bytes from the length, because the serialization
361        // magic number (2 bytes) and version number (2 bytes) are
362        // both written to the stream before the object
363        //
364        return baos.toByteArray().length - 4 * keys.size();
365    }
366
367    /**
368     * @see org.apache.fulcrum.cache.GlobalCacheService#getNumberOfObjects()
369     */
370    @Override
371        public int getNumberOfObjects()
372    {
373        int count = 0;
374
375        for (String key : this.cacheManager.getGroupKeys(group))
376        {
377            if (this.cacheManager.getFromGroup(key, group) != null)
378            {
379                count++;
380            }
381        }
382
383        return count;
384    }
385
386    /**
387     * @see org.apache.fulcrum.cache.GlobalCacheService#flushCache()
388     */
389    @Override
390        public void flushCache()
391    {
392        this.cacheManager.invalidateGroup(group);
393    }
394}