package org.rhq.plugins.byteman;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.jboss.byteman.agent.submit.Submit;

import org.rhq.core.domain.configuration.Configuration;
import org.rhq.core.domain.configuration.ConfigurationUpdateStatus;
import org.rhq.core.domain.configuration.Property;
import org.rhq.core.domain.configuration.PropertyList;
import org.rhq.core.domain.configuration.PropertyMap;
import org.rhq.core.domain.configuration.PropertySimple;
import org.rhq.core.domain.content.PackageDetailsKey;
import org.rhq.core.domain.content.PackageType;
import org.rhq.core.domain.content.transfer.ContentResponseResult;
import org.rhq.core.domain.content.transfer.DeployIndividualPackageResponse;
import org.rhq.core.domain.content.transfer.DeployPackageStep;
import org.rhq.core.domain.content.transfer.DeployPackagesResponse;
import org.rhq.core.domain.content.transfer.RemoveIndividualPackageResponse;
import org.rhq.core.domain.content.transfer.RemovePackagesResponse;
import org.rhq.core.domain.content.transfer.ResourcePackageDetails;
import org.rhq.core.domain.measurement.AvailabilityType;
import org.rhq.core.domain.measurement.MeasurementDataNumeric;
import org.rhq.core.domain.measurement.MeasurementDataTrait;
import org.rhq.core.domain.measurement.MeasurementReport;
import org.rhq.core.domain.measurement.MeasurementScheduleRequest;
import org.rhq.core.domain.resource.CreateResourceStatus;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.pluginapi.availability.AvailabilityFacet;
import org.rhq.core.pluginapi.configuration.ConfigurationFacet;
import org.rhq.core.pluginapi.configuration.ConfigurationUpdateReport;
import org.rhq.core.pluginapi.content.ContentContext;
import org.rhq.core.pluginapi.content.ContentFacet;
import org.rhq.core.pluginapi.content.ContentServices;
import org.rhq.core.pluginapi.inventory.CreateChildResourceFacet;
import org.rhq.core.pluginapi.inventory.CreateResourceReport;
import org.rhq.core.pluginapi.inventory.ResourceComponent;
import org.rhq.core.pluginapi.inventory.ResourceContext;
import org.rhq.core.pluginapi.measurement.MeasurementFacet;
import org.rhq.core.pluginapi.operation.OperationFacet;
import org.rhq.core.pluginapi.operation.OperationResult;
import org.rhq.core.util.MessageDigestGenerator;
import org.rhq.core.util.exception.ThrowableUtil;

/**
 * Component that represents the remote Byteman agent listening for requests.
 * 
 * A note about adding boot/system classpath jars and the content this component supports related to that feature.
 * There are operations this component supports to add jars to the byteman classpath. Those operations tell the
 * byteman agent to add jars, but those jars must already exist and be accessible for the byteman agent to do so.
 * This component will not manage the jars added via the operations. If, however, a user pushes jar content
 * from the RHQ server to this plugin via the content facet, those jars will be managed by this component as they
 * are added to the byteman agent.
 * 
 * @author John Mazzitelli
 */
public class BytemanAgentComponent implements ResourceComponent<BytemanAgentComponent>, MeasurementFacet,
    OperationFacet, ContentFacet, CreateChildResourceFacet, ConfigurationFacet {

    private static final String PKG_TYPE_NAME_BOOT_JAR = "bootJar";
    private static final String PKG_TYPE_NAME_SYSTEM_JAR = "systemJar";

    private final Log log = LogFactory.getLog(BytemanAgentComponent.class);

    private ResourceContext<BytemanAgentComponent> resourceContext;
    private Submit bytemanClient;
    private File bootJarsDataDir; // where managed boot jars will be persisted
    private File systemJarsDataDir; // where managed system jars will be persisted
    private File scriptsDataDir; // where managed scripts will be persisted
    private Map<String, String> allKnownScripts; // cached copy of currently known scripts

    /**
     * Start the management component. This will immediately attempt to add previously
     * deployed classpath jars, if it is found that the remote Byteman agent no longer
     * has those jars in its classpath.
     * 
     * @see ResourceComponent#start(ResourceContext)
     */
    public void start(ResourceContext<BytemanAgentComponent> context) {
        this.resourceContext = context;
        this.bootJarsDataDir = getResourceDataDirectory("boot");
        this.systemJarsDataDir = getResourceDataDirectory("system");
        this.scriptsDataDir = getResourceDataDirectory("script");

        getBytemanClient(); // creates its client now

        // now that we are starting to manage the byteman agent, make sure the managed classpath jars are added
        try {
            addDeployedClasspathJars();
        } catch (Throwable t) {
            log.warn("Failed to add managed classpath jars to the byteman agent - is it up?", t);
        }

        getAvailability(); // forces the scripts cache to load

        return;
    }

    /**
     * Called when the resource component will no longer manage the remote Byteman agent.
     * This method will clean up the resource component.
     * 
     * @see ResourceComponent#stop()
     */
    public void stop() {
        this.resourceContext = null;
        this.bytemanClient = null;
        this.allKnownScripts = null;
    }

    /**
     * Determines if the Byteman agent is up by asking it for the current list of all scripts and their rules.
     * 
     * @see AvailabilityFacet#getAvailability()
     */
    public AvailabilityType getAvailability() {
        try {
            this.allKnownScripts = getBytemanClient().getAllScripts();
            return AvailabilityType.UP;
        } catch (Exception e) {
            this.allKnownScripts = null;
            return AvailabilityType.DOWN;
        }
    }

    /**
     * The plugin container will call this method when metrics are to be collected.
     *
     * @see MeasurementFacet#getValues(MeasurementReport, Set)
     */
    public void getValues(MeasurementReport report, Set<MeasurementScheduleRequest> requests) {
        Submit client = getBytemanClient();

        // a cache so we don't ask the byteman agent more than once for this while we are in here
        // we don't rely on this.allKnownScripts because we want the latest-n-greatest value for our metrics
        Map<String, String> allScripts = null;

        for (MeasurementScheduleRequest request : requests) {
            String name = request.getName();

            try {
                if (name.equals("TRAIT-clientVersion")) {
                    String clientVersion = client.getClientVersion();
                    report.addData(new MeasurementDataTrait(request, clientVersion));
                } else if (name.equals("totalNumberOfScripts")) {
                    int total = 0;
                    if (allScripts == null) {
                        allScripts = client.getAllScripts();
                    }
                    if (allScripts != null) {
                        total += allScripts.size();
                    }
                    report.addData(new MeasurementDataNumeric(request, Double.valueOf((double) total)));
                } else if (name.equals("totalNumberOfRules")) {
                    int total = 0;
                    if (allScripts == null) {
                        allScripts = client.getAllScripts();
                    }
                    if (allScripts != null) {
                        for (String script : allScripts.values()) {
                            total += client.splitAllRulesFromScript(script).size();
                        }
                    }
                    report.addData(new MeasurementDataNumeric(request, Double.valueOf((double) total)));
                } else if (name.equals("totalNumberOfBootJars")) {
                    int total = 0;
                    List<String> loadedJars = client.getLoadedBootClassloaderJars();
                    if (loadedJars != null) {
                        total = loadedJars.size();
                    }
                    report.addData(new MeasurementDataNumeric(request, Double.valueOf((double) total)));
                } else if (name.equals("totalNumberOfSystemJars")) {
                    int total = 0;
                    List<String> loadedJars = client.getLoadedSystemClassloaderJars();
                    if (loadedJars != null) {
                        total = loadedJars.size();
                    }
                    report.addData(new MeasurementDataNumeric(request, Double.valueOf((double) total)));
                } else {
                    throw new Exception("cannot collect unknown metric");
                }
            } catch (Exception e) {
                log.error("Failed to obtain measurement [" + name + "]. Cause: " + e);
            }
        }

        return;
    }

    public Configuration loadResourceConfiguration() throws Exception {
        Properties props = getBytemanClient().listSystemProperties();
        Configuration config = new Configuration();
        PropertyList list = new PropertyList("bytemanSystemProperties");
        for (Map.Entry<Object, Object> entry : props.entrySet()) {
            String name = entry.getKey().toString();
            if (name.startsWith("org.jboss.byteman.")) {
                PropertyMap map = new PropertyMap("bytemanSystemProperty");
                map.put(new PropertySimple("name", name));
                map.put(new PropertySimple("value", entry.getValue().toString()));
                list.add(map);
            }
        }

        if (list.getList().size() > 0) {
            config.put(list);
        }

        return config;
    }

    public void updateResourceConfiguration(ConfigurationUpdateReport report) {

        try {
            Properties propsToSet = new Properties();

            Configuration config = report.getConfiguration();
            PropertyList list = config.getList("bytemanSystemProperties");
            List<Property> maps = list.getList();
            for (Property map : maps) {
                Map<String, Property> nameValue = ((PropertyMap) map).getMap();
                String name = ((PropertySimple) nameValue.get("name")).getStringValue();
                if (name.startsWith("org.jboss.byteman.")) {
                    String value = ((PropertySimple) nameValue.get("value")).getStringValue();
                    // byteman will not allow us to turn off strict mode
                    if (!name.equals("org.jboss.byteman.sysprops.strict") || value.equals("true")) {
                        propsToSet.put(name, value);
                    }
                }
            }

            getBytemanClient().setSystemProperties(propsToSet);
            log.info("Set byteman configuration: " + propsToSet);
            report.setStatus(ConfigurationUpdateStatus.SUCCESS);
        } catch (Exception e) {
            report.setErrorMessageFromThrowable(e);
        }

        return;
    }

    /**
     * The plugin container will call this method when it wants to invoke an operation on
     * the Byteman agent.
     *
     * @see OperationFacet#invokeOperation(String, Configuration)
     */
    public OperationResult invokeOperation(String name, Configuration configuration) {

        OperationResult result = new OperationResult();
        Submit client = getBytemanClient();

        try {
            if ("getRule".equals(name)) {
                //
                // getRule == retrieves the rule definition for a given rule
                String ruleName = configuration.getSimpleValue("ruleName", null);
                if (ruleName == null || ruleName.length() == 0) {
                    throw new Exception("Did not specify the name of the rule to get");
                }
                Map<String, String> allScripts = client.getAllScripts();
                for (String script : allScripts.values()) {
                    List<String> rules = client.splitAllRulesFromScript(script);
                    for (String rule : rules) {
                        if (ruleName.equals(client.determineRuleName(rule))) {
                            Configuration resultConfig = result.getComplexResults();
                            resultConfig.put(new PropertySimple("ruleDefinition", rule));
                            return result;
                        }
                    }
                }
                throw new Exception("No rule was found with the name [" + ruleName + "]");
            } else if ("getClientVersion".equals(name)) {
                //
                // getClientVersion == return the version string of the client this plugin is using
                String clientVersion = client.getClientVersion();
                Configuration resultConfig = result.getComplexResults();
                resultConfig.put(new PropertySimple("version", (clientVersion == null) ? "<unknown>" : clientVersion));
                return result;
            } else if ("addJarsToSystemClasspath".equals(name)) {
                //
                // addJarsToSystemClasspath == adds a jar to the remote byteman agent's system classpath 
                String jarPaths = configuration.getSimpleValue("jarPathnames", null);
                if (jarPaths == null || jarPaths.length() == 0) {
                    throw new Exception("Did not specify any jars to add");
                }
                String[] jarPathsArr = jarPaths.split(",");
                List<String> jarPathList = new ArrayList<String>();
                for (String jarPathString : jarPathsArr) {
                    jarPathList.add(jarPathString);
                }
                String response = client.addJarsToSystemClassloader(jarPathList);
                result.setSimpleResult(response);
                return result;
            } else if ("addJarsToBootClasspath".equals(name)) {
                //
                // addJarsToBootClasspath == adds a jar to the remote byteman agent's boot classpath 
                String jarPaths = configuration.getSimpleValue("jarPathnames", null);
                if (jarPaths == null || jarPaths.length() == 0) {
                    throw new Exception("Did not specify any jars to add");
                }
                String[] jarPathsArr = jarPaths.split(",");
                List<String> jarPathList = new ArrayList<String>();
                for (String jarPathString : jarPathsArr) {
                    jarPathList.add(jarPathString);
                }
                String response = client.addJarsToBootClassloader(jarPathList);
                result.setSimpleResult(response);
                return result;
            } else if ("getAddedClasspathJars".equals(name)) {
                //
                // getAddedClasspathJars == gets all jars that were added to the byteman agent's boot and system classpaths 
                Configuration resultConfig = result.getComplexResults();
                List<String> jars;

                jars = client.getLoadedBootClassloaderJars();
                if (jars != null && !jars.isEmpty()) {
                    PropertyList list = new PropertyList("additionalBootClasspathJars");
                    for (String jar : jars) {
                        PropertyMap map = new PropertyMap("additionalBootClasspathJar");
                        map.put(new PropertySimple("jarPathname", jar));
                        list.add(map);
                    }
                    resultConfig.put(list);
                }

                jars = client.getLoadedSystemClassloaderJars();
                if (jars != null && !jars.isEmpty()) {
                    PropertyList list = new PropertyList("additionalSystemClasspathJars");
                    for (String jar : jars) {
                        PropertyMap map = new PropertyMap("additionalSystemClasspathJar");
                        map.put(new PropertySimple("jarPathname", jar));
                        list.add(map);
                    }
                    resultConfig.put(list);
                }

                return result;
            } else {
                throw new UnsupportedOperationException(name);
            }
        } catch (Exception e) {
            result.setErrorMessage(ThrowableUtil.getAllMessages(e));
            return result;
        }
    }

    /**
     * Detects the different content for the Byteman agent. This will only discover
     * content that was previously deployed by the RHQ content facet mechanism. In other words,
     * content already deployed in the Byteman agent, or deployed by some other non-RHQ means,
     * will not be detected.
     *
     * @see ContentFacet#discoverDeployedPackages(PackageType)
     */
    public Set<ResourcePackageDetails> discoverDeployedPackages(PackageType type) {
        Set<ResourcePackageDetails> details = new HashSet<ResourcePackageDetails>();
        String typeName = type.getName();
        try {
            File[] discoveredFiles = null; // will only be non-null if 1 or more jars are discovered
            File dataDir;

            if (PKG_TYPE_NAME_BOOT_JAR.equals(typeName)) {
                dataDir = this.bootJarsDataDir;
            } else if (PKG_TYPE_NAME_SYSTEM_JAR.equals(typeName)) {
                dataDir = this.systemJarsDataDir;
            } else {
                throw new UnsupportedOperationException("Can only deploy boot/system jars");
            }

            File[] files = dataDir.listFiles();
            if (files != null && files.length > 0) {
                discoveredFiles = files;
            }

            if (discoveredFiles != null) {
                for (File file : discoveredFiles) {
                    String shortName = file.getName();
                    String sha256 = null;
                    try {
                        sha256 = new MessageDigestGenerator(MessageDigestGenerator.SHA_256).calcDigestString(file);
                    } catch (Exception e) {
                        if (log.isDebugEnabled()) {
                            log.debug("Failed to generate sha256 for [" + file + "]");
                        }
                    }
                    String version = getVersion(file, sha256);
                    PackageDetailsKey detailsKey = new PackageDetailsKey(shortName, version, typeName, "noarch");
                    ResourcePackageDetails detail = new ResourcePackageDetails(detailsKey);
                    detail.setDisplayName(shortName);
                    detail.setFileCreatedDate(file.lastModified());
                    detail.setFileName(shortName);
                    detail.setFileSize(file.length());
                    detail.setMD5(MessageDigestGenerator.getDigestString(file));
                    detail.setSHA256(sha256);
                    details.add(detail);
                }
            }
        } catch (Exception e) {
            log.error("Failed to perform discovery for packages of type [" + typeName + "]", e);
        }
        return details;
    }

    private String getVersion(File file, String sha256) {
        // Version string in order of preference
        // manifestVersion + sha256, sha256, manifestVersion, "0"
        String version = "0";
        String manifestVersion = null;
        try {
            manifestVersion = BytemanAgentDiscoveryComponent.getJarAttribute(file.getAbsolutePath(),
                java.util.jar.Attributes.Name.IMPLEMENTATION_VERSION.toString(), null);
        } catch (Exception e) {
            if (log.isDebugEnabled()) {
                log.debug("Failed to determine manifestVersion for [" + file + "]");
            }
        }

        if ((null != manifestVersion) && (null != sha256)) {
            // this protects against the occasional differing binaries with poor manifest maintenance  
            version = manifestVersion + " [sha256=" + sha256 + "]";
        } else if (null != sha256) {
            version = "[sha256=" + sha256 + "]";
        } else if (null != manifestVersion) {
            version = manifestVersion;
        }

        return version;
    }

    /**
     * Deploys boot and system classpath jars to the Byteman agent.
     *
     * @see ContentFacet#deployPackages(Set, ContentServices)
     */
    public DeployPackagesResponse deployPackages(Set<ResourcePackageDetails> packages, ContentServices contentServices) {
        Submit client = getBytemanClient();
        DeployPackagesResponse response = new DeployPackagesResponse();
        DeployIndividualPackageResponse individualResponse;

        for (ResourcePackageDetails detail : packages) {
            PackageDetailsKey packageKey = detail.getKey();
            String packageType = detail.getPackageTypeName();

            individualResponse = new DeployIndividualPackageResponse(packageKey);
            response.addPackageResponse(individualResponse);

            try {
                Boolean isBootJar = null; // null if not a classpath jar (this is for the future; today, it will always get set)
                // if necessary, create the data directory where the file should be deployed
                if (PKG_TYPE_NAME_BOOT_JAR.equals(packageType)) {
                    this.bootJarsDataDir.mkdirs();
                    isBootJar = Boolean.TRUE;
                } else if (PKG_TYPE_NAME_SYSTEM_JAR.equals(packageType)) {
                    this.systemJarsDataDir.mkdirs();
                    isBootJar = Boolean.FALSE;
                } else {
                    throw new UnsupportedOperationException("Cannot deploy package of type [" + packageType + "]");
                }

                // download the package to our data directory
                File newFile = getPackageFile(detail);
                FileOutputStream fos = new FileOutputStream(newFile);
                try {
                    ContentContext contentContext = this.resourceContext.getContentContext();
                    contentServices.downloadPackageBits(contentContext, packageKey, fos, true);
                } finally {
                    fos.close();
                }

                // tell the byteman agent to add it to the proper classloader
                if (isBootJar != null) {
                    if (isBootJar.booleanValue()) {
                        client.addJarsToBootClassloader(Arrays.asList(newFile.getAbsolutePath()));
                    } else {
                        client.addJarsToSystemClassloader(Arrays.asList(newFile.getAbsolutePath()));
                    }
                }

                // everything is OK
                individualResponse.setResult(ContentResponseResult.SUCCESS);
            } catch (Exception e) {
                individualResponse.setErrorMessage(ThrowableUtil.getStackAsString(e));
                individualResponse.setResult(ContentResponseResult.FAILURE);
            }
        }

        return response;
    }

    /**
     * When a remote client wants to see the actual data content for an installed package, this method will be called.
     * This method must return a stream of data containing the full content of the package.
     *
     * @see ContentFacet#retrievePackageBits(ResourcePackageDetails)
     */
    public InputStream retrievePackageBits(ResourcePackageDetails packageDetails) {
        try {
            File file = getPackageFile(packageDetails);
            return new FileInputStream(file);
        } catch (Exception e) {
            log.error("Cannot retrieve content for package [" + packageDetails + "]");
            throw new RuntimeException(e);
        }
    }

    /**
     * Essentially a no-op - there are no installation steps associated with Byteman content.
     */
    public List<DeployPackageStep> generateInstallationSteps(ResourcePackageDetails packageDetails) {
        return new ArrayList<DeployPackageStep>(0);
    }

    /**
     * Removes the packages, if they are managed by this component. Note that the Byteman agent does not
     * support runtime removal of jars from its classpaths, so the Byteman agent will retain classpath
     * jars in its memory until the VM is restarted, even if this component was asked to remove
     * classpath jars.
     *
     * @see ContentFacet#removePackages(Set)
     */
    public RemovePackagesResponse removePackages(Set<ResourcePackageDetails> packages) {
        RemovePackagesResponse response = new RemovePackagesResponse();
        RemoveIndividualPackageResponse individualResponse;

        for (ResourcePackageDetails detail : packages) {
            individualResponse = new RemoveIndividualPackageResponse(detail.getKey());
            response.addPackageResponse(individualResponse);
            try {
                File packageFile = getPackageFile(detail);
                if (packageFile.delete()) {
                    individualResponse.setResult(ContentResponseResult.SUCCESS);
                } else {
                    individualResponse.setErrorMessage("Failed to delete [" + packageFile.getAbsolutePath() + "]");
                    individualResponse.setResult(ContentResponseResult.FAILURE);
                }
            } catch (Exception e) {
                individualResponse.setErrorMessage(ThrowableUtil.getStackAsString(e));
                individualResponse.setResult(ContentResponseResult.FAILURE);
            }
        }

        return response;
    }

    /**
     * Creates a new script by deploying the script file to the Byteman agent.
     *
     * @see CreateChildResourceFacet#createResource(CreateResourceReport)
     */
    public CreateResourceReport createResource(CreateResourceReport report) {
        try {
            this.scriptsDataDir.mkdirs();

            // determine where to store the script file when we download it;
            // do not allow the file to be placed in a subdirectory under our data dir (i.e. take out file separators)
            ResourcePackageDetails newDetails = report.getPackageDetails();
            String newName = report.getUserSpecifiedResourceName();
            if (newName == null) {
                newName = newDetails.getName();
                if (newName == null) {
                    throw new NullPointerException("was not given a name for the new script");
                }
            }
            newName = newName.replace('/', '-').replace('\\', '-');

            File newFile = new File(this.scriptsDataDir, newName);
            String newFileAbsolutePath = newFile.getAbsolutePath();

            // download the file from the server
            ContentContext contentContext = this.resourceContext.getContentContext();
            ContentServices contentServices = contentContext.getContentServices();
            ResourceType newChildResourceType = report.getResourceType();
            FileOutputStream fos = new FileOutputStream(newFile);
            BufferedOutputStream outputStream = new BufferedOutputStream(fos);
            try {
                contentServices.downloadPackageBitsForChildResource(contentContext, newChildResourceType.getName(),
                    newDetails.getKey(), outputStream);
            } finally {
                outputStream.close();
            }

            // deploy the scripts rules in byteman agent
            getBytemanClient().addRulesFromFiles(Arrays.asList(newFileAbsolutePath));

            // we know where we put the file, fill in the details
            newDetails.setDisplayName(newName);
            newDetails.setFileName(newFileAbsolutePath);
            newDetails.setFileSize(newFile.length());
            newDetails.setInstallationTimestamp(newFile.lastModified());
            newDetails.setMD5(MessageDigestGenerator.getDigestString(newFile));

            // complete the report
            report.setResourceKey(newFileAbsolutePath);
            report.setResourceName(newName);
            report.setStatus(CreateResourceStatus.SUCCESS);
        } catch (Throwable t) {
            log.error("Failed to create child resource [" + report + "]", t);
            report.setException(t);
            report.setStatus(CreateResourceStatus.FAILURE);
        }
        return report;
    }

    /**
     * Returns a client that can be used to talk to the remote Byteman agent.
     * 
     * @return client object
     */
    public Submit getBytemanClient() {
        if (this.bytemanClient == null) {
            Configuration pc = this.resourceContext.getPluginConfiguration();

            // get the address/port from the plugin config - defaults will be null to force NPEs which is OK, because nulls are error conditions
            String address = pc.getSimpleValue(BytemanAgentDiscoveryComponent.PLUGIN_CONFIG_PROP_ADDRESS, null);
            String port = pc.getSimpleValue(BytemanAgentDiscoveryComponent.PLUGIN_CONFIG_PROP_PORT, null);

            this.bytemanClient = new Submit(address, Integer.valueOf(port).intValue());
        }

        return this.bytemanClient;
    }

    /**
     * Returns a cached copy of all known scripts since the last availability check was made.
     * Use this if you do not need the most up-to-date list, which helps avoid making unnecessary
     * calls to the remote Byteman agent. If you need the most up-to-date data, call the agent
     * using {@link #getBytemanClient() the client}.
     * 
     * @return the last known set of scripts that were loaded in the remote Byteman agent. <code>null</code>
     *         if a problem occurred attempting to get the scripts
     */
    public Map<String, String> getAllKnownScripts() {
        // if we already have a non-null value, use it as-is; otherwise, try to get it now
        if (this.allKnownScripts == null) {
            try {
                this.allKnownScripts = getBytemanClient().getAllScripts();
            } catch (Exception ignore) {
            }
        }
        return this.allKnownScripts;
    }

    /**
     * Given a package details, this will attempt to find that package's file.
     * The details "file name" is examined first to figure out where the file is supposed to be.
     * Only if that isn't set will the details general "name" be used as the file name.
     * If the "file name" (or "name") is not absolute, it will be assumed to be in one
     * of the subdirectories under this component's data directory, based on the package type name.
     * 
     * @param packageDetails details describing the file
     * @return the file that corresponds to the details object - this file may or may not exist;
     *         existence is not a requirement for this method to return a valid File object
     *
     * @throws Exception if the file could not be determined
     */
    public File getPackageFile(ResourcePackageDetails packageDetails) throws Exception {
        String path = packageDetails.getFileName();
        if (path == null) {
            path = packageDetails.getName(); // if no filename was given, assume the package name is the path
        }

        File file = new File(path);
        if (!file.isAbsolute()) {
            String typeName = packageDetails.getPackageTypeName();
            if (PKG_TYPE_NAME_BOOT_JAR.equals(typeName)) {
                file = new File(this.bootJarsDataDir, path);
            } else if (PKG_TYPE_NAME_SYSTEM_JAR.equals(typeName)) {
                file = new File(this.systemJarsDataDir, path);
            } else if (BytemanScriptComponent.PKG_TYPE_NAME_SCRIPT.equals(typeName)) {
                file = new File(this.scriptsDataDir, path);
            } else {
                throw new Exception("Invalid package type - cannot get package file");
            }
        }
        return file;
    }

    /**
     * @return directory where managed boot classpath jars are persisted
     */
    public File getBootJarsDataDirectory() {
        return this.bootJarsDataDir;
    }

    /**
     * @return directory where managed system classpath jars are persisted
     */
    public File getSystemJarsDataDirectory() {
        return this.systemJarsDataDir;
    }

    /**
     * @return directory where managed scripts are persisted. Scripts are files
     * that contain rules.
     */
    public File getScriptsDataDirectory() {
        return this.scriptsDataDir;
    }

    /**
     * Returns the component's data directory that is used to persist managed content.
     * <code>suffix</code> is the last part of the file path, essentially providing a specific
     * location for different kinds of content for the component.
     *  
     * @param suffix identifies a specific location under a general data directory for this component.
     * @return data directory that can be used to persist data for this component
     */
    public File getResourceDataDirectory(String suffix) {
        File pluginDataDir = this.resourceContext.getDataDirectory();
        File resourceDataDir = new File(pluginDataDir, this.resourceContext.getResourceKey().replace(":", "-"));
        if (suffix != null) {
            resourceDataDir = new File(resourceDataDir, suffix);
        }
        return resourceDataDir;
    }

    /**
     * Goes through all jars that were deployed via RHQ and ensures they are still deployed, adding
     * them if need be.
     * 
     * @throws Exception
     */
    protected void addDeployedClasspathJars() throws Exception {
        Submit client = getBytemanClient();
        List<String> paths = new ArrayList<String>();

        // do the boot jars first
        File dataDir = this.bootJarsDataDir;
        File[] files = dataDir.listFiles();
        if (files != null && files.length > 0) {
            List<String> loadedJars = client.getLoadedBootClassloaderJars();
            for (File file : files) {
                if (!loadedJars.contains(file.getAbsolutePath())) {
                    paths.add(file.getAbsolutePath());
                }
            }
            client.addJarsToBootClassloader(paths);
        }

        // now do the system jars
        paths.clear();
        dataDir = this.systemJarsDataDir;
        files = dataDir.listFiles();
        if (files != null && files.length > 0) {
            List<String> loadedJars = client.getLoadedSystemClassloaderJars();
            for (File file : files) {
                if (!loadedJars.contains(file.getAbsolutePath())) {
                    paths.add(file.getAbsolutePath());
                }
            }
            client.addJarsToSystemClassloader(paths);
        }

        return;
    }
}
