From 10a7eaebd1af313eeb1e3662751fc289d4359251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Link?= Date: Wed, 3 Jan 2024 09:38:56 +0100 Subject: [PATCH] Added mkdir to FileBuilder. --- .../com/link_intersystems/io/FileBuilder.java | 110 ++++++++---------- .../com/link_intersystems/io/IOConsumer.java | 13 +++ .../com/link_intersystems/io/IOConsumers.java | 55 +++++++++ .../io/StringInputStream.java | 95 +++++++++++++++ .../link_intersystems/io/FileBuilderTest.java | 57 +++++---- .../io/StringInputStreamTest.java | 107 +++++++++++++++++ 6 files changed, 347 insertions(+), 90 deletions(-) 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/IOConsumers.java create mode 100644 lis-commons-io/src/main/java/com/link_intersystems/io/StringInputStream.java create mode 100644 lis-commons-io/src/test/java/com/link_intersystems/io/StringInputStreamTest.java diff --git a/lis-commons-io/src/main/java/com/link_intersystems/io/FileBuilder.java b/lis-commons-io/src/main/java/com/link_intersystems/io/FileBuilder.java index 3a9567e3..0f4871fb 100644 --- a/lis-commons-io/src/main/java/com/link_intersystems/io/FileBuilder.java +++ b/lis-commons-io/src/main/java/com/link_intersystems/io/FileBuilder.java @@ -3,93 +3,83 @@ import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; -import java.io.OutputStream; -import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.ReadableByteChannel; import java.nio.channels.WritableByteChannel; -import java.nio.charset.Charset; +import java.nio.file.Files; import java.nio.file.Path; -import java.util.Objects; -import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.file.Files.newByteChannel; import static java.nio.file.StandardOpenOption.CREATE_NEW; import static java.nio.file.StandardOpenOption.WRITE; +/** + * A helper factory for creating directory structures. + */ public class FileBuilder { - private static final int EOF = -1; public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; - private int writeBufferSize = 8192; - - public static interface ContentWriter { - - public void write(T writer) throws IOException; - } - - private final Path path; - - public FileBuilder(Path path) { - this.path = Objects.requireNonNull(path); - } - - public Path getPath() { - return path; - } - - public Path writeFile(String name) throws IOException { - - return writeFileStream(name, new ByteArrayInputStream(EMPTY_BYTE_ARRAY)); + private final Path dirpath; + + /** + * @param dirpath the directory path that this {@link FileBuilder} operates on. + */ + public FileBuilder(Path dirpath) { + if (!Files.isDirectory(dirpath)) { + throw new IllegalArgumentException(dirpath + " is not a directory."); + } + this.dirpath = dirpath; } - public Path writeFile(String name, String content) throws IOException { - - return writeFile(name, content, UTF_8); + public Path getDirpath() { + return dirpath; } - public Path writeFile(String name, String content, Charset charset) throws IOException { + /** + * Creates an empty file. + * + * @param name + * @return the path of the written file. + * @throws IOException if the file already exists. + */ + public Path createFile(String name) throws IOException { - return writeFileStream(name, new ByteArrayInputStream(content.getBytes(charset))); + return createFile(name, Channels.newChannel(new ByteArrayInputStream(EMPTY_BYTE_ARRAY))); } - public Path writeFileStream(String name, InputStream content) throws IOException { - try(ReadableByteChannel readableByteChannel = Channels.newChannel(content)){ - return writeFile(name, readableByteChannel); - } + /** + * Writes a file of the given name with the content provided by the {@link ReadableByteChannel}. + * Use {@link Channels#newChannel(InputStream)} if you want to use an {@link InputStream} instead. + * + * @param name + * @param content + * @throws IOException if the file exists. + */ + public Path createFile(String name, ReadableByteChannel content) throws IOException { + return createFile(name, IOConsumers.readableChannelCopyConsumer(content)); } - public Path writeFile(String name, ReadableByteChannel content) throws IOException { - return writeFile(name, contentWriter -> { - ByteBuffer byteBuffer = createByteBuffer(writeBufferSize); - while (content.read(byteBuffer) != EOF) { - byteBuffer.flip(); - contentWriter.write(byteBuffer); - byteBuffer.flip(); - } - }); - } - - public Path writeFileStream(String name, ContentWriter contentWriter) throws IOException { - return writeFile(name, writer -> { - try (OutputStream outputStream = Channels.newOutputStream(writer)) { - contentWriter.write(outputStream); - } - }); - } - - public Path writeFile(String name, ContentWriter contentWriter) throws IOException { - Path filepath = path.resolve(name); + /** + * Writes a file of the given name, the content is provided by the {@link IOConsumer} callback. + * Use {@link IOConsumers#adaptOutputStream(IOConsumer)} if you want to use an {@link java.io.OutputStream} instead. + * + * @param name + * @param writableChannelConsumer + * @throws IOException if the file exists. + */ + public Path createFile(String name, IOConsumer writableChannelConsumer) throws IOException { + Path filepath = getDirpath().resolve(name); try (WritableByteChannel writableChannel = newByteChannel(filepath, CREATE_NEW, WRITE)) { - contentWriter.write(writableChannel); + writableChannelConsumer.accept(writableChannel); } return filepath; } - private ByteBuffer createByteBuffer(int size) { - return ByteBuffer.allocateDirect(size); + public FileBuilder mkdir(String name) throws IOException { + Path newDirpath = dirpath.resolve(name); + Files.createDirectories(newDirpath); + return new FileBuilder(newDirpath); } - } 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..842c867d --- /dev/null +++ b/lis-commons-io/src/main/java/com/link_intersystems/io/IOConsumer.java @@ -0,0 +1,13 @@ +package com.link_intersystems.io; + +import java.io.IOException; + +/** + * An io-related {@link java.util.function.Consumer} api that supports {@link IOException}s. + * + * @param + */ +public interface IOConsumer { + + public void accept(T io) throws IOException; +} diff --git a/lis-commons-io/src/main/java/com/link_intersystems/io/IOConsumers.java b/lis-commons-io/src/main/java/com/link_intersystems/io/IOConsumers.java new file mode 100644 index 00000000..783e2bbd --- /dev/null +++ b/lis-commons-io/src/main/java/com/link_intersystems/io/IOConsumers.java @@ -0,0 +1,55 @@ +package com.link_intersystems.io; + +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; + +/** + * Factory for creating {@link IOConsumer} adapters. + */ +public class IOConsumers { + + /** + * Creates an {@link IOConsumer} adapter for an {@link IOConsumer}. + * + * @param outputStreamIOConsumer + * @return + */ + public static IOConsumer adaptOutputStream(IOConsumer outputStreamIOConsumer) { + return writer -> { + try (OutputStream outputStream = Channels.newOutputStream(writer)) { + outputStreamIOConsumer.accept(outputStream); + } + }; + } + + /** + * Creates an {@link IOConsumer} that will copy the given {@link ReadableByteChannel} to the {@link WritableByteChannel} when + * {@link IOConsumer#accept(Object)} is invoked. A direct {@link ByteBuffer} of size 8192 is used. + * + * @param readableByteChannel + */ + public static IOConsumer readableChannelCopyConsumer(ReadableByteChannel readableByteChannel) { + return readableChannelCopyConsumer(readableByteChannel, ByteBuffer.allocateDirect(8192)); + } + + /** + * Creates an {@link IOConsumer} that will copy the given {@link ReadableByteChannel} to the {@link WritableByteChannel} when + * {@link IOConsumer#accept(Object)} is invoked. The given {@link ByteBuffer} will be used for the copy process. + * + * @param readableByteChannel + * @param byteBuffer the {@link ByteBuffer} to use for the copy process. + */ + public static IOConsumer readableChannelCopyConsumer(ReadableByteChannel readableByteChannel, ByteBuffer byteBuffer) { + return contentWriter -> { + while (readableByteChannel.read(byteBuffer) != -1) { + byteBuffer.flip(); + contentWriter.write(byteBuffer); + byteBuffer.flip(); + } + }; + } + +} diff --git a/lis-commons-io/src/main/java/com/link_intersystems/io/StringInputStream.java b/lis-commons-io/src/main/java/com/link_intersystems/io/StringInputStream.java new file mode 100644 index 00000000..436392eb --- /dev/null +++ b/lis-commons-io/src/main/java/com/link_intersystems/io/StringInputStream.java @@ -0,0 +1,95 @@ +package com.link_intersystems.io; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Objects.requireNonNull; + +/** + * An {@link InputStream} adapter for a {@link CharSequence}.Even though this class is based on the {@link CharSequence} interface, + * the is named {@link StringInputStream}, because almost noone would find it if it would be named CharSequenceInputStream. + */ +public class StringInputStream extends InputStream { + private CharBuffer charBuffer = CharBuffer.allocate(1); + private ByteBuffer byteBuffer = ByteBuffer.allocate(0); + + private int pos = 0; + private CharSequence charSequence; + + private Charset charset; + private int readLimitPos = -1; + private int resetPos = -1; + + public StringInputStream(CharSequence charSequence) { + this(charSequence, UTF_8); + } + + public StringInputStream(CharSequence charSequence, Charset charset) { + this.charSequence = requireNonNull(charSequence); + this.charset = requireNonNull(charset); + } + + @Override + public boolean markSupported() { + return true; + } + + @Override + public synchronized void mark(int readlimit) { + this.readLimitPos = pos + readlimit; + this.resetPos = pos; + } + + @Override + public synchronized void reset() throws IOException { + if (readLimitPos == -1) { + throw new IOException("Stream not marked."); + } + + if (pos > readLimitPos) { + throw new IOException("Read limit exceeded."); + } + + pos = resetPos; + byteBuffer = ByteBuffer.allocate(0); + } + + @Override + public int read() throws IOException { + try { + if (!byteBuffer.hasRemaining()) { + int charAt = readChar(); + if (charAt == -1) { + return -1; + } + + charBuffer.put((char) charAt); + charBuffer.flip(); + byteBuffer = charset.encode(charBuffer); + charBuffer.flip(); + } + + return (int) byteBuffer.get(); + } catch (NullPointerException e) { + throw new IOException("Stream closed."); + } + } + + private int readChar() { + if (pos < charSequence.length()) { + return charSequence.charAt(pos++); + } + return -1; + } + + @Override + public void close() throws IOException { + charSequence = null; + byteBuffer = null; + charBuffer = null; + } +} diff --git a/lis-commons-io/src/test/java/com/link_intersystems/io/FileBuilderTest.java b/lis-commons-io/src/test/java/com/link_intersystems/io/FileBuilderTest.java index ef248052..a8c81c97 100644 --- a/lis-commons-io/src/test/java/com/link_intersystems/io/FileBuilderTest.java +++ b/lis-commons-io/src/test/java/com/link_intersystems/io/FileBuilderTest.java @@ -1,73 +1,70 @@ package com.link_intersystems.io; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; -import java.io.ByteArrayInputStream; import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; import java.nio.file.Path; -import static java.nio.charset.StandardCharsets.UTF_16; import static java.nio.charset.StandardCharsets.UTF_8; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.*; class FileBuilderTest { private static final String TEST_CONTENT = "abcdefghijklmnopqrstuvwxyzéßöäü"; private FileBuilder fileBuilder; + private Path tempDirPath; @BeforeEach - void setUp(@TempDir Path basepath) { + void setUp(@TempDir Path tempDirPath) { + this.tempDirPath = tempDirPath; - fileBuilder = new FileBuilder(basepath); + fileBuilder = new FileBuilder(tempDirPath); - Assertions.assertEquals(basepath, fileBuilder.getPath()); + assertEquals(tempDirPath, fileBuilder.getDirpath()); } @Test - void writeFileByInputStream() throws IOException { - Path file = fileBuilder.writeFileStream("test", new ByteArrayInputStream(TEST_CONTENT.getBytes(UTF_8))); + void dirpathIsNotADirectory() throws IOException { + Path test = tempDirPath.resolve("test"); + Path filepath = Files.createFile(test); - assertThat(file).hasFileName("test"); - assertThat(file).content(UTF_8).isEqualTo(TEST_CONTENT); + assertThrows(IllegalArgumentException.class, () -> new FileBuilder(filepath)); } @Test - void writeFile() throws IOException { - Path file = fileBuilder.writeFileStream("test", out -> { - out.write(TEST_CONTENT.getBytes(UTF_8)); - }); + void createEmptyFile() throws IOException { + Path file = fileBuilder.createFile("test"); + assertEquals(tempDirPath.resolve("test"), file); assertThat(file).hasFileName("test"); - assertThat(file).content(UTF_8).isEqualTo(TEST_CONTENT); + assertThat(file).content(UTF_8).isEmpty(); } @Test - void writeFileByStringAndCharset() throws IOException { - Path file = fileBuilder.writeFile("test", TEST_CONTENT, UTF_16); + void createFile() throws IOException { + Path file = fileBuilder.createFile("test", writer -> { + writer.write(ByteBuffer.wrap("Hello World".getBytes(UTF_8))); + }); + assertEquals(tempDirPath.resolve("test"), file); assertThat(file).hasFileName("test"); - assertThat(file).content(UTF_16).isEqualTo(TEST_CONTENT); + assertThat(file).content(UTF_8).isEqualTo("Hello World"); } @Test - void writeFileByStringWithDefaultCharset() throws IOException { - Path file = fileBuilder.writeFile("test", TEST_CONTENT); + void createDirectory() throws IOException { + FileBuilder dir1 = fileBuilder.mkdir("dir1"); - assertThat(file).hasFileName("test"); - assertThat(file).content(UTF_8).isEqualTo(TEST_CONTENT); - } + assertNotNull(dir1); + Path expectedDirpath = tempDirPath.resolve("dir1"); - @Test - void writeEmptyFile() throws IOException { - Path file = fileBuilder.writeFile("test"); - - assertThat(file).hasFileName("test"); - assertThat(file).content(UTF_8).isEmpty(); + assertEquals(expectedDirpath, dir1.getDirpath()); } - } \ No newline at end of file diff --git a/lis-commons-io/src/test/java/com/link_intersystems/io/StringInputStreamTest.java b/lis-commons-io/src/test/java/com/link_intersystems/io/StringInputStreamTest.java new file mode 100644 index 00000000..48e218d8 --- /dev/null +++ b/lis-commons-io/src/test/java/com/link_intersystems/io/StringInputStreamTest.java @@ -0,0 +1,107 @@ +package com.link_intersystems.io; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.junit.jupiter.api.Assertions.*; + +class StringInputStreamTest { + + @Test + void read() throws IOException { + String str = "öäüß"; + + try (InputStream inputStream = new StringInputStream(str)) { + byte[] bytes = str.getBytes(UTF_8); + + for (int i = 0; i < bytes.length; i++) { + byte aByte = bytes[i]; + assertEquals(aByte, inputStream.read()); + } + + assertEquals(-1, inputStream.read()); + assertEquals(-1, inputStream.read()); + } + } + + @Test + void readClosedStream() throws IOException { + InputStream inputStream = new StringInputStream("öäüß"); + inputStream.close(); + + assertThrows(IOException.class, () -> inputStream.read()); + } + + @Test + void markSupported() throws IOException { + try (InputStream in = new StringInputStream("Hello World")) { + assertTrue(in.markSupported()); + } + } + + @Test + void markAndReset() throws IOException { + try (InputStream in = new StringInputStream("Hello World")) { + + in.mark(5); + byte[] bytes = new byte[5]; + + in.read(bytes); + assertArrayEquals("Hello".getBytes(UTF_8), bytes); + + in.reset(); + + in.read(bytes); + assertArrayEquals("Hello".getBytes(UTF_8), bytes); + } + } + + @Test + void multipleMarks() throws IOException { + try (InputStream in = new StringInputStream("Hello World")) { + + in.mark(2); + byte[] bytes = new byte[2]; + + in.read(bytes); + assertArrayEquals("He".getBytes(UTF_8), bytes); + in.reset(); + + in.read(bytes); + assertArrayEquals("He".getBytes(UTF_8), bytes); + + in.read(bytes); + assertArrayEquals("ll".getBytes(UTF_8), bytes); + + bytes = new byte[5]; + in.mark(5); + in.read(bytes); + assertArrayEquals("o Wor".getBytes(UTF_8), bytes); + } + } + + @Test + void readlimitExceeded() throws IOException { + try (InputStream in = new StringInputStream("Hello World")) { + + in.mark(4); + byte[] bytes = new byte[5]; + + in.read(bytes); + assertArrayEquals("Hello".getBytes(UTF_8), bytes); + + assertThrows(IOException.class, () -> in.reset()); + } + } + + @Test + void resetInitialStream() throws IOException { + try (InputStream in = new StringInputStream("Hello World")) { + assertThrows(IOException.class, in::reset); + } + } +} \ No newline at end of file