com.heliosapm.script.AbstractDeployedScript.java Source code

Java tutorial

Introduction

Here is the source code for com.heliosapm.script.AbstractDeployedScript.java

Source

/**
 * Helios, OpenSource Monitoring
 * Brought to you by the Helios Development Group
 *
 * Copyright 2014, Helios Development Group and individual contributors
 * as indicated by the @author tags. See the copyright.txt file in the
 * distribution for a full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org. 
 *
 */
package com.heliosapm.script;

import java.io.File;
import java.io.FilenameFilter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.regex.Pattern;

import javax.management.AttributeChangeNotification;
import javax.management.MBeanNotificationInfo;
import javax.management.MBeanRegistration;
import javax.management.MBeanServer;
import javax.management.Notification;
import javax.management.NotificationBroadcasterSupport;
import javax.management.NotificationFilter;
import javax.management.NotificationListener;
import javax.management.ObjectName;

import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.codahale.metrics.Histogram;
import com.codahale.metrics.JmxReporter;
import com.codahale.metrics.MetricRegistry;
import com.github.sps.metrics.opentsdb.OpenTsdb;
import com.github.sps.metrics.opentsdb.OpenTsdbReporter;
import com.heliosapm.filewatcher.ScriptFileWatcher;
import com.heliosapm.jmx.config.Configuration;
import com.heliosapm.jmx.config.ConfigurationManager;
import com.heliosapm.jmx.config.InternalConfigurationListener;
import com.heliosapm.jmx.execution.ExecutionSchedule;
import com.heliosapm.jmx.execution.ScheduleType;
import com.heliosapm.jmx.execution.ScheduledExecutionService;
import com.heliosapm.jmx.notif.SharedNotificationExecutor;
import com.heliosapm.jmx.util.helpers.JMXHelper;
import com.heliosapm.jmx.util.helpers.StringHelper;
import com.heliosapm.jmx.util.helpers.URLHelper;

/**
 * <p>Title: AbstractDeployedScript</p>
 * <p>Description: Base class for implementing {@link DeployedScript} extensions</p> 
 * <p>Company: Helios Development Group LLC</p>
 * @author Whitehead (nwhitehead AT heliosdev DOT org)
 * <p><code>com.heliosapm.script.AbstractDeployedScript</code></p>
 * @param <T> The type of the underlying executable script
 */

public abstract class AbstractDeployedScript<T> extends NotificationBroadcasterSupport
        implements DeployedScript<T>, MBeanRegistration, InternalConfigurationListener {
    /** Instance logger */
    protected final Logger log;
    /** The underlying executable component */
    protected T executable = null;
    /** The originating source file */
    protected final File sourceFile;
    /** The linked source file */
    protected final File linkedFile;
    /** The deployment short name */
    protected final String shortName;

    /** The checksum of the source */
    protected long checksum = -1L;
    /** The last modified timestamp of the source */
    protected long lastModified = -1L;

    /** The source file extension */
    protected final String extension;
    /** The root watched directory */
    protected final String rootDir;
    /** The path segments */
    protected final String[] pathSegments;
    /** The configuration for this deployment */
    protected final Configuration config;

    /** The watched configuration file */
    protected final AtomicReference<ObjectName> watchedConfig = new AtomicReference<ObjectName>(null);
    /** Flag indicating if config change listener is registered */
    protected final AtomicBoolean watchedConfigListenerRegistered = new AtomicBoolean(false);
    /** The version number of this deployment */
    protected final AtomicInteger version = new AtomicInteger(0);

    /** The deployed script metric registry */
    protected static final MetricRegistry metrics = new MetricRegistry();

    static {
        final JmxReporter reporter = JmxReporter.forRegistry(metrics).build();
        reporter.start();
        final OpenTsdbReporter tsdbReporter = OpenTsdbReporter.forRegistry(metrics).build(OpenTsdb.getInstance());
        tsdbReporter.start(5, TimeUnit.SECONDS);
    }

    /** The config change listener */
    protected final NotificationListener configChangeListener = new NotificationListener() {
        @Override
        public void handleNotification(final Notification notification, final Object handback) {
            final Configuration configUpdate = (Configuration) notification.getUserData();
            config.load(configUpdate);
            if (config.areDependenciesReady()) {
                if (getStatus() == DeploymentStatus.NOONFIG) {
                    setStatus(DeploymentStatus.READY, "All dependencies satisfied");
                }
            }
        }
    };

    /** The execution schedule */
    protected final AtomicReference<ExecutionSchedule> schedule = new AtomicReference<ExecutionSchedule>(
            ExecutionSchedule.NO_EXEC_SCHEDULE);
    /** The backup execution schedule, saved when execution was paused */
    protected final AtomicReference<ExecutionSchedule> pausedSchedule = new AtomicReference<ExecutionSchedule>(
            ExecutionSchedule.NO_EXEC_SCHEDULE);

    /** The schedule handle */
    protected final AtomicReference<ScheduledFuture<?>> scheduleHandle = new AtomicReference<ScheduledFuture<?>>(
            null);
    /** The executable's execution timeout in ms. Defaults to 0 which is no timeout */
    protected long timeout = 0;

    /** The deployment's last set status message */
    protected final AtomicReference<String> lastStatusMessage = new AtomicReference<String>("Initialized");

    /** The status of this deployment */
    protected final AtomicReference<DeploymentStatus> status = new AtomicReference<DeploymentStatus>(
            DeploymentStatus.INIT);
    /** The last mod time  */
    protected final AtomicLong lastModTime = new AtomicLong(System.currentTimeMillis());
    /** The effective timestamp of the current status  */
    protected final AtomicLong statusTime = new AtomicLong(System.currentTimeMillis());
    /** The last execution elapsed time */
    protected final AtomicLong lastExecElapsed = new AtomicLong(-1L);

    /** The last execute time  */
    protected final AtomicLong lastExecTime = new AtomicLong(-1L);
    /** The last error time  */
    protected final AtomicLong lastErrorTime = new AtomicLong(-1L);
    /** The execution count since the last reset  */
    protected final AtomicLong execCount = new AtomicLong(0L);
    /** The error count since the last reset  */
    protected final AtomicLong errorCount = new AtomicLong(0L);
    /** Notification sequence number provider */
    protected static final AtomicLong sequence = new AtomicLong(0L);
    /** The deployment's JMX ObjectName */
    protected final ObjectName objectName;

    protected final Histogram execElapsedTimes;

    /** The descriptors of the JMX notifications emitted by this service */
    private static final MBeanNotificationInfo[] notificationInfos = new MBeanNotificationInfo[] {
            JMXHelper.META_CHANGED_NOTIF,
            new MBeanNotificationInfo(new String[] { NOTIF_STATUS_CHANGE },
                    AttributeChangeNotification.class.getName(),
                    "JMX notification broadcast when the status of a deployment changes"),
            new MBeanNotificationInfo(new String[] { AttributeChangeNotification.ATTRIBUTE_CHANGE },
                    Notification.class.getName(),
                    "JMX notification broadcast when the configuration of a deployment changes"),
            new MBeanNotificationInfo(new String[] { NOTIF_RECOMPILE }, Notification.class.getName(),
                    "JMX notification broadcast when a deployment is recompiled") };

    /** Pattern to replace the skip entry in a source header */
    public static final Pattern SKIP_REPLACER = Pattern.compile("skip=(.*?),|$|\\s");

    /** The deployment's insta notifications */
    protected final Set<MBeanNotificationInfo> instanceNotificationInfos = new HashSet<MBeanNotificationInfo>(
            Arrays.asList(notificationInfos));

    /**
     * Creates a new AbstractDeployedScript
     * @param sourceFile The originating source file
     */
    public AbstractDeployedScript(File sourceFile) {
        super(SharedNotificationExecutor.getInstance(), notificationInfos);
        this.sourceFile = sourceFile;
        shortName = URLHelper.getPlainFileName(sourceFile);
        final Path link = this.sourceFile.toPath();
        Path tmpPath = null;
        if (Files.isSymbolicLink(link)) {
            try {
                tmpPath = Files.readSymbolicLink(link);
            } catch (Exception ex) {
                tmpPath = null;
            }
        }
        linkedFile = tmpPath == null ? null : tmpPath.toFile().getAbsoluteFile();
        String tmp = URLHelper.getFileExtension(sourceFile);
        if (tmp == null || tmp.trim().isEmpty())
            throw new RuntimeException("The source file [" + sourceFile + "] has no extension");
        extension = tmp.toLowerCase();
        rootDir = ScriptFileWatcher.getInstance().getRootDir(sourceFile.getParentFile().getAbsolutePath());
        pathSegments = calcPathSegments();
        log = LoggerFactory.getLogger(
                StringHelper.fastConcatAndDelim("/", pathSegments) + "/" + sourceFile.getName().replace('.', '_'));
        objectName = buildObjectName();
        if (CONFIG_DOMAIN.equals(objectName.getDomain())) {
            config = null;
        } else {
            config = new Configuration(objectName, this);
            config.registerInternalListener(this);
        }
        checksum = URLHelper.adler32(URLHelper.toURL(sourceFile));
        lastModified = URLHelper.getLastModified(URLHelper.toURL(sourceFile));
        execElapsedTimes = metrics.histogram(MetricRegistry.name("name=" + shortName, "fx=execution"));

    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#isCommentLine(java.lang.String)
     */
    @Override
    public boolean isCommentLine(final String text) {
        if (text == null || text.trim().isEmpty())
            return false;
        String _text = text.trim();
        return (_text.startsWith("//") || (_text.startsWith("/*") && _text.endsWith("*/")));
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.jmx.config.InternalConfigurationListener#onConfigurationItemChange(java.lang.String, java.lang.String)
     */
    @Override
    public void onConfigurationItemChange(final String key, final String value) {

    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.jmx.config.InternalConfigurationListener#onDependencyReadinessChange(boolean, java.lang.String)
     */
    @Override
    public void onDependencyReadinessChange(final boolean ready, final String message) {
        if (ready) {
            setStatus(DeploymentStatus.READY, DeploymentStatus.setOf(DeploymentStatus.NOONFIG), message);
        } else {
            setStatus(DeploymentStatus.NOONFIG, DeploymentStatus.setOf(DeploymentStatus.READY), message);
            setStatus(DeploymentStatus.READY, DeploymentStatus.setOf(DeploymentStatus.NOONFIG), message);
        }
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#getWatchedConfiguration()
     */
    @Override
    public ObjectName getWatchedConfiguration() {
        return watchedConfig.get();
    }

    //   public Set<ObjectName> getListenOnTargets() {
    //      try {
    //         final String onformat = String.format("%s:root=%s,%sextension=config,subextension=*,name=%%s", 
    //               DeployedScript.CONFIG_DOMAIN, rootDir.replace(':', ';'), configDirs());
    //         return new LinkedHashSet<ObjectName>(Arrays.asList(
    //               //com.heliosapm.configuration:
    //                  //root=C;\hprojects\heliosJMX\.\src\test\resources\testdir\hotdir,
    //                  //d1=X,
    //                  //d2=Y,
    //                  // name=jmx,
    //                  // extension=config,
    //                  //subextension=properties
    //               
    //               JMXHelper.objectName(String.format(onformat, shortName)),
    //               JMXHelper.objectName(String.format(onformat, pathSegments[pathSegments.length-1]))               
    //         ));
    //      } catch (Exception ex) {
    //         log.error("Failed to get listen on targets", ex);
    //         throw new RuntimeException("Failed to get listen on targets", ex);
    //      }
    //   }

    /**
     * Compiles the path segment into a set of ObjectName keypairs
     * @return a set of ObjectName keypairs
     */
    protected String configDirs() {
        StringBuilder b = new StringBuilder();
        for (int i = 1; i < pathSegments.length; i++) {
            b.append("d").append(i).append("=").append(pathSegments[i]).append(",");
        }
        return b.toString();
    }

    //   /**
    //    * Builds the standard JMX ObjectName for this deployment
    //    * @return an ObjectName
    //    */
    //   protected ObjectName buildObjectName() {
    //      return JMXHelper.objectName(new StringBuilder(getDomain()).append(":")
    //            .append("path=").append(join("/", pathSegments)).append(",")
    //            .append("extension=").append(extension).append(",")
    //            .append("name=").append(shortName)         
    //            );
    //   }

    /**
     * Builds the standard JMX ObjectName for this deployment
     * @return an ObjectName
     */
    protected ObjectName buildObjectName() {
        final StringBuilder b = new StringBuilder(getDomain()).append(":").append("root=")
                .append(rootDir.replace(':', ';')).append(",");
        for (int i = 1; i < pathSegments.length; i++) {
            b.append("d").append(i).append("=").append(pathSegments[i]).append(",");
        }
        b.append("name=").append(shortName).append(",").append("extension=").append(extension);
        return JMXHelper.objectName(b);
    }

    /**
     * {@inheritDoc}
     * @see javax.management.NotificationBroadcasterSupport#getNotificationInfo()
     */
    @Override
    public MBeanNotificationInfo[] getNotificationInfo() {
        if (instanceNotificationInfos.isEmpty()) {
            return super.getNotificationInfo();
        }
        return instanceNotificationInfos.toArray(new MBeanNotificationInfo[instanceNotificationInfos.size()]);
    }

    /**
     * Adds a new notification type for this deployment
     * @param infos The notification infos to add
     */
    protected void registerNotifications(final MBeanNotificationInfo... infos) {
        if (infos == null || infos.length == 0)
            return;
        if (instanceNotificationInfos.isEmpty()) {
            Collections.addAll(instanceNotificationInfos, notificationInfos);
        }
        int added = 0;
        for (MBeanNotificationInfo info : infos) {
            if (info == null)
                continue;
            if (instanceNotificationInfos.add(info)) {
                added++;
            }
        }
        if (added > 0) {
            final Notification notif = new Notification(JMXHelper.MBEAN_INFO_CHANGED, objectName,
                    sequence.incrementAndGet(), System.currentTimeMillis(), "Updated MBeanInfo");
            notif.setUserData(JMXHelper.getMBeanInfo(objectName));
            sendNotification(notif);
        }
    }

    /**
     * Cleans up any allocated resources in the executable that is being discarded
     * @param executable the executable to clean
     */
    protected abstract void closeOldExecutable(final T executable);

    public String getDomain() {
        return DEPLOYMENT_DOMAIN;
    }

    public int getVersion() {
        return version.get();
    }

    public String[] getPathSegments(final int trim) {
        if (trim == 0)
            return pathSegments.clone();
        final int pLength = pathSegments.length;
        final int absTrim = Math.abs(trim);

        if (absTrim > pLength)
            throw new IllegalArgumentException("The requested trim [" + trim + "] is larger than the path segment ["
                    + pathSegments.length + "]");
        if (absTrim == pLength)
            return new String[0];
        LinkedList<String> psegs = new LinkedList<String>(Arrays.asList(pathSegments));
        for (int i = 0; i < absTrim; i++) {
            if (trim < 0)
                psegs.removeFirst();
            else
                psegs.removeLast();
        }
        return psegs.toArray(new String[pLength - absTrim]);
    }

    public Map<String, String> getPendingDependencies() {
        return config.getPendingDependencies();
    }

    /**
     * Builds the path segments for this file
     * @return the path segments for this file
     */
    protected String[] calcPathSegments() {
        final File rootFile = new File(rootDir);
        if (rootFile.equals(sourceFile.getParentFile())) {
            return new String[] { rootFile.getName() };
        }
        final List<String> segments = new ArrayList<String>();
        segments.add(rootFile.getName());
        Collections.addAll(segments, rootFile.toPath().relativize(sourceFile.getParentFile().toPath()).toString()
                .replace("\\", "/").split("/"));
        return segments.toArray(new String[segments.size()]);
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#execute()
     */
    @Override
    public T execute() {
        final long now = System.currentTimeMillis();
        DeploymentStatus ds = getStatus();
        if (ds == DeploymentStatus.NOONFIG) {
            if (config.areDependenciesReady()) {
                ds = getStatus();
            }
        }
        if (ds.canExec) {
            try {
                final Object ret = doExecute();
                execCount.incrementAndGet();
                lastExecTime.set(now);
                long elapsed = System.currentTimeMillis() - now;
                execElapsedTimes.update(elapsed);
                lastExecElapsed.set(elapsed);
                log.debug("Elapsed: [{}] ms.", elapsed);
                return (T) ret;
            } catch (Exception ex) {
                final long er = errorCount.incrementAndGet();
                lastErrorTime.set(System.currentTimeMillis());
                log.error("Failed to execute. Error Count: {}", er, ex);
                throw new RuntimeException("Failed to execute deployed script [" + this.getFileName() + "]", ex);
            }
        }
        return null;
    }

    /**
     * {@inheritDoc}
     * @see java.util.concurrent.Callable#call()
     */
    @Override
    public T call() throws Exception {
        return execute();
    }

    //   /**
    //    * {@inheritDoc}
    //    * @see java.lang.Runnable#run()
    //    */
    //   @Override
    //   public void run() {
    //      execute();
    //   }

    /**
     * To be implemented by concrete deployment scripts
     * @return the return value of the execution
     * @throws Exception thrown on any error in the execution
     */
    protected abstract Object doExecute() throws Exception;

    /**
     * Joins an array of strings to a delim separated string
     * @param delim The delimeter 
     * @param segs The array to join
     * @return the joined string
     */
    protected String join(final String delim, final String[] segs) {
        return Arrays.toString(segs).replace("]", "").replace("[", "").replace(" ", "").replace(",", delim);
    }

    /**
     * Sets the status for this deployment
     * @param status The status to set to
     * @return the prior status
     */
    protected DeploymentStatus setStatus(final DeploymentStatus status) {
        return setStatus(status, null);
    }

    /**
     * Sets the status for this deployment
     * @param status The status to set to
     * @param messageFormat An optional message format, ignored if null
     * @param tokens The message format tokens, ignored if the format is null 
     * @return the prior status
     */
    protected DeploymentStatus setStatus(final DeploymentStatus status, final String messageFormat,
            final Object... tokens) {
        return setStatus(status, null, messageFormat, tokens);
    }

    /**
     * Sets the status for this deployment
     * @param status The status to set to
     * @param ifStatusIn Optional set of statuses that the current status must be in for the status change to fire.
     * Ignored if null or empty
     * @param messageFormat An optional message format, ignored if null
     * @param tokens The message format tokens, ignored if the format is null 
     * @return the prior status
     */
    protected DeploymentStatus setStatus(final DeploymentStatus status, final Set<DeploymentStatus> ifStatusIn,
            final String messageFormat, final Object... tokens) {
        if (ifStatusIn != null && !ifStatusIn.isEmpty() && !ifStatusIn.contains(this.status.get()))
            return this.status.get();
        final DeploymentStatus priorStatus = this.status.getAndSet(status);
        if (priorStatus != status) {
            final long now = System.currentTimeMillis();
            statusTime.set(System.currentTimeMillis());
            if (messageFormat == null) {
                lastStatusMessage.set(String.format("[%s]: Status set to %s", new Date(now), status));
            } else {
                lastStatusMessage.set(String.format("[%s]: Status set to %s", new Date(now), status) + " : "
                        + String.format(messageFormat, tokens));
            }
        }
        return priorStatus;
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#setExecutable(java.lang.Object, long, long)
     */
    @Override
    public void setExecutable(final T executable, final long checksum, final long timestamp) {
        if (executable == null)
            throw new IllegalArgumentException("The passed executable was null");
        if (this.executable != null) {
            closeOldExecutable(this.executable);
        }
        if (this.checksum != checksum || this.lastModified != timestamp) {
            this.executable = executable;
            this.checksum = checksum;
            this.lastModified = timestamp;
            final long now = System.currentTimeMillis();
            lastModTime.set(now);
            final int v = version.incrementAndGet();
            initExcutable();
            final String message = String.format("[%s]: Recompiled Deployment v.%s, [%s]", new Date(now), v,
                    sourceFile.getAbsolutePath());
            lastStatusMessage.set(message);
            sendNotification(
                    new Notification(NOTIF_RECOMPILE, objectName, sequence.incrementAndGet(), now, message));
        }
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#initExcutable()
     */
    @Override
    public void initExcutable() {
        if (getConfiguration().areDependenciesReady()) {
            setStatus(DeploymentStatus.READY);
        } else {
            setStatus(DeploymentStatus.NOONFIG, "Pending Dependencies: %s",
                    getConfiguration().getPendingDependencyKeys().toString());
        }
    }

    /**
     * Executable initialization hook to process any class level annotations that
     * may not have been materialized during compilation
     */
    protected void processAnnotations() {

    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScriptMXBean#getDeploymentClassName()
     */
    @Override
    public String getDeploymentClassName() {
        if (executable == null)
            return null;
        return executable.getClass().getName();
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#setFailedExecutable(java.lang.String, long, long)
     */
    public void setFailedExecutable(final String errorMessage, long checksum, long timestamp) {
        // for now, if the exe is not null, we set it null to mark it broken.
        // other options that could be supported are:
        // keep, but mark as out-of-sync
        final long now = System.currentTimeMillis();
        lastModTime.set(now);
        final DeploymentStatus prior = status.getAndSet(DeploymentStatus.BROKEN);
        this.executable = null;
        this.checksum = checksum;
        this.lastModified = timestamp;

        final String message = String.format("[%s]: Deployment Recompilation Failed for [%s], Error:",
                new Date(now), sourceFile.getAbsolutePath(), errorMessage);
        lastStatusMessage.set(message);

        if (prior != DeploymentStatus.BROKEN) {
            sendStatusChangeNotification(prior, DeploymentStatus.BROKEN, timestamp);
        }

    }

    public void setExecutionSchedule(final String scheduleExpression) {
        if (scheduleExpression == null)
            throw new IllegalArgumentException("The passed Schedule Expression was null");
        final ExecutionSchedule newSchedule = ExecutionSchedule.getInstance(scheduleExpression, false);
        setExecutionSchedule(newSchedule);
    }

    /**
     * Activates the passed execution schedule for this depoyment
     * @param newSchedule the new execution schedule for this depoyment
     */
    public void setExecutionSchedule(final ExecutionSchedule newSchedule) {
        if (newSchedule == null)
            throw new IllegalArgumentException("The passed ExecutionSchedule was null");
        if (newSchedule.getScheduleType() != ScheduleType.NONE) {
            pausedSchedule.set(newSchedule);
        }
        final ExecutionSchedule priorSchedule = schedule.getAndSet(newSchedule);
        if (!newSchedule.equals(priorSchedule)) {
            final ScheduledFuture<?> priorFuture = scheduleHandle.get();
            if (priorFuture != null) {
                priorFuture.cancel(true);
            }
            scheduleHandle.set(ScheduledExecutionService.getInstance().scheduleDeploymentExecution(this));
        }
    }

    public void pauseScheduledExecutions() {
        setExecutionSchedule(ExecutionSchedule.NO_EXEC_SCHEDULE);
    }

    public String resumeScheduledExecutions() {
        final ExecutionSchedule es = pausedSchedule.get();
        if (es == null) {
            return ExecutionSchedule.NO_EXEC_SCHEDULE.toString();
        }
        setExecutionSchedule(es);
        return getSchedule();
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#getExecutionSchedule()
     */
    @Override
    public ExecutionSchedule getExecutionSchedule() {
        return schedule.get();
    }

    public String getFileName() {
        return sourceFile.getAbsolutePath();
    }

    public String getShortName() {
        return shortName;
    }

    public String getLinkedFileName() {
        return linkedFile == null ? null : linkedFile.getAbsolutePath();
    }

    public String getExtension() {
        return extension;
    }

    public String getRoot() {
        return rootDir;
    }

    public String[] getPathSegments() {
        return pathSegments;
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#getExecutable()
     */
    @Override
    public T getExecutable() {
        if (executable == null)
            throw new RuntimeException("Executable has not been initialized");
        return executable;
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#getConfiguration()
     */
    @Override
    public Configuration getConfiguration() {
        return config;
    }

    //==============================================================================================================================
    //      Add Configuration Ops 
    //==============================================================================================================================

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#addConfiguration(java.util.Map)
     */
    @Override
    public void addConfiguration(final Map<String, Object> config) {
        if (config == null)
            throw new IllegalArgumentException("The passed config map was null");
        config.putAll(config);
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#addConfiguration(java.lang.String, java.lang.Object)
     */
    @Override
    public void addConfiguration(final String key, final Object value) {
        if (key == null || key.trim().isEmpty())
            throw new IllegalArgumentException("The passed key was null or empty");
        if (value == null)
            throw new IllegalArgumentException("The passed value was null");
        getConfiguration().putTyped(key, value);
        //      if(SCHEDULE_KEY.equals(key)) {
        //         try {
        //            
        //         } catch (Exception x) {
        //            
        //         }
        //      }
    }

    public void addConfiguration(final String key, final String value) {
        addConfiguration(key, (Object) value);
    }

    //==============================================================================================================================   

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#callInvocable(java.lang.String)
     */
    @Override
    public String callInvocable(final String name) {
        if (name == null || name.trim().isEmpty())
            throw new IllegalArgumentException("The passed name was null");
        return null;
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#getConfig(java.lang.String, java.lang.Class)
     */
    @Override
    public <E> E getConfig(String key, Class<E> type) {
        if (key == null || key.trim().isEmpty())
            throw new IllegalArgumentException("The passed key was null or empty");
        return (E) getConfiguration().get(key);
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#getConfig(java.lang.String)
     */
    @Override
    public Object getConfig(String key) {
        if (key == null || key.trim().isEmpty())
            throw new IllegalArgumentException("The passed key was null or empty");
        return getConfiguration().get(key);
    }

    /**
     * {@inheritDoc}
     * @see com.heliosapm.script.DeployedScript#getStatus()
     */
    @Override
    public DeploymentStatus getStatus() {
        return status.get();
    }

    public byte[] returnSourceBytes() {
        return URLHelper.getBytesFromURL(URLHelper.toURL(sourceFile));
    }

    public String returnSource() {
        return URLHelper.getTextFromURL(URLHelper.toURL(sourceFile), 1000, 1000);
    }

    public long getLastModTime() {
        return lastModTime.get();
    }

    public long getLastExecTime() {
        return lastExecTime.get();
    }

    public long getLastErrorTime() {
        return lastErrorTime.get();
    }

    public long getLastExecElapsed() {
        return lastExecElapsed.get();
    }

    public String getStatusMessage() {
        return lastStatusMessage.get();
    }

    public long getExecutionTimeout() {
        return timeout;
    }

    public void setExecutionTimeout(final long timeout) {
        this.timeout = timeout;
    }

    public long getExecutionCount() {
        return execCount.get();
    }

    public long getErrorCount() {
        return errorCount.get();
    }

    public ObjectName getObjectName() {
        return objectName;
    }

    public void pause() {
        setStatus(DeploymentStatus.PAUSED);
    }

    public void resume() {
        // TODO: How do we know what to resume to ?
    }

    public String toJSON() {
        JSONObject json = new JSONObject();
        json.put("sourceFile", this.sourceFile.getAbsolutePath());
        json.put("extension", this.extension);
        json.put("objectName", this.objectName.toString());
        json.put("status", this.status);
        json.put("statusTime", this.statusTime.get());
        json.put("lastModTime", this.lastModTime.get());
        json.put("lastExecTime", this.lastExecTime.get());
        json.put("lastErrorTime", this.lastErrorTime.get());
        json.put("execCount", this.execCount.get());
        json.put("errorCount", this.errorCount.get());
        JSONObject cfig = new JSONObject();
        for (final String key : getConfiguration().keySet()) {
            final String value = getConfiguration().get(key);
            try {
                cfig.put(key, value);
            } catch (Exception x) {
                /* No Op */}
        }
        json.put("config", cfig);
        return json.toString();
    }

    public String getSchedule() {
        return schedule.get().toString();
    }

    public void updateSource(final String source, final boolean recompile) {
        String header = URLHelper.getLines(URLHelper.toURL(sourceFile), 1)[0].trim();
        String newHeader = null;
        if (header.startsWith("//")) {
            if (recompile) {
                newHeader = SKIP_REPLACER.matcher(header).replaceAll(ScriptFileWatcher.SKIP_PATTERN + ",");
            } else {
                newHeader = header;
            }
        } else {
            newHeader = recompile ? ("//" + ScriptFileWatcher.SKIP_PATTERN) : "//";
        }
        URLHelper.writeToURL(URLHelper.toURL(sourceFile), (newHeader + "\n" + source).getBytes(), false);

    }

    public void undeploy() {
        executable = null;
        getConfiguration().clear();
    }

    public boolean isExecutable() {
        return status.get().canExec;
    }

    public boolean isScheduleExecutable() {
        return status.get().canSchedulerExec;
    }

    public String executeForString() {
        Object obj = execute();
        return obj == null ? null : obj.toString();
    }

    public String getConfigString(final String key) {
        Object obj = getConfig(key);
        return obj == null ? null : obj.toString();
    }

    public Map<String, String> getConfigurationMap() {
        return config.getInternalConfig();
    }

    public Map<String, String> getParentConfigurationMap() {
        if (watchedConfig.get() != null) {
            final Configuration cfg = ConfigurationManager.getInstance().getConfig(watchedConfig.get());
            if (cfg != null && !cfg.isEmpty()) {
                Map<String, String> smap = cfg.getInternalConfig();
                return smap;
            }
        }
        return Collections.emptyMap();
    }

    public String getStatusName() {
        return getStatus().name();
    }

    /**
     * Callack when a deployment listens on the {pwd}.config because the {shortName}.config
     * did not exist when this deployment started, but the {pwd}.config was just registered,
     * so we drop the listener on {pwd}.config and put it on {shortName}.config
     * @param directConfigObjectName The ObjectName of the {shortName}.config started notification
     */
    protected void onReplaceWatchedConfiguration(final ObjectName directConfigObjectName) {

    }

    /**
     * Initializes the configuration
     */
    public void initConfig() {
        watchedConfig.set(findWatchedConfiguration());
        if (watchedConfig.get() != null) {
            Map<String, String> parentConfig = getParentConfigurationMap();
            if (parentConfig != null && !parentConfig.isEmpty()) {
                this.getConfiguration().load(parentConfig);
            }
            if (watchedConfigListenerRegistered.compareAndSet(false, true)) {
                try {
                    JMXHelper.addNotificationListener(watchedConfig.get(), configChangeListener,
                            new NotificationFilter() {
                                /**  */
                                private static final long serialVersionUID = -2890751194005498532L;

                                @Override
                                public boolean isNotificationEnabled(final Notification notification) {
                                    final Object userData = notification.getUserData();
                                    return (notification.getSource().equals(watchedConfig.get())
                                            && NOTIF_CONFIG_MOD.equals(notification.getType()) && userData != null
                                            && (userData instanceof Configuration)
                                            && !(((Configuration) userData).isEmpty()));
                                }
                            }, null);
                } catch (Exception ex) {
                    try {
                        JMXHelper.removeNotificationListener(watchedConfig.get(), configChangeListener);
                    } catch (Exception x) {
                        /* No Op */}
                    log.error("Failed to register configuration listener", ex);
                    watchedConfigListenerRegistered.set(false);
                }
            }
        }
    }

    /**
     * Finds the ObjectName of the configuration MBean to watch
     * @return the ObjectName of the configuration MBean to watch
     */
    protected ObjectName findWatchedConfiguration() {
        /*
         * if extension != config
         *       look for {shortName}.config
         *       if not found
         *          look for {pwd}.config   (error if not found)
         *          listen for registration of a future {shortName}.config
         *             when notified:
         *                stop listening on {pwd}.config and listen on {shortName}.config  
         * 
         * else   (we are a config)
         *       if(shortName != {pwd})
         *          look for {pwd}.config
         *       else  (we are the {pwd}.config
         *          look for {pwd.parent}.config (error if not found)
         */

        final Hashtable<String, String> keyAttrs = new Hashtable<String, String>(objectName.getKeyPropertyList());

        final String pwd = sourceFile.getParentFile().getName();

        if (!"config".equals(extension)) {
            // look for {shortName}.config
            keyAttrs.put("extension", "config");
            ObjectName watchedObjectName = JMXHelper.objectName(CONFIG_DOMAIN, keyAttrs);
            if (JMXHelper.isRegistered(watchedObjectName)) {
                return watchedObjectName;
            }
            // nope. register a listener in case he shows up, then look for {pwd}.config
            final NotificationListener lateComerListener = new NotificationListener() {
                @Override
                public void handleNotification(Notification notification, Object handback) {

                }
            };
            JMXHelper.addMBeanRegistrationListener(watchedObjectName, lateComerListener, 1);
            keyAttrs.put("name", pwd);
            watchedObjectName = JMXHelper.objectName(CONFIG_DOMAIN, keyAttrs);
            if (JMXHelper.isRegistered(watchedObjectName)) {
                return watchedObjectName;
            }
            log.warn("Failed to find expected dir watched configuration \n\tfor [" + objectName
                    + "] \n\tat ObjectName [" + watchedObjectName + "]");
            throw new RuntimeException("Failed to find expected dir watched configuration for [" + objectName
                    + "] at ObjectName [" + watchedObjectName + "]");
        }
        // we're a config
        if (!shortName.equals(pwd)) {
            // we're a script config, so look for {pwd}.config
            keyAttrs.put("name", pwd);
            ObjectName watchedObjectName = JMXHelper.objectName(CONFIG_DOMAIN, keyAttrs);
            if (JMXHelper.isRegistered(watchedObjectName)) {
                return watchedObjectName;
            }
            log.warn("Failed to find expected dir watched configuration \n\tfor [" + objectName
                    + "] \n\tat ObjectName [" + watchedObjectName + "]");
            return null;
            //throw new RuntimeException("Failed to find expected dir watched configuration for [" + objectName + "] at ObjectName [" + watchedObjectName + "]");            
        }
        // we're a {pwd}.connfig, so we need to find {pwd.parent}.config
        // yank the highest d# attribute so we go up one dir
        Integer high = JMXHelper.getHighestKey(objectName, "d");
        if (high == null) {
            return null;
        }
        keyAttrs.remove("d" + JMXHelper.getHighestKey(objectName, "d"));
        if (this.rootDir.equals(sourceFile.getParentFile().getParentFile().getAbsolutePath())) {
            return null;
        }
        // update the name to the {pwd.parent}            
        keyAttrs.put("name", sourceFile.getParentFile().getParentFile().getName());
        ObjectName watchedObjectName = JMXHelper.objectName(CONFIG_DOMAIN, keyAttrs);
        if (JMXHelper.isRegistered(watchedObjectName)) {
            return watchedObjectName;
        }
        log.warn("Failed to find expected parent dir watched configuration \n\tfor [" + objectName
                + "] \n\tat ObjectName [" + watchedObjectName + "]");
        throw new RuntimeException("Failed to find expected parent dir watched configuration for [" + objectName
                + "] at ObjectName [" + watchedObjectName + "]");
    }

    /**
     * Sends a status change notification
     * @param prior The prior status
     * @param current The current status
     * @param timestamp The timestamp
     */
    protected void sendStatusChangeNotification(final DeploymentStatus prior, final DeploymentStatus current,
            final long timestamp) {
        final String msg = String.format("[%s] Status change for [%s]: from[%s] to [%s]", new Date(timestamp),
                objectName, prior, current);
        // last event msg
        sendNotification(new AttributeChangeNotification(objectName, sequence.incrementAndGet(), timestamp, msg,
                "Status", DeploymentStatus.class.getName(), prior.name(), current.name()));
    }

    /**
     * Sends a configuration change notification when the configuration of the deployment changes
     * @param jsonConfig The new configuration represented in JSON which is included as the user data in the sent notification
     * @param timestamp The timestamp
     */
    protected void sendConfigurationChangeNotification(final String jsonConfig, final long timestamp) {
        final String msg = String.format("[%s] Configuration change", new Date(timestamp));
        // last event msg      
        final Notification notif = new Notification(NOTIF_CONFIG_CHANGE, objectName, sequence.incrementAndGet(),
                timestamp, msg);
        notif.setUserData(jsonConfig);
        sendNotification(notif);
    }

    //   /**
    //    * Sends a recompilation change notification when the deployment is recompiled
    //    * @param timestamp The timestamp
    //    */
    //   protected void sendRecompilationChangeNotification(final long timestamp) {
    //      final String msg = String.format("[%s] Recompilation change", new Date(timestamp));
    //      // last event msg
    //      sendNotification(new Notification(NOTIF_RECOMPILE, objectName, sequence.incrementAndGet(), timestamp, msg));
    //   }

    protected static class ConfigFileFilter implements FilenameFilter {
        static final String ext = "config";
        String plainName = null;

        ConfigFileFilter(final String plainName) {
            this.plainName = plainName;
        }

        @Override
        public boolean accept(final File dir, final String name) {
            if (ext.equals(URLHelper.getExtension(new File(dir, name)))) {
                return (plainName != null && plainName.equals(URLHelper.getPlainFileName(name)));
            }
            return false;
        }
    }

    protected static LinkedHashSet<File> locateConfigFiles(final File sourceFile, final String rootDir,
            final String[] pathSegments) {
        final LinkedHashSet<File> configs = new LinkedHashSet<File>();
        /*
         * root config:  root.config, root.*.config
         * for each dir:  foo.config, foo.*.config, dir.config, dir.*.config
         * 
         */
        final String deplName = URLHelper.getPlainFileName(sourceFile);
        final ConfigFileFilter cff = new ConfigFileFilter("root");
        final File root = new File(rootDir);
        for (File f : root.listFiles(cff)) {
            configs.add(f);
        }
        cff.plainName = deplName;
        File subDir = root;
        for (int i = 1; i < pathSegments.length; i++) {
            subDir = new File(subDir, pathSegments[i]);
            cff.plainName = pathSegments[i];
            for (File f : subDir.listFiles(cff)) {
                configs.add(f);
            }
            cff.plainName = deplName;
            for (File f : subDir.listFiles(cff)) {
                configs.add(f);
            }
        }
        System.out
                .println(String.format("Config Files for deployment [%s] --> %s", sourceFile, configs.toString()));
        return configs;
    }

    public boolean isConfigFor(String deployment) {
        return false;
    }

    public long getChecksum() {
        return checksum;
    }

    public long getLastModified() {
        return lastModified;
    }

    public Date getLastModifiedDate() {
        return new Date(lastModified);
    }

    //================================================================================================================================
    //      MBeanRegistration default implementation
    //================================================================================================================================

    /**
     * {@inheritDoc}
     * @see javax.management.MBeanRegistration#preRegister(javax.management.MBeanServer, javax.management.ObjectName)
     */
    @Override
    public ObjectName preRegister(final MBeanServer server, final ObjectName name) throws Exception {
        return name;
    }

    /**
     * {@inheritDoc}
     * @see javax.management.MBeanRegistration#postRegister(java.lang.Boolean)
     */
    @Override
    public void postRegister(final Boolean registrationDone) {
        //this.watchedConfig.set(findWatchedConfiguration());
    }

    /**
     * {@inheritDoc}
     * @see javax.management.MBeanRegistration#preDeregister()
     */
    @Override
    public void preDeregister() throws Exception {
        /* No Op */
    }

    /**
     * {@inheritDoc}
     * @see javax.management.MBeanRegistration#postDeregister()
     */
    @Override
    public void postDeregister() {
        /* No Op */
    }

    //================================================================================================================================

}