PackageManagerHelper.java
/*
* #%L
* wcm.io
* %%
* Copyright (C) 2014 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;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.regex.Pattern;
import javax.net.ssl.SSLContext;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpRequest;
import org.apache.http.HttpRequestInterceptor;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.AuthState;
import org.apache.http.auth.Credentials;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.auth.BasicScheme;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HttpContext;
import org.apache.http.ssl.SSLContextBuilder;
import org.jdom2.Document;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.wcm.tooling.commons.packmgr.httpaction.BundleStatus;
import io.wcm.tooling.commons.packmgr.httpaction.BundleStatusCall;
import io.wcm.tooling.commons.packmgr.httpaction.HttpCall;
import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerHtmlCall;
import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerHtmlMessageCall;
import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerInstallStatus;
import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerInstallStatusCall;
import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerJsonCall;
import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerStatusCall;
import io.wcm.tooling.commons.packmgr.httpaction.PackageManagerXmlCall;
import io.wcm.tooling.commons.packmgr.util.HttpClientUtil;
/**
* Common functionality for all mojos.
*/
public final class PackageManagerHelper {
/**
* Prefix or error message from CRX HTTP interfaces when uploading a package that already exists.
*/
public static final String CRX_PACKAGE_EXISTS_ERROR_MESSAGE_PREFIX = "Package already exists: ";
private static final String HTTP_CONTEXT_ATTRIBUTE_PREEMPTIVE_AUTHENTICATION_CREDS = PackageManagerHelper.class.getName() + "_PreemptiveAuthenticationCreds";
private static final String HTTP_CONTEXT_ATTRIBUTE_OAUTH2_ACCESS_TOKEN = PackageManagerHelper.class.getName() + "_oauth2AccessToken";
private final PackageManagerProperties props;
private static final Logger log = LoggerFactory.getLogger(PackageManagerHelper.class);
/**
* @param props Package manager properties
*/
public PackageManagerHelper(PackageManagerProperties props) {
this.props = props;
}
/**
* Get HTTP client to be used for all communications (package manager and Felix console).
* @return HTTP client
*/
public @NotNull CloseableHttpClient getHttpClient() {
HttpClientBuilder httpClientBuilder = HttpClients.custom()
// keep reusing connections to a minimum - may conflict when instance is restarting and responds in unexpected manner
.setKeepAliveStrategy((response, context) -> 1)
.addInterceptorFirst(new HttpRequestInterceptor() {
@Override
public void process(HttpRequest request, HttpContext context) throws HttpException, IOException {
Credentials credentials = (Credentials)context.getAttribute(HTTP_CONTEXT_ATTRIBUTE_PREEMPTIVE_AUTHENTICATION_CREDS);
if (credentials != null) {
// enable preemptive authentication
AuthState authState = (AuthState)context.getAttribute(HttpClientContext.TARGET_AUTH_STATE);
authState.update(new BasicScheme(), credentials);
}
String oauth2AccessToken = (String)context.getAttribute(HTTP_CONTEXT_ATTRIBUTE_OAUTH2_ACCESS_TOKEN);
if (oauth2AccessToken != null) {
// send OAuth 2 bearer token
request.setHeader("Authorization", "Bearer " + oauth2AccessToken);
}
}
});
// relaxed SSL check
if (props.isRelaxedSSLCheck()) {
try {
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, new TrustSelfSignedStrategy()).build();
SSLConnectionSocketFactory sslsf = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
httpClientBuilder.setSSLSocketFactory(sslsf);
}
catch (KeyManagementException | KeyStoreException | NoSuchAlgorithmException ex) {
throw new PackageManagerException("Could not set relaxedSSLCheck", ex);
}
}
// proxy support
Proxy proxy = getProxyForUrl(props.getPackageManagerUrl());
if (proxy != null) {
httpClientBuilder.setProxy(new HttpHost(proxy.getHost(), proxy.getPort(), proxy.getProtocol()));
}
return httpClientBuilder.build();
}
/**
* Set up http client context with credentials for CRX package manager.
* @return Http client context
*/
public @NotNull HttpClientContext getPackageManagerHttpClientContext() {
return getHttpClientContext(props.getPackageManagerUrl(),
props.getUserId(), props.getPassword(), props.getOAuth2AccessToken());
}
/**
* Set up http client context with credentials for Felix console.
* @return Http client context. May be null of bundle status URL is set to "-".
*/
public @Nullable HttpClientContext getConsoleHttpClientContext() {
String bundleStatusUrl = props.getBundleStatusUrl();
if (bundleStatusUrl == null) {
return null;
}
return getHttpClientContext(bundleStatusUrl,
props.getConsoleUserId(), props.getConsolePassword(), props.getConsoleOAuth2AccessToken());
}
private @NotNull HttpClientContext getHttpClientContext(String url, String userId, String password, String oauth2AccessToken) {
URI uri;
try {
uri = new URI(url);
}
catch (URISyntaxException ex) {
throw new PackageManagerException("Invalid url: " + url, ex);
}
final CredentialsProvider credsProvider = new BasicCredentialsProvider();
HttpClientContext context = new HttpClientContext();
context.setCredentialsProvider(credsProvider);
if (StringUtils.isNotBlank(oauth2AccessToken)) {
context.setAttribute(HTTP_CONTEXT_ATTRIBUTE_OAUTH2_ACCESS_TOKEN, oauth2AccessToken);
}
else {
// use basic (preemptive) authentication with username/password
final AuthScope authScope = new AuthScope(uri.getHost(), uri.getPort());
final Credentials credentials = new UsernamePasswordCredentials(userId, password);
credsProvider.setCredentials(authScope, credentials);
context.setAttribute(HTTP_CONTEXT_ATTRIBUTE_PREEMPTIVE_AUTHENTICATION_CREDS, credentials);
}
// timeout settings
context.setRequestConfig(HttpClientUtil.buildRequestConfig(props));
// proxy support
Proxy proxy = getProxyForUrl(url);
if (proxy != null && proxy.useAuthentication()) {
AuthScope proxyAuthScope = new AuthScope(proxy.getHost(), proxy.getPort());
Credentials proxyCredentials = new UsernamePasswordCredentials(proxy.getUsername(), proxy.getPassword());
credsProvider.setCredentials(proxyAuthScope, proxyCredentials);
}
return context;
}
/**
* Get proxy for given URL
* @param requestUrl Request URL
* @return Proxy or null if none matching found
*/
private Proxy getProxyForUrl(String requestUrl) {
List<Proxy> proxies = props.getProxies();
if (proxies == null || proxies.isEmpty()) {
return null;
}
final URI uri = URI.create(requestUrl);
for (Proxy proxy : proxies) {
if (!proxy.isNonProxyHost(uri.getHost())) {
return proxy;
}
}
return null;
}
/**
* Execute HTTP call with automatic retry as configured for the MOJO.
* @param call HTTP call
* @param runCount Number of runs this call was already executed
*/
@SuppressWarnings("PMD.GuardLogStatement")
private <T> T executeHttpCallWithRetry(HttpCall<T> call, int runCount) {
try {
return call.execute();
}
catch (PackageManagerHttpActionException ex) {
// retry again if configured so...
if (runCount < props.getRetryCount()) {
log.warn("ERROR: {}", ex.getMessage());
log.debug("HTTP call failed.", ex);
log.warn("---------------");
StringBuilder msg = new StringBuilder();
msg.append("HTTP call failed, try again (" + (runCount + 1) + "/" + props.getRetryCount() + ")");
if (props.getRetryDelaySec() > 0) {
msg.append(" after " + props.getRetryDelaySec() + " second(s)");
}
msg.append("...");
log.warn(msg.toString());
if (props.getRetryDelaySec() > 0) {
try {
Thread.sleep(props.getRetryDelaySec() * DateUtils.MILLIS_PER_SECOND);
}
catch (InterruptedException ex1) {
// ignore
}
}
return executeHttpCallWithRetry(call, runCount + 1);
}
else {
throw ex;
}
}
}
/**
* Execute CRX HTTP Package manager method and parse JSON response.
* @param httpClient HTTP client
* @param context HTTP client context
* @param method Get or Post method
* @return JSON object
*/
public JSONObject executePackageManagerMethodJson(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
PackageManagerJsonCall call = new PackageManagerJsonCall(httpClient, context, method);
return executeHttpCallWithRetry(call, 0);
}
/**
* Execute CRX HTTP Package manager method and parse XML response.
* @param httpClient HTTP client
* @param context HTTP client context
* @param method Get or Post method
* @return XML document
*/
public Document executePackageManagerMethodXml(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
PackageManagerXmlCall call = new PackageManagerXmlCall(httpClient, context, method);
return executeHttpCallWithRetry(call, 0);
}
/**
* Execute CRX HTTP Package manager method and get HTML response.
* @param httpClient HTTP client
* @param context HTTP client context
* @param method Get or Post method
* @return Response from HTML server
*/
public String executePackageManagerMethodHtml(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
PackageManagerHtmlCall call = new PackageManagerHtmlCall(httpClient, context, method);
return executeHttpCallWithRetry(call, 0);
}
/**
* Execute CRX HTTP Package manager method and output HTML response.
* @param httpClient HTTP client
* @param context HTTP client context
* @param method Get or Post method
*/
public void executePackageManagerMethodHtmlOutputResponse(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
PackageManagerHtmlMessageCall call = new PackageManagerHtmlMessageCall(httpClient, context, method, props);
executeHttpCallWithRetry(call, 0);
}
/**
* Execute CRX HTTP Package manager method and checks response status. If the response status is not 200 the call
* fails (after retrying).
* @param httpClient HTTP client
* @param context HTTP client context
* @param method Get or Post method
*/
public void executePackageManagerMethodStatus(CloseableHttpClient httpClient, HttpClientContext context, HttpRequestBase method) {
PackageManagerStatusCall call = new PackageManagerStatusCall(httpClient, context, method);
executeHttpCallWithRetry(call, 0);
}
/**
* Wait for bundles to become active.
* @param httpClient HTTP client
* @param context HTTP client context
*/
@SuppressWarnings("PMD.GuardLogStatement")
public void waitForBundlesActivation(CloseableHttpClient httpClient, HttpClientContext context) {
if (StringUtils.isBlank(props.getBundleStatusUrl())) {
log.debug("Skipping check for bundle activation state because no bundleStatusURL is defined.");
return;
}
final int WAIT_INTERVAL_SEC = 3;
final long CHECK_RETRY_COUNT = props.getBundleStatusWaitLimitSec() / WAIT_INTERVAL_SEC;
log.info("Check bundle activation status...");
for (int i = 1; i <= CHECK_RETRY_COUNT; i++) {
BundleStatusCall call = new BundleStatusCall(httpClient, context, props.getBundleStatusUrl(),
props.getBundleStatusWhitelistBundleNames());
BundleStatus bundleStatus = executeHttpCallWithRetry(call, 0);
boolean instanceReady = true;
// check if bundles are still stopping/staring
if (!bundleStatus.isAllBundlesRunning()) {
log.info("Bundles starting/stopping: {} - wait {} sec (max. {} sec) ...",
bundleStatus.getStatusLineCompact(), WAIT_INTERVAL_SEC, props.getBundleStatusWaitLimitSec());
sleep(WAIT_INTERVAL_SEC);
instanceReady = false;
}
// check if any of the blacklisted bundles is still present
if (instanceReady) {
for (Pattern blacklistBundleNamePattern : props.getBundleStatusBlacklistBundleNames()) {
String bundleSymbolicName = bundleStatus.getMatchingBundle(blacklistBundleNamePattern);
if (bundleSymbolicName != null) {
log.info("Bundle '{}' is still deployed - wait {} sec (max. {} sec) ...",
bundleSymbolicName, WAIT_INTERVAL_SEC, props.getBundleStatusWaitLimitSec());
sleep(WAIT_INTERVAL_SEC);
instanceReady = false;
break;
}
}
}
// instance is ready
if (instanceReady) {
break;
}
}
}
/**
* Wait for package manager install status to become finished.
* @param httpClient HTTP client
* @param context HTTP client context
*/
@SuppressWarnings("PMD.GuardLogStatement")
public void waitForPackageManagerInstallStatusFinished(CloseableHttpClient httpClient, HttpClientContext context) {
if (StringUtils.isBlank(props.getPackageManagerInstallStatusURL())) {
log.debug("Skipping check for package manager install state because no packageManagerInstallStatusURL is defined.");
return;
}
final int WAIT_INTERVAL_SEC = 3;
final long CHECK_RETRY_COUNT = props.getPackageManagerInstallStatusWaitLimitSec() / WAIT_INTERVAL_SEC;
log.info("Check package manager installation status...");
for (int i = 1; i <= CHECK_RETRY_COUNT; i++) {
PackageManagerInstallStatusCall call = new PackageManagerInstallStatusCall(httpClient, context,
props.getPackageManagerInstallStatusURL());
PackageManagerInstallStatus packageManagerStatus = executeHttpCallWithRetry(call, 0);
boolean instanceReady = true;
// check if package manager is still installing packages
if (!packageManagerStatus.isFinished()) {
log.info("Packager manager not ready: {} packages left for installation - wait {} sec (max. {} sec) ...",
packageManagerStatus.getItemCount(), WAIT_INTERVAL_SEC, props.getPackageManagerInstallStatusWaitLimitSec());
sleep(WAIT_INTERVAL_SEC);
instanceReady = false;
}
// instance is ready
if (instanceReady) {
break;
}
}
}
private void sleep(int sec) {
try {
Thread.sleep(sec * DateUtils.MILLIS_PER_SECOND);
}
catch (InterruptedException e) {
// ignore
}
}
}