blob: 1087158637d4ba42eec25cfd4d89fed13cad97ab [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.clients;
import org.apache.http.*;
import org.apache.http.annotation.Immutable;
import org.apache.http.client.*;
import org.apache.http.client.methods.*;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.message.BasicHttpRequest;
import org.apache.http.protocol.HttpContext;
import org.apache.sling.testing.clients.util.HttpUtils;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* The abstract base client for all implementing integration test clients.
*/
@Immutable
public class AbstractSlingClient implements HttpClient, Closeable {
private final org.slf4j.Logger log = LoggerFactory.getLogger(getClass());
private final static URI slash = URI.create("/");
/**
* The clientId for the client, generated automatically during instantiation of client.
*/
private final String clientId;
/**
* The HttpClient object to which http calls are delegated.
* It can be shared across multiple AbstractSlingClients (by using adaptTo())
*/
private final CloseableHttpClient http;
/**
* A wrapper object containing the sling config for this client.
* It can be shared across multiple AbstractSlingClients (by using adaptTo())
*/
private final SlingClientConfig config;
/**
* Constructor used by Builders and adaptTo(). <b>Should never be called directly from the code.</b>
*
* @param http http client to handle the delegated calls
* @param config immutable object holding the config
* @throws ClientException if the client could not be initialized
*/
AbstractSlingClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
// Generate client ID
this.clientId = this.getClass() + "-" + UUID.randomUUID().toString();
this.http = http;
this.config = config;
}
/**
* Returns the unique id for this client, generated automatically during instantiation.<br>
*
* @return client's unique id
*/
protected String getClientId() {
return clientId;
}
/**
* <p>Base HTTP URI of the server under test. It includes the context path, if present, and always ends with a slash</p>
* <p>Example: {@code http://localhost:8080/a/}</p>
*
* @return the server's URL
*/
public URI getUrl() {
return config.getUrl();
}
/**
* Returns the name of the user that will be used to authenticate the requests (by basic auth, if not replaced).
*
* @return user's name
*/
public String getUser() {
return config.getUser();
}
/**
* Returns the password of the user that will be used to authenticate the requests (by basic auth, if not replaced).
*
* @return user's password
*/
public String getPassword() {
return config.getPassword();
}
/**
* <p>Gets the full URL for a given path.</p>
*
* <p>The input path is considered relative to server url path ("/" or context path), even though it starts with a slash.
* The path is relativized and appended to the {@code server url}.</p>
*
* <p>Note: in the case of a server url with context path - the input path should not contain the context path, otherwise
* it will be duplicated in the resulting url</p>
*
* @param path the relative path
* @return the absolute URI
* @throws IllegalArgumentException if path cannot be parsed into an URI
* @throws NullPointerException if path is null
*/
public URI getUrl(String path) {
try {
URI pathUri = slash.relativize(new URI(path));
return getUrl().resolve(pathUri);
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
/**
* Creates a full URL for a given path with additional parameters. Same as {@link #getUrl(String)}, but adds the parameters in the URI.
*
* @param path path relative to server url; can start with / but should not include the server context path
* @param parameters url parameters to be added to the url. If the given argument is {@code null}, nothing will be added to the url.
* If the given argument is an empty array, it will force a "?" at the end of the url.
* @return full url as URI
* @throws IllegalArgumentException if path or parameters cannot be parsed into an URI
* @throws NullPointerException if path is null
*/
public URI getUrl(String path, List<NameValuePair> parameters) {
// add server url and path
URIBuilder uriBuilder = new URIBuilder(getUrl(path));
// add parameters
if(parameters != null) {
uriBuilder.addParameters(parameters);
}
try {
return uriBuilder.build();
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
/**
* <p>Transforms an external {@code url} into a sling path, by subtracting the {@code server url} (incl. contextPath).
* The returned path will not contain the context path, so it can be used with {@link #getUrl(String)}</p>
*
* <p>The url can be absolute (incl. hostname) or relative to root (starts with "/").</p>
*
* <p>If the server url is not a prefix of the given url, it returns the given url</p>
*
* <p>If the url is just a path, it returns the path (with leading slash if not already present)</p>
*
* @param url full url
* @return sling path
*/
public URI getPath(URI url) {
// special case for urls that are server urls, but without trailing slash
if (url.relativize(getUrl()).equals(URI.create(""))) {
return slash;
}
URI contextPath = URI.create(getUrl().getPath());
URI relativeUrl = contextPath.relativize(slash.resolve(url));
if (relativeUrl.relativize(contextPath).equals(URI.create(""))) {
return slash;
}
return slash.resolve(getUrl().relativize(relativeUrl));
}
/**
* Extracts the relative sling path (to server url) from an url. Identical to {@link AbstractSlingClient#getPath(URI)},
* except that it also parses the String int URI
*
* @param url string containing the full url
* @return relative path as URI
* @throws IllegalArgumentException if the parameter cannot be parsed
* @throws NullPointerException if url is null
*/
public URI getPath(String url) {
try {
return getPath(new URI(url));
} catch (URISyntaxException e) {
throw new IllegalArgumentException(e);
}
}
/**
* <p>Returns an instance of any class extending the AbstractSlingClient. The new client will
* use the the same {@link HttpClient} and {@link SlingClientConfig} </p>
*
* @param clientClass the type of client requested, identified by its Class
* @param <T> any class extending the AbstractSlingClient
* @return instance of a class extending the AbstractSlingClient
* @throws ClientException if client can't be instantiated
*/
@SuppressWarnings("unchecked")
public <T extends AbstractSlingClient> T adaptTo(Class<T> clientClass) throws ClientException {
T client;
try {
Constructor cons = clientClass.getConstructor(CloseableHttpClient.class, SlingClientConfig.class);
client = (T) cons.newInstance(this.http, this.config);
} catch (Exception e) {
throw new ClientException("Could not initialize client: '" + clientClass.getCanonicalName() + "'.", e);
}
return client;
}
/**
* Gets the value for {@code key} from the generic values
*
* @param key the key
* @return the value
*/
public String getValue(String key) {
return this.config.getValues().get(key);
}
/**
* Adds the extra {@code key, value} to the generic values
*
* @param key the key for witch to add a value
* @param value the value
*/
public void addValue(String key, String value) {
this.config.getValues().put(key, value);
}
/**
* Checks whether the handler has the given generic value
*
* @param key the key
* @return true if the value was found
*/
public boolean hasValue(String key) {
return config.getValues().containsKey(key);
}
/**
* Returns the extra values map
*
* @return the map of values
*/
public Map<String, String> getValues() {
return config.getValues();
}
/**
* @return the cookie store reference
*/
public CookieStore getCookieStore() {
return config.getCookieStore();
}
/**
* @return the credentials provider
*/
public CredentialsProvider getCredentialsProvider() {
return config.getCredsProvider();
}
//
// HTTP convenience methods
//
/**
* <p>Executes an HTTP request, WITHOUT consuming the entity in the response. The caller is responsible for consuming the entity or
* closing the response's InputStream in order to release the connection.
* Otherwise, the client might run out of connections and will block</p>
*
* <p><b>Use this with caution and only if necessary for streaming</b>, otherwise use the safe method
* {@link #doRequest(HttpUriRequest, List, int...)}</p>
*
* <p>Adds the headers and checks the response against expected status</p>
*
* @param request the request to be executed
* @param headers optional headers to be added to the request
* @param expectedStatus if passed, the response status is checked against it/them, and has to match at least one of them
* @return the response, with the entity not consumed
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doStreamRequest(HttpUriRequest request, List<Header> headers, int... expectedStatus)
throws ClientException {
// create context from config
HttpClientContext context = createHttpClientContextFromConfig();
// add headers
if (headers != null) {
request.setHeaders(headers.toArray(new Header[headers.size()]));
}
try {
log.debug("request {} {}", request.getMethod(), request.getURI());
SlingHttpResponse response = new SlingHttpResponse(this.execute(request, context));
log.debug("response {}", HttpUtils.getHttpStatus(response));
// Check the status and throw a ClientException if it doesn't match expectedStatus, but close the entity before
if (expectedStatus != null && expectedStatus.length > 0) {
try {
HttpUtils.verifyHttpStatus(response, expectedStatus);
} catch (ClientException e) {
// catch the exception to make sure we close the entity before re-throwing it
response.close();
throw e;
}
}
return response;
} catch (IOException e) {
throw new ClientException("Could not execute http request", e);
}
}
/**
* <p>Executes a raw HTTP request, WITHOUT consuming the entity in the response. The caller is responsible for consuming the entity or
* closing the response's InputStream in order to release the connection.
* Otherwise, the client might run out of connections and will block</p>
*
* <p><b>Use this with caution and only if necessary for custom methods or for paths that must not be encoded</b>,
* otherwise use the safe method {@link #doRequest(HttpUriRequest, List, int...)}</p>
*
* <p>It behaves as {@link #doStreamRequest(HttpUriRequest, List, int...)}, so the entity is not consumed.</p>
* <p>Adds the headers and checks the response against expected status</p>
*
* @param method the request to be executed
* @param uri the uri to be sent as it is (will not prepend the context path)
* @param headers optional headers to be added to the request
* @param expectedStatus if passed, the response status is checked against it/them, and has to match at least one of them
* @return the response, with the entity not consumed
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doRawRequest(String method, String uri, List<Header> headers, int... expectedStatus)
throws ClientException {
// create context from config
HttpClientContext context = createHttpClientContextFromConfig();
HttpHost host = new HttpHost(getUrl().getHost(), getUrl().getPort(), getUrl().getScheme());
HttpRequest request = new BasicHttpRequest(method, uri);
// add headers
if (headers != null) {
request.setHeaders(headers.toArray(new Header[headers.size()]));
}
try {
log.debug("request {} {}", method, uri);
SlingHttpResponse response = new SlingHttpResponse(this.execute(host, request, context));
log.debug("response {}", HttpUtils.getHttpStatus(response));
// Check the status and throw a ClientException if it doesn't match expectedStatus, but close the entity before
if (expectedStatus != null && expectedStatus.length > 0) {
try {
HttpUtils.verifyHttpStatus(response, expectedStatus);
} catch (ClientException e) {
// catch the exception to make sure we close the entity before re-throwing it
response.close();
throw e;
}
}
return response;
} catch (IOException e) {
throw new ClientException("Could not execute http request", e);
}
}
private HttpClientContext createHttpClientContextFromConfig() {
// create context from config
HttpClientContext context = HttpClientContext.create();
if (config.getCookieStore() != null) {
context.setCookieStore(config.getCookieStore());
}
if (config.getCredsProvider() != null) {
context.setCredentialsProvider(config.getCredsProvider());
}
if (config.getAuthCache() != null) {
context.setAuthCache(config.getAuthCache());
}
return context;
}
/**
* <p>Executes a GET request WITHOUT consuming the entity in the response. The caller is responsible to close the connection.
* Otherwise, the client might run out of connections and will block</p>
*
* <p><b>Use this with caution and only if necessary for streaming</b>, otherwise use the safe method
* {@link #doGet(String, List, List, int...)}</p>
*
* <p>Adds the given parameters and headers and checks the response against expected status</p>
* @param requestPath path relative to client url
* @param parameters optional url parameters to be added
* @param headers optional headers to be added
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity not consumed
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doStreamGet(String requestPath, List<NameValuePair> parameters, List<Header> headers, int... expectedStatus)
throws ClientException {
// create full uri, including server url, given path and given parameters
URI uri = getUrl(requestPath, parameters);
// execute request
HttpUriRequest request = new HttpGet(uri);
return doStreamRequest(request, headers, expectedStatus);
}
/**
* <p>Executes a POST request WITHOUT consuming the entity in the response. The caller is responsible to close the connection</p>
*
* <p><b>Use this with caution and only if necessary for streaming</b>, otherwise use the safe method
* {@link #doPost(String, HttpEntity, List, int...)}</p>
*
* <p>Adds the headers and checks the response against expected status</p>
* @param requestPath path relative to client url
* @param entity http entity to be sent by POST
* @param headers optional headers to be added
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity not consumed
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doStreamPost(String requestPath, HttpEntity entity, List<Header> headers, int... expectedStatus)
throws ClientException {
HttpEntityEnclosingRequestBase request = new HttpPost(getUrl(requestPath));
if (entity != null) {
request.setEntity(entity);
}
return doStreamRequest(request, headers, expectedStatus);
}
private boolean isServiceUnavailable(SlingHttpResponse response) {
StatusLine statusLine = response.getStatusLine();
return statusLine != null && statusLine.getStatusCode() == HttpStatus.SC_SERVICE_UNAVAILABLE;
}
/**
* <p>Execute an HTTP request and consumes the entity in the response. The content is cached and can be retrieved using
* {@code response.getContent()}.
* This method is safe to use because it closes the entity so the caller has no responsibility.</p>
*
* <p>This means the response entity SHOULD NOT BE USED to read the content, e.g. {@code response.getEntity().getContent()}</p>
*
* @param request the request to be executed
* @param headers optional headers to be added to the request
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity consumed and the content cached
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doRequest(HttpUriRequest request, List<Header> headers, int... expectedStatus) throws ClientException {
int maxRetries = Constants.HTTP_RETRIES;
boolean needRetry = false;
SlingHttpResponse response = null;
do {
try {
if (needRetry) {
// add some pacing
Thread.sleep(Constants.HTTP_RETRIES_DELAY);
}
response = doStreamRequest(request, headers, expectedStatus);
needRetry = maxRetries > 0 && isServiceUnavailable(response);
// Consume entity and cache the content so the connection is closed
response.getContent();
} catch (ClientException | RuntimeException ex ) {
needRetry = ex.getMessage().contains("Could not read content from response")
|| ex.getMessage().contains("Instead 503 was returned!");
if (needRetry) {
log.warn("Retry needed due to " + ex.getMessage());
}
if (maxRetries == 0) {
throw ex;
}
} catch (InterruptedException ex) {
throw new ClientException("Interrupted while pacing request", ex);
}
} while (needRetry && maxRetries-- > 0);
return response;
}
/**
* <p>Executes a GET request and consumes the entity in the response (so the connection is closed immediately)
* The content is cached and can be retrieved using {@code response.getContent()}.</p>
*
* <p>Adds the passed parameters and headers and checks the expected status</p>
*
* @param requestPath path relative to client url
* @param parameters optional url parameters to be added
* @param headers optional headers to be added
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity consumed amd the content cached
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doGet(String requestPath, List<NameValuePair> parameters, List<Header> headers, int... expectedStatus)
throws ClientException {
SlingHttpResponse response = doStreamGet(requestPath, parameters, headers, expectedStatus);
// Consume entity and cache the content so the connection is closed
response.getContent();
return response;
}
/**
* <p>Executes a GET request and consumes the entity in the response (so the connection is closed immediately)
* The content is cached and can be retrieved using {@code response.getContent()}.</p>
*
* <p>Adds the passed parameters and checks the expected status</p>
*
* @param requestPath path relative to client url
* @param parameters optional url parameters to be added
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity consumed amd the content cached
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doGet(String requestPath, List<NameValuePair> parameters, int... expectedStatus)
throws ClientException {
return doGet(requestPath, parameters, null, expectedStatus);
}
/**
* <p>Executes a GET request and consumes the entity in the response (so the connection is closed immediately)
* The content is cached and can be retrieved using {@code response.getContent()}.</p>
*
* @param requestPath path relative to client url
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity consumed amd the content cached
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doGet(String requestPath, int... expectedStatus)
throws ClientException {
return doGet(requestPath, null, null, expectedStatus);
}
/**
* <p>Executes a HEAD request</p>
*
* <p>Adds the passed parameters and headers and checks the expected status</p>
*
* @param requestPath path relative to client url
* @param parameters optional url parameters to be added
* @param headers optional headers to be added
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doHead(String requestPath, List<NameValuePair> parameters, List<Header> headers, int... expectedStatus)
throws ClientException {
HttpUriRequest request = new HttpHead(getUrl(requestPath, parameters));
return doRequest(request, headers, expectedStatus);
}
/**
* <p>Executes a POST request and consumes the entity in the response. The content is cached and be retrieved by calling
* {@code response.getContent()}</p>
*
* <p>Adds the passed entity and headers and checks the expected status</p>
*
* @param requestPath path relative to client url
* @param entity the entity to be added to request
* @param headers optional headers to be added
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity consumed and the content cached
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doPost(String requestPath, HttpEntity entity, List<Header> headers, int... expectedStatus)
throws ClientException {
HttpEntityEnclosingRequestBase request = new HttpPost(getUrl(requestPath));
if (entity != null) {
request.setEntity(entity);
}
return doRequest(request, headers, expectedStatus);
}
/**
* <p>Executes a POST request and consumes the entity in the response. The content is cached and be retrieved by calling
* {@code response.getContent()}</p>
*
* <p>Adds the passed entity and checks the expected status</p>
*
* @param requestPath path relative to client url
* @param entity the entity to be added to request
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity consumed and the content cached
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doPost(String requestPath, HttpEntity entity, int... expectedStatus)
throws ClientException {
return doPost(requestPath, entity, null, expectedStatus);
}
/**
* <p>Executes a PUT request and consumes the entity in the response. The content is cached and be retrieved by calling
* {@code response.getContent()}</p>
*
* <p>Adds the passed entity and headers and checks the expected status</p>
*
* @param requestPath path relative to client url
* @param entity the entity to be added to request
* @param headers optional url parameters to be added
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity consumed and the content cached
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doPut(String requestPath, HttpEntity entity, List<Header> headers, int... expectedStatus)
throws ClientException {
HttpEntityEnclosingRequestBase request = new HttpPut(getUrl(requestPath));
if (entity != null) {
request.setEntity(entity);
}
return doRequest(request, headers, expectedStatus);
}
/**
* <p>Executes a PATCH request and consumes the entity in the response. The content is cached and be retrieved by calling
* {@code response.getContent()}</p>
*
* <p>Adds the passed entity and headers and checks the expected status</p>
*
* @param requestPath path relative to client url
* @param entity the entity to be added to request
* @param headers optional url parameters to be added
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity consumed and the content cached
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doPatch(String requestPath, HttpEntity entity, List<Header> headers, int... expectedStatus)
throws ClientException {
HttpEntityEnclosingRequestBase request = new HttpPatch(getUrl(requestPath));
if (entity != null) {
request.setEntity(entity);
}
return doRequest(request, headers, expectedStatus);
}
/**
* <p>Executes a DELETE request and consumes the entity in the response. The content is cached and be retrieved by calling
* {@code response.getContent()}</p>
*
* <p>Adds the passed parameters and headers and checks the expected status</p>
*
* @param requestPath path relative to client url
* @param parameters optional url parameters to be added
* @param headers optional url parameters to be added
* @param expectedStatus if passed, the response status will have to match one of them
* @return the response with the entity consumed and the content cached
* @throws ClientException if the request could not be executed
*/
public SlingHttpResponse doDelete(String requestPath, List<NameValuePair> parameters, List<Header> headers, int... expectedStatus)
throws ClientException {
HttpUriRequest request = new HttpDelete(getUrl(requestPath, parameters));
return doRequest(request, headers, expectedStatus);
}
@Override
/**
* <p>Closes the http client and makes sure all the underlying resources, like the connection manager, shut down </p>
*
*/
public void close() throws IOException {
this.http.close();
}
//
// HttpClient base methods
//
@Deprecated
@SuppressWarnings("deprecation")
public org.apache.http.params.HttpParams getParams() {
return this.http.getParams();
}
@Deprecated
@SuppressWarnings("deprecation")
public org.apache.http.conn.ClientConnectionManager getConnectionManager() {
return this.http.getConnectionManager();
}
@SuppressWarnings("DuplicateThrows")
public HttpResponse execute(HttpUriRequest request) throws IOException, ClientProtocolException {
return this.http.execute(request);
}
// maybe throw UnsupportedMethodException
@SuppressWarnings("DuplicateThrows")
public CloseableHttpResponse execute(HttpUriRequest request, HttpContext context)
throws IOException, ClientProtocolException {
return this.http.execute(request, context);
}
@SuppressWarnings("DuplicateThrows")
public HttpResponse execute(HttpHost target, HttpRequest request)
throws IOException, ClientProtocolException {
return this.http.execute(target, request);
}
@SuppressWarnings("DuplicateThrows")
public CloseableHttpResponse execute(HttpHost target, HttpRequest request, HttpContext context)
throws IOException, ClientProtocolException {
return this.http.execute(target, request, context);
}
@SuppressWarnings("DuplicateThrows")
public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler)
throws IOException, ClientProtocolException {
return this.http.execute(request, responseHandler);
}
@SuppressWarnings("DuplicateThrows")
public <T> T execute(HttpUriRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)
throws IOException, ClientProtocolException {
return this.http.execute(request, responseHandler, context);
}
@SuppressWarnings("DuplicateThrows")
public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler)
throws IOException, ClientProtocolException {
return this.http.execute(target, request, responseHandler);
}
@SuppressWarnings("DuplicateThrows")
public <T> T execute(HttpHost target, HttpRequest request, ResponseHandler<? extends T> responseHandler, HttpContext context)
throws IOException, ClientProtocolException {
return this.http.execute(target, request, responseHandler, context);
}
}