Skip to content

Commit 7724f25

Browse files
authored
Merge pull request #156 from bogovicj/splittable-readdata
Add slice and limit methods to ReadData
2 parents 0f69cfd + ca470ab commit 7724f25

11 files changed

Lines changed: 569 additions & 128 deletions

File tree

pom.xml

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,6 @@
202202
<groupId>org.apache.commons</groupId>
203203
<artifactId>commons-compress</artifactId>
204204
</dependency>
205-
<dependency>
206-
<groupId>commons-io</groupId>
207-
<artifactId>commons-io</artifactId>
208-
</dependency>
209205

210206
<!-- JMH -->
211207
<dependency>

src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
import java.io.Writer;
6666
import java.net.URI;
6767
import java.net.URISyntaxException;
68+
import java.nio.ByteBuffer;
6869
import java.nio.channels.Channels;
6970
import java.nio.channels.FileChannel;
7071
import java.nio.channels.OverlappingFileLockException;
@@ -85,6 +86,8 @@
8586
import java.util.Iterator;
8687
import java.util.stream.Stream;
8788

89+
import org.janelia.saalfeldlab.n5.readdata.ReadData;
90+
8891
/**
8992
* Filesystem {@link KeyValueAccess}.
9093
*
@@ -145,6 +148,10 @@ protected LockedFileChannel(final Path path, final boolean readOnly) throws IOEx
145148
}
146149
}
147150

151+
protected FileChannel getFileChannel() {
152+
return channel;
153+
}
154+
148155
private void truncateChannel(int size) {
149156

150157
try {
@@ -202,7 +209,12 @@ public FileSystemKeyValueAccess(final FileSystem fileSystem) {
202209
}
203210

204211
@Override
205-
public LockedChannel lockForReading(final String normalPath) throws N5IOException {
212+
public ReadData createReadData(final String normalPath) {
213+
return new KeyValueAccessReadData(new FileLazyRead(normalPath));
214+
}
215+
216+
@Override
217+
public LockedFileChannel lockForReading(final String normalPath) throws N5IOException {
206218

207219
try {
208220
return new LockedFileChannel(normalPath, true);
@@ -268,6 +280,18 @@ public boolean exists(final String normalPath) {
268280
return Files.exists(path);
269281
}
270282

283+
@Override
284+
public long size(final String normalPath) {
285+
286+
try {
287+
return Files.size(fileSystem.getPath(normalPath));
288+
} catch (NoSuchFileException e) {
289+
throw new N5Exception.N5NoSuchKeyException("No such file", e);
290+
} catch (IOException | UncheckedIOException e) {
291+
throw new N5Exception.N5IOException(e);
292+
}
293+
}
294+
271295
@Override
272296
public String[] listDirectories(final String normalPath) throws N5IOException {
273297

@@ -595,4 +619,58 @@ protected static void createAndCheckIsDirectory(
595619
throw x;
596620
}
597621
}
622+
623+
private class FileLazyRead implements LazyRead {
624+
625+
private final String normalKey;
626+
627+
FileLazyRead(String normalKey) {
628+
this.normalKey = normalKey;
629+
}
630+
631+
@Override
632+
public long size() {
633+
return FileSystemKeyValueAccess.this.size(normalKey);
634+
}
635+
636+
@Override
637+
public ReadData materialize(final long offset, final long length) {
638+
639+
try (final LockedFileChannel lfs = new LockedFileChannel(normalKey, true)) {
640+
final FileChannel channel = lfs.getFileChannel();
641+
channel.position(offset);
642+
if (length > Integer.MAX_VALUE)
643+
throw new IOException("Attempt to materialize too large data");
644+
645+
final long channelSize = channel.size();
646+
if (!validBounds(channelSize, offset, length))
647+
throw new IndexOutOfBoundsException();
648+
649+
final int sz = (int) (length < 0 ? channelSize : length);
650+
final byte[] data = new byte[sz];
651+
final ByteBuffer buf = ByteBuffer.wrap(data);
652+
channel.read(buf);
653+
return ReadData.from(data);
654+
655+
} catch (final NoSuchFileException e) {
656+
throw new N5NoSuchKeyException("No such file", e);
657+
} catch (IOException | UncheckedIOException e) {
658+
throw new N5Exception.N5IOException(e);
659+
}
660+
}
661+
662+
}
663+
664+
private static boolean validBounds(long channelSize, long offset, long length) {
665+
666+
if (offset < 0)
667+
return false;
668+
else if (channelSize > 0 && offset >= channelSize) // offset == 0 and arrayLength == 0 is okay
669+
return false;
670+
else if (length >= 0 && offset + length > channelSize)
671+
return false;
672+
673+
return true;
674+
}
675+
598676
}

src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@
2929
package org.janelia.saalfeldlab.n5;
3030

3131
import org.apache.commons.io.IOUtils;
32+
3233
import org.apache.commons.lang3.function.TriFunction;
3334
import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;
3435
import org.janelia.saalfeldlab.n5.http.ListResponseParser;
36+
import org.janelia.saalfeldlab.n5.readdata.ReadData;
3537

3638
import java.io.Closeable;
3739
import java.io.IOException;
@@ -57,6 +59,13 @@
5759
*/
5860
public class HttpKeyValueAccess implements KeyValueAccess {
5961

62+
public static final String HEAD = "HEAD";
63+
public static final String GET = "GET";
64+
65+
public static final String RANGE = "Range";
66+
public static final String ACCEPT_RANGE = "Accept-Range";
67+
public static final String BYTES = "bytes";
68+
6069
private int readTimeoutMilliseconds;
6170
private int connectionTimeoutMilliseconds;
6271

@@ -127,6 +136,12 @@ public boolean exists(final String normalPath) {
127136
}
128137
}
129138

139+
@Override public long size(String normalPath) {
140+
141+
final HttpURLConnection head = requireValidHttpResponse(normalPath, "HEAD", "Error checking existence: " + normalPath, true);
142+
return head.getContentLengthLong();
143+
}
144+
130145
/**
131146
* Test whether the path is a directory.
132147
* <p>
@@ -142,7 +157,7 @@ public boolean exists(final String normalPath) {
142157
public boolean isDirectory(final String normalPath) {
143158

144159
try {
145-
requireValidHttpResponse(getDirectoryPath(normalPath), "HEAD", (code, msg,http) -> {
160+
requireValidHttpResponse(getDirectoryPath(normalPath), HEAD, (code, msg,http) -> {
146161
final N5Exception cause = validExistsResponse(code, "Error checking directory: " + normalPath, msg, true);
147162
if (code >= 300 && code < 400) {
148163
final String redirectLocation = http.getHeaderField("Location");
@@ -184,7 +199,7 @@ public boolean isFile(final String normalPath) {
184199

185200
/* Files must not end in `/` And Don't accept a redirect to a location ending in `/` */
186201
try {
187-
requireValidHttpResponse(getFilePath(normalPath), "HEAD", (code, msg, http) -> {
202+
requireValidHttpResponse(getFilePath(normalPath), HEAD, (code, msg, http) -> {
188203
final N5Exception cause = validExistsResponse(code, "Error accessing file: " + normalPath, msg, true);
189204
if (code >= 300 && code < 400) {
190205
final String redirectLocation = http.getHeaderField("Location");
@@ -216,12 +231,16 @@ private HttpURLConnection httpRequest(String normalPath, String method) throws I
216231
}
217232

218233
@Override
234+
public ReadData createReadData(final String normalPath) {
235+
return new KeyValueAccessReadData(new HttpLazyRead(normalPath));
236+
}
237+
219238
public LockedChannel lockForReading(final String normalPath) throws N5IOException {
220239
//TODO Caleb: Maybe check exists lazily when attempting to read
221240
try {
222241
if (!exists(normalPath))
223242
throw new N5Exception.N5NoSuchKeyException("Key does not exist: " + normalPath);
224-
return new HttpObjectChannel(uri(normalPath));
243+
return new HttpObjectChannel(uri(normalPath), 0, -1);
225244
} catch (URISyntaxException e) {
226245
throw new N5Exception("Invalid URI Syntax", e);
227246
}
@@ -272,7 +291,7 @@ public String[] list(final String normalPath) throws N5IOException {
272291

273292
private String[] queryListEntries(String normalPath, ListResponseParser parser, boolean allowRedirect) throws N5IOException{
274293

275-
final HttpURLConnection http = requireValidHttpResponse(normalPath, "GET", "Error listing directory at " + normalPath, allowRedirect);
294+
final HttpURLConnection http = requireValidHttpResponse(normalPath, GET, "Error listing directory at " + normalPath, allowRedirect);
276295
try {
277296
final String listResponse = responseToString(http.getInputStream());
278297
return parser.parseListResponse(listResponse);
@@ -333,23 +352,47 @@ public void delete(final String normalPath) {
333352
private class HttpObjectChannel implements LockedChannel {
334353

335354
protected final URI uri;
355+
private final long startByte;
356+
private final long size;
336357
private final ArrayList<Closeable> resources = new ArrayList<>();
337358

338-
protected HttpObjectChannel(final URI uri) {
359+
protected HttpObjectChannel(final URI uri, long startByte, long size) {
339360

340361
this.uri = uri;
362+
this.startByte = startByte;
363+
this.size = size;
364+
}
365+
366+
private boolean isPartialRead() {
367+
return startByte > 0 || (size >= 0 && size != Long.MAX_VALUE);
341368
}
342369

343370
@Override
344371
public InputStream newInputStream() throws N5IOException {
345372

346373
try {
347-
return uri.toURL().openStream();
374+
HttpURLConnection conn = (HttpURLConnection)uri.toURL().openConnection();
375+
if (isPartialRead()) {
376+
conn.setRequestProperty(RANGE, rangeString());
377+
final String acceptRanges = conn.getHeaderField(ACCEPT_RANGE);
378+
if (acceptRanges == null || !acceptRanges.equals(BYTES)) {
379+
conn.disconnect();
380+
conn = (HttpURLConnection)uri.toURL().openConnection();
381+
return ReadData.from(conn.getInputStream()).materialize().slice(startByte, size).inputStream();
382+
}
383+
}
384+
return conn.getInputStream();
348385
} catch (IOException e) {
349386
throw new N5IOException("Could not open stream for " + uri, e);
350387
}
351388
}
352389

390+
private String rangeString() {
391+
392+
final String lastByte = (size > 0) ? Long.toString(startByte + size - 1) : "";
393+
return String.format("%s=%d-%s", BYTES, startByte, lastByte);
394+
}
395+
353396
@Override
354397
public Reader newReader() throws N5IOException {
355398

@@ -384,4 +427,29 @@ public void close() throws IOException {
384427
}
385428
}
386429

430+
private class HttpLazyRead implements LazyRead {
431+
432+
private final String normalKey;
433+
434+
HttpLazyRead(String normalKey) {
435+
this.normalKey = normalKey;
436+
}
437+
438+
@Override
439+
public long size() {
440+
return HttpKeyValueAccess.this.size(normalKey);
441+
}
442+
443+
@Override
444+
public ReadData materialize(long offset, long length) {
445+
try (final HttpObjectChannel ch = new HttpObjectChannel(uri(normalKey), offset, length)) {
446+
return ReadData.from(ch.newInputStream()).materialize();
447+
} catch (IOException e) {
448+
throw new N5IOException(e);
449+
} catch (URISyntaxException e) {
450+
throw new N5Exception(e);
451+
}
452+
}
453+
}
454+
387455
}

0 commit comments

Comments
 (0)