View Javadoc
1   package org.apache.commons.jcs3.admin;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *   http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import java.io.IOException;
23  import java.io.ObjectOutputStream;
24  import java.io.Serializable;
25  import java.text.DateFormat;
26  import java.util.Date;
27  import java.util.LinkedList;
28  import java.util.List;
29  import java.util.Map;
30  import java.util.Set;
31  import java.util.TreeMap;
32  import java.util.TreeSet;
33  import java.util.stream.Collectors;
34  
35  import org.apache.commons.jcs3.access.exception.CacheException;
36  import org.apache.commons.jcs3.auxiliary.remote.server.RemoteCacheServer;
37  import org.apache.commons.jcs3.auxiliary.remote.server.RemoteCacheServerFactory;
38  import org.apache.commons.jcs3.engine.CacheElementSerialized;
39  import org.apache.commons.jcs3.engine.behavior.ICacheElement;
40  import org.apache.commons.jcs3.engine.behavior.IElementAttributes;
41  import org.apache.commons.jcs3.engine.control.CompositeCache;
42  import org.apache.commons.jcs3.engine.control.CompositeCacheManager;
43  import org.apache.commons.jcs3.engine.memory.behavior.IMemoryCache;
44  
45  /**
46   * A servlet which provides HTTP access to JCS. Allows a summary of regions to be viewed, and
47   * removeAll to be run on individual regions or all regions. Also provides the ability to remove
48   * items (any number of key arguments can be provided with action 'remove'). Should be initialized
49   * with a properties file that provides at least a classpath resource loader.
50   */
51  public class JCSAdminBean implements JCSJMXBean
52  {
53      /** The cache manager. */
54      private final CompositeCacheManager cacheHub;
55  
56      /**
57       * Default constructor
58       */
59      public JCSAdminBean()
60      {
61          try
62          {
63              this.cacheHub = CompositeCacheManager.getInstance();
64          }
65          catch (final CacheException e)
66          {
67              throw new RuntimeException("Could not retrieve cache manager instance", e);
68          }
69      }
70  
71      /**
72       * Parameterized constructor
73       *
74  	 * @param cacheHub the cache manager instance
75  	 */
76  	public JCSAdminBean(final CompositeCacheManager cacheHub)
77  	{
78  		this.cacheHub = cacheHub;
79  	}
80  
81  	/**
82       * Builds up info about each element in a region.
83       * <p>
84       * @param cacheName
85       * @return List of CacheElementInfo objects
86       * @throws IOException
87       */
88      @Override
89      public List<CacheElementInfo> buildElementInfo( final String cacheName )
90          throws IOException
91      {
92          final CompositeCache<Object, Object> cache = cacheHub.getCache( cacheName );
93  
94          // Convert all keys to string, store in a sorted map
95          final TreeMap<String, ?> keys = new TreeMap<>(cache.getMemoryCache().getKeySet()
96                  .stream()
97                  .collect(Collectors.toMap(Object::toString, k -> k)));
98  
99          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 }