CrxPackageInstaller.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2017 wcm.io
 * %%
 * 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.
 * #L%
 */
package io.wcm.tooling.commons.packmgr.install.crx;

import static io.wcm.tooling.commons.packmgr.PackageManagerHelper.CRX_PACKAGE_EXISTS_ERROR_MESSAGE_PREFIX;

import java.io.IOException;
import java.net.URISyntaxException;
import java.util.Map;

import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.mime.MultipartEntityBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.jackrabbit.vault.packaging.PackageProperties;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.wcm.tooling.commons.packmgr.PackageManagerException;
import io.wcm.tooling.commons.packmgr.PackageManagerHelper;
import io.wcm.tooling.commons.packmgr.PackageManagerProperties;
import io.wcm.tooling.commons.packmgr.install.PackageFile;
import io.wcm.tooling.commons.packmgr.install.VendorInstallerFactory;
import io.wcm.tooling.commons.packmgr.install.VendorPackageInstaller;
import io.wcm.tooling.commons.packmgr.util.ContentPackageProperties;
import io.wcm.tooling.commons.packmgr.util.HttpClientUtil;

/**
 * Package Installer for AEM's CRX Package Manager
 */
public class CrxPackageInstaller implements VendorPackageInstaller {

  private final String url;

  private static final Logger log = LoggerFactory.getLogger(CrxPackageInstaller.class);

  /**
   * @param url URL
   */
  public CrxPackageInstaller(String url) {
    this.url = url;
  }

  @Override
  public void installPackage(PackageFile packageFile, boolean replicate, PackageManagerHelper pkgmgr,
      CloseableHttpClient httpClient, HttpClientContext packageManagerHttpClientContext, HttpClientContext consoleHttpClientContext,
      PackageManagerProperties props) throws IOException, PackageManagerException {

    boolean force = packageFile.isForce();

    if (force) {
      // in force mode, just check that package manager is available and then start uploading
      ensurePackageManagerAvailability(pkgmgr, httpClient, packageManagerHttpClientContext);
    }
    else {
      // otherwise check if package is already installed first, and skip further processing if it is
      // this implicitly also checks the availability of the package manager
      PackageInstalledStatus status = getPackageInstalledStatus(packageFile, pkgmgr, httpClient, packageManagerHttpClientContext);
      switch (status) {
        case NOT_FOUND:
          log.debug("Package is not found in package list: proceed with install.");
          break;
        case INSTALLED:
          log.info("Package skipped because it was already uploaded.");
          return;
        case UPLOADED:
          log.info("Package was already uploaded but not installed: proceed with install and switch to force mode.");
          force = true;
          break;
        case INSTALLED_OTHER_VERSION:
          log.info("Package was already uploaded, but another version was installed more recently: proceed with install and switch to force mode.");
          force = true;
          break;
        default:
          throw new PackageManagerException("Unexpected status: " + status);
      }
    }

    // prepare post method
    HttpPost post = new HttpPost(url + "/.json?cmd=upload");
    HttpClientUtil.applyRequestConfig(post, packageFile, props);
    MultipartEntityBuilder entityBuilder = MultipartEntityBuilder.create()
        .addBinaryBody("package", packageFile.getFile());
    if (force) {
      entityBuilder.addTextBody("force", "true");
    }
    post.setEntity(entityBuilder.build());

    // execute post
    JSONObject jsonResponse = pkgmgr.executePackageManagerMethodJson(httpClient, packageManagerHttpClientContext, post);
    boolean success = jsonResponse.optBoolean("success", false);
    String msg = jsonResponse.optString("msg", null);
    String path = jsonResponse.optString("path", null);
    if (success) {
      if (packageFile.isInstall()) {
        log.info("Package uploaded to {}, now installing...", path);

        try {
          post = new HttpPost(url + "/console.html" + new URIBuilder().setPath(path).build().getRawPath() + "?cmd=install"
              + (packageFile.isRecursive() ? "&recursive=true" : ""));
          HttpClientUtil.applyRequestConfig(post, packageFile, props);
        }
        catch (URISyntaxException ex) {
          throw new PackageManagerException("Invalid path: " + path, ex);
        }

        // execute post
        pkgmgr.executePackageManagerMethodHtmlOutputResponse(httpClient, packageManagerHttpClientContext, post);

        // delay further processing after install (if activated)
        delay(packageFile.getDelayAfterInstallSec());

        // after install: if bundles are still stopping/starting, wait for completion
        pkgmgr.waitForBundlesActivation(httpClient, consoleHttpClientContext);
        // after install: if packages are still installing, wait for completion
        pkgmgr.waitForPackageManagerInstallStatusFinished(httpClient, packageManagerHttpClientContext);
      }
      else {
        log.info("Package uploaded successfully to {} (without installing).", path);
      }
    }
    else if (StringUtils.startsWith(msg, CRX_PACKAGE_EXISTS_ERROR_MESSAGE_PREFIX) && !force) {
      log.info("Package skipped because it was already uploaded.");
    }
    else {
      throw new PackageManagerException("Package upload failed: " + msg);
    }

    // replicate content package
    if (success && replicate) {
      log.info("Replicate package {}...", path);

      try {
        post = new HttpPost(url + "/console.html" + new URIBuilder().setPath(path).build().getRawPath() + "?cmd=replicate");
        HttpClientUtil.applyRequestConfig(post, packageFile, props);
      }
      catch (URISyntaxException ex) {
        throw new PackageManagerException("Invalid path: " + path, ex);
      }

      // execute post
      pkgmgr.executePackageManagerMethodHtmlOutputResponse(httpClient, packageManagerHttpClientContext, post);
    }
  }

  @SuppressWarnings("PMD.GuardLogStatement")
  private void delay(int seconds) {
    if (seconds > 0) {
      log.info("Wait {} seconds after package install...", seconds);
      try {
        Thread.sleep(seconds * 1000L);
      }
      catch (InterruptedException ex) {
        // ignore
      }
    }
  }

  private void ensurePackageManagerAvailability(PackageManagerHelper pkgmgr, CloseableHttpClient httpClient, HttpClientContext context) {
    // do a help GET call before upload to ensure package manager is running
    HttpGet get = new HttpGet(url + ".jsp?cmd=help");
    pkgmgr.executePackageManagerMethodStatus(httpClient, context, get);
  }

  private PackageInstalledStatus getPackageInstalledStatus(PackageFile packageFile, PackageManagerHelper pkgmgr,
      CloseableHttpClient httpClient, HttpClientContext context) throws IOException {
    // list packages in AEM instances and check for exact match
    String baseUrl = VendorInstallerFactory.getBaseUrl(url);
    String packageListUrl = baseUrl + PackageInstalledChecker.PACKMGR_LIST_URL;
    HttpGet get = new HttpGet(packageListUrl);
    JSONObject result = pkgmgr.executePackageManagerMethodJson(httpClient, context, get);

    Map<String, Object> props = ContentPackageProperties.get(packageFile.getFile());
    String group = (String)props.get(PackageProperties.NAME_GROUP);
    String name = (String)props.get(PackageProperties.NAME_NAME);
    String version = (String)props.get(PackageProperties.NAME_VERSION);

    PackageInstalledChecker checker = new PackageInstalledChecker(result);
    return checker.getStatus(group, name, version);
  }

}