com.adobe.cq.testing.client.components.AbstractComponent 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.components;
import com.adobe.cq.testing.client.ComponentClient;
import org.apache.sling.testing.clients.ClientException;
import org.apache.sling.testing.Constants;
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.codehaus.jackson.JsonNode;
import org.codehaus.jackson.node.ArrayNode;
import java.util.HashMap;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.concurrent.TimeoutException;
import static org.apache.http.HttpStatus.SC_CREATED;
import static org.apache.http.HttpStatus.SC_OK;
public abstract class AbstractComponent {
/**
* Stores the currently set of edited property values
*/
protected HashMap changeProperties = new HashMap<>();
/**
* The default location inside a CQ page where the component gets added
* @deprecated - use the component's {@link #getLocation()} method as this value is not applicable to all websites
*/
@Deprecated
public static final String DEFAULT_LOCATION = "/jcr:content/par/";
/**
* Path to the parent node of the component
*/
String parentPath = null;
/**
* Name of the component node.
*/
String name = null;
/**
* Reference to the {@link com.adobe.cq.testing.client.FoundationClient } that created
* this instance
*/
protected ComponentClient client = null;
/**
* The complete path to this component in the repository.
*/
protected String componentPath = null;
/**
* Path to the CQ page that contains this component
*/
protected String pagePath = null;
/**
* relative location inside the page
*/
protected String location = DEFAULT_LOCATION;
/**
* contains the Json Structure as it is currently saved on the server
*/
protected JsonNode jsonNode = null;
/**
* The constructor stores all the component path information like parentPage, name etc.
*
* @param client The ComponentClient that will be used for sending the requests.
* @param pagePath path to the page that will contain the component.
* @param location relative location to the parent node inside the page that will contain the component node.
* @param nameHint name to be used for the component node. Might get altered by the server if a naming conflict
* occurs. The {@link #getName()} method will return the correct name after {@link #create
* (order,int...)} has been called.
*/
public AbstractComponent(ComponentClient client, String pagePath,
String location, String nameHint) {
if (location != null) {
this.location = location;
}
this.parentPath = pagePath + this.location;
if (nameHint == null) {
this.name = this.getClass().getSimpleName().toLowerCase();
} else {
this.name = nameHint;
}
this.componentPath = this.parentPath + this.name;
this.pagePath = pagePath;
this.client = client;
}
/**
* Creates the component on the server by sending the http request.
* @param order Defines where the component should be added in relation to its siblings. Possible values
* are {@code first}, {@code last}, {@code before [nodeName]}, {@code after [nodeName]}.
* @param expectedStatus list of expected HTTP Status to be returned, if not set, 201 is assumed.
* @return Sling response
* @throws ClientException if something fails during the request/response cycle
* @throws InterruptedException to mark this method as waiting
*/
public SlingHttpResponse create(String order, int... expectedStatus) throws ClientException, InterruptedException {
// get the default setting
SlingHttpResponse exec = client.doPost(parentPath, getCreateFormEntity(order).build());
String errorMessage = "Creating a '" + this.getClass().getSimpleName() + "' component below " +
getParentPath() + " failed!";
HttpUtils.verifyHttpStatus(exec, errorMessage, HttpUtils.getExpectedStatus(SC_CREATED, expectedStatus));
// store the path that was really used
componentPath = exec.getSlingPath();
// store the name that was really used
name = componentPath.substring(componentPath.lastIndexOf("/") + 1);
// parse all known props and store them in the properties list
getComponentNode();
return exec;
}
/**
* Returns the FormEntityBuilder used for the create request in {@link #create(String,int...)}. This method can be
* overridden in a subclass to extend the parameters that need to sent with the creation request.
* @return An FormEntityBuilder with {@code _charset_},{@code ./sling:resourceType} and
* {@code :nameHint} set.
* @param order Defines where the component should be added in relation to its siblings. Possible values
* are {@code first}, {@code last}, {@code before [nodeName]}, {@code after [nodeName]}.
*/
public FormEntityBuilder getCreateFormEntity(String order) {
// build the form data for the request
FormEntityBuilder fBuilder = FormEntityBuilder.create()
.addParameter(Constants.PARAMETER_CHARSET, Constants.CHARSET_UTF8)
.addParameter("./sling:resourceType", getResourceType())
.addParameter(":nameHint", getName())
.addParameter("./jcr:created", "")
.addParameter("./jcr:createdBy", "")
.addParameter("./jcr:lastModified", "")
.addParameter("./jcr:lastModifiedBy", "");
// if order is set
if (order != null && !order.equals("")){
fBuilder.addParameter(":order",order);
}
return fBuilder;
}
/**
* Submits all editable values to the server. Same as pressing the {@code OK} button on the edit dialog
* of a component.
*
* @param expectedStatus list of allowed HTTP Status to be returned. if not set, status 200 is assumed
* @return Sling response
* @throws ClientException If something fails during request/response cycle
* @throws InterruptedException to mark this method as waiting
*/
public SlingHttpResponse save(int... expectedStatus) throws ClientException, InterruptedException {
// Start the form
FormEntityBuilder form = FormEntityBuilder.create()
.addParameter(Constants.PARAMETER_CHARSET, Constants.CHARSET_UTF8)
.addParameter("./sling:resourceType", getResourceType());
// add all properties
for (String key : changeProperties.keySet()) {
String[] values = changeProperties.get(key);
for (String value : values) {
if(!key.startsWith(":")){
form.addParameter("./" + key, value);
}
}
}
// execute the request
SlingHttpResponse exec = client.doPost(componentPath, form.build(), HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
// parse all known props and jsonNode and store them in the properties list
getComponentNode();
// the sling response wrapper
return exec;
}
/**
* Allows reordering of a component in relation to its siblings.
* @param order Defines where the component should be added in relation to its siblings. Possible values
* are {@code first}, {@code last}, {@code before [nodeName]}, {@code after [nodeName]}.
* @param expectedStatus list of allowed HTTP Status to be returned. if not set, status 200 is assumed
* @return Sling response
* @throws ClientException if posting the reorder fails
* @throws InterruptedException to mark this method as waiting
*/
public SlingHttpResponse reorder(String order, int... expectedStatus) throws ClientException, InterruptedException {
// Start the form
FormEntityBuilder form = FormEntityBuilder.create().addParameter(":order", order);
// execute the request
SlingHttpResponse exec = client.doPost(componentPath, form.build(), HttpUtils.getExpectedStatus(SC_OK, expectedStatus));
// parse all known props and jsonNode and store them in the properties list
getComponentNode();
// the sling response wrapper
return exec;
}
/**
* sets a string or multiple string property to be saved.
*
* @param name name of the property to set e.g. ./text.
* @param value the value(s) to be set.
*/
public void setProperty(String name, String... value) {
changeProperties.put(name, value);
}
/**
* sets a integer to be saved.
*
* @param name name of the property to set e.g. {@code ./width}.
* @param value the value(s) to be set.
*/
public void setProperty(String name, int value) {
changeProperties.put(name, new String[]{Integer.toString(value)});
}
/**
* Returns the property value as string, as it is saved at the moment on the server.
*
* @param propName name of the property
* @return prop value on server or {@code null} if no such property has been saved yet.
*/
public String getPropertyAsString(String propName) {
JsonNode propNode = getJsonNode(propName);
// if it is not set return null
if (propNode == null) return null;
// if its a multi value property
if (propNode.isArray()) {
ArrayNode array = ((ArrayNode) propNode);
// check if array is empty
if (array.size() == 0) return null;
// return only the first value
return array.get(0).getValueAsText();
}
return getJsonNode(propName).getValueAsText();
}
/**
* Returns the property value as string array, as it is saved at the moment on the server.
*
* @param propName name of the property
* @return value on server or {@code null} if no such property has been saved yet.
*/
public String[] getPropertyAsStringArray(String propName) {
return getStringArrayFromJsonNode(getJsonNode(propName));
}
/**
* returns a components property as JsonNode.
*
* @param propName the property to get
* @return jsonNode of the property stored
*/
public JsonNode getProperty(String propName) {
return getJsonNode(propName);
}
/**
* Returns the root node of the component node as a JsonNode
*
* @return a JsonNode of the component node
* @throws ClientException if something fails during request
* @throws InterruptedException to mark this method as waiting
*/
public JsonNode getComponentNode() throws ClientException, InterruptedException {
try {
client.waitExists(getComponentPath(), 60000, 100);
} catch (TimeoutException e) {
throw new ClientException("Component does not exist " + getComponentPath(), e);
}
jsonNode = client.doGetJson(getComponentPath(), -1);
return jsonNode;
}
private String[] getStringArrayFromJsonNode(JsonNode propNode) {
// if it is not set return null
if (propNode == null) return null;
// if its a not an array
if (!propNode.isArray()) {
// as string array with one entry
return new String[]{propNode.getValueAsText()};
}
ArrayNode array = ((ArrayNode) propNode);
// check if array is empty
if (array.size() == 0) return null;
// return values
String[] values = new String[array.size()];
for (int i = 0; i < array.size(); i++) {
values[i] = array.get(i).getValueAsText();
}
return values;
}
/**
* Returns the name of the component node. Gets preset with the nameHint passed in the constructor and
* updated if required after the create() request.
*
* @return Component node name
*/
public String getName() {
return name;
}
/**
* Path to the parent node of the component
*
* @return Node path
*/
public String getParentPath() {
return parentPath;
}
/**
* The complete path to the component.
*
* @return node path
*/
public String getComponentPath() {
return componentPath;
}
/**
* returns the path to the page that contains the component
*
* @return page path
*/
public String getPagePath() {
return pagePath;
}
/**
* returns the relative path inside the page
* @return relative node path
*/
public String getLocation() {
return location;
}
/**
* The resource type for the component, e.g. foundation/components/text. this will be set in the
* {@code sling:resourceType } property of the node.
*
* @return the resource type
*/
public abstract String getResourceType();
/**
* Returns the client that created this component.
*
* @return the Component Client
*/
public ComponentClient getClient() {
return client;
}
/**
* Get the JsonNode of a property. The property can have multiple levels.
*
* @param propName the name of the property
* @return the json node for the property
*/
private JsonNode getJsonNode(String propName) {
StringTokenizer st = new StringTokenizer(propName, "/");
JsonNode prop = jsonNode;
while (st.hasMoreElements()) {
prop = prop.get((String) st.nextElement());
}
return prop;
}
/**
* Returns the component wrapper instance for the following sibling or null if this component
* is already the last one. It looks for the sling:resourceType property an tries to find a matching
* component wrapper that has been registered with the ComponentClient (see {@link
* com.adobe.cq.testing.client.ComponentClient#registerComponent(String, Class)})
*
* @param component type
* @return A component wrapper instance or null if the node has no or an unknown resource type.
* @throws ClientException if something goes wrong during request/response of json.
* @throws InterruptedException to mark this method as waiting
*/
public T getNext() throws ClientException, InterruptedException {
String nextPath = getNextNodePath();
if (nextPath == null) return null;
return client.getComponent(nextPath);
}
/**
* Returns the component path for the following sibling or null if this component is already
* the last one.
* @return The component path to the following sibling.
* @throws ClientException if something goes wrong during request/response of json.
* @throws InterruptedException to mark this method as waiting
*/
public String getNextNodePath() throws ClientException, InterruptedException {
// get the parent node
try {
client.waitExists(getParentPath(), 60000, 100);
} catch (TimeoutException e) {
throw new ClientException("Parent path does not exist " + getComponentPath(), e);
}
JsonNode parNode = client.doGetJson(getParentPath(), 1);
// go through the list of field names
for(Iterator it = parNode.getFieldNames();it.hasNext();){
// get next field name
String testName = it.next();
// if the field references an object
if (parNode.get(testName).isObject()){
// if the field name the name of this node
if (testName.equals(getName())){
// if there is a next entry
if (it.hasNext()){
return getParentPath() + it.next();
}else{
return null;
}
}
}
}
return null;
}
/**
* returns the first child node that is a component or null if not found.
*
* @param a subclass of AbstractComponent
* @return an Component wrapper or null if it has no child components or the
* component type is unknown
* @throws ClientException if requesting json fails.
* @throws InterruptedException to mark this method as waiting
*/
public T getFirstChild() throws ClientException, InterruptedException {
// shortcut
JsonNode parentNode = getComponentNode();
// go trough the fields of the component node
for(Iterator it = parentNode.getFieldNames();it.hasNext();){
// get the next field name
String childName = it.next();
// get the next field node
JsonNode child = parentNode.get(childName);
// if the next field is an object and has a sling resource type property
if (child.isObject() && !child.get("sling:resourceType").isMissingNode()){
return client.getComponent(getComponentPath() + "/" + childName );
}
}
// nothing found return null
return null;
}
/**
* Returns the component wrapper instance for the previous sibling or null if this component
* is already the first one. It looks for the sling:resourceType property an tries to find a matching
* component wrapper that has been registered with the ComponentClient (see {@link
* com.adobe.cq.testing.client.ComponentClient#registerComponent(String, Class)})
*
* @param component type
* @return A component wrapper instance or null if the node has no or an unknown resource type.
* @throws ClientException if something goes wrong during request/response of json
* @throws InterruptedException to mark this method as waiting
*/
public T getPrevious() throws ClientException, InterruptedException {
String prevPath = getPreviousNodePath();
if (prevPath == null) return null;
return client.getComponent(prevPath);
}
/**
* Returns the component path for the previous sibling or null if this component is already
* the last one.
* @return The component path to the following sibling.
* @throws ClientException if something goes wrong during request/response of json.
* @throws InterruptedException to mark this method as waiting
*/
public String getPreviousNodePath() throws ClientException, InterruptedException {
// get the parent node
try {
client.waitExists(getParentPath(), 60000, 100);
} catch (TimeoutException e) {
throw new ClientException("Component does not exist " + getComponentPath(), e);
}
JsonNode parNode = client.doGetJson(getParentPath(), 1);
String prevNodeName = null;
// go through the list of field names
for(Iterator it = parNode.getFieldNames();it.hasNext();){
// get next field name
String testName = it.next();
// if the field references an object
if (parNode.get(testName).isObject()){
// if the field name is the current components name
if (testName.equals(getName())){
// if the node is not at first location
if (prevNodeName != null){
return getParentPath() + prevNodeName;
} else {
return null;
}
} else {
// store it as the possible previous value
prevNodeName = testName;
}
}
}
return null;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy