Home | History | Annotate | Download | only in scandir
      1 /*
      2  * Copyright (c) 2006, 2011, Oracle and/or its affiliates. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions
      6  * are met:
      7  *
      8  *   - Redistributions of source code must retain the above copyright
      9  *     notice, this list of conditions and the following disclaimer.
     10  *
     11  *   - Redistributions in binary form must reproduce the above copyright
     12  *     notice, this list of conditions and the following disclaimer in the
     13  *     documentation and/or other materials provided with the distribution.
     14  *
     15  *   - Neither the name of Oracle nor the names of its
     16  *     contributors may be used to endorse or promote products derived
     17  *     from this software without specific prior written permission.
     18  *
     19  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
     20  * IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
     21  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     22  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR
     23  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     24  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     25  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     26  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
     27  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
     28  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
     29  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     30  */
     31 
     32 /*
     33  * This source code is provided to illustrate the usage of a given feature
     34  * or technique and has been deliberately simplified. Additional steps
     35  * required for a production-quality application, such as security checks,
     36  * input validation and proper error handling, might not be present in
     37  * this sample code.
     38  */
     39 
     40 
     41 package com.sun.jmx.examples.scandir;
     42 
     43 import static com.sun.jmx.examples.scandir.ScanManagerMXBean.ScanState.*;
     44 import com.sun.jmx.examples.scandir.ScanManagerMXBean.ScanState;
     45 import com.sun.jmx.examples.scandir.config.DirectoryScannerConfig;
     46 import com.sun.jmx.examples.scandir.config.ScanManagerConfig;
     47 import java.io.File;
     48 
     49 import java.io.IOException;
     50 import java.lang.management.ManagementFactory;
     51 import java.util.ArrayList;
     52 import java.util.Collections;
     53 import java.util.EnumSet;
     54 import java.util.HashMap;
     55 import java.util.Map;
     56 import java.util.Map.Entry;
     57 import java.util.Timer;
     58 import java.util.TimerTask;
     59 import java.util.concurrent.BlockingQueue;
     60 import java.util.concurrent.ConcurrentHashMap;
     61 import java.util.concurrent.ConcurrentLinkedQueue;
     62 import java.util.concurrent.LinkedBlockingQueue;
     63 import java.util.concurrent.Semaphore;
     64 import java.util.concurrent.TimeUnit;
     65 import java.util.logging.Level;
     66 import java.util.logging.Logger;
     67 import javax.management.AttributeChangeNotification;
     68 import javax.management.InstanceNotFoundException;
     69 import javax.management.JMException;
     70 import javax.management.JMX;
     71 import javax.management.ListenerNotFoundException;
     72 import javax.management.MBeanNotificationInfo;
     73 import javax.management.MBeanRegistration;
     74 import javax.management.MBeanServer;
     75 import javax.management.MBeanServerConnection;
     76 import javax.management.MalformedObjectNameException;
     77 import javax.management.Notification;
     78 import javax.management.NotificationBroadcasterSupport;
     79 import javax.management.NotificationEmitter;
     80 import javax.management.NotificationFilter;
     81 import javax.management.NotificationListener;
     82 import javax.management.ObjectInstance;
     83 import javax.management.ObjectName;
     84 
     85 /**
     86  * <p>
     87  * The <code>ScanManager</code> is responsible for applying a configuration,
     88  * starting and scheduling directory scans, and reporting application state.
     89  * </p>
     90  * <p>
     91  * The ScanManager MBean is a singleton MBean which controls
     92  * scan session. The ScanManager name is defined by
     93  * {@link #SCAN_MANAGER_NAME ScanManager.SCAN_MANAGER_NAME}.
     94  * </p>
     95  * <p>
     96  * The <code>ScanManager</code> MBean is the entry point of the <i>scandir</i>
     97  * application management interface. It is from this MBean that all other MBeans
     98  * will be created and registered.
     99  * </p>
    100  *
    101  * @author Sun Microsystems, 2006 - All rights reserved.
    102  */
    103 public class ScanManager implements ScanManagerMXBean,
    104         NotificationEmitter, MBeanRegistration {
    105 
    106     /**
    107      * A logger for this class.
    108      **/
    109     private static final Logger LOG =
    110             Logger.getLogger(ScanManager.class.getName());
    111 
    112     /**
    113      * The name of the ScanManager singleton MBean.
    114      **/
    115     public final static ObjectName SCAN_MANAGER_NAME =
    116             makeSingletonName(ScanManagerMXBean.class);
    117 
    118     /**
    119      * Sequence number used for sending notifications. We use this
    120      * sequence number throughout the application.
    121      **/
    122     private static long seqNumber=0;
    123 
    124     /**
    125      * The NotificationBroadcasterSupport object used to handle
    126      * listener registration.
    127      **/
    128     private final NotificationBroadcasterSupport broadcaster;
    129 
    130     /**
    131      * The MBeanServer in which this MBean is registered. We obtain
    132      * this reference by implementing the {@link MBeanRegistration}
    133      * interface.
    134      **/
    135     private volatile MBeanServer mbeanServer;
    136 
    137     /**
    138      * A queue of pending notifications we are about to send.
    139      * We're using a BlockingQueue in order to avoid sending
    140      * notifications from within a synchronized block.
    141      **/
    142     private final BlockingQueue<Notification> pendingNotifs;
    143 
    144     /**
    145      * The state of the scan session.
    146      **/
    147     private volatile ScanState state = STOPPED;
    148 
    149     /**
    150      * The list of DirectoryScannerMBean that are run by a scan session.
    151      **/
    152     private final Map<ObjectName,DirectoryScannerMXBean> scanmap;
    153 
    154     /**
    155      * The list of ScanDirConfigMXBean that were created by this MBean.
    156      **/
    157     private final Map<ObjectName, ScanDirConfigMXBean> configmap;
    158 
    159     // The ResultLogManager for this application.
    160     private final ResultLogManager log;
    161 
    162     /**
    163      * We use a semaphore to ensure proper sequencing of exclusive
    164      * action. The logic we have implemented is to fail - rather
    165      * than block, if an exclusive action is already in progress.
    166      **/
    167     private final Semaphore sequencer = new Semaphore(1);
    168 
    169     // A proxy to the current ScanDirConfigMXBean which holds the current
    170     // configuration data.
    171     //
    172     private volatile ScanDirConfigMXBean config = null;
    173 
    174     // Avoid to write parameters twices when creating a new ConcurrentHashMap.
    175     //
    176     private static <K, V> Map<K, V> newConcurrentHashMap() {
    177         return new ConcurrentHashMap<K, V>();
    178     }
    179 
    180     // Avoid to write parameters twices when creating a new HashMap.
    181     //
    182     private static <K, V> Map<K, V> newHashMap() {
    183         return new HashMap<K, V>();
    184     }
    185 
    186     /**
    187      * Creates a default singleton ObjectName for a given class.
    188      * @param clazz The interface class of the MBean for which we want to obtain
    189      *        a default singleton name, or its implementation class.
    190      *        Give one or the other depending on what you wish to see in
    191      *        the value of the key {@code type=}.
    192      * @return A default singleton name for a singleton MBean class.
    193      * @throws IllegalArgumentException if the name can't be created
    194      *         for some unfathomable reason (e.g. an unexpected
    195      *         exception was raised).
    196      **/
    197     public final static ObjectName makeSingletonName(Class clazz) {
    198         try {
    199             final Package p = clazz.getPackage();
    200             final String packageName = (p==null)?null:p.getName();
    201             final String className   = clazz.getSimpleName();
    202             final String domain;
    203             if (packageName == null || packageName.length()==0) {
    204                 // We use a reference to ScanDirAgent.class to ease
    205                 // to keep track of possible class renaming.
    206                 domain = ScanDirAgent.class.getSimpleName();
    207             } else {
    208                 domain = packageName;
    209             }
    210             final ObjectName name = new ObjectName(domain,"type",className);
    211             return name;
    212         } catch (Exception x) {
    213             final IllegalArgumentException iae =
    214                     new IllegalArgumentException(String.valueOf(clazz),x);
    215             throw iae;
    216         }
    217     }
    218 
    219     /**
    220      * Creates a default ObjectName with keys <code>type=</code> and
    221      * <code>name=</code> for an instance of a given MBean interface class.
    222      * @param clazz The interface class of the MBean for which we want to obtain
    223      *        a default name, or its implementation class.
    224      *        Give one or the other depending on what you wish to see in
    225      *        the value of the key {@code type=}.
    226      * @param name The value of the <code>name=</code> key.
    227      * @return A default name for an instance of the given MBean interface class.
    228      * @throws IllegalArgumentException if the name can't be created.
    229      *         (e.g. an unexpected exception was raised).
    230      **/
    231     public static final ObjectName makeMBeanName(Class clazz, String name) {
    232         try {
    233             return ObjectName.
    234                 getInstance(makeSingletonName(clazz)
    235                         .toString()+",name="+name);
    236         } catch (MalformedObjectNameException x) {
    237             final IllegalArgumentException iae =
    238                     new IllegalArgumentException(String.valueOf(name),x);
    239             throw iae;
    240         }
    241     }
    242 
    243     /**
    244      * Return the ObjectName for a DirectoryScannerMXBean of that name.
    245      * This is {@code makeMBeanName(DirectoryScannerMXBean.class,name)}.
    246      * @param name The value of the <code>name=</code> key.
    247      * @return the ObjectName for a DirectoryScannerMXBean of that name.
    248      */
    249     public static final ObjectName makeDirectoryScannerName(String name) {
    250         return makeMBeanName(DirectoryScannerMXBean.class,name);
    251     }
    252 
    253     /**
    254      * Return the ObjectName for a {@code ScanDirConfigMXBean} of that name.
    255      * This is {@code makeMBeanName(ScanDirConfigMXBean.class,name)}.
    256      * @param name The value of the <code>name=</code> key.
    257      * @return the ObjectName for a {@code ScanDirConfigMXBean} of that name.
    258      */
    259     public static final ObjectName makeScanDirConfigName(String name) {
    260         return makeMBeanName(ScanDirConfigMXBean.class,name);
    261     }
    262 
    263     /**
    264      * Create and register a new singleton instance of the ScanManager
    265      * MBean in the given {@link MBeanServerConnection}.
    266      * @param mbs The MBeanServer in which the new singleton instance
    267      *         should be created.
    268      * @throws JMException The MBeanServer connection raised an exception
    269      *         while trying to instantiate and register the singleton MBean
    270      *         instance.
    271      * @throws IOException There was a connection problem while trying to
    272      *         communicate with the underlying MBeanServer.
    273      * @return A proxy for the registered MBean.
    274      **/
    275     public static ScanManagerMXBean register(MBeanServerConnection mbs)
    276         throws IOException, JMException {
    277         final ObjectInstance moi =
    278                 mbs.createMBean(ScanManager.class.getName(),SCAN_MANAGER_NAME);
    279         final ScanManagerMXBean proxy =
    280                 JMX.newMXBeanProxy(mbs,moi.getObjectName(),
    281                                   ScanManagerMXBean.class,true);
    282         return proxy;
    283     }
    284 
    285     /**
    286      * Creates a new {@code ScanManagerMXBean} proxy over the given
    287      * {@code MBeanServerConnection}. Does not check whether a
    288      * {@code ScanManagerMXBean}
    289      * is actually registered in that {@code MBeanServerConnection}.
    290      * @return a new {@code ScanManagerMXBean} proxy.
    291      * @param mbs The {@code MBeanServerConnection} which holds the
    292      * {@code ScanManagerMXBean} to proxy.
    293      */
    294     public static ScanManagerMXBean
    295             newSingletonProxy(MBeanServerConnection mbs) {
    296         final ScanManagerMXBean proxy =
    297                 JMX.newMXBeanProxy(mbs,SCAN_MANAGER_NAME,
    298                                   ScanManagerMXBean.class,true);
    299         return proxy;
    300     }
    301 
    302     /**
    303      * Creates a new {@code ScanManagerMXBean} proxy over the platform
    304      * {@code MBeanServer}. This is equivalent to
    305      * {@code newSingletonProxy(ManagementFactory.getPlatformMBeanServer())}.
    306      * @return a new {@code ScanManagerMXBean} proxy.
    307      **/
    308     public static ScanManagerMXBean newSingletonProxy() {
    309         return newSingletonProxy(ManagementFactory.getPlatformMBeanServer());
    310     }
    311 
    312     /**
    313      * Create and register a new singleton instance of the ScanManager
    314      * MBean in the given {@link MBeanServerConnection}.
    315      * @throws JMException The MBeanServer connection raised an exception
    316      *         while trying to instantiate and register the singleton MBean
    317      *         instance.
    318      * @throws IOException There was a connection problem while trying to
    319      *         communicate with the underlying MBeanServer.
    320      * @return A proxy for the registered MBean.
    321      **/
    322     public static ScanManagerMXBean register()
    323         throws IOException, JMException {
    324         final MBeanServer mbs = ManagementFactory.getPlatformMBeanServer();
    325         return register(mbs);
    326     }
    327 
    328     /**
    329      * Create a new ScanManager MBean
    330      **/
    331     public ScanManager() {
    332         broadcaster = new NotificationBroadcasterSupport();
    333         pendingNotifs = new LinkedBlockingQueue<Notification>(100);
    334         scanmap = newConcurrentHashMap();
    335         configmap = newConcurrentHashMap();
    336         log = new ResultLogManager();
    337     }
    338 
    339 
    340     // Creates a new DirectoryScannerMXBean, from the given configuration data.
    341     DirectoryScannerMXBean createDirectoryScanner(DirectoryScannerConfig config) {
    342             return new DirectoryScanner(config,log);
    343     }
    344 
    345     // Applies a configuration.
    346     // throws IllegalStateException if lock can't be acquired.
    347     // Unregisters all existing directory scanners, the create and registers
    348     // new directory scanners according to the given config.
    349     // Then pushes the log config to the result log manager.
    350     //
    351     private void applyConfiguration(ScanManagerConfig bean)
    352         throws IOException, JMException {
    353         if (bean == null) return;
    354         if (!sequencer.tryAcquire()) {
    355             throw new IllegalStateException("Can't acquire lock");
    356         }
    357         try {
    358             unregisterScanners();
    359             final DirectoryScannerConfig[] scans = bean.getScanList();
    360             if (scans == null) return;
    361             for (DirectoryScannerConfig scan : scans) {
    362                 addDirectoryScanner(scan);
    363             }
    364             log.setConfig(bean.getInitialResultLogConfig());
    365         } finally {
    366             sequencer.release();
    367         }
    368     }
    369 
    370     // See ScanManagerMXBean
    371     public void applyConfiguration(boolean fromMemory)
    372         throws IOException, JMException {
    373         if (fromMemory == false) config.load();
    374         applyConfiguration(config.getConfiguration());
    375     }
    376 
    377     // See ScanManagerMXBean
    378     public void applyCurrentResultLogConfig(boolean toMemory)
    379         throws IOException, JMException {
    380         final ScanManagerConfig bean = config.getConfiguration();
    381         bean.setInitialResultLogConfig(log.getConfig());
    382         config.setConfiguration(bean);
    383         if (toMemory==false) config.save();
    384     }
    385 
    386     // See ScanManagerMXBean
    387     public void setConfigurationMBean(ScanDirConfigMXBean config) {
    388         this.config = config;
    389     }
    390 
    391     // See ScanManagerMXBean
    392     public ScanDirConfigMXBean getConfigurationMBean() {
    393         return config;
    394     }
    395 
    396     // Creates and registers a new directory scanner.
    397     // Called by applyConfiguration.
    398     // throws IllegalStateException if state is not STOPPED or COMPLETED
    399     // (you cannot change the config while scanning is scheduled or running).
    400     //
    401     private DirectoryScannerMXBean addDirectoryScanner(
    402                 DirectoryScannerConfig bean)
    403         throws JMException {
    404         try {
    405             final DirectoryScannerMXBean scanner;
    406             final ObjectName scanName;
    407             synchronized (this) {
    408                 if (state != STOPPED && state != COMPLETED)
    409                    throw new IllegalStateException(state.toString());
    410                 scanner = createDirectoryScanner(bean);
    411                 scanName = makeDirectoryScannerName(bean.getName());
    412             }
    413             LOG.fine("server: "+mbeanServer);
    414             LOG.fine("scanner: "+scanner);
    415             LOG.fine("scanName: "+scanName);
    416             final ObjectInstance moi =
    417                 mbeanServer.registerMBean(scanner,scanName);
    418             final ObjectName moiName = moi.getObjectName();
    419             final DirectoryScannerMXBean proxy =
    420                 JMX.newMXBeanProxy(mbeanServer,moiName,
    421                 DirectoryScannerMXBean.class,true);
    422             scanmap.put(moiName,proxy);
    423             return proxy;
    424         } catch (RuntimeException x) {
    425             final String msg = "Operation failed: "+x;
    426             if (LOG.isLoggable(Level.FINEST))
    427                 LOG.log(Level.FINEST,msg,x);
    428             else LOG.fine(msg);
    429             throw x;
    430         } catch (JMException x) {
    431             final String msg = "Operation failed: "+x;
    432             if (LOG.isLoggable(Level.FINEST))
    433                 LOG.log(Level.FINEST,msg,x);
    434             else LOG.fine(msg);
    435             throw x;
    436         }
    437     }
    438 
    439     // See ScanManagerMXBean
    440     public ScanDirConfigMXBean createOtherConfigurationMBean(String name,
    441             String filename)
    442         throws JMException {
    443         final ScanDirConfig profile = new ScanDirConfig(filename);
    444         final ObjectName profName = makeScanDirConfigName(name);
    445         final ObjectInstance moi = mbeanServer.registerMBean(profile,profName);
    446         final ScanDirConfigMXBean proxy =
    447                 JMX.newMXBeanProxy(mbeanServer,profName,
    448                     ScanDirConfigMXBean.class,true);
    449         configmap.put(moi.getObjectName(),proxy);
    450         return proxy;
    451     }
    452 
    453 
    454     // See ScanManagerMXBean
    455     public Map<String,DirectoryScannerMXBean> getDirectoryScanners() {
    456         final Map<String,DirectoryScannerMXBean> proxyMap = newHashMap();
    457         for (Entry<ObjectName,DirectoryScannerMXBean> item : scanmap.entrySet()){
    458             proxyMap.put(item.getKey().getKeyProperty("name"),item.getValue());
    459         }
    460         return proxyMap;
    461     }
    462 
    463     // ---------------------------------------------------------------
    464     // State Management
    465     // ---------------------------------------------------------------
    466 
    467     /**
    468      * For each operation, this map stores a list of states from
    469      * which the corresponding operation can be legally called.
    470      * For instance, it is legal to call "stop" regardless of the
    471      * application state. However, "schedule" can be called only if
    472      * the application state is STOPPED, etc...
    473      **/
    474     private final static Map<String,EnumSet<ScanState>> allowedStates;
    475     static {
    476         allowedStates = newHashMap();
    477         // You can always call stop
    478         allowedStates.put("stop",EnumSet.allOf(ScanState.class));
    479 
    480         // You can only call closed when stopped
    481         allowedStates.put("close",EnumSet.of(STOPPED,COMPLETED,CLOSED));
    482 
    483         // You can call schedule only when the current task is
    484         // completed or stopped.
    485         allowedStates.put("schedule",EnumSet.of(STOPPED,COMPLETED));
    486 
    487         // switch reserved for background task: goes from SCHEDULED to
    488         //    RUNNING when it enters the run() method.
    489         allowedStates.put("scan-running",EnumSet.of(SCHEDULED));
    490 
    491         // switch reserved for background task: goes from RUNNING to
    492         //    SCHEDULED when it has completed but needs to reschedule
    493         //    itself for specified interval.
    494         allowedStates.put("scan-scheduled",EnumSet.of(RUNNING));
    495 
    496         // switch reserved for background task:
    497         //     goes from RUNNING to COMPLETED upon successful completion
    498         allowedStates.put("scan-done",EnumSet.of(RUNNING));
    499     }
    500 
    501     // Get this object's state. No need to synchronize because
    502     // state is volatile.
    503     // See ScanManagerMXBean
    504     public ScanState getState() {
    505         return state;
    506     }
    507 
    508     /**
    509      * Enqueue a state changed notification for the given states.
    510      **/
    511     private void queueStateChangedNotification(
    512                     long sequence,
    513                     long time,
    514                     ScanState old,
    515                     ScanState current) {
    516         final AttributeChangeNotification n =
    517                 new AttributeChangeNotification(SCAN_MANAGER_NAME,sequence,time,
    518                 "ScanManager State changed to "+current,"State",
    519                 ScanState.class.getName(),old.toString(),current.toString());
    520         // Queue the notification. We have created an unlimited queue, so
    521         // this method should always succeed.
    522         try {
    523             if (!pendingNotifs.offer(n,2,TimeUnit.SECONDS)) {
    524                 LOG.fine("Can't queue Notification: "+n);
    525             }
    526         } catch (InterruptedException x) {
    527                 LOG.fine("Can't queue Notification: "+x);
    528         }
    529     }
    530 
    531     /**
    532      * Send all notifications present in the queue.
    533      **/
    534     private void sendQueuedNotifications() {
    535         Notification n;
    536         while ((n = pendingNotifs.poll()) != null) {
    537             broadcaster.sendNotification(n);
    538         }
    539     }
    540 
    541     /**
    542      * Checks that the current state is allowed for the given operation,
    543      * and if so, switch its value to the new desired state.
    544      * This operation also enqueue the appropriate state changed
    545      * notification.
    546      **/
    547     private ScanState switchState(ScanState desired,String forOperation) {
    548         return switchState(desired,allowedStates.get(forOperation));
    549     }
    550 
    551     /**
    552      * Checks that the current state is one of the allowed states,
    553      * and if so, switch its value to the new desired state.
    554      * This operation also enqueue the appropriate state changed
    555      * notification.
    556      **/
    557     private ScanState switchState(ScanState desired,EnumSet<ScanState> allowed) {
    558         final ScanState old;
    559         final long timestamp;
    560         final long sequence;
    561         synchronized(this) {
    562             old = state;
    563             if (!allowed.contains(state))
    564                throw new IllegalStateException(state.toString());
    565             state = desired;
    566             timestamp = System.currentTimeMillis();
    567             sequence  = getNextSeqNumber();
    568         }
    569         LOG.fine("switched state: "+old+" -> "+desired);
    570         if (old != desired)
    571             queueStateChangedNotification(sequence,timestamp,old,desired);
    572         return old;
    573     }
    574 
    575 
    576     // ---------------------------------------------------------------
    577     // schedule() creates a new SessionTask that will be executed later
    578     // (possibly right away if delay=0) by a Timer thread.
    579     // ---------------------------------------------------------------
    580 
    581     // The timer used by this object. Lazzy evaluation. Cleaned in
    582     // postDeregister()
    583     //
    584     private Timer timer = null;
    585 
    586     // See ScanManagerMXBean
    587     public void schedule(long delay, long interval) {
    588         if (!sequencer.tryAcquire()) {
    589             throw new IllegalStateException("Can't acquire lock");
    590         }
    591         try {
    592             LOG.fine("scheduling new task: state="+state);
    593             final ScanState old = switchState(SCHEDULED,"schedule");
    594             final boolean scheduled =
    595                 scheduleSession(new SessionTask(interval),delay);
    596             if (scheduled)
    597                 LOG.fine("new task scheduled: state="+state);
    598         } finally {
    599             sequencer.release();
    600         }
    601         sendQueuedNotifications();
    602     }
    603 
    604     // Schedule a SessionTask. The session task may reschedule
    605     // a new identical task when it eventually ends.
    606     // We use this logic so that the 'interval' time is measured
    607     // starting at the end of the task that finishes, rather than
    608     // at its beginning. Therefore if a repeated task takes x ms,
    609     // it will be repeated every x+interval ms.
    610     //
    611     private synchronized boolean scheduleSession(SessionTask task, long delay) {
    612         if (state == STOPPED) return false;
    613         if (timer == null) timer = new Timer("ScanManager");
    614         tasklist.add(task);
    615         timer.schedule(task,delay);
    616         return true;
    617     }
    618 
    619     // ---------------------------------------------------------------
    620     // start() is equivalent to schedule(0,0)
    621     // ---------------------------------------------------------------
    622 
    623     // See ScanManagerMXBean
    624     public void start() throws IOException, InstanceNotFoundException {
    625         schedule(0,0);
    626     }
    627 
    628     // ---------------------------------------------------------------
    629     // Methods used to implement stop() -  stop() is asynchronous,
    630     // and needs to notify any running background task that it needs
    631     // to stop. It also needs to prevent scheduled task from being
    632     // run.
    633     // ---------------------------------------------------------------
    634 
    635     // See ScanManagerMXBean
    636     public void stop() {
    637         if (!sequencer.tryAcquire())
    638             throw new IllegalStateException("Can't acquire lock");
    639         int errcount = 0;
    640         final StringBuilder b = new StringBuilder();
    641 
    642         try {
    643             switchState(STOPPED,"stop");
    644 
    645             errcount += cancelSessionTasks(b);
    646             errcount += stopDirectoryScanners(b);
    647         } finally {
    648             sequencer.release();
    649         }
    650 
    651         sendQueuedNotifications();
    652         if (errcount > 0) {
    653             b.insert(0,"stop partially failed with "+errcount+" error(s):");
    654             throw new RuntimeException(b.toString());
    655         }
    656     }
    657 
    658     // See ScanManagerMXBean
    659     public void close() {
    660         switchState(CLOSED,"close");
    661         sendQueuedNotifications();
    662     }
    663 
    664     // Appends exception to a StringBuilder message.
    665     //
    666     private void append(StringBuilder b,String prefix,Throwable t) {
    667         final String first = (prefix==null)?"\n":"\n"+prefix;
    668         b.append(first).append(String.valueOf(t));
    669         Throwable cause = t;
    670         while ((cause = cause.getCause())!=null) {
    671             b.append(first).append("Caused by:").append(first);
    672             b.append('\t').append(String.valueOf(cause));
    673         }
    674     }
    675 
    676     // Cancels all scheduled session tasks
    677     //
    678     private int cancelSessionTasks(StringBuilder b) {
    679         int errcount = 0;
    680         // Stops scheduled tasks if any...
    681         //
    682         for (SessionTask task : tasklist) {
    683             try {
    684                 task.cancel();
    685                 tasklist.remove(task);
    686             } catch (Exception ex) {
    687                 errcount++;
    688                 append(b,"\t",ex);
    689             }
    690         }
    691         return errcount;
    692     }
    693 
    694     // Stops all DirectoryScanners configured for this object.
    695     //
    696     private int stopDirectoryScanners(StringBuilder b) {
    697         int errcount = 0;
    698         // Stops directory scanners if any...
    699         //
    700         for (DirectoryScannerMXBean s : scanmap.values()) {
    701             try {
    702                 s.stop();
    703             } catch (Exception ex) {
    704                 errcount++;
    705                 append(b,"\t",ex);
    706             }
    707         }
    708         return errcount;
    709     }
    710 
    711 
    712     // ---------------------------------------------------------------
    713     // We start scanning in background in a Timer thread.
    714     // The methods below implement that logic.
    715     // ---------------------------------------------------------------
    716 
    717     private void scanAllDirectories()
    718         throws IOException, InstanceNotFoundException {
    719 
    720         int errcount = 0;
    721         final StringBuilder b = new StringBuilder();
    722         for (ObjectName key : scanmap.keySet()) {
    723             final DirectoryScannerMXBean s = scanmap.get(key);
    724             try {
    725                 if (state == STOPPED) return;
    726                 s.scan();
    727             } catch (Exception ex) {
    728                 LOG.log(Level.FINE,key + " failed to scan: "+ex,ex);
    729                 errcount++;
    730                 append(b,"\t",ex);
    731             }
    732         }
    733         if (errcount > 0) {
    734             b.insert(0,"scan partially performed with "+errcount+" error(s):");
    735             throw new RuntimeException(b.toString());
    736         }
    737     }
    738 
    739     // List of scheduled session task. Needed by stop() to cancel
    740     // scheduled sessions. There's usually at most 1 session in
    741     // this list (unless there's a bug somewhere ;-))
    742     //
    743     private final ConcurrentLinkedQueue<SessionTask> tasklist =
    744             new ConcurrentLinkedQueue<SessionTask>();
    745 
    746     // Used to give a unique id to session task - useful for
    747     // debugging.
    748     //
    749     private volatile static long taskcount = 0;
    750 
    751     /**
    752      * A session task will be scheduled to run in background in a
    753      * timer thread. There can be at most one session task running
    754      * at a given time (this is ensured by using a timer - which is
    755      * a single threaded object).
    756      *
    757      * If the session needs to be repeated, it will reschedule an
    758      * identical session when it finishes to run. This ensure that
    759      * two session runs are separated by the given interval time.
    760      *
    761      **/
    762     private class SessionTask extends TimerTask {
    763 
    764         /**
    765          * Delay after which the next iteration of this task will
    766          * start. This delay is measured  starting at the end of
    767          * the previous iteration.
    768          **/
    769         final long delayBeforeNext;
    770 
    771         /**
    772          * A unique id for this task.
    773          **/
    774         final long taskid;
    775 
    776         /**
    777          * Whether it's been cancelled by stop()
    778          **/
    779         volatile boolean cancelled=false;
    780 
    781         /**
    782          * create a new SessionTask.
    783          **/
    784         SessionTask(long scheduleNext) {
    785             delayBeforeNext = scheduleNext;
    786             taskid = taskcount++;
    787         }
    788 
    789         /**
    790          * When run() begins, the state is switched to RUNNING.
    791          * When run() ends then:
    792          *      If the task is repeated, the state will be switched
    793          *      to SCHEDULED (because a new task was scheduled).
    794          *      Otherwise the state will be switched to either
    795          *      STOPPED (if it was stopped before it could complete)
    796          *      or COMPLETED (if it completed gracefully)
    797          * This method is used to switch to the desired state and
    798          * send the appropriate notifications.
    799          * When entering the method, we check whether the state is
    800          * STOPPED. If so, we return false - and the SessionTask will
    801          * stop. Otherwise, we switch the state to the desired value.
    802          **/
    803         private boolean notifyStateChange(ScanState newState,String condition) {
    804             synchronized (ScanManager.this) {
    805                 if (state == STOPPED || state == CLOSED) return false;
    806                 switchState(newState,condition);
    807             }
    808             sendQueuedNotifications();
    809             return true;
    810         }
    811 
    812         // Cancels this task.
    813         public boolean cancel() {
    814             cancelled=true;
    815             return super.cancel();
    816         }
    817 
    818         /**
    819          * Invoke all directories scanners in sequence. At each
    820          * step, checks to see whether the task should stop.
    821          **/
    822         private boolean execute() {
    823             final String tag = "Scheduled session["+taskid+"]";
    824             try {
    825                 if (cancelled) {
    826                     LOG.finer(tag+" cancelled: done");
    827                     return false;
    828                 }
    829                 if (!notifyStateChange(RUNNING,"scan-running")) {
    830                     LOG.finer(tag+" stopped: done");
    831                     return false;
    832                 }
    833                 scanAllDirectories();
    834             } catch (Exception x) {
    835                 if (LOG.isLoggable(Level.FINEST)) {
    836                     LOG.log(Level.FINEST,
    837                             tag+" failed to scan: "+x,x);
    838                 } else if (LOG.isLoggable(Level.FINE)) {
    839                     LOG.fine(tag+" failed to scan: "+x);
    840                 }
    841             }
    842             return true;
    843         }
    844 
    845         /**
    846          * Schedule an identical task for next iteration.
    847          **/
    848         private boolean scheduleNext() {
    849             final String tag = "Scheduled session["+taskid+"]";
    850 
    851             // We need now to reschedule a new task for after 'delayBeforeNext' ms.
    852             try {
    853                 LOG.finer(tag+": scheduling next session for "+ delayBeforeNext + "ms");
    854                 if (cancelled || !notifyStateChange(SCHEDULED,"scan-scheduled")) {
    855                     LOG.finer(tag+" stopped: do not reschedule");
    856                     return false;
    857                 }
    858                 final SessionTask nextTask = new SessionTask(delayBeforeNext);
    859                 if (!scheduleSession(nextTask,delayBeforeNext)) return false;
    860                 LOG.finer(tag+": next session successfully scheduled");
    861             } catch (Exception x) {
    862                 if (LOG.isLoggable(Level.FINEST)) {
    863                     LOG.log(Level.FINEST,tag+
    864                             " failed to schedule next session: "+x,x);
    865                 } else if (LOG.isLoggable(Level.FINE)) {
    866                     LOG.fine(tag+" failed to schedule next session: "+x);
    867                 }
    868             }
    869             return true;
    870         }
    871 
    872 
    873         /**
    874          * The run method:
    875          * executes scanning logic, the schedule next iteration if needed.
    876          **/
    877         public void run() {
    878             final String tag = "Scheduled session["+taskid+"]";
    879             LOG.entering(SessionTask.class.getName(),"run");
    880             LOG.finer(tag+" starting...");
    881             try {
    882                 if (execute()==false) return;
    883 
    884                 LOG.finer(tag+" terminating - state is "+state+
    885                     ((delayBeforeNext >0)?(" next session is due in "+delayBeforeNext+" ms."):
    886                         " no additional session scheduled"));
    887 
    888                 // if delayBeforeNext <= 0 we are done, either because the session was
    889                 // stopped or because it successfully completed.
    890                 if (delayBeforeNext <= 0) {
    891                     if (!notifyStateChange(COMPLETED,"scan-done"))
    892                         LOG.finer(tag+" stopped: done");
    893                     else
    894                         LOG.finer(tag+" completed: done");
    895                     return;
    896                 }
    897 
    898                 // we need to reschedule a new session for 'delayBeforeNext' ms.
    899                 scheduleNext();
    900 
    901             } finally {
    902                 tasklist.remove(this);
    903                 LOG.finer(tag+" finished...");
    904                 LOG.exiting(SessionTask.class.getName(),"run");
    905             }
    906         }
    907     }
    908 
    909     // ---------------------------------------------------------------
    910     // ---------------------------------------------------------------
    911 
    912     // ---------------------------------------------------------------
    913     // MBean Notification support
    914     // The methods below are imported from {@link NotificationEmitter}
    915     // ---------------------------------------------------------------
    916 
    917     /**
    918      * Delegates the implementation of this method to the wrapped
    919      * {@code NotificationBroadcasterSupport} object.
    920      **/
    921     public void addNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws IllegalArgumentException {
    922         broadcaster.addNotificationListener(listener, filter, handback);
    923     }
    924 
    925 
    926     /**
    927      * We emit an {@code AttributeChangeNotification} when the {@code State}
    928      * attribute changes.
    929      **/
    930     public MBeanNotificationInfo[] getNotificationInfo() {
    931         return new MBeanNotificationInfo[] {
    932             new MBeanNotificationInfo(new String[] {
    933                 AttributeChangeNotification.ATTRIBUTE_CHANGE},
    934                 AttributeChangeNotification.class.getName(),
    935                 "Emitted when the State attribute changes")
    936             };
    937     }
    938 
    939     /**
    940      * Delegates the implementation of this method to the wrapped
    941      * {@code NotificationBroadcasterSupport} object.
    942      **/
    943     public void removeNotificationListener(NotificationListener listener) throws ListenerNotFoundException {
    944         broadcaster.removeNotificationListener(listener);
    945     }
    946 
    947     /**
    948      * Delegates the implementation of this method to the wrapped
    949      * {@code NotificationBroadcasterSupport} object.
    950      **/
    951     public void removeNotificationListener(NotificationListener listener, NotificationFilter filter, Object handback) throws ListenerNotFoundException {
    952         broadcaster.removeNotificationListener(listener, filter, handback);
    953     }
    954 
    955     /**
    956      * Returns and increment the sequence number used for
    957      * notifications. We use the same sequence number throughout the
    958      * application - this is why this method is only package protected.
    959      * @return A unique sequence number for the next notification.
    960      */
    961     static synchronized long getNextSeqNumber() {
    962         return seqNumber++;
    963     }
    964 
    965     // ---------------------------------------------------------------
    966     // End of MBean Notification support
    967     // ---------------------------------------------------------------
    968 
    969     // ---------------------------------------------------------------
    970     // MBeanRegistration support
    971     // The methods below are imported from {@link MBeanRegistration}
    972     // ---------------------------------------------------------------
    973 
    974     /**
    975      * Allows the MBean to perform any operations it needs before being
    976      * registered in the MBean server. If the name of the MBean is not
    977      * specified, the MBean can provide a name for its registration. If
    978      * any exception is raised, the MBean will not be registered in the
    979      * MBean server.
    980      * <p>In this implementation, we check that the provided name is
    981      * either {@code null} or equals to {@link #SCAN_MANAGER_NAME}. If it
    982      * isn't then we throw an IllegalArgumentException, otherwise we return
    983      * {@link #SCAN_MANAGER_NAME}.</p>
    984      * <p>This ensures that there will be a single instance of ScanManager
    985      * registered in a given MBeanServer, and that it will always be
    986      * registered with the singleton's {@link #SCAN_MANAGER_NAME}.</p>
    987      * <p>We do not need to check whether an MBean by that name is
    988      *    already registered because the MBeanServer will perform
    989      *    this check just after having called preRegister().</p>
    990      * @param server The MBean server in which the MBean will be registered.
    991      * @param name The object name of the MBean. This name is null if the
    992      * name parameter to one of the createMBean or registerMBean methods in
    993      * the MBeanServer interface is null. In that case, this method must
    994      * return a non-null ObjectName for the new MBean.
    995      * @return The name under which the MBean is to be registered. This value
    996      * must not be null. If the name parameter is not null, it will usually
    997      * but not necessarily be the returned value.
    998      * @throws Exception This exception will be caught by the MBean server and
    999      * re-thrown as an MBeanRegistrationException.
   1000      */
   1001     public ObjectName preRegister(MBeanServer server, ObjectName name) throws Exception {
   1002         if (name != null) {
   1003             if (!SCAN_MANAGER_NAME.equals(name))
   1004                 throw new IllegalArgumentException(String.valueOf(name));
   1005         }
   1006         mbeanServer = server;
   1007         return SCAN_MANAGER_NAME;
   1008     }
   1009 
   1010     // Returns the default configuration filename
   1011     static String getDefaultConfigurationFileName() {
   1012         // This is a file calles 'jmx-scandir.xml' located
   1013         // in the user directory.
   1014         final String user = System.getProperty("user.home");
   1015         final String defconf = user+File.separator+"jmx-scandir.xml";
   1016         return defconf;
   1017     }
   1018 
   1019     /**
   1020      * Allows the MBean to perform any operations needed after having
   1021      * been registered in the MBean server or after the registration has
   1022      * failed.
   1023      * <p>
   1024      * If registration was not successful, the method returns immediately.
   1025      * <p>
   1026      * If registration is successful, register the {@link ResultLogManager}
   1027      * and default {@link ScanDirConfigMXBean}. If registering these
   1028      * MBean fails, the {@code ScanManager} state will be switched to
   1029      * {@link #close CLOSED}, and postRegister ends there.
   1030      * </p>
   1031      * <p>Otherwise the {@code ScanManager} will ask the
   1032      * {@link ScanDirConfigMXBean} to load its configuration.
   1033      * If it succeeds, the configuration will be {@link
   1034      * #applyConfiguration applied}. Otherwise, the method simply returns,
   1035      * assuming that the user will later create/update a configuration and
   1036      * apply it.
   1037      * @param registrationDone Indicates whether or not the MBean has been
   1038      * successfully registered in the MBean server. The value false means
   1039      * that the registration has failed.
   1040      */
   1041     public void postRegister(Boolean registrationDone) {
   1042         if (!registrationDone) return;
   1043         Exception test=null;
   1044         try {
   1045             mbeanServer.registerMBean(log,
   1046                     ResultLogManager.RESULT_LOG_MANAGER_NAME);
   1047             final String defconf = getDefaultConfigurationFileName();
   1048             final String conf = System.getProperty("scandir.config.file",defconf);
   1049             final String confname = ScanDirConfig.guessConfigName(conf,defconf);
   1050             final ObjectName defaultProfileName =
   1051                     makeMBeanName(ScanDirConfigMXBean.class,confname);
   1052             if (!mbeanServer.isRegistered(defaultProfileName))
   1053                 mbeanServer.registerMBean(new ScanDirConfig(conf),
   1054                         defaultProfileName);
   1055             config = JMX.newMXBeanProxy(mbeanServer,defaultProfileName,
   1056                     ScanDirConfigMXBean.class,true);
   1057             configmap.put(defaultProfileName,config);
   1058         } catch (Exception x) {
   1059             LOG.config("Failed to populate MBeanServer: "+x);
   1060             close();
   1061             return;
   1062         }
   1063         try {
   1064             config.load();
   1065         } catch (Exception x) {
   1066             LOG.finest("No config to load: "+x);
   1067             test = x;
   1068         }
   1069         if (test == null) {
   1070             try {
   1071                 applyConfiguration(config.getConfiguration());
   1072             } catch (Exception x) {
   1073                 if (LOG.isLoggable(Level.FINEST))
   1074                     LOG.log(Level.FINEST,"Failed to apply config: "+x,x);
   1075                 LOG.config("Failed to apply config: "+x);
   1076             }
   1077         }
   1078     }
   1079 
   1080     // Unregisters all created DirectoryScanners
   1081     private void unregisterScanners() throws JMException {
   1082         unregisterMBeans(scanmap);
   1083     }
   1084 
   1085     // Unregisters all created ScanDirConfigs
   1086     private void unregisterConfigs() throws JMException {
   1087         unregisterMBeans(configmap);
   1088     }
   1089 
   1090     // Unregisters all MBeans named by the given map
   1091     private void unregisterMBeans(Map<ObjectName,?> map) throws JMException {
   1092         for (ObjectName key : map.keySet()) {
   1093             if (mbeanServer.isRegistered(key))
   1094                 mbeanServer.unregisterMBean(key);
   1095             map.remove(key);
   1096         }
   1097     }
   1098 
   1099     // Unregisters the ResultLogManager.
   1100     private void unregisterResultLogManager() throws JMException {
   1101         final ObjectName name = ResultLogManager.RESULT_LOG_MANAGER_NAME;
   1102         if (mbeanServer.isRegistered(name)) {
   1103             mbeanServer.unregisterMBean(name);
   1104         }
   1105     }
   1106 
   1107     /**
   1108      * Allows the MBean to perform any operations it needs before being
   1109      * unregistered by the MBean server.
   1110      * This implementation also unregisters all the MXBeans
   1111      * that were created by this object.
   1112      * @throws IllegalStateException if the lock can't be acquire, or if
   1113      *         the MBean's state doesn't allow the MBean to be unregistered
   1114      *         (e.g. because it's scheduled or running).
   1115      * @throws Exception This exception will be caught by the MBean server and
   1116      * re-thrown as an MBeanRegistrationException.
   1117      */
   1118     public void preDeregister() throws Exception {
   1119         try {
   1120             close();
   1121             if (!sequencer.tryAcquire())
   1122                 throw new IllegalStateException("can't acquire lock");
   1123             try {
   1124                 unregisterScanners();
   1125                 unregisterConfigs();
   1126                 unregisterResultLogManager();
   1127             } finally {
   1128                 sequencer.release();
   1129             }
   1130         } catch (Exception x) {
   1131             LOG.log(Level.FINEST,"Failed to unregister: "+x,x);
   1132             throw x;
   1133         }
   1134     }
   1135 
   1136     /**
   1137      * Allows the MBean to perform any operations needed after having been
   1138      * unregistered in the MBean server.
   1139      * Cancels the internal timer - if any.
   1140      */
   1141     public synchronized void postDeregister() {
   1142         if (timer != null) {
   1143             try {
   1144                 timer.cancel();
   1145             } catch (Exception x) {
   1146                 if (LOG.isLoggable(Level.FINEST))
   1147                     LOG.log(Level.FINEST,"Failed to cancel timer",x);
   1148                 else if (LOG.isLoggable(Level.FINE))
   1149                     LOG.fine("Failed to cancel timer: "+x);
   1150             } finally {
   1151                 timer = null;
   1152             }
   1153         }
   1154    }
   1155 
   1156     // ---------------------------------------------------------------
   1157     // End of MBeanRegistration support
   1158     // ---------------------------------------------------------------
   1159 
   1160 }
   1161