blob: e06617dafa5cff60983280b097f15c5ce6b6d62d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.sling.testing.teleporter.client;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonException;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.json.JsonValue.ValueType;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.io.IOUtils;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runners.model.MultipleFailureException;
/** Barebones HTTP client that supports just what the teleporter needs,
* with no dependencies outside of java.* and org.junit. Prevents us
* from imposing a particular HTTP client version.
*/
class TeleporterHttpClient {
private final String CHARSET = "UTF-8";
private final String baseUrl;
private String credentials = null;
private final String testServletPath;
static final class SimpleHttpResponse {
private final int status;
private final String body;
public SimpleHttpResponse(int status, String body) {
super();
this.status = status;
this.body = body;
}
public int getStatus() {
return status;
}
public String getBody() {
return body;
}
}
TeleporterHttpClient(String baseUrl, String testServletPath) {
this.baseUrl = baseUrl;
if(!testServletPath.endsWith("/")) {
testServletPath += "/";
}
this.testServletPath = testServletPath;
}
void setCredentials(String cred) {
credentials = cred;
}
public void setConnectionCredentials(URLConnection c) {
if(credentials != null && !credentials.isEmpty()) {
final String basicAuth = "Basic " + new String(DatatypeConverter.printBase64Binary(credentials.getBytes()));
c.setRequestProperty ("Authorization", basicAuth);
}
}
/** Wait until specified URL returns specified status */
public String waitForStatus(String url, int expectedStatus, int timeoutMsec) throws IOException {
final long end = System.currentTimeMillis() + timeoutMsec;
final Set<Integer> statusSet = new HashSet<Integer>();
final ExponentialBackoffDelay d = new ExponentialBackoffDelay(50, 250);
while(System.currentTimeMillis() < end) {
try {
final SimpleHttpResponse response = getHttpGetStatus(url);
statusSet.add(response.getStatus());
if(response.getStatus() == expectedStatus) {
return response.getBody();
}
d.waitNextDelay();
} catch(Exception ignore) {
}
}
throw new IOException("Did not get status " + expectedStatus + " at " + url + " after " + timeoutMsec + " msec, got " + statusSet);
}
void installBundle(InputStream bundle, String bundleSymbolicName, int webConsoleReadyTimeoutSeconds) throws MalformedURLException, IOException {
// Equivalent of
// curl -u admin:admin -F action=install -Fbundlestart=1 -Fbundlefile=@somefile.jar http://localhost:8080/system/console/bundles
final String url = baseUrl + "/system/console/bundles";
final String contentType = "application/octet-stream";
final HttpURLConnection c = (HttpURLConnection)new URL(url).openConnection();
waitForStatus(url, 200, webConsoleReadyTimeoutSeconds * 1000);
try {
setConnectionCredentials(c);
new MultipartAdapter(c, CHARSET)
.parameter("action", "install")
.parameter("bundlestart", "1")
.file("bundlefile", bundleSymbolicName + ".jar", contentType, bundle)
.close();
final int status = c.getResponseCode();
if(status != 302) {
throw new IOException("Got status code " + status + " for " + url);
}
} finally {
cleanup(c);
}
}
void verifyCorrectBundleState(String bundleSymbolicName, int webConsoleReadyTimeoutSeconds) throws IOException {
final String url = baseUrl + "/system/console/bundles/" + bundleSymbolicName + ".json";
String jsonBody = waitForStatus(url, 200, webConsoleReadyTimeoutSeconds * 1000);
// deserialize json (https://issues.apache.org/jira/browse/SLING-6536)
try {
JsonReader jsonReader = Json.createReader(new StringReader(jsonBody));
// extract state
JsonArray jsonArray = jsonReader.readObject().getJsonArray("data");
if (jsonArray == null) {
throw new JsonException("Could not find 'data' array");
}
JsonObject bundleObject = jsonArray.getJsonObject(0);
String state = bundleObject.getString("state");
if ("Active".equals(state)) {
return;
}
// otherwise evaluate the import section
JsonArray propsArray = bundleObject.getJsonArray("props");
if (propsArray == null) {
throw new JsonException("Could not find 'props' object");
}
// iterate through all of them until key="Imported Packages" is found
for (JsonValue propValue : propsArray) {
if (propValue.getValueType().equals(ValueType.OBJECT)) {
JsonObject propObject = (JsonObject)propValue;
if ("Imported Packages".equals(propObject.getString("key"))) {
JsonArray importedPackagesArray = propObject.getJsonArray("value");
String reason = "Unknown";
for (JsonValue importedPackageValue : importedPackagesArray) {
if (importedPackageValue.getValueType().equals(ValueType.STRING)) {
String importedPackage = ((JsonString)importedPackageValue).getString();
if (importedPackage.startsWith("ERROR:")) {
reason = importedPackage;
}
}
}
throw new IllegalStateException("The test bundle is in state " + state +". Most probably this is due to unresolved import-packages: " + reason);
}
}
}
} catch (JsonException|IndexOutOfBoundsException e) {
throw new IllegalArgumentException("Test bundle '" + bundleSymbolicName +"' not correctly installed. Could not parse JSON response though to expose further information: " + jsonBody, e);
}
}
void uninstallBundle(String bundleSymbolicName, int webConsoleReadyTimeoutSeconds) throws MalformedURLException, IOException {
// equivalent of
// curl -u admin:admin -F action=uninstall http://localhost:8080/system/console/bundles/$N
final String url = baseUrl + "/system/console/bundles/" + bundleSymbolicName;
final HttpURLConnection c = (HttpURLConnection)new URL(url).openConnection();
waitForStatus(url, 200, webConsoleReadyTimeoutSeconds * 1000);
try {
setConnectionCredentials(c);
new MultipartAdapter(c, CHARSET)
.parameter("action", "uninstall")
.close();
final int status = c.getResponseCode();
if(status != 200) {
throw new IOException("Got status code " + status + " for " + url);
}
} finally {
cleanup(c);
}
}
public SimpleHttpResponse getHttpGetStatus(String url) throws MalformedURLException, IOException {
final HttpURLConnection c = (HttpURLConnection)new URL(url).openConnection();
setConnectionCredentials(c);
c.setUseCaches(false);
c.setDoOutput(true);
c.setDoInput(true);
c.setInstanceFollowRedirects(false);
boolean gotStatus = false;
int status = 0;
try {
status = c.getResponseCode();
gotStatus = true;
//4xx: client error, 5xx: server error. See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
boolean isError = status >= 400;
//In HTTP error cases, HttpURLConnection only gives you the input stream via #getErrorStream().
InputStream is = isError ? c.getErrorStream() : c.getInputStream();
StringWriter writer = new StringWriter();
IOUtils.copy(is, writer, StandardCharsets.UTF_8);
return new SimpleHttpResponse(status, writer.toString());
} finally {
// If we didn't get a status, do not attempt
// to get input streams as this would retry connecting
cleanup(c, gotStatus);
}
}
void runTests(String testSelectionPath, int testReadyTimeoutSeconds) throws MalformedURLException, IOException, MultipleFailureException {
final String testUrl = baseUrl + "/" + testServletPath + testSelectionPath + ".junit_result";
// Wait for non-404 response that signals that test bundle is ready
final long timeout = System.currentTimeMillis() + (testReadyTimeoutSeconds * 1000L);
final ExponentialBackoffDelay delay = new ExponentialBackoffDelay(25, 1000);
while(true) {
if(getHttpGetStatus(testUrl).getStatus() == 200) {
break;
}
if(System.currentTimeMillis() > timeout) {
fail("Timeout waiting for test at " + testUrl + " (" + testReadyTimeoutSeconds + " seconds)");
break;
}
delay.waitNextDelay();
}
final HttpURLConnection c = (HttpURLConnection)new URL(testUrl).openConnection();
try {
setConnectionCredentials(c);
c.setRequestMethod("POST");
c.setUseCaches(false);
c.setDoOutput(true);
c.setDoInput(true);
c.setInstanceFollowRedirects(false);
final int status = c.getResponseCode();
if(status != 200) {
throw new IOException("Got status code " + status + " for " + testUrl);
}
final Result result = (Result)new ObjectInputStream(c.getInputStream()).readObject();
if(result.getFailureCount() > 0) {
final List<Throwable> failures = new ArrayList<Throwable>(result.getFailureCount());
for (Failure f : result.getFailures()) {
failures.add(f.getException());
}
throw new MultipleFailureException(failures);
}
} catch(ClassNotFoundException e) {
throw new IOException("Exception reading test results:" + e, e);
} finally {
cleanup(c);
}
}
private void consumeAndClose(InputStream is) throws IOException {
if(is == null) {
return;
}
final byte [] buffer = new byte[16384];
while(is.read(buffer) != -1) {
// nothing to do, just consume the stream
}
is.close();
}
private void cleanup(HttpURLConnection c) {
cleanup(c, true);
}
private void cleanup(HttpURLConnection c, boolean includeInputStreams) {
if(includeInputStreams) {
try {
consumeAndClose(c.getInputStream());
} catch(IOException ignored) {
}
try {
consumeAndClose(c.getErrorStream());
} catch(IOException ignored) {
}
}
c.disconnect();
}
}