blob: 95f503f528486aa54d2f1a2df4593f169a2b4548 [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.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.regex.Pattern;
import javax.jcr.Node;
import javax.jcr.RepositoryException;
import javax.servlet.ServletException;
import org.apache.jackrabbit.JcrConstants;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.request.RequestParameter;
import org.apache.sling.api.request.RequestParameterMap;
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.servlets.post.Modification;
import org.apache.sling.servlets.post.NodeNameGenerator;
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.Chunk;
import org.apache.sling.servlets.post.impl.helper.DefaultNodeNameGenerator;
import org.apache.sling.servlets.post.impl.helper.RequestProperty;
abstract class AbstractCreateOperation extends AbstractPostOperation {
private final Random randomCollisionIndex = new Random();
/**
* The default node name generator
*/
private NodeNameGenerator defaultNodeNameGenerator;
/**
* utility class for generating node names
*/
private NodeNameGenerator[] extraNodeNameGenerators;
/**
* regular expression for parameters to ignore
*/
private Pattern ignoredParameterNamePattern;
protected AbstractCreateOperation() {
this.defaultNodeNameGenerator = new DefaultNodeNameGenerator();
this.ignoredParameterNamePattern = null;
}
public void setDefaultNodeNameGenerator(
NodeNameGenerator defaultNodeNameGenerator) {
this.defaultNodeNameGenerator = defaultNodeNameGenerator;
}
public void setExtraNodeNameGenerators(
NodeNameGenerator[] extraNodeNameGenerators) {
this.extraNodeNameGenerators = extraNodeNameGenerators;
}
public void setIgnoredParameterNamePattern(
final Pattern ignoredParameterNamePattern) {
this.ignoredParameterNamePattern = ignoredParameterNamePattern;
}
/**
* Returns true if any of the request parameters starts with
* {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT <code>./</code>}.
* In this case only parameters starting with either of the prefixes
* {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_CURRENT <code>./</code>},
* {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT <code>../</code>}
* and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE <code>/</code>} 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.
*/
private 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 <code>./</code>},
* {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT <code>../</code>}
* and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE <code>/</code>}.
*
* @param name The name
* @return {@code true} if the name has a prefix
*/
private 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);
}
/**
* Create resource(s) according to current request
*
* @throws PersistenceException if a resource error occurs
*/
protected void processCreate(final ResourceResolver resolver,
final Map<String, RequestProperty> reqProperties,
final PostResponse response,
final List<Modification> changes,
final VersioningConfiguration versioningConfiguration)
throws PersistenceException, RepositoryException {
final String path = response.getPath();
final Resource resource = resolver.getResource(path);
if ( resource == null || ResourceUtil.isSyntheticResource(resource) ) {
deepGetOrCreateResource(resolver, path, reqProperties, changes, versioningConfiguration);
response.setCreateRequest(true);
} else {
updateNodeType(resolver, path, reqProperties, changes, versioningConfiguration);
updateMixins(resolver, path, reqProperties, changes, versioningConfiguration);
}
}
protected void updateNodeType(final ResourceResolver resolver,
final String path,
final Map<String, RequestProperty> reqProperties,
final List<Modification> changes,
final VersioningConfiguration versioningConfiguration)
throws PersistenceException, RepositoryException {
final String nodeType = getPrimaryType(reqProperties, path);
if (nodeType != null) {
final Resource rsrc = resolver.getResource(path);
final ModifiableValueMap mvm = rsrc.adaptTo(ModifiableValueMap.class);
if ( mvm != null ) {
final Node node = rsrc.adaptTo(Node.class);
final boolean wasVersionable = (node == null ? false : this.jcrSsupport.isVersionable(rsrc));
if ( node != null ) {
this.jcrSsupport.checkoutIfNecessary(rsrc, changes, versioningConfiguration);
node.setPrimaryType(nodeType);
} else {
mvm.put(JcrConstants.JCR_PRIMARYTYPE, nodeType);
}
if ( node != null ) {
// this is a bit of a cheat; there isn't a formal checkout, but assigning
// the mix:versionable mixin does an implicit checkout
if (!wasVersionable &&
versioningConfiguration.isCheckinOnNewVersionableNode() &&
this.jcrSsupport.isVersionable(rsrc)) {
changes.add(Modification.onCheckout(path));
}
}
}
}
}
protected void updateMixins(final ResourceResolver resolver,
final String path,
final Map<String, RequestProperty> reqProperties,
final List<Modification> changes,
final VersioningConfiguration versioningConfiguration)
throws PersistenceException {
final String[] mixins = getMixinTypes(reqProperties, path);
if (mixins != null) {
final Resource rsrc = resolver.getResource(path);
final ModifiableValueMap mvm = rsrc.adaptTo(ModifiableValueMap.class);
if ( mvm != null ) {
this.jcrSsupport.checkoutIfNecessary(rsrc, changes, versioningConfiguration);
mvm.put(JcrConstants.JCR_MIXINTYPES, mixins);
for(final String mixin : mixins) {
// this is a bit of a cheat; there isn't a formal checkout, but assigning
// the mix:versionable mixin does an implicit checkout
if (mixin.equals(JcrConstants.MIX_VERSIONABLE) &&
versioningConfiguration.isCheckinOnNewVersionableNode()) {
changes.add(Modification.onCheckout(path));
}
}
}
}
}
/**
* Collects the properties that form the content to be written back to the
* resource tree.
*
* @throws RepositoryException if a repository error occurs
* @throws ServletException if an internal error occurs
*/
protected Map<String, RequestProperty> collectContent(
final SlingHttpServletRequest request,
final PostResponse response) {
final boolean requireItemPrefix = requireItemPathPrefix(request);
// walk the request parameters and collect the properties
final LinkedHashMap<String, RequestProperty> reqProperties = new LinkedHashMap<>();
for (final Map.Entry<String, RequestParameter[]> e : request.getRequestParameterMap().entrySet()) {
final String paramName = e.getKey();
if (ignoreParameter(paramName)) {
continue;
}
// skip parameters that do not start with the save prefix
if (requireItemPrefix && !hasItemPathPrefix(paramName)) {
continue;
}
// ensure the paramName is an absolute property name
final String propPath = toPropertyPath(paramName, response);
// @TypeHint example
// <input type="text" name="./age" />
// <input type="hidden" name="./age@TypeHint" value="long" />
// causes the setProperty using the 'long' property type
if (propPath.endsWith(SlingPostConstants.TYPE_HINT_SUFFIX)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.TYPE_HINT_SUFFIX);
final RequestParameter[] rp = e.getValue();
if (rp.length > 0) {
prop.setTypeHintValue(rp[0].getString());
}
continue;
}
// @DefaultValue
if (propPath.endsWith(SlingPostConstants.DEFAULT_VALUE_SUFFIX)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.DEFAULT_VALUE_SUFFIX);
prop.setDefaultValues(e.getValue());
continue;
}
// SLING-130: VALUE_FROM_SUFFIX means take the value of this
// property from a different field
// @ValueFrom example:
// <input name="./Text@ValueFrom" type="hidden" value="fulltext" />
// causes the JCR Text property to be set to the value of the
// fulltext form field.
if (propPath.endsWith(SlingPostConstants.VALUE_FROM_SUFFIX)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.VALUE_FROM_SUFFIX);
// @ValueFrom params must have exactly one value, else ignored
if (e.getValue().length == 1) {
final String refName = e.getValue()[0].getString();
final RequestParameter[] refValues = request.getRequestParameters(refName);
if (refValues != null) {
prop.setValues(refValues);
}
}
continue;
}
// SLING-458: Allow Removal of properties prior to update
// @Delete example:
// <input name="./Text@Delete" type="hidden" />
// causes the JCR Text property to be deleted before update
if (propPath.endsWith(SlingPostConstants.SUFFIX_DELETE)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath, SlingPostConstants.SUFFIX_DELETE);
prop.setDelete(true);
continue;
}
// SLING-455: @MoveFrom means moving content to another location
// @MoveFrom example:
// <input name="./Text@MoveFrom" type="hidden" value="/tmp/path" />
// causes the JCR Text property to be set by moving the /tmp/path
// property to Text.
if (propPath.endsWith(SlingPostConstants.SUFFIX_MOVE_FROM)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.SUFFIX_MOVE_FROM);
// @MoveFrom params must have exactly one value, else ignored
if (e.getValue().length == 1) {
final String sourcePath = e.getValue()[0].getString();
prop.setRepositorySource(sourcePath, true);
}
continue;
}
// SLING-455: @CopyFrom means moving content to another location
// @CopyFrom example:
// <input name="./Text@CopyFrom" type="hidden" value="/tmp/path" />
// causes the JCR Text property to be set by copying the /tmp/path
// property to Text.
if (propPath.endsWith(SlingPostConstants.SUFFIX_COPY_FROM)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.SUFFIX_COPY_FROM);
// @MoveFrom params must have exactly one value, else ignored
if (e.getValue().length == 1) {
final String sourcePath = e.getValue()[0].getString();
prop.setRepositorySource(sourcePath, false);
}
continue;
}
// SLING-1412: @IgnoreBlanks
// @Ignore example:
// <input name="./Text" type="hidden" value="test" />
// <input name="./Text" type="hidden" value="" />
// <input name="./Text@String[]" type="hidden" value="true" />
// <input name="./Text@IgnoreBlanks" type="hidden" value="true" />
// causes the JCR Text property to be set by copying the /tmp/path
// property to Text.
if (propPath.endsWith(SlingPostConstants.SUFFIX_IGNORE_BLANKS)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.SUFFIX_IGNORE_BLANKS);
if (e.getValue().length == 1) {
prop.setIgnoreBlanks(true);
}
continue;
}
if (propPath.endsWith(SlingPostConstants.SUFFIX_USE_DEFAULT_WHEN_MISSING)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.SUFFIX_USE_DEFAULT_WHEN_MISSING);
if (e.getValue().length == 1) {
prop.setUseDefaultWhenMissing(true);
}
continue;
}
// @Patch
// Example:
// <input name="tags@TypeHint" value="String[]" type="hidden" />
// <input name="tags@Patch" value="true" type="hidden" />
// <input name="tags" value="+apple" type="hidden" />
// <input name="tags" value="-orange" type="hidden" />
if (propPath.endsWith(SlingPostConstants.SUFFIX_PATCH)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.SUFFIX_PATCH);
prop.setPatch(true);
continue;
}
if (propPath.endsWith(SlingPostConstants.SUFFIX_OFFSET)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.SUFFIX_OFFSET);
if (e.getValue().length == 1) {
Chunk chunk = prop.getChunk();
if(chunk == null){
chunk = new Chunk();
}
chunk.setOffsetValue(Long.parseLong(e.getValue()[0].toString()));
prop.setChunk(chunk);
}
continue;
}
if (propPath.endsWith(SlingPostConstants.SUFFIX_COMPLETED)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.SUFFIX_COMPLETED);
if (e.getValue().length == 1) {
Chunk chunk = prop.getChunk();
if(chunk == null){
chunk = new Chunk();
}
chunk.setCompleted(Boolean.parseBoolean((e.getValue()[0].toString())));
prop.setChunk(chunk);
}
continue;
}
if (propPath.endsWith(SlingPostConstants.SUFFIX_LENGTH)) {
final RequestProperty prop = getOrCreateRequestProperty(
reqProperties, propPath,
SlingPostConstants.SUFFIX_LENGTH);
if (e.getValue().length == 1) {
Chunk chunk = prop.getChunk();
if(chunk == null){
chunk = new Chunk();
}
chunk.setLength(Long.parseLong(e.getValue()[0].toString()));
prop.setChunk(chunk);
}
continue;
}
// plain property, create from values
final RequestProperty prop = getOrCreateRequestProperty(reqProperties,
propPath, null);
prop.setValues(e.getValue());
}
return reqProperties;
}
/**
* Returns <code>true</code> if the parameter of the given name should be
* ignored.
*/
private boolean ignoreParameter(final String paramName) {
// do not store parameters with names starting with sling:post
if (paramName.startsWith(SlingPostConstants.RP_PREFIX)) {
return true;
}
// SLING-298: skip form encoding parameter
if (paramName.equals("_charset_")) {
return true;
}
// SLING-2120: ignore parameter match ignoredParameterNamePattern
if (this.ignoredParameterNamePattern != null
&& this.ignoredParameterNamePattern.matcher(paramName).matches()) {
return true;
}
return false;
}
/**
* Returns the <code>paramName</code> as an absolute (unnormalized) property
* path by prepending the response path (<code>response.getPath</code>) to
* the parameter name if not already absolute.
*/
private String toPropertyPath(String paramName, PostResponse response) {
if (!paramName.startsWith("/")) {
paramName = ResourceUtil.normalize(response.getPath() + '/' + paramName);
}
return paramName;
}
/**
* Returns the request property for the given property path. If such a
* request property does not exist yet it is created and stored in the
* <code>props</code>.
*
* @param props The map of already seen request properties.
* @param paramName The absolute path of the property including the
* <code>suffix</code> to be looked up.
* @param suffix The (optional) suffix to remove from the
* <code>paramName</code> before looking it up.
* @return The {@link RequestProperty} for the <code>paramName</code>.
*/
private RequestProperty getOrCreateRequestProperty(
Map<String, RequestProperty> props, String paramName, String suffix) {
if (suffix != null && paramName.endsWith(suffix)) {
paramName = paramName.substring(0, paramName.length()
- suffix.length());
}
RequestProperty prop = props.get(paramName);
if (prop == null) {
prop = new RequestProperty(paramName);
props.put(paramName, prop);
}
return prop;
}
/**
* Deep gets or creates a resource, parent-padding with default resources. If
* the path is empty, the given parent resource is returned.
*
* @param path path to resources that needs to be deep-created
* @return Resource at path
* @throws PersistenceException if an error occurs
* @throws IllegalArgumentException if the path is relative and parent is
* <code>null</code>
*/
protected Resource deepGetOrCreateResource(final ResourceResolver resolver,
final String path,
final Map<String, RequestProperty> reqProperties,
final List<Modification> changes,
final VersioningConfiguration versioningConfiguration)
throws PersistenceException, RepositoryException {
if (log.isDebugEnabled()) {
log.debug("Deep-creating resource '{}'", path);
}
if (path == null || !path.startsWith("/")) {
throw new IllegalArgumentException("path must be an absolute path.");
}
// get the starting resource
String startingResourcePath = path;
Resource startingResource = null;
while (startingResource == null) {
if (startingResourcePath.equals("/")) {
startingResource = resolver.getResource("/");
if (startingResource == null){
throw new PersistenceException("Access denied for root resource, resource can't be created: " + path);
}
} else {
final Resource r = resolver.getResource(startingResourcePath);
if ( r != null && !ResourceUtil.isSyntheticResource(r)) {
startingResource = resolver.getResource(startingResourcePath);
updateNodeType(resolver, startingResourcePath, reqProperties, changes, versioningConfiguration);
updateMixins(resolver, startingResourcePath, reqProperties, changes, versioningConfiguration);
} else {
int pos = startingResourcePath.lastIndexOf('/');
if (pos > 0) {
startingResourcePath = startingResourcePath.substring(0, pos);
} else {
startingResourcePath = "/";
}
}
}
}
// is the searched resource already existing?
if (startingResourcePath.length() == path.length()) {
return startingResource;
}
// create nodes
int from = (startingResourcePath.length() == 1
? 1
: startingResourcePath.length() + 1);
Resource resource = startingResource;
while (from > 0) {
final int to = path.indexOf('/', from);
final String name = to < 0 ? path.substring(from) : path.substring(
from, to);
// although the resource should not exist (according to the first test
// above)
// we do a sanety check.
final Resource child = resource.getChild(name);
if (child != null && !ResourceUtil.isSyntheticResource(child)) {
resource = child;
updateNodeType(resolver, resource.getPath(), reqProperties, changes, versioningConfiguration);
updateMixins(resolver, resource.getPath(), reqProperties, changes, versioningConfiguration);
} else {
final String tmpPath = to < 0 ? path : path.substring(0, to);
// check for node type
final String nodeType = getPrimaryType(reqProperties, tmpPath);
this.jcrSsupport.checkoutIfNecessary(resource, changes, versioningConfiguration);
try {
final Map<String, Object> props = new HashMap<>();
if (nodeType != null) {
props.put("jcr:primaryType", nodeType);
}
// check for mixin types
final String[] mixinTypes = getMixinTypes(reqProperties,
tmpPath);
if (mixinTypes != null) {
props.put("jcr:mixinTypes", mixinTypes);
}
resource = resolver.create(resource, name, props);
} catch (final PersistenceException e) {
log.error("Unable to create resource named " + name + " in " + resource.getPath());
throw e;
}
changes.add(Modification.onCreated(resource.getPath()));
}
from = to + 1;
}
return resource;
}
/**
* Checks the collected content for a jcr:primaryType property at the
* specified path.
*
* @param path path to check
* @return the primary type or <code>null</code>
*/
private String getPrimaryType(Map<String, RequestProperty> reqProperties,
String path) {
RequestProperty prop = reqProperties.get(path + "/jcr:primaryType");
return prop == null ? null : prop.getStringValues()[0];
}
/**
* Checks the collected content for a jcr:mixinTypes property at the
* specified path.
*
* @param path path to check
* @return the mixin types or <code>null</code>
*/
private String[] getMixinTypes(Map<String, RequestProperty> reqProperties,
String path) {
RequestProperty prop = reqProperties.get(path + "/jcr:mixinTypes");
return (prop == null) || !prop.hasValues() ? null : prop.getStringValues();
}
protected String generateName(SlingHttpServletRequest request, String basePath)
throws RepositoryException {
// SLING-1091: If a :name parameter is supplied, the (first) value of this parameter is used unmodified as the name
// for the new node. If the name is illegally formed with respect to JCR name requirements, an exception will be
// thrown when trying to create the node. The assumption with the :name parameter is, that the caller knows what
// he (or she) is supplying and should get the exact result if possible.
RequestParameterMap parameters = request.getRequestParameterMap();
RequestParameter specialParam = parameters.getValue(SlingPostConstants.RP_NODE_NAME);
if ( specialParam != null ) {
if ( specialParam.getString() != null && specialParam.getString().length() > 0 ) {
// If the path ends with a *, create a node under its parent, with
// a generated node name
basePath = basePath += "/" + specialParam.getString();
// if the resulting path already exists then report an error
if (request.getResourceResolver().getResource(basePath) != null) {
throw new RepositoryException(
"Collision in node names for path=" + basePath);
}
return basePath;
}
}
// no :name value was supplied, so generate a name
boolean requirePrefix = requireItemPathPrefix(request);
String generatedName = null;
if (extraNodeNameGenerators != null) {
for (NodeNameGenerator generator : extraNodeNameGenerators) {
generatedName = generator.getNodeName(request, basePath, requirePrefix, defaultNodeNameGenerator);
if (generatedName != null) {
break;
}
}
}
if (generatedName == null) {
generatedName = defaultNodeNameGenerator.getNodeName(request, basePath, requirePrefix, defaultNodeNameGenerator);
}
// If the path ends with a *, create a node under its parent, with
// a generated node name
basePath += "/" + generatedName;
basePath = ensureUniquePath(request, basePath);
return basePath;
}
/** Generate a unique path in case the node name generator didn't */
private String ensureUniquePath(SlingHttpServletRequest request, String basePath) throws RepositoryException {
// if resulting path exists, add a suffix until it's not the case
// anymore
final ResourceResolver resolver = request.getResourceResolver();
// if resulting path exists, add a random suffix until it's not the case
// anymore
final int MAX_TRIES = 1000;
if (resolver.getResource(basePath) != null ) {
for(int i=0; i < MAX_TRIES; i++) {
final int uniqueIndex = Math.abs(randomCollisionIndex.nextInt());
String newPath = basePath + "_" + uniqueIndex;
if (resolver.getResource(newPath) == null) {
basePath = basePath + "_" + uniqueIndex;
basePath = newPath;
break;
}
}
// Give up after MAX_TRIES
if (resolver.getResource(basePath) != null ) {
throw new RepositoryException(
"Collision in generated node names under " + basePath + ", generated path " + basePath + " already exists");
}
}
return basePath;
}
}