incubator-sling-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From fmesc...@apache.org
Subject svn commit: r618397 - in /incubator/sling/trunk/launchpad/launchpad-servlets/src/main: java/org/apache/sling/ujax/ resources/org/ resources/org/apache/ resources/org/apache/sling/ resources/org/apache/sling/ujax/
Date Mon, 04 Feb 2008 19:43:49 GMT
Author: fmeschbe
Date: Mon Feb  4 11:43:47 2008
New Revision: 618397

URL: http://svn.apache.org/viewvc?rev=618397&view=rev
Log:
SLING-213 Rewrite of ujax POST support:
   - Default response is OK (200) with HTML response and elements with well-known
     ID tags
   - modified ujax:redirect support.

Added:
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/Change.java
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/ChangeLog.java
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxHtmlResponse.java
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostProcessor.java
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPropertyValueHandler.java
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/resources/org/
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/resources/org/apache/
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/resources/org/apache/sling/
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/resources/org/apache/sling/ujax/
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/resources/org/apache/sling/ujax/UjaxHtmlResponse.html
Removed:
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPropertyValueSetter.java
Modified:
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/NodeNameGenerator.java
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/RequestProperty.java
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxFileUploadHandler.java
    incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostServlet.java

Added: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/Change.java
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/Change.java?rev=618397&view=auto
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/Change.java (added)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/Change.java Mon Feb  4 11:43:47 2008
@@ -0,0 +1,86 @@
+/*
+ * 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.ujax;
+
+/**
+ * Records a change that is used by the changelog
+ */
+public class Change {
+
+    /**
+     * available change types
+     */
+    public enum Type {
+        CREATED,
+        MODIFIED,
+        DELETED,
+        MOVED
+    }
+
+    /**
+     * type of the change
+     */
+    private final Type type;
+
+    /**
+     * arguments
+     */
+    private final String[] arguments;
+
+    /**
+     * Creates a new change with the given type and arguments
+     * @param type change type
+     * @param arguments arguments of the change
+     */
+    public Change(Type type, String ... arguments) {
+        this.type = type;
+        this.arguments = arguments;
+    }
+
+    /**
+     * Returns the type
+     * @return the type
+     */
+    public Type getType() {
+        return type;
+    }
+
+    /**
+     * Returns the arguments
+     * @return the arguments
+     */
+    public String[] getArguments() {
+        return arguments;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public String toString() {
+        StringBuffer ret = new StringBuffer(type.name().toLowerCase());
+        String delim = "(";
+        for (String a: arguments) {
+            ret.append(delim);
+            ret.append('\"');
+            ret.append(a);
+            ret.append('\"');
+            delim = ", ";
+        }
+        ret.append(");");
+        return ret.toString();
+    }
+}
\ No newline at end of file

Added: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/ChangeLog.java
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/ChangeLog.java?rev=618397&view=auto
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/ChangeLog.java (added)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/ChangeLog.java Mon Feb  4 11:43:47 2008
@@ -0,0 +1,83 @@
+/*
+ * 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.ujax;
+
+import java.util.List;
+import java.util.LinkedList;
+
+/**
+ * Implements a log that records the changes made to the repository during
+ * a ujax post request. It can be used to generate a status response.
+ */
+public class ChangeLog {
+
+    /**
+     * list of changes
+     */
+    private final List<Change> changes = new LinkedList<Change>();
+
+    /**
+     * Records a 'modified' change
+     * @param path path of the item that was modified
+     */
+    public void onModified(String path) {
+        changes.add(new Change(Change.Type.MODIFIED, path));
+    }
+
+    /**
+     * Records a 'created' change
+     * @param path path of the item that was created
+     */
+    public void onCreated(String path) {
+        changes.add(new Change(Change.Type.CREATED, path));
+    }
+
+    /**
+     * Records a 'deleted' change
+     * @param path path of the item that was deleted
+     */
+    public void onDeleted(String path) {
+        if (path != null) {
+            changes.add(new Change(Change.Type.DELETED, path));
+        }
+    }
+
+    /**
+     * Records a 'moved' change.
+     * <p/>
+     * Note: the moved change only records the basic move command. the implied
+     * changes on the moved properties and sub nodes are not recorded.
+     *
+     * @param srcPath source path of the node that was moved
+     * @param dstPath destination path of the node that was moved.
+     */
+    public void onMoved(String srcPath, String dstPath) {
+        changes.add(new Change(Change.Type.MOVED, srcPath, dstPath));
+    }
+
+    /**
+     * Dumps the changelog to the given buffer
+     * @param out the string buffer
+     * @param lf linefeed string. eg. <br/> or "\n"
+     */
+    public void dump(StringBuffer out, String lf) {
+        for (Change c: changes) {
+            out.append(c.toString());
+            out.append(lf);
+        }
+    }
+}
\ No newline at end of file

Modified: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/NodeNameGenerator.java
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/NodeNameGenerator.java?rev=618397&r1=618396&r2=618397&view=diff
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/NodeNameGenerator.java (original)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/NodeNameGenerator.java Mon Feb  4 11:43:47 2008
@@ -50,16 +50,19 @@
         
     }
     
-    /** Get a "nice" node name, if possible, based on given request 
-     *  @param prefix if provided, added in front of our parameterNames
-     *  when looking for request parameters
+    /**
+     * Get a "nice" node name, if possible, based on given request
+     *
+     * @param parameters the request parameters
+     * @param prefix if provided, added in front of our parameterNames
+     *        when looking for request parameters
+     * @return a nice node name
      */
     public String getNodeName(RequestParameterMap parameters, String prefix) {
-        String result = null;
-        if(prefix==null) {
+        if (prefix==null) {
             prefix = "";
         }
-        
+
         // find the first request parameter that matches one of
         // our parameterNames, in order, and has a value
         String valueToUse = null;
@@ -68,7 +71,6 @@
                 if(valueToUse != null) {
                     break;
                 }
-                
                 final RequestParameter[] pp = parameters.get(prefix + param);
                 if(pp!=null) {
                     for(RequestParameter p : pp) {
@@ -82,15 +84,13 @@
                 }
             }
         }
-        
-        // default value if none provided
-        if(result==null) {
-            result = (++counter) + "_" + System.currentTimeMillis();
-        }
-        
-        // filter value so that it works as a node name
+        String result;
         if(valueToUse != null) {
+            // filter value so that it works as a node name
             result = filter.filter(valueToUse);
+        } else {
+            // default value if none provided
+            result = nextCounter() + "_" + System.currentTimeMillis();
         }
 
         // max length
@@ -107,5 +107,9 @@
 
     public void setMaxLength(int maxLength) {
         this.maxLength = maxLength;
+    }
+
+    public synchronized int nextCounter() {
+        return ++counter;
     }
 }

Modified: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/RequestProperty.java
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/RequestProperty.java?rev=618397&r1=618396&r2=618397&view=diff
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/RequestProperty.java (original)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/RequestProperty.java Mon Feb  4 11:43:47 2008
@@ -16,7 +16,6 @@
  */
 package org.apache.sling.ujax;
 
-import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.request.RequestParameter;
 
 /**
@@ -33,7 +32,7 @@
 
     private final String typeHint;
 
-    private final String keyName;
+    private final String relPath;
 
     private final String propName;
 
@@ -43,34 +42,31 @@
 
     private RequestParameter[] defaultValues;
 
-    public RequestProperty(SlingHttpServletRequest req, String savePrefix,
-                           String keyName, RequestParameter[] values) {
-        this.keyName = keyName;
+    public RequestProperty(UjaxPostProcessor ctx, String relPath,
+                           RequestParameter[] values) {
+        this.relPath = relPath;
         this.values = values;
-        if (savePrefix == null) {
-            savePrefix = "";
-        }
 
         // split the relative path identifying the property to be saved
-        if (keyName.indexOf("/")>=0) {
-            parentPath = keyName.substring(0, keyName.lastIndexOf("/"));
-            propName = keyName.substring(keyName.lastIndexOf("/") + 1);
+        if (this.relPath.indexOf("/")>=0) {
+            parentPath = this.relPath.substring(0, this.relPath.lastIndexOf("/"));
+            propName = this.relPath.substring(this.relPath.lastIndexOf("/") + 1);
         } else {
             parentPath = "";
-            propName = keyName;
+            propName = this.relPath;
         }
 
         // @TypeHint example
         // <input type="text" name="./age" />
         // <input type="hidden" name="./age@TypeHint" value="long" />
         // causes the setProperty using the 'long' property type
-        final String thName = savePrefix + keyName + UjaxPostServlet.TYPE_HINT_SUFFIX;
-        final RequestParameter rp = req.getRequestParameter(thName);
+        final String thName = ctx.getSavePrefix() + this.relPath + UjaxPostServlet.TYPE_HINT_SUFFIX;
+        final RequestParameter rp = ctx.getRequest().getRequestParameter(thName);
         typeHint = rp == null ? null : rp.getString();
 
         // @DefaultValue
-        final String dvName = savePrefix + keyName + UjaxPostServlet.DEFAULT_VALUE_SUFFIX;
-        defaultValues = req.getRequestParameters(dvName);
+        final String dvName = ctx.getSavePrefix() + this.relPath + UjaxPostServlet.DEFAULT_VALUE_SUFFIX;
+        defaultValues = ctx.getRequest().getRequestParameters(dvName);
         if (defaultValues == null) {
             defaultValues = EMPTY_PARAM_ARRAY;
         }
@@ -81,8 +77,8 @@
         return typeHint;
     }
 
-    public String getKeyName() {
-        return keyName;
+    public String getRelPath() {
+        return relPath;
     }
 
     public String getName() {

Modified: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxFileUploadHandler.java
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxFileUploadHandler.java?rev=618397&r1=618396&r2=618397&view=diff
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxFileUploadHandler.java (original)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxFileUploadHandler.java Mon Feb  4 11:43:47 2008
@@ -16,18 +16,17 @@
  */
 package org.apache.sling.ujax;
 
+import org.apache.jackrabbit.util.Text;
 import org.apache.sling.api.request.RequestParameter;
-import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.commons.mime.MimeTypeService;
-import org.apache.jackrabbit.util.Text;
 
-import java.util.Calendar;
 import java.io.IOException;
+import java.util.Calendar;
 
 import javax.jcr.Node;
 import javax.jcr.RepositoryException;
-import javax.jcr.nodetype.NodeTypeManager;
 import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.NodeTypeManager;
 
 /**
  * Handles file uploads.
@@ -89,17 +88,29 @@
     public static final String JCR_DATA = "jcr:data";
 
     /**
+     * the post processor
+     */
+    private final UjaxPostProcessor ctx;
+
+    /**
+     * Constructs file upload handler
+     * @param ctx the post processor
+     */
+    public UjaxFileUploadHandler(UjaxPostProcessor ctx) {
+        this.ctx = ctx;
+    }
+
+    /**
      * 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 request the servlet request
      * @param parent the parent node
      * @param prop the assembled property info
      * @throws RepositoryException if an error occurs
      */
-    void setFile(SlingHttpServletRequest request, Node parent, RequestProperty prop)
+    void setFile(Node parent, RequestProperty prop)
             throws RepositoryException {
         RequestParameter value = prop.getValues()[0];
         assert !value.isFormField();
@@ -149,12 +160,14 @@
         if (createNtFile) {
             // create nt:file
             parent = parent.addNode(name, typeHint);
+            ctx.getChangeLog().onCreated(parent.getPath());
             name = JCR_CONTENT;
             typeHint = NT_RESOURCE;
         }
         
         // create resource node
         Node res = parent.addNode(name, typeHint);
+        ctx.getChangeLog().onCreated(res.getPath());
 
         // get content type
         String contentType = value.getContentType();
@@ -166,7 +179,7 @@
         }
         if (contentType == null || contentType.equals("application/octet-stream")) {
             // try to find a better content type
-            MimeTypeService svc = request.getServiceLocator().getService(MimeTypeService.class);
+            MimeTypeService svc = ctx.getRequest().getServiceLocator().getService(MimeTypeService.class);
             if (svc != null) {
                 contentType = svc.getMimeType(value.getFileName());
             }
@@ -176,10 +189,16 @@
         }
 
         // set properties
-        res.setProperty(JCR_LASTMODIFIED, Calendar.getInstance());
-        res.setProperty(JCR_MIMETYPE, contentType);
+        ctx.getChangeLog().onModified(
+            res.setProperty(JCR_LASTMODIFIED, Calendar.getInstance()).getPath()
+        );
+        ctx.getChangeLog().onModified(
+            res.setProperty(JCR_MIMETYPE, contentType).getPath()
+        );
         try {
-            res.setProperty(JCR_DATA, value.getInputStream());
+            ctx.getChangeLog().onModified(
+                res.setProperty(JCR_DATA, value.getInputStream()).getPath()
+            );
         } catch (IOException e) {
             throw new RepositoryException("Error while retrieving inputstream from parameter value.", e);
         }

Added: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxHtmlResponse.java
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxHtmlResponse.java?rev=618397&view=auto
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxHtmlResponse.java (added)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxHtmlResponse.java Mon Feb  4 11:43:47 2008
@@ -0,0 +1,231 @@
+/*
+ * 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.ujax;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ * Generator for a HTML status response that displays the changes made in a
+ * ujax post request.
+ *
+ * see <a href="UjaxHtmlResponse.html">UjaxHtmlResponse.html</a> for the format.
+ */
+public class UjaxHtmlResponse {
+
+    /**
+     * some human readable title like: 200 Created /foo/bar
+     */
+    public static final String PN_TITLE = "title";
+
+    /**
+     * status code. more or less http response status codes
+     */
+    public static final String PN_STATUS_CODE = "status.code";
+
+    /**
+     * some human readable status message
+     */
+    public static final String PN_STATUS_MESSAGE = "status.message";
+
+    /**
+     * externaly mapped location url of the modified path
+     */
+    public static final String PN_LOCATION = "location";
+
+    /**
+     * externaly mapped location url of the parent of the modified path
+     */
+    public static final String PN_PARENT_LOCATION = "parentLocation";
+
+    /**
+     * the path of the modified item. this is usually the addressed resource
+     * or in case of a creation request (eg: /foo/*) the path of the newly
+     * created node.
+     */
+    public static final String PN_PATH = "path";
+
+    /**
+     * the referrer of the request
+     */
+    public static final String PN_REFERER = "referer";
+
+    /**
+     * human readable changelog
+     */
+    public static final String PN_CHANGE_LOG = "changeLog";
+
+    /**
+     * name of the html template
+     */
+    private static final String TEMPLATE_NAME = "UjaxHtmlResponse.html";
+
+    /**
+     * Properties of the response
+     */
+    private final Map<String, String> properties = new HashMap<String, String>();
+
+    /**
+     * the post processor that contains the change log and all infos
+     */
+    private final UjaxPostProcessor ctx;
+
+    /**
+     * the servlet response
+     */
+    private final HttpServletResponse response;
+
+    /**
+     * Creates a new ujax html response
+     * @param ctx the request processor
+     * @param response the response
+     */
+    public UjaxHtmlResponse(UjaxPostProcessor ctx, HttpServletResponse response) {
+        this.ctx = ctx;
+        this.response = response;
+        prepare();
+    }
+
+    /**
+     * prepares the response properties
+     */
+    private void prepare() {
+        String path = ctx.getCurrentPath();
+        if (path == null) {
+            path = ctx.getRootPath();
+        }
+        setProperty(PN_PATH, path);
+
+        if (ctx.getError() != null) {
+            setStatus(500, ctx.getError().toString());
+            setTitle("Error while processing " + path);
+        } else {
+            if (ctx.isCreateRequest()) {
+                setStatus(201, "Created");
+                setTitle("Content created " + path);
+            } else {
+                setStatus(200, "OK");
+                setTitle("Content modified " + path);
+            }
+        }
+        setProperty(PN_LOCATION, ctx.getLocation());
+        setProperty(PN_PARENT_LOCATION, ctx.getParentLocation());
+        String referer = ctx.getRequest().getHeader("referer");
+        if (referer == null) {
+            referer = "";
+        }
+        setProperty(PN_REFERER, referer);
+        // get changelog
+        StringBuffer cl = new StringBuffer("<pre>");
+        ctx.getChangeLog().dump(cl, "<br/>");
+        cl.append("</pre>");
+        setProperty(PN_CHANGE_LOG, cl.toString());
+    }
+
+    /**
+     * sets the response status code properties
+     * @param code the code
+     * @param message the message
+     */
+    public void setStatus(int code, String message) {
+        setProperty(PN_STATUS_CODE, String.valueOf(code));
+        setProperty(PN_STATUS_MESSAGE, message);
+    }
+
+    /**
+     * Sets the title of the respose message
+     * @param title the title
+     */
+    public void setTitle(String title) {
+        setProperty(PN_TITLE, title);
+    }
+
+    /**
+     * Sets a generic response property with the given
+     * @param name name of the property
+     * @param value value of the property
+     */
+    public void setProperty(String name, String value) {
+        properties.put(name, value);
+    }
+
+    /**
+     * Writes the response to the given writer and replaces all ${var} patterns
+     * by the value of the respective property. if the property is not defined
+     * the pattern is not modified.
+     *
+     * @throws IOException if an i/o exception occurs
+     */
+    public void send() throws IOException {
+        response.setContentType("text/html");
+        Writer out = response.getWriter();
+        InputStream template = getClass().getResourceAsStream(TEMPLATE_NAME);
+        Reader in = new BufferedReader(new InputStreamReader(template));
+        StringBuffer varBuffer = new StringBuffer();
+        int state = 0;
+        int read;
+        while ((read = in.read()) >= 0) {
+            char c = (char) read;
+            switch (state) {
+                // initial
+                case 0:
+                    if (c=='$') {
+                        state = 1;
+                    } else {
+                        out.write(c);
+                    }
+                    break;
+                // $ read
+                case 1:
+                    if (c=='{') {
+                        state = 2;
+                    } else {
+                        state = 0;
+                        out.write('$');
+                        out.write(c);
+                    }
+                    break;
+                // { read
+                case 2:
+                    if (c=='}') {
+                        state = 0;
+                        String prop = properties.get(varBuffer.toString());
+                        if (prop == null) {
+                            out.write("${");
+                            out.write(varBuffer.toString());
+                            out.write("}");
+                        } else {
+                            out.write(prop);
+                        }
+                        varBuffer.setLength(0);
+                    } else {
+                        varBuffer.append(c);
+                    }
+            }
+        }
+        in.close();
+        out.flush();
+    }
+}
\ No newline at end of file

Added: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostProcessor.java
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostProcessor.java?rev=618397&view=auto
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostProcessor.java (added)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostProcessor.java Mon Feb  4 11:43:47 2008
@@ -0,0 +1,553 @@
+/*
+ * 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.ujax;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.resource.NonExistingResource;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.wrappers.SlingRequestPaths;
+import org.apache.sling.api.request.RequestParameter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Map;
+
+import javax.jcr.Session;
+import javax.jcr.RepositoryException;
+import javax.jcr.Node;
+import javax.servlet.ServletException;
+import javax.swing.RootPaneContainer;
+
+/**
+ * Holds various states and encapsulates method that are neede to handle a
+ * ujax post request.
+ */
+public class UjaxPostProcessor {
+
+    /**
+     * default log
+     */
+    private static final Logger log = LoggerFactory.getLogger(UjaxPostProcessor.class);
+
+
+    /**
+     * log that records the changes applied during the processing of the
+     * post request.
+     */
+    private final ChangeLog changeLog = new ChangeLog();
+
+    /**
+     * handler that deals with properties
+     */
+    private final UjaxPropertyValueHandler propHandler;
+
+    /**
+     * handler that deals with file upload
+     */
+    private final UjaxFileUploadHandler uploadHandler;
+
+    /**
+     * utility class for generating node names
+     */
+    private final NodeNameGenerator nodeNameGenerator;
+
+    /**
+     * the sling http servlet request
+     */
+    private final SlingHttpServletRequest request;
+
+    /**
+     * the jcr session to operate on
+     */
+    private final Session session;
+
+    /**
+     * the root path of this processor.
+     */
+    private final String rootPath;
+
+    /**
+     * path of the node that was targeted or created.
+     */
+    private String currentPath;
+
+    /**
+     * prefix of which the names of request properties must start with
+     * in order to be regardes as input values.
+     */
+    private String savePrefix;
+
+    /**
+     * indicates if the request contains a 'star suffix'
+     */
+    private boolean isCreateRequest;
+
+    /**
+     * records any error
+     */
+    private Exception error;
+
+    /**
+     * Creates a new post processor
+     * @param request the request to operate on
+     * @param session jcr session to operate on
+     * @param nodeNameGenerator the node name generator. use a servlet scoped one,
+     *        so that it can hold states.
+     */
+    public UjaxPostProcessor(SlingHttpServletRequest request, Session session,
+                             NodeNameGenerator nodeNameGenerator) {
+        this.request = request;
+        this.session = session;
+
+        // default to non-creating request (trailing DEFAULT_CREATE_SUFFIX)
+        isCreateRequest = false;
+        
+        // calculate the paths
+        StringBuffer rootPathBuf = new StringBuffer();
+        String suffix;
+        Resource currentResource = request.getResource();
+        if (Resource.RESOURCE_TYPE_NON_EXISTING.equals(currentResource.getResourceType())) {
+            
+            // 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
+        if (suffix != null) {
+            
+            // cut off any selectors/extension from the suffix
+            int dotPos = suffix.indexOf('.');
+            if (dotPos > 0) {
+                suffix = suffix.substring(0, dotPos);
+                
+            // otherwise check whether it is a create request (trailing /*)
+            } else if (suffix.endsWith(UjaxPostServlet.DEFAULT_CREATE_SUFFIX)) {
+                suffix = suffix.substring(0, suffix.length()
+                    - UjaxPostServlet.DEFAULT_CREATE_SUFFIX.length());
+                isCreateRequest = true;
+            }
+
+            // append the remains of the suffix to the path buffer
+            rootPathBuf.append(suffix);
+            
+        }
+        
+        rootPath = rootPathBuf.toString();
+
+        this.nodeNameGenerator = nodeNameGenerator;
+        propHandler = new UjaxPropertyValueHandler(this);
+        uploadHandler = new UjaxFileUploadHandler(this);
+    }
+
+    /**
+     * Returns the change log.
+     * @return the change log.
+     */
+    public ChangeLog getChangeLog() {
+        return changeLog;
+    }
+
+    /**
+     * Returns the jcr session
+     * @return the jcr session
+     */
+    public Session getSession() {
+        return session;
+    }
+
+    /**
+     * Returns the root path of this processor.
+     * @return the root path of this processor.
+     */
+    public String getRootPath() {
+        return rootPath;
+    }
+
+    /**
+     * Returns the path of the node that was the parent of the property
+     * modifications.
+     * @return the path of the 'current' node.
+     */
+    public String getCurrentPath() {
+        return currentPath;
+    }
+
+    /**
+     * Returns <code>true</code> if this was a create request.
+     * @return <code>true</code> if this was a create request.
+     */
+    public boolean isCreateRequest() {
+        return isCreateRequest;
+    }
+
+    /**
+     * Returns the location of the modification. this is the externalized form
+     * of the current path.
+     * @return the location of the modification.
+     */
+    public String getLocation() {
+        if (currentPath == null) {
+            return externalizePath(rootPath);
+        } else {
+            return externalizePath(currentPath);
+        }
+    }
+
+    /**
+     * Returns the parent location of the modification. this is the externalized
+     * form of the parent node of the current path.
+     * @return the location of the modification.
+     */
+    public String getParentLocation() {
+        String path = currentPath == null ? rootPath : currentPath;
+        path = path.substring(0, path.lastIndexOf('/'));
+        return externalizePath(path);
+    }
+
+    /**
+     * Returns an external form of the given path prepeding the context path
+     * and appending a display extension.
+     * @param path the path to externalize
+     * @return the url
+     */
+    private String externalizePath(String path) {
+        StringBuffer ret = new StringBuffer();
+        ret.append(SlingRequestPaths.getContextPath(request));
+        ret.append(request.getResourceResolver().map(path));
+
+        // append optional extension
+        String ext = request.getParameter(UjaxPostServlet.RP_DISPLAY_EXTENSION);
+        if (ext != null && ext.length() > 0) {
+            if (ext.charAt(0) != '.') {
+                ret.append('.');
+            }
+            ret.append(ext);
+        }
+        
+        return ret.toString();
+    }
+
+
+
+    /**
+     * Returns any recorded error or <code>null</code>
+     * @return an error or null
+     */
+    public Exception getError() {
+        return error;
+    }
+
+    /**
+     * Returns the request of this processor
+     * @return the sling servlet request
+     */
+    public SlingHttpServletRequest getRequest() {
+        return request;
+    }
+
+    /**
+     * Processes the actions defined by the request
+     */
+    public void run()  {
+        try {
+            processDeletes();
+            processMoves();
+            processContent();
+            if (session.hasPendingChanges()) {
+                session.save();
+            }
+        } catch (Exception e) {
+            error = e;
+        } finally {
+            try {
+                if (session.hasPendingChanges()) {
+                    session.refresh(false);
+                }
+            } catch (RepositoryException e) {
+                log.warn("RepositoryException in finally block: {}", e.getMessage(), e);
+            }
+        }
+    }
+
+    /**
+     * Resolves the given path with respect to the current root path.
+     *
+     * @param path the path to resolve
+     * @return the given path if it starts with a '/';
+     *         a resolved path otherwise.
+     */
+    public String resolvePath(String path) {
+        if (path.startsWith("/")) {
+            return path;
+        } else {
+            return rootPath + "/" + path;
+        }
+    }
+
+    /**
+     * Delete Items at the provided paths
+     *
+     * @throws RepositoryException if a repository error occurs
+     */
+    private void processDeletes() throws RepositoryException {
+        final String [] paths = request.getParameterValues(UjaxPostServlet.RP_DELETE_PATH);
+        if (paths != null) {
+            for (String path : paths) {
+                if (!path.equals("")) {
+                    path = resolvePath(path);
+                    if (session.itemExists(path)) {
+                        session.getItem(path).remove();
+                        changeLog.onDeleted(path);
+                        log.debug("Deleted item {}", path);
+                    } else {
+                        log.debug("Item at {} not found for deletion, ignored", path);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Move nodes at the provided paths
+     *
+     * @throws RepositoryException if a repository error occurs
+     * @throws IllegalArgumentException if the move arguments are incorrect
+     */
+    private void processMoves() throws RepositoryException,
+            IllegalArgumentException {
+        final String [] moveSrc = request.getParameterValues(UjaxPostServlet.RP_MOVE_SRC);
+        final String [] moveDest = request.getParameterValues(UjaxPostServlet.RP_MOVE_DEST);
+        if (moveSrc == null || moveDest == null) {
+            return;
+        }
+        if (moveSrc.length != moveDest.length) {
+            throw new IllegalArgumentException("Unable to process move. there " +
+                    "must be the same amount of source and destination parameters.");
+        }
+        for (int i=0; i<moveSrc.length; i++) {
+            String src = moveSrc[i];
+            String dest = moveDest[i];
+            if (src.equals(dest)) {
+                // ignore
+                continue;
+            }
+            if (src.equals("")) {
+                throw new IllegalArgumentException("Unable to process move. source argument is empty.");
+            }
+            if (dest.equals("")) {
+                throw new IllegalArgumentException("Unable to process move. destination argument is empty.");
+            }
+            src = resolvePath(src);
+            dest = resolvePath(dest);
+            session.move(src, dest);
+            changeLog.onMoved(src, dest);
+            log.debug("moved {} to {}", src, dest);
+        }
+    }
+
+
+    /**
+     * Create or update node(s) according to current request
+     * @throws RepositoryException if a repository error occurs
+     * @throws ServletException if an internal error occurs
+     */
+    private void processContent() throws RepositoryException, ServletException {
+        // get desired path.
+        String nodePath = rootPath;
+        Node currentNode;
+        // check for star suffix in request
+        if (isCreateRequest) {
+            // If the path ends with a *, create a node under its parent, with
+            // a generated node name
+            nodePath += "/" + nodeNameGenerator.getNodeName(request.getRequestParameterMap(), getSavePrefix());
+
+            // if resulting path exists, add a suffix until it's not the case anymore
+            if (session.itemExists(nodePath)) {
+                for (int suffix = 0; suffix < 1000; suffix++) {
+                    String newPath = nodePath + "_" + suffix;
+                    if(!session.itemExists(newPath)) {
+                        nodePath = newPath;
+                        break;
+                    }
+                }
+            }
+            // if it still exists there are more than 1000 nodes ?
+            if (session.itemExists(nodePath)) {
+                throw new ServletException("Collision in generated node names for path=" + nodePath);
+            }
+
+        }
+        // create or get node
+        currentNode = deepCreateNode(nodePath);
+        currentPath = currentNode.getPath();
+
+        // process the "order" command if any
+        final String order = request.getParameter(UjaxPostServlet.RP_ORDER);
+        if  (order!=null) {
+            processNodeOrder(currentNode, order);
+        }
+
+        // walk the request parameters, create and save nodes and properties
+        for (Map.Entry<String, RequestParameter[]>  e: request.getRequestParameterMap().entrySet()) {
+            final String paramName = e.getKey();
+
+            // do not store parameters with names starting with ujax:
+            if(paramName.startsWith(UjaxPostServlet.RP_PREFIX)) {
+                continue;
+            }
+            // ignore field with a '@TypeHint' suffix. this is dealt in RequestProperty
+            if (paramName.endsWith(UjaxPostServlet.TYPE_HINT_SUFFIX)) {
+                continue;
+            }
+            // ignore field with a '@DefaultValue' suffix. this is dealt in RequestProperty
+            if (paramName.endsWith(UjaxPostServlet.DEFAULT_VALUE_SUFFIX)) {
+                continue;
+            }
+            // skip parameters that do not start with the save prefix
+            if(!paramName.startsWith(getSavePrefix())) {
+                continue;
+            }
+            String propertyName = paramName.substring(getSavePrefix().length());
+            if (propertyName.length() == 0) {
+                continue;
+            }
+            // SLING-130: VALUE_FROM_SUFFIX means take the value of this
+            // property from a different field
+            RequestParameter[] values = e.getValue();
+            final int vfIndex = propertyName.indexOf(UjaxPostServlet.VALUE_FROM_SUFFIX);
+            if (vfIndex >= 0) {
+                // @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.
+                propertyName = propertyName.substring(0, vfIndex);
+                final RequestParameter[] rp = request.getRequestParameterMap().get(paramName);
+                if(rp == null || rp.length > 1) {
+                    // @ValueFrom params must have exactly one value, else ignored
+                    continue;
+                }
+                String mappedName = rp[0].getString();
+                values = request.getRequestParameterMap().get(mappedName);
+                if(values==null) {
+                    // no value for "fulltext" in our example, ignore parameter
+                    continue;
+                }
+            }
+            // create property helper and get parent node
+            RequestProperty prop = new RequestProperty(this, propertyName, values);
+            Node parent;
+            if(prop.getRelPath().startsWith("/")) {
+                parent = deepCreateNode(prop.getParentPath());
+            } else if (!prop.getParentPath().equals("")) {
+                parent = currentNode.getNode(prop.getParentPath());
+            } else {
+                parent = currentNode;
+            }
+            // call handler
+            if (prop.isFileUpload()) {
+                uploadHandler.setFile(parent, prop);
+            } else {
+                propHandler.setProperty(parent, prop);
+            }
+        }
+    }
+
+    /**
+     * Deep creates a node, parent-padding with nt:unstructured nodes
+     *
+     * @param path absolute path to node that needs to be deep-created
+     * @return node at path
+     * @throws RepositoryException if an error occurs
+     */
+    private Node deepCreateNode(String path) throws RepositoryException {
+        if(log.isDebugEnabled()) {
+            log.debug("Deep-creating Node '{}'", path);
+        }
+
+        String[] pathelems = path.substring(1).split("/");
+        Node node = session.getRootNode();
+        for (String name: pathelems) {
+            if (node.hasNode(name)) {
+                node = node.getNode(name);
+            } else {
+                node = node.addNode(name);
+                changeLog.onCreated(node.getPath());
+            }
+        }
+        return node;
+    }
+
+    /**
+     * Return the "save prefix" to use. the names of request properties must
+     * start with that prefix in order to be regarded as input values.
+     *
+     * @return the save prefix
+     */
+    public String getSavePrefix() {
+        if (savePrefix == null) {
+            savePrefix = request.getParameter(UjaxPostServlet.RP_SAVE_PARAM_PREFIX);
+            if (savePrefix == null) {
+                savePrefix = UjaxPostServlet.DEFAULT_SAVE_PARAM_PREFIX;
+            }
+            if (savePrefix.length() > 0) {
+                String prefix = "";
+                // if no parameters start with this prefix, it is not used
+                for (String name: request.getRequestParameterMap().keySet()) {
+                    if (name.startsWith(savePrefix)) {
+                        prefix = savePrefix;
+                        break;
+                    }
+                }
+                savePrefix = prefix;
+            }
+        }
+        return savePrefix;
+    }
+
+    /**
+     * If orderCode is ORDER_ZERO, move n so that it is the first child of its
+     * parent
+     * @param n th node to order
+     * @param orderCode code that specifies how to order
+     * @throws RepositoryException if a repository error occurs
+     */
+    private void processNodeOrder(Node n, String orderCode)
+            throws RepositoryException {
+        if (UjaxPostServlet.ORDER_ZERO.equals(orderCode)) {
+            final Node parent = n.getParent();
+            final String beforename=parent.getNodes().nextNode().getName();
+            parent.orderBefore(n.getName(), beforename);
+            if(log.isDebugEnabled()) {
+                log.debug("Node {} moved to be first child of its parent, " +
+                        "due to orderCode=" + orderCode, n.getPath());
+            }
+        } else {
+            if(log.isDebugEnabled()) {
+                log.debug("orderCode '{}' invalid, ignored", orderCode);
+            }
+        }
+    }
+
+}
\ No newline at end of file

Modified: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostServlet.java
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostServlet.java?rev=618397&r1=618396&r2=618397&view=diff
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostServlet.java (original)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPostServlet.java Mon Feb  4 11:43:47 2008
@@ -16,479 +16,196 @@
  */
 package org.apache.sling.ujax;
 
-import org.apache.sling.api.SlingException;
 import org.apache.sling.api.SlingHttpServletRequest;
 import org.apache.sling.api.SlingHttpServletResponse;
-import org.apache.sling.api.request.RequestParameter;
 import org.apache.sling.api.resource.Resource;
 import org.apache.sling.api.servlets.SlingAllMethodsServlet;
 import org.apache.sling.api.wrappers.SlingRequestPaths;
 import org.apache.sling.core.CoreConstants;
+import org.apache.sling.jcr.resource.JcrResourceUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.util.HashSet;
-import java.util.Map;
-import java.util.Set;
 
-import javax.jcr.Item;
 import javax.jcr.Node;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
 import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletResponse;
 
-/** POST servlet that implements the ujax "protocol" */
+/**
+ * POST servlet that implements the ujax "protocol"
+ */
 public class UjaxPostServlet extends SlingAllMethodsServlet {
+
     private static final long serialVersionUID = 1837674988291697074L;
 
+    /**
+     * default log
+     */
     private static final Logger log = LoggerFactory.getLogger(UjaxPostServlet.class);
-    private final UjaxPropertyValueSetter propertyValueSetter = new UjaxPropertyValueSetter();
-    private final UjaxFileUploadHandler uploadHandler = new UjaxFileUploadHandler();
-    private final NodeNameGenerator nodeNameGenerator = new NodeNameGenerator();
 
-    /** Prefix for parameter names which control this POST
-     *  (ujax stands for "microjax", RP_ stands for "request param")
+    /**
+     * Prefix for parameter names which control this POST
+     * (ujax stands for "microjax", RP_ stands for "request param")
      */
     public static final String RP_PREFIX = "ujax:";
 
-    /** suffix that indicates node creation */
+    /**
+     * suffix that indicates node creation
+     */
     public static final String DEFAULT_CREATE_SUFFIX = "/*";
 
-    /** Optional request parameter: redirect to the specified URL after POST */
-    public static final String RP_REDIRECT_TO =  RP_PREFIX + "redirect";
-
-    /** Optional request parameter: delete the specified content paths */
+    /**
+     * Optional request parameter: delete the specified content paths
+     */
     public static final String RP_DELETE_PATH = RP_PREFIX + "delete";
 
-    /** Optional request parameter: move the specified content paths */
+    /**
+     * Optional request parameter: move the specified content paths
+     */
     public static final String RP_MOVE_SRC = RP_PREFIX + "moveSrc";
+
+    /**
+     * Optional request parameter: move the specified content paths to this
+     * destination
+     */
     public static final String RP_MOVE_DEST = RP_PREFIX + "moveDest";
 
-    /** Optional request parameter: only request parameters starting with this prefix are
-     *  saved as Properties when creating a Node. Active only if at least one parameter
-     *  starts with this prefix, and defaults to {@link #DEFAULT_SAVE_PARAM_PREFIX}.
+    /**
+     * Optional request parameter: only request parameters starting with this prefix are
+     * saved as Properties when creating a Node. Active only if at least one parameter
+     * starts with this prefix, and defaults to {@link #DEFAULT_SAVE_PARAM_PREFIX}.
      */
     public static final String RP_SAVE_PARAM_PREFIX = RP_PREFIX + "saveParamPrefix";
 
-    /** Default value for {@link #RP_SAVE_PARAM_PREFIX} */
+    /**
+     * Default value for {@link #RP_SAVE_PARAM_PREFIX}
+     */
     public static final String DEFAULT_SAVE_PARAM_PREFIX = "./";
 
-    /** Optional request parameter: if value is 0, created node is ordered so as
-     *  to be the first child of its parent.
+    /**
+     * Optional request parameter: if value is 0, created node is ordered so as
+     * to be the first child of its parent.
      */
     public static final String RP_ORDER = RP_PREFIX + "order";
 
-    /** Code value for RP_ORDER */
+    /**
+     * Code value for RP_ORDER
+     */
     public static final String ORDER_ZERO = "0";
 
-    /** Optional request parameter: if provided, added at the end of the computed
-     *  (or supplied) redirect URL
+    /**
+     * Optional request parameter: redirect to the specified URL after POST
+     */
+    public static final String RP_REDIRECT_TO =  RP_PREFIX + "redirect";
+
+    /**
+     * 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 */
+
+    /**
+     * SLING-130, suffix that maps form field names to different JCR property names
+     */
     public static final String VALUE_FROM_SUFFIX = "@ValueFrom";
 
+    /**
+     * suffix that indicates a type hint parameter
+     */
     public static final String TYPE_HINT_SUFFIX = "@TypeHint";
 
-    public static final String DEFAULT_VALUE_SUFFIX = "@DefaultValue";
-
-    @Override
-    protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response)
-            throws ServletException, IOException {
-
-        Session s = null;
-        try {
-
-            // select the Resource to process
-            Resource currentResource = request.getResource();
-            Node currentNode = currentResource.adaptTo(Node.class);
-
-            // need a Node, path and Session
-            final String currentPath;
-            if(currentNode != null) {
-                currentPath = currentNode.getPath();
-                s = currentNode.getSession();
-            } else {
-                currentPath = SlingRequestPaths.getPathInfo(request);
-                s = (Session)request.getAttribute(CoreConstants.SESSION);
-            }
-            
-            if(s==null) {
-                throw new ServletException("No JCR Session available, currentNode=" + currentNode);
-            }
-
-            // process changes
-            processDeletes(request, s, currentPath);
-            processMoves(request, s, currentPath);
-            createOrUpdateNodesFromRequest(request, response, s);
-
-        } catch(RepositoryException re) {
-            throw new SlingException(re.toString(), re);
-
-        } finally {
-            try {
-                if (s != null && s.hasPendingChanges()) {
-                    s.refresh(false);
-                }
-            } catch(RepositoryException re) {
-                log.warn("RepositoryException in finally block: "+ re.getMessage(),re);
-            }
-        }
-    }
-
-    /** Create or update node(s) according to current request , and send response */
-    protected void createOrUpdateNodesFromRequest(SlingHttpServletRequest request, SlingHttpServletResponse response, Session s)
-            throws RepositoryException, IOException {
-
-        // find out the actual "save prefix" to use - only parameters starting with
-        // this prefix are saved as Properties, when creating nodes, see setPropertiesFromRequest()
-        final String savePrefix = getSavePrefix(request);
-
-        // use the request path (disregarding resource resolution)
-        // but remove any extension or selectors
-        String currentPath = SlingRequestPaths.getPathInfo(request);
-        Node currentNode = null;
-        final int dotPos = currentPath.indexOf('.');
-        if(dotPos >= 0) {
-            currentPath = currentPath.substring(0,dotPos);
-        }
-
-        final String starSuffix = DEFAULT_CREATE_SUFFIX;
-        if(currentPath.endsWith(starSuffix)) {
-            // If the path ends with a *, create a node under its parent, with
-            // a generated node name
-            currentPath = currentPath.substring(0, currentPath.length() - starSuffix.length());
-            currentPath += "/" + nodeNameGenerator.getNodeName(request.getRequestParameterMap(), savePrefix);
-            
-            // if resulting path exists, add a suffix until it's not the case anymore
-            if(s.itemExists(currentPath)) {
-                for(int suffix = 0; suffix < 100; suffix++) {
-                    String newPath = currentPath + "_" + suffix;
-                    if(!s.itemExists(newPath)) {
-                        currentPath = newPath;
-                        break;
-                    }
-                }
-            }
-            
-            if(s.itemExists(currentPath)) {
-                response.sendError(
-                    HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
-                    "Collision in generated node names for path=" + currentPath);
-                return;
-            }
-
-        } else if(s.itemExists(currentPath)) {
-            // update to an existing node
-            final Item item = s.getItem(currentPath);
-            if(item.isNode()) {
-                currentNode = (Node)item;
-            } else {
-                response.sendError(HttpServletResponse.SC_CONFLICT,
-                    "Item at path " + currentPath + " is not a Node");
-                return;
-            }
-
-        } else {
-            // request to create a new node at a specific path - use the supplied path as is
-        }
-
-        Set<Node> createdNodes = new HashSet<Node>();
-        if(currentNode == null) {
-            currentNode = deepCreateNode(s, currentPath, createdNodes);
-        }
-        currentPath = currentNode.getPath();
-
-        // process the "order" command if any
-        final String order = request.getParameter(RP_ORDER);
-        if(order!=null) {
-            processNodeOrder(currentNode,order);
-        }
-
-        // walk the request parameters, create and save nodes and properties
-        setPropertiesFromRequest(currentNode, request, savePrefix, createdNodes);
-
-        // sava data and send redirect
-        s.save();
-        response.sendRedirect(getRedirectUrl(request,currentNode.getPath()));
-    }
-    
-    /** compute redirect URL (SLING-126) */
-    protected String getRedirectUrl(SlingHttpServletRequest request, String currentNodePath) {
-        
-        // redirect param has priority (but see below, magic star)
-        String result = request.getParameter(RP_REDIRECT_TO);
-        final boolean magicStar = "*".equals(result);
-        
-        if(result==null || result.trim().length()==0) {
-            // try Referer
-            result = request.getHeader("Referer");
-        }
-        
-        // redirect param = star means "redirect to current node", useful in browsers
-        // when you don't want to use the Referer
-        if(magicStar || result==null || result.trim().length()==0) {
-            // use path of current node, with optional extension 
-            final String redirectExtension = request.getParameter(RP_DISPLAY_EXTENSION);
-            result = currentNodePath;
-            
-            if(redirectExtension!=null) {
-                if(redirectExtension.startsWith(".")) {
-                    result += redirectExtension;
-                } else {
-                    result += "." + redirectExtension;
-                }
-            }
-            
-            result =
-                SlingRequestPaths.getContextPath(request)
-                + SlingRequestPaths.getServletPath(request)
-                + result;
-        }
-        
-        if(log.isDebugEnabled()) {
-            log.debug("Will redirect to " + result);
-        }
-        
-        return result;
-    }
-
-    /** Set Node properties from current request
+    /**
+     * suffix that indicates a default value parameter
      */
-    private void setPropertiesFromRequest(Node n, SlingHttpServletRequest request,
-            String savePrefix, Set<Node> createdNodes)
-            throws RepositoryException {
-
-        for(Map.Entry<String, RequestParameter[]>  e : request.getRequestParameterMap().entrySet()) {
-            final String paramName = e.getKey();
-            
-            if(paramName.startsWith(RP_PREFIX)) {
-                // do not store parameters with names starting with ujax:  
-                continue;
-            }
-            
-            String propertyName = paramName;
-            if(savePrefix!=null) {
-                if(!paramName.startsWith(savePrefix)) {
-                    continue;
-                }
-                propertyName = paramName.substring(savePrefix.length());
-            }
-
-            // ignore field with a '@TypeHint' suffix. this is dealt in RequestProperty
-            if (propertyName.endsWith(TYPE_HINT_SUFFIX)) {
-                continue;
-            }
-
-            // ignore field with a '@DefaultValue' suffix. this is dealt in RequestProperty
-            if (propertyName.endsWith(DEFAULT_VALUE_SUFFIX)) {
-                continue;
-            }
-
-            // SLING-130: VALUE_FROM_SUFFIX means take the value of this
-            // property from a different field
-            RequestParameter[] values = e.getValue();
-            final int vfIndex = propertyName.indexOf(VALUE_FROM_SUFFIX);
-            if(vfIndex >= 0) {
-                // @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. 
-                propertyName = propertyName.substring(0, vfIndex);
-                final RequestParameter[] rp = request.getRequestParameterMap().get(paramName);
-                if(rp == null || rp.length > 1) {
-                    // @ValueFrom params must have exactly one value, else ignored
-                    continue;
-                }
-                String mappedName = rp[0].getString();
-                values = request.getRequestParameterMap().get(mappedName);
-                if(values==null) {
-                    // no value for "fulltext" in our example, ignore parameter
-                    continue;
-                }
-            }
-
-            RequestProperty prop = new RequestProperty(request, savePrefix, propertyName, values);
-            
-            setProperty(n, request, createdNodes, prop);
-        }
-    }
+    public static final String DEFAULT_VALUE_SUFFIX = "@DefaultValue";
 
     /**
-     * Set a single Property on node N
-     *
-     * @throws RepositoryException if a repository error occurs
-     */
-    private void setProperty(Node n, SlingHttpServletRequest request,
-                             Set<Node> createdNodes, RequestProperty prop)
-            throws RepositoryException {
-
-        if (prop.getName().equals("")) {
-            return;
-        }
+     * utility class for generating node names
+     */
+    private final NodeNameGenerator nodeNameGenerator = new NodeNameGenerator();
 
-        // get or create the parent node
-        final String path = n.getPath();
-        final Session s = n.getSession();
-        Node parent;
-        if(prop.getKeyName().startsWith("/")) {
-            parent = deepCreateNode(s, prop.getParentPath(), createdNodes);
-        } else if (!prop.getParentPath().equals("")) {
-            parent = (Node) s.getItem(path + "/" + prop.getParentPath());
-        } else {
-            parent = (Node) s.getItem(path);
-        }
+    @Override
+    protected void doPost(SlingHttpServletRequest request,
+                          SlingHttpServletResponse response)
+            throws ServletException, IOException {
 
-        // call setter
-        if (prop.isFileUpload()) {
-            uploadHandler.setFile(request, parent, prop);
+        // create a post processor and process changes
+        UjaxPostProcessor p = createPostProcessor(request);
+        p.run();
+
+        // check for redirect url
+        String redirect = getRedirectUrl(p);
+        if (redirect != null && p.getError() == null) {
+            response.sendRedirect(redirect);
         } else {
-            final boolean nodeIsNew = createdNodes.contains(parent);
-            propertyValueSetter.setProperty(parent, prop, nodeIsNew);
-        }
-    }
-
-    /**
-     * Deep creates a node, parent-padding with nt:unstructured nodes
-     *
-     * @param path absolute path to node that needs to be deep-created
-     */
-    private Node deepCreateNode(Session s, String path, Set<Node> createdNodes)
-            throws RepositoryException {
-        if(log.isDebugEnabled()) {
-            log.debug("Deep-creating Node '" + path + "'");
-        }
-
-        String[] pathelems = path.substring(1).split("/");
-        int i = 0;
-        String mypath = "";
-        Node parent = s.getRootNode();
-        while (i < pathelems.length) {
-            String name = pathelems[i];
-            mypath += "/" + name;
-            if (!s.itemExists(mypath)) {
-                createdNodes.add(parent.addNode(name));
-            }
-            parent = (Node) s.getItem(mypath);
-            i++;
+            // create a html response and send
+            UjaxHtmlResponse resp = new UjaxHtmlResponse(p, response);
+            resp.send();
         }
-        return (parent);
     }
 
     /**
-     * Delete Items at the provided paths
-     * @param request the servlet request
-     * @param s the session
-     * @param currentPath the current path
-     * @throws RepositoryException if a repository error occurs
-     */
-    private void processDeletes(SlingHttpServletRequest request, Session s,
-                                String currentPath)
-            throws RepositoryException {
-        final String [] pathsToDelete = request.getParameterValues(RP_DELETE_PATH);
-        int deleteCount = 0;
-        
-        if (pathsToDelete != null) {
-            for(String path : pathsToDelete) {
-                if(!path.startsWith("/")) {
-                    path = currentPath + "/" + path;
-                }
-                if(s.itemExists(path)) {
-                    s.getItem(path).remove();
-                    deleteCount++;
-                    if(log.isDebugEnabled()) {
-                        log.debug("Deleted item " + path);
-                    }
-                } else {
-                    if(log.isDebugEnabled()) {
-                        log.debug("Item '" + path + "' not found for deletion, ignored");
-                    }
-                }
-            }
+     * Creats a post processor for the given request.
+     * @param request the request for the processor
+     * @return a post context
+     * @throws ServletException if no session can be aquired or if there is a
+     *         repository error.
+     */
+    private UjaxPostProcessor createPostProcessor(SlingHttpServletRequest request)
+            throws ServletException {
+        Session s = request.getResourceResolver().adaptTo(Session.class);
+        if (s == null) {
+            throw new ServletException("No JCR Session available");
         }
         
-        if(deleteCount > 0) {
-            s.save();
-        }
+        // create the post context
+        return new UjaxPostProcessor(request, s, nodeNameGenerator);
     }
 
     /**
-     * Move nodes at the provided paths
-     * @param request the servlet request
-     * @param s the session
-     * @param currentPath the current path
-     * @throws RepositoryException if a repository error occurs
-     */
-    private void processMoves(SlingHttpServletRequest request, Session s,
-                                String currentPath)
-            throws RepositoryException {
-        int moveCount = 0;
-        final String [] moveSrc = request.getParameterValues(RP_MOVE_SRC);
-        final String [] moveDest = request.getParameterValues(RP_MOVE_DEST);
-        if (moveSrc == null || moveDest == null) {
-            return;
-        }
-        if (moveSrc.length != moveDest.length) {
-            return;
-        }
-        for (int i=0; i<moveSrc.length; i++) {
-            String src = moveSrc[i];
-            if (!src.startsWith("/")) {
-                src = currentPath + "/" + src;
-            }
-            String dest = moveDest[i];
-            if (!dest.startsWith("/")) {
-                dest = currentPath + "/" + dest;
+     * compute redirect URL (SLING-126)
+     * @param ctx the post processor
+     * @return the redirect location or <code>null</code>
+     */
+    protected String getRedirectUrl(UjaxPostProcessor ctx) {
+        // redirect param has priority (but see below, magic star)
+        String result = ctx.getRequest().getParameter(RP_REDIRECT_TO);
+        if (result != 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
+                String nodePath = ctx.getCurrentPath();
+                if (nodePath == null) {
+                    nodePath = ctx.getRootPath();
+                }
+                buf.append(JcrResourceUtil.getName(nodePath));
+                
+                // 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();
             }
-            s.move(src, dest);
-            moveCount++;
+            
             if (log.isDebugEnabled()) {
-                log.debug("moved {} to {}", src, dest);
-            }
-        }
-        if(moveCount > 0) {
-            s.save();
-        }
-    }
-
-    /** Return the "save prefix" to use, null if none */
-    private String getSavePrefix(SlingHttpServletRequest request) {
-        String prefix = request.getParameter(RP_SAVE_PARAM_PREFIX);
-        if (prefix==null) {
-            prefix = DEFAULT_SAVE_PARAM_PREFIX;
-        }
-
-        // if no parameters start with this prefix, it is not used
-        for (String name : request.getRequestParameterMap().keySet()) {
-            if (name.startsWith(prefix)) {
-                return prefix;
+                log.debug("Will redirect to " + result);
             }
         }
-        return null;
+        return result;
     }
 
-    /** If orderCode is ORDER_ZERO, move n so that it is the first
-     *  child of its parent
-     * @throws RepositoryException */
-    private void processNodeOrder(Node n, String orderCode) throws RepositoryException {
-        if(ORDER_ZERO.equals(orderCode)) {
-            final String path = n.getPath();
-            final Node parent=(Node) n.getSession().getItem(path.substring(0,path.lastIndexOf('/')));
-            final String myname=path.substring(path.lastIndexOf('/')+1);
-            final String beforename=parent.getNodes().nextNode().getName();
-            parent.orderBefore(myname, beforename);
-
-            if(log.isDebugEnabled()) {
-                log.debug("Node " + n.getPath() + " moved to be first child of its parent, due to orderCode=" + orderCode);
-            }
-
-        } else {
-            if(log.isDebugEnabled()) {
-                log.debug("orderCode '" + orderCode + "' invalid, ignored");
-            }
-        }
-    }
 }
 

Added: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPropertyValueHandler.java
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPropertyValueHandler.java?rev=618397&view=auto
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPropertyValueHandler.java (added)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/java/org/apache/sling/ujax/UjaxPropertyValueHandler.java Mon Feb  4 11:43:47 2008
@@ -0,0 +1,201 @@
+/*
+ * 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.ujax;
+
+import java.util.Calendar;
+
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+
+/**
+ * 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.
+ */
+class UjaxPropertyValueHandler {
+
+    public static final String CREATED_FIELD = "created";
+    public static final String CREATED_BY_FIELD = "createdBy";
+    public static final String LAST_MODIFIED_FIELD = "lastModified";
+    public static final String LAST_MODIFIED_BY_FIELD = "lastModifiedBy";
+
+    /**
+     * the post processor
+     */
+    private final UjaxPostProcessor ctx;
+
+    /**
+     * current date for all properties in this request
+     */
+    private final Calendar now = Calendar.getInstance();
+
+    /**
+     * Constructs a propert value handler
+     * @param ctx the post processor
+     */
+    public UjaxPropertyValueHandler(UjaxPostProcessor ctx) {
+        this.ctx = ctx;
+    }
+
+
+    /**
+     * 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="dateCreated"/>
+     *   <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
+     */
+    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 (CREATED_FIELD.equals(name)) {
+            if (parent.isNew()) {
+                setCurrentDate(parent, name);
+            }
+
+        } else if (CREATED_BY_FIELD.equals(name)) {
+            if (parent.isNew()) {
+                setCurrentUser(parent, name);
+            }
+
+        } else if (LAST_MODIFIED_FIELD.equals(name)) {
+            setCurrentDate(parent, name);
+
+        } else if (LAST_MODIFIED_BY_FIELD.equals(name)) {
+            setCurrentUser(parent, name);
+
+        } 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);
+        ctx.getChangeLog().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);
+        ctx.getChangeLog().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.STRING;
+        if (prop.getTypeHint() != null) {
+            try {
+                type = PropertyType.valueFromName(prop.getTypeHint());
+            } catch (Exception e) {
+                // ignore
+            }
+        }
+
+        String[] values = prop.getStringValues();
+        if (values == null) {
+            // remove property
+            ctx.getChangeLog().onDeleted(
+                removePropertyIfExists(parent, prop.getName())
+            );
+        } else if (values.length == 0) {
+            // do not create new prop here, but clear existing
+            if (parent.hasProperty(prop.getName())) {
+                ctx.getChangeLog().onModified(
+                    parent.setProperty(prop.getName(), "").getPath()
+                );
+            }
+        } else if (values.length == 1) {
+            removePropertyIfExists(parent, prop.getName());
+            ctx.getChangeLog().onModified(
+                parent.setProperty(prop.getName(), values[0], type).getPath()
+            );
+        } else {
+            removePropertyIfExists(parent, prop.getName());
+            ctx.getChangeLog().onModified(
+                parent.setProperty(prop.getName(), values, type).getPath()
+            );
+        }
+    }
+
+}

Added: incubator/sling/trunk/launchpad/launchpad-servlets/src/main/resources/org/apache/sling/ujax/UjaxHtmlResponse.html
URL: http://svn.apache.org/viewvc/incubator/sling/trunk/launchpad/launchpad-servlets/src/main/resources/org/apache/sling/ujax/UjaxHtmlResponse.html?rev=618397&view=auto
==============================================================================
--- incubator/sling/trunk/launchpad/launchpad-servlets/src/main/resources/org/apache/sling/ujax/UjaxHtmlResponse.html (added)
+++ incubator/sling/trunk/launchpad/launchpad-servlets/src/main/resources/org/apache/sling/ujax/UjaxHtmlResponse.html Mon Feb  4 11:43:47 2008
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
+<!-- 
+   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.
+ -->
+ <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+    <title>${title}</title>
+</head>
+    <body>
+    <h1>${title}</h1>
+    <table>
+        <tbody>
+            <tr>
+                <td>Status</td>
+                <td><div id="Status">${status.code}</div></td>
+            </tr>
+            <tr>
+                <td>Message</td>
+                <td><div id="Message">${status.message}</div></td>
+            </tr>
+            <tr>
+                <td>Location</td>
+                <td><a href="${location}" id="Location">${location}</a></td>
+            </tr>
+            <tr>
+                <td>Parent Location</td>
+                <td><a href="${parentLocation}" id="ParentLocation">${parentLocation}</a></td>
+            </tr>
+            <tr>
+                <td>Path</td>
+                <td><div id="Path">${path}</div></td>
+            </tr>
+            <tr>
+                <td>Referer</td>
+                <td><a href="${referer}" id="Referer">${referer}</a></td>
+            </tr>
+            <tr>
+                <td>ChangeLog</td>
+                <td><div id="ChangeLog">${changeLog}</div></td>
+            </tr>
+        </tbody>
+    </table>
+    <p><a href="${referer}">Go Back</a></p>
+    <p><a href="${location}">Modified Resource</a></p>
+    <p><a href="${parentLocation}">Parent of Modified Resource</a></p>
+    </body>
+</html>
\ No newline at end of file



Mime
View raw message