Bug 567736: [IO] Add FileUtils

Change-Id: I614eadf63b2f983b4f75ca0441145d1ed0dbfdc6
diff --git a/jcommons/org.eclipse.statet.jcommons.util-tests/META-INF/MANIFEST.MF b/jcommons/org.eclipse.statet.jcommons.util-tests/META-INF/MANIFEST.MF
index 574a5b5..83b7c28 100644
--- a/jcommons/org.eclipse.statet.jcommons.util-tests/META-INF/MANIFEST.MF
+++ b/jcommons/org.eclipse.statet.jcommons.util-tests/META-INF/MANIFEST.MF
@@ -7,5 +7,7 @@
 Bundle-Vendor: Eclipse StatET
 Bundle-Name: StatET JCommons - Util - Tests  (Incubation)
 Bundle-RequiredExecutionEnvironment: JavaSE-11
-Import-Package: org.junit.jupiter.api;version="5.6.0",
- org.junit.jupiter.api.condition;version="5.6.0"
+Import-Package: org.opentest4j;version="[1.2,2)",
+ org.junit.jupiter.api;version="5.6.0",
+ org.junit.jupiter.api.condition;version="5.6.0",
+ org.junit.jupiter.api.function;version="5.6.0"
diff --git a/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/io/FileUtilsTest.java b/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/io/FileUtilsTest.java
new file mode 100644
index 0000000..64b33d0
--- /dev/null
+++ b/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/io/FileUtilsTest.java
@@ -0,0 +1,264 @@
+/*=============================================================================#
+ # Copyright (c) 2020 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.jcommons.io;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assumptions.assumeFalse;
+
+import static org.eclipse.statet.jcommons.lang.ObjectUtils.nonNullAssert;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ProcessBuilder.Redirect;
+import java.nio.file.FileSystemException;
+import java.nio.file.Files;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.EnabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+
+
+@NonNullByDefault
+public class FileUtilsTest {
+	
+	
+	public FileUtilsTest() {
+	}
+	
+	
+	@Test
+	public void getUserWorkingDirectory() {
+		assertEquals(FileUtils.getUserWorkingDirectory(),
+				new File(System.getProperty("user.dir")).toPath().toAbsolutePath() );
+	}
+	
+	
+	@Test
+	@SuppressWarnings("null")
+	public void requireFileName_requireNotNull() {
+		assertThrows(NullPointerException.class, () -> {
+			FileUtils.requireFileName(FileUtils.requireFileName(null));
+		});
+	}
+	
+	@Test
+	public void requireFileName() {
+		assertEquals(FileUtils.requireFileName(Path.of("dir/test")),
+				Path.of("test") );
+		assertThrows(IllegalArgumentException.class, () -> {
+			FileUtils.requireFileName(FileUtils.requireFileName(Path.of("/")));
+		});
+	}
+	
+	
+	@Test
+	@SuppressWarnings("null")
+	public void deleteRecursively_requireNotNull() throws IOException {
+		assertThrows(NullPointerException.class, () -> {
+			FileUtils.deleteRecursively(null);
+		});
+	}
+	
+	@Test
+	public void deleteRecursively() throws IOException {
+		final Path testDir= Files.createTempDirectory("FileUtilsTest").toAbsolutePath();
+		final Path dir= Files.createDirectory(testDir.resolve("dir"));
+		
+		Files.writeString(dir.resolve("file"), "Hello");
+		Files.writeString(dir.resolve("file_2"), "Hello");
+		final Path subDir= Files.createDirectory(dir.resolve("sub"));
+		Files.writeString(subDir.resolve("file"), "Hello");
+		
+		FileUtils.deleteRecursively(dir);
+		
+		assertTrue(Files.notExists(dir));
+	}
+	
+	@Test
+	public void deleteRecursively_notExisting() throws IOException {
+		final Path testDir= Files.createTempDirectory("FileUtilsTest").toAbsolutePath();
+		final Path dir= testDir.resolve("dir");
+		
+		FileUtils.deleteRecursively(dir);
+		
+		assertTrue(Files.notExists(dir));
+	}
+	
+	@Test
+	public void deleteRecursively_SymbolicLinkFile() throws IOException {
+		final Path testDir= Files.createTempDirectory("FileUtilsTest").toAbsolutePath();
+		final Path dir= Files.createDirectory(testDir.resolve("dir"));
+		
+		final Path targetFile= Files.writeString(testDir.resolve("file"), "Hello");
+		
+		Files.writeString(dir.resolve("file"), "Hello");
+		try {
+			Files.createSymbolicLink(dir.resolve("file-link"), targetFile);
+		}
+		catch (final UnsupportedOperationException | FileSystemException e) {
+			assumeFalse(true, e.toString());
+		}
+		
+		FileUtils.deleteRecursively(dir);
+		
+		assertTrue(Files.notExists(dir));
+		assertTrue(Files.exists(targetFile));
+	}
+	
+	@Test
+	public void deleteRecursively_SymbolicLinkDir() throws IOException {
+		final Path testDir= Files.createTempDirectory("FileUtilsTest").toAbsolutePath();
+		final Path dir= Files.createDirectory(testDir.resolve("dir"));
+		
+		final Path targetDir= Files.createDirectory(testDir.resolve("file"));
+		final Path targetDirFile= Files.writeString(targetDir.resolve("file"), "Hello");
+		
+		Files.writeString(dir.resolve("file"), "Hello");
+		try {
+			Files.createSymbolicLink(dir.resolve("dir-link"), targetDir);
+		}
+		catch (final UnsupportedOperationException | FileSystemException e) {
+			assumeFalse(true, e.toString());
+		}
+		
+		FileUtils.deleteRecursively(dir);
+		
+		assertTrue(Files.notExists(dir));
+		assertTrue(Files.exists(targetDir));
+		assertTrue(Files.exists(targetDirFile));
+	}
+	
+	@Test
+	public void deleteRecursively_HardLinkFile() throws IOException {
+		final Path testDir= Files.createTempDirectory("FileUtilsTest").toAbsolutePath();
+		final Path dir= Files.createDirectory(testDir.resolve("dir"));
+		
+		final Path targetFile= Files.writeString(testDir.resolve("file"), "Hello");
+		
+		Files.writeString(dir.resolve("file"), "Hello");
+		try {
+			Files.createLink(dir.resolve("file-link"), targetFile);
+		}
+		catch (final UnsupportedOperationException | FileSystemException e) {
+			assumeFalse(true, e.toString());
+		}
+		
+		FileUtils.deleteRecursively(dir);
+		
+		assertTrue(Files.notExists(dir));
+		assertTrue(Files.exists(targetFile));
+	}
+	
+	@Test
+	@EnabledOnOs(OS.WINDOWS)
+	public void deleteRecursively_WinJunctionDir() throws IOException, InterruptedException {
+		final Path testDir= Files.createTempDirectory("FileUtilsTest").toAbsolutePath();
+		final Path dir= Files.createDirectory(testDir.resolve("dir"));
+		
+		final Path targetDir= Files.createDirectory(testDir.resolve("file"));
+		final Path targetDirFile= Files.writeString(targetDir.resolve("file"), "Hello");
+		
+		Files.writeString(dir.resolve("file"), "Hello");
+		try {
+			final Path linkDir= dir.resolve("dir-link");
+			final Process t= new ProcessBuilder("cmd", "/C", "mklink", "/J", linkDir.toString(), targetDir.toString())
+					.directory(testDir.toFile())
+					.redirectOutput(Redirect.DISCARD)
+					.redirectErrorStream(true)
+					.start();
+			t.waitFor(5, TimeUnit.SECONDS);
+			assertTrue(Files.isDirectory(linkDir));
+		}
+		catch (final UnsupportedOperationException | FileSystemException e) {
+			assumeFalse(true, e.toString());
+		}
+		
+		FileUtils.deleteRecursively(dir);
+		
+		assertTrue(Files.notExists(dir));
+		assertTrue(Files.exists(targetDir));
+		assertTrue(Files.exists(targetDirFile));
+	}
+	
+	
+	@Test
+	@SuppressWarnings("null")
+	public void cleanDirectory_requireNotNull() throws IOException {
+		assertThrows(NullPointerException.class, () -> {
+			FileUtils.cleanDirectory(null);
+		});
+	}
+	
+	@Test
+	public void cleanDirectory_requireDirectory() throws IOException {
+		final Path testDir= Files.createTempDirectory("FileUtilsTest").toAbsolutePath();
+		final Path dir= testDir.resolve("dir");
+		
+		assertThrows(NoSuchFileException.class, () -> {
+			FileUtils.cleanDirectory(dir);
+		});
+		
+		Files.writeString(dir, "Hello");
+		assertThrows(NotDirectoryException.class, () -> {
+			FileUtils.cleanDirectory(dir);
+		});
+	}
+	
+	@Test
+	public void cleanDirectory() throws IOException {
+		final Path testDir= Files.createTempDirectory("FileUtilsTest").toAbsolutePath();
+		final Path dir= Files.createDirectory(testDir.resolve("dir"));
+		
+		final Path file1= Files.writeString(dir.resolve("file"), "Hello");
+		final Path file2= Files.writeString(dir.resolve("file_2"), "Hello");
+		final Path subDir= Files.createDirectory(dir.resolve("sub"));
+		Files.writeString(subDir.resolve("file"), "Hello");
+		
+		FileUtils.cleanDirectory(dir);
+		
+		assertTrue(Files.exists(dir));
+		assertTrue(Files.notExists(file1));
+		assertTrue(Files.notExists(file2));
+		assertTrue(Files.notExists(subDir));
+	}
+	
+	@Test
+	public void cleanDirectory_withFilter() throws IOException {
+		final Path testDir= Files.createTempDirectory("FileUtilsTest").toAbsolutePath();
+		final Path dir= Files.createDirectory(testDir.resolve("dir"));
+		
+		final Path file1= Files.writeString(dir.resolve("file"), "Hello");
+		final Path file2= Files.writeString(dir.resolve("file_2"), "Hello");
+		final Path subDir= Files.createDirectory(dir.resolve("sub"));
+		Files.writeString(subDir.resolve("file"), "Hello");
+		
+		FileUtils.cleanDirectory(dir,
+				(path) -> !nonNullAssert(path.getFileName()).toString().equals("file_2") );
+		
+		assertTrue(Files.exists(dir));
+		assertTrue(Files.notExists(file1));
+		assertTrue(Files.exists(file2));
+		assertTrue(Files.notExists(subDir));
+	}
+	
+}
diff --git a/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/runtime/UriUtilsTest.java b/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/io/UriUtilsTest.java
similarity index 61%
rename from jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/runtime/UriUtilsTest.java
rename to jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/io/UriUtilsTest.java
index 7801442..31c399c 100644
--- a/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/runtime/UriUtilsTest.java
+++ b/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/io/UriUtilsTest.java
@@ -12,16 +12,20 @@
  #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
  #=============================================================================*/
 
-package org.eclipse.statet.jcommons.runtime;
+package org.eclipse.statet.jcommons.io;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.net.URI;
 import java.net.URISyntaxException;
 
 import org.junit.jupiter.api.Test;
 
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
 
+
+@NonNullByDefault
 public class UriUtilsTest {
 	
 	
@@ -57,6 +61,7 @@
 		assertEquals(false, UriUtils.isFileUrl("jar:/test/file.jar"));
 	}
 	
+	
 	@Test
 	public void isJarUrl() throws URISyntaxException {
 		assertEquals(true, UriUtils.isJarUrl(new URI("jar", "file:/test/file.jar!/", null)));
@@ -84,4 +89,66 @@
 		assertEquals(false, UriUtils.isJarUrl("file:/test/file.jar!/"));
 	}
 	
+	@Test
+	public void getJarFileUrl_requireJarUrl() throws URISyntaxException {
+		assertThrows(IllegalArgumentException.class, () -> {
+			UriUtils.getJarFileUrl(new URI("file:/test/file.jar"));
+		});
+		assertThrows(URISyntaxException.class, () -> {
+			UriUtils.getJarFileUrl(new URI("jar:file:/test/file.jar"));
+		});
+	}
+	
+	@Test
+	public void getJarFileUrl() throws URISyntaxException {
+		assertEquals(new URI("file:/test/file.jar"),
+				UriUtils.getJarFileUrl(new URI("jar:file:/test/file.jar!/")) );
+		
+		assertEquals(new URI("file:/test/file.jar"),
+				UriUtils.getJarFileUrl(new URI("jar:file:/test/file.jar!/path/file.txt")) );
+		
+		assertEquals(new URI("jar:file:/test/file.jar!/path/nested.jar"),
+				UriUtils.getJarFileUrl(new URI("jar:file:/test/file.jar!/path/nested.jar!/")) );
+	}
+	
+	@Test
+	public void getJarEntryPath_requireJarUrl() throws URISyntaxException {
+		assertThrows(IllegalArgumentException.class, () -> {
+			UriUtils.getJarEntryPath(new URI("file:/test/file.jar"));
+		});
+		assertThrows(URISyntaxException.class, () -> {
+			UriUtils.getJarEntryPath(new URI("jar:file:/test/file.jar"));
+		});
+	}
+	
+	@Test
+	public void getJarEntryPath() throws URISyntaxException {
+		assertEquals("",
+				UriUtils.getJarEntryPath(new URI("jar:file:/test/file.jar!/")) );
+		
+		assertEquals("path/file.txt",
+				UriUtils.getJarEntryPath(new URI("jar:file:/test/file.jar!/path/file.txt")) );
+		
+		assertEquals("",
+				UriUtils.getJarEntryPath(new URI("jar:file:/test/file.jar!/path/nested.jar!/")) );
+	}
+	
+	@Test
+	public void toJarUrl() throws URISyntaxException {
+		assertEquals(new URI("jar:file:/test/file.jar!/"),
+				UriUtils.toJarUrl("file:/test/file.jar") );
+		
+		assertEquals(new URI("jar:file:/test/file.jar!/path/nested.jar!/"),
+				UriUtils.toJarUrl("jar:file:/test/file.jar!/path/nested.jar") );
+	}
+	
+	@Test
+	public void toJarUrlString() throws URISyntaxException {
+		assertEquals("jar:file:/test/file.jar!/",
+				UriUtils.toJarUrlString("file:/test/file.jar") );
+		
+		assertEquals("jar:file:/test/file.jar!/path/nested.jar!/",
+				UriUtils.toJarUrlString("jar:file:/test/file.jar!/path/nested.jar") );
+	}
+	
 }
diff --git a/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/runtime/bundle/BundleEntryTest.java b/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/runtime/bundle/BundleEntryTest.java
index 1259973..4641328 100644
--- a/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/runtime/bundle/BundleEntryTest.java
+++ b/jcommons/org.eclipse.statet.jcommons.util-tests/src/org/eclipse/statet/jcommons/runtime/bundle/BundleEntryTest.java
@@ -24,7 +24,6 @@
 import java.net.URI;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
@@ -45,7 +44,7 @@
 	public void Entry_DevFolder() throws Exception { // in IDE
 		final String folder= nonNullAssert(System.getenv("STATET_TEST_FILES"));
 		
-		final Path path= Paths.get(
+		final Path path= Path.of(
 				new URI("file:/" + folder.replace('\\', '/') + "/bundle-locations/DevFolder/org.eclipse.statet-rj/core/org.eclipse.statet.rj.server") );
 		final BundleEntry entry= new BundleEntry.Dir("org.eclipse.statet.rj.server",
 				path, path.resolve("target/classes") );
@@ -59,7 +58,7 @@
 	public void Entry_SimpleServerJar() throws Exception { // in IDE
 		final String folder= nonNullAssert(System.getenv("STATET_TEST_FILES"));
 		
-		final Path path= Paths.get(
+		final Path path= Path.of(
 				new URI("file:/" + folder.replace('\\', '/') + "/bundle-locations/SimpleServerJar/rserver/org.eclipse.statet.rj.server.jar") );
 		final BundleEntry entry= new BundleEntry.Jar("org.eclipse.statet.rj.server", path);
 		
diff --git a/jcommons/org.eclipse.statet.jcommons.util/META-INF/MANIFEST.MF b/jcommons/org.eclipse.statet.jcommons.util/META-INF/MANIFEST.MF
index ba3e36a..d0cbf7f 100644
--- a/jcommons/org.eclipse.statet.jcommons.util/META-INF/MANIFEST.MF
+++ b/jcommons/org.eclipse.statet.jcommons.util/META-INF/MANIFEST.MF
@@ -11,6 +11,7 @@
 Require-Bundle: org.eclipse.core.runtime;bundle-version="3.16.0";resolution:=optional
 Export-Package: org.eclipse.statet.jcommons.collections;version="4.3.0",
  org.eclipse.statet.jcommons.concurrent;version="4.3.0",
+ org.eclipse.statet.jcommons.io;version="4.3.0",
  org.eclipse.statet.jcommons.lang;version="4.3.0",
  org.eclipse.statet.jcommons.net;version="4.3.0",
  org.eclipse.statet.jcommons.rmi;version="4.3.0",
diff --git a/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/io/FileUtils.java b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/io/FileUtils.java
new file mode 100644
index 0000000..1391046
--- /dev/null
+++ b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/io/FileUtils.java
@@ -0,0 +1,152 @@
+/*=============================================================================#
+ # Copyright (c) 2020 Stephan Wahlbrink and others.
+ # 
+ # This program and the accompanying materials are made available under the
+ # terms of the Eclipse Public License 2.0 which is available at
+ # https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
+ # which is available at https://www.apache.org/licenses/LICENSE-2.0.
+ # 
+ # SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
+ # 
+ # Contributors:
+ #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
+ #=============================================================================*/
+
+package org.eclipse.statet.jcommons.io;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.FileVisitResult;
+import java.nio.file.FileVisitor;
+import java.nio.file.Files;
+import java.nio.file.NotDirectoryException;
+import java.nio.file.Path;
+import java.nio.file.attribute.BasicFileAttributes;
+
+import org.eclipse.statet.jcommons.lang.NonNullByDefault;
+import org.eclipse.statet.jcommons.lang.Nullable;
+
+
+@NonNullByDefault
+public class FileUtils {
+	
+	
+	private static final Path WORKING_DIRECTORY_REF_PATH= Path.of(""); //$NON-NLS-1$
+	
+	/**
+	 * Returns the current working directory (= system property {@code user.dir}).
+	 * 
+	 * @return the path of the working directory
+	 * 
+	 * @see System#getProperties()
+	 */
+	public static final Path getUserWorkingDirectory() {
+		return WORKING_DIRECTORY_REF_PATH.toAbsolutePath();
+	}
+	
+	
+	/**
+	 * Checks if the specified path is not empty (= has a {@link Path#getFileName() file name}) and
+	 * returns its file name.
+	 * 
+	 * @param path the path
+	 * @return the file name segment of the path
+	 * @throws IllegalArgumentException if the path is empty
+	 */
+	public static final Path requireFileName(final Path path) {
+		final Path fileName= path.getFileName();
+		if (fileName == null) {
+			throw new IllegalArgumentException("path is empty"); //$NON-NLS-1$
+		}
+		return fileName;
+	}
+	
+	
+	private static final FileVisitor<Path> DELETE_R_VISITOR = new FileVisitor<>() {
+		
+		@Override
+		public FileVisitResult preVisitDirectory(final Path dir, final BasicFileAttributes attrs)
+				throws IOException {
+			if (attrs.isSymbolicLink() || attrs.isOther()) {
+				Files.delete(dir);
+				return FileVisitResult.SKIP_SUBTREE;
+			}
+			return FileVisitResult.CONTINUE;
+		}
+		
+		@Override
+		public FileVisitResult visitFile(final Path file, final BasicFileAttributes attrs)
+				throws IOException {
+			Files.delete(file);
+			return FileVisitResult.CONTINUE;
+		}
+		
+		@Override
+		public FileVisitResult visitFileFailed(final Path file, final IOException exc)
+				throws IOException {
+			throw exc;
+		}
+		
+		@Override
+		public FileVisitResult postVisitDirectory(final Path dir, @Nullable final IOException exc)
+				throws IOException {
+			Files.delete(dir);
+			return FileVisitResult.CONTINUE;
+		}
+		
+	};
+	
+	
+	/**
+	 * Deletes the specified file if it exists. If the file is a directory, its entries are deleted
+	 * recursively.
+	 * 
+	 * @param path the path of the file to delete
+	 * @return {@code true} if the file was deleted, {@code false} if the file does not exists
+	 * @throws IOException if an I/O error occurs
+	 */
+	public static final boolean deleteRecursively(final Path path) throws IOException {
+		if (Files.notExists(path)) {
+			return false;
+		}
+		Files.walkFileTree(path, DELETE_R_VISITOR);
+		return true;
+	}
+	
+	/**
+	 * Cleans the specified file directory.
+	 * 
+	 * @param path the path of the directory to clean
+	 * @param filter to select entries of the directory to delete
+	 * @throws NotDirectoryException if the file is not a directory
+	 * @throws IOException if an I/O error occurs
+	 */
+	public static final void cleanDirectory(final Path path,
+			final DirectoryStream.Filter<? super Path> filter) throws IOException {
+		try (final var stream= Files.newDirectoryStream(path, filter)) {
+			for (final Path entry : stream) {
+				deleteRecursively(entry);
+			}
+		}
+	}
+	
+	/**
+	 * Cleans the specified file directory.
+	 * 
+	 * @param path the path of the directory to clean
+	 * @throws NotDirectoryException if the file is not a directory
+	 * @throws IOException if an I/O error occurs
+	 */
+	public static final void cleanDirectory(final Path path) throws IOException {
+		try (final var entries= Files.newDirectoryStream(path)) {
+			for (final Path entry : entries) {
+				deleteRecursively(entry);
+			}
+		}
+	}
+	
+	
+	private FileUtils() {
+	}
+	
+}
diff --git a/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/UriUtils.java b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/io/UriUtils.java
similarity index 92%
rename from jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/UriUtils.java
rename to jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/io/UriUtils.java
index 4fecdb8..4dd810b 100644
--- a/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/UriUtils.java
+++ b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/io/UriUtils.java
@@ -12,7 +12,7 @@
  #     Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
  #=============================================================================*/
 
-package org.eclipse.statet.jcommons.runtime;
+package org.eclipse.statet.jcommons.io;
 
 import java.net.URI;
 import java.net.URISyntaxException;
@@ -134,7 +134,10 @@
 	 * 
 	 * @param url the JAR URL
 	 * @return the URL of the archive file
-	 * @throws URISyntaxException
+	 * @throws IllegalArgumentException if the specified URL is not a JAR URL
+	 * @throws URISyntaxException if the specified URL is invalid
+	 * 
+	 * @see {@link #getJarEntryPath(URI)} for the other part of the URL
 	 */
 	public static final URI getJarFileUrl(final URI url) throws URISyntaxException {
 		assertJarUrlArgument(url);
@@ -162,8 +165,11 @@
 	 * Return the path of the entry inside the JAR archive.
 	 * 
 	 * @param url the JAR URL
-	 * @return the path of the entry as String
-	 * @throws URISyntaxException
+	 * @return the path of the entry as String (without leading slash)
+	 * @throws IllegalArgumentException if the specified URL is not a JAR URL
+	 * @throws URISyntaxException if the specified URL is invalid
+	 * 
+	 * @see {@link #getJarFileUrl(URI)} for the other part of the URL
 	 */
 	public static final String getJarEntryPath(final URI url) throws URISyntaxException {
 		assertJarUrlArgument(url);
@@ -185,7 +191,7 @@
 	 * @param jarFileUrlString URL as String pointing to the JAR file itself
 	 * @return the JAR URL as String
 	 * @throws URISyntaxException
-	 **/
+	 */
 	public static final URI toJarUrl(final String jarFileUrlString) throws URISyntaxException {
 		return new URI(toJarUrlString(jarFileUrlString));
 	}
@@ -196,7 +202,7 @@
 	 * @param jarFileUrlString URL as String pointing to the JAR file itself
 	 * @return the JAR URL as String
 	 * @throws URISyntaxException
-	 **/
+	 */
 	public static final String toJarUrlString(final String jarFileUrlString) throws URISyntaxException {
 		final StringBuilder sb= new StringBuilder(jarFileUrlString.length() + 6);
 		if (!isSchemeUrl(jarFileUrlString, JAR_SCHEME)) {
diff --git a/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/ClassLoaderUtils.java b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/ClassLoaderUtils.java
index 6143a87..a6e3f28 100644
--- a/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/ClassLoaderUtils.java
+++ b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/ClassLoaderUtils.java
@@ -17,9 +17,9 @@
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.security.CodeSource;
 
+import org.eclipse.statet.jcommons.io.UriUtils;
 import org.eclipse.statet.jcommons.lang.NonNullByDefault;
 
 
@@ -99,7 +99,7 @@
 		}
 		try {
 			final URI uri= new URI(fileUrl);
-			final Path path= Paths.get(uri);
+			final Path path= Path.of(uri);
 			return path.toString();
 		}
 		catch (final URISyntaxException e) {
diff --git a/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/bundle/BundleEntry.java b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/bundle/BundleEntry.java
index 9f0cf07..fb9fa9b 100644
--- a/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/bundle/BundleEntry.java
+++ b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/bundle/BundleEntry.java
@@ -22,10 +22,10 @@
 import java.nio.file.ProviderNotFoundException;
 
 import org.eclipse.statet.internal.jcommons.runtime.CommonsRuntimeInternals;
+import org.eclipse.statet.jcommons.io.UriUtils;
 import org.eclipse.statet.jcommons.lang.NonNullByDefault;
 import org.eclipse.statet.jcommons.lang.Nullable;
 import org.eclipse.statet.jcommons.runtime.CommonsRuntime;
-import org.eclipse.statet.jcommons.runtime.UriUtils;
 import org.eclipse.statet.jcommons.status.ErrorStatus;
 
 
diff --git a/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/bundle/Bundles.java b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/bundle/Bundles.java
index 730556a..44220c0 100644
--- a/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/bundle/Bundles.java
+++ b/jcommons/org.eclipse.statet.jcommons.util/src/org/eclipse/statet/jcommons/runtime/bundle/Bundles.java
@@ -26,7 +26,6 @@
 import java.nio.file.FileSystemNotFoundException;
 import java.nio.file.FileSystems;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -36,10 +35,10 @@
 
 import org.eclipse.statet.jcommons.collections.ImCollections;
 import org.eclipse.statet.jcommons.collections.ImList;
+import org.eclipse.statet.jcommons.io.UriUtils;
 import org.eclipse.statet.jcommons.lang.NonNullByDefault;
 import org.eclipse.statet.jcommons.lang.Nullable;
 import org.eclipse.statet.jcommons.runtime.ClassLoaderUtils;
-import org.eclipse.statet.jcommons.runtime.UriUtils;
 import org.eclipse.statet.jcommons.runtime.bundle.BundleEntryProvider.DevBinPathEntryProvider;
 import org.eclipse.statet.jcommons.runtime.bundle.BundleEntryProvider.JarFilePathEntryProvider;
 import org.eclipse.statet.jcommons.status.ErrorStatus;
@@ -338,7 +337,7 @@
 		FileSystem fs= null;
 		while (true) {
 			try {
-				final Path path= Paths.get(url);
+				final Path path= Path.of(url);
 				return path.normalize();
 			}
 			catch (final FileSystemNotFoundException e) {
diff --git a/jcommons/org.eclipse.statet.jcommons.util/srcERuntime/org/eclipse/statet/internal/jcommons/runtime/eplatform/EPlatformBundleResolver.java b/jcommons/org.eclipse.statet.jcommons.util/srcERuntime/org/eclipse/statet/internal/jcommons/runtime/eplatform/EPlatformBundleResolver.java
index 2e3a454..76a2fcd 100644
--- a/jcommons/org.eclipse.statet.jcommons.util/srcERuntime/org/eclipse/statet/internal/jcommons/runtime/eplatform/EPlatformBundleResolver.java
+++ b/jcommons/org.eclipse.statet.jcommons.util/srcERuntime/org/eclipse/statet/internal/jcommons/runtime/eplatform/EPlatformBundleResolver.java
@@ -22,7 +22,6 @@
 import java.net.URL;
 import java.nio.file.Files;
 import java.nio.file.Path;
-import java.nio.file.Paths;
 import java.util.Set;
 
 import org.osgi.framework.Bundle;
@@ -31,10 +30,10 @@
 import org.eclipse.core.runtime.Platform;
 
 import org.eclipse.statet.internal.jcommons.runtime.CommonsRuntimeInternals;
+import org.eclipse.statet.jcommons.io.UriUtils;
 import org.eclipse.statet.jcommons.lang.NonNullByDefault;
 import org.eclipse.statet.jcommons.lang.Nullable;
 import org.eclipse.statet.jcommons.lang.ObjectUtils.ToStringBuilder;
-import org.eclipse.statet.jcommons.runtime.UriUtils;
 import org.eclipse.statet.jcommons.runtime.bundle.BundleEntry;
 import org.eclipse.statet.jcommons.runtime.bundle.BundleResolver;
 import org.eclipse.statet.jcommons.runtime.bundle.BundleSpec;
@@ -171,7 +170,7 @@
 			if (isJar && UriUtils.getJarEntryPath(url).isEmpty()) {
 				url= UriUtils.getJarFileUrl(url);
 			}
-			final Path path= Paths.get(url);
+			final Path path= Path.of(url);
 			if (Platform.inDevelopmentMode() && !isJar) {
 				final Path jClassPath;
 				if (Files.isDirectory(jClassPath= path.resolve("target/classes"))) {