/** * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding copyright ownership. Apereo * 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 the * following location: * * <p>http://www.apache.org/licenses/LICENSE-2.0 * * <p>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. */ package org.apereo.portal.utils.cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.collect.MapMaker; import java.util.Collections; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import net.sf.ehcache.CacheException; import net.sf.ehcache.Ehcache; import net.sf.ehcache.Element; import net.sf.ehcache.event.CacheEventListenerAdapter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; /** * Tracks entries added to {@link Ehcache} instances that have keys or values which implement {@link * TaggedCacheEntry}. Allows for external removal of elements that match a specified tag * */ @Service("tagTrackingCacheEventListener") public class TagTrackingCacheEventListener extends CacheEventListenerAdapter implements TaggedCacheEntryPurger { protected final Logger logger = LoggerFactory.getLogger(getClass()); // tag type -> set of caches that contain keys tagged with that type // I don't believe that this will leak Ehcache references as this class should have the same lifecycle as the CacheManager private final LoadingCache<String, Set<Ehcache>> taggedCaches = CacheBuilder.newBuilder() .build( new CacheLoader<String, Set<Ehcache>>() { @Override public Set<Ehcache> load(String key) throws Exception { return Collections.newSetFromMap( new ConcurrentHashMap<Ehcache, Boolean>()); } }); // Cache Name -> Key Tag -> Set of Keys private final LoadingCache<String, LoadingCache<CacheEntryTag, Set<Object>>> taggedCacheKeys = CacheBuilder.newBuilder() .build( new CacheLoader<String, LoadingCache<CacheEntryTag, Set<Object>>>() { @Override public LoadingCache<CacheEntryTag, Set<Object>> load(String key) throws Exception { // Key Tag -> Set of Tagged Cache Keys return CacheBuilder.newBuilder() .build( new CacheLoader<CacheEntryTag, Set<Object>>() { @Override public Set<Object> load(CacheEntryTag key) throws Exception { // Set of Tagged Cache Keys return Collections.newSetFromMap( new MapMaker() .weakKeys() .<Object, Boolean> makeMap()); } }); } }); /** Remove all cache entries with keys that have the specified tag */ @Override public int purgeCacheEntries(CacheEntryTag tag) { final String tagType = tag.getTagType(); final Set<Ehcache> caches = taggedCaches.getIfPresent(tagType); //Tag exists in cache(s) if (caches == null || caches.isEmpty()) { return 0; } int purgeCount = 0; //Iterate over each cache to remove the tagged entries for (final Ehcache cache : caches) { final String cacheName = cache.getName(); //See if there are any tagged cache keys for the cache final LoadingCache<CacheEntryTag, Set<Object>> cacheKeys = taggedCacheKeys.getIfPresent(cacheName); if (cacheKeys != null) { //Remove all cache keys from the cache final Set<Object> taggedKeys = cacheKeys.asMap().remove(tag); if (taggedKeys != null) { final int keyCount = taggedKeys.size(); purgeCount += keyCount; logger.debug("Removing {} keys from {} for tag {}", keyCount, cacheName, tag); cache.removeAll(taggedKeys); } } } return purgeCount; } /** Get the tags associated with the element */ protected Set<CacheEntryTag> getTags(Element element) { final Object key = element.getObjectKey(); if (key instanceof TaggedCacheEntry) { return ((TaggedCacheEntry) key).getTags(); } final Object value = element.getObjectValue(); if (value instanceof TaggedCacheEntry) { return ((TaggedCacheEntry) value).getTags(); } return null; } /** If the element has a TaggedCacheKey record the tag associations */ protected void putElement(Ehcache cache, Element element) { final Set<CacheEntryTag> tags = this.getTags(element); //Check if the key is tagged if (tags != null && !tags.isEmpty()) { final String cacheName = cache.getName(); final Object key = element.getObjectKey(); final LoadingCache<CacheEntryTag, Set<Object>> cacheKeys = taggedCacheKeys.getUnchecked(cacheName); logger.debug("Tracking {} tags in cache {} for key {}", tags.size(), cacheName, key); //Add all the tags to the tracking map for (final CacheEntryTag tag : tags) { //Record that this tag type is stored in this cache final String tagType = tag.getTagType(); final Set<Ehcache> caches = taggedCaches.getUnchecked(tagType); caches.add(cache); //Record the tag->key association final Set<Object> taggedKeys = cacheKeys.getUnchecked(tag); taggedKeys.add(key); } } } /** If the element has a TaggedCacheKey remove the tag associations */ protected void removeElement(Ehcache cache, Element element) { final Set<CacheEntryTag> tags = this.getTags(element); //Check if the key is tagged if (tags != null && !tags.isEmpty()) { final String cacheName = cache.getName(); final LoadingCache<CacheEntryTag, Set<Object>> cacheKeys = taggedCacheKeys.getIfPresent(cacheName); //If there are tracked tagged keys remove matching tags if (cacheKeys != null) { final Object key = element.getObjectKey(); logger.debug( "Tracking removing key cache {} with tag {} : {}", cacheName, tags, key); for (final CacheEntryTag tag : tags) { final Set<Object> taggedKeys = cacheKeys.getIfPresent(tag); //Remove the tagged key if (taggedKeys != null) { taggedKeys.remove(key); } } } } } @Override public void notifyElementPut(Ehcache cache, Element element) throws CacheException { putElement(cache, element); } @Override public void notifyElementRemoved(Ehcache cache, Element element) throws CacheException { removeElement(cache, element); } @Override public void notifyElementExpired(Ehcache cache, Element element) { removeElement(cache, element); } @Override public void notifyElementEvicted(Ehcache cache, Element element) { removeElement(cache, element); } @Override public void notifyRemoveAll(Ehcache cache) { final String cacheName = cache.getName(); final LoadingCache<CacheEntryTag, Set<Object>> cacheKeys = taggedCacheKeys.getIfPresent(cacheName); if (cacheKeys != null) { logger.debug("Tracking remove all tagged keys for cache {}", new Object[] {cacheName}); cacheKeys.invalidateAll(); } } }