From 3f095c315aa7946ad65653a51f791fe576d732c6 Mon Sep 17 00:00:00 2001 From: Sylwester Lachiewicz Date: Tue, 30 Dec 2025 23:07:02 +0100 Subject: [PATCH] [WAGON-583] WebDavWagon and tests now adhere to RFC 4918 - Handle 409 Conflict status when parent collections don't exist - Handle redirects on MKCOL operations - Accept 200 OK status for already-existing collections - Fix tests to use WebDAV PROPFIND instead of filesystem checks --- .../wagon/providers/webdav/WebDavWagon.java | 51 +++++++++++++++++-- .../providers/webdav/WebDavWagonTest.java | 40 +++++++-------- 2 files changed, 67 insertions(+), 24 deletions(-) diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java index 97a57912..d3f4b862 100644 --- a/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java +++ b/wagon-providers/wagon-webdav-jackrabbit/src/main/java/org/apache/maven/wagon/providers/webdav/WebDavWagon.java @@ -122,16 +122,24 @@ protected void mkdirs(String dir) throws IOException { do { String url = baseUrl + "/" + navigator.getPath(); status = doMkCol(url); - if (status == HttpStatus.SC_CREATED || status == HttpStatus.SC_METHOD_NOT_ALLOWED) { + // RFC 4918: Accept 201 Created, 200 OK (already exists), 405 Method Not Allowed (already exists) + if (status == HttpStatus.SC_CREATED + || status == HttpStatus.SC_OK + || status == HttpStatus.SC_METHOD_NOT_ALLOWED) { break; } + // RFC 4918: 409 Conflict means intermediate collection is missing, continue traversing backwards + if (status == HttpStatus.SC_CONFLICT) { + continue; + } } while (navigator.backward()); // traverse forward creating missing directories while (navigator.forward()) { String url = baseUrl + "/" + navigator.getPath(); status = doMkCol(url); - if (status != HttpStatus.SC_CREATED) { + // RFC 4918: Accept 201 Created or 200 OK (if collection already exists from another request) + if (status != HttpStatus.SC_CREATED && status != HttpStatus.SC_OK) { throw new IOException("Unable to create collection: " + url + "; status code = " + status); } } @@ -140,7 +148,21 @@ protected void mkdirs(String dir) throws IOException { private int doMkCol(String url) throws IOException { HttpMkcol method = new HttpMkcol(url); try (CloseableHttpResponse closeableHttpResponse = execute(method)) { - return closeableHttpResponse.getStatusLine().getStatusCode(); + int statusCode = closeableHttpResponse.getStatusLine().getStatusCode(); + + // RFC 4918: Handle redirects for MKCOL + // 3xx redirects should be followed to the new location + if (statusCode >= HttpStatus.SC_MULTIPLE_CHOICES && statusCode < HttpStatus.SC_BAD_REQUEST) { + org.apache.http.Header locationHeader = closeableHttpResponse.getFirstHeader("Location"); + if (locationHeader != null) { + String redirectUrl = locationHeader.getValue(); + // Recursive call to handle redirect - execute() will handle the redirect automatically + // but we need to return the final status + return doMkCol(redirectUrl); + } + } + + return statusCode; } catch (HttpException e) { throw new IOException(e.getMessage(), e); } finally { @@ -172,6 +194,29 @@ public void putDirectory(File sourceDirectory, String destinationDirectory) } } + protected boolean collectionExists(String url) throws IOException { + DavPropertyNameSet nameSet = new DavPropertyNameSet(); + nameSet.add(DavPropertyName.create(DavConstants.PROPERTY_RESOURCETYPE)); + + CloseableHttpResponse closeableHttpResponse = null; + HttpPropfind method = null; + try { + method = new HttpPropfind(url, nameSet, DavConstants.DEPTH_0); + closeableHttpResponse = execute(method); + int statusCode = closeableHttpResponse.getStatusLine().getStatusCode(); + return statusCode == HttpStatus.SC_OK || statusCode == HttpStatus.SC_MULTI_STATUS; + } catch (HttpException e) { + throw new IOException(e.getMessage(), e); + } finally { + if (method != null) { + method.releaseConnection(); + } + if (closeableHttpResponse != null) { + closeableHttpResponse.close(); + } + } + } + private boolean isDirectory(String url) throws IOException, DavException { DavPropertyNameSet nameSet = new DavPropertyNameSet(); nameSet.add(DavPropertyName.create(DavConstants.PROPERTY_RESOURCETYPE)); diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonTest.java b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonTest.java index 223e2888..b458f362 100644 --- a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonTest.java +++ b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonTest.java @@ -137,34 +137,35 @@ public void testMkdirs() throws Exception { wagon.connect(testRepository, getAuthInfo()); try { - File dir = getRepositoryDirectory(); - - // check basedir also doesn't exist and will need to be created - dir = new File(dir, testRepository.getBasedir()); - assertFalse(dir.exists()); + String repositoryUrl = testRepository.getUrl(); // test leading / - assertFalse(new File(dir, "foo").exists()); + String fooUrl = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + "foo"; + assertFalse("Collection should not exist before creation", wagon.collectionExists(fooUrl)); wagon.mkdirs("/foo"); - assertTrue(new File(dir, "foo").exists()); + assertTrue("Collection should exist after creation", wagon.collectionExists(fooUrl)); // test trailing / - assertFalse(new File(dir, "bar").exists()); + String barUrl = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + "bar"; + assertFalse("Collection should not exist before creation", wagon.collectionExists(barUrl)); wagon.mkdirs("bar/"); - assertTrue(new File(dir, "bar").exists()); + assertTrue("Collection should exist after creation", wagon.collectionExists(barUrl)); - // test when already exists + // test when already exists (should not fail) wagon.mkdirs("bar"); + assertTrue("Collection should still exist", wagon.collectionExists(barUrl)); // test several parts - assertFalse(new File(dir, "1/2/3/4").exists()); + String deepUrl = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + "1/2/3/4"; + assertFalse("Deep collection should not exist before creation", wagon.collectionExists(deepUrl)); wagon.mkdirs("1/2/3/4"); - assertTrue(new File(dir, "1/2/3/4").exists()); + assertTrue("Deep collection should exist after creation", wagon.collectionExists(deepUrl)); // test additional part and trailing / - assertFalse(new File(dir, "1/2/3/4/5").exists()); + String deeperUrl = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + "1/2/3/4/5"; + assertFalse("Deeper collection should not exist before creation", wagon.collectionExists(deeperUrl)); wagon.mkdirs("1/2/3/4/5/"); - assertTrue(new File(dir, "1/2/3/4").exists()); + assertTrue("Deeper collection should exist after creation", wagon.collectionExists(deeperUrl)); } finally { wagon.disconnect(); @@ -186,16 +187,13 @@ public void testMkdirsWithNoBasedir() throws Exception { wagon.connect(testRepository, getAuthInfo()); try { - File dir = getRepositoryDirectory(); - - // check basedir also doesn't exist and will need to be created - dir = new File(dir, testRepository.getBasedir()); - assertTrue(dir.exists()); + String repositoryUrl = testRepository.getUrl(); // test leading / - assertFalse(new File(dir, "foo").exists()); + String fooUrl = repositoryUrl + (repositoryUrl.endsWith("/") ? "" : "/") + "foo"; + assertFalse("Collection should not exist before creation", wagon.collectionExists(fooUrl)); wagon.mkdirs("/foo"); - assertTrue(new File(dir, "foo").exists()); + assertTrue("Collection should exist after creation", wagon.collectionExists(fooUrl)); } finally { wagon.disconnect();