You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by du...@apache.org on 2020/04/09 13:50:43 UTC
svn commit: r38870 [2/6] - in /release/sling: ./ src/ src/main/
src/main/java/ src/main/java/org/ src/main/java/org/apache/
src/main/java/org/apache/sling/ src/main/java/org/apache/sling/testing/
src/main/java/org/apache/sling/testing/clients/ src/main...
Added: release/sling/src/main/java/org/apache/sling/testing/clients/ClientException.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/ClientException.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/ClientException.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,65 @@
+/*
+ * 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;
+
+/**
+ *
+ */
+public class ClientException extends Exception {
+
+ private static final long serialVersionUID = 1L;
+ private int httpStatusCode = -1;
+
+ public ClientException(String message) {
+ this(message, null);
+ }
+
+ public ClientException(String message, Throwable throwable) {
+ this(message, -1, throwable);
+ }
+
+ public ClientException(String message, int htmlStatusCode) {
+ this(message, htmlStatusCode, null);
+ }
+
+ public ClientException(String message, int htmlStatusCode, Throwable throwable) {
+ super(message, throwable);
+ this.httpStatusCode = htmlStatusCode;
+ }
+
+ /**
+ * @return the htmlStatusCode
+ */
+ public int getHttpStatusCode() {
+ return httpStatusCode;
+ }
+
+ /*
+ * (non-Javadoc)
+ *
+ * @see java.lang.Throwable#getMessage()
+ */
+ @Override
+ public String getMessage() {
+ String message = super.getMessage();
+ if (httpStatusCode > -1) {
+ message = message + "(return code=" + httpStatusCode + ")";
+ }
+ return message;
+ }
+
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/SlingClient.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/SlingClient.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/SlingClient.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,791 @@
+/*
+ * 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.commons.lang3.StringUtils;
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpRequestInterceptor;
+import org.apache.http.HttpResponseInterceptor;
+import org.apache.http.NameValuePair;
+import org.apache.http.annotation.Immutable;
+import org.apache.http.client.CookieStore;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.RedirectStrategy;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.entity.ContentType;
+import org.apache.http.entity.mime.MultipartEntityBuilder;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.sling.testing.clients.interceptors.DelayRequestInterceptor;
+import org.apache.sling.testing.clients.interceptors.TestDescriptionInterceptor;
+import org.apache.sling.testing.clients.util.*;
+import org.apache.sling.testing.clients.util.poller.AbstractPoller;
+import org.apache.sling.testing.clients.util.poller.Polling;
+import org.codehaus.jackson.JsonNode;
+
+import java.io.File;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+import static org.apache.http.HttpStatus.SC_CREATED;
+import static org.apache.http.HttpStatus.SC_OK;
+
+/**
+ * <p>The Base class for all Integration Test Clients. It provides generic methods to send HTTP requests to a server. </p>
+ *
+ * <p>It has methods to perform simple node operations on the server like creating and deleting nodes, etc.
+ * on the server using requests. </p>
+ */
+@Immutable
+public class SlingClient extends AbstractSlingClient {
+
+ public static final String DEFAULT_NODE_TYPE = "sling:OrderedFolder";
+
+ /**
+ * Constructor used by Builders and adaptTo(). <b>Should never be called directly from the code.</b>
+ *
+ * @param http the underlying HttpClient to be used
+ * @param config sling specific configs
+ * @throws ClientException if the client could not be created
+ *
+ * @see AbstractSlingClient#AbstractSlingClient(CloseableHttpClient, SlingClientConfig)
+ */
+ public SlingClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ super(http, config);
+ }
+
+ /**
+ * <p>Handy constructor easy to use in simple tests. Creates a client that uses basic authentication.</p>
+ *
+ * <p>For constructing clients with complex configurations, use a {@link InternalBuilder}</p>
+ *
+ * <p>For constructing clients with the same configuration, but a different class, use {@link #adaptTo(Class)}</p>
+ *
+ * @param url url of the server (including context path)
+ * @param user username for basic authentication
+ * @param password password for basic authentication
+ * @throws ClientException never, kept for uniformity with the other constructors
+ */
+ public SlingClient(URI url, String user, String password) throws ClientException {
+ super(Builder.create(url, user, password).buildHttpClient(), Builder.create(url, user, password).buildSlingClientConfig());
+ }
+
+ /**
+ * Moves a sling path to a new location (:operation move)
+ *
+ * @param srcPath source path
+ * @param destPath destination path
+ * @param expectedStatus list of accepted status codes in response
+ * @return the response
+ * @throws ClientException if an error occurs during operation
+ */
+ public SlingHttpResponse move(String srcPath, String destPath, int... expectedStatus) throws ClientException {
+ UrlEncodedFormEntity entity = FormEntityBuilder.create()
+ .addParameter(":operation", "move")
+ .addParameter(":dest", destPath)
+ .build();
+
+ return this.doPost(srcPath, entity, expectedStatus);
+ }
+
+ /**
+ * Deletes a sling path (:operation delete)
+ *
+ * @param path path to be deleted
+ * @param expectedStatus list of accepted status codes in response
+ * @return the response
+ * @throws ClientException if an error occurs during operation
+ */
+ public SlingHttpResponse deletePath(String path, int... expectedStatus) throws ClientException {
+ HttpEntity entity = FormEntityBuilder.create().addParameter(":operation", "delete").build();
+
+ return this.doPost(path, entity, expectedStatus);
+ }
+
+ /**
+ * Recursively creates all the none existing nodes in the given path using the {@link SlingClient#createNode(String, String)} method.
+ * All the created nodes will have the given node type.
+ *
+ * @param path the path to use for creating all the none existing nodes
+ * @param nodeType the node type to use for the created nodes
+ * @return the response to the creation of the leaf node
+ * @throws ClientException if one of the nodes can't be created
+ */
+ public SlingHttpResponse createNodeRecursive(final String path, final String nodeType) throws ClientException {
+ final String parentPath = getParentPath(path);
+ if (!parentPath.isEmpty() && !exists(parentPath)) {
+ createNodeRecursive(parentPath, nodeType);
+ }
+
+ return createNode(path, nodeType);
+ }
+
+ /**
+ * Creates the node specified by a given path with the given node type.<br>
+ * If the given node type is {@code null}, the node will be created with the default type: {@value DEFAULT_NODE_TYPE}.<br>
+ * If the node already exists, the method will return null, with no errors.<br>
+ * The method ignores trailing slashes so a path like this <i>/a/b/c///</i> is accepted and will create the <i>c</i> node if the rest of
+ * the path exists.
+ *
+ * @param path the path to the node to create
+ * @param nodeType the type of the node to create
+ * @return the sling HTTP response or null if the path already existed
+ * @throws ClientException if the node can't be created
+ */
+ public SlingHttpResponse createNode(final String path, final String nodeType) throws ClientException {
+ if (!exists(path)) {
+
+ String nodeTypeValue = nodeType;
+ if (nodeTypeValue == null) {
+ nodeTypeValue = DEFAULT_NODE_TYPE;
+ }
+
+ // Use the property for creating the actual node for working around the Sling issue with dot containing node names.
+ // The request will be similar with doing:
+ // curl -F "nodeName/jcr:primaryType=nodeTypeValue" -u admin:admin http://localhost:8080/nodeParentPath
+ final String nodeName = getNodeNameFromPath(path);
+ final String nodeParentPath = getParentPath(path);
+ final HttpEntity entity = FormEntityBuilder.create().addParameter(nodeName + "/jcr:primaryType", nodeTypeValue).build();
+ return this.doPost(nodeParentPath, entity, SC_OK, SC_CREATED);
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * <p>Checks whether a path exists or not by making a GET request to that path with the {@code json} extension</p>
+ * @param path path to be checked
+ * @return true if GET response returns 200
+ * @throws ClientException if the request could not be performed
+ */
+ public boolean exists(String path) throws ClientException {
+ SlingHttpResponse response = this.doGet(path + ".json");
+ final int status = response.getStatusLine().getStatusCode();
+ return status == SC_OK;
+ }
+
+ /**
+ * Extracts the parent path from the given String
+ *
+ * @param path string containing the path
+ * @return the parent path if exists or empty string otherwise
+ */
+ protected String getParentPath(final String path) {
+ // TODO define more precisely what is the parent of a folder and of a file
+ final String normalizedPath = StringUtils.removeEnd(path, "/"); // remove trailing slash in case of folders
+ return StringUtils.substringBeforeLast(normalizedPath, "/");
+ }
+
+ /**
+ * Extracts the node from path
+ *
+ * @param path string containing the path
+ * @return the node without parent path
+ */
+ protected String getNodeNameFromPath(final String path) {
+ // TODO define the output for all the cases (e.g. paths with trailing slash)
+ final String normalizedPath = StringUtils.removeEnd(path, "/"); // remove trailing slash in case of folders
+ final int pos = normalizedPath.lastIndexOf('/');
+ if (pos != -1) {
+ return normalizedPath.substring(pos + 1, normalizedPath.length());
+ }
+ return normalizedPath;
+ }
+
+ /**
+ * <p>Checks whether a path exists or not by making a GET request to that path with the {@code json extension} </p>
+ * <p>It polls the server and waits until the path exists </p>
+ *
+ * @deprecated use {@link #waitExists(String, long, long)} instead.
+ *
+ * @param path path to be checked
+ * @param waitMillis time to wait between retries
+ * @param retryCount number of retries before throwing an exception
+ * @throws ClientException if the path was not found
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ @Deprecated
+ public void waitUntilExists(final String path, final long waitMillis, int retryCount)
+ throws ClientException, InterruptedException {
+ AbstractPoller poller = new AbstractPoller(waitMillis, retryCount) {
+ boolean found = false;
+ public boolean call() {
+ try {
+ found = exists(path);
+ } catch (ClientException e) {
+ // maybe log
+ found = false;
+ }
+ return true;
+ }
+
+ public boolean condition() {
+ return found;
+ }
+ };
+
+ boolean found = poller.callUntilCondition();
+ if (!found) {
+ throw new ClientException("path " + path + " does not exist after " + retryCount + " retries");
+ }
+ }
+
+ /**
+ * <p>Waits until a path exists by making successive GET requests to that path with the {@code json extension} </p>
+ * <p>Polls the server until the path exists or until timeout is reached </p>
+ * @param path path to be checked
+ * @param timeout max total time to wait, in milliseconds
+ * @param delay time to wait between checks, in milliseconds
+ * @throws TimeoutException if the path was not found before timeout
+ * @throws InterruptedException to mark this operation as "waiting", should be rethrown by callers
+ * @since 1.1.0
+ */
+ public void waitExists(final String path, final long timeout, final long delay)
+ throws TimeoutException, InterruptedException {
+
+ Polling p = new Polling() {
+ @Override
+ public Boolean call() throws Exception {
+ return exists(path);
+ }
+
+ @Override
+ protected String message() {
+ return "Path " + path + " does not exist after %1$d ms";
+ }
+ };
+
+ p.poll(timeout, delay);
+ }
+
+ /**
+ * Sets String component property on a node.
+ *
+ * @param nodePath path to the node to be edited
+ * @param propName name of the property to be edited
+ * @param propValue value of the property to be edited
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed.
+ * @return the response object
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse setPropertyString(String nodePath, String propName, String propValue, int... expectedStatus)
+ throws ClientException {
+ // prepare the form
+ HttpEntity formEntry = FormEntityBuilder.create().addParameter(propName, propValue).build();
+ // send the request
+ return this.doPost(nodePath, formEntry, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ }
+
+ /**
+ * Sets a String[] component property on a node.
+ *
+ * @param nodePath path to the node to be edited
+ * @param propName name of the property to be edited
+ * @param propValueList List of String values
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed.
+ * @return the response
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse setPropertyStringArray(String nodePath, String propName, List<String> propValueList, int... expectedStatus)
+ throws ClientException {
+ // prepare the form
+ FormEntityBuilder formEntry = FormEntityBuilder.create();
+ for (String propValue : (propValueList != null) ? propValueList : new ArrayList<String>(0)) {
+ formEntry.addParameter(propName, propValue);
+ }
+ // send the request and return the sling response
+ return this.doPost(nodePath, formEntry.build(), HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ }
+
+ /**
+ * Sets multiple String properties on a node in a single request
+ * @param nodePath path to the node to be edited
+ * @param properties list of NameValue pairs with the name and value for each property. String[] properties can be defined
+ * by adding multiple time the same property name with different values
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed.
+ * @return the response
+ * @throws ClientException if the operation could not be completed
+ */
+ public SlingHttpResponse setPropertiesString(String nodePath, List<NameValuePair> properties, int... expectedStatus)
+ throws ClientException {
+ // prepare the form
+ HttpEntity formEntry = FormEntityBuilder.create().addAllParameters(properties).build();
+ // send the request and return the sling response
+ return this.doPost(nodePath, formEntry, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ }
+
+ /**
+ * Returns the JSON content of a node already mapped to a {@link org.codehaus.jackson.JsonNode}.<br>
+ * Waits max 10 seconds for the node to be created.
+ *
+ * @deprecated use {@link #waitExists(String, long, long)} and {@link #doGetJson(String, int, int...)} instead
+ * @param path the path to the content node
+ * @param depth the number of levels to go down the tree, -1 for infinity
+ * @return a {@link org.codehaus.jackson.JsonNode} mapping to the requested content node.
+ * @throws ClientException if something fails during request/response processing
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ @Deprecated
+ public JsonNode getJsonNode(String path, int depth) throws ClientException, InterruptedException {
+ return getJsonNode(path, depth, 500, 20);
+ }
+
+ /**
+ * Returns JSON format of a content node already mapped to a {@link org.codehaus.jackson.JsonNode}.
+ *
+ * @deprecated use {@link #waitExists(String, long, long)} and {@link #doGetJson(String, int, int...)} instead
+ * @param path the path to the content node
+ * @param depth the number of levels to go down the tree, -1 for infinity
+ * @param waitMillis how long it should wait between requests
+ * @param retryNumber number of retries before throwing an exception
+ * @param expectedStatus list of allowed HTTP Status to be returned. If not set,
+ * http status 200 (OK) is assumed.
+ * @return a {@link org.codehaus.jackson.JsonNode} mapping to the requested content node.
+ * @throws ClientException if something fails during request/response cycle
+ * @throws InterruptedException to mark this operation as "waiting"
+ */
+ @Deprecated
+ public JsonNode getJsonNode(String path, int depth, final long waitMillis, final int retryNumber, int... expectedStatus)
+ throws ClientException, InterruptedException {
+
+ // check if path exist and wait if needed
+ waitUntilExists(path, waitMillis, retryNumber);
+
+ // check for infinity
+ if (depth == -1) {
+ path += ".infinity.json";
+ } else {
+ path += "." + depth + ".json";
+ }
+
+ // request the JSON for the page node
+ SlingHttpResponse response = this.doGet(path);
+ HttpUtils.verifyHttpStatus(response, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+
+ return JsonUtils.getJsonNodeFromString(response.getContent());
+ }
+
+ /**
+ * Returns the {@link org.codehaus.jackson.JsonNode} object corresponding to a content node.
+ *
+ * @param path the path to the content node
+ * @param depth the number of levels to go down the tree, -1 for infinity
+ * @param expectedStatus list of allowed HTTP Status to be returned. If not set, 200 (OK) is assumed.
+ *
+ * @return a {@link org.codehaus.jackson.JsonNode} mapping to the requested content node.
+ * @throws ClientException if the path does not exist or something fails during request/response cycle
+ * @since 1.1.0
+ */
+ public JsonNode doGetJson(String path, int depth, int... expectedStatus) throws ClientException {
+
+ // check for infinity
+ if (depth == -1) {
+ path += ".infinity.json";
+ } else {
+ path += "." + depth + ".json";
+ }
+
+ // request the JSON for the node
+ SlingHttpResponse response = this.doGet(path, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
+ return JsonUtils.getJsonNodeFromString(response.getContent());
+ }
+
+ /**
+ * Uploads a file to the repository. It creates a leaf node typed {@code nt:file}. The intermediary nodes are created with
+ * type "sling:OrderedFolder" if parameter {@code createFolders} is true
+ *
+ * @param file the file to be uploaded
+ * @param mimeType the MIME Type of the file
+ * @param toPath the complete path of the file in the repository including file name
+ * @param createFolders if true, all non existing parent nodes will be created using node type {@code sling:OrderedFolder}
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 201 is assumed.
+ * @return the response
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse upload(File file, String mimeType, String toPath, boolean createFolders, int... expectedStatus)
+ throws ClientException {
+ // Determine filename and parent folder, depending on whether toPath is a folder or a file
+ String toFileName;
+ String toFolder;
+ if (toPath.endsWith("/")) {
+ toFileName = file.getName();
+ toFolder = toPath;
+ } else {
+ toFileName = getNodeNameFromPath(toPath);
+ toFolder = getParentPath(toPath);
+ }
+
+ if (createFolders) {
+ createNodeRecursive(toFolder, "sling:OrderedFolder");
+ }
+
+ if (mimeType == null) {
+ mimeType = "application/octet-stream";
+ }
+
+ HttpEntity entity = MultipartEntityBuilder.create()
+ .addBinaryBody(toFileName, file, ContentType.create(mimeType), toFileName)
+ .build();
+
+ // return the sling response
+ return this.doPost(toFolder, entity, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
+ }
+
+ /**
+ * Creates a new Folder of type sling:OrderedFolder. Same as using {@code New Folder...} in the Site Admin.
+ *
+ * @param folderName The name of the folder to be used in the URL.
+ * @param folderTitle Title of the Folder to be set in jcr:title
+ * @param parentPath The parent path where the folder gets added.
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 201 is assumed.
+ * @return the response
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse createFolder(String folderName, String folderTitle, String parentPath, int... expectedStatus)
+ throws ClientException {
+ // we assume the parentPath is a folder, even though it doesn't end with a slash
+ parentPath = StringUtils.appendIfMissing(parentPath, "/");
+ String folderPath = parentPath + folderName;
+ HttpEntity feb = FormEntityBuilder.create()
+ .addParameter("./jcr:primaryType", "sling:OrderedFolder") // set primary type for folder node
+ .addParameter("./jcr:content/jcr:primaryType", "nt:unstructured") // add jcr:content as sub node
+ .addParameter("./jcr:content/jcr:title", folderTitle) //set the title
+ .build();
+
+ // execute request and return the sling response
+ return this.doPost(folderPath, feb, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
+ }
+
+ /**
+ * <p>Create a tree structure under {@code parentPath} by providing a {@code content} in one
+ * of the supported formats: xml, jcr.xml, json, jar, zip.</p>
+ *
+ * <p>This is the implementation of {@code :operation import}, as documented in
+ * <a href="http://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html#importing-content-structures">importing-content-structures</a></p>
+ *
+ * @param parentPath path where the tree is created
+ * @param contentType format of the content
+ * @param content string expressing the structure to be created, in the specified format
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 201 is assumed
+ * @return the response
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse importContent(String parentPath, String contentType, String content, int... expectedStatus)
+ throws ClientException {
+ HttpEntity entity = FormEntityBuilder.create()
+ .addParameter(":operation", "import")
+ .addParameter(":contentType", contentType)
+ .addParameter(":content", content)
+ .build();
+ // execute request and return the sling response
+ return this.doPost(parentPath, entity, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
+ }
+
+ /**
+ * <p>Create a tree structure under {@code parentPath} by providing a {@code contentFile} in one
+ * of the supported formats: xml, jcr.xml, json, jar, zip.</p>
+ *
+ * <p>This is the implementation of {@code :operation import}, as documented in
+ * <a href="http://sling.apache.org/documentation/bundles/manipulating-content-the-slingpostservlet-servlets-post.html#importing-content-structures">importing-content-structures</a></p>
+ *
+ * @param parentPath path where the tree is created
+ * @param contentType format of the content
+ * @param contentFile file containing the structure to be created, in the specified format
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed
+ * @return the response
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse importContent(String parentPath, String contentType, File contentFile, int... expectedStatus)
+ throws ClientException {
+ HttpEntity entity = MultipartEntityBuilder.create()
+ .addTextBody(":operation", "import")
+ .addTextBody(":contentType", contentType)
+ .addBinaryBody(":contentFile", contentFile)
+ .build();
+ // execute request and return the sling response
+ return this.doPost(parentPath, entity, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
+ }
+
+ /**
+ * Wrapper method over {@link #importContent(String, String, String, int...)} for directly importing a json node
+ * @param parentPath path where the tree is created
+ * @param json json node with the desired structure
+ * @param expectedStatus list of expected HTTP Status to be returned, if not set, 201 is assumed
+ * @return the response
+ * @throws ClientException if something fails during the request/response cycle
+ */
+ public SlingHttpResponse importJson(String parentPath, JsonNode json, int... expectedStatus)
+ throws ClientException {
+ return importContent(parentPath, "json", json.toString(), expectedStatus);
+ }
+
+ /**
+ * Get the UUID of a repository path
+ *
+ * @param path path in repository
+ * @return uuid as String or null if path does not exist
+ * @throws ClientException if something fails during request/response cycle
+ */
+ public String getUUID(String path) throws ClientException {
+ if (!exists(path)) {
+ return null;
+ }
+ JsonNode jsonNode = doGetJson(path, -1);
+ return getUUId(jsonNode);
+ }
+
+ /**
+ * Get the UUID from a node that was already parsed in a {@link JsonNode}
+ *
+ * @param jsonNode {@link JsonNode} object of the repository node
+ * @return UUID as String or null if jsonNode is null or if the UUID was not found
+ * @throws ClientException if something fails during request/response cycle
+ */
+ // TODO make this method static
+ public String getUUId(JsonNode jsonNode) throws ClientException {
+ if (jsonNode == null) {
+ return null;
+ }
+
+ JsonNode uuidNode = jsonNode.get("jcr:uuid");
+
+ if (uuidNode == null) {
+ return null;
+ }
+
+ return uuidNode.getValueAsText();
+ }
+
+ //
+ // InternalBuilder class and builder related methods
+ //
+
+ /**
+ * <p>Extensible InternalBuilder for SlingClient. Can be used by calling: {@code SlingClient.builder().create(...).build()}.
+ * Between create() and build(), any number of <i>set</i> methods can be called to customize the client.<br>
+ * It also exposes the underling httpClientBuilder through {@link #httpClientBuilder()} which can be used to customize the client
+ * at http level.
+ * </p>
+ *
+ * <p>The InternalBuilder is created to be easily extensible. A class, e.g. {@code MyClient extends SlingClient}, can have its own InternalBuilder.
+ * This is worth creating if MyClient has fields that need to be initialized. The Skeleton of such InternalBuilder (created inside MyClient) is:
+ * </p>
+ * <blockquote><pre>
+ * {@code
+ * public static abstract class InternalBuilder<T extends MyClient> extends SlingClient.InternalBuilder<T> {
+ * private String additionalField;
+ *
+ * public InternalBuilder(URI url, String user, String password) { super(url, user, password); }
+ *
+ * public InternalBuilder<T> setAdditionalField(String s) { additionalField = s; }
+ * }
+ * }
+ * </pre></blockquote>
+ * <p>Besides this, two more methods need to be implemented directly inside {@code MyClient}: </p>
+ * <blockquote><pre>
+ * {@code
+ * public static InternalBuilder<?> builder(URI url, String user, String password) {
+ * return new InternalBuilder<MyClient>(url, user, password) {
+ * {@literal @}Override
+ * public MyClient build() throws ClientException { return new MyClient(this); }
+ * };
+ * }
+ *
+ * protected MyClient(InternalBuilder<MyClient> builder) throws ClientException {
+ * super(builder);
+ * additionalField = builder.additionalField;
+ * }
+ * }
+ * </pre></blockquote>
+ * Of course, the Clients and InternalBuilder are extensible on several levels, so MyClient.InternalBuilder can be further extended.
+ *
+ * @param <T> type extending SlingClient
+ */
+ public static abstract class InternalBuilder<T extends SlingClient> {
+
+ private final SlingClientConfig.Builder configBuilder;
+
+ private final HttpClientBuilder httpClientBuilder;
+
+ protected InternalBuilder(URI url, String user, String password) {
+ this.httpClientBuilder = HttpClientBuilder.create();
+ this.configBuilder = SlingClientConfig.Builder.create().setUrl(url).setUser(user).setPassword(password);
+
+ setDefaults();
+ }
+
+ public InternalBuilder<T> setUrl(URI url) {
+ this.configBuilder.setUrl(url);
+ return this;
+ }
+
+ public InternalBuilder<T> setUser(String user) {
+ this.configBuilder.setUser(user);
+ return this;
+ }
+
+ public InternalBuilder<T> setPassword(String password) {
+ this.configBuilder.setPassword(password);
+ return this;
+ }
+
+ public InternalBuilder<T> setCredentialsProvider(CredentialsProvider cp) {
+ this.configBuilder.setCredentialsProvider(cp);
+ return this;
+ }
+
+ public InternalBuilder<T> setPreemptiveAuth(boolean isPreemptiveAuth) {
+ this.configBuilder.setPreemptiveAuth(isPreemptiveAuth);
+ return this;
+ }
+
+ public InternalBuilder<T> setCookieStore(CookieStore cs) {
+ this.configBuilder.setCookieStore(cs);
+ return this;
+ }
+
+ public HttpClientBuilder httpClientBuilder() {
+ return httpClientBuilder;
+ }
+
+ public abstract T build() throws ClientException;
+
+ protected CloseableHttpClient buildHttpClient() {
+ return httpClientBuilder.build();
+ }
+
+ protected SlingClientConfig buildSlingClientConfig() throws ClientException {
+ return configBuilder.build();
+ }
+
+ /**
+ * Sets defaults to the builder.
+ *
+ * @return this
+ */
+ private InternalBuilder setDefaults() {
+ httpClientBuilder.useSystemProperties();
+ httpClientBuilder.setUserAgent("Java");
+ // Connection
+ httpClientBuilder.setMaxConnPerRoute(10);
+ httpClientBuilder.setMaxConnTotal(100);
+ // Interceptors
+ httpClientBuilder.addInterceptorLast(new TestDescriptionInterceptor());
+ httpClientBuilder.addInterceptorLast(new DelayRequestInterceptor(SystemPropertiesConfig.getHttpDelay()));
+
+ // HTTP request strategy
+ httpClientBuilder.setServiceUnavailableRetryStrategy(new ServerErrorRetryStrategy());
+
+ return this;
+ }
+
+ //
+ // HttpClientBuilder delegating methods
+ //
+
+ public final InternalBuilder<T> addInterceptorFirst(final HttpResponseInterceptor itcp) {
+ httpClientBuilder.addInterceptorFirst(itcp);
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the tail of the protocol processing list.
+ * <p>
+ * Please note this value can be overridden by the {@link HttpClientBuilder#setHttpProcessor(
+ * org.apache.http.protocol.HttpProcessor)} method.
+ * </p>
+ *
+ * @param itcp the interceptor
+ * @return this
+ */
+ public final InternalBuilder<T> addInterceptorLast(final HttpResponseInterceptor itcp) {
+ httpClientBuilder.addInterceptorLast(itcp);
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the head of the protocol processing list.
+ * <p>
+ * Please note this value can be overridden by the {@link HttpClientBuilder#setHttpProcessor(
+ * org.apache.http.protocol.HttpProcessor)} method.
+ * </p>
+ *
+ * @param itcp the interceptor
+ * @return this
+ */
+ public final InternalBuilder<T> addInterceptorFirst(final HttpRequestInterceptor itcp) {
+ httpClientBuilder.addInterceptorFirst(itcp);
+ return this;
+ }
+
+ /**
+ * Adds this protocol interceptor to the tail of the protocol processing list.
+ * <p>
+ * Please note this value can be overridden by the {@link HttpClientBuilder#setHttpProcessor(
+ * org.apache.http.protocol.HttpProcessor)} method.
+ * </p>
+ *
+ * @param itcp the interceptor
+ * @return this
+ */
+ public final InternalBuilder<T> addInterceptorLast(final HttpRequestInterceptor itcp) {
+ httpClientBuilder.addInterceptorLast(itcp);
+ return this;
+ }
+
+ /**
+ * Assigns {@link RedirectStrategy} instance.
+ * <p>Please note this value can be overridden by the {@link #disableRedirectHandling()} method.</p>
+ *
+ * @param redirectStrategy custom redirect strategy
+ * @return this
+ */
+ public final InternalBuilder<T> setRedirectStrategy(final RedirectStrategy redirectStrategy) {
+ httpClientBuilder.setRedirectStrategy(redirectStrategy);
+ return this;
+ }
+
+ /**
+ * Disables automatic redirect handling.
+ *
+ * @return this
+ */
+ public final InternalBuilder<T> disableRedirectHandling() {
+ httpClientBuilder.disableRedirectHandling();
+ return this;
+ }
+
+ }
+
+ public final static class Builder extends InternalBuilder<SlingClient> {
+
+ private Builder(URI url, String user, String password) {
+ super(url, user, password);
+ }
+
+ @Override
+ public SlingClient build() throws ClientException {
+ return new SlingClient(buildHttpClient(), buildSlingClientConfig());
+ }
+
+ public static Builder create(URI url, String user, String password) {
+ return new Builder(url, user, password);
+ }
+ }
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/SlingClientConfig.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,249 @@
+/*
+ * 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 com.google.common.base.Strings;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.http.HttpHost;
+import org.apache.http.annotation.ThreadSafe;
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.AuthCache;
+import org.apache.http.client.CookieStore;
+import org.apache.http.client.CredentialsProvider;
+import org.apache.http.client.utils.URIUtils;
+import org.apache.http.impl.auth.BasicScheme;
+import org.apache.http.impl.client.BasicAuthCache;
+import org.apache.http.impl.client.BasicCookieStore;
+import org.apache.http.impl.client.BasicCredentialsProvider;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@ThreadSafe
+public class SlingClientConfig {
+
+ /**
+ * Base URI of the server under test.
+ */
+ protected final URI url;
+
+ /**
+ * Name of the user that will be used to authenticate the requests.
+ */
+ protected final String user;
+
+ /**
+ * Password of the user that will be used to authenticate the requests.
+ */
+ protected final String password;
+
+ /**
+ * The cookie store
+ */
+ protected final CookieStore cookieStore;
+
+ /**
+ * The credentials provider
+ */
+ protected final CredentialsProvider credsProvider;
+
+ /**
+ * AuthCache for preemptive auth
+ */
+ protected final AuthCache authCache;
+
+
+ /**
+ * Extra values to be used in interceptors, custom auth mechanisms, etc.
+ */
+ protected final Map<String, String> values;
+
+
+ protected SlingClientConfig(URI url, String user, String password,
+ CookieStore cookieStore,
+ CredentialsProvider credentialsProvider, AuthCache authCache) {
+ this.url = url;
+ this.user = user;
+ this.password = password;
+
+ this.cookieStore = cookieStore;
+ this.credsProvider = credentialsProvider;
+ this.authCache = authCache;
+
+ this.values = new ConcurrentHashMap<String, String>();
+ }
+
+ /**
+ * @return the base URL that the sling client is pointing to. It should always end with a "/"
+ */
+ public URI getUrl() {
+ return url;
+ }
+
+ /**
+ * @return the user that the client is using.
+ */
+ public String getUser() {
+ return user;
+ }
+
+ /**
+ * @return the user that the client is using.
+ */
+ public String getPassword() {
+ return password;
+ }
+
+ /**
+ * <p>Get the map of extra custom values configured on the client</p>
+ * <p>These may be used by interceptors, for example</p>
+ *
+ * @return the reference to the map
+ */
+ public Map<String, String> getValues() {
+ return values;
+ }
+
+ /**
+ * @return a reference to the cookie store used by the client
+ */
+ public CookieStore getCookieStore() {
+ return cookieStore;
+ }
+
+ /**
+ * @return the reference to the CredentialsProvider used by the client
+ */
+ public CredentialsProvider getCredsProvider() {
+ return credsProvider;
+ }
+
+ /**
+ * @return the reference the AuthCache used by the client
+ */
+ public AuthCache getAuthCache() {
+ return authCache;
+ }
+
+ public static class Builder {
+ protected URI url;
+
+ protected String user;
+
+ protected String password;
+
+ protected CookieStore cookieStore;
+
+ protected CredentialsProvider credsProvider;
+
+ protected AuthCache authCache;
+
+ protected boolean preemptiveAuth = true;
+
+ protected Builder() {
+ }
+
+ public static Builder create() {
+ return new Builder();
+ }
+
+ public Builder setUrl(String url) throws URISyntaxException {
+ return setUrl(new URI(url));
+ }
+
+ public Builder setUrl(URI url) {
+ this.url = url;
+ // Add / as path if none is present
+ if (Strings.isNullOrEmpty(this.url.getPath()) || !this.url.getPath().endsWith("/")) {
+ this.url = this.url.resolve(Strings.nullToEmpty(this.url.getPath()) + "/");
+ }
+ return this;
+ }
+
+ public Builder setUser(String user) {
+ this.user = user;
+ return this;
+ }
+
+ public Builder setPassword(String password) {
+ this.password = password;
+ return this;
+ }
+
+ public Builder setCredentialsProvider(CredentialsProvider credsProvider) {
+ this.credsProvider = credsProvider;
+ return this;
+ }
+
+ public Builder setAuthCache(AuthCache authCache) {
+ this.authCache = authCache;
+ return this;
+ }
+
+ public Builder setPreemptiveAuth(boolean preemptiveAuth) {
+ this.preemptiveAuth = preemptiveAuth;
+ return this;
+ }
+
+ public Builder setCookieStore(CookieStore cookieStore) {
+ this.cookieStore = cookieStore;
+ return this;
+ }
+
+ public SlingClientConfig build() throws ClientException {
+ if (!this.url.isAbsolute()) {
+ throw new ClientException("Url must be absolute: " + url);
+ }
+
+ HttpHost targetHost = URIUtils.extractHost(this.url);
+ if (targetHost == null) {
+ throw new ClientException("Failed to extract hostname from url " + url);
+ }
+
+ // Create default CredentialsProvider if not set
+ if (credsProvider == null) {
+ credsProvider = new BasicCredentialsProvider();
+ if (StringUtils.isNotEmpty(this.user)) {
+ credsProvider.setCredentials(new AuthScope(targetHost.getHostName(), targetHost.getPort()),
+ new UsernamePasswordCredentials(this.user, this.password));
+ }
+ }
+
+ // Create default AuthCache for basic if not set
+ if (authCache == null) {
+ BasicScheme basicScheme = new BasicScheme();
+ authCache = new BasicAuthCache();
+ authCache.put(targetHost, basicScheme);
+ }
+
+ // if preemptive auth is disabled, force auth cache to be null
+ if (!this.preemptiveAuth) {
+ authCache = null;
+ }
+
+ // Create default CookieStore if not set
+ if (cookieStore == null) {
+ cookieStore = new BasicCookieStore();
+ }
+
+ return new SlingClientConfig(url, user, password, cookieStore, credsProvider, authCache);
+ }
+ }
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/SlingHttpResponse.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,398 @@
+/*
+ * 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.client.methods.CloseableHttpResponse;
+import org.apache.http.params.HttpParams;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+import java.util.*;
+import java.util.regex.Pattern;
+
+public class SlingHttpResponse implements CloseableHttpResponse {
+
+ public static final String STATUS = "Status";
+ public static final String MESSAGE = "Message";
+ public static final String LOCATION = "Location";
+ public static final String PARENT_LOCATION = "ParentLocation";
+ public static final String PATH = "Path";
+ public static final String REFERER = "Referer";
+ public static final String CHANGE_LOG = "ChangeLog";
+
+ private final CloseableHttpResponse httpResponse;
+ private String content;
+
+ public SlingHttpResponse(CloseableHttpResponse response) {
+ this.httpResponse = response;
+ }
+
+ /**
+ * <p>Get the {@code String} content of the response.</p>
+ * <p>The content is cached so it is safe to call this method several times.</p>
+ * <p><b>Attention!</b> Calling this method consumes the entity, so it cannot be used as an InputStream later</p>
+ *
+ * @return the content as String
+ */
+ public String getContent() {
+ if (!this.isConsumed()) {
+ try {
+ this.content = EntityUtils.toString(this.getEntity());
+ this.close();
+ } catch (IOException e) {
+ throw new RuntimeException("Could not read content from response", e);
+ }
+ }
+
+ return content;
+ }
+
+ public boolean isConsumed() {
+ return this.content != null || this.getEntity() == null;
+ }
+
+ /**
+ * <p>Assert that response matches supplied status</p>
+ *
+ * @param expected the expected http status
+ * @throws ClientException if the response does not match the expected
+ */
+ public void checkStatus(int expected) throws ClientException {
+ if (this.getStatusLine().getStatusCode() != expected) {
+ throw new ClientException(this + " has wrong response status ("
+ + this.getStatusLine().getStatusCode() + "). Expected " + expected);
+ }
+ }
+
+ /**
+ * <p>Assert that response matches supplied content type (from Content-Type header)</p>
+ *
+ * @param expected the expected content type
+ * @throws ClientException if the response content type does not match the expected
+ */
+ public void checkContentType(String expected) throws ClientException {
+ // Remove whatever follows semicolon in content-type
+ String contentType = this.getEntity().getContentType().getValue();
+ if (contentType != null) {
+ contentType = contentType.split(";")[0].trim();
+ }
+
+ // check for match
+ if (!contentType.equals(expected)) {
+ throw new ClientException(this + " has wrong content type (" + contentType + "). Expected " + expected);
+ }
+ }
+
+ /**
+ * <p>For each regular expression, assert that at least one line of the response matches the expression</p>
+ * <p>The regular expressions are automatically prefixed and suffixed with .* it order to partial-match the lines</p>
+ *
+ * @param regexp list of regular expressions
+ * @throws ClientException if the response content does not match one of the regexp
+ */
+ public void checkContentRegexp(String... regexp) throws ClientException {
+ for(String expr : regexp) {
+ final Pattern p = Pattern.compile(".*" + expr + ".*");
+ final Scanner scanner = new Scanner(this.getContent());
+ boolean matched = false;
+ while (scanner.hasNextLine()) {
+ String line = scanner.nextLine();
+ if (p.matcher(line).matches()) {
+ matched = true;
+ break;
+ }
+ }
+
+ if (!matched) {
+ throw new ClientException("Pattern " + p + " didn't match any line in content");
+ }
+ }
+ }
+
+ /**
+ * <p>Assert that all the provided {@code Strings} are contained in the response</p>
+ *
+ * @param expected list of expected strings
+ * @throws ClientException @throws ClientException if the response content does not match one of the strings
+ */
+ public void checkContentContains(String... expected) throws ClientException {
+ for (String s : expected) {
+ if (!this.getContent().contains(s)) {
+ throw new ClientException("Content does not contain string " + s + ". Content is: \n\n" + getContent());
+ }
+ }
+ }
+
+ /**
+ * Get status from Sling Response
+ *
+ * @return Sling Status
+ */
+ public String getSlingStatus() {
+ String searchPattern = "id=\"" + STATUS + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get status from Sling Response as integer
+ *
+ * @return Sling Status
+ * @throws NumberFormatException if sling status can't be parsed as a number
+ */
+ public int getSlingStatusAsInt() throws NumberFormatException {
+ String strStatus = getSlingStatus();
+ return Integer.parseInt(strStatus);
+ }
+
+ /**
+ * Get message from Sling Response
+ *
+ * @return Sling Message
+ */
+ public String getSlingMessage() {
+ String searchPattern = "id=\"" + MESSAGE + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get copy paths from message
+ *
+ * @return copy paths as String Array
+ */
+ public String[] getSlingCopyPaths() {
+ String copyPaths = getSlingMessage();
+ StringTokenizer tokenizer = new StringTokenizer(copyPaths);
+ List<String> copies = new ArrayList<String>();
+ while (tokenizer.hasMoreElements()) {
+ copies.add(tokenizer.nextToken());
+ }
+ return copies.toArray(new String[copies.size()]);
+ }
+
+ /**
+ * Get location from Sling Response
+ *
+ * @return Sling Location
+ */
+ public String getSlingLocation() {
+ String searchPattern = "id=\"" + LOCATION + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get parent location from Sling Response
+ *
+ * @return Sling Parent Location
+ */
+ public String getSlingParentLocation() {
+ String searchPattern = "id=\"" + PARENT_LOCATION + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get path from Sling Response
+ *
+ * @return Sling Path
+ */
+ public String getSlingPath() {
+ String searchPattern = "id=\"" + PATH + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get referer from Sling Response
+ *
+ * @return Sling Referer
+ */
+ public String getSlingReferer() {
+ String searchPattern = "id=\"" + REFERER + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Get change log from Sling Response
+ *
+ * @return Sling Change Log
+ */
+ public String getSlingChangeLog() {
+ String searchPattern = "id=\"" + CHANGE_LOG + "\">";
+ return extractFromHTMLResponse(searchPattern);
+ }
+
+ /**
+ * Extract information from response
+ *
+ * @param searchPattern search pattern to look for
+ * @return Sling information
+ */
+ protected String extractFromHTMLResponse(String searchPattern) {
+ String tmpResponse = null;
+ int start = getContent().indexOf(searchPattern);
+ if (start > 0) {
+ start += searchPattern.length();
+ tmpResponse = getContent().substring(start);
+ int end = tmpResponse.indexOf("<");
+ tmpResponse = tmpResponse.substring(0, end);
+ }
+ return tmpResponse;
+ }
+
+ // HttpResponse delegated methods
+
+ @Override
+ public StatusLine getStatusLine() {
+ return httpResponse.getStatusLine();
+ }
+
+ @Override
+ public void setStatusLine(StatusLine statusline) {
+ httpResponse.setStatusLine(statusline);
+ }
+
+ @Override
+ public void setStatusLine(ProtocolVersion ver, int code) {
+ httpResponse.setStatusLine(ver, code);
+ }
+
+ @Override
+ public void setStatusLine(ProtocolVersion ver, int code, String reason) {
+ httpResponse.setStatusLine(ver, code, reason);
+ }
+
+ @Override
+ public void setStatusCode(int code) throws IllegalStateException {
+ httpResponse.setStatusCode(code);
+ }
+
+ @Override
+ public void setReasonPhrase(String reason) throws IllegalStateException {
+ httpResponse.setReasonPhrase(reason);
+ }
+
+ @Override
+ public HttpEntity getEntity() {
+ return httpResponse.getEntity();
+ }
+
+ @Override
+ public void setEntity(HttpEntity entity) {
+ httpResponse.setEntity(entity);
+ }
+
+ @Override
+ public Locale getLocale() {
+ return httpResponse.getLocale();
+ }
+
+ @Override
+ public void setLocale(Locale loc) {
+ httpResponse.setLocale(loc);
+ }
+
+ @Override
+ public ProtocolVersion getProtocolVersion() {
+ return httpResponse.getProtocolVersion();
+ }
+
+ @Override
+ public boolean containsHeader(String name) {
+ return httpResponse.containsHeader(name);
+ }
+
+ @Override
+ public Header[] getHeaders(String name) {
+ return httpResponse.getHeaders(name);
+ }
+
+ @Override
+ public Header getFirstHeader(String name) {
+ return httpResponse.getFirstHeader(name);
+ }
+
+ @Override
+ public Header getLastHeader(String name) {
+ return httpResponse.getLastHeader(name);
+ }
+
+ @Override
+ public Header[] getAllHeaders() {
+ return httpResponse.getAllHeaders();
+ }
+
+ @Override
+ public void addHeader(Header header) {
+ httpResponse.addHeader(header);
+ }
+
+ @Override
+ public void addHeader(String name, String value) {
+ httpResponse.addHeader(name, value);
+ }
+
+ @Override
+ public void setHeader(Header header) {
+ httpResponse.setHeader(header);
+ }
+
+ @Override
+ public void setHeader(String name, String value) {
+ httpResponse.setHeader(name, value);
+ }
+
+ @Override
+ public void setHeaders(Header[] headers) {
+ httpResponse.setHeaders(headers);
+ }
+
+ @Override
+ public void removeHeader(Header header) {
+ httpResponse.removeHeader(header);
+ }
+
+ @Override
+ public void removeHeaders(String name) {
+ httpResponse.removeHeaders(name);
+ }
+
+ @Override
+ public HeaderIterator headerIterator() {
+ return httpResponse.headerIterator();
+ }
+
+ @Override
+ public HeaderIterator headerIterator(String name) {
+ return httpResponse.headerIterator(name);
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public HttpParams getParams() {
+ return httpResponse.getParams();
+ }
+
+ @SuppressWarnings("deprecation")
+ @Override
+ public void setParams(HttpParams params) {
+ httpResponse.setParams(params);
+ }
+
+ @Override
+ public void close() throws IOException {
+ httpResponse.close();
+ }
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/SystemPropertiesConfig.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/SystemPropertiesConfig.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/SystemPropertiesConfig.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,130 @@
+/*
+ * 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 java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+public class SystemPropertiesConfig {
+
+ /**
+ * Prefix for IT-specific system properties
+ */
+ public static final String CONFIG_PROP_PREFIX = "sling.it.";
+
+ /**
+ * System property for {@link SystemPropertiesConfig#getHttpDelay()}
+ * Prefixed by {@link SystemPropertiesConfig#CONFIG_PROP_PREFIX}
+ */
+ public static final String HTTP_DELAY_PROP = "http.delay";
+
+ /**
+ * System property for {@link SystemPropertiesConfig#getHttpRetries()}
+ * Prefixed by {@link SystemPropertiesConfig#CONFIG_PROP_PREFIX}
+ */
+ public static final String HTTP_RETRIES_PROP = "http.retries";
+
+ /**
+ * System property for {@link SystemPropertiesConfig#getHttpRetriesDelay()}
+ * Prefixed by {@link SystemPropertiesConfig#CONFIG_PROP_PREFIX}
+ */
+ public static final String HTTP_RETRIES_DELAY_PROP = "http.retriesDelay";
+
+ /**
+ * System property for {@link SystemPropertiesConfig#isHttpLogRetries()}
+ * Prefixed by {@link SystemPropertiesConfig#CONFIG_PROP_PREFIX}
+ */
+ public static final String HTTP_LOG_RETRIES_PROP = "http.logRetries";
+
+ /**
+ * System property for {@link SystemPropertiesConfig#getHttpRetriesErrorCodes()}
+ * Prefixed by {@link SystemPropertiesConfig#CONFIG_PROP_PREFIX}
+ */
+ public static final String HTTP_RETRIES_ERROR_CODES_PROP = "http.retriesErrorCodes";
+
+ public static String getPrefixedPropertyName(String prop) {
+ return SystemPropertiesConfig.CONFIG_PROP_PREFIX + prop;
+ }
+
+ /**
+ * Custom delay in milliseconds before an HTTP request goes through.
+ * Used by {@link org.apache.sling.testing.clients.interceptors.DelayRequestInterceptor}
+ */
+ public static long getHttpDelay() {
+ try {
+ return Long.getLong(getPrefixedPropertyName(HTTP_DELAY_PROP), 0);
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Number of http call retries in case of a 5XX response code
+ */
+ public static int getHttpRetries() {
+ try {
+ return Integer.getInteger(getPrefixedPropertyName(HTTP_RETRIES_PROP), 10);
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+
+ /**
+ * The delay in milliseconds between http retries
+ */
+ public static int getHttpRetriesDelay() {
+ try {
+ return Integer.getInteger(getPrefixedPropertyName(HTTP_RETRIES_DELAY_PROP), 1000);
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+
+ /**
+ * Whether to log or not http request retries
+ */
+ public static boolean isHttpLogRetries() {
+ try {
+ return Boolean.getBoolean(getPrefixedPropertyName(HTTP_LOG_RETRIES_PROP));
+ } catch (Exception e) {
+ return false;
+ }
+ }
+
+ /**
+ * Comma-separated list of http response codes for which to retry the request
+ * If empty, all 5XX error codes will be retried
+ */
+ public static Collection<Integer> getHttpRetriesErrorCodes() {
+ try {
+ final String errorCodes = System.getProperty(getPrefixedPropertyName(HTTP_RETRIES_ERROR_CODES_PROP), "");
+ return Arrays.asList(errorCodes.split(",")).stream().map(s -> {
+ try {
+ return Integer.valueOf(s);
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ }).filter(Objects::nonNull).collect(Collectors.toList());
+ } catch (Exception e) {
+ return Collections.emptyList();
+ }
+ }
+
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/email/EmailMessage.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/email/EmailMessage.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/email/EmailMessage.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,80 @@
+/*
+ * 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.email;
+
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Holds information retrieved from the mock SMTP server deployed in Sling
+ *
+ */
+public final class EmailMessage {
+
+ public static final String HEADER_FROM = "From";
+ public static final String HEADER_TO = "To";
+ public static final String HEADER_SUBJECT = "Subject";
+
+ private Map<String, String> headers = new LinkedHashMap<>();
+
+ private String content;
+
+ public EmailMessage(String content) {
+ this.content = content;
+ }
+
+ /**
+ * Adds a new header to this email message
+ *
+ * @param key the header name
+ * @param value the header value
+ */
+ public void addHeader(String key, String value) {
+ headers.put(key, value);
+ }
+
+ /**
+ * Returns the value of one of the headers of this email
+ *
+ * @param key the header name
+ * @return the value of the header, possibly <code>null</code>
+ */
+ public String getHeader(String key) {
+ return headers.get(key);
+ }
+
+ /**
+ * Returns an unmodifiable view over the email headers
+ *
+ * @return the headers
+ */
+ public Map<String, String> getHeaders() {
+ return Collections.unmodifiableMap(headers);
+ }
+
+ /**
+ * Returns the contents of the email
+ *
+ * @return the email content
+ */
+ public String getContent() {
+ return content;
+ }
+}
\ No newline at end of file
Added: release/sling/src/main/java/org/apache/sling/testing/clients/email/SlingEmailClient.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/email/SlingEmailClient.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/email/SlingEmailClient.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,123 @@
+/*
+ * 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.email;
+
+import static org.apache.http.HttpStatus.SC_NO_CONTENT;
+import static org.apache.http.HttpStatus.SC_OK;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+
+import org.apache.http.Header;
+import org.apache.http.NameValuePair;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.apache.sling.testing.clients.SlingClientConfig;
+import org.apache.sling.testing.clients.SlingHttpResponse;
+import org.codehaus.jackson.JsonNode;
+import org.codehaus.jackson.map.ObjectMapper;
+
+/**
+ * Accesses email stored by a mock SMTP server deployed to Sling
+ *
+ * <p>Requires that the <code>org.apache.sling.testing.email</code> bundle is deployed.</p>
+ */
+public final class SlingEmailClient extends SlingClient {
+
+ /**
+ * The well-known path under which the EmailServlet is deployed
+ */
+ private static final String EMAIL_SERVLET_PATH = "/system/sling/testing/email";
+
+ /**
+ * The well-known property name of the email body contents
+ */
+ private static final String PN_CONTENT = "-Content-";
+
+
+ private final ObjectMapper mapper = new ObjectMapper();
+
+ public SlingEmailClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ super(http, config);
+ }
+
+ /**
+ * Retrieves the actual bind port of the SMTP server
+ *
+ * @return the port value
+ * @throws ClientException in case of any errors
+ */
+ public int getBindPort() throws ClientException {
+ try {
+ SlingHttpResponse mockEmailConfig = doGet(EMAIL_SERVLET_PATH + "/config", SC_OK);
+
+ JsonNode configNode = mapper.readTree(mockEmailConfig.getContent());
+ return configNode.get("bindPort").getIntValue();
+ } catch (IOException e) {
+ throw new ClientException("Failed retrieving configuration", e);
+ }
+ }
+
+ /**
+ * Retrieves the list of mail messages currently stored
+ *
+ * @return the list of messages, possibly empty
+ * @throws ClientException in case of any errors
+ */
+ public List<EmailMessage> getMessages() throws ClientException {
+ List<EmailMessage> emails = new ArrayList<>();
+
+ try {
+ SlingHttpResponse response = doGet(EMAIL_SERVLET_PATH + "/messages", SC_OK);
+ JsonNode messages = mapper.readTree(response.getContent());
+ for ( JsonNode emailNode : messages.get("messages") ) {
+ EmailMessage msg = new EmailMessage(emailNode.get(PN_CONTENT).getTextValue());
+ Iterator<String> fieldNames = emailNode.getFieldNames();
+ while ( fieldNames.hasNext() ) {
+ String fieldName = fieldNames.next();
+ if ( fieldName.equals(PN_CONTENT) ) {
+ continue;
+ }
+ msg.addHeader(fieldName, emailNode.get(fieldName).getTextValue());
+ }
+
+ emails.add(msg);
+ }
+ } catch (IOException e) {
+ throw new ClientException("Failed retrieving email messages", e);
+ }
+
+
+ return emails;
+ }
+
+ /**
+ * Deletes all mail messages currently stored
+ *
+ * @throws ClientException in case of any errors
+ */
+ public void deleteMessages() throws ClientException {
+ doDelete(EMAIL_SERVLET_PATH, Collections.<NameValuePair>emptyList(),
+ Collections.<Header> emptyList(), SC_NO_CONTENT);
+ }
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/email/package-info.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/email/package-info.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/email/package-info.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+@Version("1.2.0")
+package org.apache.sling.testing.clients.email;
+
+import org.osgi.annotation.versioning.Version;
Added: release/sling/src/main/java/org/apache/sling/testing/clients/html/MicrodataClient.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/html/MicrodataClient.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/html/MicrodataClient.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,81 @@
+/*******************************************************************************
+ * 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.html;
+
+import java.net.URI;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.sling.hapi.client.HtmlClient;
+import org.apache.sling.hapi.client.impl.microdata.MicrodataDocument;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.apache.sling.testing.clients.SlingClientConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class MicrodataClient extends SlingClient implements HtmlClient {
+ protected static final Logger LOG = LoggerFactory.getLogger(MicrodataClient.class);
+
+ public MicrodataClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
+ super(http, config);
+ }
+
+ public MicrodataClient(URI url, String user, String password) throws ClientException {
+ super(url, user, password);
+ }
+
+ @Override
+ public MicrodataDocument enter(String url) throws org.apache.sling.hapi.client.ClientException {
+ return get(url);
+ }
+
+ @Override
+ public MicrodataDocument get(String url) throws org.apache.sling.hapi.client.ClientException {
+ try {
+ return newDocument(doGet(url).getContent());
+ } catch (ClientException e) {
+ throw new org.apache.sling.hapi.client.ClientException("Cannot create Microdata document", e);
+ }
+ }
+
+ @Override
+ public MicrodataDocument post(String url, HttpEntity entity) throws org.apache.sling.hapi.client.ClientException {
+ try {
+ return newDocument(doPost(url, entity).getContent());
+ } catch (ClientException e) {
+ throw new org.apache.sling.hapi.client.ClientException("Cannot create Microdata document", e);
+ }
+ }
+
+ @Override
+ public MicrodataDocument delete(String url) throws org.apache.sling.hapi.client.ClientException {
+ try {
+ return newDocument(doDelete(url, null, null).getContent());
+ } catch (ClientException e) {
+ throw new org.apache.sling.hapi.client.ClientException("Cannot create Microdata document", e);
+ }
+ }
+
+ @Override
+ public MicrodataDocument newDocument(String html) {
+ return new MicrodataDocument(html, this, this.getUrl().toString());
+ }
+
+}
Added: release/sling/src/main/java/org/apache/sling/testing/clients/html/package-info.java
==============================================================================
--- release/sling/src/main/java/org/apache/sling/testing/clients/html/package-info.java (added)
+++ release/sling/src/main/java/org/apache/sling/testing/clients/html/package-info.java Thu Apr 9 13:50:42 2020
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+
+@Version("2.3.0")
+package org.apache.sling.testing.clients.html;
+
+import org.osgi.annotation.versioning.Version;