/* * Copyright (c) 2006, 2011, Oracle and/or its affiliates. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of Oracle nor the names of its * contributors may be used to endorse or promote products derived * from this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ /* * This source code is provided to illustrate the usage of a given feature * or technique and has been deliberately simplified. Additional steps * required for a production-quality application, such as security checks, * input validation and proper error handling, might not be present in * this sample code. */ package com.sun.jmx.examples.scandir; import static com.sun.jmx.examples.scandir.ScanManager.getNextSeqNumber; import com.sun.jmx.examples.scandir.ScanManagerMXBean.ScanState; import static com.sun.jmx.examples.scandir.ScanManagerMXBean.ScanState.*; import static com.sun.jmx.examples.scandir.config.DirectoryScannerConfig.Action.*; import com.sun.jmx.examples.scandir.config.XmlConfigUtils; import com.sun.jmx.examples.scandir.config.DirectoryScannerConfig; import com.sun.jmx.examples.scandir.config.DirectoryScannerConfig.Action; import com.sun.jmx.examples.scandir.config.ResultRecord; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.LinkedList; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.management.AttributeChangeNotification; import javax.management.InstanceNotFoundException; import javax.management.ListenerNotFoundException; import javax.management.MBeanNotificationInfo; import javax.management.Notification; import javax.management.NotificationBroadcasterSupport; import javax.management.NotificationEmitter; import javax.management.NotificationFilter; import javax.management.NotificationListener; /** * A <code>DirectoryScanner</code> is an MBean that * scans a file system starting at a given root directory, * and then looks for files that match a given criteria. * <p> * When such a file is found, the <code>DirectoryScanner</code> takes * the action for which it was configured: emit a notification, * <i>and or</i> log a {@link * com.sun.jmx.examples.scandir.config.ResultRecord} for this file, * <i>and or</i> delete that file. * </p> * <p> * The code that would actually delete the file is commented out - so that * nothing valuable is lost if this example is run by mistake on the wrong * set of directories.<br> * Logged results are logged by sending them to the {@link ResultLogManager}. * </p> * <p> * <code>DirectoryScannerMXBeans</code> are created, initialized, and * registered by the {@link ScanManagerMXBean}. * The {@link ScanManagerMXBean} will also schedule and run them in * background by calling their {@link #scan} method. * </p> * <p>Client code is not expected to create or register directly any such * MBean. Instead, clients are expected to modify the configuration, using * the {@link ScanDirConfigMXBean}, and then apply it, using the {@link * ScanManagerMXBean}. Instances of <code>DirectoryScannerMXBeans</code> * will then be created and registered (or unregistered and garbage collected) * as a side effect of applying that configuration. * </p> * * @author Sun Microsystems, 2006 - All rights reserved. */ public class DirectoryScanner implements DirectoryScannerMXBean, NotificationEmitter { /** * The type for <i>com.sun.jmx.examples.scandir.filematch</i> notifications. * Notifications of this type will be emitted whenever a file that * matches this {@code DirectoryScanner} criteria is found, but only if * this {@code DirectoryScanner} was configured to {@link * Action#NOTIFY notify} for matching files. **/ public static final String FILE_MATCHES_NOTIFICATION = "com.sun.jmx.examples.scandir.filematch"; /** * A logger for this class. **/ private static final Logger LOG = Logger.getLogger(DirectoryScanner.class.getName()); // Attribute : State // private volatile ScanState state = STOPPED; // The DirectoryScanner delegates the implementation of // the NotificationEmitter interface to a wrapped instance // of NotificationBroadcasterSupport. // private final NotificationBroadcasterSupport broadcaster; // The root directory at which this DirectoryScanner will start // scanning. Constructed from config.getRootDirectory(). // private final File rootFile; // This DirectoryScanner config - this is a constant which is // provided at construction time by the {@link ScanManager}. // private final DirectoryScannerConfig config; // The set of actions for which this DirectoryScanner is configured. // Constructed from config.getActions() // final Set<Action> actions; // The ResultLogManager that this DirectoryScanner will use to log // info. This is a hard reference to another MBean, provided // at construction time by the ScanManager. // The ScanManager makes sure that the life cycle of these two MBeans // is consistent. // final ResultLogManager logManager; /** * Constructs a new {@code DirectoryScanner}. * <p>This constructor is * package protected, and this MBean cannot be created by a remote * client, because it needs a reference to the {@link ResultLogManager}, * which cannot be provided from remote. * </p> * <p>This is a conscious design choice: {@code DirectoryScanner} MBeans * are expected to be completely managed (created, registered, unregistered) * by the {@link ScanManager} which does provide this reference. * </p> * * @param config This {@code DirectoryScanner} configuration. * @param logManager The info log manager with which to log the info * records. * @throws IllegalArgumentException if one of the parameter is null, or if * the provided {@code config} doesn't have its {@code name} set, * or if the {@link DirectoryScannerConfig#getRootDirectory * root directory} provided in the {@code config} is not acceptable * (not provided or not found or not readable, etc...). **/ public DirectoryScanner(DirectoryScannerConfig config, ResultLogManager logManager) throws IllegalArgumentException { if (logManager == null) throw new IllegalArgumentException("log=null"); if (config == null) throw new IllegalArgumentException("config=null"); if (config.getName() == null) throw new IllegalArgumentException("config.name=null"); broadcaster = new NotificationBroadcasterSupport(); // Clone the config: ensure data encapsulation. // this.config = XmlConfigUtils.xmlClone(config); // Checks that the provided root directory is valid. // Throws IllegalArgumentException if it isn't. // rootFile = validateRoot(config.getRootDirectory()); // Initialize the Set<Action> for which this DirectoryScanner // is configured. // if (config.getActions() == null) actions = Collections.emptySet(); else actions = EnumSet.copyOf(Arrays.asList(config.getActions())); this.logManager = logManager; } // see DirectoryScannerMXBean public void stop() { // switch state to stop and send AttributeValueChangeNotification setStateAndNotify(STOPPED); } // see DirectoryScannerMXBean public String getRootDirectory() { return rootFile.getAbsolutePath(); } // see DirectoryScannerMXBean public ScanState getState() { return state; } // see DirectoryScannerMXBean public DirectoryScannerConfig getConfiguration() { return config; } // see DirectoryScannerMXBean public String getCurrentScanInfo() { final ScanTask currentOrLastTask = currentTask; if (currentOrLastTask == null) return "Never Run"; return currentOrLastTask.getScanInfo(); } // This variable points to the current (or latest) scan. // private volatile ScanTask currentTask = null; // see DirectoryScannerMXBean public void scan() { final ScanTask task; synchronized (this) { final LinkedList<File> list; switch (state) { case RUNNING: case SCHEDULED: throw new IllegalStateException(state.toString()); case STOPPED: case COMPLETED: // only accept to scan if state is STOPPED or COMPLETED. list = new LinkedList<File>(); list.add(rootFile); break; default: throw new IllegalStateException(String.valueOf(state)); } // Create a new ScanTask object for our root directory file. // currentTask = task = new ScanTask(list,this); // transient state... will be switched to RUNNING when // task.execute() is called. This code could in fact be modified // to use java.util.concurent.Future and, to push the task to // an executor. We would then need to wait for the task to // complete before returning. However, this wouldn't buy us // anything - since this method should wait for the task to // finish anyway: so why would we do it? // As it stands, we simply call task.execute() in the current // thread - brave and fearless readers may want to attempt the // modification ;-) // setStateAndNotify(SCHEDULED); } task.execute(); } // This method is invoked to carry out the configured actions on a // matching file. // Do not call this method from within synchronized() { } as this // method may send notifications! // void actOn(File file) { // Which action were actually taken // final Set<Action> taken = new HashSet<Action>(); boolean logresult = false; // Check out which actions are configured and carry them out. // for (Action action : actions) { switch (action) { case DELETE: if (deleteFile(file)) { // Delete succeeded: add DELETE to the set of // actions carried out. taken.add(DELETE); } break; case NOTIFY: if (notifyMatch(file)) { // Notify succeeded: add NOTIFY to the set of // actions carried out. taken.add(NOTIFY); } break; case LOGRESULT: // LOGRESULT was configured - log actions carried out. // => we must execute this action as the last action. // simply set logresult=true for now. We will do // the logging later logresult = true; break; default: LOG.fine("Failed to execute action: " +action + " - action not supported"); break; } } // Now is time for logging: if (logresult) { taken.add(LOGRESULT); if (!logResult(file,taken.toArray(new Action[taken.size()]))) taken.remove(LOGRESULT); // just for the last trace below... } LOG.finest("File processed: "+taken+" - "+file.getAbsolutePath()); } // Deletes a matching file. private boolean deleteFile(File file) { try { // file.delete() is commented so that we don't do anything // bad if the example is mistakenly run on the wrong set of // directories. // /* file.delete(); */ System.out.println("DELETE not implemented for safety reasons."); return true; } catch (Exception x) { LOG.fine("Failed to delete: "+file.getAbsolutePath()); } return false; } // Notifies of a matching file. private boolean notifyMatch(File file) { try { final Notification n = new Notification(FILE_MATCHES_NOTIFICATION,this, getNextSeqNumber(), file.getAbsolutePath()); // This method *is not* called from any synchronized block, so // we can happily call broadcaster.sendNotification() here. // Note that verifying whether a method is called from within // a synchronized block demends a thoroughful code reading, // examining each of the 'parent' methods in turn. // broadcaster.sendNotification(n); return true; } catch (Exception x) { LOG.fine("Failed to notify: "+file.getAbsolutePath()); } return false; } // Logs a result with the ResultLogManager private boolean logResult(File file,Action[] actions) { try { logManager.log(new ResultRecord(config, actions,file)); return true; } catch (Exception x) { LOG.fine("Failed to log: "+file.getAbsolutePath()); } return false; } // Contextual object used to store info about current // (or last) scan. // private static class ScanTask { // List of Files that remain to scan. // When files are discovered they are added to the list. // When they are being handled, they are removed from the list. // When the list is empty, the scanning is finished. // private final LinkedList<File> list; private final DirectoryScanner scan; // Some statistics... // private volatile long scanned=0; private volatile long matching=0; private volatile String info="Not started"; ScanTask(LinkedList<File> list, DirectoryScanner scan) { this.list = list; this.scan = scan; } public void execute() { scan(list); } private void scan(LinkedList<File> list) { scan.scan(this,list); } public String getScanInfo() { return info+" - ["+scanned+" scanned, "+matching+" matching]"; } } // The actual scan logic. Switches state to RUNNING, // and scan the list of given dirs. // The list is a live object which is updated by this method. // This would allow us to implement methods like "pause" and "resume", // since all the info needed to resume would be in the list. // private void scan(ScanTask task, LinkedList<File> list) { setStateAndNotify(RUNNING); task.info = "In Progress"; try { // The FileFilter will tell us which files match and which don't. // final FileFilter filter = config.buildFileFilter(); // We have two condition to end the loop: either the list is // empty, meaning there's nothing more to scan, or the state of // the DirectoryScanner was asynchronously switched to STOPPED by // another thread, e.g. because someone called "stop" on the // ScanManagerMXBean // while (!list.isEmpty() && state == RUNNING) { // Get and remove the first element in the list. // final File current = list.poll(); // Increment number of file scanned. task.scanned++; // If 'current' is a file, it's already been matched by our // file filter (see below): act on it. // Note that for the first iteration of this loop, there will // be one single file in the list: the root directory for this // scanner. // if (current.isFile()) { task.matching++; actOn(current); } // If 'current' is a directory, then // find files and directories that match the file filter // in this directory // if (current.isDirectory()) { // Gets matching files and directories final File[] content = current.listFiles(filter); if (content == null) continue; // Adds all matching file to the list. list.addAll(0,Arrays.asList(content)); } } // The loop terminated. If the list is empty, then we have // completed our task. If not, then somebody must have called // stop() on this directory scanner. // if (list.isEmpty()) { task.info = "Successfully Completed"; setStateAndNotify(COMPLETED); } } catch (Exception x) { // We got an exception: stop the scan // task.info = "Failed: "+x; if (LOG.isLoggable(Level.FINEST)) LOG.log(Level.FINEST,"scan task failed: "+x,x); else if (LOG.isLoggable(Level.FINE)) LOG.log(Level.FINE,"scan task failed: "+x); setStateAndNotify(STOPPED); } catch (Error e) { // We got an Error: // Should not happen unless we ran out of memory or // whatever - don't even try to notify, but // stop the scan anyway! // state=STOPPED; task.info = "Error: "+e; // rethrow error. // throw e; } } /** * MBeanNotification support - delegates to broadcaster. */ public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws IllegalArgumentException { broadcaster.addNotificationListener(listener, filter, handback); } // Switch this object state to the desired value an send // a notification. Don't call this method from within a // synchronized block! // private final void setStateAndNotify(ScanState desired) { final ScanState old = state; if (old == desired) return; state = desired; final AttributeChangeNotification n = new AttributeChangeNotification(this, getNextSeqNumber(),System.currentTimeMillis(), "state change","State",ScanState.class.getName(), String.valueOf(old),String.valueOf(desired)); broadcaster.sendNotification(n); } /** * The {@link DirectoryScannerMXBean} may send two types of * notifications: filematch, and state attribute changed. **/ public MBeanNotificationInfo[] getNotificationInfo() { return new MBeanNotificationInfo[] { new MBeanNotificationInfo( new String[] {FILE_MATCHES_NOTIFICATION}, Notification.class.getName(), "Emitted when a file that matches the scan criteria is found" ), new MBeanNotificationInfo( new String[] {AttributeChangeNotification.ATTRIBUTE_CHANGE}, AttributeChangeNotification.class.getName(), "Emitted when the State attribute changes" ) }; } /** * MBeanNotification support - delegates to broadcaster. */ public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException { broadcaster.removeNotificationListener(listener); } /** * MBeanNotification support - delegates to broadcaster. */ public void removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws ListenerNotFoundException { broadcaster.removeNotificationListener(listener, filter, handback); } // Validates the given root directory, returns a File object for // that directory. // Throws IllegalArgumentException if the given root is not // acceptable. // private static File validateRoot(String root) { if (root == null) throw new IllegalArgumentException("no root specified"); if (root.length() == 0) throw new IllegalArgumentException("specified root \"\" is invalid"); final File f = new File(root); if (!f.canRead()) throw new IllegalArgumentException("can't read "+root); if (!f.isDirectory()) throw new IllegalArgumentException("no such directory: "+root); return f; } }