This is an automated email from the ASF dual-hosted git repository.

remm pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 3f5b0cc58c Repackage dead property handling into a Catalina style 
store API
3f5b0cc58c is described below

commit 3f5b0cc58ce3c5cc7f74a9cbcfe083b303f222ff
Author: remm <r...@apache.org>
AuthorDate: Wed Oct 23 10:01:58 2024 +0200

    Repackage dead property handling into a Catalina style store API
    
    Add a PropertyStore interface with streamlined API.
    Take advantage of this as an excuse to repackage the simple non
    persistent store into a basic default option.
    Add logging "warnings" about what it does and how to configure it.
    No functional change but this way the WebDAV Servlet is fully testable
    by default (Litmus works, for example).
---
 .../catalina/servlets/LocalStrings.properties      |   3 +
 .../apache/catalina/servlets/WebdavServlet.java    | 470 ++++++++++++++-------
 .../catalina/servlets/TestWebdavServlet.java       | 108 ++++-
 .../servlets/TransientPropertiesWebdavServlet.java | 186 --------
 webapps/docs/changelog.xml                         |  14 +-
 5 files changed, 442 insertions(+), 339 deletions(-)

diff --git a/java/org/apache/catalina/servlets/LocalStrings.properties 
b/java/org/apache/catalina/servlets/LocalStrings.properties
index c02d09eaf9..dd0b8e6ed2 100644
--- a/java/org/apache/catalina/servlets/LocalStrings.properties
+++ b/java/org/apache/catalina/servlets/LocalStrings.properties
@@ -55,5 +55,8 @@ defaultServlet.xslError=XSL transformer error
 webdavservlet.externalEntityIgnored=The request included a reference to an 
external entity with PublicID [{0}] and SystemID [{1}] which was ignored
 webdavservlet.inputstreamclosefail=Failed to close the inputStream of [{0}]
 webdavservlet.jaxpfailed=JAXP initialization failed
+webdavservlet.memorystore=Non persistent memory storage will be used for dead 
properties; the 'propertyStore' init parameter of the Servlet may be used to 
configure a custom store implementing the 'WebdavServlet.PropertyStore' 
interface
 webdavservlet.nonWildcardMapping=The mapping [{0}] is not a wildcard mapping 
and should not be used for the WebDAV Servlet
 webdavservlet.noSecret=Generation of secure lock ids need a configured 
'secret' init parameter on the Servlet
+webdavservlet.noStoreParameter=Init parameter [{0}] with value [{1}] was not 
found on the configured store
+webdavservlet.storeError=Error creating store of class [{0}], the default 
memory store will be used instead
diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java 
b/java/org/apache/catalina/servlets/WebdavServlet.java
index 4a73cbe2d5..b745a5b956 100644
--- a/java/org/apache/catalina/servlets/WebdavServlet.java
+++ b/java/org/apache/catalina/servlets/WebdavServlet.java
@@ -23,6 +23,7 @@ import java.io.Serializable;
 import java.io.StringReader;
 import java.io.StringWriter;
 import java.io.Writer;
+import java.lang.reflect.InvocationTargetException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.charset.StandardCharsets;
@@ -31,6 +32,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.Deque;
+import java.util.Enumeration;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -57,6 +59,7 @@ import org.apache.catalina.connector.RequestFacade;
 import org.apache.catalina.util.DOMWriter;
 import org.apache.catalina.util.XMLWriter;
 import org.apache.tomcat.PeriodicEventListener;
+import org.apache.tomcat.util.IntrospectionUtils;
 import org.apache.tomcat.util.buf.HexUtils;
 import org.apache.tomcat.util.http.ConcurrentDateFormat;
 import org.apache.tomcat.util.http.FastHttpDateFormat;
@@ -73,7 +76,7 @@ import org.xml.sax.SAXException;
 
 /**
  * Servlet which adds support for <a 
href="https://tools.ietf.org/html/rfc4918";>WebDAV</a>
- * <a href="https://tools.ietf.org/html/rfc4918#section-18";>level 2</a>. All 
the basic HTTP requests are handled by the
+ * <a href="https://tools.ietf.org/html/rfc4918#section-18";>level 3</a>. All 
the basic HTTP requests are handled by the
  * DefaultServlet. The WebDAVServlet must not be used as the default servlet 
(ie mapped to '/') as it will not work in
  * this configuration.
  * <p>
@@ -258,6 +261,12 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
     private boolean strictIfProcessing = true;
 
 
+    /**
+     * Property store used for storage of dead properties.
+     */
+    private PropertyStore store = null;
+
+
     // --------------------------------------------------------- Public Methods
 
 
@@ -294,6 +303,41 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
         if (getServletConfig().getInitParameter("strictIfProcessing") != null) 
{
             strictIfProcessing = 
Boolean.parseBoolean(getServletConfig().getInitParameter("strictIfProcessing"));
         }
+
+        String propertyStore = 
getServletConfig().getInitParameter("propertyStore");
+        if (propertyStore != null) {
+            try {
+                Class<?> clazz = 
Thread.currentThread().getContextClassLoader().loadClass(propertyStore);
+                store = (PropertyStore) clazz.getConstructor().newInstance();
+                // Set init parameters as properties on the store
+                Enumeration<String> parameterNames = 
getServletConfig().getInitParameterNames();
+                while (parameterNames.hasMoreElements()) {
+                    String parameterName = parameterNames.nextElement();
+                    if (parameterName.startsWith("store.")) {
+                        StringBuilder actualMethod = new StringBuilder();
+                        String parameterValue = 
getServletConfig().getInitParameter(parameterName);
+                        parameterName = 
parameterName.substring("store.".length());
+                        if (!IntrospectionUtils.setProperty(store, 
parameterName, parameterValue, true, actualMethod)) {
+                            log(sm.getString("webdavservlet.noStoreParameter", 
parameterName, parameterValue));
+                        }
+                    }
+                }
+            } catch (ClassNotFoundException | InstantiationException | 
IllegalAccessException
+                    | IllegalArgumentException | InvocationTargetException | 
NoSuchMethodException | SecurityException e) {
+                log(sm.getString("webdavservlet.storeError"), e);
+            }
+        }
+        if (store == null) {
+            log(sm.getString("webdavservlet.memorystore"));
+            store = new MemoryPropertyStore();
+        }
+        store.init();
+    }
+
+
+    @Override
+    public void destroy() {
+        store.destroy();
     }
 
 
@@ -321,114 +365,144 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                 }
             }
         }
+        store.periodicEvent();
     }
 
 
-    // ------------------------------------------------------ Protected Methods
+    // ------------------------------------------------ PropertyStore Interface
 
 
     /**
-     * Copy resource. This should be overridden by subclasses to provide 
useful behavior. The default implementation
-     * prevents setting protected properties (anything from the DAV: 
namespace), and sets 507 for a set attempt on dead
-     * properties.
-     *
-     * @param source the copy source path
-     * @param dest   the copy destination path
+     * Handling of dead properties on resources. This interface allows
+     * providing storage for dead properties. Store configuration is done
+     * through the <code>propertyStore</code> init parameter of the WebDAV
+     * Servlet, which should contain the class name of the store.
      */
-    protected void copyResource(String source, String dest) {
+    public interface PropertyStore {
+
+        /**
+         * Initialize the store. This is tied to the Servlet lifecycle and is 
called by its init method.
+         */
+        void init();
+
+        /**
+         * Destroy the store. This is tied to the Servlet lifecycle and is 
called by its destroy method.
+         */
+        void destroy();
+
+        /**
+         * Periodic event for maintenance tasks.
+         */
+        void periodicEvent();
+
+        /**
+         * Copy resource. Dead properties should be copied to the destination 
path.
+         *
+         * @param source the copy source path
+         * @param destination the copy destination path
+         */
+        void copy(String source, String destination);
+
+        /**
+         * Delete specified resource. Dead properties on a deleted resource 
should be deleted.
+         *
+         * @param resource the path of the resource to delete
+         */
+        void delete(String resource);
+
+        /**
+         * Generate propfind XML fragments for dead properties.
+         *
+         * @param resource the resource path
+         * @param property the dead property, if null then all dead properties 
must be written
+         * @param nameOnly true if only the property name element should be 
generated
+         * @param generatedXML the current generated XML for the PROPFIND 
response
+         * @return true if a property was specified and a corresponding dead 
property was found on the resource,
+         *     false otherwise
+         */
+        boolean propfind(String resource, Node property, boolean nameOnly, 
XMLWriter generatedXML);
+
+        /**
+         * Apply proppatch to the specified resource.
+         *
+         * @param resource the resource path on which to apply the proppatch
+         * @param operations the set and remove to apply, the final status 
codes of the result should be set on each
+         *                       operation
+         */
+        void proppatch(String resource, ArrayList<ProppatchOperation> 
operations);
+
     }
 
+    // ----------------------------------------- ProppatchOperation Inner Class
+
 
     /**
-     * Delete specified resource. This should be overridden by subclasses to 
provide useful behavior. The default
-     * implementation prevents setting protected properties (anything from the 
DAV: namespace), and sets 507 for a set
-     * attempt on dead properties.
-     *
-     * @param path the path of the resource to delete
+     * Represents a PROPPATCH sub operation to be performed.
      */
-    protected void deleteResource(String path) {
-        unlockResource(path, null);
-    }
+    public static class ProppatchOperation {
+        private final PropertyUpdateType updateType;
+        private final Node propertyNode;
+        private final boolean protectedProperty;
+        private int statusCode = HttpServletResponse.SC_OK;
+
+        /**
+         * PROPPATCH operation constructor.
+         * @param updateType the update type, either SET or REMOVE
+         * @param propertyNode the XML node that contains the property name 
(and value if SET)
+         */
+        public ProppatchOperation(PropertyUpdateType updateType, Node 
propertyNode) {
+            this.updateType = updateType;
+            this.propertyNode = propertyNode;
+            String davName = getDAVNode(propertyNode);
+            // displayname and getcontentlanguage are the DAV: properties that 
should not be protected
+            protectedProperty =
+                    davName != null && (!(davName.equals("displayname") || 
davName.equals("getcontentlanguage")));
+        }
 
+        /**
+         * @return the updateType for this operation
+         */
+        public PropertyUpdateType getUpdateType() {
+            return this.updateType;
+        }
 
-    /**
-     * Generate propfind XML fragments for dead properties. This should be 
overridden by subclasses to provide useful
-     * behavior. The default implementation prevents setting protected 
properties (anything from the DAV: namespace),
-     * and sets 507 for a set attempt on dead properties.
-     *
-     * @param path         the resource path
-     * @param property     the dead property, if null then all dead properties 
must be written
-     * @param nameOnly     true if only the property name element should be 
generated
-     * @param generatedXML the current generated XML for the PROPFIND response
-     *
-     * @return true if property was specified and a corresponding dead 
property was found on the resource, false
-     *             otherwise
-     */
-    protected boolean propfindResource(String path, Node property, boolean 
nameOnly, XMLWriter generatedXML) {
-        if (nameOnly) {
-            generatedXML.writeElement("D", "displayname", 
XMLWriter.NO_CONTENT);
-        } else if (property == null) {
-            String resourceName = path;
-            int lastSlash = path.lastIndexOf('/');
-            if (lastSlash != -1) {
-                resourceName = resourceName.substring(lastSlash + 1);
-            }
-            generatedXML.writeElement("D", "displayname", XMLWriter.OPENING);
-            generatedXML.writeData(resourceName);
-            generatedXML.writeElement("D", "displayname", XMLWriter.CLOSING);
-        } else {
-            String davName = getDAVNode(property);
-            if ("displayname".equals(davName)) {
-                String resourceName = path;
-                int lastSlash = path.lastIndexOf('/');
-                if (lastSlash != -1) {
-                    resourceName = resourceName.substring(lastSlash + 1);
-                }
-                generatedXML.writeElement("D", "displayname", 
XMLWriter.OPENING);
-                generatedXML.writeData(resourceName);
-                generatedXML.writeElement("D", "displayname", 
XMLWriter.CLOSING);
-            }
+        /**
+         * @return the propertyNode the XML node that contains the property 
name (and value if SET)
+         */
+        public Node getPropertyNode() {
+            return this.propertyNode;
         }
-        return false;
-    }
 
+        /**
+         * @return the statusCode the statusCode to set as a result of the 
operation
+         */
+        public int getStatusCode() {
+            return this.statusCode;
+        }
 
-    /**
-     * Apply proppatch to the specified path. This should be overridden by 
subclasses to provide useful behavior. The
-     * default implementation prevents setting protected properties (anything 
from the DAV: namespace), and sets 507 for
-     * a set attempt on dead properties.
-     *
-     * @param path       the resource path on which to apply the proppatch
-     * @param operations the set and remove to apply, the final status codes 
of the result should be set on each
-     *                       operation
-     */
-    protected void proppatchResource(String path, 
ArrayList<ProppatchOperation> operations) {
-        boolean setProperty = false;
-        boolean protectedProperty = false;
-        // Check for the protected properties
-        for (ProppatchOperation operation : operations) {
-            if (operation.getUpdateType() == PropertyUpdateType.SET) {
-                setProperty = true;
-            }
-            if (operation.getProtectedProperty()) {
-                protectedProperty = true;
-                operation.setStatusCode(HttpServletResponse.SC_FORBIDDEN);
-            }
+        /**
+         * @param statusCode the statusCode to set as a result of the operation
+         */
+        public void setStatusCode(int statusCode) {
+            this.statusCode = statusCode;
         }
-        if (protectedProperty) {
-            for (ProppatchOperation operation : operations) {
-                if (!operation.getProtectedProperty()) {
-                    operation.setStatusCode(WebdavStatus.SC_FAILED_DEPENDENCY);
-                }
-            }
-        } else if (setProperty) {
-            // No dead property support
-            for (ProppatchOperation operation : operations) {
-                operation.setStatusCode(WebdavStatus.SC_INSUFFICIENT_STORAGE);
-            }
+
+        /**
+         * @return <code>true</code> if the property is protected
+         */
+        public boolean getProtectedProperty() {
+            return this.protectedProperty;
         }
     }
 
+    enum PropertyUpdateType {
+        SET,
+        REMOVE
+    }
+
+
+    // ------------------------------------------------------ Protected Methods
+
 
     /**
      * Return JAXP document builder instance.
@@ -694,7 +768,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
 
     @Override
     protected void doOptions(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
-        resp.addHeader("DAV", "1,2");
+        resp.addHeader("DAV", "1,2,3");
         resp.addHeader("Allow", determineMethodsAllowed(req));
         resp.addHeader("MS-Author-Via", "DAV");
     }
@@ -1009,7 +1083,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
             return;
         }
 
-        proppatchResource(path, operations);
+        store.proppatch(path, operations);
 
         resp.setStatus(WebdavStatus.SC_MULTI_STATUS);
 
@@ -2076,7 +2150,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                     return false;
                 }
             } else {
-                copyResource(source, dest);
+                store.copy(source, dest);
             }
 
             if (infiniteCopy) {
@@ -2120,7 +2194,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                     errorList.put(source, 
Integer.valueOf(WebdavStatus.SC_INTERNAL_SERVER_ERROR));
                     return false;
                 } else {
-                    copyResource(source, dest);
+                    store.copy(source, dest);
                 }
             } catch (IOException e) {
                 log(sm.getString("webdavservlet.inputstreamclosefail", 
source), e);
@@ -2186,7 +2260,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                 sendNotAllowed(req, resp);
                 return false;
             }
-            deleteResource(path);
+            unlockResource(path, null);
         } else {
 
             Map<String,Integer> errorList = new LinkedHashMap<>();
@@ -2209,7 +2283,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                     errorList.put(path, 
Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED));
                 }
             } else {
-                deleteResource(path);
+                unlockResource(path, null);
             }
 
             if (!errorList.isEmpty()) {
@@ -2243,6 +2317,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                 }
             }
         }
+        store.delete(path);
     }
 
 
@@ -2311,7 +2386,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                         errorList.put(childName, 
Integer.valueOf(WebdavStatus.SC_METHOD_NOT_ALLOWED));
                     }
                 } else {
-                    deleteResource(childName);
+                    unlockResource(childName, null);
                 }
             }
         }
@@ -2429,7 +2504,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                     generatedXML.writeElement("D", "collection", 
XMLWriter.NO_CONTENT);
                     generatedXML.writeElement("D", "resourcetype", 
XMLWriter.CLOSING);
                 }
-                propfindResource(path, null, false, generatedXML);
+                store.propfind(path, null, false, generatedXML);
 
                 generatedXML.writeElement("D", "supportedlock", 
XMLWriter.OPENING);
                 generatedXML.writeRaw(SUPPORTED_LOCKS);
@@ -2460,7 +2535,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                 }
                 generatedXML.writeElement("D", "resourcetype", 
XMLWriter.NO_CONTENT);
                 generatedXML.writeElement("D", "lockdiscovery", 
XMLWriter.NO_CONTENT);
-                propfindResource(path, null, true, generatedXML);
+                store.propfind(path, null, true, generatedXML);
 
                 generatedXML.writeElement("D", "prop", XMLWriter.CLOSING);
                 generatedXML.writeElement("D", "status", XMLWriter.OPENING);
@@ -2482,7 +2557,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
                 for (Node propertyNode : properties) {
                     String property = getDAVNode(propertyNode);
                     if (property == null) {
-                        if (!propfindResource(path, propertyNode, false, 
generatedXML)) {
+                        if (!store.propfind(path, propertyNode, false, 
generatedXML)) {
                             propertiesNotFound.add(propertyNode);
                         }
                     } else if (property.equals("creationdate")) {
@@ -2632,7 +2707,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
      *
      * @return the formatted creation date
      */
-    private String getISOCreationDate(long creationDate) {
+    private static String getISOCreationDate(long creationDate) {
         return creationDateFormat.format(new Date(creationDate));
     }
 
@@ -2645,6 +2720,16 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
     }
 
 
+    private static boolean propertyEquals(Node node1, Node node2) {
+        if (node1.getLocalName().equals(node2.getLocalName()) &&
+                ((node1.getNamespaceURI() == null && node2.getNamespaceURI() 
== null) ||
+                        (node1.getNamespaceURI() != null && 
node1.getNamespaceURI().equals(node2.getNamespaceURI())))) {
+            return true;
+        }
+        return false;
+    }
+
+
     // -------------------------------------------------- LockInfo Inner Class
 
     /**
@@ -2766,6 +2851,7 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
 
     // --------------------------------------------- WebdavResolver Inner Class
 
+
     /**
      * Work around for XML parsers that don't fully respect
      * {@link DocumentBuilderFactory#setExpandEntityReferences(boolean)} when 
called with <code>false</code>. External
@@ -2785,72 +2871,162 @@ public class WebdavServlet extends DefaultServlet 
implements PeriodicEventListen
         }
     }
 
-    // ----------------------------------------- ProppatchOperation Inner Class
+
+    // ------------------------------------- TransientPropertyStore Inner Class
+
 
     /**
-     * Represents a PROPPATCH sub operation to be performed.
+     * Default property store, which provides memory storage without 
persistence.
      */
-    protected static class ProppatchOperation {
-        private final PropertyUpdateType updateType;
-        private final Node propertyNode;
-        private final boolean protectedProperty;
-        private int statusCode = HttpServletResponse.SC_OK;
+    private class MemoryPropertyStore implements PropertyStore {
 
-        /**
-         * PROPPATCH operation constructor.
-         * @param updateType the update type, either SET or REMOVE
-         * @param propertyNode the XML node that contains the property name 
(and value if SET)
-         */
-        public ProppatchOperation(PropertyUpdateType updateType, Node 
propertyNode) {
-            this.updateType = updateType;
-            this.propertyNode = propertyNode;
-            String davName = getDAVNode(propertyNode);
-            // displayname and getcontentlanguage are the DAV: properties that 
should not be protected
-            protectedProperty =
-                    davName != null && (!(davName.equals("displayname") || 
davName.equals("getcontentlanguage")));
+        private final ConcurrentHashMap<String,ArrayList<Node>> deadProperties 
= new ConcurrentHashMap<>();
+
+        @Override
+        public void init() {
         }
 
-        /**
-         * @return the updateType for this operation
-         */
-        public PropertyUpdateType getUpdateType() {
-            return this.updateType;
+        @Override
+        public void destroy() {
         }
 
-        /**
-         * @return the propertyNode the XML node that contains the property 
name (and value if SET)
-         */
-        public Node getPropertyNode() {
-            return this.propertyNode;
+        @Override
+        public void periodicEvent() {
         }
 
-        /**
-         * @return the statusCode the statusCode to set as a result of the 
operation
-         */
-        public int getStatusCode() {
-            return this.statusCode;
+        @Override
+        public void copy(String source, String destination) {
+            ArrayList<Node> properties = deadProperties.get(source);
+            ArrayList<Node> propertiesDest = deadProperties.get(destination);
+            if (properties != null) {
+                if (propertiesDest == null) {
+                    propertiesDest = new ArrayList<>();
+                    deadProperties.put(destination, propertiesDest);
+                }
+                synchronized (properties) {
+                    synchronized (propertiesDest) {
+                        for (Node node : properties) {
+                            node = node.cloneNode(true);
+                            boolean found = false;
+                            for (int i = 0; i < propertiesDest.size(); i++) {
+                                Node propertyNode = propertiesDest.get(i);
+                                if (propertyEquals(node, propertyNode)) {
+                                    found = true;
+                                    propertiesDest.set(i, node);
+                                    break;
+                                }
+                            }
+                            if (!found) {
+                                propertiesDest.add(node);
+                            }
+                        }
+                    }
+                }
+            }
         }
 
-        /**
-         * @param statusCode the statusCode to set as a result of the operation
-         */
-        public void setStatusCode(int statusCode) {
-            this.statusCode = statusCode;
+        @Override
+        public void delete(String resource) {
+            deadProperties.remove(resource);
         }
 
-        /**
-         * @return <code>true</code> if the property is protected
-         */
-        public boolean getProtectedProperty() {
-            return this.protectedProperty;
+        @Override
+        public boolean propfind(String resource, Node property, boolean 
nameOnly, XMLWriter generatedXML) {
+            ArrayList<Node> properties = deadProperties.get(resource);
+            if (properties != null) {
+                synchronized (properties) {
+                    if (nameOnly) {
+                        // Add the names of all properties
+                        for (Node node : properties) {
+                            generatedXML.writeElement(null, 
node.getNamespaceURI(), node.getLocalName(),
+                                    XMLWriter.NO_CONTENT);
+                        }
+                    } else if (property != null) {
+                        // Add a single property
+                        Node foundNode = null;
+                        for (Node node : properties) {
+                            if (propertyEquals(node, property)) {
+                                foundNode = node;
+                            }
+                        }
+                        if (foundNode != null) {
+                            StringWriter strWriter = new StringWriter();
+                            DOMWriter domWriter = new DOMWriter(strWriter);
+                            domWriter.print(foundNode);
+                            generatedXML.writeRaw(strWriter.toString());
+                            return true;
+                        }
+                    } else {
+                        StringWriter strWriter = new StringWriter();
+                        DOMWriter domWriter = new DOMWriter(strWriter);
+                        // Add all properties
+                        for (Node node : properties) {
+                            domWriter.print(node);
+                        }
+                        generatedXML.writeRaw(strWriter.toString());
+                    }
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public void proppatch(String resource, ArrayList<ProppatchOperation> 
operations) {
+            boolean protectedProperty = false;
+            // Check for the protected properties
+            for (ProppatchOperation operation : operations) {
+                if (operation.getProtectedProperty()) {
+                    protectedProperty = true;
+                    operation.setStatusCode(HttpServletResponse.SC_FORBIDDEN);
+                }
+            }
+            if (protectedProperty) {
+                for (ProppatchOperation operation : operations) {
+                    if (!operation.getProtectedProperty()) {
+                        
operation.setStatusCode(WebdavStatus.SC_FAILED_DEPENDENCY);
+                    }
+                }
+            } else {
+                ArrayList<Node> properties = deadProperties.get(resource);
+                if (properties == null) {
+                    properties = new ArrayList<>();
+                    deadProperties.put(resource, properties);
+                }
+                synchronized (properties) {
+                    for (ProppatchOperation operation : operations) {
+                        if (operation.getUpdateType() == 
PropertyUpdateType.SET) {
+                            Node node = 
operation.getPropertyNode().cloneNode(true);
+                            boolean found = false;
+                            for (int i = 0; i < properties.size(); i++) {
+                                Node propertyNode = properties.get(i);
+                                if (propertyEquals(node, propertyNode)) {
+                                    found = true;
+                                    properties.set(i, node);
+                                    break;
+                                }
+                            }
+                            if (!found) {
+                                properties.add(node);
+                            }
+                        }
+                        if (operation.getUpdateType() == 
PropertyUpdateType.REMOVE) {
+                            Node node = operation.getPropertyNode();
+                            for (int i = 0; i < properties.size(); i++) {
+                                Node propertyNode = properties.get(i);
+                                if (propertyEquals(node, propertyNode)) {
+                                    properties.remove(i);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+            }
         }
-    }
 
-    enum PropertyUpdateType {
-        SET,
-        REMOVE
     }
 
+
 }
 
 
diff --git a/test/org/apache/catalina/servlets/TestWebdavServlet.java 
b/test/org/apache/catalina/servlets/TestWebdavServlet.java
index a55f1fb208..526ee9c6e8 100644
--- a/test/org/apache/catalina/servlets/TestWebdavServlet.java
+++ b/test/org/apache/catalina/servlets/TestWebdavServlet.java
@@ -20,6 +20,7 @@ import java.io.File;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.StringReader;
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -32,11 +33,15 @@ import org.junit.Test;
 
 import org.apache.catalina.Context;
 import org.apache.catalina.Wrapper;
+import org.apache.catalina.servlets.WebdavServlet.PropertyStore;
+import org.apache.catalina.servlets.WebdavServlet.ProppatchOperation;
 import org.apache.catalina.startup.SimpleHttpClient;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.catalina.startup.TomcatBaseTest;
+import org.apache.catalina.util.XMLWriter;
 import org.apache.tomcat.util.buf.ByteChunk;
 import org.apache.tomcat.websocket.server.WsContextListener;
+import org.w3c.dom.Node;
 import org.xml.sax.InputSource;
 
 public class TestWebdavServlet extends TomcatBaseTest {
@@ -246,7 +251,7 @@ public class TestWebdavServlet extends TomcatBaseTest {
         File tempWebapp = new File(getTemporaryDirectory(), 
"webdav-properties");
         Assert.assertTrue(tempWebapp.mkdirs());
         Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
-        Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new 
TransientPropertiesWebdavServlet());
+        Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new 
WebdavServlet());
         webdavServlet.addInitParameter("listings", "true");
         webdavServlet.addInitParameter("secret", "foo");
         webdavServlet.addInitParameter("readonly", "false");
@@ -1035,6 +1040,107 @@ public class TestWebdavServlet extends TomcatBaseTest {
 
     }
 
+    @Test
+    public void testPropertyStore() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        // Create a temp webapp that can be safely written to
+        File tempWebapp = new File(getTemporaryDirectory(), "webdav-store");
+        Assert.assertTrue(tempWebapp.mkdirs());
+        Context ctxt = tomcat.addContext("", tempWebapp.getAbsolutePath());
+        Wrapper webdavServlet = Tomcat.addServlet(ctxt, "webdav", new 
WebdavServlet());
+        webdavServlet.addInitParameter("listings", "true");
+        webdavServlet.addInitParameter("secret", "foo");
+        webdavServlet.addInitParameter("readonly", "false");
+        webdavServlet.addInitParameter("propertyStore", 
"org.apache.catalina.servlets.TestWebdavServlet$CustomPropertyStore");
+        webdavServlet.addInitParameter("store.propertyName", "mytestproperty");
+        webdavServlet.addInitParameter("store.propertyValue", "testvalue");
+        ctxt.addServletMappingDecoded("/*", "webdav");
+        ctxt.addMimeMapping("txt", "text/plain");
+        tomcat.start();
+
+        Client client = new Client();
+        client.setPort(getPort());
+
+        client.setRequest(new String[] { "PROPFIND / HTTP/1.1" + 
SimpleHttpClient.CRLF +
+                "Host: localhost:" + getPort() + SimpleHttpClient.CRLF +
+                "Connection: Close" + SimpleHttpClient.CRLF +
+                SimpleHttpClient.CRLF });
+        client.connect();
+        client.processRequest(true);
+        Assert.assertEquals(WebdavStatus.SC_MULTI_STATUS, 
client.getStatusCode());
+        
Assert.assertTrue(client.getResponseBody().contains(">testvalue</mytestproperty>"));
+        validateXml(client.getResponseBody());
+
+    }
+
+    public static class CustomPropertyStore implements PropertyStore {
+
+        private String propertyName = null;
+        private String propertyValue = null;
+
+        @Override
+        public void init() {
+        }
+
+        @Override
+        public void destroy() {
+        }
+
+        @Override
+        public void periodicEvent() {
+        }
+
+        @Override
+        public void copy(String source, String destination) {
+        }
+
+        @Override
+        public void delete(String resource) {
+        }
+
+        @Override
+        public boolean propfind(String resource, Node property, boolean 
nameOnly, XMLWriter generatedXML) {
+            generatedXML.writeElement(null, 
"https://tomcat.apache.org/testsuite";, propertyName, XMLWriter.OPENING);
+            generatedXML.writeText(propertyValue);
+            generatedXML.writeElement(null, propertyName, XMLWriter.CLOSING);
+            return true;
+        }
+
+        @Override
+        public void proppatch(String resource, ArrayList<ProppatchOperation> 
operations) {
+        }
+
+        /**
+         * @return the propertyName
+         */
+        public String getPropertyName() {
+            return this.propertyName;
+        }
+
+        /**
+         * @param propertyName the propertyName to set
+         */
+        public void setPropertyName(String propertyName) {
+            this.propertyName = propertyName;
+        }
+
+        /**
+         * @return the propertyValue
+         */
+        public String getPropertyValue() {
+            return this.propertyValue;
+        }
+
+        /**
+         * @param propertyValue the propertyValue to set
+         */
+        public void setPropertyValue(String propertyValue) {
+            this.propertyValue = propertyValue;
+        }
+
+    }
+
     private void validateXml(String xmlContent) throws Exception {
         SAXParserFactory.newInstance().newSAXParser().getXMLReader().parse(new 
InputSource(new StringReader(xmlContent)));
     }
diff --git 
a/test/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java 
b/test/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
deleted file mode 100644
index b27f974ad4..0000000000
--- a/test/org/apache/catalina/servlets/TransientPropertiesWebdavServlet.java
+++ /dev/null
@@ -1,186 +0,0 @@
-/*
- * 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.catalina.servlets;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.concurrent.ConcurrentHashMap;
-
-import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletRequest;
-import jakarta.servlet.http.HttpServletResponse;
-
-import org.apache.catalina.util.DOMWriter;
-import org.apache.catalina.util.XMLWriter;
-import org.w3c.dom.Node;
-
-/**
- * Extended WebDAV Servlet that implements dead properties using storage in 
memory.
- */
-public class TransientPropertiesWebdavServlet extends WebdavServlet {
-
-    private static final long serialVersionUID = 1L;
-
-    private final ConcurrentHashMap<String,ArrayList<Node>> deadProperties = 
new ConcurrentHashMap<>();
-
-    @Override
-    protected void doOptions(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
-        resp.addHeader("DAV", "1,2,3");
-        resp.addHeader("Allow", determineMethodsAllowed(req));
-        resp.addHeader("MS-Author-Via", "DAV");
-    }
-
-    @Override
-    protected void proppatchResource(String path, 
ArrayList<ProppatchOperation> operations) {
-        boolean protectedProperty = false;
-        // Check for the protected properties
-        for (ProppatchOperation operation : operations) {
-            if (operation.getProtectedProperty()) {
-                protectedProperty = true;
-                operation.setStatusCode(HttpServletResponse.SC_FORBIDDEN);
-            }
-        }
-        if (protectedProperty) {
-            for (ProppatchOperation operation : operations) {
-                if (!operation.getProtectedProperty()) {
-                    operation.setStatusCode(WebdavStatus.SC_FAILED_DEPENDENCY);
-                }
-            }
-        } else {
-            ArrayList<Node> properties = deadProperties.get(path);
-            if (properties == null) {
-                properties = new ArrayList<>();
-                deadProperties.put(path, properties);
-            }
-            synchronized (properties) {
-                for (ProppatchOperation operation : operations) {
-                    if (operation.getUpdateType() == PropertyUpdateType.SET) {
-                        Node node = 
operation.getPropertyNode().cloneNode(true);
-                        boolean found = false;
-                        for (int i = 0; i < properties.size(); i++) {
-                            Node propertyNode = properties.get(i);
-                            if (propertyEquals(node, propertyNode)) {
-                                found = true;
-                                properties.set(i, node);
-                                break;
-                            }
-                        }
-                        if (!found) {
-                            properties.add(node);
-                        }
-                    }
-                    if (operation.getUpdateType() == 
PropertyUpdateType.REMOVE) {
-                        Node node = operation.getPropertyNode();
-                        for (int i = 0; i < properties.size(); i++) {
-                            Node propertyNode = properties.get(i);
-                            if (propertyEquals(node, propertyNode)) {
-                                properties.remove(i);
-                                break;
-                            }
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    @Override
-    protected boolean propfindResource(String path, Node property, boolean 
nameOnly, XMLWriter generatedXML) {
-        ArrayList<Node> properties = deadProperties.get(path);
-        if (properties != null) {
-            synchronized (properties) {
-                if (nameOnly) {
-                    // Add the names of all properties
-                    for (Node node : properties) {
-                        generatedXML.writeElement(null, 
node.getNamespaceURI(), node.getLocalName(),
-                                XMLWriter.NO_CONTENT);
-                    }
-                } else if (property != null) {
-                    // Add a single property
-                    Node foundNode = null;
-                    for (Node node : properties) {
-                        if (propertyEquals(node, property)) {
-                            foundNode = node;
-                        }
-                    }
-                    if (foundNode != null) {
-                        StringWriter strWriter = new StringWriter();
-                        DOMWriter domWriter = new DOMWriter(strWriter);
-                        domWriter.print(foundNode);
-                        generatedXML.writeRaw(strWriter.toString());
-                        return true;
-                    }
-                } else {
-                    StringWriter strWriter = new StringWriter();
-                    DOMWriter domWriter = new DOMWriter(strWriter);
-                    // Add all properties
-                    for (Node node : properties) {
-                        domWriter.print(node);
-                    }
-                    generatedXML.writeRaw(strWriter.toString());
-                }
-            }
-        }
-        return false;
-    }
-
-    @Override
-    protected void copyResource(String source, String dest) {
-        ArrayList<Node> properties = deadProperties.get(source);
-        ArrayList<Node> propertiesDest = deadProperties.get(dest);
-        if (properties != null) {
-            if (propertiesDest == null) {
-                propertiesDest = new ArrayList<>();
-                deadProperties.put(dest, propertiesDest);
-            }
-            synchronized (properties) {
-                synchronized (propertiesDest) {
-                    for (Node node : properties) {
-                        node = node.cloneNode(true);
-                        boolean found = false;
-                        for (int i = 0; i < propertiesDest.size(); i++) {
-                            Node propertyNode = propertiesDest.get(i);
-                            if (propertyEquals(node, propertyNode)) {
-                                found = true;
-                                propertiesDest.set(i, node);
-                                break;
-                            }
-                        }
-                        if (!found) {
-                            propertiesDest.add(node);
-                        }
-                    }
-                }
-            }
-        }
-    }
-
-    @Override
-    protected void deleteResource(String path) {
-        deadProperties.remove(path);
-    }
-
-    private boolean propertyEquals(Node node1, Node node2) {
-        if (node1.getLocalName().equals(node2.getLocalName()) &&
-                ((node1.getNamespaceURI() == null && node2.getNamespaceURI() 
== null) ||
-                        (node1.getNamespaceURI() != null && 
node1.getNamespaceURI().equals(node2.getNamespaceURI())))) {
-            return true;
-        }
-        return false;
-    }
-}
\ No newline at end of file
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 59671bff7b..4a38201139 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -188,12 +188,16 @@
         Implement WebDAV <code>If</code> header using code from the Apache
         Jackrabbit project. (remm)
       </update>
+      <add>
+        Add <code>PropertyStore</code> interface in the WebDAV Servlet,
+        to allow implementation of dead properties storage. The store used
+        can be configured using the 'propertyStore' init parameter of the
+        WebDAV servlet. A simple non persistent implementation is used if no
+        custom store is configured. (remm)
+      </add>
       <update>
-        Add base WebDAV <code>PROPPATCH</code> implementation, to be extended
-        by overriding the <code>proppatchResource</code>,
-        <code>propfindResource</code>, <code>copyResource</code> and
-        <code>deleteResource</code> methods of the WebDAV Servlet to
-        provide the desired level of support for dead properties. (remm)
+        Implement WebDAV <code>PROPPATCH</code> method using the newly added
+        <code>PropertyStore</code>. (remm)
       </update>
     </changelog>
   </subsection>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org


Reply via email to