| package org.apache.commons.jcs3.admin; |
| |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import java.io.IOException; |
| import java.io.ObjectOutputStream; |
| import java.io.Serializable; |
| import java.text.DateFormat; |
| import java.util.Date; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import java.util.TreeMap; |
| import java.util.TreeSet; |
| import java.util.stream.Collectors; |
| |
| import org.apache.commons.jcs3.access.exception.CacheException; |
| import org.apache.commons.jcs3.auxiliary.remote.server.RemoteCacheServer; |
| import org.apache.commons.jcs3.auxiliary.remote.server.RemoteCacheServerFactory; |
| import org.apache.commons.jcs3.engine.CacheElementSerialized; |
| import org.apache.commons.jcs3.engine.behavior.ICacheElement; |
| import org.apache.commons.jcs3.engine.behavior.IElementAttributes; |
| import org.apache.commons.jcs3.engine.control.CompositeCache; |
| import org.apache.commons.jcs3.engine.control.CompositeCacheManager; |
| import org.apache.commons.jcs3.engine.memory.behavior.IMemoryCache; |
| |
| /** |
| * A servlet which provides HTTP access to JCS. Allows a summary of regions to be viewed, and |
| * removeAll to be run on individual regions or all regions. Also provides the ability to remove |
| * items (any number of key arguments can be provided with action 'remove'). Should be initialized |
| * with a properties file that provides at least a classpath resource loader. |
| */ |
| public class JCSAdminBean implements JCSJMXBean |
| { |
| /** The cache manager. */ |
| private final CompositeCacheManager cacheHub; |
| |
| /** |
| * Default constructor |
| */ |
| public JCSAdminBean() |
| { |
| try |
| { |
| this.cacheHub = CompositeCacheManager.getInstance(); |
| } |
| catch (final CacheException e) |
| { |
| throw new RuntimeException("Could not retrieve cache manager instance", e); |
| } |
| } |
| |
| /** |
| * Parameterized constructor |
| * |
| * @param cacheHub the cache manager instance |
| */ |
| public JCSAdminBean(final CompositeCacheManager cacheHub) |
| { |
| this.cacheHub = cacheHub; |
| } |
| |
| /** |
| * Builds up info about each element in a region. |
| * <p> |
| * @param cacheName |
| * @return List of CacheElementInfo objects |
| * @throws IOException |
| */ |
| @Override |
| public List<CacheElementInfo> buildElementInfo( final String cacheName ) |
| throws IOException |
| { |
| final CompositeCache<Object, Object> cache = cacheHub.getCache( cacheName ); |
| |
| // Convert all keys to string, store in a sorted map |
| final TreeMap<String, ?> keys = new TreeMap<>(cache.getMemoryCache().getKeySet() |
| .stream() |
| .collect(Collectors.toMap(Object::toString, k -> k))); |
| |
| final LinkedList<CacheElementInfo> records = new LinkedList<>(); |
| |
| final DateFormat format = DateFormat.getDateTimeInstance( DateFormat.SHORT, DateFormat.SHORT ); |
| |
| final long now = System.currentTimeMillis(); |
| |
| for (final Map.Entry<String, ?> key : keys.entrySet()) |
| { |
| final ICacheElement<?, ?> element = cache.getMemoryCache().getQuiet( key.getValue() ); |
| |
| final IElementAttributes attributes = element.getElementAttributes(); |
| |
| final CacheElementInfo elementInfo = new CacheElementInfo( |
| key.getKey(), |
| attributes.getIsEternal(), |
| format.format(new Date(attributes.getCreateTime())), |
| attributes.getMaxLife(), |
| (now - attributes.getCreateTime() - attributes.getMaxLife() * 1000 ) / -1000); |
| |
| records.add( elementInfo ); |
| } |
| |
| return records; |
| } |
| |
| /** |
| * Builds up data on every region. |
| * <p> |
| * TODO we need a most light weight method that does not count bytes. The byte counting can |
| * really swamp a server. |
| * @return List of CacheRegionInfo objects |
| */ |
| @Override |
| public List<CacheRegionInfo> buildCacheInfo() |
| { |
| final TreeSet<String> cacheNames = new TreeSet<>(cacheHub.getCacheNames()); |
| |
| final LinkedList<CacheRegionInfo> cacheInfo = new LinkedList<>(); |
| |
| for (final String cacheName : cacheNames) |
| { |
| final CompositeCache<?, ?> cache = cacheHub.getCache( cacheName ); |
| |
| final CacheRegionInfo regionInfo = new CacheRegionInfo( |
| cache.getCacheName(), |
| cache.getSize(), |
| cache.getStatus().toString(), |
| cache.getStats(), |
| cache.getHitCountRam(), |
| cache.getHitCountAux(), |
| cache.getMissCountNotFound(), |
| cache.getMissCountExpired(), |
| getByteCount( cache )); |
| |
| cacheInfo.add( regionInfo ); |
| } |
| |
| return cacheInfo; |
| } |
| |
| |
| /** |
| * Tries to estimate how much data is in a region. This is expensive. If there are any non serializable objects in |
| * the region or an error occurs, suppresses exceptions and returns 0. |
| * <p> |
| * |
| * @return int The size of the region in bytes. |
| */ |
| @Override |
| public long getByteCount(final String cacheName) |
| { |
| return getByteCount(cacheHub.getCache(cacheName)); |
| } |
| |
| /** |
| * Tries to estimate how much data is in a region. This is expensive. If there are any non serializable objects in |
| * the region or an error occurs, suppresses exceptions and returns 0. |
| * <p> |
| * |
| * @return int The size of the region in bytes. |
| */ |
| public <K, V> long getByteCount(final CompositeCache<K, V> cache) |
| { |
| if (cache == null) |
| { |
| throw new IllegalArgumentException("The cache object specified was null."); |
| } |
| |
| long size = 0; |
| final IMemoryCache<K, V> memCache = cache.getMemoryCache(); |
| |
| for (final K key : memCache.getKeySet()) |
| { |
| ICacheElement<K, V> ice = null; |
| try |
| { |
| ice = memCache.get(key); |
| } |
| catch (final IOException e) |
| { |
| throw new RuntimeException("IOException while trying to get a cached element", e); |
| } |
| |
| if (ice == null) |
| { |
| continue; |
| } |
| |
| if (ice instanceof CacheElementSerialized) |
| { |
| size += ((CacheElementSerialized<K, V>) ice).getSerializedValue().length; |
| } |
| else |
| { |
| final Object element = ice.getVal(); |
| |
| //CountingOnlyOutputStream: Keeps track of the number of bytes written to it, but doesn't write them anywhere. |
| final CountingOnlyOutputStream counter = new CountingOnlyOutputStream(); |
| try (ObjectOutputStream out = new ObjectOutputStream(counter);) |
| { |
| out.writeObject(element); |
| } |
| catch (final IOException e) |
| { |
| throw new RuntimeException("IOException while trying to measure the size of the cached element", e); |
| } |
| finally |
| { |
| try |
| { |
| counter.close(); |
| } |
| catch (final IOException e) |
| { |
| // ignore |
| } |
| } |
| |
| // 4 bytes lost for the serialization header |
| size += counter.getCount() - 4; |
| } |
| } |
| |
| return size; |
| } |
| |
| /** |
| * Clears all regions in the cache. |
| * <p> |
| * If this class is running within a remote cache server, clears all regions via the <code>RemoteCacheServer</code> |
| * API, so that removes will be broadcast to client machines. Otherwise clears all regions in the cache directly via |
| * the usual cache API. |
| */ |
| @Override |
| public void clearAllRegions() throws IOException |
| { |
| final RemoteCacheServer<?, ?> remoteCacheServer = RemoteCacheServerFactory.getRemoteCacheServer(); |
| |
| if (remoteCacheServer == null) |
| { |
| // Not running in a remote cache server. |
| // Remove objects from the cache directly, as no need to broadcast removes to client machines... |
| for (final String name : cacheHub.getCacheNames()) |
| { |
| cacheHub.getCache(name).removeAll(); |
| } |
| } |
| else |
| { |
| // Running in a remote cache server. |
| // Remove objects via the RemoteCacheServer API, so that removes will be broadcast to client machines... |
| // Call remoteCacheServer.removeAll(String) for each cacheName... |
| for (final String name : cacheHub.getCacheNames()) |
| { |
| remoteCacheServer.removeAll(name); |
| } |
| } |
| } |
| |
| /** |
| * Clears a particular cache region. |
| * <p> |
| * If this class is running within a remote cache server, clears the region via the <code>RemoteCacheServer</code> |
| * API, so that removes will be broadcast to client machines. Otherwise clears the region directly via the usual |
| * cache API. |
| */ |
| @Override |
| public void clearRegion(final String cacheName) throws IOException |
| { |
| if (cacheName == null) |
| { |
| throw new IllegalArgumentException("The cache name specified was null."); |
| } |
| if (RemoteCacheServerFactory.getRemoteCacheServer() == null) |
| { |
| // Not running in a remote cache server. |
| // Remove objects from the cache directly, as no need to broadcast removes to client machines... |
| cacheHub.getCache(cacheName).removeAll(); |
| } |
| else |
| { |
| // Running in a remote cache server. |
| // Remove objects via the RemoteCacheServer API, so that removes will be broadcast to client machines... |
| try |
| { |
| // Call remoteCacheServer.removeAll(String)... |
| final RemoteCacheServer<?, ?> remoteCacheServer = RemoteCacheServerFactory.getRemoteCacheServer(); |
| remoteCacheServer.removeAll(cacheName); |
| } |
| catch (final IOException e) |
| { |
| throw new IllegalStateException("Failed to remove all elements from cache region [" + cacheName + "]: " + e, e); |
| } |
| } |
| } |
| |
| /** |
| * Removes a particular item from a particular region. |
| * <p> |
| * If this class is running within a remote cache server, removes the item via the <code>RemoteCacheServer</code> |
| * API, so that removes will be broadcast to client machines. Otherwise clears the region directly via the usual |
| * cache API. |
| * |
| * @param cacheName |
| * @param key |
| * |
| * @throws IOException |
| */ |
| @Override |
| public void removeItem(final String cacheName, final String key) throws IOException |
| { |
| if (cacheName == null) |
| { |
| throw new IllegalArgumentException("The cache name specified was null."); |
| } |
| if (key == null) |
| { |
| throw new IllegalArgumentException("The key specified was null."); |
| } |
| if (RemoteCacheServerFactory.getRemoteCacheServer() == null) |
| { |
| // Not running in a remote cache server. |
| // Remove objects from the cache directly, as no need to broadcast removes to client machines... |
| cacheHub.getCache(cacheName).remove(key); |
| } |
| else |
| { |
| // Running in a remote cache server. |
| // Remove objects via the RemoteCacheServer API, so that removes will be broadcast to client machines... |
| try |
| { |
| Object keyToRemove = null; |
| final CompositeCache<?, ?> cache = CompositeCacheManager.getInstance().getCache(cacheName); |
| |
| // A String key was supplied, but to remove elements via the RemoteCacheServer API, we need the |
| // actual key object as stored in the cache (i.e. a Serializable object). To find the key in this form, |
| // we iterate through all keys stored in the memory cache until we find one whose toString matches |
| // the string supplied... |
| final Set<?> allKeysInCache = cache.getMemoryCache().getKeySet(); |
| for (final Object keyInCache : allKeysInCache) |
| { |
| if (keyInCache.toString().equals(key)) |
| { |
| if (keyToRemove == null) |
| { |
| keyToRemove = keyInCache; |
| } |
| else |
| { |
| // A key matching the one specified was already found... |
| throw new IllegalStateException("Unexpectedly found duplicate keys in the cache region matching the key specified."); |
| } |
| } |
| } |
| if (keyToRemove == null) |
| { |
| throw new IllegalStateException("No match for this key could be found in the set of keys retrieved from the memory cache."); |
| } |
| // At this point, we have retrieved the matching K key. |
| |
| // Call remoteCacheServer.remove(String, Serializable)... |
| final RemoteCacheServer<Serializable, Serializable> remoteCacheServer = RemoteCacheServerFactory.getRemoteCacheServer(); |
| remoteCacheServer.remove(cacheName, key); |
| } |
| catch (final Exception e) |
| { |
| throw new IllegalStateException("Failed to remove element with key [" + key + ", " + key.getClass() + "] from cache region [" + cacheName + "]: " + e, e); |
| } |
| } |
| } |
| } |