com.day.cq.wcm.foundation.forms.FormResourceEdit Maven / Gradle / Ivy
Show all versions of aem-sdk-api Show documentation
/*
* Copyright 1997-2011 Day Management AG
* Barfuesserplatz 6, 4001 Basel, Switzerland
* All Rights Reserved.
*
* This software is the confidential and proprietary information of
* Day Management AG, ("Confidential Information"). You shall not
* disclose such Confidential Information and shall use it only in
* accordance with the terms of the license agreement you entered into
* with Day.
*/
package com.day.cq.wcm.foundation.forms;
import static org.apache.sling.servlets.post.SlingPostConstants.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.jackrabbit.util.Text;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.request.RequestDispatcherOptions;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.request.RequestParameterMap;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.wrappers.SlingHttpServletRequestWrapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Helper class for bulk editing of multiple resources via CQ forms and the
* Sling POST servlet.
*
* @since 5.5
*/
public abstract class FormResourceEdit {
private static final Logger log = LoggerFactory.getLogger(FormResourceEdit.class);
public static final String RESOURCES_ATTRIBUTE = "cq.form.editresources";
public static final String RESOURCES_PARAM = ":resource";
public static final String REOPEN_PARAM = "reopen";
// suffix for checkbox which indicates if a property has to be written if multiple resources are loaded
public static final String WRITE_SUFFIX = "@Write";
/**
* Sets the list of resources to be handled by the "edit" resources form action.
* @param req current request
* @param resources the list of resources
*/
public static void setResources(ServletRequest req, List resources) {
req.setAttribute(RESOURCES_ATTRIBUTE, resources);
}
/**
* Get the list of resources to be handled by the "edit" resources form action.
* @param req current request
* @return the list of resources (or null
if not set)
*/
@SuppressWarnings("unchecked")
public static List getResources(ServletRequest req) {
return (List) req.getAttribute(RESOURCES_ATTRIBUTE);
}
/**
* Retrieves a resource that presents a synthetic resource with merged
* values for all the given resources. Uses {@link MergedValueMap}.
*
* @param resources
* list of resources to merge
* @return a synthetic resource with merged values
*/
public static Resource getMergedResource(List resources) {
return new MergedMultiResource(resources);
}
/**
* Helper struct for the result of
* {@link FormResourceEdit#getCommonAndPartialMultiValues(List, String)}.
*/
public static class CommonAndPartial {
/**
* Common values, present in all multi-value properties.
*/
public Set common = new HashSet();
/**
* Partial values, present only in one or a few multi-value properties,
* but not in all.
*/
public Set partial = new HashSet();
}
/**
* Calculates the set of common values and the set of partially present
* values for a multi-value property on a list of resources. The
* {@link CommonAndPartial#common} values will be the ones that are present in the property
* in all resources, whereas the {@link CommonAndPartial#partial} values will be the set of
* all values that are present at least in one resource, but not all of
* them. The multi-value property is seen as set, so the order and multiple
* occurrences of the same value in a single property do not make any
* difference.
*
* @param resources
* a list of resources
* @param name
* the name of the multi-value property to inspect
* @return a struct object with the {@link CommonAndPartial#common} and {@link CommonAndPartial#partial}
* sets of values
*/
@SuppressWarnings("unchecked")
public static CommonAndPartial getCommonAndPartialMultiValues(List resources, String name) {
CommonAndPartial r = new CommonAndPartial();
boolean firstResource = true;
for (Resource resource : resources) {
ValueMap map = resource.adaptTo(ValueMap.class);
if (map != null) {
String[] values = map.get(name, new String[0]);
if (firstResource) {
for (String v: values) {
r.common.add(v);
}
firstResource = false;
} else {
List newValues = Arrays.asList(values);
// partial: add all that are not in common
r.partial.addAll(CollectionUtils.disjunction(r.common, newValues));
// common: reset to only what actually overlaps, all the time
r.common = new HashSet(CollectionUtils.intersection(r.common, newValues));
}
}
}
return r;
}
/**
* Returns if exactly a single resource is handled by the "edit" resource
* form action.
*
* @param req
* current request
* @return true if a single resource is handled, false if multiple resources
* or no resource at all is handled
*/
public static boolean isSingleResource(ServletRequest req) {
List r = getResources(req);
return r != null && r.size() == 1;
}
/**
* Returns if multiple resources are handled by the "edit" resource form
* action.
*
* @param req
* current request
* @return true if multiple resources are handled, false if a single or no
* resource at all is handled
*/
public static boolean isMultiResource(ServletRequest req) {
List r = getResources(req);
return r != null && r.size() > 1;
}
/**
* Returns whether the given form POST based on the "edit" resource action
* targets a single resource.
*
* @param request
* current request
* @return if a single resource is target of the POST
*/
public static boolean isSingleResourcePost(SlingHttpServletRequest request) {
RequestParameter[] resourceParams = request.getRequestParameters(RESOURCES_PARAM);
return resourceParams == null || resourceParams.length == 1;
}
/**
* Returns whether the given form POST based on the "edit" resource action
* targets multiple resources.
*
* @param request
* current request
* @return if multiple resources are target of the POST
*/
public static boolean isMultiResourcePost(SlingHttpServletRequest request) {
RequestParameter[] resourceParams = request.getRequestParameters(RESOURCES_PARAM);
return resourceParams != null && resourceParams.length > 1;
}
/**
* Returns the (unvalidated) path of the single resource that is target of
* the form POST request. If this is not a form POST based on the "edit"
* resource action or if multiple resources are the target, this will return
* null
.
*
* @param request
* current request
* @return path of the resource or null
*/
public static String getPostResourcePath(SlingHttpServletRequest request) {
RequestParameter[] resourceParams = request.getRequestParameters(RESOURCES_PARAM);
if (resourceParams != null && resourceParams.length == 1) {
return resourceParams[0].getString();
}
return null;
}
/**
* Returns a list of all resources that are the target of the form POST
* request based on the "edit" resource action, and which can actually
* be written to using the request session.
*
* @param request
* current request
* @return list with all resolved target resources
*/
public static List getPostResources(SlingHttpServletRequest request) {
ResourceResolver resolver = request.getResourceResolver();
RequestParameter[] resourceParams = request.getRequestParameters(RESOURCES_PARAM);
Session session = request.getResourceResolver().adaptTo(Session.class);
List resources = new ArrayList();
if (resourceParams != null) {
for (RequestParameter rp : resourceParams) {
Resource r = resolver.getResource(rp.getString());
try {
if (r != null && session.hasPermission(r.getPath(), Session.ACTION_SET_PROPERTY)) {
resources.add(r);
}
} catch (RepositoryException e) {
log.error("Could not check write permission on node", e);
}
}
}
return resources;
}
/**
* Performs a Sling POST servlet modify operation, but on multiple
* resources.
*
* The Sling POST servlet (more specifically, its modify operation) itself
* can only handle a single resource (using the request resource) or by
* using absolute paths to properties. This method will automatically
* rewrite the parameters for the multiple resources and then call the Sling
* POST servlet. All resources will be changed in a single transaction. The
* response will look like the standard Sling POST response.
*
* @param resources
* list of resources to bulk-edit
* @param request
* current POST request, including the parameters for the Sling
* POST servlet
* @param response
* current response
* @throws ServletException if post fails
* @throws IOException if post fails
*/
public static void multiPost(List resources, SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException {
// 1. rewrite request params
final RequestParameterMap originalParams = request.getRequestParameterMap();
// group params by their property
//
// foo => foo = 1.1.2011
// foo@Delete = x
// foo@TypeHint = Date
// bar => bar = test
// ...
Map > groupedParams = new TreeMap>();
// 1. collect params, identify which to consider for writing
boolean requireItemPrefix = false;
Set paramsToKeep = new HashSet();
Set paramsToRemove = new HashSet();
for (Entry param : originalParams.entrySet()) {
final String name = param.getKey();
if (RP_OPERATION.equals(name)) {
String op = originalParams.getValue(name).getString();
// abort if operation is not modify as we can't bulk create/copy/move/checkin/checkout
if (!"modify".equals(op)) {
throw new ServletException("Only :operation=modify can be used when posting to multiple resources (was: '" + op + "')");
}
}
if (name.startsWith(ITEM_PREFIX_RELATIVE_CURRENT)) {
requireItemPrefix = true;
}
// if param name ends with "@something", the property name is the part before it
final int pos = name.indexOf("@");
String propName = pos >= 0 ? name.substring(0, pos) : name;
// add to map or use already existing entry for this property
Map map = groupedParams.get(propName);
if (map == null) {
groupedParams.put(propName, map = new TreeMap());
}
map.put(name, param.getValue());
// properties to write end with @Write
if (name.endsWith(WRITE_SUFFIX)) {
paramsToKeep.add(propName);
map.remove(name); // we don't need the "@Write" param in the sling post servlet
}
// remove form params (for cleaner request, technically not needed, as they are ignored by sling anyway)
if (FormsConstants.REQUEST_PROPERTY_FORMID.equals(name)
|| FormsConstants.REQUEST_PROPERTY_FORM_START.equals(name)
|| RESOURCES_PARAM.equals(name)) {
continue;
}
// all special :something sling post params must be kept
if (name.startsWith(RP_PREFIX)) {
paramsToKeep.add(name);
}
// @MoveFrom must be ignored (doesn't work for multiple resources)
if (name.endsWith(SUFFIX_MOVE_FROM)) {
paramsToRemove.add(propName);
}
}
// 2. remove params that should not be written
for (Iterator iter = groupedParams.keySet().iterator(); iter.hasNext();) {
String name = iter.next();
if (!paramsToKeep.contains(name) || paramsToRemove.contains(name)) {
iter.remove();
}
}
// 3. rewrite params for each resource
ParameterMap params = new ParameterMap();
log.debug("posting to multiple resources:");
boolean first = true;
for (Resource r: resources) {
String path = r.getPath();
log.debug("{}", path);
for (Map p : groupedParams.values()) {
for (Entry param : p.entrySet()) {
String name = param.getKey();
// include :params and absolute paths only once and don't rewrite them
if (name.startsWith(RP_PREFIX) || name.startsWith(ITEM_PREFIX_ABSOLUTE)) {
if (first) {
params.put(name, param.getValue());
}
} else if (requireItemPrefix) {
// only use params with item prefix (and skip others)
if (name.startsWith(ITEM_PREFIX_RELATIVE_CURRENT)) {
params.put(path + "/" + name.substring(ITEM_PREFIX_RELATIVE_CURRENT.length()), param.getValue());
} else if (name.startsWith(ITEM_PREFIX_RELATIVE_PARENT)) {
path = Text.getRelativeParent(path, 1);
params.put(path + "/" + name.substring(ITEM_PREFIX_RELATIVE_PARENT.length()), param.getValue());
}
} else /* if (!requireItemPrefix) */ {
// rewrite all params
params.put(path + "/" + name, param.getValue());
}
}
}
first = false;
}
if (log.isDebugEnabled()) {
log.debug("rewritten parameters:");
logParams(params);
}
// 4. send new internal request to sling post servlet
// forward but make sure we remove the "forms" selector and the suffix
RequestDispatcherOptions options = new RequestDispatcherOptions();
options.setReplaceSelectors("");
options.setReplaceSuffix("");
RequestDispatcher dispatcher = request.getRequestDispatcher(request.getResource(), options);
dispatcher.forward(new CustomParameterRequest(request, params), response);
}
private static void logParams(Map parameters) {
for (Entry ps : parameters.entrySet()) {
for (RequestParameter rp : ps.getValue()) {
log.debug("{} = {}", ps.getKey(), rp.getString());
}
}
}
/**
* Custom-tailored internal request to the Sling POST servlet, which wraps
* an existing request but allows to overwrite the parameters. It uses
* Sling's {@link RequestParameter}s, allowing to pass them through from the
* original request, but also providing the standard String representation
* of the parameters.
*/
private static class CustomParameterRequest extends SlingHttpServletRequestWrapper {
private ParameterMap parameters;
public CustomParameterRequest(SlingHttpServletRequest request, ParameterMap params) {
super(request);
this.parameters = params;
}
@Override
public RequestParameter getRequestParameter(String name) {
return parameters.getValue(name);
}
@Override
public RequestParameterMap getRequestParameterMap() {
return parameters;
}
@Override
public RequestParameter[] getRequestParameters(String name) {
return parameters.getValues(name);
}
@Override
public String getParameter(String name) {
return parameters.getStringValue(name);
}
@Override
public Map getParameterMap() {
return parameters.getStringParameterMap();
}
@Override
public Enumeration getParameterNames() {
return Collections.enumeration(parameters.keySet());
}
@Override
public String[] getParameterValues(String name) {
return parameters.getStringValues(name);
}
}
/**
* Custom implementation of Sling's {@link RequestParameterMap} that we have
* to provide for our custom-tailored internal request to the Sling POST
* servlet.
*/
private static class ParameterMap extends TreeMap implements RequestParameterMap {
private static final long serialVersionUID = 4554110574522792609L;
private Map stringParameterMap;
public RequestParameter[] getValues(String name) {
return get(name);
}
public RequestParameter getValue(String name) {
RequestParameter[] params = get(name);
return (params != null && params.length > 0) ? params[0] : null;
}
//---------- String parameter support
public String getStringValue(final String name) {
final RequestParameter param = getValue(name);
return (param != null) ? param.getString() : null;
}
public String[] getStringValues(final String name) {
return toStringArray(getValues(name));
}
public Map getStringParameterMap() {
if (this.stringParameterMap == null) {
LinkedHashMap pm = new LinkedHashMap();
for (Map.Entry ppmEntry : entrySet()) {
pm.put(ppmEntry.getKey(), toStringArray(ppmEntry.getValue()));
}
this.stringParameterMap = Collections.unmodifiableMap(pm);
}
return stringParameterMap;
}
private static String[] toStringArray(final RequestParameter[] params) {
if (params == null) {
return null;
}
final String[] ps = new String[params.length];
for (int i = 0; i < params.length; i++) {
ps[i] = params[i].getString();
}
return ps;
}
}
}