Skip to content

Commit 16258a5

Browse files
committed
Merge branch 'wip/codecs' of github.com:saalfeldlab/n5 into wip/codecs
2 parents bd92d78 + 760b1a3 commit 16258a5

18 files changed

Lines changed: 607 additions & 181 deletions

doc/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,6 @@ Custom compression schemes can be implemented using the annotation discovery mec
132132
133133
## Disclaimer
134134
135-
HDF5 is a great format that provides a wealth of conveniences that I do not want to miss. It's inefficiency for parallel writing, however, limit its applicability for handling of very large n-dimensional data.
135+
HDF5 is a great format that provides a wealth of conveniences that I do not want to miss. Its inefficiency for parallel writing, however, limits its applicability for handling of very large n-dimensional data.
136136
137137
N5 uses the native filesystem of the target platform and JSON files to specify basic and custom meta-data as attributes. It aims at preserving the convenience of HDF5 where possible but doesn't try too hard to be a full replacement.

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/GsonKeyValueN5Reader.java

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

3131
import java.io.IOException;
32+
import java.io.InputStream;
33+
import java.io.InputStreamReader;
3234
import java.io.UncheckedIOException;
33-
import java.util.Arrays;
3435

3536
import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;
3637
import org.janelia.saalfeldlab.n5.codec.Codec.ArrayCodec;
@@ -81,14 +82,13 @@ default JsonElement getAttributes(final String pathName) throws N5Exception {
8182
final String groupPath = N5URI.normalizeGroupPath(pathName);
8283
final String attributesPath = absoluteAttributesPath(groupPath);
8384

84-
try (final LockedChannel lockedChannel = getKeyValueAccess().lockForReading(attributesPath)) {
85-
return GsonUtils.readAttributes(lockedChannel.newReader(), getGson());
85+
try ( final InputStream in = getKeyValueAccess().createReadData(attributesPath).inputStream() ) {
86+
return GsonUtils.readAttributes(new InputStreamReader(in), getGson());
8687
} catch (final N5Exception.N5NoSuchKeyException e) {
8788
return null;
8889
} catch (final IOException | UncheckedIOException | N5IOException e) {
8990
throw new N5IOException("Failed to read attributes from dataset " + pathName, e);
9091
}
91-
9292
}
9393

9494
@Override
@@ -99,15 +99,12 @@ default DataBlock<?> readBlock(
9999

100100
final String path = absoluteDataBlockPath(N5URI.normalizeGroupPath(pathName), gridPosition);
101101

102-
try (final LockedChannel lockedChannel = getKeyValueAccess().lockForReading(path)) {
103-
final ArrayCodec codec = datasetAttributes.getArrayCodec();
104-
return codec.decode(ReadData.from(lockedChannel.newInputStream()), gridPosition);
105-
} catch (final N5Exception.N5NoSuchKeyException e) {
102+
try {
103+
final ReadData blockData = getKeyValueAccess().createReadData(path);
104+
final ArrayCodec arrayCodec = datasetAttributes.getArrayCodec();
105+
return arrayCodec.decode(blockData, gridPosition);
106+
} catch (N5Exception.N5NoSuchKeyException e) {
106107
return null;
107-
} catch (final IOException | UncheckedIOException | N5IOException e) {
108-
throw new N5IOException(
109-
"Failed to read block " + Arrays.toString(gridPosition) + " from dataset " + path,
110-
e);
111108
}
112109
}
113110

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

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,10 @@
3333
import org.apache.commons.lang3.function.TriFunction;
3434
import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;
3535
import org.janelia.saalfeldlab.n5.http.ListResponseParser;
36+
import org.janelia.saalfeldlab.n5.readdata.ReadData;
3637

3738
import java.io.Closeable;
39+
import java.io.FileNotFoundException;
3840
import java.io.IOException;
3941
import java.io.InputStream;
4042
import java.io.InputStreamReader;
@@ -135,6 +137,12 @@ public boolean exists(final String normalPath) {
135137
}
136138
}
137139

140+
@Override public long size(String normalPath) {
141+
142+
final HttpURLConnection head = requireValidHttpResponse(normalPath, "HEAD", "Error checking existence: " + normalPath, true);
143+
return head.getContentLengthLong();
144+
}
145+
138146
/**
139147
* Test whether the path is a directory.
140148
* <p>
@@ -224,12 +232,16 @@ private HttpURLConnection httpRequest(String normalPath, String method) throws I
224232
}
225233

226234
@Override
235+
public ReadData createReadData(final String normalPath) {
236+
return new KeyValueAccessReadData(new HttpLazyRead(normalPath));
237+
}
238+
227239
public LockedChannel lockForReading(final String normalPath) throws N5IOException {
228240
//TODO Caleb: Maybe check exists lazily when attempting to read
229241
try {
230242
if (!exists(normalPath))
231243
throw new N5Exception.N5NoSuchKeyException("Key does not exist: " + normalPath);
232-
return new HttpObjectChannel(uri(normalPath));
244+
return new HttpObjectChannel(uri(normalPath), 0, -1);
233245
} catch (URISyntaxException e) {
234246
throw new N5Exception("Invalid URI Syntax", e);
235247
}
@@ -341,23 +353,49 @@ public void delete(final String normalPath) {
341353
private class HttpObjectChannel implements LockedChannel {
342354

343355
protected final URI uri;
356+
private final long startByte;
357+
private final long size;
344358
private final ArrayList<Closeable> resources = new ArrayList<>();
345359

346-
protected HttpObjectChannel(final URI uri) {
360+
protected HttpObjectChannel(final URI uri, long startByte, long size) {
347361

348362
this.uri = uri;
363+
this.startByte = startByte;
364+
this.size = size;
365+
}
366+
367+
private boolean isPartialRead() {
368+
return startByte > 0 || (size >= 0 && size != Long.MAX_VALUE);
349369
}
350370

351371
@Override
352372
public InputStream newInputStream() throws N5IOException {
353373

354374
try {
355-
return uri.toURL().openStream();
375+
HttpURLConnection conn = (HttpURLConnection)uri.toURL().openConnection();
376+
if (isPartialRead()) {
377+
conn.setRequestProperty(RANGE, rangeString());
378+
final String acceptRanges = conn.getHeaderField(ACCEPT_RANGE);
379+
if (acceptRanges == null || !acceptRanges.equals(BYTES)) {
380+
conn.disconnect();
381+
conn = (HttpURLConnection)uri.toURL().openConnection();
382+
return ReadData.from(conn.getInputStream()).materialize().slice(startByte, size).inputStream();
383+
}
384+
}
385+
return conn.getInputStream();
386+
} catch (FileNotFoundException e) {
387+
throw new N5Exception.N5NoSuchKeyException("Could not open stream for " + uri, e);
356388
} catch (IOException e) {
357389
throw new N5IOException("Could not open stream for " + uri, e);
358390
}
359391
}
360392

393+
private String rangeString() {
394+
395+
final String lastByte = (size > 0) ? Long.toString(startByte + size - 1) : "";
396+
return String.format("%s=%d-%s", BYTES, startByte, lastByte);
397+
}
398+
361399
@Override
362400
public Reader newReader() throws N5IOException {
363401

@@ -392,4 +430,29 @@ public void close() throws IOException {
392430
}
393431
}
394432

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

0 commit comments

Comments
 (0)