| /* |
| * 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 static org.apache.http.HttpStatus.SC_CREATED; |
| import static org.apache.http.HttpStatus.SC_OK; |
| |
| import java.io.File; |
| import java.net.URI; |
| import java.util.ArrayList; |
| import java.util.Date; |
| import java.util.List; |
| import java.util.Optional; |
| import java.util.concurrent.TimeUnit; |
| import java.util.concurrent.TimeoutException; |
| |
| 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.config.RequestConfig; |
| 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.http.impl.cookie.BasicClientCookie; |
| import org.apache.sling.testing.clients.interceptors.DelayRequestInterceptor; |
| import org.apache.sling.testing.clients.interceptors.HttpRequestResponseInterceptor; |
| import org.apache.sling.testing.clients.interceptors.TestDescriptionInterceptor; |
| import org.apache.sling.testing.clients.util.FormEntityBuilder; |
| import org.apache.sling.testing.clients.util.HttpUtils; |
| import org.apache.sling.testing.clients.util.JsonUtils; |
| import org.apache.sling.testing.clients.util.ServerErrorRetryStrategy; |
| import org.apache.sling.testing.clients.util.poller.AbstractPoller; |
| import org.apache.sling.testing.clients.util.poller.Polling; |
| import org.apache.sling.testing.timeouts.TimeoutsProvider; |
| import org.codehaus.jackson.JsonNode; |
| |
| /** |
| * <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"; |
| public static final String CLIENT_CONNECTION_TIMEOUT_PROP = "sling.client.connection.timeout.seconds"; |
| public static final String SUDO_COOKIE_NAME = "sling.sudo.cookie.name"; |
| |
| /** |
| * 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); |
| } |
| |
| private String getSudoCookieName() { |
| return Optional.ofNullable(this.getValue(SUDO_COOKIE_NAME)).orElse("sling.sudo"); |
| } |
| |
| /** |
| * 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(); |
| } |
| |
| @Override |
| public String getUser() { |
| // get the username from the sudo cookie or default from client config |
| return getCookieStore().getCookies().stream().filter(c -> c.getName().equals(getSudoCookieName())).findFirst() |
| .map(c -> c.getValue().replace("\"", "")).orElse(super.getUser()); |
| } |
| |
| /** |
| * Impersonate user with the given <code>userId</code> |
| * <p> |
| * By impersonating a user SlingClient can access content from the perspective of that user. |
| * </p> |
| *Passing a <code>null</code> will clear impersonation. |
| * |
| * @param userId the user to impersonate. A <code>null</code> value clears impersonation |
| */ |
| public SlingClient impersonate(String userId) { |
| BasicClientCookie c = new BasicClientCookie(getSudoCookieName(), "\"" + userId + "\""); |
| c.setPath("/"); |
| c.setDomain(getUrl().getHost()); |
| if (userId == null || "-".equals(userId)) { |
| // setting expiry date in the past will remove the cookie |
| c.setExpiryDate(new Date(0)); |
| } |
| getCookieStore().addCookie(c); |
| return this; |
| } |
| |
| // |
| // 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()); |
| |
| // connection timeouts |
| int timeoutSeconds = TimeoutsProvider.getInstance().getTimeout(CLIENT_CONNECTION_TIMEOUT_PROP, -1); |
| if (timeoutSeconds > 0) { |
| int timeoutMs = (int)TimeUnit.SECONDS.toMillis(timeoutSeconds); |
| RequestConfig config = RequestConfig.custom() |
| .setConnectTimeout(timeoutMs) |
| .setConnectionRequestTimeout(timeoutMs) |
| .setSocketTimeout(timeoutMs).build(); |
| this.httpClientBuilder.setDefaultRequestConfig(config); |
| } |
| |
| 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; |
| } |
| |
| /** |
| * Adds this protocol interceptor to the head of the protocol processing list for both requests and responses |
| * <p> |
| * Please note this value can be overridden by the {@link HttpClientBuilder#setHttpProcessor( |
| * org.apache.http.protocol.HttpProcessor)} method. |
| * </p> |
| * |
| * @param itcp the request and response interceptor |
| * @return this |
| */ |
| public final InternalBuilder<T> addInterceptorFirst(final HttpRequestResponseInterceptor itcp) { |
| httpClientBuilder.addInterceptorFirst((HttpRequestInterceptor) itcp); |
| httpClientBuilder.addInterceptorFirst((HttpResponseInterceptor) itcp); |
| return this; |
| } |
| |
| /** |
| * Adds this protocol interceptor to the tail of the protocol processing list for both requests and responses |
| * <p> |
| * Please note this value can be overridden by the {@link HttpClientBuilder#setHttpProcessor( |
| * org.apache.http.protocol.HttpProcessor)} method. |
| * </p> |
| * |
| * @param itcp the request and response interceptor |
| * @return this |
| */ |
| public final InternalBuilder<T> addInterceptorLast(final HttpRequestResponseInterceptor itcp) { |
| httpClientBuilder.addInterceptorLast((HttpRequestInterceptor) itcp); |
| httpClientBuilder.addInterceptorLast((HttpResponseInterceptor) 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); |
| } |
| } |
| } |