001package org.apache.commons.jcs3.admin;
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.IOException;
023import java.io.ObjectOutputStream;
024import java.io.Serializable;
025import java.text.DateFormat;
026import java.util.Date;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Map;
030import java.util.Set;
031import java.util.TreeMap;
032import java.util.TreeSet;
033import java.util.stream.Collectors;
034
035import org.apache.commons.jcs3.access.exception.CacheException;
036import org.apache.commons.jcs3.auxiliary.remote.server.RemoteCacheServer;
037import org.apache.commons.jcs3.auxiliary.remote.server.RemoteCacheServerFactory;
038import org.apache.commons.jcs3.engine.CacheElementSerialized;
039import org.apache.commons.jcs3.engine.behavior.ICacheElement;
040import org.apache.commons.jcs3.engine.behavior.IElementAttributes;
041import org.apache.commons.jcs3.engine.control.CompositeCache;
042import org.apache.commons.jcs3.engine.control.CompositeCacheManager;
043import org.apache.commons.jcs3.engine.memory.behavior.IMemoryCache;
044
045/**
046 * A servlet which provides HTTP access to JCS. Allows a summary of regions to be viewed, and
047 * removeAll to be run on individual regions or all regions. Also provides the ability to remove
048 * items (any number of key arguments can be provided with action 'remove'). Should be initialized
049 * with a properties file that provides at least a classpath resource loader.
050 */
051public class JCSAdminBean implements JCSJMXBean
052{
053    /** The cache manager. */
054    private final CompositeCacheManager cacheHub;
055
056    /**
057     * Default constructor
058     */
059    public JCSAdminBean()
060    {
061        try
062        {
063            this.cacheHub = CompositeCacheManager.getInstance();
064        }
065        catch (final CacheException e)
066        {
067            throw new RuntimeException("Could not retrieve cache manager instance", e);
068        }
069    }
070
071    /**
072     * Parameterized constructor
073     *
074         * @param cacheHub the cache manager instance
075         */
076        public JCSAdminBean(final CompositeCacheManager cacheHub)
077        {
078                this.cacheHub = cacheHub;
079        }
080
081        /**
082     * Builds up info about each element in a region.
083     * <p>
084     * @param cacheName
085     * @return List of CacheElementInfo objects
086     * @throws IOException
087     */
088    @Override
089    public List<CacheElementInfo> buildElementInfo( final String cacheName )
090        throws IOException
091    {
092        final CompositeCache<Object, Object> cache = cacheHub.getCache( cacheName );
093
094        // Convert all keys to string, store in a sorted map
095        final TreeMap<String, ?> keys = new TreeMap<>(cache.getMemoryCache().getKeySet()
096                .stream()
097                .collect(Collectors.toMap(Object::toString, k -> k)));
098
099        final LinkedList<CacheElementInfo> records = new LinkedList<>();
100
101        final DateFormat format = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT );
102
103        final long now = System.currentTimeMillis();
104
105        for (final Map.Entry<String, ?> key : keys.entrySet())
106        {
107            final ICacheElement<?, ?> element = cache.getMemoryCache().getQuiet( key.getValue() );
108
109            final IElementAttributes attributes = element.getElementAttributes();
110
111            final CacheElementInfo elementInfo = new CacheElementInfo(
112                        key.getKey(),
113                        attributes.getIsEternal(),
114                        format.format(new Date(attributes.getCreateTime())),
115                        attributes.getMaxLife(),
116                        (now - attributes.getCreateTime() - attributes.getMaxLife() * 1000 ) / -1000);
117
118            records.add( elementInfo );
119        }
120
121        return records;
122    }
123
124    /**
125     * Builds up data on every region.
126     * <p>
127     * TODO we need a most light weight method that does not count bytes. The byte counting can
128     *       really swamp a server.
129     * @return List of CacheRegionInfo objects
130     */
131    @Override
132    public List<CacheRegionInfo> buildCacheInfo()
133    {
134        final TreeSet<String> cacheNames = new TreeSet<>(cacheHub.getCacheNames());
135
136        final LinkedList<CacheRegionInfo> cacheInfo = new LinkedList<>();
137
138        for (final String cacheName : cacheNames)
139        {
140            final CompositeCache<?, ?> cache = cacheHub.getCache( cacheName );
141
142            final CacheRegionInfo regionInfo = new CacheRegionInfo(
143                    cache.getCacheName(),
144                    cache.getSize(),
145                    cache.getStatus().toString(),
146                    cache.getStats(),
147                    cache.getHitCountRam(),
148                    cache.getHitCountAux(),
149                    cache.getMissCountNotFound(),
150                    cache.getMissCountExpired(),
151                    getByteCount( cache ));
152
153            cacheInfo.add( regionInfo );
154        }
155
156        return cacheInfo;
157    }
158
159
160        /**
161     * Tries to estimate how much data is in a region. This is expensive. If there are any non serializable objects in
162     * the region or an error occurs, suppresses exceptions and returns 0.
163     * <p>
164     *
165     * @return int The size of the region in bytes.
166     */
167        @Override
168    public long getByteCount(final String cacheName)
169        {
170                return getByteCount(cacheHub.getCache(cacheName));
171        }
172
173        /**
174     * Tries to estimate how much data is in a region. This is expensive. If there are any non serializable objects in
175     * the region or an error occurs, suppresses exceptions and returns 0.
176     * <p>
177     *
178     * @return int The size of the region in bytes.
179     */
180    public <K, V> long getByteCount(final CompositeCache<K, V> cache)
181    {
182        if (cache == null)
183        {
184            throw new IllegalArgumentException("The cache object specified was null.");
185        }
186
187        long size = 0;
188        final IMemoryCache<K, V> memCache = cache.getMemoryCache();
189
190        for (final K key : memCache.getKeySet())
191        {
192            ICacheElement<K, V> ice = null;
193                        try
194                        {
195                                ice = memCache.get(key);
196                        }
197                        catch (final IOException e)
198                        {
199                throw new RuntimeException("IOException while trying to get a cached element", e);
200                        }
201
202                        if (ice == null)
203                        {
204                                continue;
205                        }
206
207                        if (ice instanceof CacheElementSerialized)
208            {
209                size += ((CacheElementSerialized<K, V>) ice).getSerializedValue().length;
210            }
211            else
212            {
213                final Object element = ice.getVal();
214
215                //CountingOnlyOutputStream: Keeps track of the number of bytes written to it, but doesn't write them anywhere.
216                final CountingOnlyOutputStream counter = new CountingOnlyOutputStream();
217                try (ObjectOutputStream out = new ObjectOutputStream(counter))
218                {
219                    out.writeObject(element);
220                }
221                catch (final IOException e)
222                {
223                    throw new RuntimeException("IOException while trying to measure the size of the cached element", e);
224                }
225                finally
226                {
227                        try
228                        {
229                                                counter.close();
230                                        }
231                        catch (final IOException e)
232                        {
233                                // ignore
234                                        }
235                }
236
237                // 4 bytes lost for the serialization header
238                size += counter.getCount() - 4;
239            }
240        }
241
242        return size;
243    }
244
245    /**
246     * Clears all regions in the cache.
247     * <p>
248     * If this class is running within a remote cache server, clears all regions via the <code>RemoteCacheServer</code>
249     * API, so that removes will be broadcast to client machines. Otherwise clears all regions in the cache directly via
250     * the usual cache API.
251     */
252    @Override
253    public void clearAllRegions() throws IOException
254    {
255        final RemoteCacheServer<?, ?> remoteCacheServer = RemoteCacheServerFactory.getRemoteCacheServer();
256
257        if (remoteCacheServer == null)
258        {
259            // Not running in a remote cache server.
260            // Remove objects from the cache directly, as no need to broadcast removes to client machines...
261            for (final String name : cacheHub.getCacheNames())
262            {
263                cacheHub.getCache(name).removeAll();
264            }
265        }
266        else
267        {
268            // Running in a remote cache server.
269            // Remove objects via the RemoteCacheServer API, so that removes will be broadcast to client machines...
270            // Call remoteCacheServer.removeAll(String) for each cacheName...
271            for (final String name : cacheHub.getCacheNames())
272            {
273                remoteCacheServer.removeAll(name);
274            }
275        }
276    }
277
278    /**
279     * Clears a particular cache region.
280     * <p>
281     * If this class is running within a remote cache server, clears the region via the <code>RemoteCacheServer</code>
282     * API, so that removes will be broadcast to client machines. Otherwise clears the region directly via the usual
283     * cache API.
284     */
285    @Override
286    public void clearRegion(final String cacheName) throws IOException
287    {
288        if (cacheName == null)
289        {
290            throw new IllegalArgumentException("The cache name specified was null.");
291        }
292        if (RemoteCacheServerFactory.getRemoteCacheServer() == null)
293        {
294            // Not running in a remote cache server.
295            // Remove objects from the cache directly, as no need to broadcast removes to client machines...
296            cacheHub.getCache(cacheName).removeAll();
297        }
298        else
299        {
300            // Running in a remote cache server.
301            // Remove objects via the RemoteCacheServer API, so that removes will be broadcast to client machines...
302            try
303            {
304                // Call remoteCacheServer.removeAll(String)...
305                final RemoteCacheServer<?, ?> remoteCacheServer = RemoteCacheServerFactory.getRemoteCacheServer();
306                remoteCacheServer.removeAll(cacheName);
307            }
308            catch (final IOException e)
309            {
310                throw new IllegalStateException("Failed to remove all elements from cache region [" + cacheName + "]: " + e, e);
311            }
312        }
313    }
314
315    /**
316     * Removes a particular item from a particular region.
317     * <p>
318     * If this class is running within a remote cache server, removes the item via the <code>RemoteCacheServer</code>
319     * API, so that removes will be broadcast to client machines. Otherwise clears the region directly via the usual
320     * cache API.
321     *
322     * @param cacheName
323     * @param key
324     *
325     * @throws IOException
326     */
327    @Override
328    public void removeItem(final String cacheName, final String key) throws IOException
329    {
330        if (cacheName == null)
331        {
332            throw new IllegalArgumentException("The cache name specified was null.");
333        }
334        if (key == null)
335        {
336            throw new IllegalArgumentException("The key specified was null.");
337        }
338        if (RemoteCacheServerFactory.getRemoteCacheServer() == null)
339        {
340            // Not running in a remote cache server.
341            // Remove objects from the cache directly, as no need to broadcast removes to client machines...
342            cacheHub.getCache(cacheName).remove(key);
343        }
344        else
345        {
346            // Running in a remote cache server.
347            // Remove objects via the RemoteCacheServer API, so that removes will be broadcast to client machines...
348            try
349            {
350                Object keyToRemove = null;
351                final CompositeCache<?, ?> cache = CompositeCacheManager.getInstance().getCache(cacheName);
352
353                // A String key was supplied, but to remove elements via the RemoteCacheServer API, we need the
354                // actual key object as stored in the cache (i.e. a Serializable object). To find the key in this form,
355                // we iterate through all keys stored in the memory cache until we find one whose toString matches
356                // the string supplied...
357                final Set<?> allKeysInCache = cache.getMemoryCache().getKeySet();
358                for (final Object keyInCache : allKeysInCache)
359                {
360                    if (keyInCache.toString().equals(key))
361                    {
362                        if (keyToRemove != null) {
363                            // A key matching the one specified was already found...
364                            throw new IllegalStateException("Unexpectedly found duplicate keys in the cache region matching the key specified.");
365                        }
366                        keyToRemove = keyInCache;
367                    }
368                }
369                if (keyToRemove == null)
370                {
371                    throw new IllegalStateException("No match for this key could be found in the set of keys retrieved from the memory cache.");
372                }
373                // At this point, we have retrieved the matching K key.
374
375                // Call remoteCacheServer.remove(String, Serializable)...
376                final RemoteCacheServer<Serializable, Serializable> remoteCacheServer = RemoteCacheServerFactory.getRemoteCacheServer();
377                remoteCacheServer.remove(cacheName, key);
378            }
379            catch (final Exception e)
380            {
381                throw new IllegalStateException("Failed to remove element with key [" + key + ", " + key.getClass() + "] from cache region [" + cacheName + "]: " + e, e);
382            }
383        }
384    }
385}