/*
 * ScanDirConfig.java
 *
 * Created on July 12, 2006, 7:43 PM 
 *
 * %W% %E%
 *
 * Copyright (c) 2006, 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:
 *
 * -Redistribution of source code must retain the above copyright notice, this
 *  list of conditions and the following disclaimer.
 *
 * -Redistribution 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 or the names of contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any kind. ALL
 * EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND WARRANTIES, INCLUDING
 * ANY IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE
 * OR NON-INFRINGEMENT, ARE HEREBY EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN")
 * AND ITS LICENSORS SHALL NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE
 * AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR ANY LOST
 * REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL, CONSEQUENTIAL,
 * INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND REGARDLESS OF THE THEORY
 * OF LIABILITY, ARISING OUT OF THE USE OF OR INABILITY TO USE THIS SOFTWARE,
 * EVEN IF SUN HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed, licensed or intended
 * for use in the design, construction, operation or maintenance of any
 * nuclear facility.
 */

package com.sun.jmx.examples.scandir;

import static com.sun.jmx.examples.scandir.ScanManager.getNextSeqNumber;
import static com.sun.jmx.examples.scandir.ScanDirConfigMXBean.SaveState.*;
import com.sun.jmx.examples.scandir.config.XmlConfigUtils;
import com.sun.jmx.examples.scandir.config.DirectoryScannerConfig;
import com.sun.jmx.examples.scandir.config.FileMatch;
import com.sun.jmx.examples.scandir.config.ScanManagerConfig;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Date;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.management.*;
import javax.xml.bind.JAXBException;

/**
 * <p>The <code>ScanDirConfig</code> MBean is in charge of the 
 * <i>scandir</i> application configuration.
 * </p>
 * <p>The <code>ScanDirConfig</code> MBean is able to
 * load and save the <i>scandir</i> application configuration to and from an 
 * XML file.
 * </p>
 * <p>
 * It will let you also interactively modify that configuration, which you
 * can later save to the file, by calling {@link #save}, or discard, by
 * reloading the file without saving - see {@link #load}.
 * </p>
 * <p>
 * There can be as many <code>ScanDirConfigMXBean</code> registered
 * in the MBeanServer as you like, but only one of them will be identified as
 * the current configuration of the {@link ScanManagerMXBean}. 
 * You can switch to another configuration by calling {@link 
 * ScanManagerMXBean#setConfigurationMBean 
 * ScanManagerMXBean.setConfigurationMBean}.
 * </p>
 * <p>
 * Once the current configuration has been loaded (by calling {@link #load})
 * or modified (by calling one of {@link #addDirectoryScanner 
 * addDirectoryScanner}, {@link #removeDirectoryScanner removeDirectoryScanner} 
 * or {@link #setConfiguration setConfiguration}) it can be pushed
 * to the {@link ScanManagerMXBean} by calling {@link 
 * ScanManagerMXBean#applyConfiguration 
 * ScanManagerMXBean.applyConfiguration(true)} - 
 * <code>true</code> means that we apply the configuration from memory, 
 * without first reloading the file.
 * </p>
 * <p>
 * The <code>ScanDirConfig</code> uses the XML annotated Java Beans defined
 * in the {@link com.sun.jmx.examples.scandir.config} package.
 * </p>
 * <p>
 * <u>Note:</u> The <code>ScanDirConfig</code> should probably use 
 * {@code java.nio.channels.FileLock} and lock its configuration file so that
 * two <code>ScanDirConfig</code> object do not share the same file, but it
 * doesn't. Feel free to improve the application in that way.
 * </p>
 * @author Sun Microsystems, 2006 - All rights reserved.
 */
public class ScanDirConfig extends NotificationBroadcasterSupport
        implements ScanDirConfigMXBean, MBeanRegistration {
    
    /**
     * A logger for this class.
     **/
    private static final Logger LOG =
            Logger.getLogger(ScanDirConfig.class.getName());
    
    // We will emit a notification when the save state of this object 
    // chenges. We use directly the base notification class, with a 
    // notification type that indicates the new state at which the 
    // object has arrived.
    //
    // All these notification types will have the same prefix, which is
    // 'com.sun.jmx.examples.scandir.config'.
    //
    private final static String NOTIFICATION_PREFIX =
            ScanManagerConfig.class.getPackage().getName();
    
    /**
     * The <i>com.sun.jmx.examples.scandir.config.saved</i> notification
     * indicates that the configuration data was saved.
     **/
    public final static String NOTIFICATION_SAVED =
            NOTIFICATION_PREFIX+".saved";
    /**
     * The <i>com.sun.jmx.examples.scandir.config.loaded</i> notification
     * indicates that the configuration data was loaded.
     **/
    public final static String NOTIFICATION_LOADED =
            NOTIFICATION_PREFIX+".loaded";
    
    /**
     * The <i>com.sun.jmx.examples.scandir.config.modified</i> notification
     * indicates that the configuration data was modified.
     **/
    public final static String NOTIFICATION_MODIFIED =
            NOTIFICATION_PREFIX+".modified";
    
    // The array of MBeanNotificationInfo that will be exposed in the
    // ScanDirConfigMXBean MBeanInfo.
    // We will pass this array to the NotificationBroadcasterSupport
    // constructor.
    //
    private static MBeanNotificationInfo[] NOTIFICATION_INFO = {
        new MBeanNotificationInfo(
                new String[] {NOTIFICATION_SAVED},
                Notification.class.getName(),
                "Emitted when the configuration is saved"),
        new MBeanNotificationInfo(
                new String[] {NOTIFICATION_LOADED},
                Notification.class.getName(),
                "Emitted when the configuration is loaded"),
        new MBeanNotificationInfo(
                new String[] {NOTIFICATION_MODIFIED},
                Notification.class.getName(),
                "Emitted when the configuration is modified"),
    };

     // The ScanDirConfigMXBean configuration data.
    private volatile ScanManagerConfig config;
    
    // The name of the configuration file
    private String filename = null;
    
    // The name of this configuration. This is usually both equal to
    // config.getName() and objectName.getKeyProperty(name).
    private volatile String configname = null;
    
    // This object save state. CREATED is the initial state.
    //
    private volatile SaveState status = CREATED;
    
    /**
     * Creates a new {@link ScanDirConfigMXBean}. 
     * <p>{@code ScanDirConfigMXBean} can be created by the {@link 
     * ScanManagerMXBean}, or directly by a remote client, using
     * {@code createMBean} or {@code registerMBean}.
     * </p>
     * <p>{@code ScanDirConfigMXBean} created by the {@link 
     * ScanManagerMXBean} will be unregistered by the 
     * {@code ScanManagerMXBean}. {@code ScanDirConfigMXBean} created 
     * directly by a remote client will not be unregistered by the
     * {@code ScanManagerMXBean} - this will remain to the responsibility of
     * the code/client that created them.
     * </p>
     * <p>This object is created empty, you should call load() if you want it
     *    to load its data from the configuration file.
     * </p>
     * @param  filename The configuration file used by this MBean.
     *         Can be null (in which case load() and save() will fail).
     *         Can point to a file that does not exists yet (in which case
     *         load() will fail if called before save(), and save() will 
     *         attempt to create that file). Can point to an existing file,
     *         in which case load() will load that file and save() will save
     *         to that file.
     *
     **/
    public ScanDirConfig(String filename) {
        this(filename,null);
    }

    /**
     * Create a new ScanDirConfig MBean with an initial configuration.
     * @param filename The name of the configuration file.
     * @param initialConfig an initial configuration.
     **/
    public ScanDirConfig(String filename, ScanManagerConfig initialConfig) {
        super(NOTIFICATION_INFO);
        this.filename = filename;
        this.config = initialConfig;
    }

    
    // see ScanDirConfigMXBean
    public void load() throws IOException {
        if (filename == null)
            throw new UnsupportedOperationException("load");
         
        synchronized(this) {
            config = new XmlConfigUtils(filename).readFromFile();
            if (configname != null) config = config.copy(configname);
            else configname = config.getName();
        
            status=LOADED;
        }
        sendNotification(NOTIFICATION_LOADED);
    }
    
    // see ScanDirConfigMXBean
    public void save() throws IOException {
        if (filename == null)
            throw new UnsupportedOperationException("load");
        synchronized (this) {
            new XmlConfigUtils(filename).writeToFile(config);
            status = SAVED;
        }
        sendNotification(NOTIFICATION_SAVED);
    }
        
    // see ScanDirConfigMXBean
    public ScanManagerConfig getConfiguration() {
        synchronized (this) {
            return XmlConfigUtils.xmlClone(config);
        }
    }

   
    // sends a notification indicating the new save state.
    private void sendNotification(String type) {
        final Object source = (objectName==null)?this:objectName;
        final Notification n = new Notification(type,source,
                getNextSeqNumber(),
                "The configuration is "+
                type.substring(type.lastIndexOf('.')+1));
        sendNotification(n);
    }
    

    /**
     * Allows the MBean to perform any operations it needs before being
     * registered in the MBean server. If the name of the MBean is not
     * specified, the MBean can provide a name for its registration. If
     * any exception is raised, the MBean will not be registered in the
     * MBean server.
     * @param server The MBean server in which the MBean will be registered.
     * @param name The object name of the MBean. This name is null if the
     * name parameter to one of the createMBean or registerMBean methods in
     * the MBeanServer interface is null. In that case, this method will
     * try to guess its MBean name by examining its configuration data.
     * If its configuration data is null (nothing was provided in the 
     * constructor) or doesn't contain a name, this method returns {@code null},
     * and registration will fail.
     * <p> 
     * Otherwise, if {@code name} wasn't {@code null} or if a default name could
     * be constructed, the name of the configuration will be set to
     * the value of the ObjectName's {@code name=} key, and the configuration 
     * data will always be renamed to reflect this change.
     * </p>
     * 
     * @return The name under which the MBean is to be registered. 
     * @throws Exception This exception will be caught by the MBean server and
     * re-thrown as an MBeanRegistrationException.
     */
    public ObjectName preRegister(MBeanServer server, ObjectName name) 
        throws Exception {
        if (name == null) {
            if (config == null) return null;
            if (config.getName() == null) return null;
            name = ScanManager.
                    makeMBeanName(ScanDirConfigMXBean.class,config.getName());
        }
        objectName = name;
        mbeanServer = server;
        synchronized (this) {
            configname = name.getKeyProperty("name");
            if (config == null) config = new ScanManagerConfig(configname);
            else config = config.copy(configname);
        }
        return name;
    }

    /**
     * Allows the MBean to perform any operations needed after having
     * been registered in the MBean server or after the registration has
     * failed.
     * <p>This implementation does nothing</p>
     * @param registrationDone Indicates whether or not the MBean has been
     * successfully registered in the MBean server. The value false means
     * that the registration has failed.
     */
    public void postRegister(Boolean registrationDone) {
        // Nothing to do here.
    }

    /**
     * Allows the MBean to perform any operations it needs before being
     * unregistered by the MBean server.
     * <p>This implementation does nothing</p>
     * @throws Exception This exception will be caught by the MBean server and
     * re-thrown as an MBeanRegistrationException.
     */
    public void preDeregister() throws Exception {
        // Nothing to do here.
    }

    /**
     * Allows the MBean to perform any operations needed after having been
     * unregistered in the MBean server.
     * <p>This implementation does nothing</p>
     */
    public void postDeregister() {
        // Nothing to do here.
    }

    // see ScanDirConfigMXBean
    public String getConfigFilename() {
        return filename;
    }

    // see ScanDirConfigMXBean
    public void setConfiguration(ScanManagerConfig config) {
        synchronized (this) {
            if (config == null) {
                this.config = null; 
                return;
            }
        
            if (configname == null) 
                configname = config.getName();
        
            this.config = config.copy(configname);
            status = MODIFIED;
        }
        sendNotification(NOTIFICATION_MODIFIED);
    }
    
    // see ScanDirConfigMXBean
    public DirectoryScannerConfig 
            addDirectoryScanner(String name, String dir, String filePattern, 
                                long sizeExceedsMaxBytes, long sinceLastModified) {
         final DirectoryScannerConfig scanner = 
                 new DirectoryScannerConfig(name);
         scanner.setRootDirectory(dir);
         if (filePattern!=null||sizeExceedsMaxBytes>0||sinceLastModified>0) {
            final FileMatch filter = new FileMatch();
            filter.setFilePattern(filePattern);
            filter.setSizeExceedsMaxBytes(sizeExceedsMaxBytes);
            if (sinceLastModified > 0)
                filter.setLastModifiedBefore(new Date(new Date().getTime()
                                                -sinceLastModified));
            scanner.addIncludeFiles(filter);
         }
         synchronized (this) {
            config.putScan(scanner);
            status = MODIFIED;
         }
         LOG.fine("config: "+config);
         sendNotification(NOTIFICATION_MODIFIED);
         return scanner;
    }

    // see ScanDirConfigMXBean
    public DirectoryScannerConfig removeDirectoryScanner(String name) 
        throws IOException, InstanceNotFoundException {
        final DirectoryScannerConfig scanner;
        synchronized (this) {
            scanner = config.removeScan(name);
            if (scanner == null)
                throw new IllegalArgumentException(name+": scanner not found");
            status = MODIFIED;
        }
        sendNotification(NOTIFICATION_MODIFIED);
        return scanner;
    }

    // see ScanDirConfigMXBean
    public SaveState getSaveState() {
        return status;
    }
    
    // These methods are used by ScanManager to guess a configuration name from
    // a configuration filename.
    //
    static String DEFAULT = "DEFAULT";
    
    private static String getBasename(String name) {
        final int dot = name.indexOf('.');
        if (dot<0)  return name;
        if (dot==0) return getBasename(name.substring(1));
        return name.substring(0,dot);
    }
    
    static String guessConfigName(String configFileName,String defaultFile) {
        try {
            if (configFileName == null) return DEFAULT;
            final File f = new File(configFileName);
            if (f.canRead()) {
                final String confname = XmlConfigUtils.read(f).getName();
                if (confname != null && confname.length()>0) return confname;
            }
            final File f2 = new File(defaultFile);
            if (f.equals(f2)) return DEFAULT;
            final String guess = getBasename(f.getName());
            if (guess == null) return DEFAULT;
            if (guess.length()==0) return DEFAULT;
            return guess;
        } catch (Exception x) {
            return DEFAULT;
        }
    }
    
    // Set by preRegister()
    private volatile MBeanServer mbeanServer;
    private volatile ObjectName objectName;
    
}


