com.adobe.cq.testing.client.CQClient Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aem-cloud-testing-clients Show documentation
Show all versions of aem-cloud-testing-clients Show documentation
AEM related clients and testing utilities for AEM as a Cloud Service
/*
* Copyright 2017 Adobe Systems Incorporated
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.adobe.cq.testing.client;
import com.adobe.cq.testing.util.WCMCommands;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.annotation.Immutable;
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.message.BasicNameValuePair;
import org.apache.sling.testing.Constants;
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.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.ResourceUtil;
import org.apache.sling.testing.clients.util.poller.Polling;
import org.codehaus.jackson.JsonNode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
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;
/**
* Base client for all CQ related clients. It provides a core set of commonly used website and page functions
* e.g. creating/deleting/moving pages, versioning, activation/deactivation, restore tree etc.
*
* It extends from {@link SlingClient} which in turn provides functions for
* manipulating repository nodes directly.
*/
@Immutable
public class CQClient extends SlingClient {
public static Logger LOG = LoggerFactory.getLogger(CQClient.class);
/**
* Path where statistics are stored
*/
protected static final String STATISTICS_ROOT = "/var/statistics/pages";
/**
* WCMCommands object that encapsulates all available WCM commands
*/
protected final WCMCommands wcmCommands = new WCMCommands(this);
/**
* Constructor used by Builders and adaptTo(). Should never be called directly from the code.
* See AbstractSlingClient#AbstractSlingClient(CloseableHttpClient, SlingClientConfig)
*
* @param http the underlying HttpClient to be used
* @param config sling specific configs
* @throws ClientException if the client could not be created
*/
public CQClient(CloseableHttpClient http, SlingClientConfig config) throws ClientException {
super(http, config);
}
/**
* Handy constructor easy to use in simple tests. Creates a client that uses basic authentication.
*
* For constructing clients with complex configurations, use a {@link InternalBuilder}
*
* For constructing clients with the same configuration, but a different class, use {@link #adaptTo(Class)}
*
* @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 CQClient(URI url, String user, String password) throws ClientException {
super(url, user, password);
}
/**
* Creates a CQ page in the repository.
*
* @param pageName name of the page
* @param pageTitle title of the page
* @param parentPath path to the parent page
* @param templatePath path to the template
* @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed.
* @return a {@link SlingHttpResponse}
* @throws ClientException if something fails during the request/response cycle
*/
public SlingHttpResponse createPage(String pageName, String pageTitle, String parentPath, String templatePath,
int... expectedStatus) throws ClientException {
return wcmCommands.createPage(pageName, pageTitle, parentPath, templatePath, expectedStatus);
}
/**
* Tries to create a CQ page until the request succeeds or timeout is reached
*
* @param pageName name of the page
* @param pageTitle title of the page
* @param parentPath path to the parent page
* @param templatePath path to the template definition
* @param timeout max execution time, in milliseconds
* @param delay time to wait between retries, in milliseconds
* @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed.
* @return a {@link SlingHttpResponse}
* @throws ClientException if something fails during the request/response cycle
* @throws InterruptedException to mark this method as waiting
*/
public SlingHttpResponse createPageWithRetry(final String pageName, final String pageTitle,
final String parentPath, final String templatePath,
long timeout, long delay, final int... expectedStatus)
throws ClientException, InterruptedException {
class CreatePagePolling extends Polling {
SlingHttpResponse response;
@Override
public Boolean call() throws Exception {
response = wcmCommands.createPage(pageName, pageTitle, parentPath, templatePath, expectedStatus);
return true;
}
}
CreatePagePolling createPolling = new CreatePagePolling();
try {
createPolling.poll(timeout, delay);
} catch (TimeoutException e) {
throw new ClientException("Failed to create page " + pageName + " in " + createPolling.getWaited(), e);
}
return createPolling.response;
}
/**
* Deletes an array of pages.
* The caller must ensure that the paths can be deleted
*
* @param pagePaths array of paths to be deleted
* @param force force param passed to wcmCommands
* @param shallow shallow param passed to wcmCommands
* @param expectedStatus list of expected HTTP status to be returned
* @return the response
* @throws ClientException if one of the pages fails to delete
*/
public SlingHttpResponse deletePage(String[] pagePaths, boolean force, boolean shallow, int... expectedStatus)
throws ClientException {
return wcmCommands.deletePage(pagePaths, force, shallow, expectedStatus);
}
/**
* Tries to deletes a CQ page multiple times if the request fails
*
* @param pagePath the path to delete
* @param force passed to wcmCommands
* @param shallow passed to wcmCommands
* @param expectedStatus list of expected HTTP status to be returned
* @param timeout max execution time, in milliseconds
* @param delay time to wait between retries, in milliseconds
* @return the response
* @throws ClientException if the page(s) wre not deleted
* @throws InterruptedException if the method was interrupted
*/
public SlingHttpResponse deletePageWithRetry(final String pagePath, final boolean force, final boolean shallow,
long timeout, long delay, final int... expectedStatus)
throws ClientException, InterruptedException {
// The deletePage call might fail with a server-side StaleItemStateException, as we're deleting a page
// that we just created and CQ listeners might still be making modifications to it.
// Retry a few times if that happens - this is an "unusual" use case that we don't need to solve server-side.
class DeletePagePolling extends Polling {
SlingHttpResponse response;
@Override
public Boolean call() throws ClientException {
response = deletePage(new String[]{pagePath}, force, shallow, expectedStatus);
return !pageExists(pagePath);
}
}
DeletePagePolling deletePagePolling = new DeletePagePolling();
try {
deletePagePolling.poll(timeout, delay);
} catch (TimeoutException e) {
throw new ClientException("Could not delete page " + pagePath + " as user " + getUser(), e);
}
return deletePagePolling.response;
}
/**
* Returns whether a CQ page exists or not
*
* @param pagePath The path of the page
* @return whether the CQ page exists
* @throws ClientException If the request failed
*/
public boolean pageExists(String pagePath) throws ClientException {
SlingHttpResponse response = getAuthorSitesPage(pagePath);
final int status = response.getStatusLine().getStatusCode();
return status == SC_OK;
}
/**
* Polls on whether a CQ page exists or not
*
* @param pagePath The path of the page
* @param timeout Timeout in milliseconds for the poller
* @return whether the CQ page exists
* @throws InterruptedException if interrupted
*/
public boolean pageExistsWithRetry(String pagePath, int timeout) throws InterruptedException {
try {
new Polling(() -> pageExists(pagePath)).poll(timeout, 500);
} catch (TimeoutException e) {
return false;
}
return true;
}
/**
* Polls on whether a CQ page exists or not after 1 second
*
* @param pagePath The path of the page
* @return whether the CQ page exists
* @throws InterruptedException if interrupted
*/
public boolean pageExistsWithRetry(String pagePath) throws InterruptedException {
return pageExistsWithRetry(pagePath, 1000);
}
/**
* Get a CQ Page (.html extension)
*
* @param pagePath The path of the page
* @param expectedStatus An array of expected HTTP status codes for the response
* @return the http response
* @throws ClientException if the request failed
*/
public SlingHttpResponse getAuthorSitesPage(String pagePath, int... expectedStatus) throws ClientException {
final String uriPath = getPageAbsoluteUri(pagePath).toString();
return this.doGet(uriPath, expectedStatus);
}
private URI getPageAbsoluteUri(String pagePath) {
final String path = pagePath.endsWith(".html")
? pagePath
: pagePath.replaceFirst("/*$", "").concat(".html");
return this.getUrl(path);
}
/**
* Copies one or more CQ pages to a specified location in the repository.
*
* @param srcPaths list of pages to copy
* @param destName name given to the copied page at new location. Only works for single page copy, otherwise
* the operation fails.
* @param destPath destination of copy operation. Can be used instead of destParentPath + destName.
* Works only for single page copy, otherwise the operation fails.
* @param destParentPath target location of the copy operation
* @param before if true, the copied page will be ordered before the page with this label (Name)
* @param shallow if true, the only the page itself gets copied
* @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed.
* @return the http response
* @throws ClientException if something fails during the request/response cycle
*/
public SlingHttpResponse copyPage(String[] srcPaths, String destName, String destPath, String destParentPath,
String before, boolean shallow, int... expectedStatus)
throws ClientException {
return wcmCommands.copyPage(srcPaths, destName, destParentPath, before, shallow,
HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Moves one or more CQ pages to a specified location.
*
* Setting {@code integrity} to false and list of referrers in {@code adjusts} to null equals
* a drag'n'drop move in the site admin. The server then performs an auto adjustment on referring pages.
*
* Passing a list of referrers and setting integrity flag to {@code true} is the same as the
* {@code Move...} command in the site admin.
*
* @param srcPaths list of pages to copy
* @param destName name given to the moved page at new location. Only works for single page copy, otherwise
* the operation fails.
* @param destPath destination of copy operation. Can be used instead of destParentPath + destName. Only
* works for single page copy, otherwise the operation fails.
* @param destParentPath target location of the move operation
* @param before if true, the copied page will be ordered before the page with this label (Name)
* @param shallow if true, the only the page itself gets copied
* @param integrity if true, no auto adjustment of referred pages will be done
* @param adjusts List of referrer page paths that need adjusting
* @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 movePage(String[] srcPaths, String destName, String destPath, String destParentPath,
String before, boolean shallow, boolean integrity, String[] adjusts,
int... expectedStatus)
throws ClientException {
return wcmCommands.movePage(srcPaths, destName, destParentPath, before, shallow,
integrity, adjusts, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Moves one or more CQ pages to a specified location.
*
* Setting {@code integrity} to false and list of referrers in {@code adjusts} to null equals
* a drag'n'drop move in the site admin. The server then performs an auto adjustment on referring pages.
*
* Passing a list of referrers and setting integrity flag to {@code true} is the same as the
* {@code Move...} command in the site admin.
*
* @param srcPaths list of pages to copy
* @param destName name given to the moved page at new location. Only works for single page copy, otherwise
* the operation fails.
* @param destPath destination of copy operation. Can be used instead of destParentPath + destName. Only
* works for single page copy, otherwise the operation fails.
* @param destParentPath target location of the move operation
* @param before if true, the copied page will be ordered before the page with this label (Name)
* @param shallow if true, the only the page itself gets copied
* @param integrity if true, no auto adjustment of referred pages will be done
* @param adjusts list of referrer page paths that need adjusting
* @param publishes list of page paths that need to be published
* @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 movePage(String[] srcPaths, String destName, String destPath, String destParentPath,
String before, boolean shallow, boolean integrity, String[] adjusts,
String[] publishes, int... expectedStatus)
throws ClientException {
return wcmCommands.movePage(srcPaths, destName, destParentPath, before, shallow,
integrity, adjusts, publishes, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Locks a CQ page so it can only be edited by the person who locked it.
*
* @param path path of the page to lock
* @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 lockPage(String path, int... expectedStatus) throws ClientException {
return wcmCommands.lockPage(path, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Unlocks a previously locked CQ page.
*
* @param path path of the page to unlock
* @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 unlockPage(String path, int... expectedStatus) throws ClientException {
return wcmCommands.unlockPage(path, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Sets a single page property on a CQ page.
*
* @param pagePath path of the page to be edited
* @param propName name of the property to be edited
* @param propValue value to be set
* @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 setPageProperty(String pagePath, String propName, String propValue, int... expectedStatus)
throws ClientException {
ArrayList list = new ArrayList<>();
list.add(new BasicNameValuePair(propName, propValue));
return setPageProperties(pagePath, list, expectedStatus);
}
/**
* Sets multiple page properties on a CQ page with one request.
*
* @param pagePath path of the page to be edited
* @param props list of name/value string pairs to be set
* @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 setPageProperties(String pagePath, List props, int... expectedStatus)
throws ClientException {
UrlEncodedFormEntity formEntry = FormEntityBuilder.create()
.addParameter(Constants.PARAMETER_CHARSET, Constants.CHARSET_UTF8)
.addAllParameters(props)
.build();
return doPost(pagePath + "/jcr:content", formEntry, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Sets the teaser image of a content page.
*
* @param pagePath path to the page to be edited
* @param mimeType MIME type of the image getting uploaded
* @param fileName file name of the image
* @param resourcePath defines the path to the resource
* @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 setPagePropertyImage(String pagePath, String mimeType, String fileName, String resourcePath,
int... expectedStatus) throws ClientException {
HttpEntity entity = MultipartEntityBuilder.create()
.addBinaryBody("./image/file", ResourceUtil.getResourceAsStream(resourcePath),
ContentType.create(mimeType), fileName)
.setCharset(StandardCharsets.UTF_8)
.build();
// send the request with the multipart entity as content
return doPost(pagePath + "/jcr:content", entity, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Uploads an File to the repository. Same as using {@code New File...} in the Site Admin outside of the
* {@code Digital Assets} folder.
*
* This will create a folder with the file name and upload the file below it in a node typed {@code nt:file}.
* To upload a file that is to be handled as an Asset use {@link CQAssetsClient#uploadAsset} instead.
* To upload a file directly using sling use {@link #upload(java.io.File, String, String, boolean, int...) upload}.
*
* @param fileName file name. The file name will become part of the URL to the file.
* @param resourcePath defines the path to the resource
* @param mimeType MIME type of the image getting uploaded
* @param parentPath parent page (folder) that will contain the file
* @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 uploadFileCQStyle(String fileName, String resourcePath, String mimeType,
String parentPath, int... expectedStatus) throws ClientException {
String filePath = parentPath + "/" + fileName;
HttpEntity entity = MultipartEntityBuilder.create()
.addBinaryBody("file", ResourceUtil.getResourceAsStream(resourcePath),
ContentType.create(mimeType), fileName)
.addTextBody("fileName", fileName)
.setCharset(StandardCharsets.UTF_8)
.build();
return doPost(filePath, entity, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
}
/**
* Creates a Version for a CQ page. See {@code Version} tab in the sidekick of a page when opened on
* an author instance.
*
* @param pagePath path of the page we want to create a version of
* @param comment comment to be set for this version
* @param label Version label to be set
* @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 createVersion(String pagePath, String comment, String label, int... expectedStatus)
throws ClientException {
// execute the request
return wcmCommands.createVersion(pagePath, comment, label, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Restores a specified version of a CQ page. See {@code Version} tab in the sidekick of a page when opened
* on an author instance.
*
* @param versionIds version Id of the current page and/or version ids of sub pages of {@code pagePath}
* @param pagePath path to the page we want to restore the version
* @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 restoreVersion(String[] versionIds, String pagePath, int... expectedStatus)
throws ClientException {
// execute the request
return wcmCommands.restoreVersion(versionIds, pagePath, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Restores a sub page in path to the version that existed at the given date.
*
* @param path path of the root page of the tree
* @param date the date to restore
* @param preserveNVP whether to preserve NVP or not
* @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 restoreTree(String path, Date date, boolean preserveNVP,
int... expectedStatus) throws ClientException {
return wcmCommands.restoreTree(path, date, preserveNVP, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Creates language copies af a master site.
*
* @param sitePath path to the site with the master content
* @param relPaths list of string pairs, first being the relative path to be created, second the language
* shortcut to be copied from e.g. {fr/products/circle,en}
* @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 copyLanguages(String sitePath, List relPaths, int... expectedStatus)
throws ClientException {
return wcmCommands.copyLanguages(sitePath, relPaths, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Rolls out changes to the livecopy
*
* @param srcPath the blue print path
* @param targetPaths the live copy paths
* @param deep if set to false, page is the fallback
* @param reset reset
* @param useBackgroundJob if set to true background jobs are used for rollout
* @param expectedStatus list of expected HTTP Status to be returned, if not set, 200 is assumed
* @return Sling response
* @throws ClientException If something fails during request/response cycle
*/
public SlingHttpResponse rolloutPage(String srcPath, String[] targetPaths, boolean deep, boolean reset,
boolean useBackgroundJob, int... expectedStatus)
throws ClientException {
return wcmCommands.rollout(new String[]{srcPath}, targetPaths, null, (deep) ? "deep" : "page",
reset, useBackgroundJob, HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
}
/**
* Requests a text search on the content of a path and returns the pages matching the criteria.
*
* @param startPath Path to look at
* @param searchQuery The text to search for
* @param caseSensitive Do a "case-sensitive" search or not
* @param wholeWordOnly Do a "whole word" search or not
* @return The list of pages matching the search criteria
* @throws Exception If something fails during the request/response cycle
*/
public JsonNode searchInPages(String startPath, String searchQuery, boolean caseSensitive, boolean wholeWordOnly)
throws Exception {
String searchPath = startPath + ".find.json";
ArrayList params = new ArrayList<>();
params.add(new BasicNameValuePair("f", searchQuery));
params.add(new BasicNameValuePair(Constants.PARAMETER_CHARSET, Constants.CHARSET_UTF8));
params.add(new BasicNameValuePair("cs", Boolean.toString(caseSensitive)));
params.add(new BasicNameValuePair("wwo", Boolean.toString(wholeWordOnly)));
SlingHttpResponse exec = doGet(searchPath, params, SC_OK);
return JsonUtils.getJsonNodeFromString(exec.getContent());
}
/**
* Requests a search and replaces the content of a path.
*
* @param startPath Path to look at
* @param searchQuery The text to search for
* @param replace The text to replace with
* @param caseSensitive Do a "case-sensitive" search or not
* @param wholeWordOnly Do a "whole word" search or not
* @throws Exception If something fails during the request/response cycle
*/
public void searchAndReplaceInPages(String startPath, String searchQuery, String replace, boolean caseSensitive,
boolean wholeWordOnly)
throws Exception {
JsonNode pages = searchInPages(startPath, searchQuery, caseSensitive, wholeWordOnly).get("matches");
if (pages.isArray() && pages.size() > 0) {
String replacePath = startPath + ".replace.json";
FormEntityBuilder entityBuilder = FormEntityBuilder.create();
entityBuilder.addParameter(Constants.PARAMETER_CHARSET, Constants.CHARSET_UTF8);
entityBuilder.addParameter("f", searchQuery);
entityBuilder.addParameter("r", replace);
if (caseSensitive) {
entityBuilder.addParameter("cs", "on");
}
if (wholeWordOnly) {
entityBuilder.addParameter("wwo", "on");
}
for (Iterator it = pages.getElements(); it.hasNext(); ) {
JsonNode page = it.next();
entityBuilder.addParameter("p", page.get("path").getTextValue());
}
doPost(replacePath, entityBuilder.build(), SC_OK);
}
}
/**
* Deletes existing statistics (page impressions) of a page.
*
* @param pagePath path of the page to delete the statistics for.
* @throws ClientException if something fails during the request/response cycle
*/
public void resetPageStatistics(String pagePath) throws ClientException {
deletePath(STATISTICS_ROOT + pagePath);
}
// Builders
public static abstract class InternalBuilder extends SlingClient.InternalBuilder {
protected InternalBuilder(URI url, String user, String password) {
super(url, user, password);
}
}
public final static class Builder extends InternalBuilder {
private Builder(URI url, String user, String password) {
super(url, user, password);
}
@Override
public CQClient build() throws ClientException {
return new CQClient(buildHttpClient(), buildSlingClientConfig());
}
public static Builder create(URI url, String user, String password) {
return new Builder(url, user, password);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy