/********************************************************************************** * $URL: https://source.sakaiproject.org/svn/kernel/trunk/kernel-impl/src/main/java/org/sakaiproject/event/impl/NotificationCache.java $ * $Id: NotificationCache.java 122028 2013-04-01 19:49:35Z azeckoski@unicon.net $ *********************************************************************************** * * Copyright (c) 2003, 2004, 2005, 2006, 2007, 2008 Sakai Foundation * * Licensed under the Educational Community 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.opensource.org/licenses/ECL-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. * **********************************************************************************/ package org.sakaiproject.event.impl; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Observable; import java.util.Observer; import java.util.Vector; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.event.api.Event; import org.sakaiproject.event.api.EventTrackingService; import org.sakaiproject.event.api.Notification; import org.sakaiproject.memory.api.Cache; import org.sakaiproject.memory.api.CacheRefresher; import org.sakaiproject.memory.api.Cacher; import org.sakaiproject.memory.api.MemoryService; /** * <p> * A Cache of objects with keys with a limited lifespan. * </p> * <p> * When the object expires, the cache calls upon a CacheRefresher to update the key's value. The update is done in a separate thread. * </p> */ public class NotificationCache implements Cacher, Observer { /** Our logger. */ private static Log M_log = LogFactory.getLog(NotificationCache.class); /** Map holding cached entries (by reference). */ // protected Map m_map = null; private Cache cache = null; /** Map of notification function to Set of notifications - same objects as in m_map. */ protected Map m_functionMap = null; /** The object that will deal with expired entries. */ protected CacheRefresher m_refresher = null; /** The string that all resources in this cache will start with. */ protected String m_resourcePattern = null; /** If true, we are disabled. */ protected boolean m_disabled = false; /** If true, we have all the entries that there are in the cache. */ protected boolean m_complete = false; /** If true, we are going to hold any events we see in the m_heldEvents list for later processing. */ protected boolean m_holdEventProcessing = false; private Object m_holdEventProcessingLock = new Object(); /** The events we are holding for later processing. */ protected List m_heldEvents = new Vector(); protected EventTrackingService eventTrackingService; protected MemoryService memoryService; /** * Construct the Cache. Attempts to keep complete on Event notification by calling the refresher. * * @param refresher * The object that will handle refreshing of event notified modified or added entries. * @param pattern * The "startsWith()" string for all resources that may be in this cache. */ public NotificationCache(CacheRefresher refresher, String pattern, EventTrackingService eventTrackingService, MemoryService memoryService) { this.memoryService = memoryService; cache = memoryService.newCache("org.sakaiproject.event.api.NotificationService.cache", refresher, pattern); // TODO check logic with proxied class m_functionMap = new HashMap(); m_refresher = refresher; m_resourcePattern = pattern; // register to get events - first, before others this.eventTrackingService = eventTrackingService; eventTrackingService.addPriorityObserver(this); } // NotificationCache /** * Clear all entries. */ public synchronized void clear() { cache.clear(); m_functionMap.clear(); m_complete = false; } // clear /** * Test for an entry in the cache. * * @param key * The cache key. * @return true if the key maps to an entry, false if not. */ public boolean containsKey(Object key) { return cache.containsKey(key); } // containsKey /** * Disable the cache. */ public void disable() { m_disabled = true; eventTrackingService.deleteObserver(this); clear(); } // disable /** * Is the cache disabled? * * @return true if the cache is disabled, false if it is enabled. */ public boolean disabled() { return m_disabled; } // disabled /** * Enable the cache. */ public void enable() { m_disabled = false; eventTrackingService.addPriorityObserver(this); } // enable /** * Clean up. */ protected void finalize() { // unregister as a cacher memoryService.unregisterCacher(this); // unregister to get events eventTrackingService.deleteObserver(this); } // finalize /** * Get the entry associated with the key, or null if not there * * @param key * The cache key. * @return The entry associated with the key, or null if the a null is cached, or the key is not found (Note: use containsKey() to remove this ambiguity). */ public Notification get(Object key) { if (disabled()) return null; return (Notification) cache.get(key); } // get /** * Get all the non-null entries. * * @return all the non-null entries, or an empty list if none. */ public List getAll() { List rv = new Vector(); if (disabled()) return rv; List allObjectsInCache = cache.getAll(); for (Object object : allObjectsInCache) { if(object != null && object instanceof Notification) rv.add(object); } return rv; } // getAll /** * Get all the Notification entries that are watching this Event function. * * @param function * The function to use to select Notifications. * @return all the Notification entries that are watching this Event function. */ public List getAll(String function) { return (List) m_functionMap.get(function); } // getAll /** * Return a description of the cacher. * * @return The cacher's description. */ public String getDescription() { StringBuilder buf = new StringBuilder(); buf.append("NotificationCache:"); if (m_disabled) { buf.append(" disabled:"); } if (m_complete) { buf.append(" complete:"); } if (m_resourcePattern != null) { buf.append(" pattern: " + m_resourcePattern); } if (m_refresher != null) { buf.append(" refresher: " + m_refresher.toString()); } return buf.toString(); } /** * Get all the keys, each modified to remove the resourcePattern prefix. Note: only works with String keys. * * @return The List of keys converted from references to ids (String). */ public List getIds() { //FIXME Need to create a separate cache for Notifications... return Collections.EMPTY_LIST; // List rv = new Vector(); // // List keys = new Vector(); // keys.addAll(m_map.keySet()); // // Iterator it = keys.iterator(); // while (it.hasNext()) // { // String key = (String) it.next(); // int i = key.indexOf(m_resourcePattern); // if (i != -1) key = key.substring(i + m_resourcePattern.length()); // rv.add(key); // } // // return rv; } // getKeys /** * Get all the keys * * @return The List of key values (Object). */ public List getKeys() { return cache.getKeys(); } // getKeys /** * Return the size of the cacher - indicating how much memory in use. * * @return The size of the cacher. */ public long getSize() { return cache.getSize(); } /** * Are we complete? * * @return true if we have all the possible entries cached, false if not. */ public boolean isComplete() { if (disabled()) return false; return m_complete; } // isComplete /** * Set the cache to hold events for later processing to assure an atomic "complete" load. */ public synchronized void holdEvents() { m_holdEventProcessing = true; } // holdEvents /** * Restore normal event processing in the cache, and process any held events now. */ public synchronized void processEvents() { m_holdEventProcessing = false; synchronized (m_heldEvents) { for (int i = 0; i < m_heldEvents.size(); i++) { Event event = (Event) m_heldEvents.get(i); continueUpdate(event); } m_heldEvents.clear(); } } // holdEvents /** * Cache an object. * * @param key * The key with which to find the object. * @param payload * The object to cache. * @param duration * The time to cache the object (seconds). */ public synchronized void put(Notification payload) { if (disabled() || payload == null) return; cache.put(payload.getReference(), payload); // put in m_functionMap, too, for each function of the notification List funcs = payload.getFunctions(); for (Iterator iFuncs = funcs.iterator(); iFuncs.hasNext();) { String func = (String) iFuncs.next(); List notifications = (List) m_functionMap.get(func); if (notifications == null) { notifications = new Vector(); m_functionMap.put(func, notifications); } if (!notifications.contains(payload)) notifications.add(payload); } } // cache /** * Remove this entry from the cache. * * @param key * The cache key. */ public synchronized void remove(Object key) { if (disabled()) return; Notification payload = (Notification) cache.get(key); cache.remove(key); if (payload == null) return; // remove it from the function map for each function List funcs = payload.getFunctions(); for (Iterator iFuncs = funcs.iterator(); iFuncs.hasNext();) { String func = (String) iFuncs.next(); List notifications = (List) m_functionMap.get(func); if (notifications != null) { notifications.remove(payload); if (notifications.isEmpty()) { m_functionMap.remove(func); } } } } // remove /** * Clear out as much as possible anything cached; re-sync any cache that is needed to be kept. */ public void resetCache() { clear(); } // resetCache /** * Set the cache to be complete, containing all possible entries. */ public void setComplete() { if (disabled()) return; m_complete = true; } // isComplete /** * This method is called whenever the observed object is changed. An application calls an <tt>Observable</tt> object's <code>notifyObservers</code> method to have all the object's observers notified of the change. default implementation is to * cause the courier service to deliver to the interface controlled by my controller. Extensions can override. * * @param o * the observable object. * @param arg * an argument passed to the <code>notifyObservers</code> method. */ public void update(Observable o, Object arg) { if (disabled()) return; // arg is Event if (!(arg instanceof Event)) return; Event event = (Event) arg; // if this is just a read, not a modify event, we can ignore it if (!event.getModify()) return; String key = event.getResource(); // if this resource is not in my pattern of resources, we can ignore it if (key != null && !key.startsWith(m_resourcePattern)) return; // if we are holding event processing synchronized (m_holdEventProcessingLock) { if (m_holdEventProcessing) { synchronized (m_heldEvents) { m_heldEvents.add(event); } return; } } continueUpdate(event); } // update /** * Complete the update, given an event that we know we need to act upon. * * @param event * The event to process. */ private void continueUpdate(Event event) { String key = event.getResource(); if (M_log.isDebugEnabled()) M_log.debug("update() [" + m_resourcePattern + "] resource: " + key + " event: " + event.getEvent()); // do we have this in our cache? Object oldValue = get(key); if (cache.containsKey(key)) { // invalidate our copy remove(key); } // if we are being complete, we need to get this cached. if (m_complete) { // we can only get it cached if we have a refresher if (m_refresher != null) { // ask the refresher for the value Notification value = (Notification) m_refresher.refresh(key, oldValue, event); if (value != null) { put(value); } } else { // we can no longer claim to be complete m_complete = false; } } } // continueUpdate }