diff --git a/lis-commons-io/pom.xml b/lis-commons-io/pom.xml index bbc033bc..691b5a5e 100644 --- a/lis-commons-io/pom.xml +++ b/lis-commons-io/pom.xml @@ -1,5 +1,6 @@ - + lis-commons com.link-intersystems.commons @@ -12,4 +13,12 @@ LIS Commons IO Link Intersystems Commons IO (lis-commons-io) provides utilities for java.io. + + + org.assertj + assertj-core + 3.25.0 + test + + \ No newline at end of file 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 new file mode 100644 index 00000000..0f4871fb --- /dev/null +++ b/lis-commons-io/src/main/java/com/link_intersystems/io/FileBuilder.java @@ -0,0 +1,85 @@ +package com.link_intersystems.io; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.channels.Channels; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.file.Files; +import java.nio.file.Path; + +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 { + + public static final byte[] EMPTY_BYTE_ARRAY = new byte[0]; + 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 getDirpath() { + return dirpath; + } + + /** + * 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 createFile(name, Channels.newChannel(new ByteArrayInputStream(EMPTY_BYTE_ARRAY))); + } + + /** + * 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)); + } + + /** + * 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)) { + writableChannelConsumer.accept(writableChannel); + } + + return filepath; + } + + 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 new file mode 100644 index 00000000..a8c81c97 --- /dev/null +++ b/lis-commons-io/src/test/java/com/link_intersystems/io/FileBuilderTest.java @@ -0,0 +1,70 @@ +package com.link_intersystems.io; + +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.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; + +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 tempDirPath) { + this.tempDirPath = tempDirPath; + + fileBuilder = new FileBuilder(tempDirPath); + + assertEquals(tempDirPath, fileBuilder.getDirpath()); + + } + + @Test + void dirpathIsNotADirectory() throws IOException { + Path test = tempDirPath.resolve("test"); + Path filepath = Files.createFile(test); + + assertThrows(IllegalArgumentException.class, () -> new FileBuilder(filepath)); + } + + @Test + void createEmptyFile() throws IOException { + Path file = fileBuilder.createFile("test"); + + assertEquals(tempDirPath.resolve("test"), file); + assertThat(file).hasFileName("test"); + assertThat(file).content(UTF_8).isEmpty(); + } + + @Test + 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_8).isEqualTo("Hello World"); + } + + @Test + void createDirectory() throws IOException { + FileBuilder dir1 = fileBuilder.mkdir("dir1"); + + assertNotNull(dir1); + Path expectedDirpath = tempDirPath.resolve("dir1"); + + 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