You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maven.apache.org by sl...@apache.org on 2026/09/04 11:33:16 UTC
(maven-wagon) branch master updated: Give MKCOL the status vocabulary RFC 4918 defines
This is an automated email from the ASF dual-hosted git repository.
slachiewicz pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-wagon.git
The following commit(s) were added to refs/heads/master by this push:
new c0a7f255 Give MKCOL the status vocabulary RFC 4918 defines
c0a7f255 is described below
commit c0a7f2553c7fdcafbcacf03bf45db1db7a234560
Author: Sylwester Lachiewicz <sl...@apache.org>
AuthorDate: Fri Sep 4 11:49:18 2026 +0200
Give MKCOL the status vocabulary RFC 4918 defines
Only 409 means an ancestor is missing, so only 409 should send the walk
further back; treating every other answer that way turned a 403 or an
authentication failure into a confusing error about the top of the path.
On the way down, 405 means the collection is already there, which is as
good as having created it -- rejecting it failed uploads whenever a
collection appeared underneath us. Do not narrow either set back to
201-only; the walk is not atomic and the server is entitled to both.
---
.../maven/wagon/providers/webdav/WebDavWagon.java | 39 ++++++-
.../providers/webdav/WebDavWagonMkColTest.java | 126 +++++++++++++++++++++
2 files changed, 159 insertions(+), 6 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 49ef3625..8d2cc95f 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
@@ -108,30 +108,57 @@ protected void mkdirs(String dir) throws IOException {
// create relative path that will always have a leading and trailing slash
String relpath = FileUtils.normalize(getPath(basedir, dir) + "/");
+ mkColPath(baseUrl, relpath);
+ }
+
+ /**
+ * Creates every missing collection along {@code relpath}, one at a time.
+ * <p>
+ * RFC 4918 section 9.3.1 gives MKCOL a small vocabulary, and only two of the answers mean "keep going".
+ * {@code 201 Created} is a collection we made, {@code 405 Method Not Allowed} one that was already there, and
+ * {@code 409 Conflict} says an ancestor is missing -- which is the only reason to step further back. Anything
+ * else, a {@code 403} or an authentication failure for instance, is reported where it happens rather than
+ * being mistaken for a missing parent.
+ *
+ * @param baseUrl scheme, host and port of the repository, without a trailing slash
+ * @param relpath repository-relative path with a leading and trailing slash
+ */
+ void mkColPath(String baseUrl, String relpath) throws IOException {
PathNavigator navigator = new PathNavigator(relpath);
- // traverse backwards until we hit a directory that already exists (OK/NOT_ALLOWED), or that we were able to
- // create (CREATED), or until we get to the top of the path
- int status = -1;
+ // step backwards while the server reports a missing ancestor, until one exists or we create one
+ int status;
do {
String url = baseUrl + "/" + navigator.getPath();
status = doMkCol(url);
if (status == HttpStatus.SC_CREATED || status == HttpStatus.SC_METHOD_NOT_ALLOWED) {
break;
}
+ if (status != HttpStatus.SC_CONFLICT) {
+ throw new IOException(unableToCreate(url, status));
+ }
} while (navigator.backward());
+ if (status != HttpStatus.SC_CREATED && status != HttpStatus.SC_METHOD_NOT_ALLOWED) {
+ throw new IOException(unableToCreate(baseUrl + "/" + navigator.getPath(), status));
+ }
+
// traverse forward creating missing directories
while (navigator.forward()) {
String url = baseUrl + "/" + navigator.getPath();
status = doMkCol(url);
- if (status != HttpStatus.SC_CREATED) {
- throw new IOException("Unable to create collection: " + url + "; status code = " + status);
+ // a collection that already exists is as good as one we just created
+ if (status != HttpStatus.SC_CREATED && status != HttpStatus.SC_METHOD_NOT_ALLOWED) {
+ throw new IOException(unableToCreate(url, status));
}
}
}
- private int doMkCol(String url) throws IOException {
+ private static String unableToCreate(String url, int status) {
+ return "Unable to create collection: " + url + "; status code = " + status;
+ }
+
+ int doMkCol(String url) throws IOException {
DavMethods.HttpMkcol method = new DavMethods.HttpMkcol(url);
try (CloseableHttpResponse closeableHttpResponse = execute(method)) {
return closeableHttpResponse.getStatusLine().getStatusCode();
diff --git a/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonMkColTest.java b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonMkColTest.java
new file mode 100644
index 00000000..a906383d
--- /dev/null
+++ b/wagon-providers/wagon-webdav-jackrabbit/src/test/java/org/apache/maven/wagon/providers/webdav/WebDavWagonMkColTest.java
@@ -0,0 +1,126 @@
+/*
+ * 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.maven.wagon.providers.webdav;
+
+import java.io.IOException;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Deque;
+import java.util.List;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Drives the MKCOL status handling of {@link WebDavWagon#mkColPath} with scripted responses, which the
+ * server-backed tests in {@link WebDavWagonTest} cannot do: a real server never answers 403 on demand, and never
+ * loses a race that turns a forward MKCOL into a 405.
+ */
+class WebDavWagonMkColTest {
+
+ private static final String BASE = "http://host";
+
+ @Test
+ void createsEveryMissingCollectionFromTheTopDown() throws IOException {
+ // nothing exists, so the walk back reports a missing ancestor until the root, then creates on the way down
+ ScriptedWagon wagon = new ScriptedWagon(409, 409, 409, 201, 201, 201, 201);
+
+ wagon.mkColPath(BASE, "/a/b/c/");
+
+ assertEquals(
+ Arrays.asList(
+ BASE + "/a/b/c/",
+ BASE + "/a/b/",
+ BASE + "/a/",
+ BASE + "/",
+ BASE + "/a/",
+ BASE + "/a/b/",
+ BASE + "/a/b/c/"),
+ wagon.requested);
+ }
+
+ @Test
+ void stopsWalkingBackAtACollectionThatAlreadyExists() throws IOException {
+ // 405 means "already a collection here", so the walk back stops and only the rest is created
+ ScriptedWagon wagon = new ScriptedWagon(409, 409, 405, 201, 201);
+
+ wagon.mkColPath(BASE, "/a/b/c/");
+
+ assertEquals(5, wagon.requested.size());
+ assertEquals(BASE + "/a/", wagon.requested.get(2));
+ }
+
+ @Test
+ void toleratesACollectionAppearingUnderneathUsOnTheWayDown() throws IOException {
+ // someone else created a/b between our walk back and our walk down; 405 there is not a failure
+ ScriptedWagon wagon = new ScriptedWagon(409, 409, 405, 405, 201);
+
+ wagon.mkColPath(BASE, "/a/b/c/");
+
+ assertEquals(5, wagon.requested.size());
+ }
+
+ @Test
+ void reportsARefusalWhereItHappensRatherThanWalkingPastIt() {
+ // 403 is not "the parent is missing", so it must stop at once instead of walking to the root
+ ScriptedWagon wagon = new ScriptedWagon(403);
+
+ IOException e = assertThrows(IOException.class, () -> wagon.mkColPath(BASE, "/a/b/c/"));
+
+ assertEquals(1, wagon.requested.size(), "walked past a refusal instead of reporting it");
+ assertTrue(e.getMessage().contains(BASE + "/a/b/c/"), e.getMessage());
+ assertTrue(e.getMessage().contains("403"), e.getMessage());
+ }
+
+ @Test
+ void failsWhenTheWholePathIsExhaustedAndStillConflicting() {
+ // the server claims a missing ancestor all the way to the root; there is nowhere left to go
+ ScriptedWagon wagon = new ScriptedWagon(409, 409, 409, 409);
+
+ IOException e = assertThrows(IOException.class, () -> wagon.mkColPath(BASE, "/a/b/c/"));
+
+ assertEquals(4, wagon.requested.size());
+ assertTrue(e.getMessage().contains("409"), e.getMessage());
+ }
+
+ private static final class ScriptedWagon extends WebDavWagon {
+
+ private final Deque<Integer> statuses = new ArrayDeque<>();
+ private final List<String> requested = new ArrayList<>();
+
+ ScriptedWagon(int... statuses) {
+ for (int status : statuses) {
+ this.statuses.add(status);
+ }
+ }
+
+ @Override
+ int doMkCol(String url) {
+ requested.add(url);
+ if (statuses.isEmpty()) {
+ throw new IllegalStateException("unexpected MKCOL of " + url + "; the script ran out");
+ }
+ return statuses.removeFirst();
+ }
+ }
+}