This is an automated email from the ASF dual-hosted git repository.
hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git
The following commit(s) were added to refs/heads/main by this push:
new f75a439056 WebDAV VFS and metadata type #7123 (#7124)
f75a439056 is described below
commit f75a439056c138a6a6636140d657ff5149152e41
Author: Bart Maertens <[email protected]>
AuthorDate: Mon May 18 11:33:14 2026 +0000
WebDAV VFS and metadata type #7123 (#7124)
* WebDAV: expose metadata name in FileName URIs (logical scheme)
HopWebDavConnectionFileName wraps Webdav4FileName so getURI/getRootURI/
getFriendlyURI use the connection metadata name (e.g. nextcloud:///path)
while wire host/path stay on webdav4 for HTTP.
Undo: git revert this commit (files: HopWebDavLogicalUris.java,
HopWebDavConnectionFileName.java, parser + tests).
Co-authored-by: Cursor <[email protected]>
* WebDAV: wrap listed/resolved files with logical connection URI
Webdav4FileSystem.createFile was still attaching plain Webdav4FileName
from PROPFIND/listing. Use HopWebDavConnectionFileSystem to wrap every
Webdav4FileName so HopVfsFileDialog and getURI() show the metadata scheme.
Undo: git revert this commit.
* WebDAV: logical URIs after PROPFIND list (Get File Names)
doListChildrenResolved returns FileObjects that bypass createFile;
subclass Webdav4FileObject to re-resolve children with
HopWebDavConnectionFileName.
Undo: git revert this commit.
* WebDAV: logical getScheme + resolveName URI shape for child listing
DefaultFileSystemManager.resolveName picks the provider from
parent.getScheme();
HopWebDavConnectionFileName still reported webdav4 so PROPFIND child
resolution
used the global Webdav4FileProvider. Return the connection name from
getScheme,
keep wireScheme for Webdav4FileName children, and parse resolveName-style
URIs
(rootURI + full wire path) into the correct synthetic webdav4 URL.
Undo: git revert this commit.
* initial metadata type for webdav
* initial webdav metadata type working version
* webdav doc and i18n updates
* cleanup #7123
---------
---
assemblies/plugins/pom.xml | 6 +
core/pom.xml | 4 +
.../hop/metadata/api/HopMetadataPropertyType.java | 1 +
docs/hop-user-manual/modules/ROOT/nav.adoc | 1 +
.../modules/ROOT/pages/metadata-types/index.adoc | 1 +
.../pages/metadata-types/webdav-connection.adoc | 58 ++++++
docs/hop-user-manual/modules/ROOT/pages/vfs.adoc | 11 +-
lib/pom.xml | 5 +
plugins/tech/pom.xml | 1 +
plugins/tech/{ => webdav}/pom.xml | 50 +++--
plugins/tech/webdav/src/assembly/assembly.xml | 53 +++++
.../hop/vfs/webdav/HopRawWebDavFileSystem.java | 38 ++++
.../hop/vfs/webdav/HopWebDavConnectionAuth.java | 44 ++++
.../apache/hop/vfs/webdav/HopWebDavFileName.java | 91 ++++++++
.../hop/vfs/webdav/HopWebDavFileNameParser.java | 67 ++++++
.../apache/hop/vfs/webdav/HopWebDavFileObject.java | 91 ++++++++
.../hop/vfs/webdav/HopWebDavFileProvider.java | 106 ++++++++++
.../apache/hop/vfs/webdav/HopWebDavFileSystem.java | 141 +++++++++++++
.../hop/vfs/webdav/HopWebDavLogicalUris.java | 95 +++++++++
.../hop/vfs/webdav/HopWebDavWireBootstrap.java | 79 +++++++
.../apache/hop/vfs/webdav/HopWebDavWireNames.java | 51 +++++
.../hop/vfs/webdav/WebDavSecureVfsPlugin.java | 51 +++++
.../org/apache/hop/vfs/webdav/WebDavVfsPlugin.java | 69 ++++++
.../hop/vfs/webdav/metadata/WebDavConnection.java | 66 ++++++
.../webdav/metadata/WebDavConnectionEditor.java | 232 +++++++++++++++++++++
.../metadata/messages/messages_en_US.properties | 26 +++
plugins/tech/webdav/src/main/resources/version.xml | 20 ++
.../vfs/webdav/HopWebDavFileNameParserTest.java | 67 ++++++
.../hop/vfs/webdav/HopWebDavFileNameTest.java | 55 +++++
.../hop/vfs/webdav/HopWebDavLogicalUrisTest.java | 81 +++++++
.../apache/hop/ui/core/vfs/HopVfsFileDialog.java | 2 +-
31 files changed, 1637 insertions(+), 26 deletions(-)
diff --git a/assemblies/plugins/pom.xml b/assemblies/plugins/pom.xml
index 940320ef30..3e7e6fbe81 100644
--- a/assemblies/plugins/pom.xml
+++ b/assemblies/plugins/pom.xml
@@ -811,6 +811,12 @@
<version>${project.version}</version>
<type>zip</type>
</dependency>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-tech-webdav</artifactId>
+ <version>${project.version}</version>
+ <type>zip</type>
+ </dependency>
<dependency>
<groupId>org.apache.hop</groupId>
<artifactId>hop-transform-abort</artifactId>
diff --git a/core/pom.xml b/core/pom.xml
index fb80755901..afe26470d8 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -221,6 +221,10 @@
</exclusion>
</exclusions>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-vfs2-jackrabbit2</artifactId>
+ </dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpcore</artifactId>
diff --git
a/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java
b/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java
index cf0e62e867..bc72cd8cd5 100644
---
a/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java
+++
b/core/src/main/java/org/apache/hop/metadata/api/HopMetadataPropertyType.java
@@ -100,6 +100,7 @@ public enum HopMetadataPropertyType {
VFS_AZURE_CONNECTION,
VFS_MINIO_CONNECTION,
VFS_S3_CONNECTION,
+ VFS_WEBDAV_CONNECTION,
// DATA STREAM
DATA_STREAM,
diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index 40fedf4405..a741cefcfc 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -461,6 +461,7 @@ under the License.
***
xref:metadata-types/variable-resolver/pipeline-variable-resolver.adoc[Pipeline
variable resolver]
** xref:metadata-types/static-schema-definition.adoc[Static Schema Definition]
** xref:hop-server/web-service.adoc[Web Service]
+** xref:metadata-types/webdav-connection.adoc[WebDAV Connection]
** xref:metadata-types/workflow-log.adoc[Workflow Log]
** xref:metadata-types/workflow-run-config.adoc[Workflow Run Configuration]
* xref:password/passwords.adoc[Passwords]
diff --git a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc
index 505eff74ce..2252c38e26 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/index.adoc
@@ -48,6 +48,7 @@ xref:metadata-types/data-stream/data-stream.adoc[Data
Stream]: A Data Stream can
* xref:metadata-types/mongodb-connection.adoc[MongoDB Connection]: Describes a
MongoDB connection
* xref:metadata-types/mail-server-connection.adoc[Mail Server Connection]:
Describes a mail server connection
* xref:metadata-types/minio-connection.adoc[Minio (S3) Connection]: Connect to
one or more S3 endpoints using a Minio client
+* xref:metadata-types/webdav-connection.adoc[WebDAV Connection]: Named WebDAV
or WebDAV-over-HTTPS endpoint used as a VFS scheme in file paths
* xref:metadata-types/neo4j/neo4j-connection.adoc[Neo4j Connection]: A shared
connection to a Neo4j server
* xref:metadata-types/neo4j/neo4j-graphmodel.adoc[Neo4j Graph Model]:
Description of the nodes, relationships, indexes, ... of a Neo4j graph
* xref:metadata-types/partition-schema.adoc[Partition Schema]: Describes a
partition schema
diff --git
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/webdav-connection.adoc
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/webdav-connection.adoc
new file mode 100644
index 0000000000..86f6ef70a4
--- /dev/null
+++
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/webdav-connection.adoc
@@ -0,0 +1,58 @@
+////
+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.
+////
+:imagesdir: ../../assets/images/
+:page-pagination:
+:description: Register a named WebDAV (HTTP/WebDAV or HTTPS) endpoint for use
in Hop VFS paths.
+
+= WebDAV Connection
+
+== Description
+
+This metadata type registers a **named** WebDAV endpoint in your project.
+Hop exposes each connection under the **connection name** as a VFS scheme, so
you reference files with:
+
+`+myConnectionName:///path/under/webdav/root+`
+
+The **WebDAV root URL** in the metadata must be a full Apache Commons VFS
WebDAV URL (`webdav4://` for HTTP or `webdav4s://` for HTTPS), including the
path to your DAV root (for example a Nextcloud folder root such as
`+/remote.php/dav/files/username/+`).
+
+Credentials are configured in this metadata object and applied through
VFS—they are **not** embedded in Hop file paths.
+
+For the underlying `webdav4` / `webdav4s` URI syntax and options, see
xref:vfs.adoc[VFS] (WebDAV section).
+
+IMPORTANT: Many hosted servers redirect HTTP to HTTPS with `301`. Use
**`webdav4s://`** in **WebDAV root URL** when the server expects TLS, otherwise
listing or type detection can fail while desktop clients (that follow redirects
for WebDAV methods) may still work.
+
+NOTE: **Username**, **password**, and **WebDAV root URL** support variable
substitution. Passwords can be stored encrypted in metadata; Hop decrypts them
when resolving the connection.
+
+== Options
+
+[options="header"]
+|===
+|Option |Description
+|Name| Name of this connection; used as the URI scheme in paths
(`+name:///...+`)
+|Description| Optional longer description
+|WebDAV root URL| Full URL including scheme `webdav4://` or `webdav4s://`,
host, optional port, and DAV path (often with a trailing slash). Example:
`+webdav4s://cloud.example.com/remote.php/dav/files/admin/+`
+|Username| User name for authentication (optional if the server allows
anonymous access)
+|Password| Password or application password; supports variables and encrypted
values
+|Follow HTTP redirects| Passed to the HTTP client (see VFS/WebDAV provider
behavior for redirect limits on WebDAV methods)
+|Preemptive basic authentication| Sends credentials proactively for servers
that require it
+|===
+
+== Tips
+
+* To verify a connection, open **File → Open** (or any file dialog) and browse
`+YourConnectionName:///+` or drill into a subfolder path.
+* Prefer **`webdav4s://`** for Nextcloud and similar hosts that enforce HTTPS.
+* Pick a **Name** that does not clash with
https://commons.apache.org/proper/commons-vfs/filesystems.html[built-in VFS
schemes] (`file`, `ftp`, `http`, …) or other registered plugins (`s3`, `azure`,
`gs`, etc.).
diff --git a/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc
b/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc
index 6ad706632a..846ff157da 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/vfs.adoc
@@ -43,6 +43,7 @@ Click the File system name to access more detailed file
system documentation.
|xref:vfs/google-cloud-storage-vfs.adoc[Google Cloud Storage]|Provides access
to Google Cloud Storage buckets|`gs://`
|xref:vfs/google-drive-vfs.adoc[Google Drive]|Provides access to Google Drive
folders|`googledrive://`
|xref:metadata-types/minio-connection.adoc[Minio connection]|Provides access
to S3 endpoints using a Minio client|`any://`
+|xref:metadata-types/webdav-connection.adoc[WebDAV Connection]|Provides access
to WebDAV servers via a named connection (metadata)|`+connectionName://+`
|===
== Apache VFS File System Types
@@ -286,11 +287,12 @@ Examples
//
// WebDAV
//
-|WebDAV|Provides access to files on a WebDAV server through the modules
`commons-vfs2-jackrabbit1` and `commons-vfs2-jackrabbit2`.
+|WebDAV|Provides access through `commons-vfs2-jackrabbit2`. Hop registers
providers in
link:https://github.com/apache/hop/blob/main/core/src/main/java/org/apache/hop/core/vfs/HopVfs.java[HopVfs.java].
a|
-URI Format
+URI Format — Apache Commons VFS 2 schemes (not legacy `webdav://`):
-`+webdav://[ username[: password]@] hostname[: port][ absolute-path]+`
+* `+webdav4://[ username[: password]@] hostname[: port][ absolute-path]+`
(HTTP)
+* `+webdav4s://[ username[: password]@] hostname[: port][ absolute-path]+`
(HTTPS)
File System Options
@@ -300,7 +302,8 @@ If not set the user name used to authenticate will be used.
Examples
-* `+webdav://somehost:8080/dist+`
+* `+webdav4://someuser@somehost:8080/remote.php/dav/files/someuser/+`
+* `+webdav4s://[email protected]/remote.php/dav/files/someuser/+`
//
// Zip
diff --git a/lib/pom.xml b/lib/pom.xml
index ac748030ad..5d9c4df7f7 100644
--- a/lib/pom.xml
+++ b/lib/pom.xml
@@ -655,6 +655,11 @@
<artifactId>commons-vfs2</artifactId>
<version>${commons-vfs2.version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-vfs2-jackrabbit2</artifactId>
+ <version>${commons-vfs2.version}</version>
+ </dependency>
<dependency>
<groupId>commons-net</groupId>
<artifactId>commons-net</artifactId>
diff --git a/plugins/tech/pom.xml b/plugins/tech/pom.xml
index 6904c55e10..4d78da480d 100644
--- a/plugins/tech/pom.xml
+++ b/plugins/tech/pom.xml
@@ -44,5 +44,6 @@
<module>parquet</module>
<module>salesforce</module>
<module>vault</module>
+ <module>webdav</module>
</modules>
</project>
diff --git a/plugins/tech/pom.xml b/plugins/tech/webdav/pom.xml
similarity index 55%
copy from plugins/tech/pom.xml
copy to plugins/tech/webdav/pom.xml
index 6904c55e10..998a5c46d2 100644
--- a/plugins/tech/pom.xml
+++ b/plugins/tech/webdav/pom.xml
@@ -20,29 +20,37 @@
<parent>
<groupId>org.apache.hop</groupId>
- <artifactId>hop-plugins</artifactId>
+ <artifactId>hop-plugins-tech</artifactId>
<version>2.18.0-SNAPSHOT</version>
</parent>
- <artifactId>hop-plugins-tech</artifactId>
- <packaging>pom</packaging>
- <name>Hop Plugins Technology</name>
+ <artifactId>hop-tech-webdav</artifactId>
+ <packaging>jar</packaging>
+ <name>Hop Plugins Technology WebDAV</name>
- <modules>
- <module>arrow</module>
- <module>avro</module>
- <module>aws</module>
- <module>azure</module>
- <module>cassandra</module>
- <module>databricks</module>
- <module>dropbox</module>
- <module>elastic</module>
- <module>google</module>
- <module>minio</module>
- <module>neo4j</module>
- <module>opensearch</module>
- <module>parquet</module>
- <module>salesforce</module>
- <module>vault</module>
- </modules>
+ <dependencyManagement>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.hop</groupId>
+ <artifactId>hop-libs</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>import</scope>
+ </dependency>
+ </dependencies>
+ </dependencyManagement>
+
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.commons</groupId>
+ <artifactId>commons-vfs2-jackrabbit2</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit.jupiter</groupId>
+ <artifactId>junit-jupiter</artifactId>
+ <version>${junit.version}</version>
+ <scope>test</scope>
+ </dependency>
+ </dependencies>
</project>
diff --git a/plugins/tech/webdav/src/assembly/assembly.xml
b/plugins/tech/webdav/src/assembly/assembly.xml
new file mode 100644
index 0000000000..a96a757132
--- /dev/null
+++ b/plugins/tech/webdav/src/assembly/assembly.xml
@@ -0,0 +1,53 @@
+<!--
+ ~ 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.
+ -->
+
+<assembly xmlns="http://maven.apache.org/ASSEMBLY/2.2.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/ASSEMBLY/2.2.0
http://maven.apache.org/xsd/assembly-2.2.0.xsd">
+ <id>hop-tech-webdav</id>
+ <formats>
+ <format>zip</format>
+ </formats>
+ <baseDirectory>.</baseDirectory>
+ <files>
+ <file>
+ <source>${project.basedir}/src/main/resources/version.xml</source>
+ <outputDirectory>plugins/tech/webdav</outputDirectory>
+ <filtered>true</filtered>
+ </file>
+ </files>
+
+ <dependencySets>
+ <dependencySet>
+ <includes>
+ <include>org.apache.hop:hop-tech-webdav:jar</include>
+ </includes>
+ <outputDirectory>plugins/tech/webdav</outputDirectory>
+ </dependencySet>
+ <dependencySet>
+ <scope>runtime</scope>
+ <excludes>
+ <exclude>org.apache.hop:hop-tech-webdav:jar</exclude>
+ <exclude>org.apache.commons:commons-lang3:jar</exclude>
+ <exclude>commons-io:commons-io:jar</exclude>
+ <exclude>org.apache.commons:commons-vfs2:jar</exclude>
+
<exclude>org.apache.commons:commons-vfs2-jackrabbit2:jar</exclude>
+ </excludes>
+ <outputDirectory>plugins/tech/webdav/lib</outputDirectory>
+ </dependencySet>
+ </dependencySets>
+</assembly>
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopRawWebDavFileSystem.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopRawWebDavFileSystem.java
new file mode 100644
index 0000000000..05d817f0b9
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopRawWebDavFileSystem.java
@@ -0,0 +1,38 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystem;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.protocol.HttpClientContext;
+
+/**
+ * Subclass solely to expose {@link Webdav4FileSystem}'s protected constructor
from application
+ * code.
+ */
+final class HopRawWebDavFileSystem extends Webdav4FileSystem {
+
+ HopRawWebDavFileSystem(
+ FileName rootName,
+ FileSystemOptions fileSystemOptions,
+ HttpClient httpClient,
+ HttpClientContext httpClientContext) {
+ super(rootName, fileSystemOptions, httpClient, httpClientContext);
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavConnectionAuth.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavConnectionAuth.java
new file mode 100644
index 0000000000..94564354f5
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavConnectionAuth.java
@@ -0,0 +1,44 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.auth.StaticUserAuthenticator;
+import org.apache.commons.vfs2.impl.DefaultFileSystemConfigBuilder;
+import org.apache.commons.vfs2.provider.http4.Http4FileSystemConfigBuilder;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.util.Utils;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+
+final class HopWebDavConnectionAuth {
+
+ private HopWebDavConnectionAuth() {}
+
+ static void apply(FileSystemOptions opts, IVariables variables,
WebDavConnection meta) {
+ String user = Const.NVL(variables.resolve(meta.getUsername()), "");
+ String pass = Const.NVL(Utils.resolvePassword(variables,
meta.getPassword()), "");
+ if (StringUtils.isNotEmpty(user)) {
+ DefaultFileSystemConfigBuilder.getInstance()
+ .setUserAuthenticator(opts, new StaticUserAuthenticator(null, user,
pass));
+ }
+ Http4FileSystemConfigBuilder http4 =
Http4FileSystemConfigBuilder.getInstance();
+ http4.setFollowRedirect(opts, meta.isFollowRedirects());
+ http4.setPreemptiveAuth(opts, meta.isPreemptiveAuth());
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileName.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileName.java
new file mode 100644
index 0000000000..add3e3b988
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileName.java
@@ -0,0 +1,91 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.provider.AbstractFileName;
+
+public final class HopWebDavFileName extends AbstractFileName {
+
+ public HopWebDavFileName(String scheme, String path, FileType type) {
+ super(scheme, normalizeStoredPath(path, type), type);
+ }
+
+ public static HopWebDavFileName buildChild(
+ HopWebDavFileName parent, String baseName, FileType childType) {
+ String p = parent.getPath();
+ String childPath;
+ if (p == null || p.isEmpty() || "/".equals(p)) {
+ childPath = "/" + baseName;
+ } else if (p.endsWith("/")) {
+ childPath = p + baseName;
+ } else {
+ childPath = p + "/" + baseName;
+ }
+ return new HopWebDavFileName(parent.getScheme(), childPath, childType);
+ }
+
+ private static String normalizeStoredPath(String path, FileType type) {
+ String p = StringUtils.defaultString(path);
+ if (p.isEmpty() || "/".equals(p)) {
+ return "/";
+ }
+ if (!p.startsWith("/")) {
+ p = "/" + p;
+ }
+ if (type == FileType.FOLDER && p.length() > 1 && !p.endsWith("/")) {
+ p = p + "/";
+ }
+ return p;
+ }
+
+ @Override
+ public FileName createName(String absPath, FileType fileType) {
+ return new HopWebDavFileName(getScheme(), absPath, fileType);
+ }
+
+ @Override
+ public String getURI() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(getScheme()).append(":///");
+ String p = getPath();
+ if (p == null || p.isEmpty() || "/".equals(p)) {
+ return sb.toString();
+ }
+ String suffix = p.startsWith("/") ? p.substring(1) : p;
+ sb.append(suffix);
+ return sb.toString();
+ }
+
+ @Override
+ public String getRootURI() {
+ return getScheme() + ":///";
+ }
+
+ @Override
+ public String getFriendlyURI() {
+ return getURI();
+ }
+
+ @Override
+ protected void appendRootUri(StringBuilder buffer, boolean addPassword) {
+ buffer.append(getScheme());
+ buffer.append(":///");
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParser.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParser.java
new file mode 100644
index 0000000000..2b0fb3aea9
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParser.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.provider.AbstractFileNameParser;
+import org.apache.commons.vfs2.provider.VfsComponentContext;
+import org.apache.hop.core.Const;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+
+final class HopWebDavFileNameParser extends AbstractFileNameParser {
+
+ private final WebDavConnection meta;
+
+ HopWebDavFileNameParser(WebDavConnection meta) {
+ this.meta = meta;
+ }
+
+ @Override
+ public FileName parseUri(VfsComponentContext context, FileName base, String
uri)
+ throws FileSystemException {
+ String conn = Const.NVL(meta.getName(), "").trim();
+ if (StringUtils.isEmpty(conn)) {
+ throw new FileSystemException("WebDAV connection has no name");
+ }
+ int c = uri.indexOf(':');
+ if (c < 0) {
+ throw new FileSystemException("Missing URI scheme: " + uri);
+ }
+ String scheme = uri.substring(0, c);
+ if (!scheme.equals(conn)) {
+ throw new FileSystemException(
+ "URI scheme must match WebDAV connection name \"" + conn + "\", got:
" + scheme);
+ }
+ String pathPart = HopWebDavLogicalUris.rawPathFromUri(uri);
+ String path = pathPart.isEmpty() || "/".equals(pathPart) ? "/" : pathPart;
+ FileType type = inferType(uri, path);
+ return new HopWebDavFileName(scheme, path, type);
+ }
+
+ private static FileType inferType(String uri, String path) {
+ if (uri.endsWith("/")) {
+ return FileType.FOLDER;
+ }
+ if ("/".equals(path) || (path.length() > 1 && path.endsWith("/"))) {
+ return FileType.FOLDER;
+ }
+ return FileType.FILE;
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileObject.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileObject.java
new file mode 100644
index 0000000000..5ee6405685
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileObject.java
@@ -0,0 +1,91 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.provider.DelegateFileObject;
+
+/**
+ * Presents {@link HopWebDavFileName} to callers while delegating operations
to wire WebDAV4.
+ *
+ * <p>Stock {@code DelegateFileObject} lists only base names; VFS then calls
{@code resolveFile} for
+ * each child, repeating wire work already done during the directory listing.
Returning resolved
+ * children reuses the wire {@link FileObject}s from that listing.
+ *
+ * <p>Children produced by {@link #doListChildrenResolved()} carry the wire
{@link FileType} from
+ * the PROPFIND so {@link #getType()} / {@link #exists()} need not hit the
wire again (Hop's {@code
+ * FileInputList} may call both during the same scan).
+ */
+final class HopWebDavFileObject extends
DelegateFileObject<HopWebDavFileSystem> {
+
+ /** When non-null, DAV listing already resolved type; cleared on {@link
#refresh()}. */
+ private volatile FileType listingTypeHint;
+
+ HopWebDavFileObject(HopWebDavFileName name, HopWebDavFileSystem fs,
FileObject wireDelegate)
+ throws FileSystemException {
+ super(name, fs, wireDelegate);
+ }
+
+ HopWebDavFileObject(
+ HopWebDavFileName name,
+ HopWebDavFileSystem fs,
+ FileObject wireDelegate,
+ FileType listingTypeHint)
+ throws FileSystemException {
+ super(name, fs, wireDelegate);
+ this.listingTypeHint = listingTypeHint;
+ }
+
+ @Override
+ protected FileType doGetType() throws FileSystemException {
+ FileType hint = listingTypeHint;
+ if (hint != null) {
+ return hint;
+ }
+ return super.doGetType();
+ }
+
+ @Override
+ public void refresh() throws FileSystemException {
+ listingTypeHint = null;
+ super.refresh();
+ }
+
+ @Override
+ protected FileObject[] doListChildrenResolved() throws Exception {
+ FileObject wireDir = getDelegateFile();
+ if (wireDir == null) {
+ return null;
+ }
+ FileObject[] wireChildren = wireDir.getChildren();
+ HopWebDavFileSystem fs = getAbstractFileSystem();
+ HopWebDavFileName parentName = (HopWebDavFileName) getName();
+ FileObject[] out = new FileObject[wireChildren.length];
+ for (int i = 0; i < wireChildren.length; i++) {
+ FileObject w = wireChildren[i];
+ FileType wt = w.getType();
+ HopWebDavFileName childName =
+ HopWebDavFileName.buildChild(parentName, w.getName().getBaseName(),
wt);
+ HopWebDavFileObject child = new HopWebDavFileObject(childName, fs, w,
wt);
+ fs.rememberListedChild(child);
+ out[i] = child;
+ }
+ return out;
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileProvider.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileProvider.java
new file mode 100644
index 0000000000..8e4df7223d
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileProvider.java
@@ -0,0 +1,106 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.Capability;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystem;
+import org.apache.commons.vfs2.FileSystemConfigBuilder;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.provider.AbstractOriginatingFileProvider;
+import org.apache.commons.vfs2.provider.FileNameParser;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileName;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileNameParser;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileProvider;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystem;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystemConfigBuilder;
+import org.apache.commons.vfs2.provider.webdav4s.Webdav4sFileNameParser;
+import org.apache.hop.core.Const;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+
+public class HopWebDavFileProvider extends AbstractOriginatingFileProvider {
+
+ private static final Collection<Capability> CAPABILITIES;
+
+ static {
+ Collection<Capability> c = new ArrayList<>(new
Webdav4FileProvider().getCapabilities());
+ CAPABILITIES = Collections.unmodifiableCollection(c);
+ }
+
+ private final IVariables variables;
+ private final WebDavConnection meta;
+
+ public HopWebDavFileProvider(IVariables variables, WebDavConnection meta) {
+ this.variables = variables;
+ this.meta = meta;
+ setFileNameParser(new HopWebDavFileNameParser(meta));
+ }
+
+ @Override
+ public Collection<Capability> getCapabilities() {
+ return CAPABILITIES;
+ }
+
+ @Override
+ public FileSystemConfigBuilder getConfigBuilder() {
+ return Webdav4FileSystemConfigBuilder.getInstance();
+ }
+
+ @Override
+ protected FileSystem doCreateFileSystem(FileName rootName, FileSystemOptions
fileSystemOptions)
+ throws FileSystemException {
+ FileSystemOptions opts =
+ fileSystemOptions != null
+ ? (FileSystemOptions) fileSystemOptions.clone()
+ : new FileSystemOptions();
+ HopWebDavConnectionAuth.apply(opts, variables, meta);
+ Webdav4FileSystemConfigBuilder httpCfg =
Webdav4FileSystemConfigBuilder.getInstance();
+ httpCfg.setMaxConnectionsPerHost(opts, 32);
+ httpCfg.setMaxTotalConnections(opts, 64);
+ httpCfg.setKeepAlive(opts, true);
+
+ String rootUrl = Const.NVL(variables.resolve(meta.getRootUrl()),
"").trim();
+ if (StringUtils.isEmpty(rootUrl)) {
+ throw new FileSystemException("WebDAV connection \"" + meta.getName() +
"\" has no rootUrl");
+ }
+ String lower = rootUrl.toLowerCase();
+ if (!lower.startsWith("webdav4://") && !lower.startsWith("webdav4s://")) {
+ throw new FileSystemException(
+ "WebDAV rootUrl must start with webdav4:// or webdav4s://, got: " +
rootUrl);
+ }
+ FileNameParser wireParser =
+ lower.startsWith("webdav4s://")
+ ? Webdav4sFileNameParser.getInstance()
+ : Webdav4FileNameParser.getInstance();
+ FileName wireRootParsed = wireParser.parseUri(getContext(), null, rootUrl);
+ Webdav4FileName wireRoot = HopWebDavWireNames.asWebdav4(wireRootParsed);
+
+ Webdav4FileSystem wireFs = HopWebDavWireBootstrap.create(opts, wireRoot);
+ // Wire FS is not manager-registered; share context so resolveFile can use
the files cache.
+ wireFs.setContext(getContext());
+ wireFs.init();
+
+ String prefix = HopWebDavLogicalUris.wirePathPrefixFromRootUrl(rootUrl);
+ return new HopWebDavFileSystem((HopWebDavFileName) rootName, opts, wireFs,
wireRoot, prefix);
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileSystem.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileSystem.java
new file mode 100644
index 0000000000..8df5d270c6
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavFileSystem.java
@@ -0,0 +1,141 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.util.Collection;
+import org.apache.commons.vfs2.Capability;
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileObject;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.provider.AbstractFileName;
+import org.apache.commons.vfs2.provider.AbstractFileSystem;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileName;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileProvider;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystem;
+
+class HopWebDavFileSystem extends AbstractFileSystem {
+
+ private final Webdav4FileSystem wireFs;
+ private final Webdav4FileName wireRoot;
+ private final String rootWirePathPrefix;
+
+ HopWebDavFileSystem(
+ HopWebDavFileName rootName,
+ FileSystemOptions fileSystemOptions,
+ Webdav4FileSystem wireFs,
+ Webdav4FileName wireRoot,
+ String rootWirePathPrefix) {
+ super(rootName, null, fileSystemOptions);
+ this.wireFs = wireFs;
+ this.wireRoot = wireRoot;
+ this.rootWirePathPrefix = rootWirePathPrefix;
+ }
+
+ @Override
+ protected FileObject createFile(AbstractFileName name) throws Exception {
+ HopWebDavFileName logical = (HopWebDavFileName) name;
+ FileObject delegate = wireFs.resolveFile(toWire(logical));
+ return new HopWebDavFileObject(logical, this, delegate);
+ }
+
+ @Override
+ protected void addCapabilities(Collection<Capability> caps) {
+ caps.addAll(new Webdav4FileProvider().getCapabilities());
+ }
+
+ /**
+ * Register a logical file object built from a DAV listing so a later {@code
resolveFile} on the
+ * same name reuses it (and the existing wire delegate) instead of {@link
#createFile}.
+ */
+ void rememberListedChild(HopWebDavFileObject fo) throws FileSystemException {
+ putFileToCache(fo);
+ }
+
+ Webdav4FileName toWire(HopWebDavFileName logical) throws Exception {
+ if (isLogicalRoot(logical)) {
+ return wireRoot;
+ }
+ String rel = effectiveRelativeSegment(rootWirePathPrefix,
logical.getPath());
+ String wirePath = wirePathCombine(rootWirePathPrefix, rel);
+ FileName child = wireRoot.createName(wirePath, logical.getType());
+ return HopWebDavWireNames.asWebdav4(child);
+ }
+
+ private static boolean isLogicalRoot(HopWebDavFileName logical) {
+ String p = logical.getPath();
+ return p == null || p.isEmpty() || "/".equals(p);
+ }
+
+ /**
+ * When {@code rootUrl} already ends at e.g. {@code
/remote.php/dav/files/admin}, a logical URI
+ * that repeats that path ({@code myconn:///remote.php/dav/files/admin/})
must not be appended
+ * again under the wire prefix.
+ */
+ static String effectiveRelativeSegment(String rootWirePathPrefix, String
logicalPath) {
+ String rel = normalizeLogicalPathToRelativeSegment(logicalPath);
+ if (rel.isEmpty()) {
+ return "";
+ }
+ String prefix = rootWirePathPrefix == null ? "/" : rootWirePathPrefix;
+ if (!prefix.startsWith("/")) {
+ prefix = "/" + prefix;
+ }
+ String rootNoTrail = prefix;
+ while (rootNoTrail.length() > 1 && rootNoTrail.endsWith("/")) {
+ rootNoTrail = rootNoTrail.substring(0, rootNoTrail.length() - 1);
+ }
+ if ("/".equals(rootNoTrail)) {
+ return rel;
+ }
+ String logicalAbs = "/" + rel;
+ if (logicalAbs.equals(rootNoTrail)) {
+ return "";
+ }
+ if (logicalAbs.startsWith(rootNoTrail + "/")) {
+ return logicalAbs.substring(rootNoTrail.length() + 1);
+ }
+ return rel;
+ }
+
+ /** Logical path {@code /a/b} → {@code a/b} for appending under DAV root. */
+ private static String normalizeLogicalPathToRelativeSegment(String path) {
+ String p = path == null ? "" : path;
+ if (p.startsWith("/")) {
+ p = p.substring(1);
+ }
+ while (p.length() > 1 && p.endsWith("/")) {
+ p = p.substring(0, p.length() - 1);
+ }
+ return p;
+ }
+
+ static String wirePathCombine(String rootPrefix, String
relativeNoLeadingSlash) {
+ String prefix = rootPrefix == null ? "/" : rootPrefix;
+ if (!prefix.endsWith("/")) {
+ prefix = prefix + "/";
+ }
+ if (relativeNoLeadingSlash == null || relativeNoLeadingSlash.isEmpty()) {
+ String p = prefix;
+ if (p.endsWith("/") && p.length() > 1) {
+ p = p.substring(0, p.length() - 1);
+ }
+ return p;
+ }
+ return prefix + relativeNoLeadingSlash;
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUris.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUris.java
new file mode 100644
index 0000000000..32045691aa
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUris.java
@@ -0,0 +1,95 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.net.URI;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.hop.core.Const;
+
+/** Helpers for logical {@code <connection-name>:///…} URIs vs internal WebDAV
wire paths. */
+final class HopWebDavLogicalUris {
+
+ private HopWebDavLogicalUris() {}
+
+ /**
+ * Path prefix under the WebDAV host for {@link
+ * org.apache.hop.vfs.webdav.metadata.WebDavConnection#getRootUrl()}, always
starting with {@code
+ * /} and ending with {@code /} when non-empty.
+ */
+ static String wirePathPrefixFromRootUrl(String resolvedRootUrl) throws
FileSystemException {
+ String rootUrl = Const.NVL(resolvedRootUrl, "").trim();
+ if (StringUtils.isEmpty(rootUrl)) {
+ throw new FileSystemException("WebDAV connection has no rootUrl");
+ }
+ URI u;
+ try {
+ u = URI.create(rootUrl);
+ } catch (IllegalArgumentException e) {
+ throw new FileSystemException(e);
+ }
+ String p = u.getPath();
+ if (p == null || p.isEmpty()) {
+ return "/";
+ }
+ if (!p.endsWith("/")) {
+ p = p + "/";
+ }
+ if (!p.startsWith("/")) {
+ p = "/" + p;
+ }
+ return p;
+ }
+
+ /**
+ * Parsed connection URI path for {@link HopWebDavFileNameParser} and for
VFS {@code resolveName}
+ * output (leading {@code /}, slashes after {@code scheme://} collapsed).
+ *
+ * <p>Does not use {@link URI#create(String)}: WebDAV paths may contain
spaces and other
+ * characters that are illegal in strict URIs.
+ */
+ static String rawPathFromUri(String uri) throws FileSystemException {
+ if (StringUtils.isEmpty(uri)) {
+ return "";
+ }
+ int colon = uri.indexOf(':');
+ if (colon < 0) {
+ throw new FileSystemException("Missing URI scheme: " + uri);
+ }
+ String rest = uri.substring(colon + 1);
+ int hash = rest.indexOf('#');
+ if (hash >= 0) {
+ rest = rest.substring(0, hash);
+ }
+ if (!rest.startsWith("//")) {
+ throw new FileSystemException("Expected // after scheme in connection
URI: " + uri);
+ }
+ rest = rest.substring(2);
+ int q = rest.indexOf('?');
+ if (q >= 0) {
+ rest = rest.substring(0, q);
+ }
+ while (rest.startsWith("/")) {
+ rest = rest.substring(1);
+ }
+ String path = rest.isEmpty() ? "" : "/" + rest;
+ while (path.startsWith("//")) {
+ path = path.substring(1);
+ }
+ return path;
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireBootstrap.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireBootstrap.java
new file mode 100644
index 0000000000..1d39507dc3
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireBootstrap.java
@@ -0,0 +1,79 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileSystemOptions;
+import org.apache.commons.vfs2.UserAuthenticationData;
+import org.apache.commons.vfs2.provider.GenericFileName;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileProvider;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystem;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileSystemConfigBuilder;
+import org.apache.commons.vfs2.provider.webdav4s.Webdav4sFileProvider;
+import org.apache.commons.vfs2.util.UserAuthenticatorUtils;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.protocol.HttpClientContext;
+
+/** Builds an internal {@link Webdav4FileSystem} using stock WebDAV4 HTTP(S)
client setup. */
+final class HopWebDavWireBootstrap {
+
+ private HopWebDavWireBootstrap() {}
+
+ static Webdav4FileSystem create(FileSystemOptions opts, GenericFileName
wireRoot)
+ throws FileSystemException {
+ Webdav4FileSystemConfigBuilder builder =
Webdav4FileSystemConfigBuilder.getInstance();
+ UserAuthenticationData auth = null;
+ try {
+ auth = UserAuthenticatorUtils.authenticate(opts,
Webdav4FileProvider.AUTHENTICATOR_TYPES);
+ if (wireRoot.getScheme().equalsIgnoreCase("webdav4s")) {
+ return HttpsWire.create(wireRoot, opts, builder, auth);
+ }
+ return HttpWire.create(wireRoot, opts, builder, auth);
+ } finally {
+ UserAuthenticatorUtils.cleanup(auth);
+ }
+ }
+
+ /** Subclass to access {@link Webdav4FileProvider} protected HTTP factory
methods. */
+ private static final class HttpWire extends Webdav4FileProvider {
+ static Webdav4FileSystem create(
+ GenericFileName wireRoot,
+ FileSystemOptions opts,
+ Webdav4FileSystemConfigBuilder builder,
+ UserAuthenticationData auth)
+ throws FileSystemException {
+ HttpWire p = new HttpWire();
+ HttpClientContext ctx = p.createHttpClientContext(builder, wireRoot,
opts, auth);
+ HttpClient client = p.createHttpClient(builder, wireRoot, opts);
+ return new HopRawWebDavFileSystem(wireRoot, opts, client, ctx);
+ }
+ }
+
+ private static final class HttpsWire extends Webdav4sFileProvider {
+ static Webdav4FileSystem create(
+ GenericFileName wireRoot,
+ FileSystemOptions opts,
+ Webdav4FileSystemConfigBuilder builder,
+ UserAuthenticationData auth)
+ throws FileSystemException {
+ HttpsWire p = new HttpsWire();
+ HttpClientContext ctx = p.createHttpClientContext(builder, wireRoot,
opts, auth);
+ HttpClient client = p.createHttpClient(builder, wireRoot, opts);
+ return new HopRawWebDavFileSystem(wireRoot, opts, client, ctx);
+ }
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireNames.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireNames.java
new file mode 100644
index 0000000000..29f72998f5
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/HopWebDavWireNames.java
@@ -0,0 +1,51 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileType;
+import org.apache.commons.vfs2.provider.GenericURLFileName;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileName;
+
+/** Normalizes {@link GenericURLFileName} from webdav4 parse/create to {@link
Webdav4FileName}. */
+final class HopWebDavWireNames {
+
+ private HopWebDavWireNames() {}
+
+ static Webdav4FileName asWebdav4(FileName name) throws FileSystemException {
+ if (name instanceof Webdav4FileName) {
+ return (Webdav4FileName) name;
+ }
+ if (name instanceof GenericURLFileName) {
+ GenericURLFileName g = (GenericURLFileName) name;
+ boolean trailing = g.getType() == FileType.FOLDER;
+ return new Webdav4FileName(
+ g.getScheme(),
+ g.getHostName(),
+ g.getPort(),
+ g.getDefaultPort(),
+ g.getUserName(),
+ g.getPassword(),
+ g.getPath(),
+ g.getType(),
+ g.getQueryString(),
+ trailing);
+ }
+ throw new FileSystemException("Unexpected WebDAV wire file name type: " +
name.getClass());
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavSecureVfsPlugin.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavSecureVfsPlugin.java
new file mode 100644
index 0000000000..91974b6998
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavSecureVfsPlugin.java
@@ -0,0 +1,51 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.util.Collections;
+import java.util.Map;
+import org.apache.commons.vfs2.provider.FileProvider;
+import org.apache.commons.vfs2.provider.webdav4s.Webdav4sFileProvider;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.vfs.plugin.IVfs;
+import org.apache.hop.core.vfs.plugin.VfsPlugin;
+
+/**
+ * Registers the built-in secure WebDAV scheme through the VFS plugin
mechanism, aligned with other
+ * storage plugins. Named providers are loaded by {@link WebDavVfsPlugin}.
+ */
+@VfsPlugin(
+ type = "webdav4s",
+ typeDescription = "WebDAV HTTPS VFS plugin",
+ classLoaderGroup = "vfs-webdav")
+public class WebDavSecureVfsPlugin implements IVfs {
+
+ @Override
+ public String[] getUrlSchemes() {
+ return new String[] {"webdav4s"};
+ }
+
+ @Override
+ public FileProvider getProvider() {
+ return new Webdav4sFileProvider();
+ }
+
+ @Override
+ public Map<String, FileProvider> getProviders(IVariables variables) {
+ return Collections.emptyMap();
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavVfsPlugin.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavVfsPlugin.java
new file mode 100644
index 0000000000..5f6d5a7017
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/WebDavVfsPlugin.java
@@ -0,0 +1,69 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.vfs2.provider.FileProvider;
+import org.apache.commons.vfs2.provider.webdav4.Webdav4FileProvider;
+import org.apache.hop.core.logging.LogChannel;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.vfs.plugin.IVfs;
+import org.apache.hop.core.vfs.plugin.VfsPlugin;
+import org.apache.hop.metadata.api.IHopMetadataProvider;
+import org.apache.hop.metadata.util.HopMetadataUtil;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+
+@VfsPlugin(
+ type = "webdav-connection",
+ typeDescription = "WebDAV VFS (named connections)",
+ classLoaderGroup = "vfs-webdav")
+public class WebDavVfsPlugin implements IVfs {
+
+ @Override
+ public String[] getUrlSchemes() {
+ return new String[] {"webdav4"};
+ }
+
+ @Override
+ public FileProvider getProvider() {
+ return new Webdav4FileProvider();
+ }
+
+ @Override
+ public Map<String, FileProvider> getProviders(IVariables variables) {
+ Map<String, FileProvider> providers = new HashMap<>();
+ try {
+ IHopMetadataProvider metadataProvider =
+ HopMetadataUtil.getStandardHopMetadataProvider(variables);
+ List<WebDavConnection> connections =
+ metadataProvider.getSerializer(WebDavConnection.class).loadAll();
+ for (WebDavConnection connection : connections) {
+ String name = connection.getName();
+ if (StringUtils.isEmpty(name)) {
+ continue;
+ }
+ providers.put(name, new HopWebDavFileProvider(variables, connection));
+ }
+ } catch (Exception e) {
+ LogChannel.GENERAL.logError("Unable to load WebDAV VFS providers", e);
+ }
+ return providers;
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnection.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnection.java
new file mode 100644
index 0000000000..0357decc10
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnection.java
@@ -0,0 +1,66 @@
+/*
+ * 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.hop.vfs.webdav.metadata;
+
+import java.io.Serializable;
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.hop.metadata.api.HopMetadata;
+import org.apache.hop.metadata.api.HopMetadataBase;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+import org.apache.hop.metadata.api.HopMetadataPropertyType;
+import org.apache.hop.metadata.api.IHopMetadata;
+
+/**
+ * Named WebDAV connection. Use URLs {@code
<connection-name>:///path/under/root} where {@code
+ * rootUrl} is a full Commons VFS WebDAV root (for example {@code
+ * webdav4://localhost/remote.php/dav/files/admin/}). Credentials are stored
here and applied via
+ * VFS options, not embedded in the URL.
+ */
+@Getter
+@Setter
+@HopMetadata(
+ key = "WebDavConnectionDefinition",
+ name = "i18n::WebDavConnection.Name",
+ description = "i18n::WebDavConnection.Description",
+ image = "ui/images/authentication.svg",
+ documentationUrl = "/metadata-types/webdav-connection.html",
+ hopMetadataPropertyType = HopMetadataPropertyType.VFS_WEBDAV_CONNECTION)
+public class WebDavConnection extends HopMetadataBase implements Serializable,
IHopMetadata {
+
+ @HopMetadataProperty private String description;
+
+ /**
+ * Full WebDAV root URL including scheme {@code webdav4://} or {@code
webdav4s://}, host, optional
+ * port, and path ending at the user's DAV files root (typically with a
trailing slash).
+ */
+ @HopMetadataProperty private String rootUrl;
+
+ @HopMetadataProperty private String username;
+
+ @HopMetadataProperty(password = true)
+ private String password;
+
+ @HopMetadataProperty private boolean followRedirects;
+
+ @HopMetadataProperty private boolean preemptiveAuth;
+
+ public WebDavConnection() {
+ followRedirects = true;
+ preemptiveAuth = true;
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnectionEditor.java
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnectionEditor.java
new file mode 100644
index 0000000000..349fa4719f
--- /dev/null
+++
b/plugins/tech/webdav/src/main/java/org/apache/hop/vfs/webdav/metadata/WebDavConnectionEditor.java
@@ -0,0 +1,232 @@
+/*
+ * 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.hop.vfs.webdav.metadata;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.vfs.HopVfs;
+import org.apache.hop.i18n.BaseMessages;
+import org.apache.hop.ui.core.PropsUi;
+import org.apache.hop.ui.core.metadata.MetadataEditor;
+import org.apache.hop.ui.core.metadata.MetadataManager;
+import org.apache.hop.ui.core.widget.PasswordTextVar;
+import org.apache.hop.ui.core.widget.TextVar;
+import org.apache.hop.ui.hopgui.HopGui;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FormAttachment;
+import org.eclipse.swt.layout.FormData;
+import org.eclipse.swt.widgets.Button;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Label;
+import org.eclipse.swt.widgets.Text;
+
+@GuiPlugin(description = "Editor for WebDAV connection metadata")
+public class WebDavConnectionEditor extends MetadataEditor<WebDavConnection> {
+
+ private static final Class<?> PKG = WebDavConnectionEditor.class;
+
+ private Text wName;
+ private Text wDescription;
+ private TextVar wRootUrl;
+ private TextVar wUsername;
+ private PasswordTextVar wPassword;
+ private Button wFollowRedirects;
+ private Button wPreemptiveAuth;
+
+ public WebDavConnectionEditor(
+ HopGui hopGui, MetadataManager<WebDavConnection> manager,
WebDavConnection metadata) {
+ super(hopGui, manager, metadata);
+ }
+
+ @Override
+ public void createControl(Composite parent) {
+ PropsUi props = PropsUi.getInstance();
+ int middle = props.getMiddlePct();
+ int margin = PropsUi.getMargin() + 2;
+
+ Control lastControl;
+
+ Label wlName = new Label(parent, SWT.RIGHT);
+ PropsUi.setLook(wlName);
+ wlName.setText(BaseMessages.getString(PKG,
"WebDavConnectionEditor.Name.Label"));
+ FormData fdlName = new FormData();
+ fdlName.top = new FormAttachment(0, margin);
+ fdlName.left = new FormAttachment(0, 0);
+ fdlName.right = new FormAttachment(middle, -margin);
+ wlName.setLayoutData(fdlName);
+ wName = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wName);
+ FormData fdName = new FormData();
+ fdName.top = new FormAttachment(wlName, 0, SWT.CENTER);
+ fdName.left = new FormAttachment(middle, 0);
+ fdName.right = new FormAttachment(95, 0);
+ wName.setLayoutData(fdName);
+ lastControl = wName;
+
+ Label wlDescription = new Label(parent, SWT.RIGHT);
+ PropsUi.setLook(wlDescription);
+ wlDescription.setText(BaseMessages.getString(PKG,
"WebDavConnectionEditor.Description.Label"));
+ FormData fdlDescription = new FormData();
+ fdlDescription.top = new FormAttachment(lastControl, margin);
+ fdlDescription.left = new FormAttachment(0, 0);
+ fdlDescription.right = new FormAttachment(middle, -margin);
+ wlDescription.setLayoutData(fdlDescription);
+ wDescription = new Text(parent, SWT.SINGLE | SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wDescription);
+ FormData fdDescription = new FormData();
+ fdDescription.top = new FormAttachment(wlDescription, 0, SWT.CENTER);
+ fdDescription.left = new FormAttachment(middle, 0);
+ fdDescription.right = new FormAttachment(95, 0);
+ wDescription.setLayoutData(fdDescription);
+ lastControl = wDescription;
+
+ Label wlRootUrl = new Label(parent, SWT.RIGHT);
+ PropsUi.setLook(wlRootUrl);
+ wlRootUrl.setText(BaseMessages.getString(PKG,
"WebDavConnectionEditor.RootUrl.Label"));
+ FormData fdlRootUrl = new FormData();
+ fdlRootUrl.top = new FormAttachment(lastControl, margin);
+ fdlRootUrl.left = new FormAttachment(0, 0);
+ fdlRootUrl.right = new FormAttachment(middle, -margin);
+ wlRootUrl.setLayoutData(fdlRootUrl);
+ wRootUrl = new TextVar(getVariables(), parent, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ PropsUi.setLook(wRootUrl);
+ FormData fdRootUrl = new FormData();
+ fdRootUrl.top = new FormAttachment(wlRootUrl, 0, SWT.CENTER);
+ fdRootUrl.left = new FormAttachment(middle, 0);
+ fdRootUrl.right = new FormAttachment(95, 0);
+ wRootUrl.setLayoutData(fdRootUrl);
+ lastControl = wRootUrl;
+
+ Label wlUsername = new Label(parent, SWT.RIGHT);
+ PropsUi.setLook(wlUsername);
+ wlUsername.setText(BaseMessages.getString(PKG,
"WebDavConnectionEditor.Username.Label"));
+ FormData fdlUsername = new FormData();
+ fdlUsername.top = new FormAttachment(lastControl, margin);
+ fdlUsername.left = new FormAttachment(0, 0);
+ fdlUsername.right = new FormAttachment(middle, -margin);
+ wlUsername.setLayoutData(fdlUsername);
+ wUsername = new TextVar(getVariables(), parent, SWT.SINGLE | SWT.LEFT |
SWT.BORDER);
+ PropsUi.setLook(wUsername);
+ FormData fdUsername = new FormData();
+ fdUsername.top = new FormAttachment(wlUsername, 0, SWT.CENTER);
+ fdUsername.left = new FormAttachment(middle, 0);
+ fdUsername.right = new FormAttachment(95, 0);
+ wUsername.setLayoutData(fdUsername);
+ lastControl = wUsername;
+
+ Label wlPassword = new Label(parent, SWT.RIGHT);
+ PropsUi.setLook(wlPassword);
+ wlPassword.setText(BaseMessages.getString(PKG,
"WebDavConnectionEditor.Password.Label"));
+ FormData fdlPassword = new FormData();
+ fdlPassword.top = new FormAttachment(lastControl, margin);
+ fdlPassword.left = new FormAttachment(0, 0);
+ fdlPassword.right = new FormAttachment(middle, -margin);
+ wlPassword.setLayoutData(fdlPassword);
+ wPassword = new PasswordTextVar(getVariables(), parent, SWT.SINGLE |
SWT.LEFT | SWT.BORDER);
+ PropsUi.setLook(wPassword);
+ FormData fdPassword = new FormData();
+ fdPassword.top = new FormAttachment(wlPassword, 0, SWT.CENTER);
+ fdPassword.left = new FormAttachment(middle, 0);
+ fdPassword.right = new FormAttachment(95, 0);
+ wPassword.setLayoutData(fdPassword);
+ lastControl = wPassword;
+
+ Label wlFollowRedirects = new Label(parent, SWT.RIGHT);
+ PropsUi.setLook(wlFollowRedirects);
+ wlFollowRedirects.setText(
+ BaseMessages.getString(PKG,
"WebDavConnectionEditor.FollowRedirects.Label"));
+ FormData fdlFollowRedirects = new FormData();
+ fdlFollowRedirects.top = new FormAttachment(lastControl, margin);
+ fdlFollowRedirects.left = new FormAttachment(0, 0);
+ fdlFollowRedirects.right = new FormAttachment(middle, -margin);
+ wlFollowRedirects.setLayoutData(fdlFollowRedirects);
+ wFollowRedirects = new Button(parent, SWT.CHECK);
+ PropsUi.setLook(wFollowRedirects);
+ FormData fdFollowRedirects = new FormData();
+ fdFollowRedirects.top = new FormAttachment(wlFollowRedirects, 0,
SWT.CENTER);
+ fdFollowRedirects.left = new FormAttachment(middle, 0);
+ fdFollowRedirects.right = new FormAttachment(95, 0);
+ wFollowRedirects.setLayoutData(fdFollowRedirects);
+ lastControl = wFollowRedirects;
+
+ Label wlPreemptive = new Label(parent, SWT.RIGHT);
+ PropsUi.setLook(wlPreemptive);
+ wlPreemptive.setText(
+ BaseMessages.getString(PKG,
"WebDavConnectionEditor.PreemptiveAuth.Label"));
+ FormData fdlPreemptive = new FormData();
+ fdlPreemptive.top = new FormAttachment(lastControl, margin);
+ fdlPreemptive.left = new FormAttachment(0, 0);
+ fdlPreemptive.right = new FormAttachment(middle, -margin);
+ wlPreemptive.setLayoutData(fdlPreemptive);
+ wPreemptiveAuth = new Button(parent, SWT.CHECK);
+ PropsUi.setLook(wPreemptiveAuth);
+ FormData fdPreemptive = new FormData();
+ fdPreemptive.top = new FormAttachment(wlPreemptive, 0, SWT.CENTER);
+ fdPreemptive.left = new FormAttachment(middle, 0);
+ fdPreemptive.right = new FormAttachment(95, 0);
+ wPreemptiveAuth.setLayoutData(fdPreemptive);
+
+ setWidgetsContent();
+
+ wName.addModifyListener(e -> setChanged());
+ wDescription.addModifyListener(e -> setChanged());
+ wRootUrl.addModifyListener(e -> setChanged());
+ wUsername.addModifyListener(e -> setChanged());
+ wPassword.addModifyListener(e -> setChanged());
+ wFollowRedirects.addListener(SWT.Selection, e -> setChanged());
+ wPreemptiveAuth.addListener(SWT.Selection, e -> setChanged());
+ }
+
+ @Override
+ public void setWidgetsContent() {
+ WebDavConnection c = getMetadata();
+ wName.setText(Const.NVL(c.getName(), ""));
+ wDescription.setText(Const.NVL(c.getDescription(), ""));
+ wRootUrl.setText(Const.NVL(c.getRootUrl(), ""));
+ wUsername.setText(Const.NVL(c.getUsername(), ""));
+ wPassword.setText(Const.NVL(c.getPassword(), ""));
+ wFollowRedirects.setSelection(c.isFollowRedirects());
+ wPreemptiveAuth.setSelection(c.isPreemptiveAuth());
+ }
+
+ @Override
+ public void getWidgetsContent(WebDavConnection c) {
+ c.setName(wName.getText());
+ c.setDescription(wDescription.getText());
+ c.setRootUrl(wRootUrl.getText());
+ c.setUsername(wUsername.getText());
+ c.setPassword(wPassword.getText());
+ c.setFollowRedirects(wFollowRedirects.getSelection());
+ c.setPreemptiveAuth(wPreemptiveAuth.getSelection());
+ }
+
+ @Override
+ public boolean setFocus() {
+ if (wName == null || wName.isDisposed()) {
+ return false;
+ }
+ return wName.setFocus();
+ }
+
+ @Override
+ public void save() throws HopException {
+ super.save();
+ HopVfs.reset();
+ }
+}
diff --git
a/plugins/tech/webdav/src/main/resources/org/apache/hop/vfs/webdav/metadata/messages/messages_en_US.properties
b/plugins/tech/webdav/src/main/resources/org/apache/hop/vfs/webdav/metadata/messages/messages_en_US.properties
new file mode 100644
index 0000000000..61f6c171e0
--- /dev/null
+++
b/plugins/tech/webdav/src/main/resources/org/apache/hop/vfs/webdav/metadata/messages/messages_en_US.properties
@@ -0,0 +1,26 @@
+#
+# 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.
+#
+WebDavConnection.Name = WebDAV Connection
+WebDavConnection.Description = WebDAV root URL and credentials for named VFS
URLs (connection name as scheme)
+WebDavConnectionEditor.Name.Label = Name
+WebDavConnectionEditor.Description.Label = Description
+WebDavConnectionEditor.RootUrl.Label = WebDAV root URL
+WebDavConnectionEditor.RootUrl.Tooltip = Full URL including scheme webdav4://
or webdav4s://, e.g. webdav4s://localhost/remote.php/dav/files/admin/
+WebDavConnectionEditor.Username.Label = Username
+WebDavConnectionEditor.Password.Label = Password
+WebDavConnectionEditor.FollowRedirects.Label = Follow HTTP redirects
+WebDavConnectionEditor.PreemptiveAuth.Label = Preemptive basic authentication
diff --git a/plugins/tech/webdav/src/main/resources/version.xml
b/plugins/tech/webdav/src/main/resources/version.xml
new file mode 100644
index 0000000000..36ab20e22e
--- /dev/null
+++ b/plugins/tech/webdav/src/main/resources/version.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~ 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.
+ ~
+ -->
+
+<version>${project.version}</version>
diff --git
a/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParserTest.java
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParserTest.java
new file mode 100644
index 0000000000..7ff4f861d3
--- /dev/null
+++
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameParserTest.java
@@ -0,0 +1,67 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.apache.commons.vfs2.FileName;
+import org.apache.commons.vfs2.FileSystemException;
+import org.apache.commons.vfs2.FileType;
+import org.apache.hop.vfs.webdav.metadata.WebDavConnection;
+import org.junit.jupiter.api.Test;
+
+class HopWebDavFileNameParserTest {
+
+ @Test
+ void parseRoot() throws Exception {
+ WebDavConnection c = new WebDavConnection();
+ c.setName("mywebdav");
+ HopWebDavFileNameParser p = new HopWebDavFileNameParser(c);
+ FileName n = p.parseUri(null, null, "mywebdav:///");
+ assertEquals("mywebdav:///", n.getURI());
+ assertEquals(FileType.FOLDER, n.getType());
+ }
+
+ @Test
+ void parseFile_withSpaceInName() throws Exception {
+ WebDavConnection c = new WebDavConnection();
+ c.setName("myconn");
+ HopWebDavFileNameParser p = new HopWebDavFileNameParser(c);
+ FileName n = p.parseUri(null, null, "myconn:///Documents/New
document.docx");
+ assertEquals("myconn:///Documents/New document.docx", n.getURI());
+ assertEquals(FileType.FILE, n.getType());
+ }
+
+ @Test
+ void parseFile() throws Exception {
+ WebDavConnection c = new WebDavConnection();
+ c.setName("myconn");
+ HopWebDavFileNameParser p = new HopWebDavFileNameParser(c);
+ FileName n = p.parseUri(null, null, "myconn:///Documents/x.txt");
+ assertEquals("myconn:///Documents/x.txt", n.getURI());
+ assertEquals(FileType.FILE, n.getType());
+ }
+
+ @Test
+ void wrongSchemeThrows() {
+ WebDavConnection c = new WebDavConnection();
+ c.setName("a");
+ HopWebDavFileNameParser p = new HopWebDavFileNameParser(c);
+ assertThrows(FileSystemException.class, () -> p.parseUri(null, null,
"b:///x"));
+ }
+}
diff --git
a/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameTest.java
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameTest.java
new file mode 100644
index 0000000000..798dbb0534
--- /dev/null
+++
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavFileNameTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.commons.vfs2.FileType;
+import org.junit.jupiter.api.Test;
+
+class HopWebDavFileNameTest {
+
+ @Test
+ void getUri_usesConnectionScheme() {
+ HopWebDavFileName n = new HopWebDavFileName("mywebdav",
"/Documents/x.txt", FileType.FILE);
+ assertEquals("mywebdav", n.getScheme());
+ assertEquals("mywebdav:///Documents/x.txt", n.getURI());
+ assertEquals("mywebdav:///", n.getRootURI());
+ }
+
+ @Test
+ void createName_preservesScheme() {
+ HopWebDavFileName parent = new HopWebDavFileName("mywebdav", "/Documents",
FileType.FOLDER);
+ HopWebDavFileName child =
+ (HopWebDavFileName) parent.createName("/Documents/y.bin",
FileType.FILE);
+ assertEquals("mywebdav:///Documents/y.bin", child.getURI());
+ }
+
+ @Test
+ void buildChild_underRoot() {
+ HopWebDavFileName parent = new HopWebDavFileName("mywebdav", "/",
FileType.FOLDER);
+ HopWebDavFileName child = HopWebDavFileName.buildChild(parent,
"Readme.md", FileType.FILE);
+ assertEquals("mywebdav:///Readme.md", child.getURI());
+ }
+
+ @Test
+ void buildChild_underFolder() {
+ HopWebDavFileName parent = new HopWebDavFileName("mywebdav",
"/Documents/", FileType.FOLDER);
+ HopWebDavFileName child = HopWebDavFileName.buildChild(parent, "y.bin",
FileType.FILE);
+ assertEquals("mywebdav:///Documents/y.bin", child.getURI());
+ }
+}
diff --git
a/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUrisTest.java
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUrisTest.java
new file mode 100644
index 0000000000..73a1145269
--- /dev/null
+++
b/plugins/tech/webdav/src/test/java/org/apache/hop/vfs/webdav/HopWebDavLogicalUrisTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.hop.vfs.webdav;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+class HopWebDavLogicalUrisTest {
+
+ @Test
+ void rawPathFromUri_acceptsSpacesAndExtraSlashesFromVfsResolveName() throws
Exception {
+ assertEquals(
+ "/remote.php/dav/files/admin/New document.docx",
+ HopWebDavLogicalUris.rawPathFromUri(
+ "mywebdav:////remote.php/dav/files/admin/New document.docx"));
+ assertEquals(
+ "/Documents/New document.docx",
+ HopWebDavLogicalUris.rawPathFromUri("myconn:///Documents/New
document.docx"));
+ }
+
+ @Test
+ void wirePathPrefixFromRootUrl_normalizesSlash() throws Exception {
+ assertEquals(
+ "/remote.php/dav/files/admin/",
+ HopWebDavLogicalUris.wirePathPrefixFromRootUrl(
+ "webdav4://localhost/remote.php/dav/files/admin"));
+ }
+
+ @Test
+ void wirePathCombine_appendsRelativeUnderPrefix() {
+ assertEquals(
+ "/remote.php/dav/files/admin/Documents/x.txt",
+ HopWebDavFileSystem.wirePathCombine("/remote.php/dav/files/admin/",
"Documents/x.txt"));
+ }
+
+ @Test
+ void effectiveRelativeSegment_stripsDuplicateOfRootPrefix() {
+ assertEquals(
+ "",
+ HopWebDavFileSystem.effectiveRelativeSegment(
+ "/remote.php/dav/files/admin/", "/remote.php/dav/files/admin"));
+ assertEquals(
+ "",
+ HopWebDavFileSystem.effectiveRelativeSegment(
+ "/remote.php/dav/files/admin/", "/remote.php/dav/files/admin/"));
+ }
+
+ @Test
+ void effectiveRelativeSegment_keepsSuffixBeyondRootPrefix() {
+ assertEquals(
+ "Documents",
+ HopWebDavFileSystem.effectiveRelativeSegment(
+ "/remote.php/dav/files/admin/",
"/remote.php/dav/files/admin/Documents"));
+ assertEquals(
+ "Documents/x.txt",
+ HopWebDavFileSystem.effectiveRelativeSegment(
+ "/remote.php/dav/files/admin/",
"/remote.php/dav/files/admin/Documents/x.txt"));
+ }
+
+ @Test
+ void effectiveRelativeSegment_forHostRootPrefix_passesThroughLogicalPath() {
+ assertEquals(
+ "remote.php/dav/files/admin",
+ HopWebDavFileSystem.effectiveRelativeSegment("/",
"/remote.php/dav/files/admin/"));
+ }
+}
diff --git a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
index a63c66ba2a..7b0f75a6d2 100644
--- a/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
+++ b/ui/src/main/java/org/apache/hop/ui/core/vfs/HopVfsFileDialog.java
@@ -1247,7 +1247,7 @@ public class HopVfsFileDialog implements IFileDialog,
IDirectoryDialog {
}
}
- if (HopVfs.getFileObject(filename).isFolder()) {
+ if (HopVfs.getFileObject(filename, variables).isFolder()) {
String fullPath = FilenameUtils.concat(filename, saveFilename);
wFilename.setText(fullPath);
// Select the saveFilename part...