From 02cfa98d2a6c4e9ceb386cf02828e541bf65fd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Link?= Date: Sun, 21 Apr 2024 16:08:55 +0200 Subject: [PATCH] Added Directory and RegularFile facades for convenient file operations. --- lis-commons-io/README.md | 0 .../com/link_intersystems/io/IOConsumer.java | 25 +++ .../io/file/AbstractFile.java | 54 ++++++ .../link_intersystems/io/file/Directory.java | 117 ++++++++++++ .../io/file/RegularFile.java | 131 ++++++++++++++ .../io/file/AbstractFileTest.java | 61 +++++++ .../io/file/DirectoryTest.java | 86 +++++++++ .../io/file/FileExampleTest.java | 20 +++ .../io/file/RegularFileTest.java | 170 ++++++++++++++++++ pom.xml | 6 + 10 files changed, 670 insertions(+) create mode 100644 lis-commons-io/README.md create mode 100644 lis-commons-io/src/main/java/com/link_intersystems/io/IOConsumer.java create mode 100644 lis-commons-io/src/main/java/com/link_intersystems/io/file/AbstractFile.java create mode 100644 lis-commons-io/src/main/java/com/link_intersystems/io/file/Directory.java create mode 100644 lis-commons-io/src/main/java/com/link_intersystems/io/file/RegularFile.java create mode 100644 lis-commons-io/src/test/java/com/link_intersystems/io/file/AbstractFileTest.java create mode 100644 lis-commons-io/src/test/java/com/link_intersystems/io/file/DirectoryTest.java create mode 100644 lis-commons-io/src/test/java/com/link_intersystems/io/file/FileExampleTest.java create mode 100644 lis-commons-io/src/test/java/com/link_intersystems/io/file/RegularFileTest.java diff --git a/lis-commons-io/README.md b/lis-commons-io/README.md new file mode 100644 index 00000000..e69de29b diff --git a/lis-commons-io/src/main/java/com/link_intersystems/io/IOConsumer.java b/lis-commons-io/src/main/java/com/link_intersystems/io/IOConsumer.java new file mode 100644 index 00000000..2a641f63 --- /dev/null +++ b/lis-commons-io/src/main/java/com/link_intersystems/io/IOConsumer.java @@ -0,0 +1,25 @@ +package com.link_intersystems.io; + +import java.io.IOException; + +/** + * An {@link java.util.function.Consumer} like interface for I/O related stuff. + * + * @param an I/O type that might raise an {@link IOException}. + */ +@FunctionalInterface +public interface IOConsumer { + + /** + * An {@link IOConsumer} that does nothing (no operation). + * + * @param + * @return + */ + public static IOConsumer noop() { + return t -> { + }; + } + + public void accept(T t) throws IOException; +} diff --git a/lis-commons-io/src/main/java/com/link_intersystems/io/file/AbstractFile.java b/lis-commons-io/src/main/java/com/link_intersystems/io/file/AbstractFile.java new file mode 100644 index 00000000..97a01270 --- /dev/null +++ b/lis-commons-io/src/main/java/com/link_intersystems/io/file/AbstractFile.java @@ -0,0 +1,54 @@ +package com.link_intersystems.io.file; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Objects; + +import static java.util.Objects.requireNonNull; + +public abstract class AbstractFile { + + private Path path; + + protected AbstractFile(Path path) { + this.path = requireNonNull(path); + } + + /** + * @return the {@link Path} of this {@link AbstractFile}. + */ + public Path getPath() { + return path; + } + + /** + * Creates this file. This method does nothing, if the file already exists. + * + * @throws IOException + */ + public abstract void create() throws IOException; + + /** + * @return the parent file of this file if any or null. + * @throws IOException + */ + public abstract AbstractFile getParent(); + + public boolean exists() { + return Files.exists(getPath()); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AbstractFile that = (AbstractFile) o; + return Objects.equals(getPath(), that.getPath()); + } + + @Override + public int hashCode() { + return Objects.hash(getPath()); + } +} diff --git a/lis-commons-io/src/main/java/com/link_intersystems/io/file/Directory.java b/lis-commons-io/src/main/java/com/link_intersystems/io/file/Directory.java new file mode 100644 index 00000000..5bf0b98b --- /dev/null +++ b/lis-commons-io/src/main/java/com/link_intersystems/io/file/Directory.java @@ -0,0 +1,117 @@ +package com.link_intersystems.io.file; + +import com.link_intersystems.io.IOConsumer; + +import java.io.File; +import java.io.IOException; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; + +public class Directory extends AbstractFile { + + /** + * Creates a {@link Directory} based on the given {@link File}. + */ + public Directory(File dir) { + this(dir.toPath()); + } + + /** + * Creates a {@link Directory} based on the given dirpath. + * + * @param dirpath the path of this {@link Directory}. + */ + public Directory(Path dirpath) { + super(dirpath); + } + + /** + * {@inheritDoc} + *

+ * If this directory does not exist, all parent directories that also do not exist + * will be created as well. + * + * @throws IOException + */ + @Override + public void create() throws IOException { + Files.createDirectories(getPath()); + } + + @Override + public Directory getParent() { + Path parent = getPath().getParent(); + + if (parent != null) { + return new Directory(parent); + } + + return null; + } + + /** + * @param directoryRelativePath + * @return the {@link RegularFile} of the given path relative to this directory. + * @throws IOException + */ + public RegularFile file(String directoryRelativePath) throws IOException { + return file(Paths.get(directoryRelativePath)); + } + + /** + * @param directoryRelativePath + * @return the {@link RegularFile} of the given path relative to this directory. + * @throws IOException + */ + public RegularFile file(Path directoryRelativePath) throws IOException { + return new RegularFile(getPath().resolve(directoryRelativePath)); + } + + /** + * @return all regular files in this directory. + * @throws IOException + */ + public List listFiles() throws IOException { + return listFiles(path -> true); + } + + /** + * @return all regular files in this directory that match the given file filter. + * @throws IOException + */ + public List listFiles(DirectoryStream.Filter fileFilter) throws IOException { + List files = new ArrayList<>(); + forEachFiles(files::add, fileFilter); + return files; + } + + /** + * Invokes the given {@link IOConsumer} for each {@link RegularFile} in this directory. + * + * @throws IOException + */ + public void forEachFiles(IOConsumer fileConsumer) throws IOException { + forEachFiles(fileConsumer, path -> true); + } + + /** + * Invokes the given {@link IOConsumer} for each {@link RegularFile} in this directory + * that match the given file filter. + * + * @throws IOException + */ + public void forEachFiles(IOConsumer fileConsumer, DirectoryStream.Filter fileFilter) throws IOException { + DirectoryStream.Filter filter = path -> Files.isRegularFile(path) && fileFilter.accept(path); + + try (DirectoryStream directoryStream = Files.newDirectoryStream(getPath(), filter)) { + for (Path path : directoryStream) { + RegularFile regularFile = new RegularFile(path); + fileConsumer.accept(regularFile); + } + } + } +} diff --git a/lis-commons-io/src/main/java/com/link_intersystems/io/file/RegularFile.java b/lis-commons-io/src/main/java/com/link_intersystems/io/file/RegularFile.java new file mode 100644 index 00000000..c1109245 --- /dev/null +++ b/lis-commons-io/src/main/java/com/link_intersystems/io/file/RegularFile.java @@ -0,0 +1,131 @@ +package com.link_intersystems.io.file; + +import com.link_intersystems.io.IOConsumer; + +import java.io.File; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.Charset; +import java.nio.file.Files; +import java.nio.file.Path; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.nio.file.StandardOpenOption.*; + +public class RegularFile extends AbstractFile { + + + /** + * Creates a {@link RegularFile} based on the given {@link File}. + */ + public RegularFile(File file) { + this(file.toPath()); + } + + /** + * Creates a {@link RegularFile} based on the given filepath. + * + * @param filepath the path of this {@link RegularFile}. + */ + public RegularFile(Path filepath) { + super(filepath); + } + + /** + * {@inheritDoc} + *

+ * If the file is created it will be empty. + *

+ * You do not have to call this method before any + * {@link #write(IOConsumer, Charset)} or {@link #append(IOConsumer, Charset)} + * invocation, because these methods will also create this file if it does not exist. + * + * @throws IOException + */ + @Override + public void create() throws IOException { + append(IOConsumer.noop()); + } + + @Override + public Directory getParent() { + Path parent = getPath().getParent(); + + if (parent != null) { + return new Directory(parent); + } + + return null; + } + + /** + * Writes the content provided by the {@link Appendable} to this {@link RegularFile} using {@link java.nio.charset.StandardCharsets#UTF_8}. + * + * @see #write(IOConsumer, Charset) + */ + public void write(IOConsumer contentWriter) throws IOException { + write(contentWriter, UTF_8); + } + + /** + * Writes the content provided by the {@link Appendable} to this {@link RegularFile} using the specified {@link Charset}. + * + *

    + *
  • If parent directories do not exist, they will be created.
  • + *
  • If the file does not exist, it will be created.
  • + *
  • If the file already exists, it will be overwritten.
  • + *
  • If the file is an existent directory, an {@link IOException} is raised.
  • + *
+ *

+ * This method can also be used to create an empty file + * + *

+     *      RegularFile regularFile = ...;
+     *      regularFile.write({@link IOConsumer#noop()});
+     *  
+ * + * @param contentWriter an {@link Appendable} {@link IOConsumer} used to write the content of this file. + * @throws IOException if the file is an existent directory or if the content could not be written. + */ + public void write(IOConsumer contentWriter, Charset charset) throws IOException { + ensureParentDirs(); + + try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(Files.newOutputStream(getPath(), CREATE, TRUNCATE_EXISTING), charset))) { + contentWriter.accept(pw); + } + } + + private void ensureParentDirs() throws IOException { + Path filePath = getPath(); + Path parentPath = filePath.getParent(); + if (parentPath != null && !Files.exists(parentPath)) { + Files.createDirectories(parentPath); + } + } + + public void append(IOConsumer contentWriter) throws IOException { + append(contentWriter, UTF_8); + } + + /** + * Appends content provided by the {@link Appendable} to this {@link RegularFile} using the specified {@link Charset}. + * + *
    + *
  • If parent directories do not exist, they will be created.
  • + *
  • If the file does not exist, it will be created and the content will be appended.
  • + *
  • If the file already exists, the content will be appended.
  • + *
  • If the file is an existent directory an {@link IOException} is raised.
  • + *
+ * + * @param contentWriter an {@link Appendable} {@link IOConsumer} used to append to the content of this file. + * @throws IOException if the file is an existent directory or if the content could not be appended. + */ + public void append(IOConsumer contentWriter, Charset charset) throws IOException { + ensureParentDirs(); + + try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(Files.newOutputStream(getPath(), CREATE, APPEND), charset))) { + contentWriter.accept(pw); + } + } +} \ No newline at end of file diff --git a/lis-commons-io/src/test/java/com/link_intersystems/io/file/AbstractFileTest.java b/lis-commons-io/src/test/java/com/link_intersystems/io/file/AbstractFileTest.java new file mode 100644 index 00000000..235d9c7f --- /dev/null +++ b/lis-commons-io/src/test/java/com/link_intersystems/io/file/AbstractFileTest.java @@ -0,0 +1,61 @@ +package com.link_intersystems.io.file; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.*; + +class AbstractFileTest { + + private static class TestFile extends AbstractFile { + + protected TestFile(Path path) { + super(path); + } + + @Override + public void create() { + + } + + @Override + public AbstractFile getParent() { + return null; + } + } + + private AbstractFile abstractFile; + + @BeforeEach + void setUp(@TempDir Path tempDir) { + abstractFile = new TestFile(tempDir); + } + + @Test + void exists() { + assertTrue(abstractFile.exists()); + } + + @Test + void testEquals(@TempDir Path unequalPath) { + TestFile equal = new TestFile(abstractFile.getPath()); + TestFile unequal = new TestFile(unequalPath); + + assertEquals(abstractFile, equal); + assertEquals(equal, abstractFile); + assertNotEquals(equal, unequal); + assertNotEquals(equal, null); + assertNotEquals(equal, ""); + } + + @Test + void testHashCode() { + TestFile actual = new TestFile(abstractFile.getPath()); + + assertEquals(abstractFile.hashCode(), actual.hashCode()); + } +} \ No newline at end of file diff --git a/lis-commons-io/src/test/java/com/link_intersystems/io/file/DirectoryTest.java b/lis-commons-io/src/test/java/com/link_intersystems/io/file/DirectoryTest.java new file mode 100644 index 00000000..84fe456a --- /dev/null +++ b/lis-commons-io/src/test/java/com/link_intersystems/io/file/DirectoryTest.java @@ -0,0 +1,86 @@ +package com.link_intersystems.io.file; + + +import com.link_intersystems.io.IOConsumer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +class DirectoryTest { + + @Test + void createDir(@TempDir File tempDir) throws IOException { + Directory newDir = new Directory(new File(tempDir, "newDir")); + + newDir.create(); + org.assertj.core.api.Assertions.assertThat(newDir.getPath()).exists(); + } + + @Test + void createDirIfExist(@TempDir File tempDir) throws IOException { + File dir = new File(tempDir, "newDir"); + Files.createDirectories(dir.toPath()); + org.assertj.core.api.Assertions.assertThat(dir.toPath()).exists(); + + Directory newDir = new Directory(dir); + newDir.create(); + + org.assertj.core.api.Assertions.assertThat(newDir.getPath()).exists(); + } + + @Test + void getParent(@TempDir File tempDir) throws IOException { + createDir(tempDir); + + Directory newDir = new Directory(new File(tempDir, "newDir")); + + Directory parentDirectory = newDir.getParent(); + assertNotNull(parentDirectory); + + Directory directory = new Directory(tempDir); + assertEquals(directory, parentDirectory); + } + + @Test + void createFile(@TempDir File tempDir) throws IOException { + + Directory directory = new Directory(tempDir); + RegularFile readme = directory.file("README.md"); + assertNotNull(readme); + readme.write(IOConsumer.noop()); + assertTrue(new File(tempDir, "README.md").isFile()); + assertEquals(tempDir.toPath(), readme.getPath().getParent()); + } + + @Test + void listFiles(@TempDir File tempDir) throws IOException { + File src = new File(tempDir, "src"); + assertTrue(src.mkdirs()); + File main = new File(src, "main"); + assertTrue(main.mkdirs()); + File java = new File(main, "java"); + assertTrue(java.mkdirs()); + File mainJava = new File(java, "Main.java"); + assertTrue(mainJava.createNewFile()); + + File readme = new File(tempDir, "README.md"); + assertTrue(readme.createNewFile()); + + File license = new File(tempDir, "LICENSE.md"); + assertTrue(license.createNewFile()); + + + Directory directory = new Directory(tempDir); + + List files = directory.listFiles(); + + assertEquals(2, files.size()); + + } +} \ No newline at end of file diff --git a/lis-commons-io/src/test/java/com/link_intersystems/io/file/FileExampleTest.java b/lis-commons-io/src/test/java/com/link_intersystems/io/file/FileExampleTest.java new file mode 100644 index 00000000..2dbd429b --- /dev/null +++ b/lis-commons-io/src/test/java/com/link_intersystems/io/file/FileExampleTest.java @@ -0,0 +1,20 @@ +package com.link_intersystems.io.file; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Path; + +public class FileExampleTest { + + @Test + void generateAGradleProject(@TempDir Path tempDir) throws IOException { + Directory project = new Directory(tempDir.resolve("project")); + RegularFile settings = project.file("settings.gradle.kts"); + settings.write(content -> { + content.append("plugins {\n"); + content.append("plugins {\n"); + }); + } +} diff --git a/lis-commons-io/src/test/java/com/link_intersystems/io/file/RegularFileTest.java b/lis-commons-io/src/test/java/com/link_intersystems/io/file/RegularFileTest.java new file mode 100644 index 00000000..11115893 --- /dev/null +++ b/lis-commons-io/src/test/java/com/link_intersystems/io/file/RegularFileTest.java @@ -0,0 +1,170 @@ +package com.link_intersystems.io.file; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; + +import static java.nio.charset.StandardCharsets.*; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class RegularFileTest { + + @Test + void create(@TempDir File tempDir) throws IOException { + RegularFile regularFile = new RegularFile(new File(tempDir, "greetings.txt")); + org.assertj.core.api.Assertions.assertThat(regularFile.getPath()).doesNotExist(); + + regularFile.create(); + + org.assertj.core.api.Assertions.assertThat(regularFile.getPath()).exists(); + org.assertj.core.api.Assertions.assertThat(regularFile.getPath()).isEmptyFile(); + } + + @Test + void writeCreateParentDirs(@TempDir File tempDir) throws IOException { + File noneExistingDirectory = new File(tempDir, "someDir/someOtherDir"); + writeNoneExistingFile(noneExistingDirectory); + } + + @Test + void writeNoneExistingFile(@TempDir File tempDir) throws IOException { + RegularFile regularFile = createRegularFile(tempDir, "greetings.txt"); + + regularFile.write(appender -> { + appender.append("Hello\nRené!"); + }); + + org.assertj.core.api.Assertions.assertThat(regularFile.getPath()).content(UTF_8).isEqualTo( + "Hello\nRené!" + ); + } + + private RegularFile createRegularFile(File tempDir, String filename) { + File file = new File(tempDir, filename); + return new RegularFile(file); + } + + @Test + void overwriteExistingFile(@TempDir File tempDir) throws IOException { + writeNoneExistingFile(tempDir); + + RegularFile regularFile = createRegularFile(tempDir, "greetings.txt"); + + regularFile.write(appender -> { + appender.append("Hello\nLink!"); + }); + + org.assertj.core.api.Assertions.assertThat(regularFile.getPath()).content(UTF_8).isEqualTo( + "Hello\nLink!" + ); + } + + @Test + void tryWriteToADirectory(@TempDir File tempDir) { + + RegularFile regularFile = new RegularFile(tempDir); + + assertThrows(IOException.class, () -> regularFile.write(appender -> { + })); + } + + @Test + void writeWithCharset(@TempDir File tempDir) throws IOException { + RegularFile regularFile = createRegularFile(tempDir, "greetings.txt"); + + regularFile.write(appender -> { + appender.append("Hello\nRené!"); + }, UTF_16); + + org.assertj.core.api.Assertions.assertThat(regularFile.getPath()).content(UTF_16).isEqualTo( + "Hello\nRené!" + ); + } + + @Test + void appendCreateParentDirs(@TempDir File tempDir) throws IOException { + File noneExistingDirectory = new File(tempDir, "someDir/someOtherDir"); + appendToNoneExistingFile(noneExistingDirectory); + } + + + @Test + void appendToNoneExistingFile(@TempDir File tempDir) throws IOException { + RegularFile regularFile = createRegularFile(tempDir, "greetings.txt"); + + regularFile.append(appender -> { + appender.append("Hello\nRené!"); + }); + + org.assertj.core.api.Assertions.assertThat(regularFile.getPath()).content(UTF_8).isEqualTo( + "Hello\nRené!" + ); + } + + @Test + void appendToFile(@TempDir File tempDir) throws IOException { + appendToNoneExistingFile(tempDir); + + RegularFile regularFile = createRegularFile(tempDir, "greetings.txt"); + + regularFile.append(appender -> { + appender.append("\nHello\nWorld!"); + }); + + org.assertj.core.api.Assertions.assertThat(regularFile.getPath()).content(UTF_8).isEqualTo( + "Hello\nRené!\nHello\nWorld!" + ); + } + + @Test + void appendToADirectory(@TempDir File tempDir) { + + RegularFile regularFile = new RegularFile(tempDir); + + assertThrows(IOException.class, () -> regularFile.write(appender -> { + })); + } + + @Test + void appendContent(@TempDir File tempDir) throws IOException { + RegularFile fileBuilder = createRegularFile(tempDir, "greetings.txt"); + + fileBuilder.write(appender -> { + appender.append("Hello\nRené!"); + }); + + fileBuilder.append(appender -> { + appender.append("\nHow are you?"); + }); + + org.assertj.core.api.Assertions.assertThat(fileBuilder.getPath()).content(UTF_8).isEqualTo( + "Hello\nRené!\nHow are you?" + ); + } + + + @Test + void appendContentDifferentCharsets(@TempDir File tempDir) throws IOException { + RegularFile fileBuilder = createRegularFile(tempDir, "greetings.txt"); + + fileBuilder.write(appender -> { + appender.append("Hello\nRené!"); + }, UTF_8); + + fileBuilder.append(appender -> { + appender.append("\nHow are you René?"); + }, ISO_8859_1); + + org.assertj.core.api.Assertions.assertThat(fileBuilder.getPath()).content(UTF_8).isEqualTo( + "Hello\nRené!\nHow are you Ren�?" + ); + + org.assertj.core.api.Assertions.assertThat(fileBuilder.getPath()).content(ISO_8859_1).isEqualTo( + "Hello\nRené!\nHow are you René?" + ); + } + + +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index f6d0bf44..fcb5ce7f 100644 --- a/pom.xml +++ b/pom.xml @@ -64,6 +64,12 @@ org.mockito mockito-core + + org.assertj + assertj-core + 3.25.3 + test +