diff --git a/java/org/apache/catalina/servlets/DataSourcePropertyStore.java b/java/org/apache/catalina/servlets/DataSourcePropertyStore.java new file mode 100644 index 000000000000..397f365c81d1 --- /dev/null +++ b/java/org/apache/catalina/servlets/DataSourcePropertyStore.java @@ -0,0 +1,356 @@ +/* + * 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.StringWriter; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +import javax.naming.InitialContext; +import javax.naming.NamingException; +import javax.sql.DataSource; + +import jakarta.servlet.http.HttpServletResponse; + +import org.apache.catalina.servlets.WebdavServlet.PropertyUpdateType; +import org.apache.catalina.servlets.WebdavServlet.ProppatchOperation; +import org.apache.catalina.util.DOMWriter; +import org.apache.catalina.util.XMLWriter; +import org.apache.juli.logging.Log; +import org.apache.juli.logging.LogFactory; +import org.apache.tomcat.util.res.StringManager; +import org.w3c.dom.Node; + +/** + * WebDAV dead properties storage backed by a DataSource. Usually table and column names + * are configurable, but for simplicity this is not the case. + * The schema is: + * table properties ( path, namespace, name, node ) + * path: the resource path + * namespace: the node namespace + * name: the local name in the namespace + * node: the full serialized XML node including the name + */ +public class DataSourcePropertyStore implements WebdavServlet.PropertyStore { + + protected static final StringManager sm = StringManager.getManager(DataSourcePropertyStore.class); + private final Log log = LogFactory.getLog(DataSourcePropertyStore.class); + + private static String ADD_PROPERTY_STMT = "INSERT INTO properties (path, namespace, name, node) VALUES (?, ?, ?, ?)"; + private static String SET_PROPERTY_STMT = "UPDATE properties SET node = ? WHERE path = ? AND namespace = ? AND name = ?"; + private static String REMOVE_ALL_PROPERTIES_STMT = "DELETE FROM properties WHERE path = ?"; + private static String REMOVE_PROPERTY_STMT = "DELETE FROM properties WHERE path = ? AND namespace = ? AND name = ?"; + private static String GET_PROPERTY_STMT = "SELECT node FROM properties WHERE path = ? AND namespace = ? AND name = ?"; + private static String GET_PROPERTIES_NAMES_STMT = "SELECT namespace, name FROM properties WHERE path = ?"; + private static String GET_PROPERTIES_STMT = "SELECT namespace, name, node FROM properties WHERE path = ?"; + private static String GET_PROPERTIES_NODES_STMT = "SELECT node FROM properties WHERE path = ?"; + + /** + * DataSource JNDI name, will be prefixed with java:comp/env for the lookup. + */ + private String dataSourceName = "WebdavPropertyStore"; + + private final ReentrantReadWriteLock dbLock = new ReentrantReadWriteLock(); + private final Lock dbReadLock = dbLock.readLock(); + private final Lock dbWriteLock = dbLock.writeLock(); + + /** + * @return the dataSourceName + */ + public String getDataSourceName() { + return this.dataSourceName; + } + + /** + * @param dataSourceName the dataSourceName to set + */ + public void setDataSourceName(String dataSourceName) { + this.dataSourceName = dataSourceName; + } + + /** + * DataSource instance being used. + */ + protected DataSource dataSource = null; + + @Override + public void init() { + if (dataSource == null) { + try { + dataSource = (DataSource) ((new InitialContext()).lookup("java:comp/env/" + dataSourceName)); + } catch (NamingException e) { + throw new IllegalArgumentException(sm.getString("webdavservlet.dataSourceStore.noDataSource", dataSourceName), e); + } + } + } + + @Override + public void destroy() { + } + + @Override + public void periodicEvent() { + } + + @Override + public void copy(String source, String destination) { + if (dataSource == null) { + return; + } + dbWriteLock.lock(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(GET_PROPERTIES_STMT)) { + statement.setString(1, source); + if (statement.execute()) { + ResultSet rs = statement.getResultSet(); + while (rs.next()) { + String namespace = rs.getString(1); + String name = rs.getString(2); + String node = rs.getString(3); + boolean found = false; + try (PreparedStatement statement2 = connection.prepareStatement(GET_PROPERTY_STMT)) { + statement2.setString(1, destination); + statement2.setString(2, namespace); + statement2.setString(3, name); + if (statement2.execute()) { + ResultSet rs2 = statement2.getResultSet(); + if (rs2.next()) { + found = true; + } + } + } + if (found) { + try (PreparedStatement statement2 = connection.prepareStatement(SET_PROPERTY_STMT)) { + statement2.setString(1, node); + statement2.setString(2, destination); + statement2.setString(3, namespace); + statement2.setString(4, name); + statement2.execute(); + } + } else { + try (PreparedStatement statement2 = connection.prepareStatement(ADD_PROPERTY_STMT)) { + statement2.setString(1, destination); + statement2.setString(2, namespace); + statement2.setString(3, name); + statement2.setString(4, node); + statement2.execute(); + } + } + } + } + } catch (SQLException e) { + log.warn(sm.getString("webdavservlet.dataSourceStore.error", "copy", source), e); + } finally { + dbWriteLock.unlock(); + } + } + + @Override + public void delete(String resource) { + if (dataSource == null) { + return; + } + dbWriteLock.lock(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(REMOVE_ALL_PROPERTIES_STMT)) { + statement.setString(1, resource); + statement.execute(); + } catch (SQLException e) { + log.warn(sm.getString("webdavservlet.dataSourceStore.error", "delete", resource), e); + } finally { + dbWriteLock.unlock(); + } + } + + @Override + public boolean propfind(String resource, Node property, boolean nameOnly, XMLWriter generatedXML) { + if (dataSource == null) { + return false; + } + if (nameOnly) { + // Add the names of all properties + dbReadLock.lock(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(GET_PROPERTIES_NAMES_STMT)) { + statement.setString(1, resource); + if (statement.execute()) { + ResultSet rs = statement.getResultSet(); + while (rs.next()) { + String namespace = rs.getString(1); + String name = rs.getString(2); + generatedXML.writeElement(null, namespace, name, XMLWriter.NO_CONTENT); + } + } + } catch (SQLException e) { + log.warn(sm.getString("webdavservlet.dataSourceStore.error", "propfind", resource), e); + } finally { + dbReadLock.unlock(); + } + } else if (property != null) { + // Add a single property + dbReadLock.lock(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(GET_PROPERTY_STMT)) { + statement.setString(1, resource); + statement.setString(2, property.getNamespaceURI()); + statement.setString(3, property.getLocalName()); + if (statement.execute()) { + ResultSet rs = statement.getResultSet(); + if (rs.next()) { + String node = rs.getString(1); + generatedXML.writeRaw(node); + return true; + } + } + } catch (SQLException e) { + log.warn(sm.getString("webdavservlet.dataSourceStore.error", "propfind", resource), e); + } finally { + dbReadLock.unlock(); + } + } else { + // Add all properties + dbReadLock.lock(); + try (Connection connection = dataSource.getConnection(); + PreparedStatement statement = connection.prepareStatement(GET_PROPERTIES_NODES_STMT)) { + statement.setString(1, resource); + if (statement.execute()) { + ResultSet rs = statement.getResultSet(); + while (rs.next()) { + String node = rs.getString(1); + generatedXML.writeRaw(node); + } + } + } catch (SQLException e) { + log.warn(sm.getString("webdavservlet.dataSourceStore.error", "propfind", resource), e); + } finally { + dbReadLock.unlock(); + } + } + return false; + } + + @Override + public void proppatch(String resource, ArrayList 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 { + if (dataSource == null) { + for (ProppatchOperation operation : operations) { + operation.setStatusCode(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + return; + } + boolean failure = false; + dbWriteLock.lock(); + try (Connection connection = dataSource.getConnection()) { + connection.setAutoCommit(false); + for (ProppatchOperation operation : operations) { + if (operation.getUpdateType() == PropertyUpdateType.SET) { + Node node = operation.getPropertyNode().cloneNode(true); + StringWriter strWriter = new StringWriter(); + DOMWriter domWriter = new DOMWriter(strWriter); + domWriter.print(node); + String serializedNode = strWriter.toString(); + boolean found = false; + try { + try (PreparedStatement statement = connection.prepareStatement(GET_PROPERTY_STMT)) { + statement.setString(1, resource); + statement.setString(2, node.getNamespaceURI()); + statement.setString(3, node.getLocalName()); + if (statement.execute()) { + ResultSet rs = statement.getResultSet(); + if (rs.next()) { + found = true; + } + } + } + if (found) { + try (PreparedStatement statement = connection.prepareStatement(SET_PROPERTY_STMT)) { + statement.setString(1, serializedNode); + statement.setString(2, resource); + statement.setString(3, node.getNamespaceURI()); + statement.setString(4, node.getLocalName()); + statement.execute(); + } + } else { + try (PreparedStatement statement = connection.prepareStatement(ADD_PROPERTY_STMT)) { + statement.setString(1, resource); + statement.setString(2, node.getNamespaceURI()); + statement.setString(3, node.getLocalName()); + statement.setString(4, serializedNode); + statement.execute(); + } + } + } catch (SQLException e) { + failure = true; + operation.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + break; + } + } + if (operation.getUpdateType() == PropertyUpdateType.REMOVE) { + Node node = operation.getPropertyNode(); + try (PreparedStatement statement = connection.prepareStatement(REMOVE_PROPERTY_STMT)) { + statement.setString(1, resource); + statement.setString(2, node.getNamespaceURI()); + statement.setString(3, node.getLocalName()); + statement.execute(); + } catch (SQLException e) { + failure = true; + operation.setStatusCode(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + break; + } + } + } + if (failure) { + connection.rollback(); + for (ProppatchOperation operation : operations) { + if (operation.getStatusCode() == HttpServletResponse.SC_OK) { + operation.setStatusCode(WebdavStatus.SC_FAILED_DEPENDENCY); + } + } + } else { + connection.commit(); + } + } catch (SQLException e) { + log.warn(sm.getString("webdavservlet.dataSourceStore.error", "proppatch", resource), e); + for (ProppatchOperation operation : operations) { + operation.setStatusCode(WebdavStatus.SC_INTERNAL_SERVER_ERROR); + } + } finally { + dbWriteLock.unlock(); + } + } + } + +} diff --git a/java/org/apache/catalina/servlets/LocalStrings.properties b/java/org/apache/catalina/servlets/LocalStrings.properties index ed67a340491f..6f172cbbc8b0 100644 --- a/java/org/apache/catalina/servlets/LocalStrings.properties +++ b/java/org/apache/catalina/servlets/LocalStrings.properties @@ -61,3 +61,6 @@ webdavservlet.memorystore=Non persistent memory storage will be used for dead pr webdavservlet.nonWildcardMapping=The mapping [{0}] is not a wildcard mapping and should not be used for the WebDAV 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 + +webdavservlet.dataSourceStore.error=Error processing [{0}] on dead properties for path [{1}] +webdavservlet.dataSourceStore.noDataSource=DataSource [{0}] was not found in the webapp environment diff --git a/java/org/apache/catalina/servlets/WebdavServlet.java b/java/org/apache/catalina/servlets/WebdavServlet.java index 6d4a8f33791e..c7f7cfad5569 100644 --- a/java/org/apache/catalina/servlets/WebdavServlet.java +++ b/java/org/apache/catalina/servlets/WebdavServlet.java @@ -2888,7 +2888,7 @@ public InputSource resolveEntity(String publicId, String systemId) { /** * Default property store, which provides memory storage without persistence. */ - private class MemoryPropertyStore implements PropertyStore { + public static class MemoryPropertyStore implements PropertyStore { private final ConcurrentHashMap> deadProperties = new ConcurrentHashMap<>(); diff --git a/java/org/apache/catalina/util/DOMWriter.java b/java/org/apache/catalina/util/DOMWriter.java index 7ef95b1ffa5c..abedc0355b4c 100644 --- a/java/org/apache/catalina/util/DOMWriter.java +++ b/java/org/apache/catalina/util/DOMWriter.java @@ -66,6 +66,10 @@ public void print(Node node) { Attr attrs[] = sortAttributes(node.getAttributes()); boolean xmlns = false; for (Attr attr : attrs) { + if ("xmlns".equals(attr.getPrefix())) { + // Skip namespace prefixes as they are removed + continue; + } out.print(' '); out.print(attr.getLocalName()); if ("xmlns".equals(attr.getLocalName())) { diff --git a/test/org/apache/catalina/servlets/TestWebdavPropertyStore.java b/test/org/apache/catalina/servlets/TestWebdavPropertyStore.java new file mode 100644 index 000000000000..3f03ff710e03 --- /dev/null +++ b/test/org/apache/catalina/servlets/TestWebdavPropertyStore.java @@ -0,0 +1,245 @@ +/* + * 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.ByteArrayInputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.SQLFeatureNotSupportedException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.logging.Logger; + +import javax.sql.DataSource; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; + +import org.apache.catalina.servlets.WebdavServlet.PropertyStore; +import org.apache.catalina.servlets.WebdavServlet.PropertyUpdateType; +import org.apache.catalina.servlets.WebdavServlet.ProppatchOperation; +import org.apache.catalina.startup.LoggingBaseTest; +import org.apache.catalina.util.XMLWriter; +import org.w3c.dom.Document; +import org.w3c.dom.Node; +import org.xml.sax.InputSource; + +@RunWith(Parameterized.class) +public class TestWebdavPropertyStore extends LoggingBaseTest { + + private static final String PROPERTY1 = + "\n" + + "\n" + + " \n" + + ""; + + private static final String PROPERTY2 = + "\n" + + "\n" + + " bla\n" + + ""; + + private static final String PROPERTY3 = + "\n" + + "\n" + + " foooooooo\n" + + ""; + + public static final String SIMPLE_SCHEMA = + "create table properties (\n" + + " path varchar(256) not null,\n" + + " namespace varchar(64) not null,\n" + + " name varchar(64) not null,\n" + + " node varchar(1024) not null" + + ")"; + + public static class CustomDataSourcePropertyStore extends DataSourcePropertyStore { + public void setDataSource(DataSource dataSource) { + this.dataSource = dataSource; + } + } + + private class DerbyDataSource implements DataSource { + + Connection connection = null; + + DerbyDataSource() { + try { + Class.forName("org.apache.derby.jdbc.EmbeddedDriver"); + connection = DriverManager.getConnection("jdbc:derby:" + getTemporaryDirectory().getAbsolutePath() + + "/webdavproperties;create=true"); + try (Statement statement = connection.createStatement()) { + statement.execute(SIMPLE_SCHEMA); + } + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @Override + public Logger getParentLogger() throws SQLFeatureNotSupportedException { + return null; + } + + @Override + public T unwrap(Class iface) throws SQLException { + return null; + } + + @Override + public boolean isWrapperFor(Class iface) throws SQLException { + return false; + } + + @Override + public Connection getConnection() throws SQLException { + if (connection.isClosed()) { + connection = DriverManager.getConnection("jdbc:derby:" + getTemporaryDirectory().getAbsolutePath() + + "/webdavproperties"); + } + return connection; + } + + @Override + public Connection getConnection(String username, String password) throws SQLException { + return getConnection(); + } + + @Override + public PrintWriter getLogWriter() throws SQLException { + return null; + } + + @Override + public void setLogWriter(PrintWriter out) throws SQLException { + } + + @Override + public void setLoginTimeout(int seconds) throws SQLException { + } + + @Override + public int getLoginTimeout() throws SQLException { + return 0; + } + + } + + @Parameterized.Parameters(name = "{0}") + public static Collection parameters() { + List parameterSets = new ArrayList<>(); + parameterSets.add(new Object[] { "org.apache.catalina.servlets.WebdavServlet$MemoryPropertyStore" }); + parameterSets.add(new Object[] { "org.apache.catalina.servlets.TestWebdavPropertyStore$CustomDataSourcePropertyStore" }); + return parameterSets; + } + + @Parameter(0) + public String storeName; + + @Test + public void testStore() throws Exception { + DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance(); + documentBuilderFactory.setNamespaceAware(true); + documentBuilderFactory.setExpandEntityReferences(false); + DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder(); + Document document1 = documentBuilder.parse(new InputSource(new ByteArrayInputStream(PROPERTY1.getBytes(StandardCharsets.UTF_8)))); + Node node1 = document1.getDocumentElement(); + Document document2 = documentBuilder.parse(new InputSource(new ByteArrayInputStream(PROPERTY2.getBytes(StandardCharsets.UTF_8)))); + Node node2 = document2.getDocumentElement(); + Document document3 = documentBuilder.parse(new InputSource(new ByteArrayInputStream(PROPERTY3.getBytes(StandardCharsets.UTF_8)))); + Node node3 = document3.getDocumentElement(); + + PropertyStore propertyStore = (PropertyStore) Class.forName(storeName).getDeclaredConstructor().newInstance(); + if (propertyStore instanceof CustomDataSourcePropertyStore) { + ((CustomDataSourcePropertyStore) propertyStore).setDataSource(new DerbyDataSource()); + } + + // Add properties + ArrayList operations = new ArrayList<>(); + operations.add(new ProppatchOperation(PropertyUpdateType.SET, node1)); + operations.add(new ProppatchOperation(PropertyUpdateType.SET, node2)); + propertyStore.proppatch("/some/path1", operations); + + // Add properties + operations = new ArrayList<>(); + operations.add(new ProppatchOperation(PropertyUpdateType.SET, node1)); + propertyStore.proppatch("/other/path2", operations); + + // Get single property + XMLWriter xmlWriter1 = new XMLWriter(); + Assert.assertTrue(propertyStore.propfind("/some/path1", node1, false, xmlWriter1)); + Assert.assertTrue(xmlWriter1.toString().contains("bla")); + + propertyStore.copy("/some/path1", "/some/path2"); + XMLWriter xmlWriter4 = new XMLWriter(); + Assert.assertFalse(propertyStore.propfind("/some/path2", null, true, xmlWriter4)); + Assert.assertTrue(xmlWriter4.toString().contains("(); + operations.add(new ProppatchOperation(PropertyUpdateType.REMOVE, node1)); + propertyStore.proppatch("/other/path2", operations); + + XMLWriter xmlWriter7 = new XMLWriter(); + Assert.assertFalse(propertyStore.propfind("/other/path2", null, false, xmlWriter7)); + Assert.assertFalse(xmlWriter7.toString().contains("bla")); + + operations = new ArrayList<>(); + operations.add(new ProppatchOperation(PropertyUpdateType.SET, node3)); + propertyStore.proppatch("/other/path2", operations); + + XMLWriter xmlWriter8 = new XMLWriter(); + Assert.assertFalse(propertyStore.propfind("/other/path2", null, false, xmlWriter8)); + Assert.assertFalse(xmlWriter8.toString().contains("foooooooo")); + + XMLWriter xmlWriter9 = new XMLWriter(); + Assert.assertFalse(propertyStore.propfind("/other/path2", node1, false, xmlWriter9)); + Assert.assertTrue(xmlWriter9.toString().isEmpty()); + + } +} diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml index 900ddb931727..a1e359e1c884 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -228,6 +228,10 @@ the default servlet. It will be removed in Tomcat 12 onwards where it will effectively be hard coded to true. (markt) + + Add DataSource based property storage for the + WebdavServlet. (remm) +