com.vmware.appfactory.cws.simulator.CwsSimulator.java Source code

Java tutorial

Introduction

Here is the source code for com.vmware.appfactory.cws.simulator.CwsSimulator.java

Source

/* ***********************************************************************
 * VMware ThinApp Factory
 * Copyright (c) 2009-2013 VMware, Inc. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * ***********************************************************************/

package com.vmware.appfactory.cws.simulator;

import java.net.URI;
import java.net.URISyntaxException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.TimeZone;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.vmware.appfactory.common.base.AbstractApiController;
import com.vmware.appfactory.common.exceptions.AfNotFoundException;
import com.vmware.appfactory.common.exceptions.AfServerErrorException;
import com.vmware.appfactory.config.ConfigRegistryConstants;
import com.vmware.appfactory.cws.CwsRegistryRequest;
import com.vmware.appfactory.cws.CwsServerInfo;
import com.vmware.appfactory.cws.CwsSettings;
import com.vmware.appfactory.cws.CwsSettingsDir;
import com.vmware.appfactory.cws.CwsSettingsIni;
import com.vmware.appfactory.cws.CwsSettingsIniData;
import com.vmware.appfactory.cws.CwsSettingsRegKey;
import com.vmware.appfactory.cws.CwsSettingsRegSubKeyData;
import com.vmware.appfactory.cws.CwsSettingsRegValue;
import com.vmware.thinapp.common.converter.dto.Command;
import com.vmware.thinapp.common.converter.dto.CommandList;
import com.vmware.thinapp.common.converter.dto.ConversionJobStatus;
import com.vmware.thinapp.common.converter.dto.ConversionJobStatus.JobState;
import com.vmware.thinapp.common.converter.dto.ConversionPhase;
import com.vmware.thinapp.common.converter.dto.ConversionRequest;
import com.vmware.thinapp.common.converter.dto.ConversionResponse;
import com.vmware.thinapp.common.converter.dto.ConversionResult;
import com.vmware.thinapp.common.converter.dto.DsLocation;
import com.vmware.thinapp.common.converter.dto.PerformanceData;
import com.vmware.thinapp.common.converter.dto.Project;
import com.vmware.thinapp.common.converter.dto.ProjectFile;
import com.vmware.thinapp.common.converter.dto.Status;
import com.vmware.thinapp.common.converter.dto.ThinAppRuntime;
import com.vmware.thinapp.common.util.AfCalendar;
import com.vmware.thinapp.common.util.AfUtil;

/**
 * Fake controller which pretends to be the Converter Web Service. This can be
 * used for UI testing; it does not actually "convert" anything.
 */
@Controller
@RequestMapping(value = { "/cws", "/webui/cws" })
public class CwsSimulator extends AbstractApiController {
    /** How many (max) fake exe's to create per conversion job */
    private static final int MAX_EXES_PER_JOB = 4;

    /** How often to fail a job */
    private static final float REBUILD_FAILURE_RATE = 0.005f;

    /** How often we provide performance data */
    private static final int PERF_DATA_REFRESH_RATE = 10;

    private static final int REBUILD_MAX_TICKS = 10;

    private static long NEXT_ID = 1;

    private static final Random RANDOM = new Random();

    /** Running jobs: map job IDs to job status */
    private static final Map<Long, ConversionJobStatus> CONVERSION_ID_TO_STATUS_MAP = new HashMap<Long, ConversionJobStatus>();

    /** Running jobs: map job IDs to original requests */
    private static final Map<Long, ConversionRequest> CONVERSION_ID_TO_REQUEST_MAP = new HashMap<Long, ConversionRequest>();

    /** Running rebuilds: map project ids to projects being rebuilt */
    private static final Map<Long, Project> REBUILD_QUEUE = new HashMap<Long, Project>();

    /** Rebuild times: map project ids to elapsed rebuild time in 'ticks' */
    private static final Map<Long, Integer> REBUILD_TICKS = new HashMap<Long, Integer>();

    /** Completed jobs: map job ids to projects */
    private static final Map<Long, Project> PROJECT_MAP = new HashMap<Long, Project>();

    /** Completed jobs: map project ids to settings */
    private static final Map<Long, CwsSettings> SETTINGS_MAP = new HashMap<Long, CwsSettings>();

    /** A collection of ALL registry nodes we create */
    private static final Map<Long, CwsSettingsRegKey> REGISTRY_CACHE = new HashMap<Long, CwsSettingsRegKey>();

    /** A boolean to store time synchronization status */
    private boolean _timesync = false;

    /**
     * A license expiration date which will disable any WRITE calls to AppFactory
     * ISO8601 formatter for date without time zone.
     * The format used is <tt>yyyy-MM-dd</tt>.
     */
    private static final String LICENSE_EXPIRATION_DATE = "2020-02-20";

    public CwsSimulator() {
        ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
        executor.scheduleWithFixedDelay(new CwsSimulatorDaemon(), 1, 1, TimeUnit.SECONDS);
    }

    /**
     * Create a new conversion request.
     *
     * @param cwsRequest
     */
    @ResponseBody
    @RequestMapping(value = "/conversions", method = RequestMethod.POST)
    public ConversionResponse createConversion(@RequestBody ConversionRequest cwsRequest) {
        /* Assign this "job" a new ID. */
        cwsRequest.setId(Long.valueOf(NEXT_ID++));

        /* Create a successful status */
        // TODO: Randomly fail a CWS request
        ConversionJobStatus status = new ConversionJobStatus();
        status.setJobId(cwsRequest.getId());
        status.setState(ConversionJobStatus.JobState.created);

        /* Add to the queue of things to "work" on */
        CONVERSION_ID_TO_STATUS_MAP.put(cwsRequest.getId(), status);
        CONVERSION_ID_TO_REQUEST_MAP.put(cwsRequest.getId(), cwsRequest);

        /* Write the response. */
        return new ConversionResponse(cwsRequest.getId());
    }

    /**
     * Get the status of a conversion request.
     *
     * @param jobId
     */
    @ResponseBody
    @RequestMapping(value = "/conversions/{id}", method = RequestMethod.GET)
    public ConversionJobStatus getConversionSatus(@PathVariable("id") Long jobId) throws AfNotFoundException {
        ConversionJobStatus result = CONVERSION_ID_TO_STATUS_MAP.get(jobId);
        if (null != result) {
            return result;
        }

        /* There was no such job id */
        throw new AfNotFoundException("Invalid job ID " + jobId);
    }

    /**
     * Cancel (abort) an ongoing conversion request.
     *
     * @param conversionId
     */
    @ResponseBody
    @RequestMapping(value = "/conversions/{id}/cancel", method = RequestMethod.POST)
    public void cancelConversion(@PathVariable("id") Long conversionId) throws AfNotFoundException {
        /* Mark it as aborting; the update timer will handle it from there. */
        ConversionJobStatus status = CONVERSION_ID_TO_STATUS_MAP.get(conversionId);
        if (null != status) {
            status.setState(ConversionJobStatus.JobState.cancelling);
            status.setPercent(0);
            return;
        }

        /* There was no such job id */
        throw new AfNotFoundException("Invalid job ID " + conversionId);
    }

    /**
     * This is the simulator method to create dummy thinApp runtime.
     *
     * @return
     */
    @ResponseBody
    @RequestMapping(value = "/runtimes", method = RequestMethod.GET)
    public ThinAppRuntime[] getThinAppRuntimes() {
        ThinAppRuntime[] tas = { new ThinAppRuntime("4.6.2", 456451, "/wherever/4.6.2", 456451L),
                new ThinAppRuntime("4.7", 519532, "/wherever/4.7", 519532L) };
        return tas;
    }

    @ResponseBody
    @RequestMapping(value = "/projects/{id}", method = RequestMethod.GET)
    public Project getProjectStatus(@PathVariable("id") Long projectId) throws AfNotFoundException {
        Project proj = PROJECT_MAP.get(projectId);

        if (proj == null) {
            /* There was no such project id */
            throw new AfNotFoundException("Invalid project ID " + projectId);
        }

        return proj;
    }

    /**
     * Rebuild a project.
     *
     * @param projectId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{id}/rebuild", method = RequestMethod.POST)
    public void rebuildProject(@PathVariable("id") Long projectId) throws AfNotFoundException {
        if (!PROJECT_MAP.containsKey(projectId)) {
            /* There was no such project id */
            throw new AfNotFoundException("Invalid project ID " + projectId);
        }

        _log.debug("Project rebuild for " + projectId);

        /* Get the project and change its state to rebuilding */
        Project project = PROJECT_MAP.get(projectId);
        project.setState(Project.State.rebuilding);

        /* Add the project to the collection of projects currently being rebuilt */
        REBUILD_QUEUE.put(project.getId(), project);
        REBUILD_TICKS.put(project.getId(), Integer.valueOf(0));

        return;
    }

    /**
     * Refresh a project.
     * This tells CWS to rescan the project output directory and refresh the
     * contents; used when the user modifies the output directory contents
     * manually when editing a build.
     *
     * @param projectId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{id}/refresh", method = RequestMethod.POST)
    public void refreshProject(@PathVariable("id") Long projectId) throws AfNotFoundException {
        if (!PROJECT_MAP.containsKey(projectId)) {
            /* There was no such project id */
            throw new AfNotFoundException("Invalid project ID " + projectId);
        }

        _log.debug("Project refresh for " + projectId);

        /* There is nothing we simulator for this */

        return;
    }

    /**
     * Delete a project.
     * In the real CWS service, this deletes the files associated with a
     * project.
     *
     * @param projectId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}", method = RequestMethod.DELETE)
    public void deleteProject(@PathVariable Long projectId) throws AfNotFoundException {
        /* We have nothing to do, and nothing to return */

        if (!PROJECT_MAP.containsKey(projectId)) {
            /* There was no such project id */
            throw new AfNotFoundException("Invalid project ID " + projectId);
        }
    }

    /**
     * Update the project with one of:
     * 1. ThinApp runtime to a different version.
     * 2. State
     *
     * @param projectId
     * @param runtimeId
     * @param state
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}", method = RequestMethod.PUT)
    public void updateProject(@PathVariable Long projectId, @RequestParam(required = false) String state,
            @RequestParam(required = false) Long runtimeId) throws AfNotFoundException {
        Project project = PROJECT_MAP.get(projectId);
        if (project == null) {
            /* There was no such project id */
            throw new AfNotFoundException("Invalid project ID " + projectId);
        }

        if (runtimeId != null) {
            project.setRuntimeId(runtimeId);
        }
    }

    /**
     * Get package.ini settings for a project.
     *
     * @param request
     * @param projectId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}/packageini", method = RequestMethod.GET)
    public CwsSettingsIni getProjectPacakgeIni(HttpServletRequest request, @PathVariable Long projectId)
            throws AfNotFoundException {
        CwsSettings settings = getProjectSettings(cwsUrlRoot(request), projectId);
        return settings.getPackageIni();
    }

    /**
     * Update package.ini settings for a project.
     *
     * @param request
     * @param projectId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}/packageini", method = RequestMethod.PUT)
    public HashMap<String, Boolean> updateProjectPackageIni(@RequestBody CwsSettingsIni newPackageIni,
            HttpServletRequest request, @PathVariable Long projectId) throws AfNotFoundException {
        CwsSettings settings = getProjectSettings(cwsUrlRoot(request), projectId);

        /* Update packageIni and we're done */
        boolean changed = settings.setPackageIniIfChanged(newPackageIni);
        _log.debug("Update packageini of project " + projectId + ", changed = " + changed);
        HashMap<String, Boolean> res = new HashMap<String, Boolean>();
        res.put("modified", Boolean.valueOf(changed));
        return res;
    }

    /**
     * Get root directory settings for a project.
     *
     * @param request
     * @param projectId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}/directory", method = RequestMethod.GET)
    public CwsSettingsDir getProjectDirectoryRoot(HttpServletRequest request, @PathVariable Long projectId)
            throws AfNotFoundException {
        CwsSettings settings = getProjectSettings(cwsUrlRoot(request), projectId);
        return settings.getDirRoot();
    }

    /**
     * Get child directory settings for a project.
     *
     * @param request
     * @param projectId
     * @param childNum
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}/directory/{childNum}", method = RequestMethod.GET)
    public CwsSettingsDir getProjectDirectory(HttpServletRequest request, @PathVariable Long projectId,
            @PathVariable Long childNum) throws AfNotFoundException {
        CwsSettings settings = getProjectSettings(cwsUrlRoot(request), projectId);

        /* Get the root, but make up subkeys on the fly */
        CwsSettingsDir root = settings.getDirRoot();
        return createDummyDirectories(cwsUrlRoot(request), projectId, root);
    }

    // TODO: GET /projects/{projectId}/file/{fileId}

    // TODO: PUT /projects/{projectId}/directory/{directoryId}

    // TODO: POST /projects/{projectId}/directory/new

    // TODO: POST /projects/{projectId}/file/new

    // TODO: DELETE /projects/{projectId}/directory/{directoryId}

    // TODO: DELETE /projects/{projectId}/file/{fileId}

    /**
     * Get root registry settings for a project.
     *
     * From the API spec:
     * "Returns all top level keys (registry 'folders') modified in ThinApp
     * capture."
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}/registry", method = RequestMethod.GET)
    public CwsSettingsRegKey getProjectRegistryRoot(HttpServletRequest request, @PathVariable Long projectId)
            throws AfNotFoundException {
        CwsSettings settings = getProjectSettings(cwsUrlRoot(request), projectId);
        return settings.getRegistryRoot();
    }

    /**
     * Get child registry settings for a project.
     *
     * From the API spec:
     * "Returns the key, a list of its subkeys and URIs to access the contents
     * of said subkeys, and all values immediately under that key."
     *
     * @param request
     * @param response
     * @param projectId
     * @param registryId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}/registry/{registryId}", method = RequestMethod.GET)
    public CwsSettingsRegKey getProjectRegistrySubkey(HttpServletRequest request, HttpServletResponse response,
            @PathVariable Long projectId, @PathVariable Long registryId) throws AfNotFoundException {
        getProjectSettings(cwsUrlRoot(request), projectId);
        return REGISTRY_CACHE.get(registryId);
    }

    /**
     * Delete a registry key for a project.
     *
     * @param request
     * @param projectId
     * @param registryId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}/registry/{registryId}", method = RequestMethod.DELETE)
    public void deleteProjectRegistrySubkey(HttpServletRequest request, @PathVariable Long projectId,
            @PathVariable Long registryId) throws AfNotFoundException {
        getProjectSettings(cwsUrlRoot(request), projectId);

        // TODO: Find the parent and remove the child. For now, just log it
        _log.debug("** DELETED REGISTRY NODE:");
        logRegistryKey(REGISTRY_CACHE.get(registryId));
        // REGISTRY_CACHE.remove(registryId);
    }

    /**
     * Replace a registry key for a project.
     *
     * @param request
     * @param projectId
     * @param registryId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}/registry/{registryId}", method = RequestMethod.PUT)
    public HashMap<String, Boolean> updateProjectRegistrySubkey(HttpServletRequest request,
            @RequestBody CwsSettingsRegKey regKey, @PathVariable Long projectId, @PathVariable Long registryId)
            throws AfNotFoundException {
        getProjectSettings(cwsUrlRoot(request), projectId);

        /* Update the given registry ID with the given key */
        boolean changed = updateRegistryKey(registryId, regKey);
        _log.debug("Update packageini of project " + projectId + ", changed = " + changed);
        HashMap<String, Boolean> res = new HashMap<String, Boolean>();
        res.put("modified", Boolean.valueOf(changed));
        return res;
    }

    /**
     * Create a new registry key for a project.
     *
     * @param request
     * @param requestData
     * @param projectId
     */
    @ResponseBody
    @RequestMapping(value = "/projects/{projectId}/registry/new", method = RequestMethod.POST)
    public Map<String, String> createProjectRegistryKey(HttpServletRequest request, @PathVariable Long projectId,
            @RequestBody CwsRegistryRequest requestData) throws AfNotFoundException, AfServerErrorException {
        getProjectSettings(cwsUrlRoot(request), projectId);

        try {
            /* Remember this node */
            Long regId = Long.valueOf(NEXT_ID++);
            REGISTRY_CACHE.put(regId, requestData.getKey());
            String regUrl = cwsUrlRoot(request) + "/projects/" + projectId + "/registry/" + regId;

            // TODO: Do something with the data. For now, just log it
            _log.debug("Created registry node:");
            logRegistryKey(requestData.getKey());

            /* Create the result structure and return it */
            Map<String, String> result = new HashMap<String, String>();
            result.put("url", regUrl);
            return result;
        } catch (Exception ex) {
            _log.error("Registry key creation failed", ex);
            throw new AfServerErrorException(ex.getMessage());
        }
    }

    /**
     * Reboot the simulated appliance.
     */
    @ResponseBody
    @RequestMapping(value = "/config/reboot", method = RequestMethod.POST)
    public void reboot() {
        _log.debug("Reboot command issued");
    }

    /**
     * Return the byte array for a test zip file.
     *
     * @param response
     */
    @ResponseBody
    @RequestMapping(value = "/config/logs", method = RequestMethod.GET)
    public byte[] logs(HttpServletResponse response) {
        _log.debug("Get ZIP appliance system log file, returning byte[] with " + "length = "
                + CwsSimulator._zipFileBytes.length);

        /* Note: See AdminApiController.getLogs for an explanation of why we must
         * set these headers manually.
         */
        response.setHeader(AfUtil.CONTENT_LENGTH, Integer.toString(_zipFileBytes.length));
        response.setHeader(AfUtil.CONTENT_DISPOSITION, "attachment; filename=\"sample_logs.zip\"");

        /* Return the file data. */
        return CwsSimulator._zipFileBytes;
    }

    /**
     * Return the simulated date/time and uptime.
     */
    @ResponseBody
    @RequestMapping(value = "/config/info", method = RequestMethod.GET)
    public CwsServerInfo getInfo() {
        _log.debug("GET /config/info");
        CwsServerInfo info = new CwsServerInfo();
        SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        format.setTimeZone(TimeZone.getTimeZone("GMT"));
        info.date = format.format(new Date()).replace(' ', 'T') + "Z";
        info.uptime = AfCalendar.Now() / 1000;
        return info;
    }

    /**
     * Return the simulated state of host/guest time synchronization.
     *
     * @throws InterruptedException
     */
    @ResponseBody
    @RequestMapping(value = "/config/timesync", method = RequestMethod.GET)
    public boolean getTimeSync() throws InterruptedException {
        _log.debug("GET /config/timesync, state = " + _timesync);

        //Wait for 2 seconds to simulate CWS being slowsauce
        Thread.sleep(2000);

        return _timesync;
    }

    /**
     * Enable simulated host/guest time synchronization.
     */
    @ResponseBody
    @RequestMapping(value = "/config/timesync/enable", method = RequestMethod.POST)
    public void enableTimeSync() {
        _log.debug("POST /config/timesync/enable");
        _timesync = true;
    }

    /**
     * Disable simulated host/guest time synchronization.
     */
    @ResponseBody
    @RequestMapping(value = "/config/timesync/disable", method = RequestMethod.POST)
    public void disableTimeSync() {
        _log.debug("POST /config/timesync/disable");
        _timesync = false;
    }

    @Service
    public class CwsSimulatorDaemon implements Runnable {
        @Override
        public void run() {
            try {
                if (_config.getBool(ConfigRegistryConstants.CWS_PAUSED)) {
                    return;
                }

                updateConversions();
                updateRebuilds();
            } catch (Throwable t) {
                _log.error("Simulator thread exited with error: ", t);
            }
        }
    }

    /**
     * Return the simulated license expiration date time.
     */
    @ResponseBody
    @RequestMapping(value = "/config/expire", method = RequestMethod.GET)
    public Map<String, String> getLicenseExpirationDate() {
        _log.trace("GET /config/expire, expiration date = " + LICENSE_EXPIRATION_DATE);
        Map<String, String> resMap = new HashMap<String, String>();
        resMap.put("date", LICENSE_EXPIRATION_DATE);
        return resMap;
    }

    /**
     * Update any 'running' conversion jobs.
     */
    private void updateConversions() {
        for (Map.Entry<Long, ConversionJobStatus> entry : CONVERSION_ID_TO_STATUS_MAP.entrySet()) {

            Long jobId = entry.getKey();
            ConversionJobStatus status = entry.getValue();

            if (null == status.getState()) {
                status.setState(ConversionJobStatus.JobState.created);
            }

            ConversionRequest request = CONVERSION_ID_TO_REQUEST_MAP.get(jobId);

            int newPercent = 0;
            switch (status.getState()) {
            case created:
                status.setState(ConversionJobStatus.JobState.provisioning);
                status.setPercent(newPercent);
                break;

            case provisioning:
                processCaptureStage(request, status, status.getState().getNext());
                break;
            case downloading:
                if (status.getPercent() == 0) {
                    dumpDownloads(request);
                }
                processCaptureStage(request, status, status.getState().getNext());
                break;

            case precapture:
            case preinstall:
                processCaptureStage(request, status, status.getState().getNext());
                break;

            case install:
                if (status.getPercent() == 0) {
                    dumpCommands(request);
                }
                processCaptureStage(request, status, status.getState().getNext());
                break;

            case postinstall:
            case postcapture:
            case projectgen:
            case prebuild:
            case projectbuild:
            case projectrefresh:
                processCaptureStage(request, status, status.getState().getNext());
                break;

            case finishing:
                newPercent = status.getPercent() + (request.hashCode() % 3) + 1;
                status.setPercent(newPercent);
                if (status.getPercent() >= 100) {
                    /* finishing -> finished */
                    finishConversion(request);
                }
                break;

            case cancelling:
                status.setState(ConversionJobStatus.JobState.cancelled);
                status.setPercent(newPercent);
                break;

            case cancelled:
            case finished:
                /* Nothing to do for any of these*/
                break;
            }
        }
    }

    /**
     * Process the a simulated capture during one of the main phases.
     */
    private void processCaptureStage(ConversionRequest request, ConversionJobStatus status,
            ConversionJobStatus.JobState nextState, int multiplier) {
        if (status.getState() == JobState.install && commandLabelContains("Stall", request.getSteps().values())) {
            // Start providing performance data once we have started the install phase and the app is
            // being converted with a command label that contains the string "Stall".
            status.setPerformanceData(
                    PerformanceData.create(PERF_DATA_REFRESH_RATE, new Date(), getPerfThresholdsOver()));
        } else if (status.getState() == JobState.install
                && commandLabelContains("Fail", request.getSteps().values())) {
            // Fail the job if a command label contains the string "Fail"
            ConversionResult res = new ConversionResult(ConversionResult.Disposition.failed,
                    Status.State.installerDownloadFailed, new Command("Test command", "echo test"));
            status.setResult(res);
            status.setState(ConversionJobStatus.JobState.finished);
            status.setPercent(0);
        } else {
            int newPercent = status.getPercent() + (multiplier * (request.hashCode() % 3) + 1);
            status.setPercent(newPercent);
            if (status.getPercent() >= currentStateEndpoint(status.getState())) {
                status.setState(nextState);
            }
        }
    }

    private boolean commandLabelContains(final String text, Collection<CommandList> commandLists) {
        final Predicate<Command> anyLabelInCommand = new Predicate<Command>() {
            @Override
            public boolean apply(Command command) {
                return command.getLabel() != null && command.getLabel().contains(text);
            }
        };

        Predicate<CommandList> anyCommandInList = new Predicate<CommandList>() {
            @Override
            public boolean apply(CommandList commandList) {
                return Iterables.any(commandList.getCommands(), anyLabelInCommand);
            }
        };

        return Iterables.any(commandLists, anyCommandInList);
    }

    private Map<String, Long> getPerfThresholdsOver() {
        return ImmutableMap.of(PerformanceData.CPU_USAGE_AVERAGE_SPEC,
                _config.getLong(ConfigRegistryConstants.CWS_STALL_CPU) * 100,
                PerformanceData.DISK_USAGE_AVERAGE_SPEC,
                _config.getLong(ConfigRegistryConstants.CWS_STALL_NET) * 100,
                PerformanceData.NET_USAGE_AVERAGE_SPEC,
                _config.getLong(ConfigRegistryConstants.CWS_STALL_DISK) * 100);
    }

    private int currentStateEndpoint(ConversionJobStatus.JobState jobState) {
        return ((jobState.ordinal() + 1) * 100) / (ConversionJobStatus.JobState.finished.ordinal() + 1);
    }

    private void processCaptureStage(ConversionRequest request, ConversionJobStatus status,
            ConversionJobStatus.JobState nextState) {
        processCaptureStage(request, status, nextState, 1);
    }

    /**
     * Update any 'running' project rebuilds.
     *
     * XXX: It would be nice to clean this up using a class similar to
     * CwsJobStatus.  This would encapsulate details such as
     * REBUILD_MAX_TICKS, etc.
     */
    private void updateRebuilds() {
        List<Long> completedRebuilds = new ArrayList<Long>();
        List<Long> failedRebuilds = new ArrayList<Long>();

        /* Determine what project rebuilds have completed or failed */
        for (Long projectId : REBUILD_QUEUE.keySet()) {
            _log.debug("Updating rebuild for project id = " + projectId);

            /* Occasionally fail a rebuild */
            if (Math.random() < REBUILD_FAILURE_RATE) {
                failedRebuilds.add(projectId);
                continue;
            }
            /* Complete rebuilds that have run for REBUILD_MAX_TICKS */
            else if (REBUILD_TICKS.get(projectId) == Integer.valueOf(REBUILD_MAX_TICKS)) {
                completedRebuilds.add(projectId);
            }
            /* Note that time has elapsed for the rebuild */
            else {
                REBUILD_TICKS.put(projectId, Integer.valueOf(REBUILD_TICKS.get(projectId).intValue() + 1));
            }
        }

        /* Mark all failed rebuilds as dirty */
        for (Long projectId : failedRebuilds) {
            _log.debug("Rebuild failed for projectId = " + projectId);

            Project project = REBUILD_QUEUE.get(projectId);
            project.setState(Project.State.dirty);
            REBUILD_QUEUE.remove(projectId);
            REBUILD_TICKS.remove(projectId);
        }

        /* Mark all completed rebuilds as available */
        for (Long projectId : completedRebuilds) {
            _log.debug("Rebuild complete for projectId = " + projectId);

            Project project = REBUILD_QUEUE.get(projectId);
            project.setState(Project.State.available);
            REBUILD_QUEUE.remove(projectId);
            REBUILD_TICKS.remove(projectId);
        }
    }

    private void finishConversion(ConversionRequest request) {
        /* Get the status of this job */
        ConversionJobStatus status = CONVERSION_ID_TO_STATUS_MAP.get(request.getId());

        /* Create fake project */
        Long projectId = new Long(NEXT_ID++);
        Project project = new Project();
        project.setId(projectId);
        project.setState(Project.State.available);
        project.setSubdir("fake-" + projectId);
        project.setRuntimeId(request.getRuntimeId());
        PROJECT_MAP.put(projectId, project);

        /* If request output was a datastore, put that into the project */
        DsLocation outputPath = request.getOutput();
        if (outputPath.isDatastorePath()) {
            // The path we create is "datastore://<dsName>"
            String fullPath = outputPath.getUrl();
            Long dsId = Long.valueOf(fullPath.substring(12));
            project.setDatastoreId(dsId);
        }

        /* Put fake files into the project */
        int numFiles = 1 + (int) (Math.random() * MAX_EXES_PER_JOB - 1);
        List<ProjectFile> files = new ArrayList<ProjectFile>();
        for (int i = 0; i < numFiles; i++) {
            try {
                String fakeFileName = "Dummy File #" + i + ".exe";
                URI uri = new URI("http", "localhost", "/" + fakeFileName, null);

                ProjectFile file = new ProjectFile(fakeFileName, uri.toASCIIString());
                file.setSize(10000 * (i + 1));
                files.add(file);
            } catch (URISyntaxException ex) {
                /* Safe to ignore in this case */
            }
        }
        project.setFiles(files);

        /* Create job result */
        ConversionResult result = new ConversionResult(ConversionResult.Disposition.succeeded,
                Status.State.refreshingProjectDone, null);

        /* Update job status */
        status.setResult(result);
        status.setProjectId(projectId);
        status.setState(ConversionJobStatus.JobState.finished);
    }

    /**
     * Updates the given registry ID with the given key.
     *
     * @param registryId
     * @param regKey
     * @return true if the given registry key was different from the existing
     *         value, false otherwise
     */
    private boolean updateRegistryKey(Long registryId, CwsSettingsRegKey regKey) {
        if (REGISTRY_CACHE.get(registryId).equals(regKey)) {
            return false;
        }

        REGISTRY_CACHE.put(registryId, regKey);
        return true;
    }

    /**
     * Get settings for a project.
     *
     * Since the simulator holds all job and project state in memory only,
     * it all gets lost when the webapp reloads. This is a pain when developing.
     * So, we treat ALL project IDs as valid. Otherwise, you need to convert
     * an app each time you want to edit a project.
     *
     * @param cwsUrlRootStr
     * @param projectId
     * @return
     */
    private synchronized CwsSettings getProjectSettings(String cwsUrlRootStr, Long projectId) {
        CwsSettings settings = SETTINGS_MAP.get(projectId);

        if (settings == null) {
            /* Make new settings (dummy values) */
            settings = createDummySettings(cwsUrlRootStr, projectId);
            SETTINGS_MAP.put(projectId, settings);
        }

        return settings;
    }

    /**
     * Create a dummy set of project settings. These are used for the
     * 'default' values for our simulated CWS projects.
     *
     * @param cwsUrlRootStr
     * @param projectId
     * @return
     */
    private static final CwsSettings createDummySettings(String cwsUrlRootStr, Long projectId) {
        CwsSettings settings = new CwsSettings();

        Long regRootId = createDummyRegistry(cwsUrlRootStr, projectId, null, "", 0);
        CwsSettingsRegKey regRoot = REGISTRY_CACHE.get(regRootId);

        settings.setPackageIni(createDummyIni());
        settings.setRegistryRoot(regRoot);
        settings.setDirRoot(createDummyDirectories(cwsUrlRootStr, projectId, null));

        return settings;
    }

    /**
     * Create a dummy INI structure.
     *
     * @return
     */
    private static final CwsSettingsIni createDummyIni() {
        CwsSettingsIni ini = new CwsSettingsIni();
        int numSections = RANDOM.nextInt(10) + 10;

        for (int i = 1; i <= numSections; i++) {
            String sectionName = "Section" + i;
            CwsSettingsIniData section = new CwsSettingsIniData();
            ini.put(sectionName, section);

            int numPairs = RANDOM.nextInt(5) + 5;
            for (int j = 0; j <= numPairs; j++) {
                String key = sectionName + "-Key-" + j;
                section.put(key, AfUtil.randomString());
            }
        }

        return ini;
    }

    /**
     * Create a dummy directory structure.
     *
     * @return
     */
    private static final CwsSettingsDir createDummyDirectories(String cwsUrlRootStr, Long projectId,
            CwsSettingsDir parent) {
        CwsSettingsDir dir = new CwsSettingsDir();

        /*
         * Directory path:
         * If root, it's empty, else it has one component (good enough...)
         */
        if (parent == null) {
            dir.setPath("");
        } else {
            dir.setPath(parent.getPath() + "/pathSegment");
        }

        /*
         * Files:
         * A random number from 1 to 5
         */
        int numFiles = RANDOM.nextInt(5) + 1;
        for (int f = 1; f <= numFiles; f++) {
            dir.addFile("file-" + f, cwsUrlRootStr + "/projects/" + projectId + "/file/" + f);
        }

        /*
         * Sub-directories:
         * If root, there are 3. Else a 50% chance of none and a 50% chance
         * of 1 or 2 (good enough)
         */
        int numDirs = 0;
        if (parent == null) {
            numDirs = 3;
        } else {
            numDirs = RANDOM.nextInt(4) - 2;
        }

        if (numDirs > 0) {
            for (int d = 1; d <= numDirs; d++) {
                dir.addDirectory("directory-" + d, cwsUrlRootStr + "/projects/" + projectId + "/directory/" + d);
            }
        }

        /*
         * Attributes
         * Just a dummy INI structure (good enough)
         */
        dir.setAttributes(createDummyIni());

        return dir;
    }

    /**
     * Create a dummy registry structure.
     *
     * @param cwsUrlRootStr
     * @param projectId
     * @param parent
     * @return
     */
    private static final Long createDummyRegistry(String cwsUrlRootStr, Long projectId, CwsSettingsRegKey parent,
            String pathName, int depth) {
        Long regId = Long.valueOf(NEXT_ID++);

        /* Create a new registry key and cache it */
        CwsSettingsRegKey reg = new CwsSettingsRegKey();
        reg.setId(regId);
        REGISTRY_CACHE.put(regId, reg);

        String indent = "";

        for (int i = 0; i < depth; i++)
            indent += "  ";

        /* Isolation mode: random */
        reg.setPath(pathName);
        reg.setIsolation((RANDOM.nextBoolean() ? CwsSettingsRegKey.IsolationMode.full
                : CwsSettingsRegKey.IsolationMode.merged));

        // Initialize the hasChildren variable that will be set when the child
        // registry is created.
        boolean hasChildren = false;

        if (parent == null) {
            /*
             * Make fixed subkeys, one per hive.
             */
            for (String hive : new String[] { "HKEY_LOCAL_MACHINE", "HKEY_CURRENT_USER", "HKEY_USERS" }) {
                Long hiveId = createDummyRegistry(cwsUrlRootStr, projectId, reg, hive, depth + 1);

                // Compute hasChildren from the child registry in REGISTRY_CACHE.
                CwsSettingsRegKey childReg = REGISTRY_CACHE.get(hiveId);
                hasChildren = childReg.getSubkeys().size() > 0;

                reg.addSubkey(hive, cwsUrlRootStr + "/projects/" + projectId + "/registry/" + hiveId, hasChildren);
            }
        } else {
            /* Key has a path */
            StringBuilder path = new StringBuilder(parent.getPath());
            if (path.length() > 0) {
                path.append("/");
            }
            path.append(pathName);
            reg.setPath(path.toString());

            /* Key has subkeys? */
            int numSubkeys = (depth == 1 ? RANDOM.nextInt(4) + 1 : depth < 10 ? RANDOM.nextInt(10) - 5 : 0);

            for (int i = 1; i <= numSubkeys; i++) {
                String keyName = "KEY-" + AfUtil.randomString();
                Long registryId = createDummyRegistry(cwsUrlRootStr, projectId, reg, keyName, depth + 1);

                // Compute hasChildren from the child registry in REGISTRY_CACHE.
                CwsSettingsRegKey childReg = REGISTRY_CACHE.get(registryId);
                hasChildren = childReg.getSubkeys().size() > 0;

                reg.addSubkey(keyName, cwsUrlRootStr + "/projects/" + projectId + "/registry/" + registryId,
                        hasChildren);
            }

            /* Key has values: one for every type! */
            for (CwsSettingsRegValue.Type type : CwsSettingsRegValue.Type.values()) {
                CwsSettingsRegValue val = new CwsSettingsRegValue();
                val.setType(type);

                /* Make data according to type */
                switch (val.getType()) {
                case REG_BINARY: {
                    int data[] = new int[RANDOM.nextInt(20) + 1];
                    for (int d = 0; d < data.length; d++) {
                        data[d] = RANDOM.nextInt(256);
                    }
                    val.setData(data);
                }
                    break;

                case REG_MULTI_SZ: {
                    String data[] = new String[RANDOM.nextInt(5) + 2];
                    for (int d = 0; d < data.length; d++) {
                        data[d] = AfUtil.randomString();
                    }
                    val.setData(data);
                }
                    break;

                case REG_SZ:
                case REG_EXPAND_SZ:
                    val.setData(AfUtil.randomString());
                    break;

                case REG_DWORD:
                case REG_DWORD_LITTLE_ENDIAN:
                case REG_QWORD:
                    val.setData(Integer.valueOf(RANDOM.nextInt(1000)));
                    break;
                }

                String key = AfUtil.randomString();
                reg.addValue(key, val);
            }
        }

        return regId;
    }

    /**
     * For URI resources, we need to point back to ourself. This method
     * computes the correct absolute URL (since CWS will provide absolute
     * URLs).
     *
     * @param request
     * @return
     */
    private static String cwsUrlRoot(HttpServletRequest request) {
        StringBuilder url = new StringBuilder("http://");

        url.append(request.getServerName()).append(":").append(request.getServerPort())
                .append(request.getContextPath()).append("/cws");

        return url.toString();
    }

    /**
     * Log a registry key.
     * Primarily for debugging.
     */
    private void logRegistryKey(CwsSettingsRegKey registry) {
        _log.debug("Path ...... " + registry.getPath());
        _log.debug("IsoMode ... " + registry.getIsolation());

        Map<String, CwsSettingsRegValue> vals = registry.getValues();
        _log.debug("Values .... " + vals.size());
        for (String name : vals.keySet()) {
            _log.debug("   " + name + " = " + vals.get(name).getData());
        }

        Map<String, CwsSettingsRegSubKeyData> subs = registry.getSubkeys();
        _log.debug("Subkeys ... " + subs.size());
        for (String key : subs.keySet()) {
            _log.debug("   " + key + " = " + subs.get(key));
        }
    }

    private void dumpDownloads(ConversionRequest request) {
        _log.info("Conversion Downloads for request #" + request.getId());

        for (ProjectFile file : request.getFiles()) {
            _log.info("   " + file.getUrl());
        }
    }

    private void dumpCommands(ConversionRequest request) {
        _log.info("Conversion Commands for request #" + request.getId());

        for (ConversionPhase phase : request.getSteps().keySet()) {
            CommandList cmds = request.getSteps().get(phase);
            for (Command cmd : cmds.getCommands()) {
                _log.info("   " + phase.toString().toUpperCase() + " : " + cmd.getCommand());
            }
        }
    }

    /**
     * Raw bytes for a sample ZIP file containing a single text (.log) file
     */
    private static final byte[] _zipFileBytes = { 0x50, 0x4B, 0x03, 0x04, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16,
            0x74, (byte) 0x9D, 0x3E, (byte) 0xA5, 0x0F, (byte) 0xBC, 0x06, 0x19, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00,
            0x00, 0x0A, 0x00, 0x1C, 0x00, 0x73, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x6C, 0x6F, 0x67, 0x55, 0x54,
            0x09, 0x00, 0x03, 0x7C, 0x2E, (byte) 0xBB, 0x4D, 0x7D, 0x2E, (byte) 0xBB, 0x4D, 0x75, 0x78, 0x0B, 0x00,
            0x01, 0x04, (byte) 0xF6, 0x01, 0x00, 0x00, 0x04, 0x14, 0x00, 0x00, 0x00, 0x54, 0x68, 0x69, 0x73, 0x20,
            0x69, 0x73, 0x20, 0x61, 0x20, 0x64, 0x65, 0x6D, 0x6F, 0x20, 0x6C, 0x6F, 0x67, 0x20, 0x66, 0x69, 0x6C,
            0x65, 0x2E, 0x0A, 0x50, 0x4B, 0x01, 0x02, 0x1E, 0x03, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x74,
            (byte) 0x9D, 0x3E, (byte) 0xA5, 0x0F, (byte) 0xBC, 0x06, 0x19, 0x00, 0x00, 0x00, 0x19, 0x00, 0x00, 0x00,
            0x0A, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, (byte) 0x80, (byte) 0x81, 0x00,
            0x00, 0x00, 0x00, 0x73, 0x61, 0x6D, 0x70, 0x6C, 0x65, 0x2E, 0x6C, 0x6F, 0x67, 0x55, 0x54, 0x05, 0x00,
            0x03, 0x7C, 0x2E, (byte) 0xBB, 0x4D, 0x75, 0x78, 0x0B, 0x00, 0x01, 0x04, (byte) 0xF6, 0x01, 0x00, 0x00,
            0x04, 0x14, 0x00, 0x00, 0x00, 0x50, 0x4B, 0x05, 0x06, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00,
            0x50, 0x00, 0x00, 0x00, 0x5D, 0x00, 0x00, 0x00, 0x00, 0x00 };
}