From e74fd91bbf28e1629d3e9f6d27baa6a9b63c8670 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Tue, 20 May 2025 16:34:23 -0400 Subject: [PATCH 01/20] fix: AbstractInputStreamReadData close stream --- .../n5/readdata/AbstractInputStreamReadData.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/AbstractInputStreamReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/AbstractInputStreamReadData.java index b6b69a2b9..694f7aa95 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/AbstractInputStreamReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/AbstractInputStreamReadData.java @@ -30,6 +30,8 @@ import java.io.DataInputStream; import java.io.IOException; +import java.io.InputStream; + import org.apache.commons.io.IOUtils; // not thread-safe @@ -44,9 +46,13 @@ public ReadData materialize() throws IOException { final int length = (int) length(); if (length >= 0) { data = new byte[length]; - new DataInputStream(inputStream()).readFully(data); + try( InputStream is = inputStream()) { + new DataInputStream(is).readFully(data); + } } else { - data = IOUtils.toByteArray(inputStream()); + try( InputStream is = inputStream()) { + data = IOUtils.toByteArray(is); + } } bytes = new ByteArraySplittableReadData(data); } From 226e4933a9eeb6de1b64d34d0d8d125aa05d19b5 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Mon, 2 Jun 2025 17:17:38 -0400 Subject: [PATCH 02/20] feat: add split and limit methods to ReadData * add default methods * concrete implementation in ByteArraySplittableReadData * add test --- .../readdata/ByteArraySplittableReadData.java | 26 ++- .../saalfeldlab/n5/readdata/ReadData.java | 38 ++++ .../n5/readdata/ReadDataTests.java | 171 ++++++++++++++++++ 3 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java index 3f49860c2..8eae59cc2 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java @@ -6,13 +6,13 @@ * %% * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: - * + * * 1. Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. - * + * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE @@ -33,6 +33,9 @@ import java.io.InputStream; import java.util.Arrays; +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; + class ByteArraySplittableReadData implements ReadData { private final byte[] data; @@ -40,10 +43,12 @@ class ByteArraySplittableReadData implements ReadData { private final int length; ByteArraySplittableReadData(final byte[] data) { + this(data, 0, data.length); } ByteArraySplittableReadData(final byte[] data, final int offset, final int length) { + this.data = data; this.offset = offset; this.length = length; @@ -72,4 +77,21 @@ public byte[] allBytes() { public ReadData materialize() throws IOException { return this; } + + @Override + public ReadData slice(final long offset, final long length) throws IOException { + + if (offset < 0 || offset >= this.length || length < 0) { + throw new IndexOutOfBoundsException(); + } + final int o = this.offset + (int)offset; + final int l = Math.min((int)length, this.length - o); + return new ByteArraySplittableReadData(data, o, l); + } + + @Override + public Pair split(final long pivot) throws IOException { + + return ImmutablePair.of(slice(0, pivot), slice(offset + pivot, length - pivot)); + } } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java index b6fff2e3a..06539a534 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java @@ -32,6 +32,8 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; + +import org.apache.commons.lang3.tuple.Pair; import org.janelia.saalfeldlab.n5.KeyValueAccess; /** @@ -66,6 +68,42 @@ default long length() throws IOException { return -1; } + /** + * Returns a {@link ReadData} whose length is limited to the given value. + * + * @param length + * @return a length-limited ReadData + * @throws IOException + * if an I/O error occurs while trying to get the length + */ + default ReadData limit(final long length) throws IOException { + return slice(0, length); + } + + /** + * Returns a new {@link ReadData} representing a slice, or subset + * of this ReadData. + * + * @param offset the offset relative to this + * @param length of the returned ReadData + * @return a slice + * @throws IOException an exception + */ + default ReadData slice(final long offset, final long length) throws IOException { + return materialize().slice(offset, length); + } + + default Pair split(final long pivot) throws IOException { + /* + * TODO do we want this? how should it work? + * I suppose this could be useful for infinite data, or data of unknown length, + * So far, no uses of it though. + * + * tail below would be equivalent to slice(pivot, -1) + */ + return materialize().split(pivot); + } + /** * Open a {@code InputStream} on this data. *

diff --git a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java new file mode 100644 index 000000000..139fbe048 --- /dev/null +++ b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java @@ -0,0 +1,171 @@ +package org.janelia.saalfeldlab.n5.readdata; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Arrays; +import java.util.function.IntUnaryOperator; + +import org.apache.commons.compress.utils.IOUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.janelia.saalfeldlab.n5.readdata.ReadData.OutputStreamOperator; +import org.junit.Test; + +public class ReadDataTests { + + @Test + public void testLazyReadData() throws IOException { + + final int N = 128; + byte[] data = new byte[N]; + for( int i = 0; i < N; i++ ) + data[i] = (byte)i; + + final ReadData readData = ReadData.from(out -> { + out.write(data); + }); + assertTrue(readData instanceof LazyReadData); + + readDataTestHelper(readData, N); + splittableReadDataTestHelper(readData.materialize(), N, 5); + } + + @Test + public void testByteArrayReadData() throws IOException { + + final int N = 128; + byte[] data = new byte[N]; + for( int i = 0; i < N; i++ ) + data[i] = (byte)i; + + ReadData readData = ReadData.from(data).materialize(); + assertTrue(readData instanceof ByteArraySplittableReadData); + + readDataTestHelper(readData, N); + readDataTestEncodeHelper(readData, N); + splittableReadDataTestHelper(readData, N, 5); + } + + private void readDataTestHelper( ReadData readData, int N ) throws IOException { + + assertEquals("full length", N, readData.length()); + } + + private void readDataTestEncodeHelper( ReadData readData, int N ) throws IOException { + + final byte[] origCopy = new byte[N]; + IOUtils.readFully(readData.inputStream(), origCopy); + + final byte[] expected = Arrays.copyOf(origCopy, N); + for( int i = 0; i < expected.length; i++) + expected[i]+=2; + + final ReadData encoded = readData.encode(new ByteFun(x -> x+2)); + assertArrayEquals(expected, encoded.allBytes()); + + final ReadData encodedTwice = encoded.encode(new ByteFun(x -> x-2)); + assertArrayEquals(origCopy, encodedTwice.allBytes()); + } + + private void splittableReadDataTestHelper( ReadData readData, int N, int pivot ) throws IOException { + + assertEquals("length one", 1, readData.slice(9, 1).length()); + + assertEquals("split length zero", 0, readData.slice(9, 0).length()); + assertEquals("split length zero allBytes", 0, readData.slice(9, 0).allBytes().length); + + ReadData limited = readData.limit(2); + assertEquals(2, limited.length()); + + ReadData splitOutOfRange = readData.slice(N-1, 3); + assertEquals("Out-of-range split truncates", 1, splitOutOfRange.length()); + assertEquals("Out-of-range split truncates allBytes", 1, splitOutOfRange.allBytes().length); + + ReadData unboundedLength = readData.slice(1, Integer.MAX_VALUE); + assertEquals("unbounded length", N - 1, unboundedLength.length()); + assertEquals("unbounded length allBytes", N - 1, unboundedLength.allBytes().length); + + assertThrows("negative offset", IndexOutOfBoundsException.class, () -> readData.slice(-1, 1)); + assertThrows("negative length", IndexOutOfBoundsException.class, () -> readData.slice(0, -1)); + assertThrows("too large offset", IndexOutOfBoundsException.class, () -> readData.slice(N, 1)); + + final Pair split = readData.split(pivot); + final ReadData first = split.getLeft(); + final ReadData last = split.getRight(); + + assertEquals(pivot, first.length()); + assertEquals(0, first.allBytes()[0]); + + assertEquals(N-pivot, last.length()); + assertEquals(pivot, last.allBytes()[0]); + } + + @Test + public void testInputStreamReadData() throws IOException { + + final int N = 128; + byte[] data = new byte[N]; + for( int i = 0; i < N; i++ ) + data[i] = (byte)i; + + final InputStream is = new InputStream() { + int val = 0; + @Override + public int read() throws IOException { + return val++; + } + }; + + final ReadData readData = ReadData.from(is, N); + readDataTestHelper(readData, N); + splittableReadDataTestHelper(readData.materialize(), N, 5); + } + + /** + * Coming soon + */ +// @Test +// public void testFileKvaReadData() throws IOException { +// +// int N = 128; +// byte[] data = new byte[N]; +// for( int i = 0; i < N; i++ ) +// data[i] = (byte)i; +// +// final File tmpF = File.createTempFile("test-file-splittable-data", ".bin"); +// tmpF.deleteOnExit(); +// try (FileOutputStream os = new FileOutputStream(tmpF)) { +// os.write(data); +// } +// +// final ReadData readData = new FileSystemKeyValueAccess(FileSystems.getDefault()) +// .createReadData(tmpF.getAbsolutePath()); +// +// assertEquals("file read data length", 128, readData.length()); +// splittableReadDataTestHelper(readData.materialize(), N, 5); +// } + + private class ByteFun implements OutputStreamOperator { + + IntUnaryOperator fun; + public ByteFun(IntUnaryOperator fun) { + this.fun = fun; + } + + @Override + public OutputStream apply(OutputStream o) throws IOException { + return new OutputStream() { + @Override + public void write(int b) throws IOException { + o.write(fun.applyAsInt(b)); + } + }; + } + } + +} From 61dc4203b42ca8f4021aeddd717e11a8b58eb212 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Tue, 3 Jun 2025 13:47:34 -0400 Subject: [PATCH 03/20] feat: add KeyValueAccessLazyReadData * add two implementations * add KeyValueAccess size and createReadData methods * update ReadDataTests --- .../n5/FileSystemKeyValueAccess.java | 61 +++++++++ .../saalfeldlab/n5/HttpKeyValueAccess.java | 77 ++++++++++-- .../saalfeldlab/n5/KeyValueAccess.java | 32 +++++ .../n5/KeyValueAccessLazyReadData.java | 119 ++++++++++++++++++ .../readdata/ByteArraySplittableReadData.java | 19 ++- .../n5/readdata/ReadDataTests.java | 108 ++++++++-------- 6 files changed, 350 insertions(+), 66 deletions(-) create mode 100644 src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java diff --git a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java index 10737c1f4..86ba0668f 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java @@ -58,9 +58,11 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; +import java.io.UncheckedIOException; import java.io.Writer; import java.net.URI; import java.net.URISyntaxException; +import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.OverlappingFileLockException; @@ -81,6 +83,8 @@ import java.util.Iterator; import java.util.stream.Stream; +import org.janelia.saalfeldlab.n5.readdata.ReadData; + /** * Filesystem {@link KeyValueAccess}. * @@ -141,6 +145,10 @@ protected LockedFileChannel(final Path path, final boolean readOnly) throws IOEx } } + protected FileChannel getFileChannel() { + return channel; + } + @Override public Reader newReader() throws IOException { @@ -186,6 +194,11 @@ public FileSystemKeyValueAccess(final FileSystem fileSystem) { this.fileSystem = fileSystem; } + @Override + public ReadData createReadData(final String normalPath) { + return new FileLazyReadData(this, normalPath, 0, -1); + } + @Override public LockedFileChannel lockForReading(final String normalPath) throws IOException { @@ -237,6 +250,18 @@ public boolean exists(final String normalPath) { return Files.exists(path); } + @Override + public long size(final String normalPath) { + + try { + return Files.size(fileSystem.getPath(normalPath)); + } catch (NoSuchFileException e) { + throw new N5Exception.N5NoSuchKeyException("No such file", e); + } catch (IOException | UncheckedIOException e) { + throw new N5Exception.N5IOException(e); + } + } + @Override public String[] listDirectories(final String normalPath) throws IOException { @@ -546,4 +571,40 @@ protected static void createAndCheckIsDirectory( throw x; } } + + private class FileLazyReadData extends KeyValueAccessLazyReadData { + + public FileLazyReadData(FileSystemKeyValueAccess kva, String normalKey, long offset, long length) { + super(kva, normalKey, offset, length); + } + + @Override + void read() throws IOException { + + try (FileChannel channel = kva.lockForReading(normalKey).getFileChannel()) { + channel.position(offset); + if (length > Integer.MAX_VALUE) + throw new IOException("Attempt to materialize too large data"); + + final long channelSize = channel.size(); + if( length > 0 && offset + length > channelSize ) + throw new IndexOutOfBoundsException(); + + final int sz = (int)(length < 0 ? channelSize : length); + final byte[] data = new byte[sz]; + final ByteBuffer buf = ByteBuffer.wrap(data); + channel.read(buf); + materialized = ReadData.from(data); + + } catch (final NoSuchFileException e) { + throw new N5Exception.N5NoSuchKeyException(e); + } + } + + @Override + KeyValueAccessLazyReadData readOperationSlice(long offset, long length) throws IOException { + return new FileLazyReadData(kva, normalKey, offset, length); + } + } + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java index 7685376bf..6d889552a 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java @@ -49,8 +49,10 @@ package org.janelia.saalfeldlab.n5; import org.apache.commons.io.IOUtils; + import org.apache.commons.lang3.function.TriFunction; import org.janelia.saalfeldlab.n5.http.ListResponseParser; +import org.janelia.saalfeldlab.n5.readdata.ReadData; import java.io.Closeable; import java.io.IOException; @@ -76,6 +78,13 @@ */ public class HttpKeyValueAccess implements KeyValueAccess { + public static final String HEAD = "HEAD"; + public static final String GET = "GET"; + + public static final String RANGE = "Range"; + public static final String ACCEPT_RANGE = "Accept-Range"; + public static final String BYTES = "bytes"; + private int readTimeoutMilliseconds; private int connectionTimeoutMilliseconds; @@ -146,6 +155,12 @@ public boolean exists(final String normalPath) { } } + @Override public long size(String normalPath) { + + final HttpURLConnection head = requireValidHttpResponse(normalPath, "HEAD", "Error checking existence: " + normalPath, true); + return head.getContentLengthLong(); + } + /** * Test whether the path is a directory. *

@@ -161,7 +176,7 @@ public boolean exists(final String normalPath) { public boolean isDirectory(final String normalPath) { try { - requireValidHttpResponse(getDirectoryPath(normalPath), "HEAD", (code, msg,http) -> { + requireValidHttpResponse(getDirectoryPath(normalPath), HEAD, (code, msg,http) -> { final N5Exception cause = validExistsResponse(code, "Error checking directory: " + normalPath, msg, true); if (code >= 300 && code < 400) { final String redirectLocation = http.getHeaderField("Location"); @@ -203,7 +218,7 @@ public boolean isFile(final String normalPath) { /* Files must not end in `/` And Don't accept a redirect to a location ending in `/` */ try { - requireValidHttpResponse(getFilePath(normalPath), "HEAD", (code, msg, http) -> { + requireValidHttpResponse(getFilePath(normalPath), HEAD, (code, msg, http) -> { final N5Exception cause = validExistsResponse(code, "Error accessing file: " + normalPath, msg, true); if (code >= 300 && code < 400) { final String redirectLocation = http.getHeaderField("Location"); @@ -234,13 +249,18 @@ private HttpURLConnection httpRequest(String normalPath, String method) throws I return connection; } + @Override + public HttpLazyReadData createReadData(final String normalPath) { + return new HttpLazyReadData(this, normalPath, 0, -1); + } + @Override public LockedChannel lockForReading(final String normalPath) throws IOException { - //TODO Caleb: Maybe check exists lazily when attempting to read + try { if (!exists(normalPath)) throw new N5Exception.N5NoSuchKeyException("Key does not exist: " + normalPath); - return new HttpObjectChannel(uri(normalPath)); + return new HttpObjectChannel(uri(normalPath), 0, -1); } catch (URISyntaxException e) { throw new N5Exception("Invalid URI Syntax", e); } @@ -291,7 +311,7 @@ public String[] list(final String normalPath) throws IOException { private String[] queryListEntries(String normalPath, ListResponseParser parser, boolean allowRedirect) { - final HttpURLConnection http = requireValidHttpResponse(normalPath, "GET", "Error listing directory at " + normalPath, allowRedirect); + final HttpURLConnection http = requireValidHttpResponse(normalPath, GET, "Error listing directory at " + normalPath, allowRedirect); try { final String listResponse = responseToString(http.getInputStream()); return parser.parseListResponse(listResponse); @@ -352,17 +372,41 @@ public void delete(final String normalPath) { private class HttpObjectChannel implements LockedChannel { protected final URI uri; + private final long startByte; + private final long size; private final ArrayList resources = new ArrayList<>(); - protected HttpObjectChannel(final URI uri) { + protected HttpObjectChannel(final URI uri, long startByte, long size) { this.uri = uri; + this.startByte = startByte; + this.size = size; + } + + private boolean isPartialRead() { + return startByte > 0 || (size >= 0 && size != Long.MAX_VALUE); } @Override public InputStream newInputStream() throws IOException { - return uri.toURL().openStream(); + HttpURLConnection conn = (HttpURLConnection)uri.toURL().openConnection(); + if (isPartialRead()) { + conn.setRequestProperty(RANGE, rangeString()); + final String acceptRanges = conn.getHeaderField(ACCEPT_RANGE); + if (acceptRanges == null || !acceptRanges.equals(BYTES)) { + conn.disconnect(); + conn = (HttpURLConnection)uri.toURL().openConnection(); + return ReadData.from(conn.getInputStream()).materialize().slice(startByte, size).inputStream(); + } + } + return conn.getInputStream(); + } + + private String rangeString() { + + final String lastByte = (size > 0) ? Long.toString(startByte + size - 1) : ""; + return String.format("%s=%d-%s", BYTES, startByte, lastByte); } @Override @@ -399,4 +443,23 @@ public void close() throws IOException { } } + private class HttpLazyReadData extends KeyValueAccessLazyReadData { + + public HttpLazyReadData(HttpKeyValueAccess kva, String normalKey, long offset, long length) { + super(kva, normalKey, offset, length); + } + + @Override + void read() throws IOException { + try( final HttpObjectChannel ch = new HttpObjectChannel(kva.uri(normalKey), offset, length) ) { + materialized = ReadData.from(ch.newInputStream()).materialize(); + } catch (URISyntaxException e) {} + } + + @Override + KeyValueAccessLazyReadData readOperationSlice(long offset, long length) throws IOException { + return new HttpLazyReadData(kva, normalKey, offset, length); + } + } + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java index 73d279cb0..f710acc22 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java @@ -60,6 +60,8 @@ import java.util.Arrays; import java.util.stream.Collectors; +import org.janelia.saalfeldlab.n5.readdata.ReadData; + /** * Key value read primitives used by {@link N5KeyValueReader} * implementations. This interface implements a subset of access primitives @@ -230,6 +232,17 @@ default URI uri(final String uriString) throws URISyntaxException { */ public boolean exists(final String normalPath); + /** + * Returns the size in bytes of the object at the given normalPath if it exists. + * + * @param normalPath + * is expected to be in normalized form, no further + * efforts are made to normalize it. + * @return the size of the object in bytes. + * @throws N5NoSuchKeyException if the given key does not exist + */ + public long size(final String normalPath) throws N5Exception.N5NoSuchKeyException; + /** * Test whether the path is a directory. * @@ -250,6 +263,25 @@ default URI uri(final String uriString) throws URISyntaxException { */ public boolean isFile(String normalPath); // TODO: Looks un-used. Remove? + /** + * Create a {@link ReadData} through which data at the normal key can be read. + *

+ * Implementations should read lazily if possible. Consumers may call {@link ReadData#materialize()} to force + * a read operation if needed. + *

+ * Partial reads are possible using {@link ReadData#slice()} on the output if supported by this KeyValueAccess + * implementation. + * + * @param normalKey is expected to be in normalized form, no further efforts are made to normalize it + * @param startByte the starting byte + * @param length the number of bytes to read + * @return a materialized Read data + * @throws IOException if an error occurs + */ + default ReadData createReadData(final String normalPath) throws IOException { + return ReadData.from(this, normalPath); + } + /** * Create a lock on a path for reading. This isn't meant to be kept * around. Create, use, [auto]close, e.g. diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java new file mode 100644 index 000000000..68af80113 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java @@ -0,0 +1,119 @@ +package org.janelia.saalfeldlab.n5; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.lang3.tuple.ImmutablePair; +import org.apache.commons.lang3.tuple.Pair; +import org.janelia.saalfeldlab.n5.readdata.ReadData; + +/** + * This abstract class + * + * @param the type of {@link KeyValueAccess}. + */ +abstract class KeyValueAccessLazyReadData implements ReadData { + + protected ReadData materialized; + + protected final K kva; + protected final String normalKey; + protected final long offset; + protected long length; + + KeyValueAccessLazyReadData(K kva, String normalKey, long offset, long length) { + + if (offset < 0) + throw new IndexOutOfBoundsException("Can not create KeyValueAccesReadData with negative offset: " + offset); + + this.kva = kva; + this.normalKey = normalKey; + this.offset = offset; + this.length = length; + + // when created with a specified length, + // need to make sure it is consistent with the actual length +// final long objLength = kva.size(normalKey); +// if (length > 0 && offset + length - 1 > objLength) +// throw new IndexOutOfBoundsException("Object at key: " + normalKey + " has size " + objLength + +// ". Is too small for requested offset (" + +// offset +") and length (" + length + "). "); + + } + + KeyValueAccessLazyReadData(K kva, String normalKey, long offset) { + + this(kva, normalKey, offset, -1); + } + + @Override + public long length() throws IOException { + + if (materialized != null) + return materialized.length(); + + if (length < 0) { + length = kva.size(normalKey); + } + return length; + } + + @Override + public InputStream inputStream() throws IOException, IllegalStateException { + + return materialize().inputStream(); + } + + @Override + public byte[] allBytes() throws IOException, IllegalStateException { + + return materialize().allBytes(); + } + + @Override + public ReadData materialize() throws IOException { + + if (materialized == null) + read(); + + return materialized; + } + + abstract void read() throws IOException; + + abstract KeyValueAccessLazyReadData readOperationSlice(long offset, long length) throws IOException; + + @Override + public ReadData slice(final long offset, final long length) throws IOException { + + if (materialized != null) + return materialize().slice(offset, length); + + // if a slice of indeterminate length is requested, but the + // length is already known, use the known length; + final int lengthArg; + if (this.length > 0 && length < 0) + lengthArg = (int)(this.length - offset); + else + lengthArg = (int)length; + + return readOperationSlice(this.offset + offset, lengthArg); + } + + @Override + public Pair split(final long pivot) throws IOException { + + if (materialized != null) + return materialize().split(pivot); + + final long offsetL = 0; + final long lenL = pivot; + + final long offsetR = offset + pivot; + final long lenR = this.length - pivot; + + return new ImmutablePair( + readOperationSlice(offsetL, lenL), + readOperationSlice(offsetR, lenR)); + } +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java index 8eae59cc2..1f59babc0 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java @@ -51,7 +51,11 @@ class ByteArraySplittableReadData implements ReadData { this.data = data; this.offset = offset; - this.length = length; + + if( length < 0 ) + this.length = data.length - offset; + else + this.length = length; } @Override @@ -66,6 +70,12 @@ public InputStream inputStream() throws IOException { @Override public byte[] allBytes() { + + // Arrays.copyOfRange pads with zeros, so explicitly check for out-of-bounds + // allow offset = 0 and length = 0 to return empty array + if ( (offset > 0 && offset >= data.length) || offset + length > data.length) + throw new IndexOutOfBoundsException(); + if (offset == 0 && data.length == length) { return data; } else { @@ -81,12 +91,11 @@ public ReadData materialize() throws IOException { @Override public ReadData slice(final long offset, final long length) throws IOException { - if (offset < 0 || offset >= this.length || length < 0) { + if (offset < 0 || offset >= this.length ) throw new IndexOutOfBoundsException(); - } + final int o = this.offset + (int)offset; - final int l = Math.min((int)length, this.length - o); - return new ByteArraySplittableReadData(data, o, l); + return new ByteArraySplittableReadData(data, o, (int)length); } @Override diff --git a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java index 139fbe048..c64811b9b 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java @@ -5,14 +5,18 @@ import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.nio.file.FileSystems; import java.util.Arrays; import java.util.function.IntUnaryOperator; import org.apache.commons.compress.utils.IOUtils; import org.apache.commons.lang3.tuple.Pair; +import org.janelia.saalfeldlab.n5.FileSystemKeyValueAccess; import org.janelia.saalfeldlab.n5.readdata.ReadData.OutputStreamOperator; import org.junit.Test; @@ -51,6 +55,48 @@ public void testByteArrayReadData() throws IOException { splittableReadDataTestHelper(readData, N, 5); } + @Test + public void testInputStreamReadData() throws IOException { + + final int N = 128; + byte[] data = new byte[N]; + for( int i = 0; i < N; i++ ) + data[i] = (byte)i; + + final InputStream is = new InputStream() { + int val = 0; + @Override + public int read() throws IOException { + return val++; + } + }; + + final ReadData readData = ReadData.from(is, N); + readDataTestHelper(readData, N); + splittableReadDataTestHelper(readData.materialize(), N, 5); + } + + @Test + public void testFileKvaReadData() throws IOException { + + int N = 128; + byte[] data = new byte[N]; + for( int i = 0; i < N; i++ ) + data[i] = (byte)i; + + final File tmpF = File.createTempFile("test-file-splittable-data", ".bin"); + tmpF.deleteOnExit(); + try (FileOutputStream os = new FileOutputStream(tmpF)) { + os.write(data); + } + + final ReadData readData = new FileSystemKeyValueAccess(FileSystems.getDefault()) + .createReadData(tmpF.getAbsolutePath()); + + assertEquals("file read data length", 128, readData.length()); + splittableReadDataTestHelper(readData, N, 5); + } + private void readDataTestHelper( ReadData readData, int N ) throws IOException { assertEquals("full length", N, readData.length()); @@ -82,17 +128,16 @@ private void splittableReadDataTestHelper( ReadData readData, int N, int pivot ) ReadData limited = readData.limit(2); assertEquals(2, limited.length()); - ReadData splitOutOfRange = readData.slice(N-1, 3); - assertEquals("Out-of-range split truncates", 1, splitOutOfRange.length()); - assertEquals("Out-of-range split truncates allBytes", 1, splitOutOfRange.allBytes().length); - - ReadData unboundedLength = readData.slice(1, Integer.MAX_VALUE); - assertEquals("unbounded length", N - 1, unboundedLength.length()); + ReadData unboundedLength = readData.slice(1, -1); assertEquals("unbounded length allBytes", N - 1, unboundedLength.allBytes().length); - + + ReadData outOfRangeSlice = readData.slice(N-1, 3); // never throws + assertThrows("Out-of-range slice read", IndexOutOfBoundsException.class, () -> outOfRangeSlice.allBytes()); + + ReadData tooLargeSlice = readData.slice(N-1, 3); // never throws + assertThrows("too large offset slice read", IndexOutOfBoundsException.class, () -> tooLargeSlice.allBytes()); + assertThrows("negative offset", IndexOutOfBoundsException.class, () -> readData.slice(-1, 1)); - assertThrows("negative length", IndexOutOfBoundsException.class, () -> readData.slice(0, -1)); - assertThrows("too large offset", IndexOutOfBoundsException.class, () -> readData.slice(N, 1)); final Pair split = readData.split(pivot); final ReadData first = split.getLeft(); @@ -105,51 +150,6 @@ private void splittableReadDataTestHelper( ReadData readData, int N, int pivot ) assertEquals(pivot, last.allBytes()[0]); } - @Test - public void testInputStreamReadData() throws IOException { - - final int N = 128; - byte[] data = new byte[N]; - for( int i = 0; i < N; i++ ) - data[i] = (byte)i; - - final InputStream is = new InputStream() { - int val = 0; - @Override - public int read() throws IOException { - return val++; - } - }; - - final ReadData readData = ReadData.from(is, N); - readDataTestHelper(readData, N); - splittableReadDataTestHelper(readData.materialize(), N, 5); - } - - /** - * Coming soon - */ -// @Test -// public void testFileKvaReadData() throws IOException { -// -// int N = 128; -// byte[] data = new byte[N]; -// for( int i = 0; i < N; i++ ) -// data[i] = (byte)i; -// -// final File tmpF = File.createTempFile("test-file-splittable-data", ".bin"); -// tmpF.deleteOnExit(); -// try (FileOutputStream os = new FileOutputStream(tmpF)) { -// os.write(data); -// } -// -// final ReadData readData = new FileSystemKeyValueAccess(FileSystems.getDefault()) -// .createReadData(tmpF.getAbsolutePath()); -// -// assertEquals("file read data length", 128, readData.length()); -// splittableReadDataTestHelper(readData.materialize(), N, 5); -// } - private class ByteFun implements OutputStreamOperator { IntUnaryOperator fun; From 83b1ea8537dcc25d2d62b538ed3ad7ab37a13690 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Tue, 3 Jun 2025 13:48:43 -0400 Subject: [PATCH 04/20] refactor: ByteArrayReadData (rm "Splittable") * because every ReadData is splittable --- .../n5/readdata/AbstractInputStreamReadData.java | 4 ++-- ...rraySplittableReadData.java => ByteArrayReadData.java} | 8 ++++---- .../org/janelia/saalfeldlab/n5/readdata/LazyReadData.java | 4 ++-- .../org/janelia/saalfeldlab/n5/readdata/ReadData.java | 2 +- .../janelia/saalfeldlab/n5/readdata/ReadDataTests.java | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) rename src/main/java/org/janelia/saalfeldlab/n5/readdata/{ByteArraySplittableReadData.java => ByteArrayReadData.java} (92%) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/AbstractInputStreamReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/AbstractInputStreamReadData.java index 694f7aa95..d2a6e0154 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/AbstractInputStreamReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/AbstractInputStreamReadData.java @@ -37,7 +37,7 @@ // not thread-safe abstract class AbstractInputStreamReadData implements ReadData { - private ByteArraySplittableReadData bytes; + private ByteArrayReadData bytes; @Override public ReadData materialize() throws IOException { @@ -54,7 +54,7 @@ public ReadData materialize() throws IOException { data = IOUtils.toByteArray(is); } } - bytes = new ByteArraySplittableReadData(data); + bytes = new ByteArrayReadData(data); } return bytes; } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java similarity index 92% rename from src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java rename to src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java index 1f59babc0..763de1b00 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArraySplittableReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java @@ -36,18 +36,18 @@ import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.Pair; -class ByteArraySplittableReadData implements ReadData { +class ByteArrayReadData implements ReadData { private final byte[] data; private final int offset; private final int length; - ByteArraySplittableReadData(final byte[] data) { + ByteArrayReadData(final byte[] data) { this(data, 0, data.length); } - ByteArraySplittableReadData(final byte[] data, final int offset, final int length) { + ByteArrayReadData(final byte[] data, final int offset, final int length) { this.data = data; this.offset = offset; @@ -95,7 +95,7 @@ public ReadData slice(final long offset, final long length) throws IOException { throw new IndexOutOfBoundsException(); final int o = this.offset + (int)offset; - return new ByteArraySplittableReadData(data, o, (int)length); + return new ByteArrayReadData(data, o, (int)length); } @Override diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyReadData.java index b9c9be981..2d59f19af 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyReadData.java @@ -59,14 +59,14 @@ class LazyReadData implements ReadData { private final OutputStreamWriter writer; - private ByteArraySplittableReadData bytes; + private ByteArrayReadData bytes; @Override public ReadData materialize() throws IOException { if (bytes == null) { final ByteArrayOutputStream baos = new ByteArrayOutputStream(8192); writeTo(baos); - bytes = new ByteArraySplittableReadData(baos.toByteArray()); + bytes = new ByteArrayReadData(baos.toByteArray()); } return bytes; } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java index 06539a534..a7f4392e2 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java @@ -285,7 +285,7 @@ static ReadData from(final KeyValueAccess keyValueAccess, final String normalPat * @return a new ReadData */ static ReadData from(final byte[] data, final int offset, final int length) { - return new ByteArraySplittableReadData(data, offset, length); + return new ByteArrayReadData(data, offset, length); } /** diff --git a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java index c64811b9b..cedc2093d 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java @@ -48,7 +48,7 @@ public void testByteArrayReadData() throws IOException { data[i] = (byte)i; ReadData readData = ReadData.from(data).materialize(); - assertTrue(readData instanceof ByteArraySplittableReadData); + assertTrue(readData instanceof ByteArrayReadData); readDataTestHelper(readData, N); readDataTestEncodeHelper(readData, N); From e276756d04b0ed9595a0f76705e3948a82d35d32 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Tue, 3 Jun 2025 13:57:16 -0400 Subject: [PATCH 05/20] doc: javadoc fixes --- .../java/org/janelia/saalfeldlab/n5/KeyValueAccess.java | 9 +++------ .../org/janelia/saalfeldlab/n5/readdata/ReadData.java | 5 +++++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java index f710acc22..8b644dd3e 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java @@ -239,7 +239,7 @@ default URI uri(final String uriString) throws URISyntaxException { * is expected to be in normalized form, no further * efforts are made to normalize it. * @return the size of the object in bytes. - * @throws N5NoSuchKeyException if the given key does not exist + * @throws N5Exception.N5NoSuchKeyException if the given key does not exist */ public long size(final String normalPath) throws N5Exception.N5NoSuchKeyException; @@ -269,12 +269,9 @@ default URI uri(final String uriString) throws URISyntaxException { * Implementations should read lazily if possible. Consumers may call {@link ReadData#materialize()} to force * a read operation if needed. *

- * Partial reads are possible using {@link ReadData#slice()} on the output if supported by this KeyValueAccess - * implementation. + * If supported by this KeyValueAccess implementation, partial reads are possible by calling slice on the output {@link ReadData}. * - * @param normalKey is expected to be in normalized form, no further efforts are made to normalize it - * @param startByte the starting byte - * @param length the number of bytes to read + * @param normalPath is expected to be in normalized form, no further efforts are made to normalize it * @return a materialized Read data * @throws IOException if an error occurs */ diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java index a7f4392e2..aa5240404 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java @@ -170,6 +170,11 @@ default ByteBuffer toByteBuffer() throws IOException, IllegalStateException { *

* The returned {@code ReadData} has a known {@link #length} and multiple * {@link #inputStream InputStreams} can be opened on it. + * + * @return + * a materialized ReadData. + * @throws IOException + * if any I/O error occurs */ ReadData materialize() throws IOException; From 7df5b071d6cf944ac73a27cdbf561cbee4f83584 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Tue, 3 Jun 2025 14:14:57 -0400 Subject: [PATCH 06/20] refactor/doc: rename KeyValueAccessLazyReadData.lazySlice * document KeyValueAccessLazyReadData --- .../n5/FileSystemKeyValueAccess.java | 2 +- .../saalfeldlab/n5/HttpKeyValueAccess.java | 2 +- .../n5/KeyValueAccessLazyReadData.java | 40 ++++++++++++------- 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java index 86ba0668f..7a7e017cc 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java @@ -602,7 +602,7 @@ void read() throws IOException { } @Override - KeyValueAccessLazyReadData readOperationSlice(long offset, long length) throws IOException { + KeyValueAccessLazyReadData lazySlice(long offset, long length) { return new FileLazyReadData(kva, normalKey, offset, length); } } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java index 6d889552a..fd873a37f 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java @@ -457,7 +457,7 @@ void read() throws IOException { } @Override - KeyValueAccessLazyReadData readOperationSlice(long offset, long length) throws IOException { + KeyValueAccessLazyReadData lazySlice(long offset, long length) { return new HttpLazyReadData(kva, normalKey, offset, length); } } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java index 68af80113..98f46e311 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java @@ -8,9 +8,11 @@ import org.janelia.saalfeldlab.n5.readdata.ReadData; /** - * This abstract class + * This abstract class represents a lazy read operation, and implements the + * shared logic for lazily reading from a {@link KeyValueAccess}. * - * @param the type of {@link KeyValueAccess}. + * @param + * the type of {@link KeyValueAccess}. */ abstract class KeyValueAccessLazyReadData implements ReadData { @@ -30,15 +32,6 @@ abstract class KeyValueAccessLazyReadData implements R this.normalKey = normalKey; this.offset = offset; this.length = length; - - // when created with a specified length, - // need to make sure it is consistent with the actual length -// final long objLength = kva.size(normalKey); -// if (length > 0 && offset + length - 1 > objLength) -// throw new IndexOutOfBoundsException("Object at key: " + normalKey + " has size " + objLength + -// ". Is too small for requested offset (" + -// offset +") and length (" + length + "). "); - } KeyValueAccessLazyReadData(K kva, String normalKey, long offset) { @@ -79,9 +72,26 @@ public ReadData materialize() throws IOException { return materialized; } + /** + * Read from the backed {@link KeyValueAccess} and set the materialized {@link ReadData} field. + * + * @throws IOException + * if an I/O error occurs + */ abstract void read() throws IOException; - abstract KeyValueAccessLazyReadData readOperationSlice(long offset, long length) throws IOException; + /** + * Return a new instance of a KeyValueAccessLazyReadData for this {@KeyValueAceess} and key, + * but that represents a read operation that slices this instance with the given arguments. + *

+ * This method should not perform any reads or calls to the backing KeyValueAccess. + * + * @param offset the offset relative to this + * @param length of the returned ReadData + * @return + * a new KeyValueAccessLazyReadData + */ + abstract KeyValueAccessLazyReadData lazySlice(long offset, long length); @Override public ReadData slice(final long offset, final long length) throws IOException { @@ -97,7 +107,7 @@ public ReadData slice(final long offset, final long length) throws IOException { else lengthArg = (int)length; - return readOperationSlice(this.offset + offset, lengthArg); + return lazySlice(this.offset + offset, lengthArg); } @Override @@ -113,7 +123,7 @@ public Pair split(final long pivot) throws IOException { final long lenR = this.length - pivot; return new ImmutablePair( - readOperationSlice(offsetL, lenL), - readOperationSlice(offsetR, lenR)); + lazySlice(offsetL, lenL), + lazySlice(offsetR, lenR)); } } From 8170919c2fb126ccb70fcb2cb63d78075a8b44bc Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Thu, 12 Jun 2025 16:33:19 -0400 Subject: [PATCH 07/20] perf/test: slice throws exception immediately when possible * TODO behavior of HttpKvaReadData currently undefined --- .../n5/FileSystemKeyValueAccess.java | 16 ++++++++++-- .../saalfeldlab/n5/HttpKeyValueAccess.java | 1 + .../n5/readdata/ByteArrayReadData.java | 25 +++++++++++++------ .../n5/readdata/ReadDataTests.java | 10 ++++---- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java index 7a7e017cc..d95e9c994 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java @@ -572,7 +572,7 @@ protected static void createAndCheckIsDirectory( } } - private class FileLazyReadData extends KeyValueAccessLazyReadData { + private static class FileLazyReadData extends KeyValueAccessLazyReadData { public FileLazyReadData(FileSystemKeyValueAccess kva, String normalKey, long offset, long length) { super(kva, normalKey, offset, length); @@ -587,7 +587,7 @@ void read() throws IOException { throw new IOException("Attempt to materialize too large data"); final long channelSize = channel.size(); - if( length > 0 && offset + length > channelSize ) + if( !validBounds(channelSize, offset, length)) throw new IndexOutOfBoundsException(); final int sz = (int)(length < 0 ? channelSize : length); @@ -607,4 +607,16 @@ KeyValueAccessLazyReadData lazySlice(long offset, long } } + private static boolean validBounds(long channelSize, long offset, long length) { + + if (offset < 0) + return false; + else if (channelSize > 0 && offset >= channelSize) // offset == 0 and arrayLength == 0 is okay + return false; + else if (length >= 0 && offset + length > channelSize) + return false; + + return true; + } + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java index fd873a37f..76c828dcf 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java @@ -451,6 +451,7 @@ public HttpLazyReadData(HttpKeyValueAccess kva, String normalKey, long offset, l @Override void read() throws IOException { + // TODO does this throw out-of-bounds when it should try( final HttpObjectChannel ch = new HttpObjectChannel(kva.uri(normalKey), offset, length) ) { materialized = ReadData.from(ch.newInputStream()).materialize(); } catch (URISyntaxException e) {} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java index 763de1b00..eb7a24d11 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java @@ -49,6 +49,9 @@ class ByteArrayReadData implements ReadData { ByteArrayReadData(final byte[] data, final int offset, final int length) { + if (!validBounds(data.length, offset, length)) + throw new IndexOutOfBoundsException(); + this.data = data; this.offset = offset; @@ -56,6 +59,7 @@ class ByteArrayReadData implements ReadData { this.length = data.length - offset; else this.length = length; + } @Override @@ -71,11 +75,7 @@ public InputStream inputStream() throws IOException { @Override public byte[] allBytes() { - // Arrays.copyOfRange pads with zeros, so explicitly check for out-of-bounds - // allow offset = 0 and length = 0 to return empty array - if ( (offset > 0 && offset >= data.length) || offset + length > data.length) - throw new IndexOutOfBoundsException(); - + // alternatively, we could always return the requested length if (offset == 0 && data.length == length) { return data; } else { @@ -91,9 +91,6 @@ public ReadData materialize() throws IOException { @Override public ReadData slice(final long offset, final long length) throws IOException { - if (offset < 0 || offset >= this.length ) - throw new IndexOutOfBoundsException(); - final int o = this.offset + (int)offset; return new ByteArrayReadData(data, o, (int)length); } @@ -103,4 +100,16 @@ public Pair split(final long pivot) throws IOException { return ImmutablePair.of(slice(0, pivot), slice(offset + pivot, length - pivot)); } + + private static boolean validBounds(int arrayLength, int offset, int length) { + + if (offset < 0) + return false; + else if (arrayLength > 0 && offset >= arrayLength) // offset == 0 and arrayLength == 0 is okay + return false; + else if (length >= 0 && offset + length > arrayLength) + return false; + + return true; + } } diff --git a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java index cedc2093d..727c6a58e 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java @@ -131,11 +131,11 @@ private void splittableReadDataTestHelper( ReadData readData, int N, int pivot ) ReadData unboundedLength = readData.slice(1, -1); assertEquals("unbounded length allBytes", N - 1, unboundedLength.allBytes().length); - ReadData outOfRangeSlice = readData.slice(N-1, 3); // never throws - assertThrows("Out-of-range slice read", IndexOutOfBoundsException.class, () -> outOfRangeSlice.allBytes()); - - ReadData tooLargeSlice = readData.slice(N-1, 3); // never throws - assertThrows("too large offset slice read", IndexOutOfBoundsException.class, () -> tooLargeSlice.allBytes()); + // slice may throw an exception if it knows its length and can detect out-of-bounds + // otherwise the exception may be thrown on a read operation (e.g. allBytes) + assertThrows("Out-of-range slice read", IndexOutOfBoundsException.class, () -> readData.slice(N-1, 3).allBytes()); + assertThrows("slice throws if offset too large", IndexOutOfBoundsException.class, () -> readData.slice(N, 0).allBytes()); + assertThrows("too large offset slice read", IndexOutOfBoundsException.class, () -> readData.slice(N-1, 3).allBytes()); assertThrows("negative offset", IndexOutOfBoundsException.class, () -> readData.slice(-1, 1)); From 05280a89b9598aaaa1c760dacbf7db2df49cdb6f Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Thu, 12 Jun 2025 16:44:41 -0400 Subject: [PATCH 08/20] feat: add ReadData.empty() --- .../saalfeldlab/n5/readdata/ByteArrayReadData.java | 3 +++ .../org/janelia/saalfeldlab/n5/readdata/ReadData.java | 10 ++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java index eb7a24d11..16e8cf986 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java @@ -38,6 +38,8 @@ class ByteArrayReadData implements ReadData { + static final ReadData EMPTY = new ByteArrayReadData(new byte[0]); + private final byte[] data; private final int offset; private final int length; @@ -112,4 +114,5 @@ else if (length >= 0 && offset + length > arrayLength) return true; } + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java index aa5240404..b2e40cb79 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java @@ -337,4 +337,14 @@ interface OutputStreamWriter { static ReadData from(OutputStreamWriter generator) { return new LazyReadData(generator); } + + /** + * Returns an empty {@code ReadData}. + * + * @return an empty ReadData + */ + public static ReadData empty() { + return ByteArrayReadData.EMPTY; + } + } From 52d86d905c4d18e61497e230d3367c62317ee752 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Thu, 12 Jun 2025 16:55:24 -0400 Subject: [PATCH 09/20] remove ReadData.split --- pom.xml | 4 ---- .../n5/KeyValueAccessLazyReadData.java | 16 ---------------- .../n5/readdata/ByteArrayReadData.java | 6 ------ .../saalfeldlab/n5/readdata/ReadData.java | 11 ----------- .../saalfeldlab/n5/readdata/ReadDataTests.java | 11 ----------- 5 files changed, 48 deletions(-) diff --git a/pom.xml b/pom.xml index 98f268f4c..b4094436b 100644 --- a/pom.xml +++ b/pom.xml @@ -202,10 +202,6 @@ org.apache.commons commons-compress - - commons-io - commons-io - diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java index 98f46e311..8d199c41c 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java @@ -110,20 +110,4 @@ public ReadData slice(final long offset, final long length) throws IOException { return lazySlice(this.offset + offset, lengthArg); } - @Override - public Pair split(final long pivot) throws IOException { - - if (materialized != null) - return materialize().split(pivot); - - final long offsetL = 0; - final long lenL = pivot; - - final long offsetR = offset + pivot; - final long lenR = this.length - pivot; - - return new ImmutablePair( - lazySlice(offsetL, lenL), - lazySlice(offsetR, lenR)); - } } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java index 16e8cf986..a92257fbd 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java @@ -97,12 +97,6 @@ public ReadData slice(final long offset, final long length) throws IOException { return new ByteArrayReadData(data, o, (int)length); } - @Override - public Pair split(final long pivot) throws IOException { - - return ImmutablePair.of(slice(0, pivot), slice(offset + pivot, length - pivot)); - } - private static boolean validBounds(int arrayLength, int offset, int length) { if (offset < 0) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java index b2e40cb79..6f9f0503e 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java @@ -93,17 +93,6 @@ default ReadData slice(final long offset, final long length) throws IOException return materialize().slice(offset, length); } - default Pair split(final long pivot) throws IOException { - /* - * TODO do we want this? how should it work? - * I suppose this could be useful for infinite data, or data of unknown length, - * So far, no uses of it though. - * - * tail below would be equivalent to slice(pivot, -1) - */ - return materialize().split(pivot); - } - /** * Open a {@code InputStream} on this data. *

diff --git a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java index 727c6a58e..00e0c134d 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java @@ -15,7 +15,6 @@ import java.util.function.IntUnaryOperator; import org.apache.commons.compress.utils.IOUtils; -import org.apache.commons.lang3.tuple.Pair; import org.janelia.saalfeldlab.n5.FileSystemKeyValueAccess; import org.janelia.saalfeldlab.n5.readdata.ReadData.OutputStreamOperator; import org.junit.Test; @@ -138,16 +137,6 @@ private void splittableReadDataTestHelper( ReadData readData, int N, int pivot ) assertThrows("too large offset slice read", IndexOutOfBoundsException.class, () -> readData.slice(N-1, 3).allBytes()); assertThrows("negative offset", IndexOutOfBoundsException.class, () -> readData.slice(-1, 1)); - - final Pair split = readData.split(pivot); - final ReadData first = split.getLeft(); - final ReadData last = split.getRight(); - - assertEquals(pivot, first.length()); - assertEquals(0, first.allBytes()[0]); - - assertEquals(N-pivot, last.length()); - assertEquals(pivot, last.allBytes()[0]); } private class ByteFun implements OutputStreamOperator { From d5d309c96a5debd96a2b3a83bd958389476e0cdb Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Fri, 13 Jun 2025 09:31:47 -0400 Subject: [PATCH 10/20] style: rm unused imports --- .../org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java | 2 -- .../org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java | 3 --- 2 files changed, 5 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java index 8d199c41c..6a2bc564b 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java @@ -3,8 +3,6 @@ import java.io.IOException; import java.io.InputStream; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java index a92257fbd..09e99ab67 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java @@ -33,9 +33,6 @@ import java.io.InputStream; import java.util.Arrays; -import org.apache.commons.lang3.tuple.ImmutablePair; -import org.apache.commons.lang3.tuple.Pair; - class ByteArrayReadData implements ReadData { static final ReadData EMPTY = new ByteArrayReadData(new byte[0]); From 725c0b2cd500a1b818079bd3bcf0e71d5949c675 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Fri, 13 Jun 2025 11:43:08 -0400 Subject: [PATCH 11/20] doc: javadoc fixes --- .../java/org/janelia/saalfeldlab/n5/readdata/ReadData.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java index 90f2bd2e5..ae39c9446 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java @@ -73,9 +73,10 @@ default long length() throws N5IOException { * Returns a {@link ReadData} whose length is limited to the given value. * * @param length + * the length of the resulting ReadData * @return a length-limited ReadData * @throws N5IOException - * if an I/O error occurs while trying to get the length + * if an I/O error occurs while trying to get the length */ default ReadData limit(final long length) throws N5IOException { return slice(0, length); @@ -163,7 +164,7 @@ default ByteBuffer toByteBuffer() throws N5IOException, IllegalStateException { * * @return * a materialized ReadData. - * @throws IOException + * @throws N5IOException * if any I/O error occurs */ ReadData materialize() throws N5IOException; From 20c6de2ef38cb951ac14c7190d9dc226601855c3 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Mon, 16 Jun 2025 16:07:13 -0400 Subject: [PATCH 12/20] style: rm unused import --- src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java index ae39c9446..582f7bf6c 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java @@ -33,7 +33,6 @@ import java.io.OutputStream; import java.nio.ByteBuffer; -import org.apache.commons.lang3.tuple.Pair; import org.janelia.saalfeldlab.n5.KeyValueAccess; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; From 9e8aa9188778bfca3626618944f9d7a108f6a8bc Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Mon, 16 Jun 2025 16:08:13 -0400 Subject: [PATCH 13/20] test/style: rm unused pivot test argument --- .../janelia/saalfeldlab/n5/readdata/ReadDataTests.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java index 00e0c134d..164ee2ee4 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java @@ -35,7 +35,7 @@ public void testLazyReadData() throws IOException { assertTrue(readData instanceof LazyReadData); readDataTestHelper(readData, N); - splittableReadDataTestHelper(readData.materialize(), N, 5); + splittableReadDataTestHelper(readData.materialize(), N); } @Test @@ -51,7 +51,7 @@ public void testByteArrayReadData() throws IOException { readDataTestHelper(readData, N); readDataTestEncodeHelper(readData, N); - splittableReadDataTestHelper(readData, N, 5); + splittableReadDataTestHelper(readData, N); } @Test @@ -72,7 +72,7 @@ public int read() throws IOException { final ReadData readData = ReadData.from(is, N); readDataTestHelper(readData, N); - splittableReadDataTestHelper(readData.materialize(), N, 5); + splittableReadDataTestHelper(readData.materialize(), N); } @Test @@ -93,7 +93,7 @@ public void testFileKvaReadData() throws IOException { .createReadData(tmpF.getAbsolutePath()); assertEquals("file read data length", 128, readData.length()); - splittableReadDataTestHelper(readData, N, 5); + splittableReadDataTestHelper(readData, N); } private void readDataTestHelper( ReadData readData, int N ) throws IOException { @@ -117,7 +117,7 @@ private void readDataTestEncodeHelper( ReadData readData, int N ) throws IOExcep assertArrayEquals(origCopy, encodedTwice.allBytes()); } - private void splittableReadDataTestHelper( ReadData readData, int N, int pivot ) throws IOException { + private void splittableReadDataTestHelper( ReadData readData, int N ) throws IOException { assertEquals("length one", 1, readData.slice(9, 1).length()); From c4c93b5aab75a7bd39687323ec1ed682b903464e Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Mon, 16 Jun 2025 16:08:31 -0400 Subject: [PATCH 14/20] test/style: make ByteFun test class static --- .../java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java index 164ee2ee4..8990b0f0e 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java @@ -139,7 +139,7 @@ private void splittableReadDataTestHelper( ReadData readData, int N ) throws IOE assertThrows("negative offset", IndexOutOfBoundsException.class, () -> readData.slice(-1, 1)); } - private class ByteFun implements OutputStreamOperator { + private static class ByteFun implements OutputStreamOperator { IntUnaryOperator fun; public ByteFun(IntUnaryOperator fun) { From bd5d072087e2b91948caacb14716048bca63439d Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Mon, 16 Jun 2025 16:19:52 -0400 Subject: [PATCH 15/20] test: slice testing works on unmaterialized ReadData * rename helper method * rm unused byte[] --- .../saalfeldlab/n5/readdata/ReadDataTests.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java index 8990b0f0e..b2e7f3c27 100644 --- a/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java +++ b/src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java @@ -35,7 +35,7 @@ public void testLazyReadData() throws IOException { assertTrue(readData instanceof LazyReadData); readDataTestHelper(readData, N); - splittableReadDataTestHelper(readData.materialize(), N); + sliceTestHelper(readData, N); } @Test @@ -51,17 +51,13 @@ public void testByteArrayReadData() throws IOException { readDataTestHelper(readData, N); readDataTestEncodeHelper(readData, N); - splittableReadDataTestHelper(readData, N); + sliceTestHelper(readData, N); } @Test public void testInputStreamReadData() throws IOException { final int N = 128; - byte[] data = new byte[N]; - for( int i = 0; i < N; i++ ) - data[i] = (byte)i; - final InputStream is = new InputStream() { int val = 0; @Override @@ -72,7 +68,7 @@ public int read() throws IOException { final ReadData readData = ReadData.from(is, N); readDataTestHelper(readData, N); - splittableReadDataTestHelper(readData.materialize(), N); + sliceTestHelper(readData, N); } @Test @@ -93,7 +89,7 @@ public void testFileKvaReadData() throws IOException { .createReadData(tmpF.getAbsolutePath()); assertEquals("file read data length", 128, readData.length()); - splittableReadDataTestHelper(readData, N); + sliceTestHelper(readData, N); } private void readDataTestHelper( ReadData readData, int N ) throws IOException { @@ -117,7 +113,7 @@ private void readDataTestEncodeHelper( ReadData readData, int N ) throws IOExcep assertArrayEquals(origCopy, encodedTwice.allBytes()); } - private void splittableReadDataTestHelper( ReadData readData, int N ) throws IOException { + private void sliceTestHelper( ReadData readData, int N ) throws IOException { assertEquals("length one", 1, readData.slice(9, 1).length()); From 493276ee8dfe330533ad14d60783b9ec69b9eea2 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Mon, 16 Jun 2025 16:27:16 -0400 Subject: [PATCH 16/20] style!: remove ReadData.from(KeyValueAccess) * and remove KeyValueAccessReadData class * because KVA.createReadData is preferred --- .../n5/readdata/KeyValueAccessReadData.java | 74 ------------------- .../saalfeldlab/n5/readdata/ReadData.java | 16 ---- 2 files changed, 90 deletions(-) delete mode 100644 src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java deleted file mode 100644 index 2fa2ca00d..000000000 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java +++ /dev/null @@ -1,74 +0,0 @@ -/*- - * #%L - * Not HDF5 - * %% - * Copyright (C) 2017 - 2025 Stephan Saalfeld - * %% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions are met: - * - * 1. Redistributions of source code must retain the above copyright notice, - * this list of conditions and the following disclaimer. - * 2. Redistributions in binary form must reproduce the above copyright notice, - * this list of conditions and the following disclaimer in the documentation - * and/or other materials provided with the distribution. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE - * POSSIBILITY OF SUCH DAMAGE. - * #L% - */ -package org.janelia.saalfeldlab.n5.readdata; - -import java.io.IOException; -import java.io.InputStream; -import org.apache.commons.io.input.ProxyInputStream; -import org.janelia.saalfeldlab.n5.KeyValueAccess; -import org.janelia.saalfeldlab.n5.LockedChannel; -import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; - -class KeyValueAccessReadData extends AbstractInputStreamReadData { - - private final KeyValueAccess keyValueAccess; - private final String normalPath; - - KeyValueAccessReadData(final KeyValueAccess keyValueAccess, final String normalPath) { - this.keyValueAccess = keyValueAccess; - this.normalPath = normalPath; - } - - /** - * Open a {@code InputStream} on this data. - *

- * This will open a {@code LockedChannel} on the underlying {@code - * KeyValueAccess}. Make sure to {@code close()} the returned {@code - * InputStream} to release the underlying {@code LockedChannel}. - * - * @return an InputStream on this data - * - * @throws N5IOException - * if any I/O error occurs - */ - @Override - public InputStream inputStream() throws N5IOException { - - @SuppressWarnings("resource") - final LockedChannel channel = keyValueAccess.lockForReading(normalPath); - return new ProxyInputStream(channel.newInputStream()) { - - @Override - public void close() throws IOException { - in.close(); - channel.close(); - } - }; - } -} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java index 582f7bf6c..724e71cdc 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java @@ -254,22 +254,6 @@ static ReadData from(final InputStream inputStream) { return from(inputStream, -1); } - /** - * Create a new {@code ReadData} that loads lazily from {@code normalPath} - * in {@code keyValueAccess}. The returned ReadData reports {@link #length() - * length() == -1} (i.e., unknown length). - * - * @param keyValueAccess - * KeyValueAccess to read from - * @param normalPath - * path in the {@code keyValueAccess} to read from - * - * @return a new ReadData - */ - static ReadData from(final KeyValueAccess keyValueAccess, final String normalPath) { - return new KeyValueAccessReadData(keyValueAccess, normalPath); - } - /** * Create a new {@code ReadData} that wraps the specified portion of a * {@code byte[]} array. From a90db723d89622e2717003fe13cf2af07001e77f Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Mon, 16 Jun 2025 16:41:58 -0400 Subject: [PATCH 17/20] fix: KVA.createReadData is now abstract --- src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java index 3a4bda520..938826a64 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java @@ -276,9 +276,7 @@ default URI uri(final String uriString) throws URISyntaxException { * @return a materialized Read data * @throws N5IOException if an error occurs */ - default ReadData createReadData(final String normalPath) throws N5IOException { - return ReadData.from(this, normalPath); - } + abstract ReadData createReadData(final String normalPath) throws N5IOException; /** * Create a lock on a path for reading. This isn't meant to be kept From 8b69fa73d5c7d769e00de1b6998f33d3e70fe39f Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Mon, 16 Jun 2025 20:31:34 -0400 Subject: [PATCH 18/20] refactor!: implementation of KVA.createReadData * add LazyRead interface, * add HttpLazyRead, FileLazyRead * add KeyValueAccessReadData using LazyRead --- .../n5/FileSystemKeyValueAccess.java | 62 +++++++++------- .../saalfeldlab/n5/HttpKeyValueAccess.java | 33 +++++---- .../n5/readdata/KeyValueAccessReadData.java | 70 +++++++++++++++++++ .../saalfeldlab/n5/readdata/LazyRead.java | 34 +++++++++ 4 files changed, 157 insertions(+), 42 deletions(-) create mode 100644 src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java create mode 100644 src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyRead.java diff --git a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java index b8768caed..cba399fe9 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java @@ -86,6 +86,8 @@ import java.util.Iterator; import java.util.stream.Stream; +import org.janelia.saalfeldlab.n5.readdata.KeyValueAccessReadData; +import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** @@ -210,7 +212,7 @@ public FileSystemKeyValueAccess(final FileSystem fileSystem) { @Override public ReadData createReadData(final String normalPath) { - return new FileLazyReadData(this, normalPath, 0, -1); + return new KeyValueAccessReadData(new FileLazyRead(normalPath)); } @Override @@ -620,39 +622,45 @@ protected static void createAndCheckIsDirectory( } } - private static class FileLazyReadData extends KeyValueAccessLazyReadData { + private class FileLazyRead implements LazyRead { - public FileLazyReadData(FileSystemKeyValueAccess kva, String normalKey, long offset, long length) { - super(kva, normalKey, offset, length); - } + private final String normalKey; - @Override - void read() throws N5IOException { + FileLazyRead(String normalKey) { + this.normalKey = normalKey; + } - try (FileChannel channel = kva.lockForReading(normalKey).getFileChannel()) { - channel.position(offset); - if (length > Integer.MAX_VALUE) - throw new IOException("Attempt to materialize too large data"); + @Override + public long size() { + return FileSystemKeyValueAccess.this.size(normalKey); + } - final long channelSize = channel.size(); - if( !validBounds(channelSize, offset, length)) - throw new IndexOutOfBoundsException(); + @Override + public ReadData materialize(final long offset, final long length) { - final int sz = (int)(length < 0 ? channelSize : length); - final byte[] data = new byte[sz]; - final ByteBuffer buf = ByteBuffer.wrap(data); - channel.read(buf); - materialized = ReadData.from(data); + try (final LockedFileChannel lfs = new LockedFileChannel(normalKey, true)) { + final FileChannel channel = lfs.getFileChannel(); + channel.position(offset); + if (length > Integer.MAX_VALUE) + throw new IOException("Attempt to materialize too large data"); - } catch (final IOException e) { - throw new N5Exception.N5IOException(e); - } - } + final long channelSize = channel.size(); + if (!validBounds(channelSize, offset, length)) + throw new IndexOutOfBoundsException(); + + final int sz = (int) (length < 0 ? channelSize : length); + final byte[] data = new byte[sz]; + final ByteBuffer buf = ByteBuffer.wrap(data); + channel.read(buf); + return ReadData.from(data); + + } catch (final NoSuchFileException e) { + throw new N5NoSuchKeyException("No such file", e); + } catch (IOException | UncheckedIOException e) { + throw new N5Exception.N5IOException(e); + } + } - @Override - KeyValueAccessLazyReadData lazySlice(long offset, long length) { - return new FileLazyReadData(kva, normalKey, offset, length); - } } private static boolean validBounds(long channelSize, long offset, long length) { diff --git a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java index 7810becd8..cd53cbf62 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java @@ -33,6 +33,8 @@ import org.apache.commons.lang3.function.TriFunction; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.http.ListResponseParser; +import org.janelia.saalfeldlab.n5.readdata.KeyValueAccessReadData; +import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.ReadData; import java.io.Closeable; @@ -231,8 +233,8 @@ private HttpURLConnection httpRequest(String normalPath, String method) throws I } @Override - public HttpLazyReadData createReadData(final String normalPath) { - return new HttpLazyReadData(this, normalPath, 0, -1); + public ReadData createReadData(final String normalPath) { + return new KeyValueAccessReadData(new HttpLazyRead(normalPath)); } public LockedChannel lockForReading(final String normalPath) throws N5IOException { @@ -427,28 +429,29 @@ public void close() throws IOException { } } - private class HttpLazyReadData extends KeyValueAccessLazyReadData { + private class HttpLazyRead implements LazyRead { - public HttpLazyReadData(HttpKeyValueAccess kva, String normalKey, long offset, long length) { - super(kva, normalKey, offset, length); + private final String normalKey; + + HttpLazyRead(String normalKey) { + this.normalKey = normalKey; + } + + @Override + public long size() { + return HttpKeyValueAccess.this.size(normalKey); } @Override - void read() throws N5IOException { - // TODO does this throw out-of-bounds when it should - try( final HttpObjectChannel ch = new HttpObjectChannel(kva.uri(normalKey), offset, length) ) { - materialized = ReadData.from(ch.newInputStream()).materialize(); + public ReadData materialize(long offset, long length) { + try (final HttpObjectChannel ch = new HttpObjectChannel(uri(normalKey), offset, length)) { + return ReadData.from(ch.newInputStream()).materialize(); } catch (IOException e) { - throw new N5Exception.N5IOException(e); + throw new N5IOException(e); } catch (URISyntaxException e) { throw new N5Exception(e); } } - - @Override - KeyValueAccessLazyReadData lazySlice(long offset, long length) { - return new HttpLazyReadData(kva, normalKey, offset, length); - } } } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java new file mode 100644 index 000000000..dea259f23 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java @@ -0,0 +1,70 @@ +package org.janelia.saalfeldlab.n5.readdata; + +import java.io.InputStream; + +import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; + +public class KeyValueAccessReadData implements ReadData { + + private final LazyRead lazyRead; + private ReadData materialized; + private final long offset; + private long length; + + public KeyValueAccessReadData(LazyRead lazyRead) { + this(lazyRead, 0, -1); + } + + public KeyValueAccessReadData(final LazyRead lazyRead, final long offset, final long length) { + this.lazyRead = lazyRead; + this.offset = offset; + this.length = length; + } + + @Override + public ReadData materialize() throws N5IOException { + if (materialized == null) + materialized = lazyRead.materialize(offset, length); + return materialized; + } + + @Override + public ReadData slice(final long offset, final long length) throws N5IOException { + if (offset < 0) + throw new IndexOutOfBoundsException("Negative offset: " + offset); + + if (materialized != null) + return materialized.slice(offset, length); + + // if a slice of indeterminate length is requested, but the + // length is already known, use the known length; + final int lengthArg; + if (this.length > 0 && length < 0) + lengthArg = (int)(this.length - offset); + else + lengthArg = (int)length; + + return new KeyValueAccessReadData(lazyRead, this.offset + offset, lengthArg); + } + + @Override + public InputStream inputStream() throws N5IOException, IllegalStateException { + return materialize().inputStream(); + } + + @Override + public byte[] allBytes() throws N5IOException, IllegalStateException { + return materialize().allBytes(); + } + + @Override + public long length() throws N5IOException { + if (materialized != null) + return materialized.length(); + if (length < 0) { + length = lazyRead.size() - offset; + } + return length; + } + +} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyRead.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyRead.java new file mode 100644 index 000000000..e72dbbe22 --- /dev/null +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyRead.java @@ -0,0 +1,34 @@ +package org.janelia.saalfeldlab.n5.readdata; + +/** + * A lazy reading strategy for lazy, partial reading of data from some source. + *

+ * Implementations of this interface handle the specifics of accessing data from + * their respective sources. + * + * @see ReadData + * @see KeyValueAccessReadData + */ +public interface LazyRead { + + /** + * Materializes a portion of the data into a concrete {@link ReadData} instance. + *

+ * This method performs the actual read operation from the underlying source, + * loading only the requested portion of data. The implementation should handle + * bounds checking and throw appropriate exceptions for invalid ranges. + * + * @param offset the starting position in the data source + * @param length the number of bytes to read, or -1 to read from offset to end + * @return a materialized {@link ReadData} instance containing the requested data + */ + ReadData materialize(long offset, long length); + + /** + * Returns the total size of the data source in bytes. + * + * @return the size of the data source in bytes + */ + long size(); + +} From e1b9cf96985b8441958fd1e9098754b7ec94c8a4 Mon Sep 17 00:00:00 2001 From: John Bogovic Date: Tue, 17 Jun 2025 09:54:59 -0400 Subject: [PATCH 19/20] refactor: move LazyRead into KeyValueAccess * remove unused KeyValueAccessLazyReadData * make KeyValueAccessReadData package-private --- .../n5/FileSystemKeyValueAccess.java | 2 - .../saalfeldlab/n5/HttpKeyValueAccess.java | 2 - .../saalfeldlab/n5/KeyValueAccess.java | 43 +++++++ .../n5/KeyValueAccessLazyReadData.java | 112 ------------------ .../KeyValueAccessReadData.java | 24 +++- .../saalfeldlab/n5/readdata/LazyRead.java | 34 ------ .../saalfeldlab/n5/readdata/LazyReadData.java | 1 - 7 files changed, 64 insertions(+), 154 deletions(-) delete mode 100644 src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java rename src/main/java/org/janelia/saalfeldlab/n5/{readdata => }/KeyValueAccessReadData.java (69%) delete mode 100644 src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyRead.java diff --git a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java index cba399fe9..d6a9803b8 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java @@ -86,8 +86,6 @@ import java.util.Iterator; import java.util.stream.Stream; -import org.janelia.saalfeldlab.n5.readdata.KeyValueAccessReadData; -import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** diff --git a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java index cd53cbf62..8826ebaf4 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java @@ -33,8 +33,6 @@ import org.apache.commons.lang3.function.TriFunction; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.http.ListResponseParser; -import org.janelia.saalfeldlab.n5.readdata.KeyValueAccessReadData; -import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.ReadData; import java.io.Closeable; diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java index 938826a64..aa771a268 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java @@ -363,4 +363,47 @@ default URI uri(final String uriString) throws URISyntaxException { * if an error occurs during deletion */ public void delete(final String normalPath) throws N5IOException; + + /** + * A lazy reading strategy for lazy, partial reading of data from some source. + *

+ * Implementations of this interface handle the specifics of accessing data from + * their respective sources. + * + * @see ReadData + * @see KeyValueAccessReadData + */ + interface LazyRead { + + /** + * Materializes a portion of the data into a concrete {@link ReadData} + * instance. + *

+ * This method performs the actual read operation from the underlying + * source, loading only the requested portion of data. The implementation + * should handle bounds checking and throw appropriate exceptions for + * invalid ranges. + * + * @param offset + * the starting position in the data source + * @param length + * the number of bytes to read, or -1 to read from offset to end + * @return a materialized {@link ReadData} instance containing the requested + * data + * @throws N5IOException + * if any I/O error occurs + */ + ReadData materialize(long offset, long length) throws N5IOException; + + /** + * Returns the total size of the data source in bytes. + * + * @return the size of the data source in bytes + * @throws N5IOException + * if an I/O error occurs while trying to get the length + */ + long size() throws N5IOException; + + } + } diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java deleted file mode 100644 index 9e008cd35..000000000 --- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessLazyReadData.java +++ /dev/null @@ -1,112 +0,0 @@ -package org.janelia.saalfeldlab.n5; - -import java.io.IOException; -import java.io.InputStream; - -import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; -import org.janelia.saalfeldlab.n5.readdata.ReadData; - -/** - * This abstract class represents a lazy read operation, and implements the - * shared logic for lazily reading from a {@link KeyValueAccess}. - * - * @param - * the type of {@link KeyValueAccess}. - */ -abstract class KeyValueAccessLazyReadData implements ReadData { - - protected ReadData materialized; - - protected final K kva; - protected final String normalKey; - protected final long offset; - protected long length; - - KeyValueAccessLazyReadData(K kva, String normalKey, long offset, long length) { - - if (offset < 0) - throw new IndexOutOfBoundsException("Can not create KeyValueAccesReadData with negative offset: " + offset); - - this.kva = kva; - this.normalKey = normalKey; - this.offset = offset; - this.length = length; - } - - KeyValueAccessLazyReadData(K kva, String normalKey, long offset) { - - this(kva, normalKey, offset, -1); - } - - @Override - public long length() throws N5IOException { - - if (materialized != null) - return materialized.length(); - - if (length < 0) { - length = kva.size(normalKey); - } - return length; - } - - @Override - public InputStream inputStream() throws N5IOException, IllegalStateException { - - return materialize().inputStream(); - } - - @Override - public byte[] allBytes() throws N5IOException, IllegalStateException { - - return materialize().allBytes(); - } - - @Override - public ReadData materialize() throws N5IOException { - - if (materialized == null) - read(); - - return materialized; - } - - /** - * Read from the backed {@link KeyValueAccess} and set the materialized {@link ReadData} field. - * - * @throws N5IOException - * if an I/O error occurs - */ - abstract void read() throws N5IOException; - - /** - * Return a new instance of a KeyValueAccessLazyReadData for this {@KeyValueAceess} and key, - * but that represents a read operation that slices this instance with the given arguments. - *

- * This method should not perform any reads or calls to the backing KeyValueAccess. - * - * @param offset the offset relative to this - * @param length of the returned ReadData - * @return - * a new KeyValueAccessLazyReadData - */ - abstract KeyValueAccessLazyReadData lazySlice(long offset, long length); - - @Override - public ReadData slice(final long offset, final long length) throws N5IOException { - - if (materialized != null) - return materialize().slice(offset, length); - - // if a slice of indeterminate length is requested, but the - // length is already known, use the known length; - final int lengthArg; - if (this.length > 0 && length < 0) - lengthArg = (int)(this.length - offset); - else - lengthArg = (int)length; - - return lazySlice(this.offset + offset, lengthArg); - } - -} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessReadData.java similarity index 69% rename from src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java rename to src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessReadData.java index dea259f23..77c3c4efd 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/KeyValueAccessReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccessReadData.java @@ -1,9 +1,15 @@ -package org.janelia.saalfeldlab.n5.readdata; +package org.janelia.saalfeldlab.n5; import java.io.InputStream; +import org.janelia.saalfeldlab.n5.KeyValueAccess.LazyRead; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; +import org.janelia.saalfeldlab.n5.readdata.ReadData; +/** + * A {@link ReadData} implementation that reads from a {@link KeyValueAccess} + * backend through a {@link LazyRead} object. + */ public class KeyValueAccessReadData implements ReadData { private final LazyRead lazyRead; @@ -11,11 +17,11 @@ public class KeyValueAccessReadData implements ReadData { private final long offset; private long length; - public KeyValueAccessReadData(LazyRead lazyRead) { + KeyValueAccessReadData(LazyRead lazyRead) { this(lazyRead, 0, -1); } - public KeyValueAccessReadData(final LazyRead lazyRead, final long offset, final long length) { + KeyValueAccessReadData(final LazyRead lazyRead, final long offset, final long length) { this.lazyRead = lazyRead; this.offset = offset; this.length = length; @@ -28,6 +34,18 @@ public ReadData materialize() throws N5IOException { return materialized; } + /** + * Returns a {@link ReadData} whose length is limited to the given value. + *

+ * This implementation defers a material read operation if allowed + * by the {@link LazyRead}. + * + * @param length + * the length of the resulting ReadData + * @return a length-limited ReadData + * @throws N5IOException + * if an I/O error occurs while trying to get the length + */ @Override public ReadData slice(final long offset, final long length) throws N5IOException { if (offset < 0) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyRead.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyRead.java deleted file mode 100644 index e72dbbe22..000000000 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyRead.java +++ /dev/null @@ -1,34 +0,0 @@ -package org.janelia.saalfeldlab.n5.readdata; - -/** - * A lazy reading strategy for lazy, partial reading of data from some source. - *

- * Implementations of this interface handle the specifics of accessing data from - * their respective sources. - * - * @see ReadData - * @see KeyValueAccessReadData - */ -public interface LazyRead { - - /** - * Materializes a portion of the data into a concrete {@link ReadData} instance. - *

- * This method performs the actual read operation from the underlying source, - * loading only the requested portion of data. The implementation should handle - * bounds checking and throw appropriate exceptions for invalid ranges. - * - * @param offset the starting position in the data source - * @param length the number of bytes to read, or -1 to read from offset to end - * @return a materialized {@link ReadData} instance containing the requested data - */ - ReadData materialize(long offset, long length); - - /** - * Returns the total size of the data source in bytes. - * - * @return the size of the data source in bytes - */ - long size(); - -} diff --git a/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyReadData.java b/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyReadData.java index c00b59e4d..9134e0ba2 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyReadData.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyReadData.java @@ -33,7 +33,6 @@ import java.io.InputStream; import java.io.OutputStream; import org.apache.commons.io.output.ProxyOutputStream; -import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; class LazyReadData implements ReadData { From ca470ab05c47e9ff2878aa059c046f3982d73a03 Mon Sep 17 00:00:00 2001 From: tpietzsch Date: Sun, 22 Jun 2025 13:54:00 +0200 Subject: [PATCH 20/20] style: rm redundant modifiers --- .../saalfeldlab/n5/KeyValueAccess.java | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java index aa771a268..5ed6eec8a 100644 --- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java +++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java @@ -81,7 +81,7 @@ public interface KeyValueAccess { * the path * @return the path components */ - public default String[] components(final String path) { + default String[] components( final String path ) { String[] components = Arrays.stream(path.split("/")) .filter(x -> !x.isEmpty()) @@ -109,7 +109,7 @@ public default String[] components(final String path) { * @param components the path components * @return the path */ - public default String compose(final URI uri, final String... components) { + default String compose( final URI uri, final String... components ) { int firstNonEmptyIdx = 0; while (firstNonEmptyIdx < components.length && (components[firstNonEmptyIdx] == null || components[firstNonEmptyIdx].isEmpty())) { @@ -148,7 +148,7 @@ else if (component.endsWith("/") || i == allComponents.length - 1) } @Deprecated - public default String compose(final String... components) { + default String compose( final String... components ) { return normalize( Arrays.stream(components) @@ -165,7 +165,7 @@ public default String compose(final String... components) { * the path * @return the parent path or null if the path has no parent */ - public default String parent(final String path) { + default String parent( final String path ) { final String removeTrailingSlash = path.replaceAll("/+$", ""); return normalize(N5URI.getAsUri(removeTrailingSlash).resolve("").toString()); } @@ -179,7 +179,7 @@ public default String parent(final String path) { * the base path * @return the result or null if the path has no parent */ - public default String relativize(final String path, final String base) { + default String relativize( final String path, final String base ) { try { /* @@ -205,7 +205,7 @@ public default String relativize(final String path, final String base) { * the path * @return the normalized path */ - public String normalize(final String path); + String normalize( final String path ); /** * Get the absolute (including scheme) {@link URI} of the given path @@ -231,7 +231,7 @@ default URI uri(final String uriString) throws URISyntaxException { * efforts are made to normalize it. * @return true if the path exists */ - public boolean exists(final String normalPath); + boolean exists( final String normalPath ); /** * Returns the size in bytes of the object at the given normalPath if it exists. @@ -242,7 +242,7 @@ default URI uri(final String uriString) throws URISyntaxException { * @return the size of the object in bytes. * @throws N5Exception.N5NoSuchKeyException if the given key does not exist */ - public long size(final String normalPath) throws N5Exception.N5NoSuchKeyException; + long size( final String normalPath ) throws N5Exception.N5NoSuchKeyException; /** * Test whether the path is a directory. @@ -252,7 +252,7 @@ default URI uri(final String uriString) throws URISyntaxException { * efforts are made to normalize it. * @return true if the path is a directory */ - public boolean isDirectory(String normalPath); + boolean isDirectory( String normalPath ); /** * Test whether the path is a file. @@ -262,7 +262,7 @@ default URI uri(final String uriString) throws URISyntaxException { * efforts are made to normalize it. * @return true if the path is a file */ - public boolean isFile(String normalPath); // TODO: Looks un-used. Remove? + boolean isFile( String normalPath ); // TODO: Looks un-used. Remove? /** * Create a {@link ReadData} through which data at the normal key can be read. @@ -276,7 +276,7 @@ default URI uri(final String uriString) throws URISyntaxException { * @return a materialized Read data * @throws N5IOException if an error occurs */ - abstract ReadData createReadData(final String normalPath) throws N5IOException; + ReadData createReadData( final String normalPath ) throws N5IOException; /** * Create a lock on a path for reading. This isn't meant to be kept @@ -294,7 +294,7 @@ default URI uri(final String uriString) throws URISyntaxException { * @throws N5IOException * if a locked channel could not be created */ - public LockedChannel lockForReading(final String normalPath) throws N5IOException; + LockedChannel lockForReading( final String normalPath ) throws N5IOException; /** * Create an exclusive lock on a path for writing. If the file doesn't @@ -314,7 +314,7 @@ default URI uri(final String uriString) throws URISyntaxException { * @throws N5IOException * if a locked channel could not be created */ - public LockedChannel lockForWriting(final String normalPath) throws N5IOException; + LockedChannel lockForWriting( final String normalPath ) throws N5IOException; /** * List all 'directory'-like children of a path. @@ -326,7 +326,7 @@ default URI uri(final String uriString) throws URISyntaxException { * @throws N5IOException * if an error occurs during listing */ - public String[] listDirectories(final String normalPath) throws N5IOException; + String[] listDirectories( final String normalPath ) throws N5IOException; /** * List all children of a path. @@ -337,7 +337,7 @@ default URI uri(final String uriString) throws URISyntaxException { * @return the the child paths * @throws N5IOException if an error occurs during listing */ - public String[] list(final String normalPath) throws N5IOException; + String[] list( final String normalPath ) throws N5IOException; /** * Create a directory and all parent paths along the way. The directory @@ -351,7 +351,7 @@ default URI uri(final String uriString) throws URISyntaxException { * @throws N5IOException * if an error occurs during creation */ - public void createDirectories(final String normalPath) throws N5IOException; + void createDirectories( final String normalPath ) throws N5IOException; /** * Delete a path. If the path is a directory, delete it recursively. @@ -362,7 +362,7 @@ default URI uri(final String uriString) throws URISyntaxException { * @throws N5IOException * if an error occurs during deletion */ - public void delete(final String normalPath) throws N5IOException; + void delete( final String normalPath ) throws N5IOException; /** * A lazy reading strategy for lazy, partial reading of data from some source.