Move Sling to new TLP location
git-svn-id: https://svn.eu.apache.org/repos/asf/sling/tags/org.apache.sling.servlets.post-2.0.2-incubator@785979 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/NOTICE b/NOTICE
new file mode 100644
index 0000000..d26b355
--- /dev/null
+++ b/NOTICE
@@ -0,0 +1,8 @@
+Apache Sling Default Post Servlets
+Copyright 2008 The Apache Software Foundation
+
+Apache Sling is based on source code originally developed
+by Day Software (http://www.day.com/).
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..c9155b5
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,37 @@
+Apache Sling Default Post Servlets
+
+Provides default POST servlets.
+
+Disclaimer
+==========
+Apache Sling is an effort undergoing incubation at The Apache Software Foundation (ASF),
+sponsored by the Apache Jackrabbit PMC. Incubation is required of all newly accepted
+projects until a further review indicates that the infrastructure, communications,
+and decision making process have stabilized in a manner consistent with other
+successful ASF projects. While incubation status is not necessarily a reflection of
+the completeness or stability of the code, it does indicate that the project has yet
+to be fully endorsed by the ASF.
+
+Getting Started
+===============
+
+This component uses a Maven 2 (http://maven.apache.org/) build
+environment. It requires a Java 5 JDK (or higher) and Maven (http://maven.apache.org/)
+2.0.7 or later. We recommend to use the latest Maven version.
+
+If you have Maven 2 installed, you can compile and
+package the jar using the following command:
+
+ mvn package
+
+See the Maven 2 documentation for other build features.
+
+The latest source code for this component is available in the
+Subversion (http://subversion.tigris.org/) source repository of
+the Apache Software Foundation. If you have Subversion installed,
+you can checkout the latest source using the following command:
+
+ svn checkout http://svn.apache.org/repos/asf/incubator/sling/trunk/servlets/post
+
+See the Subversion documentation for other source control features.
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..1c59ee5
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>sling</artifactId>
+ <version>3-incubator</version>
+ <relativePath>../../parent/pom.xml</relativePath>
+ </parent>
+
+ <artifactId>org.apache.sling.servlets.post</artifactId>
+ <packaging>bundle</packaging>
+ <version>2.0.2-incubator</version>
+
+ <name>Sling - Default POST Servlets</name>
+ <description>
+ Provides default POST servlets.
+ </description>
+
+ <scm>
+ <connection>scm:svn:http://svn.apache.org/repos/asf/incubator/sling/tags/org.apache.sling.servlets.post-2.0.2-incubator</connection>
+ <developerConnection>scm:svn:https://svn.apache.org/repos/asf/incubator/sling/tags/org.apache.sling.servlets.post-2.0.2-incubator</developerConnection>
+ <url>http://svn.apache.org/viewvc/incubator/sling/tags/org.apache.sling.servlets.post-2.0.2-incubator</url>
+ </scm>
+
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-scr-plugin</artifactId>
+ </plugin>
+
+ <plugin>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>maven-bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ <configuration>
+ <instructions>
+ <Export-Package>
+ org.apache.sling.servlets.post;version=${pom.version}
+ </Export-Package>
+ <Private-Package>
+ org.apache.sling.servlets.post.impl.*
+ </Private-Package>
+ <Sling-Bundle-Resources>
+ /system/sling.js
+ </Sling-Bundle-Resources>
+ </instructions>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ <reporting>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-javadoc-plugin</artifactId>
+ <configuration>
+ <!-- No javadocs -->
+ <excludePackageNames>
+ org.apache.sling.servlets.post.impl
+ </excludePackageNames>
+ </configuration>
+ </plugin>
+ </plugins>
+ </reporting>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.core</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.felix</groupId>
+ <artifactId>org.osgi.compendium</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>javax.servlet</groupId>
+ <artifactId>servlet-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.api</artifactId>
+ <version>2.0.2-incubator</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.jcr.api</artifactId>
+ <version>2.0.2-incubator</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.jcr.resource</artifactId>
+ <version>2.0.2-incubator</version>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.jackrabbit</groupId>
+ <artifactId>jackrabbit-jcr-commons</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.json</artifactId>
+ <version>2.0.2-incubator</version>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-simple</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.testing</artifactId>
+ <version>2.0.2-incubator</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+
+ </dependencies>
+
+</project>
+
diff --git a/src/main/java/org/apache/sling/servlets/post/AbstractSlingPostOperation.java b/src/main/java/org/apache/sling/servlets/post/AbstractSlingPostOperation.java
new file mode 100644
index 0000000..ddb22eb
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/AbstractSlingPostOperation.java
@@ -0,0 +1,365 @@
+/*
+ * 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.Enumeration;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+
+import javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+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.servlets.HtmlResponse;
+import org.apache.sling.api.wrappers.SlingRequestPaths;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Holds various states and encapsulates methods that are needed to handle a
+ * post request.
+ */
+public abstract class AbstractSlingPostOperation implements SlingPostOperation {
+
+ /**
+ * default log
+ */
+ private 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, HtmlResponse)}
+ * 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>HtmlResponse</code> to record execution
+ * progress.
+ */
+ public final void run(SlingHttpServletRequest request, HtmlResponse response) {
+
+ // calculate the paths
+ String path = getItemPath(request);
+ response.setPath(path);
+
+ // location
+ response.setLocation(externalizePath(request, path));
+
+ // parent location
+ path = ResourceUtil.getParent(path);
+ response.setParentLocation(externalizePath(request, path));
+
+ Session session = request.getResourceResolver().adaptTo(Session.class);
+
+ try {
+
+ doRun(request, response);
+
+ if (session.hasPendingChanges()) {
+ session.save();
+ }
+
+ } catch (Exception e) {
+
+ log.error("Exception during response processing.", e);
+ response.setError(e);
+
+ } finally {
+ try {
+ if (session.hasPendingChanges()) {
+ session.refresh(false);
+ }
+ } catch (RepositoryException e) {
+ log.warn("RepositoryException in finally block: {}",
+ e.getMessage(), e);
+ }
+ }
+
+ }
+
+ /**
+ * 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.
+ */
+ protected String getItemPath(SlingHttpServletRequest request) {
+ return request.getResource().getPath();
+ }
+
+ protected abstract void doRun(SlingHttpServletRequest request,
+ HtmlResponse response) throws RepositoryException;
+
+ /**
+ * 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) {
+
+ 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 path the path to externalize
+ * @return the url
+ */
+ protected final String externalizePath(SlingHttpServletRequest request,
+ String path) {
+ StringBuffer ret = new StringBuffer();
+ 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 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 <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.
+ */
+ 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 <code>./</code>},
+ * {@link SlingPostConstants#ITEM_PREFIX_RELATIVE_PARENT <code>../</code>}
+ * and {@link SlingPostConstants#ITEM_PREFIX_ABSOLUTE <code>/</code>}.
+ */
+ 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: <xmp> | 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 </xmp>
+ *
+ * @param item node to order
+ * @throws RepositoryException if an error occurs
+ */
+ protected void orderNode(SlingHttpServletRequest request, Item item)
+ 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);
+ if (log.isDebugEnabled()) {
+ log.debug("Node {} moved '{}'", item.getPath(), command);
+ }
+ } else {
+ throw new IllegalArgumentException(
+ "provided node ordering command is invalid: " + command);
+ }
+ }
+
+ 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;
+
+ ApplyToIterator(SlingHttpServletRequest request, String[] paths) {
+ this.resolver = request.getResourceResolver();
+ this.baseResource = request.getResource();
+ this.paths = paths;
+ this.pathIndex = 0;
+
+ nextResource = seek();
+ }
+
+ public boolean hasNext() {
+ return nextResource != null;
+ }
+
+ public Resource next() {
+ if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+
+ Resource result = nextResource;
+ nextResource = seek();
+
+ return result;
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+
+ private Resource seek() {
+ while (pathIndex < paths.length) {
+ String path = paths[pathIndex];
+ pathIndex++;
+
+ Resource res = resolver.getResource(baseResource, path);
+ if (res != null) {
+ return res;
+ }
+ }
+
+ // no more elements in the array
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/servlets/post/SlingPostConstants.java b/src/main/java/org/apache/sling/servlets/post/SlingPostConstants.java
new file mode 100644
index 0000000..3b123f2
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/SlingPostConstants.java
@@ -0,0 +1,303 @@
+/*
+ * 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;
+
+/**
+ * The <code>SlingPostConstants</code> interface provides constants for well
+ * known parameters of the core SlingPostServlet. Extensions of the servlet
+ * through implementations of the {@link SlingPostOperation} interface may
+ * extend this constants.
+ */
+public interface SlingPostConstants {
+
+ /**
+ * Prefix for parameter names which control this POST (RP_ stands for
+ * "request param") (value is ":"). This prefix must be used on all request
+ * parameters which have significance to POST request processing. Such
+ * parameters will not be used to denote properties to be written to the
+ * repository.
+ */
+ public static final String RP_PREFIX = ":";
+
+ /**
+ * The name of the parameter containing the operation to execute (value is
+ * ":operation"). If this parameter is missing or empty, the request is
+ * assumed to be a request to create new content or to modify existing
+ * content.
+ */
+ public static final String RP_OPERATION = RP_PREFIX + "operation";
+
+ /**
+ * The suffix to the resource path used to indicate to automatically
+ * generate the name of the new item to create during a content creation
+ * request (value is "/").
+ */
+ public static final String DEFAULT_CREATE_SUFFIX = "/";
+
+ /**
+ * An alternative suffix to the resource path used to indicate to
+ * automatically generate the name of the new item to create during a
+ * content creation request (value is "/*").
+ */
+ public static final String STAR_CREATE_SUFFIX = "/*";
+
+ /**
+ * Name of the predefined delete operation (value is "delete").
+ * <p>
+ * The delete operation requires no further request parameters and just
+ * deletes the content addressed by the request.
+ * <p>
+ * If the {@link #RP_APPLY_TO} parameter is set the resources listed in that
+ * parameter are deleted instead of the request resource.
+ */
+ public static final String OPERATION_DELETE = "delete";
+
+ /**
+ * Name of the predefined copy operation (value is "copy").
+ * <p>
+ * The copy operation requires the {@link #RP_DEST} request parameter
+ * denoting the path to copy the content to. In addition the
+ * {@link #RP_ORDER} parameter may be defined to specificy to relative node
+ * order of the destination node. Finally the {@link #RP_REPLACE} parameter
+ * may be set to indicate whether an existing item at the destination should
+ * be replaced or not.
+ * <p>
+ * If the {@link #RP_APPLY_TO} parameter is set the resources listed in that
+ * parameter are copied instead of the request resource.
+ */
+ public static final String OPERATION_COPY = "copy";
+
+ /**
+ * Name of the predefined move operation (value is "move")
+ * <p>
+ * The move operation requires the {@link #RP_DEST} request parameter
+ * denoting the path to move the content to. In addition the
+ * {@link #RP_ORDER} parameter may be defined to specificy to relative node
+ * order of the destination node. Finally the {@link #RP_REPLACE} parameter
+ * may be set to indicate whether an existing item at the destination should
+ * be replaced or not.
+ * <p>
+ * If the {@link #RP_APPLY_TO} parameter is set the resources listed in that
+ * parameter are moved instead of the request resource.
+ */
+ public static final String OPERATION_MOVE = "move";
+
+ /**
+ * Name of the request parameter used to indicate the resource to apply the
+ * operation to (value is ":applyTo").
+ * <p>
+ * This property is used by certain opertaions - namely
+ * {@link #OPERATION_COPY}, {@link #OPERATION_DELETE} and
+ * {@link #OPERATION_MOVE} - to apply the operation to multiple resources
+ * instead of the request resource.
+ */
+ public static final String RP_APPLY_TO = RP_PREFIX + "applyTo";
+
+ /**
+ * Name of the request parameter used to indicate the destination for the
+ * copy and move operations (value is ":dest"). This request parameter is
+ * required by the copy and move operations.
+ */
+ public static final String RP_DEST = RP_PREFIX + "dest";
+
+ /**
+ * Name of the request parameter indicating whether the destination for a
+ * copy or move operation is to be replaced if existing (value is
+ * ":replace"). Copy or move is only possible if the destination exists if
+ * the replace parameter is set to the case-insignificant value true.
+ */
+ public static final String RP_REPLACE = RP_PREFIX + "replace";
+
+ /**
+ * Optional request parameter indicating the order of newly created nodes in
+ * creation, copy and move operation requests (value is ":order").
+ * <p>
+ * The value of this parameter may be {@link #ORDER_FIRST},
+ * {@link #ORDER_BEFORE}, {@link #ORDER_AFTER}, {@link #ORDER_LAST} or a
+ * numberic value indicating the absolute position in the child list of the
+ * parent node.
+ */
+ public static final String RP_ORDER = RP_PREFIX + "order";
+
+ /**
+ * Possible value of the {@link #RP_ORDER} parameter indicating that the
+ * node by moved to the first position amongst its sibblings (value is
+ * "first").
+ */
+ public static final String ORDER_FIRST = "first";
+
+ /**
+ * Possible value of the {@link #RP_ORDER} parameter indicating that the
+ * node by moved immediately before the sibbling whose name is contained in
+ * the {@link #RP_ORDER} parameter (value is "before ").
+ */
+ public static final String ORDER_BEFORE = "before ";
+
+ /**
+ * Possible value of the {@link #RP_ORDER} parameter indicating that the
+ * node by moved immediately after the sibbling whose name is contained in
+ * the {@link #RP_ORDER} parameter (value is "after ").
+ */
+ public static final String ORDER_AFTER = "after ";
+
+ /**
+ * Possible value of the {@link #RP_ORDER} parameter indicating that the
+ * node by moved to the last position amongst its sibblings (value is
+ * "last").
+ */
+ public static final String ORDER_LAST = "last";
+
+ /**
+ * Optional request paramter specifying a node name for a newly created node
+ * (value is ":name").
+ */
+ public static final String RP_NODE_NAME = RP_PREFIX + "name";
+
+ /**
+ * Optional request paramter specifying a node name hint for a newly created
+ * node (value is ":nameHint").
+ */
+ public static final String RP_NODE_NAME_HINT = RP_PREFIX + "nameHint";
+
+ /**
+ * Prefix for properties addressing repository items with an absolute path
+ * (value is "/").
+ *
+ * @see #ITEM_PREFIX_RELATIVE_CURRENT
+ */
+ public static final String ITEM_PREFIX_ABSOLUTE = "/";
+
+ /**
+ * Prefix for properties addressing repository items with a path relative to
+ * the current request item (value is "./").
+ * <p>
+ * When collecting parameters addressing repository items for modification,
+ * the parameters are first scanned to see whether there is a parameter with
+ * this relative path prefix. If such a parameter exists, the modification
+ * operations only assumes parameters whose name is prefixes with this
+ * prefix or the {@link #ITEM_PREFIX_ABSOLUTE} or the
+ * {@link #ITEM_PREFIX_RELATIVE_PARENT} to be parameters addressing
+ * properties to modify. Otherwise, that is if no parameter starts with this
+ * prefix, all parameters not starting with the
+ * {@link #RP_PREFIX command prefix} are considered addressing properties to
+ * modify.
+ */
+ public static final String ITEM_PREFIX_RELATIVE_CURRENT = "./";
+
+ /**
+ * Prefix for properties addressing repository items with a path relative to
+ * the parent of the request item (value is "../").
+ *
+ * @see #ITEM_PREFIX_RELATIVE_CURRENT
+ */
+ public static final String ITEM_PREFIX_RELATIVE_PARENT = "../";
+
+ /**
+ * Optional request parameter: redirect to the specified URL after POST
+ */
+ public static final String RP_REDIRECT_TO = RP_PREFIX + "redirect";
+
+ /**
+ * Optional request parameter: define how the response is sent back to the
+ * client. Supported values for this property are
+ * {@link #STATUS_VALUE_BROWSER} and {@link #STATUS_VALUE_STANDARD}. The
+ * default is to assume {@link #STATUS_VALUE_STANDARD} if the parameter is
+ * not set or set to any other value.
+ */
+ public static final String RP_STATUS = RP_PREFIX + "status";
+
+ /**
+ * The supported value for the {@link #RP_STATUS} request parameter
+ * requesting to report success or failure of request processing using
+ * standard HTTP status codes. This value is assumed as the default value
+ * for the {@link #RP_STATUS} parameter if the parameter is missing or not
+ * any of the two supported values.
+ *
+ * @see #RP_STATUS
+ * @see #STATUS_VALUE_BROWSER
+ */
+ public static final String STATUS_VALUE_STANDARD = "standard";
+
+ /**
+ * The supported value for the {@link #RP_STATUS} request parameter
+ * requesting to not report success or failure of request processing using
+ * standard HTTP status codes but instead alwas set the status to 200/OK and
+ * only report the real success or failure status in the XHTML response.
+ *
+ * @see #RP_STATUS
+ * @see #STATUS_VALUE_STANDARD
+ */
+ public static final String STATUS_VALUE_BROWSER = "browser";
+
+ /**
+ * Optional request parameter: if provided, added at the end of the computed
+ * (or supplied) redirect URL
+ */
+ public static final String RP_DISPLAY_EXTENSION = RP_PREFIX
+ + "displayExtension";
+
+ /**
+ * SLING-130, suffix that maps form field names to different JCR property
+ * names
+ */
+ public static final String VALUE_FROM_SUFFIX = "@ValueFrom";
+
+ /**
+ * Suffix indicating a type hint for the property (value is "@TypeHint").
+ */
+ public static final String TYPE_HINT_SUFFIX = "@TypeHint";
+
+ /**
+ * Suffix indicating a default value for a property (value is
+ * "@DefaultValue").
+ */
+ public static final String DEFAULT_VALUE_SUFFIX = "@DefaultValue";
+
+ /**
+ * Suffix indicating that the named property is to be removed before
+ * applying any new content (value is "@Delete").
+ */
+ public static final String SUFFIX_DELETE = "@Delete";
+
+ /**
+ * Suffix indicating that the named item is to be set from an item whose
+ * absolute or relative path is given in the parameter's value (value is
+ * "@MoveFrom").
+ * <p>
+ * This suffix is similar to the {@link #VALUE_FROM_SUFFIX} in that the
+ * value for the item is not taken from the request parameter itself but
+ * from somewhere else. In this case the value is set by moving another
+ * repository item (in the same workspace) to the location addressed by the
+ * parameter.
+ */
+ public static final String SUFFIX_MOVE_FROM = "@MoveFrom";
+
+ /**
+ * Suffix indicating that the named item is to be set from an item whose
+ * absolute or relative path is given in the parameter's value (value is
+ * "@CopyFrom").
+ * <p>
+ * This suffix is similar to the {@link #VALUE_FROM_SUFFIX} in that the
+ * value for the item is not taken from the request parameter itself but
+ * from somewhere else. In this case the value is set by copying another
+ * repository item (in the same workspace) to the location addressed by the
+ * parameter.
+ */
+ public static final String SUFFIX_COPY_FROM = "@CopyFrom";
+}
diff --git a/src/main/java/org/apache/sling/servlets/post/SlingPostOperation.java b/src/main/java/org/apache/sling/servlets/post/SlingPostOperation.java
new file mode 100644
index 0000000..bf74058
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/SlingPostOperation.java
@@ -0,0 +1,77 @@
+/*
+ * 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 org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.servlets.HtmlResponse;
+
+/**
+ * The <code>SlingPostOperation</code> interface defines the service API to be
+ * implemented by service providers extending the Sling default POST servlet.
+ * Service providers may register OSGi services of this type to be used by the
+ * Sling default POST servlet to handle specific operations.
+ * <p>
+ * The <code>SlingPostOperation</code> service must be registered with a
+ * {@link #PROP_OPERATION_NAME} registration property giving the name(s) of the
+ * operations supported by the service. The names will be used to find the
+ * actual operation from the
+ * {@link SlingPostConstants#RP_OPERATION <code>:operation</code>} request
+ * parameter.
+ * <p>
+ * The Sling default POST servlet defines the <code>copy</code>,
+ * <code>move</code> and <code>delete</code> operation names. These names
+ * should not be used by <code>SlingPostOperation</code> service providers.
+ */
+public interface SlingPostOperation {
+
+ /**
+ * The name of the Sling POST operation service (value is
+ * "org.apache.sling.servlets.post.SlingPostOperation").
+ */
+ public static final String SERVICE_NAME = SlingPostOperation.class.getName();
+
+ /**
+ * The name of the service registration property indicating the name(s) of
+ * the operation provided by the operation implementation (value is
+ * "sling.post.operation"). The value of this service property must be a
+ * single String or an array or <code>java.util.Vector</code> of Strings.
+ * If multiple strings are defined, the service is registered for all
+ * operation names.
+ */
+ public static final String PROP_OPERATION_NAME = "sling.post.operation";
+
+ /**
+ * Executes the operation provided by this service implementation. This
+ * method is called by the Sling default POST servlet.
+ *
+ * @param request The <code>SlingHttpServletRequest</code> object
+ * providing the request input for the operation.
+ * @param response The <code>HtmlResponse</code> into which the operation
+ * steps should be recorded.
+ * @throws org.apache.sling.api.resource.ResourceNotFoundException May be
+ * thrown if the operation requires an existing request
+ * resource. If this exception is thrown the Sling default POST
+ * servlet sends back a <code>404/NOT FOUND</code> response to
+ * the client.
+ * @throws org.apache.sling.api.SlingException May be thrown if an error
+ * occurrs running the operation.
+ */
+ void run(SlingHttpServletRequest request, HtmlResponse response);
+
+}
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/SlingPostServlet.java b/src/main/java/org/apache/sling/servlets/post/impl/SlingPostServlet.java
new file mode 100644
index 0000000..ce89df1
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/SlingPostServlet.java
@@ -0,0 +1,272 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.resource.ResourceNotFoundException;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.servlets.HtmlResponse;
+import org.apache.sling.api.servlets.SlingAllMethodsServlet;
+import org.apache.sling.commons.osgi.OsgiUtil;
+import org.apache.sling.servlets.post.SlingPostConstants;
+import org.apache.sling.servlets.post.SlingPostOperation;
+import org.apache.sling.servlets.post.impl.helper.DateParser;
+import org.apache.sling.servlets.post.impl.helper.NodeNameGenerator;
+import org.apache.sling.servlets.post.impl.operations.CopyOperation;
+import org.apache.sling.servlets.post.impl.operations.DeleteOperation;
+import org.apache.sling.servlets.post.impl.operations.ModifyOperation;
+import org.apache.sling.servlets.post.impl.operations.MoveOperation;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * POST servlet that implements the sling client library "protocol"
+ *
+ * @scr.component immediate="true" label="%servlet.post.name"
+ * description="%servlet.post.description"
+ * @scr.service interface="javax.servlet.Servlet"
+ * @scr.property name="service.description" value="Sling Post Servlet"
+ * @scr.property name="service.vendor" value="The Apache Software Foundation"
+ *
+ * Use this as the default servlet for POST requests for Sling
+ * @scr.property name="sling.servlet.resourceTypes"
+ * value="sling/servlet/default" private="true"
+ * @scr.property name="sling.servlet.methods" value="POST" private="true"
+ */
+public class SlingPostServlet extends SlingAllMethodsServlet {
+
+ private static final long serialVersionUID = 1837674988291697074L;
+
+ /**
+ * default log
+ */
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ /**
+ * @scr.property values.0="EEE MMM dd yyyy HH:mm:ss 'GMT'Z"
+ * values.1="yyyy-MM-dd'T'HH:mm:ss.SSSZ"
+ * values.2="yyyy-MM-dd'T'HH:mm:ss" values.3="yyyy-MM-dd"
+ * values.4="dd.MM.yyyy HH:mm:ss" values.5="dd.MM.yyyy"
+ */
+ private static final String PROP_DATE_FORMAT = "servlet.post.dateFormats";
+
+ /**
+ * @scr.property values.0="title" values.1="jcr:title" values.2="name"
+ * values.3="description" values.4="jcr:description"
+ * values.5="abstract"
+ */
+ private static final String PROP_NODE_NAME_HINT_PROPERTIES = "servlet.post.nodeNameHints";
+
+ /**
+ * @scr.property value="20" type="Integer"
+ */
+ private static final String PROP_NODE_NAME_MAX_LENGTH = "servlet.post.nodeNameMaxLength";
+
+ /**
+ * utility class for generating node names
+ */
+ private NodeNameGenerator nodeNameGenerator;
+
+ /**
+ * utility class for parsing date strings
+ */
+ private DateParser dateParser;
+
+ private SlingPostOperation modifyOperation;
+
+ private final Map<String, SlingPostOperation> postOperations = new HashMap<String, SlingPostOperation>();
+
+ @Override
+ public void init() {
+ // default operation: create/modify
+ modifyOperation = new ModifyOperation(nodeNameGenerator, dateParser,
+ getServletContext());
+
+ // other predefined operations
+ postOperations.put(SlingPostConstants.OPERATION_COPY,
+ new CopyOperation());
+ postOperations.put(SlingPostConstants.OPERATION_MOVE,
+ new MoveOperation());
+ postOperations.put(SlingPostConstants.OPERATION_DELETE,
+ new DeleteOperation());
+ }
+
+ @Override
+ public void destroy() {
+ modifyOperation = null;
+ postOperations.clear();
+ }
+
+ @Override
+ protected void doPost(SlingHttpServletRequest request,
+ SlingHttpServletResponse response) throws IOException {
+
+ // prepare the response
+ HtmlResponse htmlResponse = new HtmlResponse();
+ htmlResponse.setReferer(request.getHeader("referer"));
+
+ SlingPostOperation operation = getSlingPostOperation(request);
+ if (operation == null) {
+
+ htmlResponse.setStatus(
+ HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
+ "Invalid operation specified for POST request");
+
+ } else {
+
+ try {
+ operation.run(request, htmlResponse);
+ } catch (ResourceNotFoundException rnfe) {
+ htmlResponse.setStatus(HttpServletResponse.SC_NOT_FOUND,
+ rnfe.getMessage());
+ } catch (Throwable throwable) {
+ htmlResponse.setError(throwable);
+ }
+
+ }
+
+ // check for redirect URL if processing succeeded
+ if (htmlResponse.isSuccessful()) {
+ String redirect = getRedirectUrl(request, htmlResponse);
+ if (redirect != null) {
+ response.sendRedirect(redirect);
+ return;
+ }
+ }
+
+ // create a html response and send if unsuccessful or no redirect
+ htmlResponse.send(response, isSetStatus(request));
+ }
+
+ private SlingPostOperation getSlingPostOperation(
+ SlingHttpServletRequest request) {
+ String operation = request.getParameter(SlingPostConstants.RP_OPERATION);
+ if (operation == null || operation.length() == 0) {
+ // standard create/modify operation;
+ return modifyOperation;
+ }
+
+ // named operation, retrieve from map
+ return postOperations.get(operation);
+ }
+
+ /**
+ * compute redirect URL (SLING-126)
+ *
+ * @param ctx the post processor
+ * @return the redirect location or <code>null</code>
+ */
+ protected String getRedirectUrl(HttpServletRequest request, HtmlResponse ctx) {
+ // redirect param has priority (but see below, magic star)
+ String result = request.getParameter(SlingPostConstants.RP_REDIRECT_TO);
+ if (result != null && ctx.getPath() != null) {
+
+ // redirect to created/modified Resource
+ int star = result.indexOf('*');
+ if (star >= 0) {
+ StringBuffer buf = new StringBuffer();
+
+ // anything before the star
+ if (star > 0) {
+ buf.append(result.substring(0, star));
+ }
+
+ // append the name of the manipulated node
+ buf.append(ResourceUtil.getName(ctx.getPath()));
+
+ // anything after the star
+ if (star < result.length() - 1) {
+ buf.append(result.substring(star + 1));
+ }
+
+ // use the created path as the redirect result
+ result = buf.toString();
+
+ } else if (result.endsWith(SlingPostConstants.DEFAULT_CREATE_SUFFIX)) {
+ // if the redirect has a trailing slash, append modified node
+ // name
+ result = result.concat(ResourceUtil.getName(ctx.getPath()));
+ }
+
+ if (log.isDebugEnabled()) {
+ log.debug("Will redirect to " + result);
+ }
+ }
+ return result;
+ }
+
+ protected boolean isSetStatus(SlingHttpServletRequest request) {
+ String statusParam = request.getParameter(SlingPostConstants.RP_STATUS);
+ if (statusParam == null) {
+ log.debug(
+ "getStatusMode: Parameter {} not set, assuming standard status code",
+ SlingPostConstants.RP_STATUS);
+ return true;
+ }
+
+ if (SlingPostConstants.STATUS_VALUE_BROWSER.equals(statusParam)) {
+ log.debug(
+ "getStatusMode: Parameter {} asks for user-friendly status code",
+ SlingPostConstants.RP_STATUS);
+ return false;
+ }
+
+ if (SlingPostConstants.STATUS_VALUE_STANDARD.equals(statusParam)) {
+ log.debug(
+ "getStatusMode: Parameter {} asks for standard status code",
+ SlingPostConstants.RP_STATUS);
+ return true;
+ }
+
+ log.debug(
+ "getStatusMode: Parameter {} set to unknown value {}, assuming standard status code",
+ SlingPostConstants.RP_STATUS);
+ return true;
+ }
+
+ // ---------- SCR Integration ----------------------------------------------
+
+ protected void activate(ComponentContext context) {
+ Dictionary<?, ?> props = context.getProperties();
+
+ String[] nameHints = OsgiUtil.toStringArray(props.get(PROP_NODE_NAME_HINT_PROPERTIES));
+ int nameMax = (int) OsgiUtil.toLong(
+ props.get(PROP_NODE_NAME_MAX_LENGTH), -1);
+ nodeNameGenerator = new NodeNameGenerator(nameHints, nameMax);
+
+ dateParser = new DateParser();
+ String[] dateFormats = OsgiUtil.toStringArray(props.get(PROP_DATE_FORMAT));
+ for (String dateFormat : dateFormats) {
+ dateParser.register(dateFormat);
+ }
+ }
+
+ protected void deactivate(ComponentContext context) {
+ nodeNameGenerator = null;
+ dateParser = null;
+ }
+}
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/helper/DateParser.java b/src/main/java/org/apache/sling/servlets/post/impl/helper/DateParser.java
new file mode 100644
index 0000000..9758da9
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/helper/DateParser.java
@@ -0,0 +1,138 @@
+/*
+ * 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.helper;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Locale;
+
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Takes a string representation of a time-date string and tries for parse it
+ * using different formats.
+ */
+public class DateParser {
+
+ /**
+ * default log
+ */
+ private static final Logger log = LoggerFactory.getLogger(DateParser.class);
+
+ /**
+ * lits of formats
+ */
+ private final List<DateFormat> formats = new LinkedList<DateFormat>();
+
+ /**
+ * Registers a format string to the list of internally checked ones.
+ * Uses the {@link SimpleDateFormat}.
+ * @param format format as in {@link SimpleDateFormat}
+ * @throws IllegalArgumentException if the format is not valid.
+ */
+ public void register(String format) {
+ register(new SimpleDateFormat(format, Locale.US));
+ }
+
+ /**
+ * Registers a date format to the list of internally checked ones.
+ * @param format date format
+ */
+ public void register(DateFormat format) {
+ formats.add(format);
+ }
+
+ /**
+ * Parses the given source string and returns the respective calendar
+ * instance. If no format matches returns <code>null</code>.
+ * <p/>
+ * Note: method is synchronized because SimpleDateFormat is not.
+ *
+ * @param source date time source string
+ * @return calendar representation of the source or <code>null</code>
+ */
+ public synchronized Calendar parse(String source) {
+ for (DateFormat fmt: formats) {
+ try {
+ Date d = fmt.parse(source);
+ if (log.isDebugEnabled()) {
+ log.debug("Parsed " + source + " using " + fmt + " into " + d);
+ }
+ Calendar c = Calendar.getInstance();
+ c.setTime(d);
+ return c;
+ } catch (ParseException e) {
+ if (log.isDebugEnabled()) {
+ log.debug("Failed parsing " + source + " using " + fmt);
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Parses the given source strings and returns the respective calendar
+ * instances. If no format matches for any of the sources
+ * returns <code>null</code>.
+ * <p/>
+ * Note: method is synchronized because SimpleDateFormat is not.
+ *
+ * @param sources date time source strings
+ * @return calendar representations of the source or <code>null</code>
+ */
+ public synchronized Calendar[] parse(String sources[]) {
+ Calendar ret[] = new Calendar[sources.length];
+ for (int i=0; i< sources.length; i++) {
+ if ((ret[i] = parse(sources[i])) == null) {
+ return null;
+ }
+ }
+ return ret;
+ }
+
+ /**
+ * Parses the given source strings and returns the respective jcr date value
+ * instances. If no format matches for any of the sources
+ * returns <code>null</code>.
+ * <p/>
+ * Note: method is synchronized because SimpleDateFormat is not.
+ *
+ * @param sources date time source strings
+ * @param factory the value factory
+ * @return jcr date value representations of the source or <code>null</code>
+ */
+ public synchronized Value[] parse(String sources[], ValueFactory factory) {
+ Value ret[] = new Value[sources.length];
+ for (int i=0; i< sources.length; i++) {
+ Calendar c = parse(sources[i]);
+ if (c == null) {
+ return null;
+ }
+ ret[i] = factory.createValue(c);
+ }
+ return ret;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameFilter.java b/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameFilter.java
new file mode 100644
index 0000000..92817c1
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameFilter.java
@@ -0,0 +1,58 @@
+/*
+ * 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.helper;
+
+
+/**
+ * Filter a String so that it can be used as a NodeName.
+ */
+public class NodeNameFilter {
+
+ public static final String ALLOWED_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789_";
+ public static final char REPLACEMENT_CHAR = '_';
+
+ public String filter(String nodeName) {
+ final StringBuffer sb = new StringBuffer();
+ char lastAdded = 0;
+
+ nodeName = nodeName.toLowerCase();
+ for(int i=0; i < nodeName.length(); i++) {
+ final char c = nodeName.charAt(i);
+ char toAdd = c;
+
+ if (ALLOWED_CHARS.indexOf(c) < 0) {
+ if (lastAdded == REPLACEMENT_CHAR) {
+ // do not add several _ in a row
+ continue;
+ }
+ toAdd = REPLACEMENT_CHAR;
+
+ } else if(i == 0 && Character.isDigit(c)) {
+ sb.append(REPLACEMENT_CHAR);
+ }
+
+ sb.append(toAdd);
+ lastAdded = toAdd;
+ }
+
+ if (sb.length()==0) {
+ sb.append(REPLACEMENT_CHAR);
+ }
+
+ return sb.toString();
+ }
+}
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameGenerator.java b/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameGenerator.java
new file mode 100644
index 0000000..32864f6
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/helper/NodeNameGenerator.java
@@ -0,0 +1,131 @@
+/*
+ * 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.helper;
+
+import org.apache.sling.api.request.RequestParameter;
+import org.apache.sling.api.request.RequestParameterMap;
+import org.apache.sling.servlets.post.SlingPostConstants;
+
+/**
+ * Generates a node name based on a set of well-known request parameters
+ * like title, description, etc.
+ * See SLING-128.
+ */
+public class NodeNameGenerator {
+
+ private final String[] parameterNames;
+ private final NodeNameFilter filter = new NodeNameFilter();
+
+ public static final int DEFAULT_MAX_NAME_LENGTH = 20;
+
+ private int maxLength = DEFAULT_MAX_NAME_LENGTH;
+ private int counter;
+
+ public NodeNameGenerator(String[] parameterNames, int maxNameLength) {
+ if (parameterNames == null) {
+ this.parameterNames = new String[0];
+ } else {
+ this.parameterNames = parameterNames;
+ }
+
+ this.maxLength = (maxNameLength > 0)
+ ? maxNameLength
+ : DEFAULT_MAX_NAME_LENGTH;
+ }
+
+ /**
+ * Get a "nice" node name, if possible, based on given request
+ *
+ * @param parameters the request parameters
+ * @param requirePrefix <code>true</code> if the parameter names for
+ * properties requires a prefix
+ * @return a nice node name
+ */
+ public String getNodeName(RequestParameterMap parameters,
+ boolean requirePrefix) {
+ String valueToUse = null;
+ boolean doFilter = true;
+
+ // find the first request parameter that matches one of
+ // our parameterNames, in order, and has a value
+ if (parameters!=null) {
+ // we first check for the special sling parameters
+ RequestParameter specialParam = parameters.getValue(SlingPostConstants.RP_NODE_NAME);
+ if ( specialParam != null ) {
+ if ( specialParam.getString() != null && specialParam.getString().length() > 0 ) {
+ valueToUse = specialParam.getString();
+ doFilter = false;
+ }
+ }
+ if ( valueToUse == null ) {
+ specialParam = parameters.getValue(SlingPostConstants.RP_NODE_NAME_HINT);
+ if ( specialParam != null ) {
+ if ( specialParam.getString() != null && specialParam.getString().length() > 0 ) {
+ valueToUse = specialParam.getString();
+ }
+ }
+ }
+
+ if (valueToUse == null) {
+ for (String param : parameterNames) {
+ if (valueToUse != null) {
+ break;
+ }
+ if (requirePrefix) {
+ param = SlingPostConstants.ITEM_PREFIX_RELATIVE_CURRENT.concat(param);
+ }
+ final RequestParameter[] pp = parameters.get(param);
+ if (pp != null) {
+ for (RequestParameter p : pp) {
+ valueToUse = p.getString();
+ if (valueToUse != null && valueToUse.length() > 0) {
+ break;
+ }
+ valueToUse = null;
+ }
+ }
+ }
+ }
+ }
+ String result;
+ // should we filter?
+ if (valueToUse != null) {
+ if ( doFilter ) {
+ // filter value so that it works as a node name
+ result = filter.filter(valueToUse);
+ } else {
+ result = valueToUse;
+ }
+ } else {
+ // default value if none provided
+ result = nextCounter() + "_" + System.currentTimeMillis();
+ }
+
+ if ( doFilter ) {
+ // max length
+ if (result.length() > maxLength) {
+ result = result.substring(0,maxLength);
+ }
+ }
+
+ return result;
+ }
+
+ public synchronized int nextCounter() {
+ return ++counter;
+ }
+}
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/helper/RequestProperty.java b/src/main/java/org/apache/sling/servlets/post/impl/helper/RequestProperty.java
new file mode 100644
index 0000000..48a795c
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/helper/RequestProperty.java
@@ -0,0 +1,255 @@
+/*
+ * 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.helper;
+
+import org.apache.sling.api.request.RequestParameter;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.servlets.post.SlingPostConstants;
+
+/**
+ * Encapsulates all infos from the respective request parameters that are needed
+ * to create the repository property
+ */
+public class RequestProperty {
+
+ private static final RequestParameter[] EMPTY_PARAM_ARRAY = new RequestParameter[0];
+
+ public static final String DEFAULT_IGNORE = SlingPostConstants.RP_PREFIX
+ + "ignore";
+
+ public static final String DEFAULT_NULL = SlingPostConstants.RP_PREFIX
+ + "null";
+
+ private final String path;
+
+ private final String name;
+
+ private final String parentPath;
+
+ private RequestParameter[] values;
+
+ private String[] stringValues;
+
+ private String typeHint;
+
+ private boolean hasMultiValueTypeHint;
+
+ private RequestParameter[] defaultValues = EMPTY_PARAM_ARRAY;
+
+ private boolean isDelete;
+
+ private String repositoryResourcePath;
+
+ private boolean isRepositoryResourceMove;
+
+ public RequestProperty(String path) {
+ assert path.startsWith("/");
+ this.path = ResourceUtil.normalize(path);
+ this.parentPath = ResourceUtil.getParent(path);
+ this.name = ResourceUtil.getName(path);
+ }
+
+ public String getTypeHint() {
+ return typeHint;
+ }
+
+ public boolean hasMultiValueTypeHint() {
+ return this.hasMultiValueTypeHint;
+ }
+
+ public void setTypeHintValue(String typeHint) {
+ if ( typeHint != null && typeHint.endsWith("[]") ) {
+ this.typeHint = typeHint.substring(0, typeHint.length() - 2);
+ this.hasMultiValueTypeHint = true;
+ } else {
+ this.typeHint = typeHint;
+ this.hasMultiValueTypeHint = false;
+ }
+ }
+
+ public String getPath() {
+ return path;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getParentPath() {
+ return parentPath;
+ }
+
+ public boolean hasValues() {
+ return values != null;
+ }
+
+ public RequestParameter[] getValues() {
+ return values;
+ }
+
+ public void setValues(RequestParameter[] values) {
+ this.values = values;
+ }
+
+ public RequestParameter[] getDefaultValues() {
+ return defaultValues;
+ }
+
+ public void setDefaultValues(RequestParameter[] defaultValues) {
+ if (defaultValues == null) {
+ this.defaultValues = EMPTY_PARAM_ARRAY;
+ } else {
+ this.defaultValues = defaultValues;
+ }
+ }
+
+ public boolean isFileUpload() {
+ return !values[0].isFormField();
+ }
+
+ /**
+ * Checks if this property provides any values. this is the case if one of
+ * the values is not empty or if the default handling is not 'ignore'
+ *
+ * @return <code>true</code> if this property provides values
+ */
+ public boolean providesValue() {
+ // should void double creation of string values
+ String[] sv = getStringValues();
+ if (sv == null) {
+ // is missleading return type. but means that property should not
+ // get auto-create values
+ return true;
+ }
+ for (String s : sv) {
+ if (!s.equals("")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the assembled string array out of the provided request values and
+ * default values.
+ *
+ * @return a String array or <code>null</code> if the property needs to be
+ * removed.
+ */
+ public String[] getStringValues() {
+ if (stringValues == null) {
+ if (values.length > 1) {
+ // TODO: how the default values work for MV props is not very
+ // clear
+ stringValues = new String[values.length];
+ for (int i = 0; i < stringValues.length; i++) {
+ stringValues[i] = values[i].getString();
+ }
+ } else {
+ String value = values[0].getString();
+ if (value.equals("")) {
+ if (defaultValues.length == 1) {
+ String defValue = defaultValues[0].getString();
+ if (defValue.equals(DEFAULT_IGNORE)) {
+ // ignore means, do not create empty values
+ return new String[0];
+ } else if (defValue.equals(DEFAULT_NULL)) {
+ // null means, remove property if exist
+ return null;
+ }
+ value = defValue;
+ }
+ }
+ stringValues = new String[] { value };
+ }
+ }
+ return stringValues;
+ }
+
+ /**
+ * Specifies whether this property should be deleted before any new content
+ * is to be set according to the values stored.
+ *
+ * @param isDelete <code>true</code> if the repository item described by
+ * this is to be deleted before any other operation.
+ */
+ public void setDelete(boolean isDelete) {
+ this.isDelete = isDelete;
+ }
+
+ /**
+ * Returns <code>true</code> if the repository item described by this is
+ * to be deleted before setting new content to it.
+ */
+ public boolean isDelete() {
+ return isDelete;
+ }
+
+ /**
+ * Sets the path of the repository item from which the content for this
+ * property is to be copied or moved. The path may be relative in which case
+ * it will be resolved relative to the absolute path of this property.
+ *
+ * @param sourcePath The path of the repository item to get the content from
+ * @param isMove <code>true</code> if the source content is to be moved,
+ * otherwise the source content is copied from the repository
+ * item.
+ */
+ public void setRepositorySource(String sourcePath, boolean isMove) {
+
+ // make source path absolute
+ if (!sourcePath.startsWith("/")) {
+ sourcePath = getParentPath() + "/" + sourcePath;
+ sourcePath = ResourceUtil.normalize(sourcePath);
+ }
+
+ this.repositoryResourcePath = sourcePath;
+ this.isRepositoryResourceMove = isMove;
+ }
+
+ /**
+ * Returns <code>true</code> if the content of this property is to be set
+ * by moving content from another repository item.
+ *
+ * @see #getRepositorySource()
+ */
+ public boolean hasRepositoryMoveSource() {
+ return isRepositoryResourceMove;
+ }
+
+ /**
+ * Returns <code>true</code> if the content of this property is to be set
+ * by copying content from another repository item.
+ *
+ * @see #getRepositorySource()
+ */
+ public boolean hasRepositoryCopySource() {
+ return getRepositorySource() != null && !hasRepositoryMoveSource();
+ }
+
+ /**
+ * Returns the absolute path of the repository item from which the content
+ * for this property is to be copied or moved.
+ *
+ * @see #hasRepositoryCopySource()
+ * @see #hasRepositoryMoveSource()
+ * @see #setRepositorySource(String, boolean)
+ */
+ public String getRepositorySource() {
+ return repositoryResourcePath;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingFileUploadHandler.java b/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingFileUploadHandler.java
new file mode 100644
index 0000000..53dcbf0
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingFileUploadHandler.java
@@ -0,0 +1,205 @@
+/*
+ * 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.helper;
+
+import java.io.IOException;
+import java.util.Calendar;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.NodeTypeManager;
+import javax.servlet.ServletContext;
+
+import org.apache.jackrabbit.util.Text;
+import org.apache.sling.api.request.RequestParameter;
+import org.apache.sling.api.servlets.HtmlResponse;
+
+/**
+ * Handles file uploads.
+ * <p/>
+ *
+ * Simple example:
+ * <xmp>
+ * <form action="/home/admin" method="POST" enctype="multipart/form-data">
+ * <input type="file" name="./portrait" />
+ * </form>
+ * </xmp>
+ *
+ * this will create a nt:file node below "/home/admin" if the node type of
+ * "admin" is (derived from) nt:folder, a nt:resource node otherwise.
+ * <p/>
+ *
+ * Filename example:
+ * <xmp>
+ * <form action="/home/admin" method="POST" enctype="multipart/form-data">
+ * <input type="file" name="./*" />
+ * </form>
+ * </xmp>
+ *
+ * same as above, but uses the filename of the uploaded file as name for the
+ * new node.
+ * <p/>
+ *
+ * Type hint example:
+ * <xmp>
+ * <form action="/home/admin" method="POST" enctype="multipart/form-data">
+ * <input type="file" name="./portrait" />
+ * <input type="hidden" name="./portrait@TypeHint" value="my:file" />
+ * </form>
+ * </xmp>
+ *
+ * this will create a new node with the type my:file below admin. if the hinted
+ * type extends from nt:file an intermediate file node is created otherwise
+ * directly a resource node.
+ */
+public class SlingFileUploadHandler {
+
+ // nodetype name string constants
+ public static final String NT_FOLDER = "nt:folder";
+ public static final String NT_FILE = "nt:file";
+ public static final String NT_RESOURCE = "nt:resource";
+ public static final String NT_UNSTRUCTURED = "nt:unstructured";
+
+ // item name string constants
+ public static final String JCR_CONTENT = "jcr:content";
+ public static final String JCR_LASTMODIFIED = "jcr:lastModified";
+ public static final String JCR_MIMETYPE = "jcr:mimeType";
+ public static final String JCR_ENCODING = "jcr:encoding";
+ public static final String JCR_DATA = "jcr:data";
+
+ /**
+ * The servlet context.
+ */
+ private final ServletContext servletContext;
+
+ /**
+ * Constructs file upload handler
+ * @param servletCtx the post processor
+ */
+ public SlingFileUploadHandler(ServletContext servletCtx) {
+ this.servletContext = servletCtx;
+ }
+
+ /**
+ * Uses the file(s) in the request parameter for creation of new nodes.
+ * if the parent node is a nt:folder a new nt:file is created. otherwise
+ * just a nt:resource. if the <code>name</code> is '*', the filename of
+ * the uploaded file is used.
+ *
+ * @param parent the parent node
+ * @param prop the assembled property info
+ * @throws RepositoryException if an error occurs
+ */
+ public void setFile(Node parent, RequestProperty prop, HtmlResponse response)
+ throws RepositoryException {
+ RequestParameter value = prop.getValues()[0];
+ assert !value.isFormField();
+
+ // ignore if empty
+ if (value.getSize() <= 0) {
+ return;
+ }
+
+ // get node name
+ String name = prop.getName();
+ if (name.equals("*")) {
+ name = value.getFileName();
+ // strip of possible path (some browsers include the entire path)
+ name = name.substring(name.lastIndexOf('/') + 1);
+ name = name.substring(name.lastIndexOf('\\') + 1);
+ }
+ name = Text.escapeIllegalJcrChars(name);
+
+ // check type hint. if the type is ok and extends from nt:file,
+ // create an nt:file with that type. if it's invalid, drop it and let
+ // the parent node type decide.
+ boolean createNtFile = parent.isNodeType(NT_FOLDER);
+ String typeHint = prop.getTypeHint();
+ if (typeHint != null) {
+ try {
+ NodeTypeManager ntMgr = parent.getSession().getWorkspace().getNodeTypeManager();
+ NodeType nt = ntMgr.getNodeType(typeHint);
+ createNtFile = nt.isNodeType(NT_FILE);
+ } catch (RepositoryException e) {
+ // assuming type not valid.
+ typeHint = null;
+ }
+ }
+
+ // also create an nt:file if the name contains an extension
+ // the rationale is that if the file name is "important" we want
+ // an nt:file, and an image name with an extension is probably "important"
+ if(!createNtFile && name.indexOf('.') > 0) {
+ createNtFile = true;
+ }
+
+ // set empty type
+ if (typeHint == null) {
+ typeHint = createNtFile ? NT_FILE : NT_RESOURCE;
+ }
+
+ // remove node
+ if (parent.hasNode(name)) {
+ parent.getNode(name).remove();
+ }
+
+ // create nt:file node if needed
+ if (createNtFile) {
+ // create nt:file
+ parent = parent.addNode(name, typeHint);
+ response.onCreated(parent.getPath());
+ name = JCR_CONTENT;
+ typeHint = NT_RESOURCE;
+ }
+
+ // create resource node
+ Node res = parent.addNode(name, typeHint);
+ response.onCreated(res.getPath());
+
+ // get content type
+ String contentType = value.getContentType();
+ if (contentType != null) {
+ int idx = contentType.indexOf(';');
+ if (idx > 0) {
+ contentType = contentType.substring(0, idx);
+ }
+ }
+ if (contentType == null || contentType.equals("application/octet-stream")) {
+ // try to find a better content type
+ contentType = this.servletContext.getMimeType(value.getFileName());
+ if (contentType == null || contentType.equals("application/octet-stream")) {
+ contentType = "application/octet-stream";
+ }
+ }
+
+ // set properties
+ response.onModified(
+ res.setProperty(JCR_LASTMODIFIED, Calendar.getInstance()).getPath()
+ );
+ response.onModified(
+ res.setProperty(JCR_MIMETYPE, contentType).getPath()
+ );
+ try {
+ response.onModified(
+ res.setProperty(JCR_DATA, value.getInputStream()).getPath()
+ );
+ } catch (IOException e) {
+ throw new RepositoryException("Error while retrieving inputstream from parameter value.", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandler.java b/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandler.java
new file mode 100644
index 0000000..9c57694
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/helper/SlingPropertyValueHandler.java
@@ -0,0 +1,285 @@
+/*
+ * 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.helper;
+
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+
+import org.apache.sling.api.servlets.HtmlResponse;
+
+/**
+ * Sets a Property on the given Node, in some cases with a specific type and
+ * value. For example, "lastModified" with an empty value is stored as the
+ * current Date.
+ */
+public class SlingPropertyValueHandler {
+
+ /**
+ * Defins a map of auto properties
+ */
+ private static final Map<String, AutoType> AUTO_PROPS = new HashMap<String, AutoType>();
+ static {
+ AUTO_PROPS.put("created", AutoType.CREATED);
+ AUTO_PROPS.put("createdBy", AutoType.CREATED_BY);
+ AUTO_PROPS.put("jcr:created", AutoType.CREATED);
+ AUTO_PROPS.put("jcr:createdBy", AutoType.CREATED_BY);
+ AUTO_PROPS.put("lastModified", AutoType.MODIFIED);
+ AUTO_PROPS.put("lastModifiedBy", AutoType.MODIFIED_BY);
+ AUTO_PROPS.put("jcr:lastModified", AutoType.MODIFIED);
+ AUTO_PROPS.put("jcr:lastModifiedBy", AutoType.MODIFIED_BY);
+ }
+
+ /**
+ * the post processor
+ */
+ private final HtmlResponse response;
+
+ private final DateParser dateParser;
+
+ /**
+ * current date for all properties in this request
+ */
+ private final Calendar now = Calendar.getInstance();
+
+ /**
+ * Constructs a propert value handler
+ */
+ public SlingPropertyValueHandler(DateParser dateParser, HtmlResponse response) {
+ this.dateParser = dateParser;
+ this.response = response;
+ }
+
+
+ /**
+ * Set property on given node, with some automatic values when user provides
+ * the field name but no value.
+ *
+ * html example for testing:
+ * <xmp>
+ * <input type="hidden" name="created"/>
+ * <input type="hidden" name="lastModified"/>
+ * <input type="hidden" name="createdBy" />
+ * <input type="hidden" name="lastModifiedBy"/>
+ * </xmp>
+ *
+ * @param parent the parent node
+ * @param prop the request property
+ * @throws RepositoryException if a repository error occurs
+ */
+ public void setProperty(Node parent, RequestProperty prop)
+ throws RepositoryException {
+
+ final String name = prop.getName();
+ if (prop.providesValue()) {
+ // if user provided a value, don't mess with it
+ setPropertyAsIs(parent, prop);
+
+ } else if (AUTO_PROPS.containsKey(name)) {
+ // avoid collision with protected properties
+ switch (AUTO_PROPS.get(name)) {
+ case CREATED:
+ if (parent.isNew()) {
+ setCurrentDate(parent, name);
+ }
+ break;
+ case CREATED_BY:
+ if (parent.isNew()) {
+ setCurrentUser(parent, name);
+ }
+ break;
+ case MODIFIED:
+ setCurrentDate(parent, name);
+ break;
+ case MODIFIED_BY:
+ setCurrentUser(parent, name);
+ break;
+ }
+ } else {
+ // no magic field, set value as provided
+ setPropertyAsIs(parent, prop);
+ }
+ }
+
+ /**
+ * Sets the property to the given date
+ * @param parent parent node
+ * @param name name of the property
+ * @throws RepositoryException if a repository error occurs
+ */
+ private void setCurrentDate(Node parent, String name)
+ throws RepositoryException {
+ removePropertyIfExists(parent, name);
+ response.onModified(
+ parent.setProperty(name, now).getPath()
+ );
+ }
+
+ /**
+ * set property to the current User id
+ * @param parent parent node
+ * @param name name of the property
+ * @throws RepositoryException if a repository error occurs
+ */
+ private void setCurrentUser(Node parent, String name)
+ throws RepositoryException {
+ removePropertyIfExists(parent, name);
+ response.onModified(
+ parent.setProperty(name, parent.getSession().getUserID()).getPath()
+ );
+ }
+
+ /**
+ * Removes the property with the given name from the parent node if it
+ * exists and if it's not a mandatory property.
+ *
+ * @param parent the parent node
+ * @param name the name of the property to remove
+ * @return path of the property that was removed or <code>null</code> if
+ * it was not removed
+ * @throws RepositoryException if a repository error occurs.
+ */
+ private String removePropertyIfExists(Node parent, String name)
+ throws RepositoryException {
+ if (parent.hasProperty(name)) {
+ Property prop = parent.getProperty(name);
+ if (!prop.getDefinition().isMandatory()) {
+ String path = prop.getPath();
+ prop.remove();
+ return path;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * set property without processing, except for type hints
+ *
+ * @param parent the parent node
+ * @param prop the request property
+ * @throws RepositoryException if a repository error occurs.
+ */
+ private void setPropertyAsIs(Node parent, RequestProperty prop)
+ throws RepositoryException {
+
+ // no explicit typehint
+ int type = PropertyType.UNDEFINED;
+ if (prop.getTypeHint() != null) {
+ try {
+ type = PropertyType.valueFromName(prop.getTypeHint());
+ } catch (Exception e) {
+ // ignore
+ }
+ }
+
+ String[] values = prop.getStringValues();
+ if (values == null) {
+ // remove property
+ response.onDeleted(
+ removePropertyIfExists(parent, prop.getName())
+ );
+ } else if (values.length == 0) {
+ // do not create new prop here, but clear existing
+ if (parent.hasProperty(prop.getName())) {
+ response.onModified(
+ parent.setProperty(prop.getName(), "").getPath()
+ );
+ }
+ } else if (values.length == 1) {
+ final String removePath = removePropertyIfExists(parent, prop.getName());
+ // if the provided value is the empty string, we don't have to do anything.
+ if ( values[0].length() == 0 ) {
+ if ( removePath != null ) {
+ response.onDeleted(removePath);
+ }
+ } else {
+ // modify property
+ if (type == PropertyType.DATE) {
+ // try conversion
+ Calendar c = dateParser.parse(values[0]);
+ if (c != null) {
+ if ( prop.hasMultiValueTypeHint() ) {
+ final Value[] array = new Value[1];
+ array[0] = parent.getSession().getValueFactory().createValue(c);
+ response.onModified(
+ parent.setProperty(prop.getName(), array).getPath()
+ );
+ } else {
+ response.onModified(
+ parent.setProperty(prop.getName(), c).getPath()
+ );
+ }
+ return;
+ }
+ // fall back to default behaviour
+ }
+ final Property p;
+ if ( type == PropertyType.UNDEFINED ) {
+ p = parent.setProperty(prop.getName(), values[0]);
+ } else {
+ if ( prop.hasMultiValueTypeHint() ) {
+ final Value[] array = new Value[1];
+ array[0] = parent.getSession().getValueFactory().createValue(values[0], type);
+ p = parent.setProperty(prop.getName(), array);
+ } else {
+ p = parent.setProperty(prop.getName(), values[0], type);
+ }
+ }
+ response.onModified(p.getPath());
+ }
+ } else {
+ removePropertyIfExists(parent, prop.getName());
+ if (type == PropertyType.DATE) {
+ // try conversion
+ ValueFactory valFac = parent.getSession().getValueFactory();
+ Value[] c = dateParser.parse(values, valFac);
+ if (c != null) {
+ response.onModified(
+ parent.setProperty(prop.getName(), c).getPath()
+ );
+ return;
+ }
+ // fall back to default behaviour
+ }
+ final Property p;
+ if ( type == PropertyType.UNDEFINED ) {
+ p = parent.setProperty(prop.getName(), values);
+ } else {
+ p = parent.setProperty(prop.getName(), values, type);
+ }
+ response.onModified(p.getPath());
+ }
+ }
+
+ /**
+ * Defines an auto property behavior
+ */
+ private enum AutoType {
+ CREATED,
+ CREATED_BY,
+ MODIFIED,
+ MODIFIED_BY
+ }
+}
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCopyMoveOperation.java b/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCopyMoveOperation.java
new file mode 100644
index 0000000..0b0ee84
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/operations/AbstractCopyMoveOperation.java
@@ -0,0 +1,156 @@
+/*
+ * 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.Iterator;
+
+import javax.jcr.Item;
+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.ResourceNotFoundException;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.servlets.HtmlResponse;
+import org.apache.sling.servlets.post.AbstractSlingPostOperation;
+import org.apache.sling.servlets.post.SlingPostConstants;
+
+/**
+ * The <code>AbstractCopyMoveOperation</code> is the abstract base close for
+ * the {@link CopyOperation} and {@link MoveOperation} classes implementing
+ * commong behaviour.
+ */
+abstract class AbstractCopyMoveOperation extends AbstractSlingPostOperation {
+
+ @Override
+ protected final void doRun(SlingHttpServletRequest request,
+ HtmlResponse response) throws RepositoryException {
+
+ Resource resource = request.getResource();
+ String source = resource.getPath();
+
+ // ensure dest is not empty/null and is absolute
+ String dest = request.getParameter(SlingPostConstants.RP_DEST);
+ if (dest == null || dest.length() == 0) {
+ throw new IllegalArgumentException("Unable to process "
+ + getOperationName() + ". Missing destination");
+ }
+
+ // register whether the path ends with a trailing slash
+ boolean trailingSlash = dest.endsWith("/");
+
+ // ensure destination is an absolute and normalized path
+ if (!dest.startsWith("/")) {
+ dest = ResourceUtil.getParent(source) + "/" + dest;
+ }
+ dest = ResourceUtil.normalize(dest);
+
+ // destination parent and name
+ String dstParent = trailingSlash ? dest : ResourceUtil.getParent(dest);
+
+ // delete destination if already exists
+ Session session = request.getResourceResolver().adaptTo(Session.class);
+ if (!trailingSlash && session.itemExists(dest)) {
+
+ final String replaceString = request.getParameter(SlingPostConstants.RP_REPLACE);
+ final boolean isReplace = "true".equalsIgnoreCase(replaceString);
+ if (!isReplace) {
+ response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED,
+ "Cannot " + getOperationName() + " " + resource + " to "
+ + dest + ": destination exists");
+ return;
+ }
+
+ } else {
+
+ // check if path to destination exists and create it, but only
+ // if it's a descendant of the current node
+ if (!dstParent.equals("") && !session.itemExists(dstParent)) {
+ response.setStatus(HttpServletResponse.SC_PRECONDITION_FAILED,
+ "Cannot " + getOperationName() + " " + resource + " to "
+ + dest + ": parent of destination does not exist");
+ return;
+ }
+
+ // the destination is newly created, hence a create request
+ response.setCreateRequest(true);
+ }
+
+ Iterator<Resource> resources = getApplyToResources(request);
+ if (resources == null) {
+
+ // ensure we have an item underlying the request's resource
+ Item item = resource.adaptTo(Item.class);
+ if (item == null) {
+ throw new ResourceNotFoundException("Missing source "
+ + resource + " for " + getOperationName());
+ }
+
+ String dstName = trailingSlash ? null : ResourceUtil.getName(dest);
+ execute(response, item, dstParent, dstName);
+
+ } else {
+
+ // multiple applyTo requires trailing slash on destination
+ if (!trailingSlash) {
+ throw new IllegalArgumentException(
+ "Applying "
+ + getOperationName()
+ + " to multiple resources requires a trailing slash on the destination");
+ }
+
+ // multiple copy will never return 201/CREATED
+ response.setCreateRequest(false);
+
+ while (resources.hasNext()) {
+ Resource applyTo = resources.next();
+ Item item = applyTo.adaptTo(Item.class);
+ if (item != null) {
+ execute(response, item, dstParent, null);
+ }
+ }
+
+ }
+
+ // finally apply the ordering parameter
+ orderNode(request, session.getItem(dest));
+ }
+
+ /**
+ * Returns a short name to be used in log and status messages.
+ */
+ protected abstract String getOperationName();
+
+ /**
+ * Actually executes the operation.
+ *
+ * @param response The <code>HtmlResponse</code> used to record success of
+ * the operation.
+ * @param source The source item to act upon.
+ * @param destParent The absolute path of the parent of the target item.
+ * @param destName The name of the target item inside the
+ * <code>destParent</code>. If <code>null</code> the name of
+ * the <code>source</code> is used as the target item name.
+ * @throws RepositoryException May be thrown if an error occurrs executing
+ * the operation.
+ */
+ protected abstract void execute(HtmlResponse response, Item source,
+ String destParent, String destName) throws RepositoryException;
+
+}
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/operations/CopyOperation.java b/src/main/java/org/apache/sling/servlets/post/impl/operations/CopyOperation.java
new file mode 100644
index 0000000..7d58515
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/operations/CopyOperation.java
@@ -0,0 +1,168 @@
+/*
+ * 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 javax.jcr.Item;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.nodetype.NodeType;
+
+import org.apache.sling.api.servlets.HtmlResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>CopyOperation</code> class implements the
+ * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_COPY copy}
+ * operation for the Sling default POST servlet.
+ */
+public class CopyOperation extends AbstractCopyMoveOperation {
+
+ /**
+ * default log
+ */
+ private static final Logger log = LoggerFactory.getLogger(CopyOperation.class);
+
+ @Override
+ protected String getOperationName() {
+ return "copy";
+ }
+
+ @Override
+ protected void execute(HtmlResponse response, Item source,
+ String destParent, String destName) throws RepositoryException {
+
+ copy(source, (Node) source.getSession().getItem(destParent), destName);
+
+ String dest = destParent + "/" + destName;
+ response.onCopied(source.getPath(), dest);
+ log.debug("copy {} to {}", source, dest);
+ }
+
+ /**
+ * Copy the <code>src</code> item into the <code>dstParent</code> node.
+ * The name of the newly created item is set to <code>name</code>.
+ *
+ * @param src The item to copy to the new location
+ * @param dstParent The node into which the <code>src</code> node is to be
+ * copied
+ * @param name The name of the newly created item. If this is
+ * <code>null</code> the new item gets the same name as the
+ * <code>src</code> item.
+ * @throws RepositoryException May be thrown in case of any problem copying
+ * the content.
+ * @see #copy(Node, Node, String)
+ * @see #copy(Property, Node, String)
+ */
+ static void copy(Item src, Node dstParent, String name)
+ throws RepositoryException {
+ if (src.isNode()) {
+ copy((Node) src, dstParent, name);
+ } else {
+ copy((Property) src, dstParent, name);
+ }
+ }
+
+ /**
+ * Copy the <code>src</code> node into the <code>dstParent</code> node.
+ * The name of the newly created node is set to <code>name</code>.
+ * <p>
+ * This method does a recursive (deep) copy of the subtree rooted at the
+ * source node to the destination. Any protected child nodes and and
+ * properties are not copied.
+ *
+ * @param src The node to copy to the new location
+ * @param dstParent The node into which the <code>src</code> node is to be
+ * copied
+ * @param name The name of the newly created node. If this is
+ * <code>null</code> the new node gets the same name as the
+ * <code>src</code> node.
+ * @throws RepositoryException May be thrown in case of any problem copying
+ * the content.
+ */
+ static void copy(Node src, Node dstParent, String name)
+ throws RepositoryException {
+
+ // ensure destination name
+ if (name == null) {
+ name = src.getName();
+ }
+
+ // ensure new node creation
+ if (dstParent.hasNode(name)) {
+ dstParent.getNode(name).remove();
+ }
+
+ // create new node
+ Node dst = dstParent.addNode(name, src.getPrimaryNodeType().getName());
+ for (NodeType mix : src.getMixinNodeTypes()) {
+ dst.addMixin(mix.getName());
+ }
+
+ // copy the properties
+ for (PropertyIterator iter = src.getProperties(); iter.hasNext();) {
+ copy(iter.nextProperty(), dst, null);
+ }
+
+ // copy the child nodes
+ for (NodeIterator iter = src.getNodes(); iter.hasNext();) {
+ Node n = iter.nextNode();
+ if (!n.getDefinition().isProtected()) {
+ copy(n, dst, null);
+ }
+ }
+ }
+
+ /**
+ * Copy the <code>src</code> property into the <code>dstParent</code>
+ * node. The name of the newly created property is set to <code>name</code>.
+ * <p>
+ * If the source property is protected, this method does nothing.
+ *
+ * @param src The property to copy to the new location
+ * @param dstParent The node into which the <code>src</code> property is
+ * to be copied
+ * @param name The name of the newly created property. If this is
+ * <code>null</code> the new property gets the same name as the
+ * <code>src</code> property.
+ * @throws RepositoryException May be thrown in case of any problem copying
+ * the content.
+ */
+ static void copy(Property src, Node dstParent, String name)
+ throws RepositoryException {
+ if (!src.getDefinition().isProtected()) {
+ if (name == null) {
+ name = src.getName();
+ }
+
+ // ensure new property creation
+ if (dstParent.hasProperty(name)) {
+ dstParent.getProperty(name).remove();
+ }
+
+ if (src.getDefinition().isMultiple()) {
+ dstParent.setProperty(name, src.getValues());
+ } else {
+ dstParent.setProperty(name, src.getValue());
+ }
+ }
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/operations/DeleteOperation.java b/src/main/java/org/apache/sling/servlets/post/impl/operations/DeleteOperation.java
new file mode 100644
index 0000000..e688b9b
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/operations/DeleteOperation.java
@@ -0,0 +1,68 @@
+/*
+ * 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.Iterator;
+
+import javax.jcr.Item;
+import javax.jcr.RepositoryException;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceNotFoundException;
+import org.apache.sling.api.servlets.HtmlResponse;
+import org.apache.sling.servlets.post.AbstractSlingPostOperation;
+
+/**
+ * The <code>DeleteOperation</code> class implements the
+ * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_DELETE delete}
+ * operation for the Sling default POST servlet.
+ */
+public class DeleteOperation extends AbstractSlingPostOperation {
+
+ @Override
+ protected void doRun(SlingHttpServletRequest request, HtmlResponse response)
+ throws RepositoryException {
+
+ Iterator<Resource> res = getApplyToResources(request);
+ if (res == null) {
+
+ Resource resource = request.getResource();
+ Item item = resource.adaptTo(Item.class);
+ if (item == null) {
+ throw new ResourceNotFoundException("Missing source "
+ + resource + " for delete");
+ }
+
+ item.remove();
+ response.onDeleted(resource.getPath());
+
+ } else {
+
+ while (res.hasNext()) {
+ Resource resource = res.next();
+ Item item = resource.adaptTo(Item.class);
+ if (item != null) {
+ item.remove();
+ response.onDeleted(resource.getPath());
+ }
+ }
+
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/operations/ModifyOperation.java b/src/main/java/org/apache/sling/servlets/post/impl/operations/ModifyOperation.java
new file mode 100644
index 0000000..b727dcb
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/operations/ModifyOperation.java
@@ -0,0 +1,685 @@
+/*
+ * 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.HashMap;
+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.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+import org.apache.sling.api.SlingException;
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.request.RequestParameter;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.api.servlets.HtmlResponse;
+import org.apache.sling.servlets.post.AbstractSlingPostOperation;
+import org.apache.sling.servlets.post.SlingPostConstants;
+import org.apache.sling.servlets.post.impl.helper.DateParser;
+import org.apache.sling.servlets.post.impl.helper.NodeNameGenerator;
+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;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 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 AbstractSlingPostOperation {
+
+ /** default log */
+ private final Logger log = LoggerFactory.getLogger(getClass());
+
+ /**
+ * utility class for generating node names
+ */
+ private final NodeNameGenerator nodeNameGenerator;
+
+ private final DateParser dateParser;
+
+ /**
+ * handler that deals with file upload
+ */
+ private final SlingFileUploadHandler uploadHandler;
+
+ public ModifyOperation(NodeNameGenerator nodeNameGenerator,
+ DateParser dateParser, ServletContext servletContext) {
+ this.nodeNameGenerator = nodeNameGenerator;
+ this.dateParser = dateParser;
+ this.uploadHandler = new SlingFileUploadHandler(servletContext);
+ }
+
+ @Override
+ protected void doRun(SlingHttpServletRequest request, HtmlResponse response)
+ throws RepositoryException {
+
+ Map<String, RequestProperty> reqProperties = collectContent(request,
+ response);
+
+ // do not change order unless you have a very good reason.
+ Session session = request.getResourceResolver().adaptTo(Session.class);
+
+ // ensure root of new content
+ processCreate(session, reqProperties, response);
+
+ // write content from existing content (@Move/CopyFrom parameters)
+ processMoves(session, reqProperties, response);
+ processCopies(session, reqProperties, response);
+
+ // cleanup any old content (@Delete parameters)
+ processDeletes(session, reqProperties, response);
+
+ // write content from form
+ writeContent(session, reqProperties, response);
+
+ // order content
+ String path = response.getPath();
+ orderNode(request, session.getItem(path));
+ }
+
+ @Override
+ protected String getItemPath(SlingHttpServletRequest request) {
+
+ // calculate the paths
+ StringBuffer rootPathBuf = new StringBuffer();
+ 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();
+
+ // 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) {
+
+ // cut off any selectors/extension from the suffix
+ int dotPos = suffix.indexOf('.');
+ if (dotPos > 0) {
+ suffix = suffix.substring(0, dotPos);
+ }
+
+ // and 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;
+ }
+
+ private String generateName(SlingHttpServletRequest request, String basePath)
+ throws RepositoryException {
+
+ // If the path ends with a *, create a node under its parent, with
+ // a generated node name
+ basePath += "/"
+ + nodeNameGenerator.getNodeName(request.getRequestParameterMap(),
+ requireItemPathPrefix(request));
+
+ // if resulting path exists, add a suffix until it's not the case
+ // anymore
+ Session session = request.getResourceResolver().adaptTo(Session.class);
+
+ // if resulting path exists, add a suffix until it's not the case
+ // anymore
+ if (session.itemExists(basePath)) {
+ for (int idx = 0; idx < 1000; idx++) {
+ String newPath = basePath + "_" + idx;
+ if (!session.itemExists(newPath)) {
+ basePath = newPath;
+ break;
+ }
+ }
+ }
+
+ // if it still exists there are more than 1000 nodes ?
+ if (session.itemExists(basePath)) {
+ throw new RepositoryException(
+ "Collision in generated node names for path=" + basePath);
+ }
+
+ return basePath;
+ }
+
+ /**
+ * Create node(s) according to current request
+ *
+ * @throws RepositoryException if a repository error occurs
+ */
+ private void processCreate(Session session,
+ Map<String, RequestProperty> reqProperties, HtmlResponse response)
+ throws RepositoryException {
+
+ String path = response.getPath();
+ if (!session.itemExists(path)) {
+ deepGetOrCreateNode(session, path, reqProperties, response);
+ response.setCreateRequest(true);
+ }
+
+ }
+
+ /**
+ * Moves all repository content listed as repository move source in the
+ * request properties to the locations indicated by the resource properties.
+ */
+ private void processMoves(Session session,
+ Map<String, RequestProperty> reqProperties, HtmlResponse response)
+ throws RepositoryException {
+
+ for (RequestProperty property : reqProperties.values()) {
+ if (property.hasRepositoryMoveSource()) {
+ processMovesCopiesInternal(property, true, session,
+ reqProperties, response);
+ }
+ }
+ }
+
+ /**
+ * Copies all repository content listed as repository copy source in the
+ * request properties to the locations indicated by the resource properties.
+ */
+ private void processCopies(Session session,
+ Map<String, RequestProperty> reqProperties, HtmlResponse response)
+ throws RepositoryException {
+
+ for (RequestProperty property : reqProperties.values()) {
+ if (property.hasRepositoryCopySource()) {
+ processMovesCopiesInternal(property, false, session,
+ reqProperties, response);
+ }
+ }
+ }
+
+ /**
+ * 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, Session session,
+ Map<String, RequestProperty> reqProperties, HtmlResponse response)
+ throws RepositoryException {
+
+ 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)) {
+ session.getItem(propPath).remove();
+ response.onDeleted(propPath);
+ } else {
+ deepGetOrCreateNode(session, property.getParentPath(),
+ reqProperties, response);
+ }
+
+ // move through the session and record operation
+ Item sourceItem = session.getItem(source);
+ if (sourceItem.isNode()) {
+
+ // node move/copy through session
+ if (isMove) {
+ session.move(source, propPath);
+ } else {
+ Node sourceNode = (Node) sourceItem;
+ Node destParent = (Node) session.getItem(property.getParentPath());
+ CopyOperation.copy(sourceNode, destParent,
+ property.getName());
+ }
+
+ } else {
+
+ // property move manually
+ Property sourceProperty = (Property) sourceItem;
+
+ // create destination property
+ Node destParent = (Node) session.getItem(property.getParentPath());
+ CopyOperation.copy(sourceProperty, destParent, null);
+
+ // remove source property (if not just copying)
+ if (isMove) {
+ 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) {
+ response.onMoved(source, propPath);
+ } else {
+ response.onCopied(source, propPath);
+ }
+ }
+ }
+
+ /**
+ * Removes all properties listed as {@link RequestProperty#isDelete()} from
+ * the repository.
+ *
+ * @param session The <code>javax.jcr.Session</code> used to access the
+ * repository 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(Session session,
+ Map<String, RequestProperty> reqProperties, HtmlResponse response)
+ throws RepositoryException {
+
+ for (RequestProperty property : reqProperties.values()) {
+ if (property.isDelete()) {
+ String propPath = property.getPath();
+ if (session.itemExists(propPath)) {
+ session.getItem(propPath).remove();
+ response.onDeleted(propPath);
+ }
+ }
+ }
+
+ }
+
+ /**
+ * Writes back the content
+ *
+ * @throws RepositoryException if a repository error occurs
+ * @throws ServletException if an internal error occurs
+ */
+ private void writeContent(Session session,
+ Map<String, RequestProperty> reqProperties, HtmlResponse response)
+ throws RepositoryException {
+
+ SlingPropertyValueHandler propHandler = new SlingPropertyValueHandler(
+ dateParser, response);
+
+ for (RequestProperty prop : reqProperties.values()) {
+ if (prop.hasValues()) {
+ Node parent = deepGetOrCreateNode(session,
+ prop.getParentPath(), reqProperties, response);
+ // skip jcr special properties
+ if (prop.getName().equals("jcr:primaryType")
+ || prop.getName().equals("jcr:mixinTypes")) {
+ continue;
+ }
+ if (prop.isFileUpload()) {
+ uploadHandler.setFile(parent, prop, response);
+ } else {
+ propHandler.setProperty(parent, prop);
+ }
+ }
+ }
+ }
+
+ /**
+ * Collects the properties that form the content to be written back to the
+ * repository.
+ *
+ * @throws RepositoryException if a repository error occurs
+ * @throws ServletException if an internal error occurs
+ */
+ private Map<String, RequestProperty> collectContent(
+ SlingHttpServletRequest request, HtmlResponse response) {
+
+ boolean requireItemPrefix = requireItemPathPrefix(request);
+
+ // walk the request parameters and collect the properties
+ Map<String, RequestProperty> reqProperties = new HashMap<String, RequestProperty>();
+ for (Map.Entry<String, RequestParameter[]> e : request.getRequestParameterMap().entrySet()) {
+ final String paramName = e.getKey();
+
+ // do not store parameters with names starting with sling:post
+ if (paramName.startsWith(SlingPostConstants.RP_PREFIX)) {
+ continue;
+ }
+ // SLING-298: skip form encoding parameter
+ if (paramName.equals("_charset_")) {
+ continue;
+ }
+ // skip parameters that do not start with the save prefix
+ if (requireItemPrefix && !hasItemPathPrefix(paramName)) {
+ continue;
+ }
+
+ // ensure the paramName is an absolute property name
+ 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)) {
+ 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)) {
+ 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)) {
+ RequestProperty prop = getOrCreateRequestProperty(
+ reqProperties, propPath,
+ SlingPostConstants.VALUE_FROM_SUFFIX);
+
+ // @ValueFrom params must have exactly one value, else ignored
+ if (e.getValue().length == 1) {
+ String refName = e.getValue()[0].getString();
+ 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)) {
+ 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)) {
+ RequestProperty prop = getOrCreateRequestProperty(
+ reqProperties, propPath,
+ SlingPostConstants.SUFFIX_MOVE_FROM);
+
+ // @MoveFrom params must have exactly one value, else ignored
+ if (e.getValue().length == 1) {
+ 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)) {
+ RequestProperty prop = getOrCreateRequestProperty(
+ reqProperties, propPath,
+ SlingPostConstants.SUFFIX_COPY_FROM);
+
+ // @MoveFrom params must have exactly one value, else ignored
+ if (e.getValue().length == 1) {
+ String sourcePath = e.getValue()[0].getString();
+ prop.setRepositorySource(sourcePath, false);
+ }
+
+ continue;
+ }
+
+ // plain property, create from values
+ RequestProperty prop = getOrCreateRequestProperty(reqProperties,
+ propPath, null);
+ prop.setValues(e.getValue());
+ }
+
+ return reqProperties;
+ }
+
+ /**
+ * 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, HtmlResponse response) {
+ if (!paramName.startsWith("/")) {
+ paramName = 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;
+ }
+
+ /**
+ * 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 ? null : prop.getStringValues();
+ }
+
+ /**
+ * Deep gets or creates a node, parent-padding with default nodes nodes. If
+ * the path is empty, the given parent node is returned.
+ *
+ * @param path path to node that needs to be deep-created
+ * @return node at path
+ * @throws RepositoryException if an error occurs
+ * @throws IllegalArgumentException if the path is relative and parent is
+ * <code>null</code>
+ */
+ private Node deepGetOrCreateNode(Session session, String path,
+ Map<String, RequestProperty> reqProperties, HtmlResponse response)
+ throws RepositoryException {
+ if (log.isDebugEnabled()) {
+ log.debug("Deep-creating Node '{}'", path);
+ }
+ if (path == null || !path.startsWith("/")) {
+ throw new IllegalArgumentException("path must be an absolute path.");
+ }
+ // get the starting node
+ String startingNodePath = path;
+ Node startingNode = null;
+ while (startingNode == null) {
+ if (startingNodePath.equals("/")) {
+ startingNode = session.getRootNode();
+ } else if (session.itemExists(startingNodePath)) {
+ startingNode = (Node) session.getItem(startingNodePath);
+ } else {
+ int pos = startingNodePath.lastIndexOf('/');
+ if (pos > 0) {
+ startingNodePath = startingNodePath.substring(0, pos);
+ } else {
+ startingNodePath = "/";
+ }
+ }
+ }
+ // is the searched node already existing?
+ if (startingNodePath.length() == path.length()) {
+ return startingNode;
+ }
+ // create nodes
+ int from = (startingNodePath.length() == 1
+ ? 1
+ : startingNodePath.length() + 1);
+ Node node = startingNode;
+ while (from > 0) {
+ final int to = path.indexOf('/', from);
+ final String name = to < 0 ? path.substring(from) : path.substring(
+ from, to);
+ // although the node should not exist (according to the first test
+ // above)
+ // we do a sanety check.
+ if (node.hasNode(name)) {
+ node = node.getNode(name);
+ } else {
+ final String tmpPath = to < 0 ? path : path.substring(0, to);
+ // check for node type
+ final String nodeType = getPrimaryType(reqProperties, tmpPath);
+ if (nodeType != null) {
+ node = node.addNode(name, nodeType);
+ } else {
+ node = node.addNode(name);
+ }
+ // check for mixin types
+ final String[] mixinTypes = getMixinTypes(reqProperties,
+ tmpPath);
+ if (mixinTypes != null) {
+ for (String mix : mixinTypes) {
+ node.addMixin(mix);
+ }
+ }
+ response.onCreated(node.getPath());
+ }
+ from = to + 1;
+ }
+ return node;
+ }
+
+}
diff --git a/src/main/java/org/apache/sling/servlets/post/impl/operations/MoveOperation.java b/src/main/java/org/apache/sling/servlets/post/impl/operations/MoveOperation.java
new file mode 100644
index 0000000..83707e7
--- /dev/null
+++ b/src/main/java/org/apache/sling/servlets/post/impl/operations/MoveOperation.java
@@ -0,0 +1,57 @@
+/*
+ * 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 javax.jcr.Item;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+
+import org.apache.sling.api.servlets.HtmlResponse;
+
+/**
+ * The <code>MoveOperation</code> class implements the
+ * {@link org.apache.sling.servlets.post.SlingPostConstants#OPERATION_MOVE move}
+ * operation for the Sling default POST servlet.
+ */
+public class MoveOperation extends AbstractCopyMoveOperation {
+
+ @Override
+ protected String getOperationName() {
+ return "move";
+ }
+
+ @Override
+ protected void execute(HtmlResponse response, Item source,
+ String destParent, String destName) throws RepositoryException {
+
+ if (destName == null) {
+ destName = source.getName();
+ }
+
+ String sourcePath = source.getPath();
+ String destPath = destParent + "/" + destName;
+ Session session = source.getSession();
+
+ if (session.itemExists(destPath)) {
+ session.getItem(destPath).remove();
+ }
+
+ session.move(sourcePath, destPath);
+ response.onMoved(sourcePath, destPath);
+ }
+
+}
diff --git a/src/main/resources/META-INF/DISCLAIMER b/src/main/resources/META-INF/DISCLAIMER
new file mode 100644
index 0000000..90850c2
--- /dev/null
+++ b/src/main/resources/META-INF/DISCLAIMER
@@ -0,0 +1,7 @@
+Apache Sling is an effort undergoing incubation at The Apache Software Foundation (ASF),
+sponsored by the Apache Jackrabbit PMC. Incubation is required of all newly accepted
+projects until a further review indicates that the infrastructure, communications,
+and decision making process have stabilized in a manner consistent with other
+successful ASF projects. While incubation status is not necessarily a reflection of
+the completeness or stability of the code, it does indicate that the project has yet
+to be fully endorsed by the ASF.
\ No newline at end of file
diff --git a/src/main/resources/META-INF/LICENSE b/src/main/resources/META-INF/LICENSE
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/src/main/resources/META-INF/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ 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.
diff --git a/src/main/resources/META-INF/NOTICE b/src/main/resources/META-INF/NOTICE
new file mode 100644
index 0000000..d26b355
--- /dev/null
+++ b/src/main/resources/META-INF/NOTICE
@@ -0,0 +1,8 @@
+Apache Sling Default Post Servlets
+Copyright 2008 The Apache Software Foundation
+
+Apache Sling is based on source code originally developed
+by Day Software (http://www.day.com/).
+
+This product includes software developed at
+The Apache Software Foundation (http://www.apache.org/).
diff --git a/src/main/resources/OSGI-INF/metatype/metatype.properties b/src/main/resources/OSGI-INF/metatype/metatype.properties
new file mode 100644
index 0000000..1346cb6
--- /dev/null
+++ b/src/main/resources/OSGI-INF/metatype/metatype.properties
@@ -0,0 +1,46 @@
+#
+# 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.
+#
+
+
+#
+# This file contains localization strings for configuration labels and
+# descriptions as used in the metatype.xml descriptor generated by the
+# the Sling SCR plugin
+
+servlet.post.name = Sling POST Servlet
+servlet.post.description = The Sling POST Servlet is registered as the default \
+ servlet to handle POST requests in Sling.
+servlet.post.dateFormats.name = Date Format
+servlet.post.dateFormats.description = List SimpleDateFormat strings for date \
+ formats supported for parsing from request input to data fields. The default \
+ value is [ "EEE MMM dd yyyy HH:mm:ss 'GMT'Z", "yyyy-MM-dd'T'HH:mm:ss.SSSZ", \
+ "yyyy-MM-dd'T'HH:mm:ss", "yyyy-MM-dd", "dd.MM.yyyy HH:mm:ss", "dd.MM.yyyy" ].
+servlet.post.nodeNameHints.name = Node Name Hint Properties
+servlet.post.nodeNameHints.description = The list of properties whose values \
+ may be used to derive a name for newly created nodes. When handling a request \
+ to create a new node, the name of the node is automatically generated if the \
+ request URL ends with a star ("*") or a slash ("/"). In this case the request \
+ parameters listed in this configuration value may be used to create the name. \
+ Default value is [ "title", "jcr:title", "name", "description", \
+ "jcr:description", "abstract" ].
+servlet.post.nodeNameMaxLength.name = Maximum Node Name Length
+servlet.post.nodeNameMaxLength.description = Maximum number of characters to \
+ use for automatically generated node names. The default value is 20. Note, \
+ that actual node names may be generated with at most 4 more characters if the \
+ numeric suffixes must be appended to make the name unique.
\ No newline at end of file
diff --git a/src/main/resources/system/sling.js b/src/main/resources/system/sling.js
new file mode 100644
index 0000000..2ba7381
--- /dev/null
+++ b/src/main/resources/system/sling.js
@@ -0,0 +1,420 @@
+/*
+ * 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.
+ */
+
+/**
+ * The sling javascript client gives access to a JCR repository
+ * from client-side java code, using the sling post servlet as a back-end.
+ *
+ * @version $Rev: $, $Date: 2007-03-27 16:30:52 +0200 (Tue, 27 Mar 2007) $
+ */
+
+var Sling = null;
+
+// start sling code scope
+(function() {
+
+ Sling = new Object();
+ Sling.NAME_OF_THIS_FILE = "sling.js";
+
+ /** This method tries to figure out what to do with a page */
+ Sling.wizard = function() {
+ //TODO add lots of magic here
+ var form=document.getElementById("slingform");
+ if (!form) form=document.forms[0];
+ if (form) {
+ var sp=new Object();
+ sp.formElement=form;
+ Sling.setupPage(sp);
+ }
+
+ }
+ /** Call this to merge sling data in an HTML page
+ TODO deprecate other functions
+ */
+ Sling.setupPage = function(options) {
+ var tree = Sling.getContent(Sling._getJsonUrl(),1);
+
+ if(options.formElement) {
+ Sling._setFormValues(options.formElement,Sling._getJsonUrl(),tree);
+ }
+
+ if(options.displayElement) {
+ Sling.displayValues(options.displayElement,tree);
+ }
+ }
+
+ /**
+ * HTTP GET XHR Helper
+ * @param {String} url The URL
+ * @return the XHR object, use .responseText for the data
+ * @type String
+ */
+ Sling.httpGet = function(url) {
+ var httpcon = Sling.getXHR();
+ if (httpcon) {
+ httpcon.open('GET', url, false);
+ httpcon.send(null);
+ return httpcon;
+ } else {
+ return null;
+ }
+ }
+ /**
+ * Produces a "sort-of-json" string representation of a object
+ * for debugging purposes only
+ * @param {Object} obj The object
+ * @param {int} level The indentation level
+ * @return The result
+ * @type String
+ */
+ Sling.dumpObj = function(obj, level) {
+ var res="";
+ for (var a in obj) {
+ if (typeof(obj[a])!="object") {
+ res+=a+":"+obj[a]+" ";
+ } else {
+ res+=a+": { ";
+ res+=Sling.dumpObj(obj[a])+"} ";
+ }
+ }
+ return (res);
+ }
+
+ /** Produces an aggregate of get all the property names used
+ * in a tree as a helper for table oriented display
+ * @param {Object} obj The Content Tree object
+ * @param {Object} names internal object used for collecting all
+ * the names during the recursion
+ * @return An Array of names of properties that exist in a tree
+ * @type Array
+ */
+ Sling.getAllPropNames = function(obj, names) {
+ var root=false;
+ if (!names) {
+ names=new Object();
+ root=true;
+ }
+ for (var a in obj) {
+ if (typeof(obj[a])!="object") {
+ names[a]="1";
+ } else {
+ getAllPropNames(obj[a], names);
+ }
+ }
+ if (root) {
+ var ar=new Array();
+ var i=0;
+ for (var a in ar) {
+ ar[i]=a;
+ i++;
+ }
+ names=ar;
+ }
+ return (names);
+ }
+
+ /** Reads a tree of items given a maxlevel from the repository as JSON
+ * @param {String} path Path into the current workspace
+ * @param {int} maxlevel maximum depth to traverse to
+ * @param {Array} filters filter only these properties
+ * @return An Object tree of content nodes and properties, null if not found
+ * @type Object
+ */
+ Sling.getContent = function(path, maxlevels, filter) {
+ var obj=new Object();
+ if (!path) {
+ path=Sling.currentPath;
+ }
+ if (path.indexOf("/")==0) {
+ /*
+ this assumes that paths that start with a slash
+ are meant to be workspace paths rather than URLs
+ and therefore need some additions before they are sent
+ to the server
+ */
+ if(maxlevels == "0" || maxlevels) {
+ maxlevels = "." + maxlevels;
+ } else {
+ maxlevels = "";
+ }
+ path=Sling.baseurl + path + maxlevels + ".json";
+ }
+ //checking for a trailing "/*"
+ if (path.indexOf("/*")>=0) return obj;
+
+ // TODO for now we explicitely defeat caching on this...there must be a better way
+ // but in tests IE6 tends to cache too much
+ var passThroughCacheParam = "?clock=" + new Date().getTime();
+ var res=Sling.httpGet(path + passThroughCacheParam + (maxlevels?"&maxlevels="+maxlevels:""));
+
+ if(res.status == 200) {
+ var obj=Sling.evalString(res.responseText);
+ if (!filter) {
+ for (var a in obj) {
+ if (a.indexOf("jcr:")==0) delete(obj[a]);
+ }
+ }
+ return obj;
+ }
+ return null;
+ }
+
+ /** Remove content by path */
+ Sling.removeContent = function(path) {
+ var httpcon = Sling.getXHR();
+ if (httpcon) {
+ var params = ":delete="+path;
+ httpcon.open('POST', Sling.baseurl + path, false);
+
+ // Send the proper header information along with the request
+ httpcon.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
+ httpcon.setRequestHeader("Content-length", params.length);
+ httpcon.setRequestHeader("Connection", "close");
+ httpcon.send(params);
+ return httpcon;
+ } else {
+ return false;
+ }
+ }
+
+ /** eval str, accepting various object delimiters */
+ Sling.evalString = function(str) {
+ var obj = null;
+ if(str.indexOf('[')==0) {
+ eval("obj="+str);
+ } else if(str.indexOf('{')==0) {
+ eval("obj="+str);
+ } else {
+ eval("obj={"+str+"}");
+ }
+ return obj;
+ }
+
+ /** Get "session info" from repository. Mainly answers the question: "Who am I"
+ * and "Which workspace am I logged into.
+ * @return An Object tree containing the session information, null if server status <> 200
+ * @type Object
+ */
+ Sling.getSessionInfo = function() {
+ var res=Sling.httpGet(Sling.baseurl+"/system/sling/info.sessionInfo.json");
+ if(res.status == 200) {
+ return Sling.evalString(res.responseText);
+ }
+ return null;
+ }
+
+ /** Replace extension in a path */
+ Sling._replaceExtension = function(path,newExtension) {
+ var i = path.lastIndexOf(".");
+ if(i >= 0) path = path.substring(0,i);
+ i = path.lastIndexOf(".");
+ if(i >= 0) path = path.substring(0,i);
+ return path + newExtension;
+ }
+
+ /** Get the JSON data URL that for the current page
+ * (assuming a .extension for the current page, .html or something else)
+ */
+ Sling._getJsonUrl = function() {
+ return Sling._replaceExtension(window.location.href,".json");
+ }
+
+ /** Get the content repository path from the URL
+ * (assuming a .extension for the current page, .html or something else)
+ */
+ Sling._getPath = function() {
+
+ var noextensions=Sling._replaceExtension(window.location.href,"");
+ var path=noextensions.substring(Sling.baseurl.length);
+ return (path);
+ }
+
+ /** Display values inside a container: an element inside given container,
+ * with an id like ./stuff, has its innerHTML set to the value of stuff
+ * in the tree, if it exists.
+ */
+ Sling.displayValues = function(container,tree) {
+ if(!tree) {
+ tree = Sling.getContent(Sling._getJsonUrl(),1);
+ }
+
+ var elements = container.getElementsByTagName("*");
+ var toSet = new Array();
+ for (var i = 0; i < elements.length; i++) {
+ var value = Sling._getElementValue(elements[i],tree);
+ if(value) {
+ toSet[toSet.length] = { e:elements[i], v:value };
+ }
+ }
+
+ for(var i = 0; i < toSet.length; i++) {
+ toSet[i].e.innerHTML = toSet[i].v;
+ }
+ }
+
+ /** If e has an ID that matches a property of tree, set e's innerHTML accordingly */
+ Sling._getElementValue = function(e,tree) {
+ var id = e.getAttribute("id");
+ if(id) {
+ return tree[id.substring(2)];
+ }
+ }
+
+
+ /** Set form elements based on the tree of items passed into the method
+ * @param {IdOrElement} form the Form element to set, or its id
+ * @param {String} path passes a string specifying the path
+ * @param {Object} tree optionally pass the content that you want the
+ * form to be populated with. This assumes an item tree as returned by
+ * getContent().
+ * Returns an object indicating whether data was found on the server.
+ *
+ */
+ Sling._setFormValues = function(form, path, tree) {
+ var result = new Object();
+
+ /** TODO: deal with abolute paths?
+ * TODO: deal with @ValueFrom
+ */
+ if (!path) return;
+
+ form.setAttribute("action", path);
+
+ if (!tree) {
+ tree=Sling.getContent(path,1);
+ }
+
+ var elems=form.elements;
+ var i=0;
+ formfieldprefix="";
+
+ while (elems.length > i) {
+ var elem=elems[i];
+ var a=elem.name;
+ if (a.indexOf("./")==0) {
+ formfieldprefix="./";
+ break;
+ }
+ i++;
+ }
+
+ var i=0;
+ while (elems.length > i) {
+ var elem=elems[i];
+ var a=elem.name;
+
+ if (a.indexOf("/")==0) {
+ var nodepath=a.substring(0,a.lastIndexOf("/"));
+ var propname=a.substring(a.lastIndexOf("/")+1);
+ var node=Sling.getContent(nodepath);
+ var propval=node[propname];
+ } else if (a.indexOf(formfieldprefix)==0) {
+ var propname=a.substring(formfieldprefix.length);
+ var propval=tree[propname];
+ }
+
+ if (propval) {
+ if (elem.type == "file") {
+ // cannot edit uplodaded files for now
+ } else if (elem.type == "checkbox") {
+ var vals;
+ if (typeof(propval)=="object") vals=propval;
+ else {
+ vals=new Array();
+ vals[0]=propval;
+ }
+ var j=0;
+ while (vals.length > j) {
+ if (vals[j] == elem.value) elem.checked=true;
+ j++;
+ }
+ } else {
+ elem.value=propval;
+ }
+ }
+ i++;
+ }
+
+ }
+
+ /** return Path as specified as the URL Parameter
+ * @param URL
+ * @return The Path parameter isolated from the URL
+ * @type String
+ */
+ Sling.TODO_NOT_USED_isolatePathFromUrl = function(url) {
+ var pattern = "[\\?&]Path=([^&#]*)";
+ var regex = new RegExp( pattern );
+ var results = regex.exec( url );
+ if( results == null )
+ // none found
+ return "";
+ else
+ // found
+ return unescape(results[1]);
+ }
+
+ /**
+ * Get an XMLHttpRequest in a portable way
+ *
+ */
+ Sling.getXHR = function () {
+ var xhr=null;
+
+ if(!xhr) {
+ try {
+ // built-in (firefox, recent Opera versions, etc)
+ xhr=new XMLHttpRequest();
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ if(!xhr) {
+ try {
+ // IE, newer versions
+ xhr=new ActiveXObject("Msxml2.XMLHTTP");
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ if(!xhr) {
+ try {
+ // IE, older versions
+ xhr=new ActiveXObject("Microsoft.XMLHTTP");
+ } catch (e) {
+ // ignore
+ }
+ }
+
+ if(!xhr) {
+ alert("Unable to access XMLHttpRequest object, sling will not work!");
+ }
+
+ return xhr;
+ }
+
+ // obtain the base_url to communicate with sling on the server
+ var scripts = document.getElementsByTagName("SCRIPT")
+ var script = scripts[scripts.length-1].src
+ Sling.baseurl = script.substring(0,script.length - ("/" + Sling.NAME_OF_THIS_FILE.length));
+ Sling.currentPath = Sling._getPath();
+ Sling.isNew = (Sling.currentPath.indexOf("/*")>=0)?true:false;
+
+// end sling code scope
+})();
diff --git a/src/test/java/org/apache/sling/servlets/post/impl/NodeNameFilterTest.java b/src/test/java/org/apache/sling/servlets/post/impl/NodeNameFilterTest.java
new file mode 100644
index 0000000..8039ca4
--- /dev/null
+++ b/src/test/java/org/apache/sling/servlets/post/impl/NodeNameFilterTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import junit.framework.TestCase;
+
+import org.apache.sling.servlets.post.impl.helper.NodeNameFilter;
+
+public class NodeNameFilterTest extends TestCase {
+ private final NodeNameFilter filter = new NodeNameFilter();
+
+ protected void runTest(String [] data) {
+ for(int i=0; i < data.length; i++) {
+ final String input = data[i];
+ i++;
+ final String expected = data[i];
+ final String actual = filter.filter(input);
+ assertEquals(expected, actual);
+ }
+ }
+
+ public void testBasicFiltering() {
+ final String [] data = {
+ "test", "test",
+ "t?st", "t_st",
+ "t??st", "t_st"
+ };
+
+ runTest(data);
+ }
+
+ public void testNoInitialNumber() {
+ final String [] data = {
+ "1234", "_1234",
+ "1", "_1"
+ };
+
+ runTest(data);
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/servlets/post/impl/SlingPostServletTest.java b/src/test/java/org/apache/sling/servlets/post/impl/SlingPostServletTest.java
new file mode 100644
index 0000000..3430c12
--- /dev/null
+++ b/src/test/java/org/apache/sling/servlets/post/impl/SlingPostServletTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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;
+
+import junit.framework.TestCase;
+
+import org.apache.sling.commons.testing.sling.MockSlingHttpServletRequest;
+import org.apache.sling.servlets.post.SlingPostConstants;
+
+public class SlingPostServletTest extends TestCase {
+
+ public void testIsSetStatus() {
+ StatusParamSlingHttpServletRequest req = new StatusParamSlingHttpServletRequest();
+ SlingPostServlet servlet = new SlingPostServlet();
+
+ // 1. null parameter, expect true
+ req.setStatusParam(null);
+ assertTrue("Standard status expected for null param",
+ servlet.isSetStatus(req));
+
+ // 2. "standard" parameter, expect true
+ req.setStatusParam(SlingPostConstants.STATUS_VALUE_STANDARD);
+ assertTrue("Standard status expected for '"
+ + SlingPostConstants.STATUS_VALUE_STANDARD + "' param",
+ servlet.isSetStatus(req));
+
+ // 3. "browser" parameter, expect false
+ req.setStatusParam(SlingPostConstants.STATUS_VALUE_BROWSER);
+ assertFalse("Browser status expected for '"
+ + SlingPostConstants.STATUS_VALUE_BROWSER + "' param",
+ servlet.isSetStatus(req));
+
+ // 4. any parameter, expect true
+ String param = "knocking on heaven's door";
+ req.setStatusParam(param);
+ assertTrue("Standard status expected for '" + param + "' param",
+ servlet.isSetStatus(req));
+ }
+
+ private static class StatusParamSlingHttpServletRequest extends
+ MockSlingHttpServletRequest {
+
+ private String statusParam;
+
+ public StatusParamSlingHttpServletRequest() {
+ // nothing to setup, we don't care
+ super(null, null, null, null, null);
+ }
+
+ @Override
+ public String getParameter(String name) {
+ if (SlingPostConstants.RP_STATUS.equals(name)) {
+ return statusParam;
+ }
+
+ return super.getParameter(name);
+ }
+
+ void setStatusParam(String statusParam) {
+ this.statusParam = statusParam;
+ }
+ }
+}