@Override
  public void install() {
    Maybe<Object> url =
        ((EntityInternal) getEntity()).config().getRaw(SoftwareProcess.DOWNLOAD_URL);
    if (url.isPresentAndNonNull()) {
      DownloadResolver resolver = Entities.newDownloader(this);
      List<String> urls = resolver.getTargets();
      downloadedFilename = resolver.getFilename();

      List<String> commands = new LinkedList<String>();
      commands.addAll(BashCommands.commandsToDownloadUrlsAs(urls, downloadedFilename));
      commands.addAll(ArchiveUtils.installCommands(downloadedFilename));

      int result =
          newScript(ImmutableMap.of(INSTALL_INCOMPLETE, true), INSTALLING)
              .failOnNonZeroResultCode(false)
              .body
              .append(commands)
              .execute();

      if (result != 0) {
        // could not install at remote machine; try resolving URL here and copying across
        for (String urlI : urls) {
          result =
              ArchiveUtils.install(
                  getMachine(), urlI, Urls.mergePaths(getInstallDir(), downloadedFilename));
          if (result == 0) break;
        }
        if (result != 0)
          throw new IllegalStateException("Error installing archive: " + downloadedFilename);
      }
    }

    // If downloadUrl did partial install (see INSTALL_INCOMPLETE above) then always execute install
    // so mark it as completed.
    String installCommand = getEntity().getConfig(VanillaSoftwareProcess.INSTALL_COMMAND);
    if (url.isPresentAndNonNull() && Strings.isBlank(installCommand))
      installCommand = "# mark as complete";

    if (Strings.isNonBlank(installCommand)) {
      newScript(INSTALLING)
          .failOnNonZeroResultCode()
          .environmentVariablesReset(getShellEnvironment())
          .body
          .append(installCommand)
          .execute();
    }
  }
  @Override
  public void customize() {
    List<String> commands = Lists.newLinkedList();

    String gitRepoUrl = getEntity().getConfig(NodeJsWebAppService.APP_GIT_REPOSITORY_URL);
    String archiveUrl = getEntity().getConfig(NodeJsWebAppService.APP_ARCHIVE_URL);
    String appName = getEntity().getConfig(NodeJsWebAppService.APP_NAME);
    if (Strings.isNonBlank(gitRepoUrl) && Strings.isNonBlank(archiveUrl)) {
      throw new IllegalStateException(
          "Only one of Git or archive URL must be set for " + getEntity());
    } else if (Strings.isNonBlank(gitRepoUrl)) {
      commands.add(String.format("git clone %s %s", gitRepoUrl, appName));
      commands.add(String.format("cd %s", appName));
    } else if (Strings.isNonBlank(archiveUrl)) {
      ArchiveUtils.deploy(archiveUrl, getMachine(), getRunDir());
    } else {
      throw new IllegalStateException(
          "At least one of Git or archive URL must be set for " + getEntity());
    }

    commands.add(BashCommands.ifFileExistsElse1("package.json", "npm install"));
    List<String> packages = getEntity().getConfig(NodeJsWebAppService.NODE_PACKAGE_LIST);
    if (packages != null && packages.size() > 0) {
      commands.add(BashCommands.sudo("npm install -g " + Joiner.on(' ').join(packages)));
    }

    newScript(CUSTOMIZING).body.append(commands).execute();
  }
  @Override
  public void customize() {
    if (downloadedFilename != null) {
      newScript(CUSTOMIZING)
          .failOnNonZeroResultCode()
          .environmentVariablesReset()
          .body
          .append(ArchiveUtils.extractCommands(downloadedFilename, getInstallDir()))
          .execute();
    }

    String customizeCommand = getEntity().getConfig(VanillaSoftwareProcess.CUSTOMIZE_COMMAND);

    if (Strings.isNonBlank(customizeCommand)) {
      newScript(CUSTOMIZING).failOnNonZeroResultCode().body.append(customizeCommand).execute();
    }
  }
  @Override
  public void customize() {
    newScript(CUSTOMIZING)
        .failOnNonZeroResultCode()
        .body
        .append(
            // workaround for AMP distribution placing everything in the root of this archive, but
            // brooklyn distribution placing everything in a subdirectory: check to see if
            // subdirectory
            // with expected name exists; symlink to same directory if it doesn't
            // FIXME remove when all downstream usages don't use this
            format("[ -d %1$s ] || ln -s . %1$s", getExpandedInstallDir(), getExpandedInstallDir()),

            // previously we only copied bin,conf and set BROOKLYN_HOME to the install dir;
            // but that does not play nicely if installing dists other than brooklyn
            // (such as what is built by our artifact)
            format("cp -R %s/* .", getExpandedInstallDir()),
            "mkdir -p ./lib/dropins/")
        .execute();

    SshMachineLocation machine = getMachine();
    BrooklynNode entity = getEntity();

    String brooklynGlobalPropertiesRemotePath =
        entity.getConfig(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_REMOTE_PATH);
    String brooklynGlobalPropertiesContents =
        entity.getConfig(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_CONTENTS);
    String brooklynGlobalPropertiesUri =
        entity.getConfig(BrooklynNode.BROOKLYN_GLOBAL_PROPERTIES_URI);

    String brooklynLocalPropertiesRemotePath =
        processTemplateContents(
            entity.getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_REMOTE_PATH));
    String brooklynLocalPropertiesContents =
        entity.getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_CONTENTS);
    String brooklynLocalPropertiesUri =
        entity.getConfig(BrooklynNode.BROOKLYN_LOCAL_PROPERTIES_URI);

    String brooklynCatalogRemotePath = entity.getConfig(BrooklynNode.BROOKLYN_CATALOG_REMOTE_PATH);
    String brooklynCatalogContents = entity.getConfig(BrooklynNode.BROOKLYN_CATALOG_CONTENTS);
    String brooklynCatalogUri = entity.getConfig(BrooklynNode.BROOKLYN_CATALOG_URI);

    // Override the ~/.brooklyn/brooklyn.properties if required
    if (brooklynGlobalPropertiesContents != null || brooklynGlobalPropertiesUri != null) {
      ExistingFileBehaviour onExisting = entity.getConfig(BrooklynNode.ON_EXISTING_PROPERTIES_FILE);
      Integer checkExists =
          DynamicTasks.queue(
                  SshEffectorTasks.ssh("ls \"" + brooklynGlobalPropertiesRemotePath + "\"")
                      .allowingNonZeroExitCode())
              .get();
      boolean doUpload = true;
      if (checkExists == 0) {
        switch (onExisting) {
          case USE_EXISTING:
            doUpload = false;
            break;
          case OVERWRITE:
            break;
          case DO_NOT_USE:
            throw new IllegalStateException(
                "Properties file "
                    + brooklynGlobalPropertiesContents
                    + " already exists and "
                    + "even though it is not being used, content for it was supplied");
          case FAIL:
            throw new IllegalStateException(
                "Properties file "
                    + brooklynGlobalPropertiesContents
                    + " already exists and "
                    + BrooklynNode.ON_EXISTING_PROPERTIES_FILE
                    + " response is to fail");
          default:
            throw new IllegalStateException(
                "Properties file "
                    + brooklynGlobalPropertiesContents
                    + " already exists and "
                    + BrooklynNode.ON_EXISTING_PROPERTIES_FILE
                    + " response "
                    + onExisting
                    + " is unknown");
        }
      }
      if (onExisting == ExistingFileBehaviour.DO_NOT_USE) {
        log.warn(
            "Global properties supplied when told not to use them; no global properties exists, so it will be installed, but it will not be used.");
      }
      if (doUpload)
        uploadFileContents(
            brooklynGlobalPropertiesContents,
            brooklynGlobalPropertiesUri,
            brooklynGlobalPropertiesRemotePath);
    }

    // Upload a local-brooklyn.properties if required
    if (brooklynLocalPropertiesContents != null || brooklynLocalPropertiesUri != null) {
      uploadFileContents(
          brooklynLocalPropertiesContents,
          brooklynLocalPropertiesUri,
          brooklynLocalPropertiesRemotePath);
    }

    // Override the ~/.brooklyn/catalog.xml if required
    if (brooklynCatalogContents != null || brooklynCatalogUri != null) {
      uploadFileContents(brooklynCatalogContents, brooklynCatalogUri, brooklynCatalogRemotePath);
    }

    // Copy additional resources to the server
    for (Map.Entry<String, String> entry :
        getEntity().getAttribute(BrooklynNode.COPY_TO_RUNDIR).entrySet()) {
      Map<String, String> substitutions = ImmutableMap.of("RUN", getRunDir());
      String localResource = entry.getKey();
      String remotePath = entry.getValue();
      String resolvedRemotePath = remotePath;
      for (Map.Entry<String, String> substitution : substitutions.entrySet()) {
        String key = substitution.getKey();
        String val = substitution.getValue();
        resolvedRemotePath =
            resolvedRemotePath.replace("${" + key + "}", val).replace("$" + key, val);
      }
      machine.copyTo(
          MutableMap.of("permissions", "0600"),
          resource.getResourceFromUrl(localResource),
          resolvedRemotePath);
    }

    for (Object entry : getEntity().getClasspath()) {
      String filename = null;
      String url = null;

      if (entry instanceof String) {
        url = (String) entry;
      } else {
        if (entry instanceof Map) {
          url = (String) ((Map) entry).get("url");
          filename = (String) ((Map) entry).get("filename");
        }
      }
      checkNotNull(url, "url");

      // If a local folder, then create archive from contents first
      if (Urls.isDirectory(url)) {
        File jarFile = ArchiveBuilder.jar().addDirContentsAt(new File(url), "").create();
        url = jarFile.getAbsolutePath();
      }

      if (filename == null) {
        // Determine filename
        filename = getFilename(url);
      }
      ArchiveUtils.deploy(
          MutableMap.<String, Object>of(),
          url,
          machine,
          getRunDir(),
          Os.mergePaths(getRunDir(), "lib", "dropins"),
          filename);
    }

    String cmd = entity.getConfig(BrooklynNode.EXTRA_CUSTOMIZATION_SCRIPT);
    if (Strings.isNonBlank(cmd)) {
      DynamicTasks.queueIfPossible(
              SshEffectorTasks.ssh(cmd)
                  .summary("Bespoke BrooklynNode customization script")
                  .requiringExitCodeZero())
          .orSubmitAndBlock(getEntity());
    }
  }