/* * Copyright (C) NetStruxr, Inc. All rights reserved. * * This software is published under the terms of the NetStruxr * Public Software License version 0.5, a copy of which has been * included with this distribution in the LICENSE.NPL file. */ package er.extensions.foundation; import java.io.File; import java.io.IOException; import java.util.Enumeration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.appserver.WOApplication; import com.webobjects.foundation.NSMutableDictionary; import com.webobjects.foundation.NSMutableSet; import com.webobjects.foundation.NSNotification; import com.webobjects.foundation.NSNotificationCenter; import com.webobjects.foundation.NSSelector; import er.extensions.appserver.ERXApplication; import er.extensions.eof.ERXConstant; /** * The file notification center is only used in development systems. It provides a nice repository about * files and their last modified dates. So instead of every dynamic spot having to keep track of * the files' dates, register and check at the end of every request-response loop, instead you * can just add an observer to this center and be notified when the file changes. Files' last modification * dates are checked at the end of every request-response loop. * * <p>It should be noted that the current version of the file notification center will retain a * reference to each registered observer. This is not ideal and will be corrected in the * future.</p> */ public class ERXFileNotificationCenter { private static final Logger log = LoggerFactory.getLogger(ERXFileNotificationCenter.class); /** Contains the name of the notification that is posted when a file changes. */ public static final String FileDidChange = "FileDidChange"; /** holds a reference to the default file notification center */ private static ERXFileNotificationCenter _defaultCenter; /** * @return the singleton instance of file notification center */ public static ERXFileNotificationCenter defaultCenter() { if (_defaultCenter == null) _defaultCenter = new ERXFileNotificationCenter(); return _defaultCenter; } /** In seconds. 0 means we will not regularly check files. */ private static int checkFilesPeriod() { return ERXProperties.intForKeyWithDefault("er.extensions.ERXFileNotificationCenter.CheckFilesPeriod", 0); } /** collections of observers by file path */ private NSMutableDictionary _observersByFilePath = new NSMutableDictionary(); /** cache for last modified dates of files by file path */ private NSMutableDictionary _lastModifiedByFilePath = new NSMutableDictionary(); /** flag to tell if caching is enabled, set in the object constructor */ private boolean developmentMode; /** The last time we checked files. We only check if !WOCachingEnabled or if there is a CheckFilesPeriod set */ private long lastCheckMillis = System.currentTimeMillis(); private boolean symlinkSupport; /** * Default constructor. If you are in development mode * then this object will register for the notification * {@link com.webobjects.appserver.WOApplication#ApplicationWillDispatchRequestNotification} * which will enable it to check if files have changed at the end of every request-response * loop. If WOCaching is enabled then this object will not register for anything and will generate * warning messages if observers are registered with caching enabled. */ public ERXFileNotificationCenter() { developmentMode = ERXApplication.isDevelopmentModeSafe(); if (developmentMode || checkFilesPeriod() > 0) { ERXRetainer.retain(this); log.debug("Caching disabled. Registering for notification: {}", WOApplication.ApplicationWillDispatchRequestNotification); NSNotificationCenter.defaultCenter().addObserver(this, new NSSelector("checkIfFilesHaveChanged", ERXConstant.NotificationClassArray), WOApplication.ApplicationWillDispatchRequestNotification, null); } // MS: In case we are touching properties before they're fully materialized or messed up from a failed reload, lets use System.props here symlinkSupport = Boolean.valueOf(System.getProperty("ERXFileNotificationCenter.symlinkSupport", "true")); } /** * When the file notification center is garbage collected it removes itself * as an observer from the * {@link com.webobjects.foundation.NSNotificationCenter NSNotificationCenter}. * Not doing this will cause exceptions. */ @Override public void finalize() throws Throwable { NSNotificationCenter.defaultCenter().removeObserver(this); super.finalize(); } /** * Used to register file observers for a particular file. * @param observer object to be notified when a file changes * @param selector selector to be invoked on the observer when * the file changes. * @param filePath location of the file */ public void addObserver(Object observer, NSSelector selector, String filePath) { if (filePath == null) throw new RuntimeException("Attempting to register observer for null filePath."); addObserver(observer, selector, new File(filePath)); } /** * Used to register file observers for a particular file. * @param observer object to be notified when a file changes * @param selector selector to be invoked on the observer when * the file changes. * @param file file to watch for changes */ public void addObserver(Object observer, NSSelector selector, File file) { if (file == null) throw new RuntimeException("Attempting to register a null file."); if (observer == null) throw new RuntimeException("Attempting to register null observer for file: " + file); if (selector == null) throw new RuntimeException("Attempting to register null selector for file: " + file); if (!developmentMode && checkFilesPeriod() == 0) { log.info("Registering an observer when file checking is disabled (WOCaching must be " + "disabled or the er.extensions.ERXFileNotificationCenter.CheckFilesPeriod " + "property must be set). This observer will not ever by default be called: {}", file); } String filePath = cacheKeyForFile(file); log.debug("Registering Observer for file at path: {}", filePath); // Register last modified date. registerLastModifiedDateForFile(file); // FIXME: This retains the observer. This is not ideal. With the 1.3 JDK we can use a ReferenceQueue to maintain weak references. NSMutableSet observerSet = (NSMutableSet)_observersByFilePath.objectForKey(filePath); if (observerSet == null) { observerSet = new NSMutableSet(); _observersByFilePath.setObjectForKey(observerSet, filePath); } observerSet.addObject(new _ObserverSelectorHolder(observer, selector)); } /** * Returns the path that should be used as the cache key for the given file. This * will return the absolute path of the file (specifically NOT the canonical path) * so that we make sure to lookup files using their original sym links rather than * resolving them at registration time. * * @param file the file to lookup a cache key for * @return the absolute path of the file */ protected String cacheKeyForFile(File file) { return file.getAbsolutePath(); } /** * Returns the value to cache to detect changes to this file. Currently this returns * the lastModified date of the canonicalized version of this file, meaning that we * compare the lastModified of the target of symlinks. * * @param file the file to lookup a cache value for * @return a value representing the current version of this file */ protected Object cacheValueForFile(File file) { if (symlinkSupport) { try { // MS: We want to compute the last modified time on the destination of a (possibly) // symlinked file. On OS X, the lastModified of the sym link itself matches the // lastModified of the referenced file, but I didn't want to presume that behavior. File canonicalizedFile = file.getCanonicalFile(); return canonicalizedFile.getPath() + ":" + Long.valueOf(canonicalizedFile.lastModified()); } catch (IOException e) { // MS: return a zero to match the previous semantics from calling file.lastModified() on a missing file. log.warn("Failed to determine the lastModified time on '{}': {}", file, e.getMessage()); return Long.valueOf(0); } } return Long.valueOf(file.lastModified()); } /** * Records the last modified date of the file for future comparison. * @param file file to record the last modified date */ public void registerLastModifiedDateForFile(File file) { if (file != null) { // Note that if the file doesn't exist, it will be registered with a 0 // lastModified time by virtue of the semantics of File.lastModified. _lastModifiedByFilePath.setObjectForKey(cacheValueForFile(file), cacheKeyForFile(file)); } } /** * Compares the last modified date of the file with the last recorded modification date. * @param file file to compare last modified date. * @return if the file has changed since the last time the <code>lastModified</code> value * was recorded. */ public boolean hasFileChanged(File file) { if (file == null) throw new RuntimeException("Attempting to check if a null file has been changed"); Object previousCacheValue = _lastModifiedByFilePath.objectForKey(cacheKeyForFile(file)); return previousCacheValue == null || !previousCacheValue.equals(cacheValueForFile(file)); } /** * Only used internally. Notifies all of the observers who have been registered for the * given file. * @param file file that has changed */ protected void fileHasChanged(File file) { NSMutableSet observers = (NSMutableSet)_observersByFilePath.objectForKey(cacheKeyForFile(file)); if (observers == null) log.warn("Unable to find observers for file: {}", file); else { NSNotification notification = new NSNotification(FileDidChange, file); for (Enumeration e = observers.objectEnumerator(); e.hasMoreElements();) { _ObserverSelectorHolder holder = (_ObserverSelectorHolder)e.nextElement(); try { holder.selector.invoke(holder.observer, notification); } catch (Exception ex) { log.error("Catching exception when invoking method on observer: {}", ex, ex); } } registerLastModifiedDateForFile(file); } } /** * Notified by the NSNotificationCenter at the end of every request-response * loop. It is here that all of the currently watched files are checked to * see if they have any changes. * @param n NSNotification notification posted from the NSNotificationCenter. */ public void checkIfFilesHaveChanged(NSNotification n) { int checkPeriod = checkFilesPeriod(); if (!developmentMode && (checkPeriod == 0 || System.currentTimeMillis() - lastCheckMillis < 1000 * checkPeriod)) { return; } lastCheckMillis = System.currentTimeMillis(); log.debug("Checking if files have changed"); for (Enumeration e = _lastModifiedByFilePath.keyEnumerator(); e.hasMoreElements();) { File file = new File((String)e.nextElement()); if (file.exists() && hasFileChanged(file)) { fileHasChanged(file); } } } /** * Simple observer-selector holder class. */ public static class _ObserverSelectorHolder { /** Observing object */ // FIXME: Should be a weak reference public Object observer; /** Selector to call on observer */ public NSSelector selector; /** Constructs a holder given an observer and a selector */ public _ObserverSelectorHolder(Object obs, NSSelector sel) { observer = obs; selector = sel; } @Override public int hashCode() { return (observer == null ? 1 : observer.hashCode()) * (selector == null ? 1 : selector.hashCode()); } /** * Overridden to return true if the object being compared has the same observer-selector pair. * @param osh object to be compared * @return result of comparison */ @Override public boolean equals(Object osh) { return osh != null && osh instanceof _ObserverSelectorHolder && ((_ObserverSelectorHolder)osh).selector.equals(selector) && ((_ObserverSelectorHolder)osh).observer.equals(observer); } } }