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