/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License, version 2, as
 * published by the Free Software Foundation, and/or the GNU Lesser
 * General Public License, version 2.1, also as published by the Free
 * Software Foundation.
 *
 * This program 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 General Public License and the GNU Lesser General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License
 * and the GNU Lesser General Public License along with this program;
 * if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.rhq.core.pc.content;

import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

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

import org.rhq.core.clientapi.agent.PluginContainerException;
import org.rhq.core.clientapi.agent.content.ContentAgentService;
import org.rhq.core.clientapi.server.content.ContentDiscoveryReport;
import org.rhq.core.clientapi.server.content.ContentServerService;
import org.rhq.core.clientapi.server.content.DeletePackagesRequest;
import org.rhq.core.clientapi.server.content.DeployPackagesRequest;
import org.rhq.core.clientapi.server.content.RetrievePackageBitsRequest;
import org.rhq.core.domain.content.PackageDetailsKey;
import org.rhq.core.domain.content.PackageType;
import org.rhq.core.domain.content.composite.PackageVersionMetadataComposite;
import org.rhq.core.domain.content.transfer.DeployPackageStep;
import org.rhq.core.domain.content.transfer.DeployPackagesResponse;
import org.rhq.core.domain.content.transfer.RemovePackagesResponse;
import org.rhq.core.domain.content.transfer.ResourcePackageDetails;
import org.rhq.core.domain.resource.Resource;
import org.rhq.core.domain.resource.ResourceType;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;
import org.rhq.core.pc.ContainerService;
import org.rhq.core.pc.PluginContainer;
import org.rhq.core.pc.PluginContainerConfiguration;
import org.rhq.core.pc.ServerServices;
import org.rhq.core.pc.agent.AgentService;
import org.rhq.core.pc.inventory.InventoryEventListener;
import org.rhq.core.pc.inventory.InventoryManager;
import org.rhq.core.pc.inventory.ResourceContainer;
import org.rhq.core.pc.util.ComponentUtil;
import org.rhq.core.pc.util.FacetLockType;
import org.rhq.core.pc.util.LoggingThreadFactory;
import org.rhq.core.pluginapi.content.ContentContext;
import org.rhq.core.pluginapi.content.ContentFacet;
import org.rhq.core.pluginapi.content.ContentServices;

public class ContentManager extends AgentService implements ContainerService, ContentAgentService, ContentServices {

    private static final int FACET_METHOD_TIMEOUT = 60 * 60 * 1000; // 60 minutes
    private final Log log = LogFactory.getLog(ContentManager.class);

    /**
     * Configuration elements for the running of this manager.
     */
    private PluginContainerConfiguration configuration;

    /**
     * Flag indicating whether or not this instance of the manager should run automatic, scheduled discoveries.
     */
    private boolean scheduledDiscoveriesEnabled;

    /**
     * Executor used in running discoveries.
     */
    private ScheduledThreadPoolExecutor discoveryThreadPoolExecutor;

    /**
     * Executor used for CRUD operations on content.
     */
    private ExecutorService crudExecutor;

    /**
     * Manages the scheduled discoveries, keeping them ordered on next execution time.
     */
    private final Queue<ScheduledContentDiscoveryInfo> scheduledDiscoveries = new PriorityQueue<ScheduledContentDiscoveryInfo>();

    /**
     * Event listener to receive notifications of changes to the inventory.
     */
    private ContentInventoryEventListener inventoryEventListener;

    public ContentManager() {
        super(ContentAgentService.class);
    }

    public void initialize() {
        log.info("Initializing Content Manager...");

        // Determine discovery mode - we only enable discovery if we are inside the agent and the period is positive non-zero
        this.scheduledDiscoveriesEnabled = (configuration.getContentDiscoveryPeriod() > 0);

        // Create thread pool executor. Used in both scheduled and non-scheduled mode for all discoveries.
        int threadPoolSize = configuration.getContentDiscoveryThreadPoolSize();

        discoveryThreadPoolExecutor = new ScheduledThreadPoolExecutor(threadPoolSize, new LoggingThreadFactory(
            "Content.discovery", true));

        discoveryThreadPoolExecutor.setContinueExistingPeriodicTasksAfterShutdownPolicy(false);
        discoveryThreadPoolExecutor.setExecuteExistingDelayedTasksAfterShutdownPolicy(false);

        crudExecutor = new ThreadPoolExecutor(1, 5, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(10000),
            new LoggingThreadFactory("Content.crud", true));

        // When running in scheduled mode, create and schedule the thread pool for discovering content
        if (scheduledDiscoveriesEnabled) {
            log.info("Initializing scheduled content discovery...");

            // Without specifying a particular piece of work, this runner will request the next piece of work from
            // the scheduled items queue
            ContentDiscoveryRunner runner = new ContentDiscoveryRunner(this);

            // Begin the automatic discovery thread
            long initialDelay = configuration.getContentDiscoveryInitialDelay();
            long discoveryPeriod = configuration.getContentDiscoveryPeriod();

            discoveryThreadPoolExecutor.scheduleAtFixedRate(runner, initialDelay, discoveryPeriod, TimeUnit.SECONDS);

            // Add inventory event listener so we can keep the scheduled discoveries consistent with the resources
            inventoryEventListener = new ContentInventoryEventListener();

            // the inventory manager has probably already activated some resources, so let's prepopulate our schedules
            InventoryManager im = PluginContainer.getInstance().getInventoryManager();
            im.notifyForAllActivatedResources(inventoryEventListener);

            // now ask that the inventory manager tell us about resources that will be activated in the future
            im.addInventoryEventListener(inventoryEventListener);
        }
        log.info("Content Manager initialized...");
    }

    public void shutdown() {
        log.info("Shutting down Content Manager...");
        discoveryThreadPoolExecutor.shutdown();
        crudExecutor.shutdown();
        PluginContainer.getInstance().getInventoryManager().removeInventoryEventListener(inventoryEventListener);
    }

    public void setConfiguration(PluginContainerConfiguration configuration) {
        this.configuration = configuration;
    }

    public Set<ResourcePackageDetails> getLastDiscoveredResourcePackages(int resourceId) {
        // Get the resource component
        InventoryManager inventoryManager = PluginContainer.getInstance().getInventoryManager();
        ResourceContainer container = inventoryManager.getResourceContainer(resourceId);

        // Nothing to do if the container doesn't exist or isn't running, so punch out
        if ((container == null)
            || (ResourceContainer.ResourceComponentState.STARTED != container.getResourceComponentState())) {
            throw new RuntimeException("Container is non-existent or is not running for resource id [" + resourceId
                + "]");
        }

        return container.getInstalledPackages();
    }

    public ContentDiscoveryReport executeResourcePackageDiscoveryImmediately(int resourceId, String packageTypeName)
        throws PluginContainerException {
        // Load the package type object
        PackageType packageType = findPackageType(resourceId, packageTypeName);
        if (packageType == null) {
            throw new PluginContainerException("Could not load package type [" + packageTypeName + "] for resource: "
                + resourceId);
        }

        // Create a new runner that is scoped to the resource/package type specified
        ScheduledContentDiscoveryInfo discoveryInfo = new ScheduledContentDiscoveryInfo(resourceId, packageType);

        ContentDiscoveryRunner oneTimeRunner = new ContentDiscoveryRunner(this, discoveryInfo);

        ContentDiscoveryReport results;
        try {
            results = discoveryThreadPoolExecutor.submit((Callable<ContentDiscoveryReport>) oneTimeRunner).get();
        } catch (Exception e) {
            throw new PluginContainerException("Exception occurred during execution of discovery", e);
        }

        return results;
    }

    public void deployPackages(DeployPackagesRequest request) {
        Runnable runner = new CreateContentRunner(this, request);
        crudExecutor.submit(runner);
    }

    public DeployPackagesResponse deployPackagesImmediately(DeployPackagesRequest request)
        throws PluginContainerException {
        Callable<DeployPackagesResponse> runner = new CreateContentRunner(this, request);
        try {
            return crudExecutor.submit(runner).get();
        } catch (Exception e) {
            throw new PluginContainerException("Error during deployment of packages. request: " + request, e);
        }
    }

    public void deletePackages(DeletePackagesRequest request) {
        DeleteContentRunner runner = new DeleteContentRunner(this, request);
        crudExecutor.submit(runner);
    }

    public void retrievePackageBits(RetrievePackageBitsRequest request) {
        RetrieveContentBitsRunner runner = new RetrieveContentBitsRunner(this, request);
        crudExecutor.submit(runner);
    }

    public List<DeployPackageStep> translateInstallationSteps(int resourceId, ResourcePackageDetails packageDetails)
        throws PluginContainerException {
        List<DeployPackageStep> steps;
        try {
            ContentFacet contentFacet = findContentFacet(resourceId);
            steps = contentFacet.generateInstallationSteps(packageDetails);
        } catch (Exception e) {
            throw new PluginContainerException("Error translating the package installation steps", e);
        }

        return steps;
    }

    // ContentServices Implementation  --------------------------------------------

    public long downloadPackageBitsForChildResource(ContentContext context, String childResourceTypeName,
        PackageDetailsKey key, OutputStream outputStream) {

        ContentContextImpl contextImpl = (ContentContextImpl) context;
        ContentServerService serverService = getContentServerService();
        outputStream = remoteOutputStream(outputStream);

        long count = serverService.downloadPackageBitsForChildResource(contextImpl.getResourceId(),
            childResourceTypeName, key, outputStream);

        return count;
    }

    public long downloadPackageBits(ContentContext context, PackageDetailsKey packageDetailsKey,
        OutputStream outputStream, boolean resourceExists) {
        ContentContextImpl contextImpl = (ContentContextImpl) context; // this has to be of this type, we gave it to the plugin
        ContentServerService serverService = getContentServerService();

        // we need to load the content to server before we will start download the content
        // it is because of timeout on remoteStreams
        serverService.preLoadRemoteContent(contextImpl.getResourceId(), packageDetailsKey);

        outputStream = remoteOutputStream(outputStream);
        long count = 0;
        if (resourceExists) {
            count = serverService.downloadPackageBitsGivenResource(contextImpl.getResourceId(), packageDetailsKey,
                outputStream);
        } else {
            // TODO: Figure out how to support this; the APIs require the resource to get the bits
        }
        return count;
    }

    public long downloadPackageBitsRange(ContentContext context, PackageDetailsKey packageDetailsKey,
        OutputStream outputStream, long startByte, long endByte, boolean resourceExists) {
        ContentContextImpl contextImpl = (ContentContextImpl) context; // this has to be of this type, we gave it to the plugin
        ContentServerService serverService = getContentServerService();
        outputStream = remoteOutputStream(outputStream);
        long count = 0;
        if (resourceExists) {
            count = serverService.downloadPackageBitsRangeGivenResource(contextImpl.getResourceId(), packageDetailsKey,
                outputStream, startByte, endByte);
        } else {
            // TODO: Figure out how to support this; the APIs require the resource to get the bits
        }
        return count;
    }

    public long getPackageBitsLength(ContentContext context, PackageDetailsKey packageDetailsKey) {
        ContentContextImpl contextImpl = (ContentContextImpl) context; // this has to be of this type, we gave it to the plugin
        ContentServerService serverService = getContentServerService();
        long size = serverService.getPackageBitsLength(contextImpl.getResourceId(), packageDetailsKey);
        return size;
    }

    public PageList<PackageVersionMetadataComposite> getPackageVersionMetadata(ContentContext context, PageControl pc) {
        ContentContextImpl contextImpl = (ContentContextImpl) context; // this has to be of this type, we gave it to the plugin
        ContentServerService serverService = getContentServerService();
        PageList<PackageVersionMetadataComposite> metadata = serverService.getPackageVersionMetadata(contextImpl
            .getResourceId(), pc);
        return metadata;
    }

    public String getResourceSubscriptionMD5(ContentContext context) {
        ContentContextImpl contextImpl = (ContentContextImpl) context; // this has to be of this type, we gave it to the plugin
        ContentServerService serverService = getContentServerService();
        String metadataMD5 = serverService.getResourceSubscriptionMD5(contextImpl.getResourceId());
        return metadataMD5;
    }

    // Package  --------------------------------------------

    /**
     * Returns the next content discovery to take place. This method will check to ensure the discovery can occur; its
     * next discovery time is not scheduled for the future.
     *
     * @return information needed to trigger a discovery; <code>null</code> if no discoveries are necessary.
     */
    synchronized ScheduledContentDiscoveryInfo getNextScheduledDiscovery() {
        // Check to see if the current time has passed the next discovery time
        ScheduledContentDiscoveryInfo next = scheduledDiscoveries.peek();

        if ((next == null) || (next.getNextDiscovery() > System.currentTimeMillis())) {
            return null;
        } else {
            return scheduledDiscoveries.poll();
        }
    }

    /**
     * Sets the next discovery time for the specified discovery.
     *
     * @param discoveryInfo discovery being rescheduled; cannot be <code>null</code>.
     */
    synchronized void rescheduleDiscovery(ScheduledContentDiscoveryInfo discoveryInfo) {
        // Sanity check
        if (!scheduledDiscoveriesEnabled) {
            log.warn("An attempt was made to reschedule a content discovery "
                + "while not running in scheduled discovery mode - returning...");
            return;
        }

        /* This is to prevent a race condition where the resource associated with this item is removed while this
         * discovery is being run. In such a case, this item cannot be removed from the queue (as it is out of the queue
         * while running) but will still be added at the end of the this call.
         */

        // Make sure the resource still exists, otherwise don't bother rescheduling
        ResourceContainer resourceContainer = PluginContainer.getInstance().getInventoryManager().getResourceContainer(
            discoveryInfo.getResourceId());

        if (resourceContainer != null) {
            boolean debugEnabled = log.isDebugEnabled();

            if (discoveryInfo.getInterval() > 0) {
                if (debugEnabled) {
                    log.debug("Rescheduling [" + discoveryInfo + "]...");
                }
                discoveryInfo.setNextDiscovery(System.currentTimeMillis() + discoveryInfo.getInterval());
                addToQueue(discoveryInfo);
                if (debugEnabled) {
                    log.debug("Finished rescheduling: " + discoveryInfo);
                }
            } else {
                if (debugEnabled) {
                    log.debug("Will not reschedule content discovery: " + discoveryInfo);
                }
            }
        }
    }

    /**
     * Unschedules any discoveries that are currently in the queue to be executed against the specified resource.
     *
     * @param resource resource whose discoveries to remove; cannot be <code>null</code>
     */
    synchronized void unscheduleDiscoveries(Resource resource) {
        if (log.isDebugEnabled()) {
            log.debug("Unscheduling content discoveries for resource id [" + resource + ']');
        }

        // Find all scheduled items for this resource
        Set<ScheduledContentDiscoveryInfo> unscheduleUs = new HashSet<ScheduledContentDiscoveryInfo>();
        for (ScheduledContentDiscoveryInfo scheduledItem : scheduledDiscoveries) {
            if (scheduledItem.getResourceId() == resource.getId()) {
                unscheduleUs.add(scheduledItem);
            }
        }

        // Remove all matching items from the queue
        for (ScheduledContentDiscoveryInfo removeMe : unscheduleUs) {
            scheduledDiscoveries.remove(removeMe);
        }
    }

    /**
     * Performs a content discovery for the provided type against the resource.
     *
     * @param  resourceId resource whose content are being discovered
     * @param  type       type of content to discover
     *
     * @return content that were discovered by this discovery
     *
     * @throws Exception if the plugin is incorrectly configured or throws an error while attempting discovery
     */
    ContentDiscoveryReport performContentDiscovery(int resourceId, PackageType type) throws Exception {
        // Perform the discovery
        // Use only a read-locked component proxy
        ContentFacet contentFacet = ComponentUtil.getComponent(resourceId, ContentFacet.class, FacetLockType.READ,
            FACET_METHOD_TIMEOUT, false, true);

        Set<ResourcePackageDetails> details = contentFacet.discoverDeployedPackages(type);

        if (log.isDebugEnabled()) {
            log.debug("Discovered [" + ((details != null) ? details.size() : 0) + "] packages of type=" + type);
        }

        // Process the results
        ContentDiscoveryReport report = handleDiscoveredContent(details, resourceId);
        return report;
    }

    /**
     * Performs a call to the ContentFacet to create a new package with the specified details.
     *
     * @param  resourceId       resource against which the content will be created
     * @param  packagesToDeploy describes the packages being deployed to the resource
     *
     * @return response from the facet
     *
     * @throws Exception                if the plugin throws an error while creating the content
     * @throws PluginContainerException if there is an error in the plugin container gathering the required data to
     *                                  perform the create
     */
    DeployPackagesResponse performPackageDeployment(int resourceId, Set<ResourcePackageDetails> packagesToDeploy)
        throws Exception {
        // Perform the create
        ContentFacet contentFacet = findContentFacet(resourceId);
        DeployPackagesResponse response = contentFacet.deployPackages(packagesToDeploy, this);

        return response;
    }

    /**
     * Performs a call to the <code>ContentFacet</code> to have the plugin delete the specified resource.
     *
     * @param  resourceId       resource in which the content exists
     * @param  packagesToDelete describes the packages being deleted from the resource
     *
     * @return response object from the facet
     *
     * @throws Exception if the plugin throws an error while trying to delete the content
     */
    RemovePackagesResponse performPackageDelete(int resourceId, Set<ResourcePackageDetails> packagesToDelete)
        throws Exception {
        // Perform the delete
        ContentFacet contentFacet = findContentFacet(resourceId);
        RemovePackagesResponse response = contentFacet.removePackages(packagesToDelete);

        return response;
    }

    InputStream performGetPackageBits(int resourceId, ResourcePackageDetails packageToRetrieve) throws Exception {
        // Perform the retrieval
        ContentFacet contentFacet = findContentFacet(resourceId);
        InputStream contentStream = contentFacet.retrievePackageBits(packageToRetrieve);

        // Wrap the content stream for sending to the original ArtifactAgentService caller
        // There is no need to check for agent mode here; the method call will wrap appropriately
        contentStream = remoteInputStream(contentStream);

        return contentStream;
    }

    /**
     * Returns the server handle to use to complete requests.
     *
     * @return server service implementation if one is registered; <code>null</code> otherwise
     */
    ContentServerService getContentServerService() {
        ContentServerService serverService = null;
        ServerServices serverServices = configuration.getServerServices();
        if (serverServices != null) {
            serverService = serverServices.getContentServerService();
        }
        return serverService;
    }

    // Private  --------------------------------------------

    /**
     * Schedules any necessary discoveries for the specified resource.
     * This is called when a resource is newly activated - as when a plugin configuration change is made.
     * If a schedule is already in place, it will remain.
     * 
     * @param resource resource for which discoveries are being scheduled
     */
    private synchronized void scheduleDiscoveries(Resource resource) {
        // Sanity check
        if (!scheduledDiscoveriesEnabled) {
            log.warn("Attempting to schedule a discovery for a resource while not running in scheduled discovery mode");
            return;
        }

        ResourceType resourceType = resource.getResourceType();
        Set<PackageType> packageTypes = resourceType.getPackageTypes();

        if ((packageTypes != null) && (packageTypes.size() > 0)) {

            int resourceId = resource.getId();

            // Check the queue to make sure we haven't already scheduled anything for this resource.
            // If a schedule already exists, we'll remove it so we reschedule it to trigger soon.
            Iterator<ScheduledContentDiscoveryInfo> iterator = scheduledDiscoveries.iterator();
            while (iterator.hasNext()) {
                ScheduledContentDiscoveryInfo contentDiscoveryInfo = iterator.next();
                if (contentDiscoveryInfo.getResourceId() == resourceId) {
                    if (log.isDebugEnabled()) {
                        log.debug("Already found scheduled content discovery for resource id [" + resourceId
                            + "], package type=[" + contentDiscoveryInfo.getPackageType()
                            + "]. Will reschedule to be triggered soon.");
                    }
                    iterator.remove();
                }
            }

            if (log.isDebugEnabled()) {
                log.debug("Scheduling [" + packageTypes.size() + "] content discoveries for resource id [" + resourceId
                    + "]");
            }

            // Add discovery items for each type. Since intervals will vary per type, create separate scheduled
            // items for each package type that can be found for this resource
            for (PackageType type : packageTypes) {
                ScheduledContentDiscoveryInfo contentDiscovery;
                contentDiscovery = new ScheduledContentDiscoveryInfo(resourceId, type);
                contentDiscovery.setNextDiscovery(System.currentTimeMillis()); // schedule the first one as soon as possible
                addToQueue(contentDiscovery);
            }
        }

        return;
    }

    /**
     * Adds a new scheduled item to the queue, taking care to synchronize on the queue and not face concurrency issues.
     * This will only add the item if we are running in the agent mode (and use the automatic scheduler)
     *
     * @param item new item to add to the queue
     */
    private synchronized void addToQueue(ScheduledContentDiscoveryInfo item) {
        // Make sure we're in scheduled mode before adding to queue
        if (scheduledDiscoveriesEnabled) {
            scheduledDiscoveries.offer(item);
        } else {
            log.warn("Attempting to add a scheduled item to the queue when not running in scheduled mode: " + item);
        }
    }

    /**
     * Handles the results received from the call to the facet to discover content. See
     * {@link ContentFacet#discoverDeployedPackages(org.rhq.core.domain.content.PackageType)}.
     *
     * @param  details    description of content that was returned from the facet
     * @param  resourceId resource against which the content were found
     *
     * @return domain model representation of the details specified
     *
     * @throws Exception if there is an error from any subsequent calls made to the facet
     */
    private ContentDiscoveryReport handleDiscoveredContent(Set<ResourcePackageDetails> details, int resourceId)
        throws Exception {
        // The plugin should at least return an empty set, but check for null too.
        if (details == null) {
            return null;
        }

        InventoryManager inventoryManager = PluginContainer.getInstance().getInventoryManager();
        ResourceContainer container = inventoryManager.getResourceContainer(resourceId);

        Set<ResourcePackageDetails> updatedPackageSet = new HashSet<ResourcePackageDetails>(details);
        Set<ResourcePackageDetails> existingInstalledPackagesSet = container.getInstalledPackages();
        if (existingInstalledPackagesSet == null) {
            existingInstalledPackagesSet = new HashSet<ResourcePackageDetails>();
        }

        // Strip out content that have been removed (i.e. not returned on the latest discovery)
        int originalPackageCount = existingInstalledPackagesSet.size();
        existingInstalledPackagesSet.retainAll(updatedPackageSet);
        int removedPackagesCount = originalPackageCount - existingInstalledPackagesSet.size();
        if (removedPackagesCount > 0) {
            if (log.isDebugEnabled()) {
                log.debug("Removed [" + removedPackagesCount + "] obsolete packages for resource id [" + resourceId
                    + "]");
            }
        }

        // Strip from updated list content that are already known for the resource, we don't need to do anything
        updatedPackageSet.removeAll(existingInstalledPackagesSet);

        // Remaining content in updated list are "new" content
        if (!updatedPackageSet.isEmpty()) {
            if (log.isDebugEnabled()) {
                log.debug("Found [" + updatedPackageSet.size() + "] new packages for resource id [" + resourceId + "]");
            }
        }

        // Add new content (yes, existingInstalledPackagesSet is same as details, but use the container's reference)
        existingInstalledPackagesSet.addAll(updatedPackageSet);

        // Add merged (current) list to the resource container
        container.setInstalledPackages(existingInstalledPackagesSet);

        // Package and send to server
        ContentDiscoveryReport report = new ContentDiscoveryReport();
        report.addAllDeployedPackages(existingInstalledPackagesSet);
        report.setResourceId(resourceId);

        ContentServerService contentServerService = getContentServerService();
        if (contentServerService != null) {
            // if there are 1+ installed packages to report OR there are 0 but there used to be packages installed,
            // then send up the report to be merged
            if (!existingInstalledPackagesSet.isEmpty() || originalPackageCount != 0) {
                if (log.isDebugEnabled()) {
                    log.debug("Merging [" + existingInstalledPackagesSet.size()
                        + "] discovered packages for resource id [" + resourceId + "] with Server");
                }
                contentServerService.mergeDiscoveredPackages(report);
            }
        }

        return report;
    }

    /**
     * Returns the <code>ContentFacet</code> for the component associated with the specified resource ID.
     *
     * @param  resourceId resource whose facet is being found
     *
     * @return <code>ContentFacet</code> to contact for the specified resource ID; this will never be <code>null</code>.
     *
     * @throws Exception if the <code>ContentFacet</code> cannot be retrieved
     */
    private ContentFacet findContentFacet(int resourceId) throws Exception {
        // in case some calls to here need only the read lock - for now, always lock down with the write lock
        return ComponentUtil.getComponent(resourceId, ContentFacet.class, FacetLockType.WRITE, FACET_METHOD_TIMEOUT,
            false, true);
    }

    /**
     * Finds a package type defined in the resource type of the specified resource.
     *
     * @param  resourceId      resource whose definition will be checked for the type
     * @param  packageTypeName name of the type being retrieved
     *
     * @return type instance if one is found for the specified name; <code>null</code> otherwise
     *
     * @throws PluginContainerException if the resource id is invalid
     */
    private PackageType findPackageType(int resourceId, String packageTypeName) throws PluginContainerException {
        ResourceType resourceType = ComponentUtil.getResourceType(resourceId);
        for (PackageType type : resourceType.getPackageTypes()) {
            if (type.getName().equals(packageTypeName)) {
                return type;
            }
        }

        return null;
    }

    // Inner Classes  --------------------------------------------

    /**
     * Listens for inventory change events and adjusts the scheduled items accordingly. That is, adds new scheduled
     * content scans for new resources and removes discoveries for resources that have been removed from inventory. This
     * class has no effect when the PC is running in embedded mode and should not be registered as a listener.
     */
    private class ContentInventoryEventListener implements InventoryEventListener {

        public void resourceActivated(Resource resource) {
            ContentManager.this.scheduleDiscoveries(resource);
        }

        public void resourceDeactivated(Resource resource) {
            ContentManager.this.unscheduleDiscoveries(resource);
        }

        public void resourcesAdded(Set<Resource> resources) {
            // when activated, we'll add the schedules
        }

        public void resourcesRemoved(Set<Resource> resources) {
            for (Resource removeMe : resources) {
                ContentManager.this.unscheduleDiscoveries(removeMe);
            }
        }
    }
}
