blob: 72b14ce7fe49c6a75d24baec904f2dc8d1400e21 [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.impl.operations;
import java.util.List;
import java.util.Map;
import javax.jcr.Item;
import javax.jcr.Node;
import javax.jcr.Property;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.nodetype.NodeType;
import javax.servlet.ServletContext;
import org.apache.sling.api.SlingException;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.ModifiableValueMap;
import org.apache.sling.api.resource.PersistenceException;
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.resource.ValueMap;
import org.apache.sling.servlets.post.Modification;
import org.apache.sling.servlets.post.PostResponse;
import org.apache.sling.servlets.post.SlingPostConstants;
import org.apache.sling.servlets.post.VersioningConfiguration;
import org.apache.sling.servlets.post.impl.helper.DateParser;
import org.apache.sling.servlets.post.impl.helper.ReferenceParser;
import org.apache.sling.servlets.post.impl.helper.RequestProperty;
import org.apache.sling.servlets.post.impl.helper.SlingFileUploadHandler;
import org.apache.sling.servlets.post.impl.helper.SlingPropertyValueHandler;
/**
* The <code>ModifyOperation</code> class implements the default operation
* called by the Sling default POST servlet if no operation is requested by the
* client. This operation is able to create and/or modify content.
*/
public class ModifyOperation extends AbstractCreateOperation {
private DateParser dateParser;
/**
* handler that deals with file upload
*/
private final SlingFileUploadHandler uploadHandler;
public ModifyOperation() {
this.dateParser = new DateParser();
this.uploadHandler = new SlingFileUploadHandler();
}
public void setServletContext(final ServletContext servletContext) {
this.uploadHandler.setServletContext(servletContext);
}
public void setDateParser(final DateParser dateParser) {
this.dateParser = dateParser;
}
@Override
protected void doRun(final SlingHttpServletRequest request,
final PostResponse response,
final List<Modification> changes)
throws PersistenceException {
try {
final Map<String, RequestProperty> reqProperties = collectContent(request, response);
final VersioningConfiguration versioningConfiguration = getVersioningConfiguration(request);
// do not change order unless you have a very good reason.
// ensure root of new content
processCreate(request.getResourceResolver(), reqProperties, response, changes, versioningConfiguration);
// write content from existing content (@Move/CopyFrom parameters)
processMoves(request.getResourceResolver(), reqProperties, changes, versioningConfiguration);
processCopies(request.getResourceResolver(), reqProperties, changes, versioningConfiguration);
// cleanup any old content (@Delete parameters)
processDeletes(request.getResourceResolver(), reqProperties, changes, versioningConfiguration);
// write content from form
writeContent(request.getResourceResolver(), reqProperties, changes, versioningConfiguration);
// order content
final Resource newResource = request.getResourceResolver().getResource(response.getPath());
this.jcrSsupport.orderNode(request, newResource, changes);
} catch ( final RepositoryException pe) {
throw new PersistenceException(pe.getMessage(), pe);
}
}
@Override
protected String getResourcePath(SlingHttpServletRequest request) {
// calculate the paths
StringBuilder rootPathBuf = new StringBuilder();
String suffix;
Resource currentResource = request.getResource();
if (ResourceUtil.isSyntheticResource(currentResource)) {
// no resource, treat the missing resource path as suffix
suffix = currentResource.getPath();
} else {
// resource for part of the path, use request suffix
suffix = request.getRequestPathInfo().getSuffix();
if (suffix != null) {
// cut off any selectors/extension from the suffix
int dotPos = suffix.indexOf('.');
if (dotPos > 0) {
suffix = suffix.substring(0, dotPos);
}
}
// and preset the path buffer with the resource path
rootPathBuf.append(currentResource.getPath());
}
// check for extensions or create suffix in the suffix
boolean doGenerateName = false;
if (suffix != null) {
// check whether it is a create request (trailing /)
if (suffix.endsWith(SlingPostConstants.DEFAULT_CREATE_SUFFIX)) {
suffix = suffix.substring(0, suffix.length()
- SlingPostConstants.DEFAULT_CREATE_SUFFIX.length());
doGenerateName = true;
// or with the star suffix /*
} else if (suffix.endsWith(SlingPostConstants.STAR_CREATE_SUFFIX)) {
suffix = suffix.substring(0, suffix.length()
- SlingPostConstants.STAR_CREATE_SUFFIX.length());
doGenerateName = true;
}
// append the remains of the suffix to the path buffer
rootPathBuf.append(suffix);
}
String path = rootPathBuf.toString();
if (doGenerateName) {
try {
path = generateName(request, path);
} catch (RepositoryException re) {
throw new SlingException("Failed to generate name", re);
}
}
return path;
}
/**
* Moves all repository content listed as repository move source in the
* request properties to the locations indicated by the resource properties.
* @param checkedOutNodes
*/
private void processMoves(final ResourceResolver resolver,
Map<String, RequestProperty> reqProperties, List<Modification> changes,
VersioningConfiguration versioningConfiguration)
throws RepositoryException, PersistenceException {
for (RequestProperty property : reqProperties.values()) {
if (property.hasRepositoryMoveSource()) {
processMovesCopiesInternal(property, true, resolver,
reqProperties, changes, versioningConfiguration);
}
}
}
/**
* Copies all repository content listed as repository copy source in the
* request properties to the locations indicated by the resource properties.
* @param checkedOutNodes
*/
private void processCopies(final ResourceResolver resolver,
Map<String, RequestProperty> reqProperties, List<Modification> changes,
VersioningConfiguration versioningConfiguration)
throws RepositoryException, PersistenceException {
for (RequestProperty property : reqProperties.values()) {
if (property.hasRepositoryCopySource()) {
processMovesCopiesInternal(property, false, resolver,
reqProperties, changes, versioningConfiguration);
}
}
}
/**
* Internal implementation of the
* {@link #processCopies(Session, Map, HtmlResponse)} and
* {@link #processMoves(Session, Map, HtmlResponse)} methods taking into
* account whether the source is actually a property or a node.
* <p>
* Any intermediary nodes to the destination as indicated by the
* <code>property</code> path are created using the
* <code>reqProperties</code> as indications for required node types.
*
* @param property The {@link RequestProperty} identifying the source
* content of the operation.
* @param isMove <code>true</code> if the source item is to be moved.
* Otherwise the source item is just copied.
* @param session The repository session to use to access the content
* @param reqProperties All accepted request properties. This is used to
* create intermediary nodes along the property path.
* @param response The <code>HtmlResponse</code> into which successfull
* copies and moves as well as intermediary node creations are
* recorded.
* @throws RepositoryException May be thrown if an error occurrs.
*/
private void processMovesCopiesInternal(
RequestProperty property,
boolean isMove, final ResourceResolver resolver,
Map<String, RequestProperty> reqProperties, List<Modification> changes,
VersioningConfiguration versioningConfiguration)
throws RepositoryException, PersistenceException {
final Session session = resolver.adaptTo(Session.class);
String propPath = property.getPath();
String source = property.getRepositorySource();
// only continue here, if the source really exists
if (session.itemExists(source)) {
// if the destination item already exists, remove it
// first, otherwise ensure the parent location
if (session.itemExists(propPath)) {
Node parent = session.getItem(propPath).getParent();
checkoutIfNecessary(parent, changes, versioningConfiguration);
session.getItem(propPath).remove();
changes.add(Modification.onDeleted(propPath));
} else {
Resource parent = deepGetOrCreateNode(resolver, property.getParentPath(),
reqProperties, changes, versioningConfiguration);
final Node node = parent.adaptTo(Node.class);
if ( node != null ) {
checkoutIfNecessary(node, changes, versioningConfiguration);
}
}
// move through the session and record operation
Item sourceItem = session.getItem(source);
if (sourceItem.isNode()) {
// node move/copy through session
if (isMove) {
checkoutIfNecessary(sourceItem.getParent(), changes, versioningConfiguration);
session.move(source, propPath);
} else {
Node sourceNode = (Node) sourceItem;
Node destParent = (Node) session.getItem(property.getParentPath());
checkoutIfNecessary(destParent, changes, versioningConfiguration);
CopyOperation.copy(sourceNode, destParent,
property.getName());
}
} else {
// property move manually
Property sourceProperty = (Property) sourceItem;
// create destination property
Node destParent = (Node) session.getItem(property.getParentPath());
checkoutIfNecessary(destParent, changes, versioningConfiguration);
CopyOperation.copy(sourceProperty, destParent, null);
// remove source property (if not just copying)
if (isMove) {
checkoutIfNecessary(sourceProperty.getParent(), changes, versioningConfiguration);
sourceProperty.remove();
}
}
// make sure the property is not deleted even in case for a given
// property both @MoveFrom and @Delete is set
property.setDelete(false);
// record successful move
if (isMove) {
changes.add(Modification.onMoved(source, propPath));
} else {
changes.add(Modification.onCopied(source, propPath));
}
}
}
/**
* Removes all properties listed as {@link RequestProperty#isDelete()} from
* the resource.
*
* @param resolver The <code>ResourceResolver</code> used to access the
* resources to delete the properties.
* @param reqProperties The map of request properties to check for
* properties to be removed.
* @param response The <code>HtmlResponse</code> to be updated with
* information on deleted properties.
* @throws RepositoryException Is thrown if an error occurrs checking or
* removing properties.
*/
private void processDeletes(final ResourceResolver resolver,
final Map<String, RequestProperty> reqProperties,
final List<Modification> changes,
final VersioningConfiguration versioningConfiguration)
throws RepositoryException, PersistenceException {
for (final RequestProperty property : reqProperties.values()) {
if (property.isDelete()) {
final Resource parent = resolver.getResource(property.getParentPath());
if ( parent == null ) {
continue;
}
final Node parentNode = parent.adaptTo(Node.class);
if ( parentNode != null ) {
checkoutIfNecessary(parentNode, changes, versioningConfiguration);
if (property.getName().equals("jcr:mixinTypes")) {
// clear all mixins
for (NodeType mixin : parentNode.getMixinNodeTypes()) {
parentNode.removeMixin(mixin.getName());
}
} else {
if ( parentNode.hasProperty(property.getName())) {
parentNode.getProperty(property.getName()).remove();
} else if ( parentNode.hasNode(property.getName())) {
parentNode.getNode(property.getName()).remove();
}
}
} else {
final ValueMap vm = parent.adaptTo(ModifiableValueMap.class);
if ( vm == null ) {
throw new PersistenceException("Resource '" + parent.getPath() + "' is not modifiable.");
}
vm.remove(property.getName());
}
changes.add(Modification.onDeleted(property.getPath()));
}
}
}
/**
* Writes back the content
*
* @throws RepositoryException if a repository error occurs
* @throws PersistenceException if a persistence error occurs
*/
private void writeContent(final ResourceResolver resolver,
final Map<String, RequestProperty> reqProperties,
final List<Modification> changes,
final VersioningConfiguration versioningConfiguration)
throws RepositoryException, PersistenceException {
final SlingPropertyValueHandler propHandler = new SlingPropertyValueHandler(
dateParser, new ReferenceParser(resolver.adaptTo(Session.class)), changes);
for (final RequestProperty prop : reqProperties.values()) {
if (prop.hasValues()) {
final Resource parent = deepGetOrCreateNode(resolver,
prop.getParentPath(), reqProperties, changes, versioningConfiguration);
final Node parentNode = parent.adaptTo(Node.class);
if ( parentNode != null ) {
checkoutIfNecessary(parentNode, changes, versioningConfiguration);
}
// skip jcr special properties
if (prop.getName().equals("jcr:primaryType")
|| prop.getName().equals("jcr:mixinTypes")) {
continue;
}
if (prop.isFileUpload()) {
uploadHandler.setFile(parent, prop, changes);
} else {
propHandler.setProperty(parent, prop);
}
}
}
}
}