blob: e62e2758e219b532675918edf3a3436f8244e569 [file] [log] [blame]
/*
* 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.servlets.post;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.jcr.Item;
import javax.jcr.ItemNotFoundException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.servlet.http.HttpServletResponse;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceUtil;
import org.apache.sling.api.wrappers.SlingRequestPaths;
import org.apache.sling.servlets.post.exceptions.PreconditionViolatedPersistenceException;
import org.apache.sling.servlets.post.exceptions.TemporaryPersistenceException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>AbstractPostOperation</code> class is a base implementation of the
* {@link PostOperation} service interface providing actual implementations with
* useful tooling and common functionality like preparing the change logs or
* saving or refreshing the JCR Session.
*
* @deprecated (SLING-6722): this class mixes Sling and JCR APIs which is not
* optimal as nowadays we favor the Sling APIs. There's no intention to remove
* it however, if you're using JCR APIs anyways in your project it's fine to
* use it. Theres no public replacement for it as I write this.
*/
@Deprecated
public abstract class AbstractPostOperation implements PostOperation {
/**
* default log
*/
protected final Logger log = LoggerFactory.getLogger(getClass());
/**
* Prepares and finalizes the actual operation. Preparation encompasses
* getting the absolute path of the item to operate on by calling the
* {@link #getItemPath(SlingHttpServletRequest)} method and setting the
* location and parent location on the response. After the operation has
* been done in the {@link #doRun(SlingHttpServletRequest, PostResponse, List)}
* method the session is saved if there are unsaved modifications. In case
* of errorrs, the unsaved changes in the session are rolled back.
*
* @param request the request to operate on
* @param response The <code>PostResponse</code> to record execution
* progress.
* @param processors The array of processors
*/
@Override
public void run(final SlingHttpServletRequest request,
final PostResponse response,
final SlingPostProcessor[] processors) throws PreconditionViolatedPersistenceException, TemporaryPersistenceException {
final Session session = request.getResourceResolver().adaptTo(Session.class);
final VersioningConfiguration versionableConfiguration = getVersioningConfiguration(request);
try {
// calculate the paths
String path = getItemPath(request);
path = removeAndValidateWorkspace(path, session);
response.setPath(path);
// location
response.setLocation(externalizePath(request, path));
// parent location
path = ResourceUtil.getParent(path);
if (path != null) {
response.setParentLocation(externalizePath(request, path));
}
final List<Modification> changes = new ArrayList<>();
doRun(request, response, changes);
// invoke processors
if (processors != null) {
for (SlingPostProcessor processor : processors) {
processor.process(request, changes);
}
}
// check modifications for remaining postfix and store the base path
final Map<String, String> modificationSourcesContainingPostfix = new HashMap<>();
final Set<String> allModificationSources = new HashSet<>(changes.size());
for (final Modification modification : changes) {
final String source = modification.getSource();
if (source != null) {
allModificationSources.add(source);
final int atIndex = source.indexOf('@');
if (atIndex > 0) {
modificationSourcesContainingPostfix.put(source.substring(0, atIndex), source);
}
}
}
// fail if any of the base paths (before the postfix) which had a postfix are contained in the modification set
if (modificationSourcesContainingPostfix.size() > 0) {
for (final Map.Entry<String, String> sourceToCheck : modificationSourcesContainingPostfix.entrySet()) {
if (allModificationSources.contains(sourceToCheck.getKey())) {
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
"Postfix-containing path " + sourceToCheck.getValue() +
" contained in the modification list. Check configuration.");
return;
}
}
}
final Set<String> nodesToCheckin = new LinkedHashSet<>();
// set changes on html response
for(Modification change : changes) {
switch ( change.getType() ) {
case MODIFY : response.onModified(change.getSource()); break;
case DELETE : response.onDeleted(change.getSource()); break;
case MOVE : response.onMoved(change.getSource(), change.getDestination()); break;
case COPY : response.onCopied(change.getSource(), change.getDestination()); break;
case CREATE :
response.onCreated(change.getSource());
if (versionableConfiguration.isCheckinOnNewVersionableNode()) {
nodesToCheckin.add(change.getSource());
}
break;
case ORDER : response.onChange("ordered", change.getSource(), change.getDestination()); break;
case CHECKOUT :
response.onChange("checkout", change.getSource());
nodesToCheckin.add(change.getSource());
break;
case CHECKIN :
response.onChange("checkin", change.getSource());
nodesToCheckin.remove(change.getSource());
break;
}
}
if (isSessionSaveRequired(session, request)) {
request.getResourceResolver().commit();
}
if (!isSkipCheckin(request)) {
// now do the checkins
for(String checkinPath : nodesToCheckin) {
if (checkin(request.getResourceResolver(), checkinPath)) {
response.onChange("checkin", checkinPath);
}
}
}
} catch (Exception e) {
log.error("Exception during response processing.", e);
response.setError(e);
} finally {
try {
if (isSessionSaveRequired(session, request)) {
request.getResourceResolver().revert();
}
} catch (RepositoryException e) {
log.warn("RepositoryException in finally block: {}",
e.getMessage(), e);
}
}
}
/**
* Actually performs the desired operation filling progress into the
* <code>changes</code> list and preparing and further information in the
* <code>response</code>.
* <p>
* The <code>response</code> comes prepared with the path, location and
* parent location set. Other properties are expected to be set by this
* implementation.
*
* @param request The <code>SlingHttpServletRequest</code> providing the
* input, mostly in terms of request parameters, to the
* operation.
* @param response The {@link PostResponse} to fill with response
* information
* @param changes A container to add {@link Modification} instances
* representing the operations done.
* @throws RepositoryException Maybe thrown if any error occurrs while
* accessing the repository.
* @throws TemporaryPersistenceException if a retry could help
* @throws PreconditionViolatedPersistenceException if a retry doesn't make sense
* and some preconditions should be changed.
*/
protected abstract void doRun(SlingHttpServletRequest request,
PostResponse response,
List<Modification> changes) throws RepositoryException, PreconditionViolatedPersistenceException, TemporaryPersistenceException;
/**
* Get the versioning configuration.
* @param request The http request
* @return The versioning configuration
*/
protected VersioningConfiguration getVersioningConfiguration(SlingHttpServletRequest request) {
VersioningConfiguration versionableConfiguration =
(VersioningConfiguration) request.getAttribute(VersioningConfiguration.class.getName());
return versionableConfiguration != null ? versionableConfiguration : new VersioningConfiguration();
}
/**
* Check if checkin should be skipped
* @param request The http request
* @return {@code true} if checkin should be skipped
*/
protected boolean isSkipCheckin(SlingHttpServletRequest request) {
return !getVersioningConfiguration(request).isAutoCheckin();
}
/**
* Check whether changes should be written back
* @param request The http request
* @return {@code true} If session handling should be skipped
*/
protected boolean isSkipSessionHandling(SlingHttpServletRequest request) {
return Boolean.parseBoolean((String) request.getAttribute(SlingPostConstants.ATTR_SKIP_SESSION_HANDLING)) == true;
}
/**
* Check whether commit to the resource resolver should be called.
* @param session The JCR session
* @param request The http request
* @return {@code true} if a save is required.
* @throws RepositoryException a repository exception
*/
protected boolean isSessionSaveRequired(Session session, SlingHttpServletRequest request)
throws RepositoryException {
return !isSkipSessionHandling(request) && request.getResourceResolver().hasChanges();
}
/**
* Remove the workspace name, if any, from the start of the path and validate that the
* session's workspace name matches the path workspace name.
* @param path The path
* @param session The JCR session
* @return The path without the workspace
* @throws RepositoryException a repository exception
*/
protected String removeAndValidateWorkspace(String path, Session session) throws RepositoryException {
final int wsSepPos = path.indexOf(":/");
if (wsSepPos != -1) {
final String workspaceName = path.substring(0, wsSepPos);
if (!workspaceName.equals(session.getWorkspace().getName())) {
throw new RepositoryException("Incorrect workspace. Expecting " + workspaceName + ". Received "
+ session.getWorkspace().getName());
}
return path.substring(wsSepPos + 1);
}
return path;
}
/**
* Returns the path of the resource of the request as the item path.
* <p>
* This method may be overwritten by extension if the operation has
* different requirements on path processing.
* @param request The http request
* @return The item path
*/
protected String getItemPath(SlingHttpServletRequest request) {
return request.getResource().getPath();
}
/**
* Returns an iterator on <code>Resource</code> instances addressed in the
* {@link SlingPostConstants#RP_APPLY_TO} request parameter. If the request
* parameter is not set, <code>null</code> is returned. If the parameter
* is set with valid resources an empty iterator is returned. Any resources
* addressed in the {@link SlingPostConstants#RP_APPLY_TO} parameter is
* ignored.
*
* @param request The <code>SlingHttpServletRequest</code> object used to
* get the {@link SlingPostConstants#RP_APPLY_TO} parameter.
* @return The iterator of resources listed in the parameter or
* <code>null</code> if the parameter is not set in the request.
*/
protected Iterator<Resource> getApplyToResources(
SlingHttpServletRequest request) {
final String[] applyTo = request.getParameterValues(SlingPostConstants.RP_APPLY_TO);
if (applyTo == null) {
return null;
}
return new ApplyToIterator(request, applyTo);
}
/**
* Returns an external form of the given path prepending the context path
* and appending a display extension.
*
* @param request The http request
* @param path the path to externalize
* @return the url
*/
protected final String externalizePath(SlingHttpServletRequest request,
String path) {
StringBuilder ret = new StringBuilder();
ret.append(SlingRequestPaths.getContextPath(request));
ret.append(request.getResourceResolver().map(path));
// append optional extension
String ext = request.getParameter(SlingPostConstants.RP_DISPLAY_EXTENSION);
if (ext != null && ext.length() > 0) {
if (ext.charAt(0) != '.') {
ret.append('.');
}
ret.append(ext);
}
return ret.toString();
}
/**
* Resolves the given path with respect to the current root path.
*
* @param absPath The absolute base path
* @param relPath the path to resolve
* @return the given path if it starts with a '/'; a resolved path
* otherwise.
*/
protected final String resolvePath(String absPath, String relPath) {
if (relPath.startsWith("/")) {
return relPath;
}
return absPath + "/" + relPath;
}
/**
* Returns true if any of the request parameters starts with
* {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT ./}.
* In this case only parameters starting with either of the prefixes
* {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT ./},
* {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT ../}
* and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE /} are
* considered as providing content to be stored. Otherwise all parameters
* not starting with the command prefix <code>:</code> are considered as
* parameters to be stored.
*
* @param request The http request
* @return If a prefix is required.
*/
protected final boolean requireItemPathPrefix(
SlingHttpServletRequest request) {
boolean requirePrefix = false;
Enumeration<?> names = request.getParameterNames();
while (names.hasMoreElements() && !requirePrefix) {
String name = (String) names.nextElement();
requirePrefix = name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT);
}
return requirePrefix;
}
/**
* Returns <code>true</code> if the <code>name</code> starts with either
* of the prefixes
* {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT ./},
* {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT ../}
* and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE /}.
*
* @param name The name
* @return {@code true} if the name has a prefix
*/
protected boolean hasItemPathPrefix(String name) {
return name.startsWith(SlingPostConstants.ITEM_PREFIX_ABSOLUTE)
|| name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT)
|| name.startsWith(SlingPostConstants.ITEM_PREFIX_RELATIVE_PARENT);
}
/**
* Orders the given node according to the specified command. The following
* syntax is supported: &lt;xmp&gt; | first | before all child nodes | before A |
* before child node A | after A | after child node A | last | after all
* nodes | N | at a specific position, N being an integer &lt;/xmp&gt;
*
* @param request The http request
* @param item node to order
* @param changes The list of modifications
* @throws RepositoryException if an error occurs
*/
protected void orderNode(SlingHttpServletRequest request, Item item,
List<Modification> changes) throws RepositoryException {
String command = request.getParameter(SlingPostConstants.RP_ORDER);
if (command == null || command.length() == 0) {
// nothing to do
return;
}
if (!item.isNode()) {
return;
}
Node parent = item.getParent();
String next = null;
if (command.equals(SlingPostConstants.ORDER_FIRST)) {
next = parent.getNodes().nextNode().getName();
} else if (command.equals(SlingPostConstants.ORDER_LAST)) {
next = "";
} else if (command.startsWith(SlingPostConstants.ORDER_BEFORE)) {
next = command.substring(SlingPostConstants.ORDER_BEFORE.length());
} else if (command.startsWith(SlingPostConstants.ORDER_AFTER)) {
String name = command.substring(SlingPostConstants.ORDER_AFTER.length());
NodeIterator iter = parent.getNodes();
while (iter.hasNext()) {
Node n = iter.nextNode();
if (n.getName().equals(name)) {
if (iter.hasNext()) {
next = iter.nextNode().getName();
} else {
next = "";
}
}
}
} else {
// check for integer
try {
// 01234
// abcde move a -> 2 (above 3)
// bcade move a -> 1 (above 1)
// bacde
int newPos = Integer.parseInt(command);
next = "";
NodeIterator iter = parent.getNodes();
while (iter.hasNext() && newPos >= 0) {
Node n = iter.nextNode();
if (n.getName().equals(item.getName())) {
// if old node is found before index, need to
// inc index
newPos++;
}
if (newPos == 0) {
next = n.getName();
break;
}
newPos--;
}
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"provided node ordering command is invalid: " + command);
}
}
if (next != null) {
if (next.equals("")) {
next = null;
}
parent.orderBefore(item.getName(), next);
changes.add(Modification.onOrder(item.getPath(), next));
if (log.isDebugEnabled()) {
log.debug("Node {} moved '{}'", item.getPath(), command);
}
} else {
throw new IllegalArgumentException(
"provided node ordering command is invalid: " + command);
}
}
protected Node findVersionableAncestor(Node node) throws RepositoryException {
if (isVersionable(node)) {
return node;
}
try {
node = node.getParent();
return findVersionableAncestor(node);
} catch (ItemNotFoundException e) {
// top-level
return null;
}
}
protected boolean isVersionable(Node node) throws RepositoryException {
return node.isNodeType("mix:versionable");
}
protected void checkoutIfNecessary(Node node, List<Modification> changes,
VersioningConfiguration versioningConfiguration) throws RepositoryException {
if (versioningConfiguration.isAutoCheckout()) {
Node versionableNode = findVersionableAncestor(node);
if (versionableNode != null) {
if (!versionableNode.isCheckedOut()) {
versionableNode.checkout();
changes.add(Modification.onCheckout(versionableNode.getPath()));
}
}
}
}
private boolean checkin(final ResourceResolver resolver, final String path) throws RepositoryException {
final Resource rsrc = resolver.getResource(path);
final Node node = (rsrc == null ? null : rsrc.adaptTo(Node.class));
if (node != null) {
if (node.isCheckedOut() && isVersionable(node)) {
node.checkin();
return true;
}
}
return false;
}
private static class ApplyToIterator implements Iterator<Resource> {
private final ResourceResolver resolver;
private final Resource baseResource;
private final String[] paths;
private int pathIndex;
private Resource nextResource;
private Iterator<Resource> resourceIterator = null;
ApplyToIterator(SlingHttpServletRequest request, String[] paths) {
this.resolver = request.getResourceResolver();
this.baseResource = request.getResource();
this.paths = paths;
this.pathIndex = 0;
nextResource = seek();
}
@Override
public boolean hasNext() {
return nextResource != null;
}
@Override
public Resource next() {
if (!hasNext()) {
throw new NoSuchElementException();
}
Resource result = nextResource;
nextResource = seek();
return result;
}
@Override
public void remove() {
throw new UnsupportedOperationException();
}
private Resource seek() {
if (resourceIterator != null) {
if (resourceIterator.hasNext()) {
//return the next resource in the iterator
Resource res = resourceIterator.next();
return res;
}
resourceIterator = null;
}
while (pathIndex < paths.length) {
String path = paths[pathIndex];
pathIndex++;
//SLING-2415 - support wildcard as the last segment of the applyTo path
if (path.endsWith("*")) {
if (path.length() == 1) {
resourceIterator = baseResource.listChildren();
} else if (path.endsWith("/*")) {
path = path.substring(0, path.length() - 2);
if (path.length() == 0) {
resourceIterator = baseResource.listChildren();
} else {
Resource res = resolver.getResource(baseResource, path);
if (res != null) {
resourceIterator = res.listChildren();
}
}
}
if (resourceIterator != null) {
//return the first resource in the iterator
if (resourceIterator.hasNext()) {
Resource res = resourceIterator.next();
return res;
}
resourceIterator = null;
}
} else {
Resource res = resolver.getResource(baseResource, path);
if (res != null) {
return res;
}
}
}
// no more elements in the array
return null;
}
}
}