diff --git a/README.md b/README.md
index bc97e9b8..59b25df2 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@ N5 group is not a single file but simply a directory on the file system. Meta-d
1. All directories of the file system are N5 groups.
2. A JSON file `attributes.json` in a directory contains arbitrary attributes. A group without attributes may not have an `attributes.json` file.
-3. The version of this specification is 4.0.0 and is stored in the "n5" attribute of the root group "/".
+3. The version of this specification is 1.0.0 and is stored in the "n5" attribute of the root group "/".
4. A dataset is a group with the mandatory attributes:
* dimensions (e.g. [100, 200, 300]),
* blockSize (e.g. [64, 64, 64]),
@@ -38,7 +38,7 @@ N5 group is not a single file but simply a directory on the file system. Meta-d
* xz with parameters
* preset (integer, default 6).
- Custom compression schemes with arbitrary parameters can be added using [compression annotations](#extensible-compression-schemes), e.g. [N5 Blosc](https://github.com/saalfeldlab/n5-blosc) and [N5 ZStandard](https://github.com/JaneliaSciComp/n5-zstandard/).
+ Custom compression schemes with arbitrary parameters can be added using [compression annotations](#extensible-compression-schemes), e.g. [N5 Blosc](https://github.com/saalfeldlab/n5-blosc).
5. Chunks are stored in a directory hierarchy that enumerates their positive integer position in the chunk grid (e.g. `0/4/1/7` for chunk grid position p=(0, 4, 1, 7)).
6. Datasets are sparse, i.e. there is no guarantee that all chunks of a dataset exist.
7. Chunks cannot be larger than 2GB (231Bytes).
@@ -134,3 +134,4 @@ Custom compression schemes can be implemented using the annotation discovery mec
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.
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.
+Please do not take this project too seriously, we will see where it will get us and report back when more data is available.
diff --git a/pom.xml b/pom.xml
index 5567ec39..e38dc5d5 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.scijavapom-scijava
- 38.0.1
+ 40.0.0
@@ -161,6 +161,18 @@
com.google.code.gsongson
+
+ org.scijava
+ scijava-common
+
+
+ org.apache.commons
+ commons-compress
+
+
+ com.google.guava
+ guava
+
@@ -168,6 +180,17 @@
junittest
+
+ org.janelia.saalfeldlab
+ n5-universe
+
+
+ org.janelia.saalfeldlab
+ n5
+
+
+ test
+ net.imagejij
@@ -194,14 +217,6 @@
${commons-collections4.version}test
-
- org.scijava
- scijava-common
-
-
- org.apache.commons
- commons-compress
-
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/AbstractDataBlock.java b/src/main/java/org/janelia/saalfeldlab/n5/AbstractDataBlock.java
index f1cbc352..59208fcf 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/AbstractDataBlock.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/AbstractDataBlock.java
@@ -25,6 +25,11 @@
*/
package org.janelia.saalfeldlab.n5;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
/**
* Abstract base class for {@link DataBlock} implementations.
*
@@ -63,4 +68,20 @@ public T getData() {
return data;
}
+
+ @Override
+ public void readData(final DataInput input) throws IOException {
+
+ final ByteBuffer buffer = toByteBuffer();
+ input.readFully(buffer.array());
+ readData(buffer);
+ }
+
+ @Override
+ public void writeData(final DataOutput output) throws IOException {
+
+ final ByteBuffer buffer = toByteBuffer();
+ output.write(buffer.array());
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/BlockParameters.java b/src/main/java/org/janelia/saalfeldlab/n5/BlockParameters.java
new file mode 100644
index 00000000..65a21497
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/BlockParameters.java
@@ -0,0 +1,11 @@
+package org.janelia.saalfeldlab.n5;
+
+public interface BlockParameters {
+
+ public long[] getDimensions();
+
+ public int getNumDimensions();
+
+ public int[] getBlockSize();
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/ByteArrayDataBlock.java b/src/main/java/org/janelia/saalfeldlab/n5/ByteArrayDataBlock.java
index 5717ad2e..4610811d 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/ByteArrayDataBlock.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/ByteArrayDataBlock.java
@@ -25,6 +25,8 @@
*/
package org.janelia.saalfeldlab.n5;
+import java.io.DataInput;
+import java.io.IOException;
import java.nio.ByteBuffer;
public class ByteArrayDataBlock extends AbstractDataBlock {
@@ -47,6 +49,12 @@ public void readData(final ByteBuffer buffer) {
buffer.get(getData());
}
+ @Override
+ public void readData(final DataInput inputStream) throws IOException {
+
+ inputStream.readFully(data);
+ }
+
@Override
public int getNumElements() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/Bzip2Compression.java b/src/main/java/org/janelia/saalfeldlab/n5/Bzip2Compression.java
index 5d3d6161..49a333f3 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/Bzip2Compression.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/Bzip2Compression.java
@@ -32,8 +32,10 @@
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorInputStream;
import org.apache.commons.compress.compressors.bzip2.BZip2CompressorOutputStream;
import org.janelia.saalfeldlab.n5.Compression.CompressionType;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
@CompressionType("bzip2")
+@NameConfig.Name("bzip2")
public class Bzip2Compression implements DefaultBlockReader, DefaultBlockWriter, Compression {
private static final long serialVersionUID = -4873117458390529118L;
@@ -52,17 +54,29 @@ public Bzip2Compression() {
}
@Override
- public InputStream getInputStream(final InputStream in) throws IOException {
+ public InputStream decode(final InputStream in) throws IOException {
return new BZip2CompressorInputStream(in);
}
@Override
- public OutputStream getOutputStream(final OutputStream out) throws IOException {
+ public InputStream getInputStream(final InputStream in) throws IOException {
+
+ return decode(in);
+ }
+
+ @Override
+ public OutputStream encode(final OutputStream out) throws IOException {
return new BZip2CompressorOutputStream(out, blockSize);
}
+ @Override
+ public OutputStream getOutputStream(final OutputStream out) throws IOException {
+
+ return encode(out);
+ }
+
@Override
public Bzip2Compression getReader() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Reader.java b/src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Reader.java
index 812c6ebb..324d242e 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Reader.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Reader.java
@@ -27,7 +27,6 @@
import java.lang.reflect.Type;
-import com.google.gson.JsonSyntaxException;
import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;
import org.janelia.saalfeldlab.n5.cache.N5JsonCache;
import org.janelia.saalfeldlab.n5.cache.N5JsonCacheableContainer;
@@ -35,6 +34,7 @@
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
/**
* {@link N5Reader} implementation through {@link KeyValueAccess} with JSON
@@ -70,7 +70,7 @@ default DatasetAttributes getDatasetAttributes(final String pathName) {
return null;
if (cacheMeta()) {
- attributes = getCache().getAttributes(normalPath, N5KeyValueReader.ATTRIBUTES_JSON);
+ attributes = getCache().getAttributes(normalPath, getAttributesKey());
} else {
attributes = GsonKeyValueN5Reader.super.getAttributes(normalPath);
}
@@ -96,7 +96,7 @@ default T getAttribute(
final JsonElement attributes;
if (cacheMeta()) {
- attributes = getCache().getAttributes(normalPathName, N5KeyValueReader.ATTRIBUTES_JSON);
+ attributes = getCache().getAttributes(normalPathName, getAttributesKey());
} else {
attributes = GsonKeyValueN5Reader.super.getAttributes(normalPathName);
}
@@ -117,7 +117,7 @@ default T getAttribute(
final String normalizedAttributePath = N5URI.normalizeAttributePath(key);
JsonElement attributes;
if (cacheMeta()) {
- attributes = getCache().getAttributes(normalPathName, N5KeyValueReader.ATTRIBUTES_JSON);
+ attributes = getCache().getAttributes(normalPathName, getAttributesKey());
} else {
attributes = GsonKeyValueN5Reader.super.getAttributes(normalPathName);
}
@@ -133,7 +133,7 @@ default boolean exists(final String pathName) {
final String normalPathName = N5URI.normalizeGroupPath(pathName);
if (cacheMeta())
- return getCache().isGroup(normalPathName, N5KeyValueReader.ATTRIBUTES_JSON);
+ return getCache().isGroup(normalPathName, getAttributesKey());
else {
return existsFromContainer(normalPathName, null);
}
@@ -176,7 +176,7 @@ default boolean datasetExists(final String pathName) throws N5IOException {
final String normalPathName = N5URI.normalizeGroupPath(pathName);
if (cacheMeta()) {
- return getCache().isDataset(normalPathName, N5KeyValueReader.ATTRIBUTES_JSON);
+ return getCache().isDataset(normalPathName, getAttributesKey());
}
return isDatasetFromContainer(normalPathName);
}
@@ -208,7 +208,7 @@ default JsonElement getAttributes(final String pathName) throws N5IOException {
/* If cached, return the cache */
if (cacheMeta()) {
- return getCache().getAttributes(groupPath, N5KeyValueReader.ATTRIBUTES_JSON);
+ return getCache().getAttributes(groupPath, getAttributesKey());
} else {
return GsonKeyValueN5Reader.super.getAttributes(groupPath);
}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Writer.java b/src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Writer.java
index d95f6345..9d509051 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Writer.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Writer.java
@@ -59,9 +59,9 @@ default void createGroup(final String path) throws N5Exception {
// else if exists is true (then a dataset is present) so throw an exception to avoid
// overwriting / invalidating existing data
if (cacheMeta()) {
- if (getCache().isGroup(normalPath, N5KeyValueReader.ATTRIBUTES_JSON))
+ if (getCache().isGroup(normalPath, getAttributesKey()))
return;
- else if (getCache().exists(normalPath, N5KeyValueReader.ATTRIBUTES_JSON)) {
+ else if (getCache().exists(normalPath, getAttributesKey())) {
throw new N5Exception("Can't make a group on existing path.");
}
}
@@ -88,8 +88,8 @@ else if (getCache().exists(normalPath, N5KeyValueReader.ATTRIBUTES_JSON)) {
for (final String child : pathParts) {
final String childPath = parent.isEmpty() ? child : parent + "/" + child;
- getCache().initializeNonemptyCache(childPath, N5KeyValueReader.ATTRIBUTES_JSON);
- getCache().updateCacheInfo(childPath, N5KeyValueReader.ATTRIBUTES_JSON);
+ getCache().initializeNonemptyCache(childPath, getAttributesKey());
+ getCache().updateCacheInfo(childPath, getAttributesKey());
// only add if the parent exists and has children cached already
if (parent != null && !child.isEmpty())
@@ -130,7 +130,7 @@ default void writeAndCacheAttributes(
nullRespectingAttributes = getGson().toJsonTree(attributes);
}
/* Update the cache, and write to the writer */
- getCache().updateCacheInfo(normalGroupPath, N5KeyValueReader.ATTRIBUTES_JSON, nullRespectingAttributes);
+ getCache().updateCacheInfo(normalGroupPath, getAttributesKey(), nullRespectingAttributes);
}
}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/Compression.java b/src/main/java/org/janelia/saalfeldlab/n5/Compression.java
index df0ca49e..2e8b9cdf 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/Compression.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/Compression.java
@@ -25,6 +25,9 @@
*/
package org.janelia.saalfeldlab.n5;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.io.Serializable;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
@@ -32,14 +35,19 @@
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
+import org.janelia.saalfeldlab.n5.codec.Codec;
import org.scijava.annotations.Indexable;
/**
+ * Deprecated: {@link Compression}s are no longer a special case.
+ *
+ * Use {@link Codec.BytesCodec} for implementing compressors
+ *
* Compression scheme interface.
*
* @author Stephan Saalfeld
*/
-public interface Compression extends Serializable {
+public interface Compression extends Serializable, Codec.BytesCodec {
/**
* Annotation for runtime discovery of compression schemes.
@@ -49,7 +57,7 @@ public interface Compression extends Serializable {
@Inherited
@Target(ElementType.TYPE)
@Indexable
- public static @interface CompressionType {
+ @interface CompressionType {
String value();
}
@@ -61,9 +69,10 @@ public interface Compression extends Serializable {
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Target(ElementType.FIELD)
- public static @interface CompressionParameter {}
+ @interface CompressionParameter {}
- public default String getType() {
+ @Override
+ default String getType() {
final CompressionType compressionType = getClass().getAnnotation(CompressionType.class);
if (compressionType == null)
@@ -72,7 +81,29 @@ public default String getType() {
return compressionType.value();
}
- public BlockReader getReader();
- public BlockWriter getWriter();
+ BlockReader getReader();
+
+ BlockWriter getWriter();
+
+ /**
+ * Decode an {@link InputStream}.
+ *
+ * @param in
+ * input stream
+ * @return the decoded input stream
+ */
+ @Override
+ InputStream decode(InputStream in) throws IOException;
+
+ /**
+ * Encode an {@link OutputStream}.
+ *
+ * @param out
+ * the output stream
+ * @return the encoded output stream
+ */
+ @Override
+ OutputStream encode(OutputStream out) throws IOException;
+
}
\ No newline at end of file
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/DataBlock.java b/src/main/java/org/janelia/saalfeldlab/n5/DataBlock.java
index 3d9dc92a..5ccdbbaf 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/DataBlock.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/DataBlock.java
@@ -25,6 +25,9 @@
*/
package org.janelia.saalfeldlab.n5;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
import java.nio.ByteBuffer;
/**
@@ -94,6 +97,10 @@ public interface DataBlock {
*/
public void readData(final ByteBuffer buffer);
+ public void readData(final DataInput inputStream) throws IOException;
+
+ public void writeData(final DataOutput output) throws IOException;
+
/**
* Returns the number of elements in this {@link DataBlock}. This number is
* not necessarily equal {@link #getNumElements(int[])
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java b/src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java
index f4aea9fe..d2cfa4a5 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java
@@ -1,33 +1,27 @@
-/**
- * Copyright (c) 2017, Stephan Saalfeld
- * All rights reserved.
- *
- * 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 OWNER 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.
- */
package org.janelia.saalfeldlab.n5;
import java.io.Serializable;
+import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.HashMap;
+import java.util.stream.Stream;
+
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.Codec.ArrayCodec;
+import org.janelia.saalfeldlab.n5.codec.Codec.BytesCodec;
+import org.janelia.saalfeldlab.n5.codec.N5BlockCodec;
+import org.janelia.saalfeldlab.n5.shard.ShardParameters;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
/**
* Mandatory dataset attributes:
@@ -36,20 +30,26 @@
*
long[] : dimensions
*
int[] : blockSize
*
{@link DataType} : dataType
- *
{@link Compression} : compression
+ *
{@link Codec}... : encode/decode routines
*
*
* @author Stephan Saalfeld
- *
*/
-public class DatasetAttributes implements Serializable {
+//TODO Caleb: try to delete ShardParameters?
+public class DatasetAttributes implements BlockParameters, ShardParameters, Serializable {
private static final long serialVersionUID = -4521467080388947553L;
public static final String DIMENSIONS_KEY = "dimensions";
public static final String BLOCK_SIZE_KEY = "blockSize";
+ public static final String SHARD_SIZE_KEY = "shardSize";
public static final String DATA_TYPE_KEY = "dataType";
public static final String COMPRESSION_KEY = "compression";
+ public static final String CODEC_KEY = "codecs";
+
+ public static final String[] N5_DATASET_ATTRIBUTES = new String[]{
+ DIMENSIONS_KEY, BLOCK_SIZE_KEY, DATA_TYPE_KEY, COMPRESSION_KEY, CODEC_KEY
+ };
/* version 0 */
protected static final String compressionTypeKey = "compressionType";
@@ -57,38 +57,132 @@ public class DatasetAttributes implements Serializable {
private final long[] dimensions;
private final int[] blockSize;
private final DataType dataType;
- private final Compression compression;
+ private final ArrayCodec arrayCodec;
+ private final BytesCodec[] byteCodecs;
+ @Nullable private final int[] shardSize;
+ /**
+ * Constructs a DatasetAttributes instance with specified dimensions, block size, data type,
+ * and array of codecs.
+ *
+ * @param dimensions the dimensions of the dataset
+ * @param blockSize the size of the blocks in the dataset
+ * @param dataType the data type of the dataset
+ * @param codecs the codecs used encode/decode the data
+ */
public DatasetAttributes(
final long[] dimensions,
+ @Nullable final int[] shardSize,
final int[] blockSize,
final DataType dataType,
- final Compression compression) {
+ final Codec... codecs) {
this.dimensions = dimensions;
+ this.shardSize = shardSize;
this.blockSize = blockSize;
this.dataType = dataType;
- this.compression = compression;
+ if (codecs == null || codecs.length == 0) {
+ byteCodecs = new BytesCodec[]{};
+ arrayCodec = new N5BlockCodec();
+ } else if (codecs.length == 1 && codecs[0] instanceof Compression) {
+ final BytesCodec compression = (BytesCodec)codecs[0];
+ byteCodecs = compression instanceof RawCompression ? new BytesCodec[]{} : new BytesCodec[]{compression};
+ arrayCodec = new N5BlockCodec();
+ } else {
+ if (!(codecs[0] instanceof ArrayCodec))
+ throw new N5Exception("Expected first element of codecs to be ArrayCodec, but was: " + codecs[0].getClass());
+
+ if (Arrays.stream(codecs).filter(c -> c instanceof ArrayCodec).count() > 1)
+ throw new N5Exception("Multiple ArrayCodecs found. Only one is allowed.");
+
+ arrayCodec = (ArrayCodec)codecs[0];
+ byteCodecs = Stream.of(codecs)
+ .skip(1)
+ .filter(c -> !(c instanceof RawCompression))
+ .filter(c -> c instanceof BytesCodec)
+ .toArray(BytesCodec[]::new);
+ }
+
+
}
+ /**
+ * Constructs a DatasetAttributes instance with specified dimensions, block size, data type,
+ * and array of codecs.
+ *
+ * @param dimensions the dimensions of the dataset
+ * @param blockSize the size of the blocks in the dataset
+ * @param dataType the data type of the dataset
+ * @param codecs the codecs used encode/decode the data
+ */
+ public DatasetAttributes(
+ final long[] dimensions,
+ final int[] blockSize,
+ final DataType dataType,
+ final Codec... codecs) {
+ this( dimensions, null, blockSize, dataType, codecs );
+ }
+
+ /**
+ * Deprecated. {@link Compression} are {@link Codec}. Use {@code Code...} constructor instead
+ * Constructs a DatasetAttributes instance with specified dimensions, block size, data type,
+ * and compression scheme. This constructor is deprecated and redirects to another constructor
+ * with codec support.
+ *
+ * @param dimensions the dimensions of the dataset
+ * @param blockSize the size of the blocks in the dataset
+ * @param dataType the data type of the dataset
+ * @param compression the compression scheme used for storing the dataset
+ */
+ @Deprecated
+ public DatasetAttributes(
+ final long[] dimensions,
+ final int[] blockSize,
+ final DataType dataType,
+ final Compression compression) {
+
+ this(dimensions, blockSize, dataType, (Codec)compression);
+ }
+
+ @Override
public long[] getDimensions() {
return dimensions;
}
+ @Override
public int getNumDimensions() {
return dimensions.length;
}
+ @Override
+ @CheckForNull
+ public int[] getShardSize() {
+
+ return shardSize;
+ }
+
+ @Override
public int[] getBlockSize() {
return blockSize;
}
+ /**
+ * Deprecated. {@link Compression} is no longer a special case. prefer to reference {@link #getCodecs()}
+ * Will return {@link RawCompression} if no compression is otherwise provided, for legacy compatibility.
+ *
+ * @return compression Codec, if one was present
+ */
+ @Deprecated
public Compression getCompression() {
- return compression;
+ return Arrays.stream(byteCodecs)
+ .filter(it -> it instanceof Compression)
+ .map(it -> (Compression)it)
+ .findFirst()
+ .orElse(new RawCompression());
}
public DataType getDataType() {
@@ -96,47 +190,136 @@ public DataType getDataType() {
return dataType;
}
+ public ArrayCodec getArrayCodec() {
+
+ return arrayCodec;
+ }
+
+ public BytesCodec[] getCodecs() {
+
+ return byteCodecs;
+ }
+
+ /**
+ * Deprecated in favor of {@link DatasetAttributesAdapter} for serialization
+ *
+ * @return serilizable properties of {@link DatasetAttributes}
+ */
+ @Deprecated
public HashMap asMap() {
final HashMap map = new HashMap<>();
map.put(DIMENSIONS_KEY, dimensions);
map.put(BLOCK_SIZE_KEY, blockSize);
map.put(DATA_TYPE_KEY, dataType);
- map.put(COMPRESSION_KEY, compression);
+ map.put(COMPRESSION_KEY, getCompression());
return map;
}
- static DatasetAttributes from(
- final long[] dimensions,
- final DataType dataType,
- int[] blockSize,
- Compression compression,
- final String compressionVersion0Name) {
- if (blockSize == null)
- blockSize = Arrays.stream(dimensions).mapToInt(a -> (int)a).toArray();
+ protected Codec[] concatenateCodecs() {
+
+ final Codec[] allCodecs = new Codec[byteCodecs.length + 1];
+ allCodecs[0] = arrayCodec;
+ for (int i = 0; i < byteCodecs.length; i++)
+ allCodecs[i + 1] = byteCodecs[i];
+
+ return allCodecs;
+ }
+
+ private static DatasetAttributesAdapter adapter = null;
+ public static DatasetAttributesAdapter getJsonAdapter() {
+ if (adapter == null) {
+ adapter = new DatasetAttributesAdapter();
+ }
+ return adapter;
+ }
+
+ public static class InvalidN5DatasetException extends N5Exception {
+
+ public InvalidN5DatasetException(String dataset, String reason, Throwable cause) {
+
+ this(String.format("Invalid dataset %s: %s", dataset, reason), cause);
+ }
+
+ public InvalidN5DatasetException(String message, Throwable cause) {
+
+ super(message, cause);
+ }
+ }
+ public static class DatasetAttributesAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override public DatasetAttributes deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+
+ if (json == null || !json.isJsonObject()) return null;
+ final JsonObject obj = json.getAsJsonObject();
+ final boolean validKeySet = obj.has(DIMENSIONS_KEY)
+ && obj.has(BLOCK_SIZE_KEY)
+ && obj.has(DATA_TYPE_KEY)
+ && (obj.has(CODEC_KEY) || obj.has(COMPRESSION_KEY) || obj.has(compressionTypeKey));
+
+ if (!validKeySet)
+ return null;
+
+ final long[] dimensions = context.deserialize(obj.get(DIMENSIONS_KEY), long[].class);
+ final int[] blockSize = context.deserialize(obj.get(BLOCK_SIZE_KEY), int[].class);
+
+ int[] shardSize = null;
+ if (obj.has(SHARD_SIZE_KEY))
+ shardSize = context.deserialize(obj.get(SHARD_SIZE_KEY), int[].class);
+
+ final DataType dataType = context.deserialize(obj.get(DATA_TYPE_KEY), DataType.class);
+
+
+ final Codec[] codecs;
+ if (obj.has(CODEC_KEY)) {
+ codecs = context.deserialize(obj.get(CODEC_KEY), Codec[].class);
+ } else if (obj.has(COMPRESSION_KEY)) {
+ final Compression compression = CompressionAdapter.getJsonAdapter().deserialize(obj.get(COMPRESSION_KEY), Compression.class, context);
+ final N5BlockCodec n5BlockCodec = dataType == DataType.UINT8 || dataType == DataType.INT8 ? new N5BlockCodec(null) : new N5BlockCodec();
+ codecs = new Codec[]{compression, n5BlockCodec};
+ } else if (obj.has(compressionTypeKey)) {
+ final Compression compression = getCompressionVersion0(obj.get(compressionTypeKey).getAsString());
+ final N5BlockCodec n5BlockCodec = dataType == DataType.UINT8 || dataType == DataType.INT8 ? new N5BlockCodec(null) : new N5BlockCodec();
+ codecs = new Codec[]{compression, n5BlockCodec};
+ } else {
+ return null;
+ }
+ return new DatasetAttributes(dimensions, shardSize, blockSize, dataType, codecs);
+ }
+
+ @Override public JsonElement serialize(DatasetAttributes src, Type typeOfSrc, JsonSerializationContext context) {
+
+ final JsonObject obj = new JsonObject();
+ obj.add(DIMENSIONS_KEY, context.serialize(src.dimensions));
+ obj.add(BLOCK_SIZE_KEY, context.serialize(src.blockSize));
+
+ //TODO Caleb: Type Hierarchy Adapter for extensions?
+ final int[] shardSize = src.getShardSize();
+ if (shardSize != null) {
+ obj.add(SHARD_SIZE_KEY, context.serialize(shardSize));
+ }
+
+ obj.add(DATA_TYPE_KEY, context.serialize(src.dataType));
+ obj.add(CODEC_KEY, context.serialize(src.concatenateCodecs()));
+
+ return obj;
+ }
+ private static Compression getCompressionVersion0(final String compressionVersion0Name) {
- /* version 0 */
- if (compression == null) {
switch (compressionVersion0Name) {
case "raw":
- compression = new RawCompression();
- break;
+ return new RawCompression();
case "gzip":
- compression = new GzipCompression();
- break;
+ return new GzipCompression();
case "bzip2":
- compression = new Bzip2Compression();
- break;
+ return new Bzip2Compression();
case "lz4":
- compression = new Lz4Compression();
- break;
+ return new Lz4Compression();
case "xz":
- compression = new XzCompression();
- break;
+ return new XzCompression();
}
+ return null;
}
-
- return new DatasetAttributes(dimensions, blockSize, dataType, compression);
}
}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/DefaultBlockReader.java b/src/main/java/org/janelia/saalfeldlab/n5/DefaultBlockReader.java
index 58c59780..0a49fc19 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/DefaultBlockReader.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/DefaultBlockReader.java
@@ -30,6 +30,10 @@
import java.io.InputStream;
import java.nio.ByteBuffer;
+import org.janelia.saalfeldlab.n5.codec.Codec.ArrayCodec;
+import org.janelia.saalfeldlab.n5.codec.Codec.BytesCodec;
+import org.janelia.saalfeldlab.n5.codec.Codec.DataBlockInputStream;
+
/**
* Default implementation of {@link BlockReader}.
*
@@ -45,12 +49,9 @@ public default > void read(
final B dataBlock,
final InputStream in) throws IOException {
- final ByteBuffer buffer = dataBlock.toByteBuffer();
- try (final InputStream inflater = getInputStream(in)) {
- final DataInputStream dis = new DataInputStream(inflater);
- dis.readFully(buffer.array());
- }
- dataBlock.readData(buffer);
+ // do not try with this input stream because subsequent block reads may happen if the stream points to a shard
+ final InputStream inflater = getInputStream(in);
+ readFromStream(dataBlock, inflater);
}
/**
@@ -71,28 +72,28 @@ public static DataBlock> readBlock(
final DatasetAttributes datasetAttributes,
final long[] gridPosition) throws IOException {
- final DataInputStream dis = new DataInputStream(in);
- final short mode = dis.readShort();
- final int numElements;
- final DataBlock> dataBlock;
- if (mode != 2) {
- final int nDim = dis.readShort();
- final int[] blockSize = new int[nDim];
- for (int d = 0; d < nDim; ++d)
- blockSize[d] = dis.readInt();
- if (mode == 0) {
- numElements = DataBlock.getNumElements(blockSize);
- } else {
- numElements = dis.readInt();
- }
- dataBlock = datasetAttributes.getDataType().createDataBlock(blockSize, gridPosition, numElements);
- } else {
- numElements = dis.readInt();
- dataBlock = datasetAttributes.getDataType().createDataBlock(null, gridPosition, numElements);
+ final BytesCodec[] codecs = datasetAttributes.getCodecs();
+ final ArrayCodec arrayCodec = datasetAttributes.getArrayCodec();
+ final DataBlockInputStream dataBlockStream = arrayCodec.decode(datasetAttributes, gridPosition, in);
+
+ InputStream stream = dataBlockStream;
+ for (final BytesCodec codec : codecs) {
+ stream = codec.decode(stream);
}
- final BlockReader reader = datasetAttributes.getCompression().getReader();
- reader.read(dataBlock, in);
+ final DataBlock> dataBlock = dataBlockStream.allocateDataBlock();
+ dataBlock.readData(dataBlockStream.getDataInput(stream));
+ stream.close();
+
return dataBlock;
}
+
+ public static > void readFromStream(final B dataBlock, final InputStream in) throws IOException {
+
+ final ByteBuffer buffer = dataBlock.toByteBuffer();
+ final DataInputStream dis = new DataInputStream(in);
+ dis.readFully(buffer.array());
+ dataBlock.readData(buffer);
+ }
+
}
\ No newline at end of file
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/DefaultBlockWriter.java b/src/main/java/org/janelia/saalfeldlab/n5/DefaultBlockWriter.java
index c53aae2d..5f27c930 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/DefaultBlockWriter.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/DefaultBlockWriter.java
@@ -25,11 +25,16 @@
*/
package org.janelia.saalfeldlab.n5;
-import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
+import org.janelia.saalfeldlab.n5.codec.Codec.ArrayCodec;
+import org.janelia.saalfeldlab.n5.codec.Codec.BytesCodec;
+import org.janelia.saalfeldlab.n5.codec.Codec.DataBlockOutputStream;
+
+import static org.janelia.saalfeldlab.n5.codec.Codec.encode;
+
/**
* Default implementation of {@link BlockWriter}.
*
@@ -70,29 +75,19 @@ public static void writeBlock(
final DatasetAttributes datasetAttributes,
final DataBlock dataBlock) throws IOException {
- final DataOutputStream dos = new DataOutputStream(out);
-
- final int mode;
- if (datasetAttributes.getDataType() == DataType.OBJECT || dataBlock.getSize() == null)
- mode = 2;
- else if (dataBlock.getNumElements() == DataBlock.getNumElements(dataBlock.getSize()))
- mode = 0;
- else
- mode = 1;
- dos.writeShort(mode);
+ final BytesCodec[] codecs = datasetAttributes.getCodecs();
+ final ArrayCodec arrayCodec = datasetAttributes.getArrayCodec();
+ final DataBlockOutputStream dataBlockOutput = arrayCodec.encode(datasetAttributes, dataBlock, out);
- if (mode != 2) {
- dos.writeShort(datasetAttributes.getNumDimensions());
- for (final int size : dataBlock.getSize())
- dos.writeInt(size);
- }
+ OutputStream stream = encode(dataBlockOutput, codecs);
- if (mode != 0)
- dos.writeInt(dataBlock.getNumElements());
+ dataBlock.writeData(dataBlockOutput.getDataOutput(stream));
+ stream.close();
+ }
- dos.flush();
+ public static void writeFromStream(final DataBlock dataBlock, final OutputStream out) throws IOException {
- final BlockWriter writer = datasetAttributes.getCompression().getWriter();
- writer.write(dataBlock, out);
+ final ByteBuffer buffer = dataBlock.toByteBuffer();
+ out.write(buffer.array());
}
}
\ No newline at end of file
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/DoubleArrayDataBlock.java b/src/main/java/org/janelia/saalfeldlab/n5/DoubleArrayDataBlock.java
index 426c7944..0240e6fa 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/DoubleArrayDataBlock.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/DoubleArrayDataBlock.java
@@ -25,6 +25,9 @@
*/
package org.janelia.saalfeldlab.n5;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
import java.nio.ByteBuffer;
public class DoubleArrayDataBlock extends AbstractDataBlock {
@@ -48,6 +51,20 @@ public void readData(final ByteBuffer buffer) {
buffer.asDoubleBuffer().get(data);
}
+ @Override
+ public void readData(final DataInput inputStream) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ data[i] = inputStream.readDouble();
+ }
+
+ @Override
+ public void writeData(final DataOutput output) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ output.writeDouble(data[i]);
+ }
+
@Override
public int getNumElements() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java
index cfb45592..05afb2d1 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java
@@ -25,6 +25,8 @@
*/
package org.janelia.saalfeldlab.n5;
+import org.apache.commons.io.input.BoundedInputStream;
+
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@@ -71,13 +73,35 @@ protected class LockedFileChannel implements LockedChannel {
protected final FileChannel channel;
+ protected final boolean truncate;
+
+ protected long len;
+
protected LockedFileChannel(final String path, final boolean readOnly) throws IOException {
- this(fileSystem.getPath(path), readOnly);
+ this(fileSystem.getPath(path), readOnly, 0, Long.MAX_VALUE);
+ }
+
+ protected LockedFileChannel(final String path, final boolean readOnly, final long startByte, final long size) throws IOException {
+
+ this(fileSystem.getPath(path), readOnly, startByte, size);
}
protected LockedFileChannel(final Path path, final boolean readOnly) throws IOException {
+ this(path, readOnly, 0, Long.MAX_VALUE);
+ }
+
+ protected LockedFileChannel(final Path path, final boolean readOnly, final long startByte, final long size)
+ throws IOException {
+
+
+ final long start = startByte < 0 ? 0L : startByte;
+ len = size < 0 ? Long.MAX_VALUE : size;
+
+ //TODO Caleb: How does this handle if manually overwriting the entire file? (e.g. len > file size)
+ truncate = (start == 0 && len == Long.MAX_VALUE);
+
final OpenOption[] options;
if (readOnly) {
options = new OpenOption[]{StandardOpenOption.READ};
@@ -96,10 +120,13 @@ protected LockedFileChannel(final Path path, final boolean readOnly) throws IOEx
}
}
+ if (startByte != 0)
+ channel.position(start);
+
for (boolean waiting = true; waiting;) {
waiting = false;
try {
- channel.lock(0L, Long.MAX_VALUE, readOnly);
+ channel.lock(start, len, readOnly);
} catch (final OverlappingFileLockException e) {
waiting = true;
try {
@@ -112,6 +139,12 @@ protected LockedFileChannel(final Path path, final boolean readOnly) throws IOEx
}
}
+ @Override
+ public long size() throws IOException {
+
+ return channel.size();
+ }
+
@Override
public Reader newReader() throws IOException {
@@ -121,20 +154,24 @@ public Reader newReader() throws IOException {
@Override
public Writer newWriter() throws IOException {
- channel.truncate(0);
+ if (truncate)
+ channel.truncate(0);
+
return Channels.newWriter(channel, StandardCharsets.UTF_8.name());
}
@Override
public InputStream newInputStream() throws IOException {
- return Channels.newInputStream(channel);
+ return BoundedInputStream.builder().setInputStream(Channels.newInputStream(channel)).setMaxCount(len).get();
}
@Override
public OutputStream newOutputStream() throws IOException {
- channel.truncate(0);
+ if (truncate)
+ channel.truncate(0);
+
return Channels.newOutputStream(channel);
}
@@ -162,7 +199,18 @@ public LockedFileChannel lockForReading(final String normalPath) throws IOExcept
try {
return new LockedFileChannel(normalPath, true);
- } catch (NoSuchFileException e) {
+ } catch (final NoSuchFileException e) {
+ throw new N5Exception.N5NoSuchKeyException("No such file", e);
+ }
+ }
+
+ @Override
+ public LockedFileChannel lockForReading(final String normalPath, final long startByte, final long size)
+ throws IOException {
+
+ try {
+ return new LockedFileChannel(normalPath, true, startByte, size);
+ } catch (final NoSuchFileException e) {
throw new N5Exception.N5NoSuchKeyException("No such file", e);
}
}
@@ -173,11 +221,18 @@ public LockedFileChannel lockForWriting(final String normalPath) throws IOExcept
return new LockedFileChannel(normalPath, false);
}
+ @Override
+ public LockedFileChannel lockForWriting(final String normalPath, final long startByte, final long size)
+ throws IOException {
+
+ return new LockedFileChannel(normalPath, false, startByte, size);
+ }
+
public LockedFileChannel lockForReading(final Path path) throws IOException {
try {
return new LockedFileChannel(path, true);
- } catch (NoSuchFileException e) {
+ } catch (final NoSuchFileException e) {
throw new N5Exception.N5NoSuchKeyException("No such file", e);
}
}
@@ -208,6 +263,16 @@ public boolean exists(final String normalPath) {
return Files.exists(path);
}
+ @Override
+ public long size(final String normalPath) throws IOException {
+
+ try {
+ return Files.size(fileSystem.getPath(normalPath));
+ } catch (NoSuchFileException e) {
+ throw new N5Exception.N5NoSuchKeyException("No such file", e);
+ }
+ }
+
@Override
public String[] listDirectories(final String normalPath) throws IOException {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/FloatArrayDataBlock.java b/src/main/java/org/janelia/saalfeldlab/n5/FloatArrayDataBlock.java
index b8d30999..a2bc2c69 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/FloatArrayDataBlock.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/FloatArrayDataBlock.java
@@ -25,6 +25,9 @@
*/
package org.janelia.saalfeldlab.n5;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
import java.nio.ByteBuffer;
public class FloatArrayDataBlock extends AbstractDataBlock {
@@ -48,6 +51,20 @@ public void readData(final ByteBuffer buffer) {
buffer.asFloatBuffer().get(data);
}
+ @Override
+ public void readData(final DataInput inputStream) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ data[i] = inputStream.readFloat();
+ }
+
+ @Override
+ public void writeData(final DataOutput output) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ output.writeFloat(data[i]);
+ }
+
@Override
public int getNumElements() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Reader.java b/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Reader.java
index 4e728eb1..78e10811 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Reader.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Reader.java
@@ -25,14 +25,19 @@
*/
package org.janelia.saalfeldlab.n5;
-import java.io.IOException;
-import java.io.UncheckedIOException;
-import java.util.Arrays;
-
-import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;
-
import com.google.gson.Gson;
import com.google.gson.JsonElement;
+import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;
+import org.janelia.saalfeldlab.n5.shard.Shard;
+import org.janelia.saalfeldlab.n5.shard.VirtualShard;
+import org.janelia.saalfeldlab.n5.util.Position;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
/**
* {@link N5Reader} implementation through {@link KeyValueAccess} with JSON
@@ -86,23 +91,56 @@ default JsonElement getAttributes(final String pathName) throws N5Exception {
}
+ @SuppressWarnings({ "unchecked", "rawtypes" })
+ default Shard readShard(
+ final String keyPath,
+ final DatasetAttributes datasetAttributes,
+ long... shardGridPosition) {
+
+ final String path = absoluteDataBlockPath(N5URI.normalizeGroupPath(keyPath), shardGridPosition);
+ return new VirtualShard<>(datasetAttributes, shardGridPosition, getKeyValueAccess(), path);
+ }
+
@Override
- default DataBlock> readBlock(
+ default DataBlock readBlock(
final String pathName,
final DatasetAttributes datasetAttributes,
final long... gridPosition) throws N5Exception {
- final String path = absoluteDataBlockPath(N5URI.normalizeGroupPath(pathName), gridPosition);
+ final long[] keyPos = datasetAttributes.getArrayCodec().getPositionForBlock(datasetAttributes, gridPosition);
+ final String keyPath = absoluteDataBlockPath(N5URI.normalizeGroupPath(pathName), keyPos);
- try (final LockedChannel lockedChannel = getKeyValueAccess().lockForReading(path)) {
- return DefaultBlockReader.readBlock(lockedChannel.newInputStream(), datasetAttributes, gridPosition);
- } catch (final N5Exception.N5NoSuchKeyException e) {
- return null;
- } catch (final IOException | UncheckedIOException e) {
- throw new N5IOException(
- "Failed to read block " + Arrays.toString(gridPosition) + " from dataset " + path,
- e);
+ return datasetAttributes.getArrayCodec().readBlock(
+ getKeyValueAccess(),
+ keyPath,
+ datasetAttributes,
+ gridPosition
+ );
+ }
+
+ @Override
+ default List> readBlocks(
+ final String pathName,
+ final DatasetAttributes datasetAttributes,
+ final List blockPositions) throws N5Exception {
+
+ // TODO which interface should have this implementation?
+ if (datasetAttributes.getShardSize() != null) {
+ /* Group by shard position */
+ final Map> shardBlockMap = datasetAttributes.groupBlockPositions(blockPositions);
+ final ArrayList> blocks = new ArrayList<>();
+ for( Entry> e : shardBlockMap.entrySet()) {
+
+ final Shard shard = readShard(pathName, datasetAttributes, e.getKey().get());
+
+ for (final long[] blkPosition : e.getValue()) {
+ blocks.add(shard.getBlock(blkPosition));
+ }
+ }
+
+ return blocks;
}
+ return GsonN5Reader.super.readBlocks(pathName, datasetAttributes, blockPositions);
}
@Override
@@ -118,6 +156,9 @@ default String[] list(final String pathName) throws N5Exception {
/**
* Constructs the path for a data block in a dataset at a given grid
* position.
+ *
+ * If the gridPosition passed in refers to shard position
+ * in a sharded dataset, this will return the path to the shard key
*
* The returned path is
*
@@ -145,6 +186,8 @@ default String absoluteDataBlockPath(
return getKeyValueAccess().compose(getURI(), components);
}
+
+
/**
* Constructs the absolute path (in terms of this store) for the group or
* dataset.
@@ -168,6 +211,6 @@ default String absoluteGroupPath(final String normalGroupPath) {
*/
default String absoluteAttributesPath(final String normalPath) {
- return getKeyValueAccess().compose(getURI(), normalPath, N5KeyValueReader.ATTRIBUTES_JSON);
+ return getKeyValueAccess().compose(getURI(), normalPath, getAttributesKey());
}
}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Writer.java b/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Writer.java
index 38c754be..b5e4cdf2 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Writer.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Writer.java
@@ -26,18 +26,26 @@
package org.janelia.saalfeldlab.n5;
import java.io.IOException;
+import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
+import java.util.stream.Collectors;
-import com.google.gson.JsonSyntaxException;
import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;
+import org.janelia.saalfeldlab.n5.shard.InMemoryShard;
+import org.janelia.saalfeldlab.n5.shard.Shard;
+import org.janelia.saalfeldlab.n5.shard.ShardParameters;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
+import com.google.gson.JsonSyntaxException;
+import org.janelia.saalfeldlab.n5.shard.VirtualShard;
+import org.janelia.saalfeldlab.n5.util.Position;
/**
* Default implementation of {@link N5Writer} with JSON attributes parsed with
@@ -74,7 +82,7 @@ default void createGroup(final String path) throws N5Exception {
try {
getKeyValueAccess().createDirectories(absoluteGroupPath(normalPath));
} catch (final IOException | UncheckedIOException e) {
- throw new N5Exception.N5IOException("Failed to create group " + path, e);
+ throw new N5IOException("Failed to create group " + path, e);
}
}
@@ -98,7 +106,7 @@ default void writeAttributes(
try (final LockedChannel lock = getKeyValueAccess().lockForWriting(absoluteAttributesPath(normalGroupPath))) {
GsonUtils.writeAttributes(lock.newWriter(), attributes, getGson());
} catch (final IOException | UncheckedIOException e) {
- throw new N5Exception.N5IOException("Failed to write attributes into " + normalGroupPath, e);
+ throw new N5IOException("Failed to write attributes into " + normalGroupPath, e);
}
}
@@ -193,7 +201,7 @@ default T removeAttribute(final String pathName, final String key, final Cla
throw new N5Exception.N5ClassCastException(e);
}
if (obj != null) {
- writeAttributes(normalPath, attributes);
+ setAttributes(normalPath, attributes);
}
return obj;
}
@@ -210,19 +218,65 @@ default boolean removeAttributes(final String pathName, final List attri
return removed;
}
+ @Override default void writeBlocks(
+ final String datasetPath,
+ final DatasetAttributes datasetAttributes,
+ final DataBlock... dataBlocks) throws N5Exception {
+
+ if (datasetAttributes.getShardSize() != null) {
+
+ /* Group blocks by shard index */
+ final Map>> shardBlockMap = datasetAttributes.groupBlocks(
+ Arrays.stream(dataBlocks).collect(Collectors.toList()));
+
+ for( final Entry>> e : shardBlockMap.entrySet()) {
+
+ final long[] shardPosition = e.getKey().get();
+ final Shard currentShard = readShard(datasetPath, datasetAttributes,
+ shardPosition);
+
+ final InMemoryShard newShard = InMemoryShard.fromShard(currentShard);
+ for( DataBlock blk : e.getValue())
+ newShard.addBlock(blk);
+
+ writeShard(datasetPath, datasetAttributes, newShard);
+ }
+
+ } else {
+ GsonN5Writer.super.writeBlocks(datasetPath, datasetAttributes, dataBlocks);
+ }
+ }
+
@Override
default void writeBlock(
final String path,
final DatasetAttributes datasetAttributes,
final DataBlock dataBlock) throws N5Exception {
- final String blockPath = absoluteDataBlockPath(N5URI.normalizeGroupPath(path), dataBlock.getGridPosition());
- try (final LockedChannel lock = getKeyValueAccess().lockForWriting(blockPath)) {
- DefaultBlockWriter.writeBlock(lock.newOutputStream(), datasetAttributes, dataBlock);
+ final long[] keyPos = datasetAttributes.getArrayCodec().getPositionForBlock(datasetAttributes, dataBlock);
+ final String keyPath = absoluteDataBlockPath(N5URI.normalizeGroupPath(path), keyPos);
+
+ datasetAttributes.getArrayCodec().writeBlock(
+ getKeyValueAccess(),
+ keyPath,
+ datasetAttributes,
+ dataBlock);
+ }
+
+ @Override
+ default void writeShard(
+ final String path,
+ final DatasetAttributes datasetAttributes,
+ final Shard shard) throws N5Exception {
+
+ final String shardPath = absoluteDataBlockPath(N5URI.normalizeGroupPath(path), shard.getGridPosition());
+ try (final LockedChannel lock = getKeyValueAccess().lockForWriting(shardPath)) {
+ try (final OutputStream shardOut = lock.newOutputStream()) {
+ InMemoryShard.fromShard(shard).write(shardOut);
+ }
} catch (final IOException | UncheckedIOException e) {
throw new N5IOException(
- "Failed to write block " + Arrays.toString(dataBlock.getGridPosition()) + " into dataset " + path,
- e);
+ "Failed to write shard " + Arrays.toString(shard.getGridPosition()) + " into dataset " + path, e);
}
}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/GsonN5Reader.java b/src/main/java/org/janelia/saalfeldlab/n5/GsonN5Reader.java
index be16ed08..0ba185e3 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/GsonN5Reader.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/GsonN5Reader.java
@@ -28,6 +28,10 @@
import java.lang.reflect.Type;
import java.util.Map;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonParseException;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonSyntaxException;
@@ -40,6 +44,8 @@ public interface GsonN5Reader extends N5Reader {
Gson getGson();
+ public String getAttributesKey();
+
@Override
default Map> listAttributes(final String pathName) throws N5Exception {
@@ -56,30 +62,15 @@ default DatasetAttributes getDatasetAttributes(final String pathName) throws N5E
default DatasetAttributes createDatasetAttributes(final JsonElement attributes) {
- try {
- final long[] dimensions = GsonUtils.readAttribute(attributes, DatasetAttributes.DIMENSIONS_KEY, long[].class, getGson());
- if (dimensions == null) {
- return null;
- }
+ final JsonDeserializationContext context = new JsonDeserializationContext() {
- final DataType dataType = GsonUtils.readAttribute(attributes, DatasetAttributes.DATA_TYPE_KEY, DataType.class, getGson());
- if (dataType == null) {
- return null;
- }
+ @Override public T deserialize(JsonElement json, Type typeOfT) throws JsonParseException {
- final int[] blockSize = GsonUtils.readAttribute(attributes, DatasetAttributes.BLOCK_SIZE_KEY, int[].class, getGson());
- final Compression compression = GsonUtils.readAttribute(attributes, DatasetAttributes.COMPRESSION_KEY, Compression.class, getGson());
-
- /* version 0 */
- final String compressionVersion0Name = compression == null
- ? GsonUtils.readAttribute(attributes, DatasetAttributes.compressionTypeKey, String.class, getGson())
- : null;
+ return getGson().fromJson(json, typeOfT);
+ }
+ };
- return DatasetAttributes.from(dimensions, dataType, blockSize, compression, compressionVersion0Name);
- } catch (JsonSyntaxException | NumberFormatException | ClassCastException e) {
- /* We cannot create a dataset, so return null. */
- return null;
- }
+ return DatasetAttributes.getJsonAdapter().deserialize(attributes, DatasetAttributes.class, context);
}
@Override
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/GsonUtils.java b/src/main/java/org/janelia/saalfeldlab/n5/GsonUtils.java
index f8a5677e..03741476 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/GsonUtils.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/GsonUtils.java
@@ -30,10 +30,14 @@
import java.io.Writer;
import java.lang.reflect.Array;
import java.lang.reflect.Type;
+import java.nio.ByteOrder;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
+import org.janelia.saalfeldlab.n5.codec.RawBytes;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonArray;
@@ -42,6 +46,7 @@
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSyntaxException;
import com.google.gson.reflect.TypeToken;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec;
/**
* Utility class for working with JSON.
@@ -54,6 +59,10 @@ static Gson registerGson(final GsonBuilder gsonBuilder) {
gsonBuilder.registerTypeAdapter(DataType.class, new DataType.JsonAdapter());
gsonBuilder.registerTypeHierarchyAdapter(Compression.class, CompressionAdapter.getJsonAdapter());
+ gsonBuilder.registerTypeHierarchyAdapter(DatasetAttributes.class, DatasetAttributes.getJsonAdapter());
+ gsonBuilder.registerTypeHierarchyAdapter(Codec.class, NameConfigAdapter.getJsonAdapter(Codec.class));
+ gsonBuilder.registerTypeHierarchyAdapter(ByteOrder.class, RawBytes.byteOrderAdapter);
+ gsonBuilder.registerTypeHierarchyAdapter(ShardingCodec.IndexLocation.class, ShardingCodec.indexLocationAdapter);
gsonBuilder.disableHtmlEscaping();
return gsonBuilder.create();
}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/GzipCompression.java b/src/main/java/org/janelia/saalfeldlab/n5/GzipCompression.java
index b691a6d3..b03a4d93 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/GzipCompression.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/GzipCompression.java
@@ -37,16 +37,24 @@
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipParameters;
import org.janelia.saalfeldlab.n5.Compression.CompressionType;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
@CompressionType("gzip")
+@NameConfig.Name("gzip")
public class GzipCompression implements DefaultBlockReader, DefaultBlockWriter, Compression {
private static final long serialVersionUID = 8630847239813334263L;
@CompressionParameter
+ @NameConfig.Parameter
+ //TODO Caleb: How to handle serialization of parameter-less constructor.
+ // For N5 the default is -1.
+ // For zarr the range is 0-9 and is required.
+ // How to map -1 to some default (1?) when serializing to zarr?
private final int level;
@CompressionParameter
+ @NameConfig.Parameter(optional = true)
private final boolean useZlib;
private final transient GzipParameters parameters = new GzipParameters();
@@ -68,7 +76,7 @@ public GzipCompression(final int level, final boolean useZlib) {
}
@Override
- public InputStream getInputStream(final InputStream in) throws IOException {
+ public InputStream decode(InputStream in) throws IOException {
if (useZlib) {
return new InflaterInputStream(in);
@@ -78,7 +86,13 @@ public InputStream getInputStream(final InputStream in) throws IOException {
}
@Override
- public OutputStream getOutputStream(final OutputStream out) throws IOException {
+ public InputStream getInputStream(final InputStream in) throws IOException {
+
+ return decode(in);
+ }
+
+ @Override
+ public OutputStream encode(OutputStream out) throws IOException {
if (useZlib) {
return new DeflaterOutputStream(out, new Deflater(level));
@@ -88,6 +102,12 @@ public OutputStream getOutputStream(final OutputStream out) throws IOException {
}
}
+ @Override
+ public OutputStream getOutputStream(final OutputStream out) throws IOException {
+
+ return encode(out);
+ }
+
@Override
public GzipCompression getReader() {
@@ -116,4 +136,5 @@ public boolean equals(final Object other) {
return useZlib == gz.useZlib && level == gz.level;
}
}
+
}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/IntArrayDataBlock.java b/src/main/java/org/janelia/saalfeldlab/n5/IntArrayDataBlock.java
index 98c5577d..4d338332 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/IntArrayDataBlock.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/IntArrayDataBlock.java
@@ -25,10 +25,13 @@
*/
package org.janelia.saalfeldlab.n5;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
import java.nio.ByteBuffer;
-public class IntArrayDataBlock extends AbstractDataBlock {
+public class IntArrayDataBlock extends AbstractDataBlock {
public IntArrayDataBlock(final int[] size, final long[] gridPosition, final int[] data) {
super(size, gridPosition, data);
@@ -48,6 +51,20 @@ public void readData(final ByteBuffer buffer) {
buffer.asIntBuffer().get(data);
}
+ @Override
+ public void readData(final DataInput input) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ data[i] = input.readInt();
+ }
+
+ @Override
+ public void writeData(final DataOutput output) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ output.writeInt(data[i]);
+ }
+
@Override
public int getNumElements() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java
index ea09269b..138bb73c 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java
@@ -117,6 +117,8 @@ public default String compose(final URI uri, final String... components) {
*/
public boolean exists(final String normalPath);
+ public long size(final String normalPath) throws IOException;
+
/**
* Test whether the path is a directory.
*
@@ -155,6 +157,9 @@ public default String compose(final URI uri, final String... components) {
*/
public LockedChannel lockForReading(final String normalPath) throws IOException;
+ public LockedChannel lockForReading(String normalPath, final long startByte, final long size)
+ throws IOException;
+
/**
* Create an exclusive lock on a path for writing. If the file doesn't
* exist yet, it will be created, including all directories leading up to
@@ -175,6 +180,9 @@ public default String compose(final URI uri, final String... components) {
*/
public LockedChannel lockForWriting(final String normalPath) throws IOException;
+ public LockedChannel lockForWriting(String normalPath, final long startByte, final long size)
+ throws IOException;
+
/**
* List all 'directory'-like children of a path.
*
@@ -222,4 +230,5 @@ public default String compose(final URI uri, final String... components) {
* if an error occurs during deletion
*/
public void delete(final String normalPath) throws IOException;
+
}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/LockedChannel.java b/src/main/java/org/janelia/saalfeldlab/n5/LockedChannel.java
index bd34a59d..c3c53f50 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/LockedChannel.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/LockedChannel.java
@@ -40,6 +40,8 @@
*/
public interface LockedChannel extends Closeable {
+ public long size() throws IOException;
+
/**
* Create a UTF-8 {@link Reader}.
*
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/LongArrayDataBlock.java b/src/main/java/org/janelia/saalfeldlab/n5/LongArrayDataBlock.java
index d3f3fc9c..be435c4f 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/LongArrayDataBlock.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/LongArrayDataBlock.java
@@ -25,6 +25,9 @@
*/
package org.janelia.saalfeldlab.n5;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
import java.nio.ByteBuffer;
public class LongArrayDataBlock extends AbstractDataBlock {
@@ -48,6 +51,20 @@ public void readData(final ByteBuffer buffer) {
buffer.asLongBuffer().get(data);
}
+ @Override
+ public void readData(final DataInput inputStream) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ data[i] = inputStream.readLong();
+ }
+
+ @Override
+ public void writeData(final DataOutput output) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ output.writeLong(data[i]);
+ }
+
@Override
public int getNumElements() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/Lz4Compression.java b/src/main/java/org/janelia/saalfeldlab/n5/Lz4Compression.java
index d76e4fe5..654ca4b5 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/Lz4Compression.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/Lz4Compression.java
@@ -33,8 +33,10 @@
import net.jpountz.lz4.LZ4BlockInputStream;
import net.jpountz.lz4.LZ4BlockOutputStream;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
@CompressionType("lz4")
+@NameConfig.Name("lz4")
public class Lz4Compression implements DefaultBlockReader, DefaultBlockWriter, Compression {
private static final long serialVersionUID = -9071316415067427256L;
@@ -53,17 +55,29 @@ public Lz4Compression() {
}
@Override
- public InputStream getInputStream(final InputStream in) throws IOException {
+ public InputStream decode(final InputStream in) throws IOException {
return new LZ4BlockInputStream(in);
}
@Override
- public OutputStream getOutputStream(final OutputStream out) throws IOException {
+ public InputStream getInputStream(final InputStream in) throws IOException {
+
+ return decode(in);
+ }
+
+ @Override
+ public OutputStream encode(final OutputStream out) throws IOException {
return new LZ4BlockOutputStream(out, blockSize);
}
+ @Override
+ public OutputStream getOutputStream(final OutputStream out) throws IOException {
+
+ return encode(out);
+ }
+
@Override
public Lz4Compression getReader() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/N5Exception.java b/src/main/java/org/janelia/saalfeldlab/n5/N5Exception.java
index 345a7cd0..7fbe0135 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/N5Exception.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/N5Exception.java
@@ -121,4 +121,31 @@ protected N5NoSuchKeyException(
super(message, cause, enableSuppression, writableStackTrace);
}
}
+
+ public static class N5ShardException extends N5IOException {
+
+ public N5ShardException(final String message) {
+
+ super(message);
+ }
+
+ public N5ShardException(final String message, final Throwable cause) {
+
+ super(message, cause);
+ }
+
+ public N5ShardException(final Throwable cause) {
+
+ super(cause);
+ }
+
+ protected N5ShardException(
+ final String message,
+ final Throwable cause,
+ final boolean enableSuppression,
+ final boolean writableStackTrace) {
+
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+ }
}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/N5KeyValueReader.java b/src/main/java/org/janelia/saalfeldlab/n5/N5KeyValueReader.java
index 29e43ce6..dd4b9cc2 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/N5KeyValueReader.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/N5KeyValueReader.java
@@ -123,7 +123,11 @@ protected N5KeyValueReader(
this.keyValueAccess = keyValueAccess;
this.gson = GsonUtils.registerGson(gsonBuilder);
this.cacheMeta = cacheMeta;
- this.cache = newCache();
+
+ if (this.cacheMeta)
+ this.cache = newCache();
+ else
+ this.cache = null;
try {
uri = keyValueAccess.uri(basePath);
@@ -144,6 +148,12 @@ protected N5KeyValueReader(
throw new N5Exception.N5IOException("No container exists at " + basePath);
}
+ @Override
+ public String getAttributesKey() {
+
+ return ATTRIBUTES_JSON;
+ }
+
@Override
public Gson getGson() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/N5Reader.java b/src/main/java/org/janelia/saalfeldlab/n5/N5Reader.java
index 010e757f..20a04b33 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/N5Reader.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/N5Reader.java
@@ -25,6 +25,8 @@
*/
package org.janelia.saalfeldlab.n5;
+import org.janelia.saalfeldlab.n5.shard.Shard;
+
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
@@ -52,7 +54,7 @@
*/
public interface N5Reader extends AutoCloseable {
- public static class Version {
+ class Version {
private final int major;
private final int minor;
@@ -189,12 +191,12 @@ public boolean isCompatible(final Version version) {
/**
* SemVer version of this N5 spec.
*/
- public static final Version VERSION = new Version(4, 0, 0);
+ Version VERSION = new Version(4, 0, 0);
/**
* Version attribute key.
*/
- public static final String VERSION_KEY = "n5";
+ String VERSION_KEY = "n5";
/**
* Get the SemVer version of this container as specified in the 'version'
@@ -220,7 +222,7 @@ default Version getVersion() throws N5Exception {
* @return the base path URI
*/
// TODO: should this throw URISyntaxException or can we assume that this is
- // never possible if we were able to instantiate this N5Reader?
+ // never possible if we were able to instantiate this N5Reader?
URI getURI();
/**
@@ -288,11 +290,50 @@ T getAttribute(
* @throws N5Exception
* the exception
*/
- DataBlock> readBlock(
+ DataBlock readBlock(
final String pathName,
final DatasetAttributes datasetAttributes,
final long... gridPosition) throws N5Exception;
+ /**
+ * Reads the {@link Shard} at the corresponding grid position.
+ *
+ * @param the data access type for the blocks in the shard
+ * @param datasetPath to read the shard from
+ * @param datasetAttributes for the shard
+ * @param shardGridPosition of the shard we are reading
+ * @return the shard
+ */
+ Shard readShard(final String datasetPath, final DatasetAttributes datasetAttributes, long... shardGridPosition);
+
+ /**
+ * Reads multiple {@link DataBlock}s.
+ *
+ * Implementations may optimize / batch read operations when possible, e.g.
+ * in the case that the datasets are sharded.
+ *
+ * @param pathName
+ * dataset path
+ * @param datasetAttributes
+ * the dataset attributes
+ * @param gridPositions
+ * a list of grid positions
+ * @return a list of data blocks
+ * @throws N5Exception
+ * the exception
+ */
+ default List> readBlocks(
+ final String pathName,
+ final DatasetAttributes datasetAttributes,
+ final List gridPositions) throws N5Exception {
+
+ final ArrayList> blocks = new ArrayList<>();
+ for( final long[] p : gridPositions )
+ blocks.add(readBlock(pathName, datasetAttributes, p));
+
+ return blocks;
+ }
+
/**
* Load a {@link DataBlock} as a {@link Serializable}. The offset is given
* in
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/N5Writer.java b/src/main/java/org/janelia/saalfeldlab/n5/N5Writer.java
index 4cfd52be..01606204 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/N5Writer.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/N5Writer.java
@@ -30,10 +30,17 @@
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.io.UncheckedIOException;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import org.checkerframework.checker.units.qual.A;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.N5BlockCodec;
+import org.janelia.saalfeldlab.n5.shard.Shard;
+import org.janelia.saalfeldlab.n5.shard.ShardParameters;
+
/**
* A simple structured container API for hierarchies of chunked
* n-dimensional datasets and attributes.
@@ -135,7 +142,7 @@ default void setDatasetAttributes(
final String datasetPath,
final DatasetAttributes datasetAttributes) throws N5Exception {
- setAttributes(datasetPath, datasetAttributes.asMap());
+ setAttribute(datasetPath, "/", datasetAttributes);
}
/**
@@ -206,10 +213,34 @@ default void createDataset(
setDatasetAttributes(normalPath, datasetAttributes);
}
+
+ /**
+ * Creates a dataset. This does not create any data but the path and
+ * mandatory attributes only.
+ *
+ * @param datasetPath dataset path
+ * @param dimensions the dataset dimensions
+ * @param blockSize the block size
+ * @param dataType the data type
+ * @param codecs codecs to encode/decode with
+ * @throws N5Exception the exception
+ */
+ default void createDataset(
+ final String datasetPath,
+ final long[] dimensions,
+ final int[] blockSize,
+ final DataType dataType,
+ final Codec... codecs) throws N5Exception {
+
+ createDataset(datasetPath, new DatasetAttributes(dimensions, blockSize, dataType, codecs));
+ }
+
/**
+ * DEPRECATED. {@link Compression}s are {@link Codec}s.
+ * Use {@link #createDataset(String, long[], int[], DataType, Codec...)}
+ *
* Creates a dataset. This does not create any data but the path and
- * mandatory
- * attributes only.
+ * mandatory attributes only.
*
* @param datasetPath dataset path
* @param dimensions the dataset dimensions
@@ -218,6 +249,7 @@ default void createDataset(
* @param compression the compression
* @throws N5Exception the exception
*/
+ @Deprecated
default void createDataset(
final String datasetPath,
final long[] dimensions,
@@ -225,7 +257,7 @@ default void createDataset(
final DataType dataType,
final Compression compression) throws N5Exception {
- createDataset(datasetPath, new DatasetAttributes(dimensions, blockSize, dataType, compression));
+ createDataset(datasetPath, dimensions, blockSize, dataType, new N5BlockCodec(), compression);
}
/**
@@ -242,6 +274,39 @@ void writeBlock(
final DatasetAttributes datasetAttributes,
final DataBlock dataBlock) throws N5Exception;
+ /**
+ * Write multiple data blocks, useful for request aggregation .
+ *
+ * @param datasetPath dataset path
+ * @param datasetAttributes the dataset attributes
+ * @param dataBlocks the data block
+ * @param the data block data type
+ * @throws N5Exception the exception
+ */
+ default void writeBlocks(
+ final String datasetPath,
+ final DatasetAttributes datasetAttributes,
+ final DataBlock... dataBlocks) throws N5Exception {
+
+ // default method is naive
+ for (DataBlock block : dataBlocks)
+ writeBlock(datasetPath, datasetAttributes, block);
+ }
+
+ /**
+ * Writes a {@link Shard}.
+ *
+ * @param datasetPath dataset path
+ * @param datasetAttributes the dataset attributes
+ * @param shard the shard
+ * @param the data block data type
+ * @throws N5Exception the exception
+ */
+ void writeShard(
+ final String datasetPath,
+ final DatasetAttributes datasetAttributes,
+ final Shard shard) throws N5Exception;
+
/**
* Deletes the block at {@code gridPosition}
*
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/NameConfigAdapter.java b/src/main/java/org/janelia/saalfeldlab/n5/NameConfigAdapter.java
new file mode 100644
index 00000000..30e45d80
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/NameConfigAdapter.java
@@ -0,0 +1,246 @@
+/**
+ * Copyright (c) 2017, Stephan Saalfeld
+ * All rights reserved.
+ *
+ * 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 OWNER 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.
+ */
+package org.janelia.saalfeldlab.n5;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.janelia.saalfeldlab.n5.serialization.N5Annotations;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
+import org.scijava.annotations.Index;
+import org.scijava.annotations.IndexItem;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map.Entry;
+
+/**
+ * T adapter, auto-discovers annotated T implementations in the classpath.
+ *
+ * @author Caleb Hulbert
+ */
+public class NameConfigAdapter implements JsonDeserializer, JsonSerializer {
+
+ private static HashMap, NameConfigAdapter>> adapters = new HashMap<>();
+
+ private static void registerAdapter(Class cls) {
+
+ adapters.put(cls, new NameConfigAdapter<>(cls));
+ update(adapters.get(cls));
+ }
+ private final HashMap> constructors = new HashMap<>();
+
+ private final HashMap> parameters = new HashMap<>();
+ private final HashMap> parameterNames = new HashMap<>();
+ private static ArrayList getDeclaredFields(Class> clazz) {
+
+ final ArrayList fields = new ArrayList<>();
+ fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ for (clazz = clazz.getSuperclass(); clazz != null; clazz = clazz.getSuperclass())
+ fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
+ return fields;
+ }
+
+ @SuppressWarnings("unchecked")
+ public static synchronized void update(final NameConfigAdapter adapter) {
+
+ final String prefix = adapter.type.getAnnotation(NameConfig.Prefix.class).value();
+ final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ final Index annotationIndex = Index.load(NameConfig.Name.class, classLoader);
+ for (final IndexItem item : annotationIndex) {
+ Class clazz;
+ try {
+ clazz = (Class)Class.forName(item.className());
+ final String name = clazz.getAnnotation(NameConfig.Name.class).value();
+ final String type = prefix + "." + name;
+
+ final Constructor constructor = clazz.getDeclaredConstructor();
+
+ final HashMap parameters = new HashMap<>();
+ final HashMap parameterNames = new HashMap<>();
+ final ArrayList fields = getDeclaredFields(clazz);
+ for (final Field field : fields) {
+ final NameConfig.Parameter parameter = field.getAnnotation(NameConfig.Parameter.class);
+ if (parameter != null) {
+
+ final String parameterName;
+ if (parameter.value().equals(""))
+ parameterName = field.getName();
+ else
+ parameterName = parameter.value();
+
+ parameterNames.put(field.getName(), parameterName);
+
+ parameters.put(field.getName(), field);
+ }
+ }
+
+ adapter.constructors.put(type, constructor);
+ adapter.parameters.put(type, parameters);
+ adapter.parameterNames.put(type, parameterNames);
+ } catch (final ClassNotFoundException | NoSuchMethodException | ClassCastException
+ | UnsatisfiedLinkError e) {
+ System.err.println("T '" + item.className() + "' could not be registered");
+ }
+ }
+ }
+
+ private final Class type;
+
+ public NameConfigAdapter(Class cls) {
+ this.type = cls;
+ }
+
+ @Override
+ public JsonElement serialize(
+ final T object,
+ final Type typeOfSrc,
+ final JsonSerializationContext context) {
+
+ final Class clazz = (Class)object.getClass();
+
+ final String name = clazz.getAnnotation(NameConfig.Name.class).value();
+ final String prefix = type.getAnnotation(NameConfig.Prefix.class).value();
+ final String type = prefix + "." + name;
+
+ final JsonObject json = new JsonObject();
+ json.addProperty("name", name);
+ final JsonObject configuration = new JsonObject();
+
+ final HashMap parameterTypes = parameters.get(type);
+ final HashMap parameterNameMap = parameterNames.get(type);
+ try {
+ for (final Entry parameterType : parameterTypes.entrySet()) {
+ final String fieldName = parameterType.getKey();
+ final Field field = clazz.getDeclaredField(fieldName);
+ final boolean isAccessible = field.isAccessible();
+ field.setAccessible(true);
+ final Object value = field.get(object);
+ field.setAccessible(isAccessible);
+ final JsonElement serialized = context.serialize(value);
+ if (field.getAnnotation(N5Annotations.ReverseArray.class) != null) {
+ final JsonArray reversedArray = reverseJsonArray(serialized.getAsJsonArray());
+ configuration.add(parameterNameMap.get(fieldName), reversedArray);
+ } else
+ configuration.add(parameterNameMap.get(fieldName), serialized);
+
+ }
+ if (!configuration.isEmpty())
+ json.add("configuration", configuration);
+ } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
+ e.printStackTrace(System.err);
+ return null;
+ }
+
+ return json;
+ }
+
+ @Override
+ public T deserialize(
+ final JsonElement json,
+ final Type typeOfT,
+ final JsonDeserializationContext context) throws JsonParseException {
+
+ final String prefix = type.getAnnotation(NameConfig.Prefix.class).value();
+
+ final JsonObject objectJson = json.getAsJsonObject();
+ final String name = objectJson.getAsJsonPrimitive("name").getAsString();
+ if (name == null) {
+ return null;
+ }
+
+ final String type = prefix + "." + name;
+
+ final JsonObject configuration = objectJson.getAsJsonObject("configuration");
+ /* It's ok to be null if all parameters are optional.
+ * Otherwise, return*/
+ if (configuration == null) {
+ for (final Field field : parameters.get(type).values()) {
+ if (!field.getAnnotation(NameConfig.Parameter.class).optional())
+ return null;
+ }
+ }
+
+ final Constructor extends T> constructor = constructors.get(type);
+ constructor.setAccessible(true);
+ final T object;
+ try {
+ object = constructor.newInstance();
+ final HashMap parameterTypes = parameters.get(type);
+ final HashMap parameterNameMap = parameterNames.get(type);
+ for (final Entry parameterType : parameterTypes.entrySet()) {
+ final String fieldName = parameterType.getKey();
+ final String paramName = parameterNameMap.get(fieldName);
+ final JsonElement paramJson = configuration == null ? null : configuration.get(paramName);
+ final Field field = parameterType.getValue();
+ if (paramJson != null) {
+ final Object parameter;
+ if (field.getAnnotation(N5Annotations.ReverseArray.class) != null) {
+ final JsonArray reversedArray = reverseJsonArray(paramJson);
+ parameter = context.deserialize(reversedArray, field.getType());
+ } else
+ parameter = context.deserialize(paramJson, field.getType());
+ ReflectionUtils.setFieldValue(object, fieldName, parameter);
+ } else if (!field.getAnnotation(NameConfig.Parameter.class).optional()) {
+ /* if param is null, and not optional, return null */
+ return null;
+ }
+ }
+ } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
+ | SecurityException | NoSuchFieldException e) {
+ e.printStackTrace(System.err);
+ return null;
+ }
+
+ return object;
+ }
+
+ private static JsonArray reverseJsonArray(JsonElement paramJson) {
+
+ final JsonArray reversedJson = new JsonArray(paramJson.getAsJsonArray().size());
+ for (int i = paramJson.getAsJsonArray().size() - 1; i >= 0; i--) {
+ reversedJson.add(paramJson.getAsJsonArray().get(i));
+ }
+ return reversedJson;
+ }
+
+ public static NameConfigAdapter getJsonAdapter(Class cls) {
+
+ if (adapters.get(cls) == null)
+ registerAdapter(cls);
+ return (NameConfigAdapter) adapters.get(cls);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/RawCompression.java b/src/main/java/org/janelia/saalfeldlab/n5/RawCompression.java
index ffa674fc..ebd58b38 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/RawCompression.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/RawCompression.java
@@ -30,6 +30,7 @@
import java.io.OutputStream;
import org.janelia.saalfeldlab.n5.Compression.CompressionType;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
@CompressionType("raw")
public class RawCompression implements DefaultBlockReader, DefaultBlockWriter, Compression {
@@ -37,17 +38,30 @@ public class RawCompression implements DefaultBlockReader, DefaultBlockWriter, C
private static final long serialVersionUID = 7526445806847086477L;
@Override
- public InputStream getInputStream(final InputStream in) throws IOException {
+ public InputStream decode(final InputStream in) throws IOException {
return in;
}
@Override
- public OutputStream getOutputStream(final OutputStream out) throws IOException {
+ public InputStream getInputStream(final InputStream in) throws IOException {
+
+ return decode(in);
+ }
+
+
+ @Override
+ public OutputStream encode(final OutputStream out) throws IOException {
return out;
}
+ @Override
+ public OutputStream getOutputStream(final OutputStream out) throws IOException {
+
+ return encode(out);
+ }
+
@Override
public RawCompression getReader() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/ShardedDatasetAttributes.java b/src/main/java/org/janelia/saalfeldlab/n5/ShardedDatasetAttributes.java
new file mode 100644
index 00000000..335c252e
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/ShardedDatasetAttributes.java
@@ -0,0 +1,124 @@
+package org.janelia.saalfeldlab.n5;
+
+import java.util.Arrays;
+
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.Codec.ArrayCodec;
+import org.janelia.saalfeldlab.n5.codec.Codec.BytesCodec;
+import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodec;
+import org.janelia.saalfeldlab.n5.shard.ShardParameters;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec.IndexLocation;
+
+@Deprecated
+public class ShardedDatasetAttributes extends DatasetAttributes implements ShardParameters {
+
+ private static final long serialVersionUID = -4559068841006651814L;
+
+ private final int[] shardSize;
+
+ private final ShardingCodec shardingCodec;
+
+ public ShardedDatasetAttributes (
+ final long[] dimensions,
+ final int[] shardSize, //in pixels
+ final int[] blockSize, //in pixels
+ final DataType dataType,
+ final Codec[] blocksCodecs,
+ final DeterministicSizeCodec[] indexCodecs,
+ final IndexLocation indexLocation
+ ) {
+ //TODO Caleb: Can we just let the super codecs() return this ShardCodec?
+ super(dimensions, blockSize, dataType, blocksCodecs);
+
+ if (!validateShardBlockSize(shardSize, blockSize)) {
+ throw new N5Exception(String.format("Invalid shard %s / block size %s",
+ Arrays.toString(shardSize),
+ Arrays.toString(blockSize)));
+ }
+
+ this.shardSize = shardSize;
+ this.shardingCodec = new ShardingCodec(
+ blockSize,
+ blocksCodecs,
+ indexCodecs,
+ indexLocation
+ );
+ }
+
+ public ShardedDatasetAttributes(
+ final long[] dimensions,
+ final int[] shardSize, //in pixels
+ final int[] blockSize, //in pixels
+ final DataType dataType,
+ final ShardingCodec codec) {
+ super(dimensions, blockSize, dataType, null, null);
+ this.shardSize = shardSize;
+ this.shardingCodec = codec;
+ }
+
+ /**
+ * Returns whether the given shard and block sizes are valid. Specifically, is
+ * the shard size a multiple of the block size in every dimension.
+ *
+ * @param shardSize size of the shard in pixels
+ * @param blockSize size of a block in pixels
+ * @return
+ */
+ public static boolean validateShardBlockSize(final int[] shardSize, final int[] blockSize) {
+
+ if (shardSize.length != blockSize.length)
+ return false;
+
+ for (int i = 0; i < shardSize.length; i++) {
+ if (shardSize[i] % blockSize[i] != 0)
+ return false;
+ }
+ return true;
+ }
+
+ public ShardingCodec getShardingCodec() {
+ return shardingCodec;
+ }
+
+ @Override public ArrayCodec getArrayCodec() {
+
+ return shardingCodec.getArrayCodec();
+ }
+
+ @Override public BytesCodec[] getCodecs() {
+
+ return shardingCodec.getCodecs();
+ }
+
+ @Override
+ protected Codec[] concatenateCodecs() {
+
+ return new Codec[] { shardingCodec };
+ }
+
+ public IndexLocation getIndexLocation() {
+
+ return getShardingCodec().getIndexLocation();
+ }
+
+ /**
+ * The size of the blocks in pixel units.
+ *
+ * @return the number of pixels per dimension for this shard.
+ */
+ @Override
+ public int[] getShardSize() {
+
+ return shardSize;
+ }
+
+ public static int[] getBlockSize(Codec[] codecs) {
+
+ for (final Codec codec : codecs)
+ if (codec instanceof ShardingCodec)
+ return ((ShardingCodec)codec).getBlockSize();
+
+ return null;
+ }
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/ShortArrayDataBlock.java b/src/main/java/org/janelia/saalfeldlab/n5/ShortArrayDataBlock.java
index 2dbf6b17..c7d141f3 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/ShortArrayDataBlock.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/ShortArrayDataBlock.java
@@ -25,6 +25,9 @@
*/
package org.janelia.saalfeldlab.n5;
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
import java.nio.ByteBuffer;
public class ShortArrayDataBlock extends AbstractDataBlock {
@@ -48,6 +51,20 @@ public void readData(final ByteBuffer buffer) {
buffer.asShortBuffer().get(data);
}
+ @Override
+ public void readData(final DataInput dataInput) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ data[i] = dataInput.readShort();
+ }
+
+ @Override
+ public void writeData(final DataOutput output) throws IOException {
+
+ for (int i = 0; i < data.length; i++)
+ output.writeShort(data[i]);
+ }
+
@Override
public int getNumElements() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/XzCompression.java b/src/main/java/org/janelia/saalfeldlab/n5/XzCompression.java
index 5204e799..d2c1ce3d 100644
--- a/src/main/java/org/janelia/saalfeldlab/n5/XzCompression.java
+++ b/src/main/java/org/janelia/saalfeldlab/n5/XzCompression.java
@@ -32,8 +32,10 @@
import org.apache.commons.compress.compressors.xz.XZCompressorInputStream;
import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream;
import org.janelia.saalfeldlab.n5.Compression.CompressionType;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
@CompressionType("xz")
+@NameConfig.Name("xz")
public class XzCompression implements DefaultBlockReader, DefaultBlockWriter, Compression {
private static final long serialVersionUID = -7272153943564743774L;
@@ -52,17 +54,29 @@ public XzCompression() {
}
@Override
- public InputStream getInputStream(final InputStream in) throws IOException {
+ public InputStream decode(final InputStream in) throws IOException {
return new XZCompressorInputStream(in);
}
@Override
- public OutputStream getOutputStream(final OutputStream out) throws IOException {
+ public InputStream getInputStream(final InputStream in) throws IOException {
+
+ return decode(in);
+ }
+
+ @Override
+ public OutputStream encode(final OutputStream out) throws IOException {
return new XZCompressorOutputStream(out, preset);
}
+ @Override
+ public OutputStream getOutputStream(final OutputStream out) throws IOException {
+
+ return encode(out);
+ }
+
@Override
public XzCompression getReader() {
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/AsTypeCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/AsTypeCodec.java
new file mode 100644
index 00000000..e8883c75
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/AsTypeCodec.java
@@ -0,0 +1,401 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.function.BiConsumer;
+
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
+
+@NameConfig.Name(AsTypeCodec.TYPE)
+public class AsTypeCodec implements Codec.BytesCodec {
+
+ private static final long serialVersionUID = 1031322606191894484L;
+
+ public static final String TYPE = "astype";
+
+ protected transient int numBytes;
+ protected transient int numEncodedBytes;
+
+ protected transient BiConsumer encoder;
+ protected transient BiConsumer decoder;
+
+ @NameConfig.Parameter
+ protected final DataType dataType;
+
+ @NameConfig.Parameter
+ protected final DataType encodedType;
+
+ private AsTypeCodec() {
+
+ this(null, null);
+ }
+
+ public AsTypeCodec(DataType dataType, DataType encodedType) {
+
+ this.dataType = dataType;
+ this.encodedType = encodedType;
+ }
+
+ @Override
+ public String getType() {
+
+ return TYPE;
+ }
+
+ public DataType getDataType() {
+
+ return dataType;
+ }
+
+ public DataType getEncodedDataType() {
+
+ return encodedType;
+ }
+
+ @Override
+ public InputStream decode(InputStream in) throws IOException {
+
+ numBytes = bytes(dataType);
+ numEncodedBytes = bytes(encodedType);
+
+ encoder = converter(dataType, encodedType);
+ decoder = converter(encodedType, dataType);
+
+ return new FixedLengthConvertedInputStream(numEncodedBytes, numBytes, decoder, in);
+ }
+
+ @Override
+ public OutputStream encode(OutputStream out) throws IOException {
+
+ numBytes = bytes(dataType);
+ numEncodedBytes = bytes(encodedType);
+
+ encoder = converter(dataType, encodedType);
+ decoder = converter(encodedType, dataType);
+
+ return new FixedLengthConvertedOutputStream(numBytes, numEncodedBytes, encoder, out);
+ }
+
+ public static int bytes(DataType type) {
+
+ switch (type) {
+ case UINT8:
+ case INT8:
+ return 1;
+ case UINT16:
+ case INT16:
+ return 2;
+ case UINT32:
+ case INT32:
+ case FLOAT32:
+ return 4;
+ case UINT64:
+ case INT64:
+ case FLOAT64:
+ return 8;
+ default:
+ return -1;
+ }
+ }
+
+ public static BiConsumer converter(final DataType from, final DataType to) {
+
+ // // TODO fill this out
+
+ if (from == to)
+ return AsTypeCodec::IDENTITY;
+ else if (from == DataType.INT8) {
+
+ if( to == DataType.INT16 )
+ return AsTypeCodec::BYTE_TO_SHORT;
+ else if( to == DataType.INT32 )
+ return AsTypeCodec::BYTE_TO_INT;
+ else if( to == DataType.INT64 )
+ return AsTypeCodec::BYTE_TO_LONG;
+ else if( to == DataType.FLOAT32 )
+ return AsTypeCodec::BYTE_TO_FLOAT;
+ else if( to == DataType.FLOAT64 )
+ return AsTypeCodec::BYTE_TO_DOUBLE;
+
+ } else if (from == DataType.INT16) {
+
+ if (to == DataType.INT8)
+ return AsTypeCodec::SHORT_TO_BYTE;
+ else if (to == DataType.INT32)
+ return AsTypeCodec::SHORT_TO_INT;
+ else if (to == DataType.INT64)
+ return AsTypeCodec::SHORT_TO_LONG;
+ else if (to == DataType.FLOAT32)
+ return AsTypeCodec::SHORT_TO_FLOAT;
+ else if (to == DataType.FLOAT64)
+ return AsTypeCodec::SHORT_TO_DOUBLE;
+
+ } else if (from == DataType.INT32) {
+
+ if (to == DataType.INT8)
+ return AsTypeCodec::INT_TO_BYTE;
+ else if (to == DataType.INT16)
+ return AsTypeCodec::INT_TO_SHORT;
+ if (to == DataType.INT8)
+ return AsTypeCodec::INT_TO_BYTE;
+ else if (to == DataType.INT16)
+ return AsTypeCodec::INT_TO_SHORT;
+ else if (to == DataType.INT32)
+ return AsTypeCodec::IDENTITY;
+ else if (to == DataType.INT64)
+ return AsTypeCodec::INT_TO_LONG;
+ else if (to == DataType.FLOAT32)
+ return AsTypeCodec::INT_TO_FLOAT;
+ else if (to == DataType.INT64)
+ return AsTypeCodec::INT_TO_LONG;
+ else if (to == DataType.FLOAT32)
+ return AsTypeCodec::INT_TO_FLOAT;
+ else if (to == DataType.FLOAT64)
+ return AsTypeCodec::INT_TO_DOUBLE;
+
+ } else if (from == DataType.INT64) {
+
+ if (to == DataType.INT8)
+ return AsTypeCodec::LONG_TO_BYTE;
+ else if (to == DataType.INT16)
+ return AsTypeCodec::LONG_TO_SHORT;
+ else if (to == DataType.INT32)
+ return AsTypeCodec::LONG_TO_INT;
+ else if (to == DataType.FLOAT32)
+ return AsTypeCodec::LONG_TO_FLOAT;
+ else if (to == DataType.FLOAT64)
+ return AsTypeCodec::LONG_TO_DOUBLE;
+
+ } else if (from == DataType.FLOAT32) {
+
+ if (to == DataType.INT8)
+ return AsTypeCodec::FLOAT_TO_BYTE;
+ else if (to == DataType.INT16)
+ return AsTypeCodec::FLOAT_TO_SHORT;
+ else if (to == DataType.INT32)
+ return AsTypeCodec::FLOAT_TO_INT;
+ else if (to == DataType.INT64)
+ return AsTypeCodec::FLOAT_TO_LONG;
+ else if (to == DataType.FLOAT64)
+ return AsTypeCodec::FLOAT_TO_DOUBLE;
+
+ } else if (from == DataType.FLOAT64) {
+
+ if (to == DataType.INT8)
+ return AsTypeCodec::DOUBLE_TO_BYTE;
+ else if (to == DataType.INT16)
+ return AsTypeCodec::DOUBLE_TO_SHORT;
+ else if (to == DataType.INT32)
+ return AsTypeCodec::DOUBLE_TO_INT;
+ else if (to == DataType.INT64)
+ return AsTypeCodec::DOUBLE_TO_LONG;
+ else if (to == DataType.FLOAT32)
+ return AsTypeCodec::DOUBLE_TO_FLOAT;
+ }
+
+ return AsTypeCodec::IDENTITY;
+ }
+
+ public static final void IDENTITY(final ByteBuffer x, final ByteBuffer y) {
+
+ for (int i = 0; i < y.capacity(); i++)
+ y.put(x.get());
+ }
+
+ public static final void IDENTITY_ONE(final ByteBuffer x, final ByteBuffer y) {
+
+ y.put(x.get());
+ }
+
+ public static final void BYTE_TO_SHORT(final ByteBuffer b, final ByteBuffer s) {
+
+ final byte zero = 0;
+ s.put(zero);
+ s.put(b.get());
+ }
+
+ public static final void BYTE_TO_INT(final ByteBuffer b, final ByteBuffer i) {
+
+ final byte zero = 0;
+ i.put(zero);
+ i.put(zero);
+ i.put(zero);
+ i.put(b.get());
+ }
+
+ public static final void BYTE_TO_LONG(final ByteBuffer b, final ByteBuffer l) {
+
+ final byte zero = 0;
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(b.get());
+ }
+
+ public static final void BYTE_TO_FLOAT(final ByteBuffer b, final ByteBuffer f) {
+
+ f.putFloat((float)b.get());
+ }
+
+ public static final void BYTE_TO_DOUBLE(final ByteBuffer b, final ByteBuffer d) {
+
+ d.putDouble((double)b.get());
+ }
+
+ public static final void SHORT_TO_BYTE(final ByteBuffer s, final ByteBuffer b) {
+
+ final byte zero = 0;
+ b.put(zero);
+ b.put(s.get());
+ }
+
+ public static final void SHORT_TO_INT(final ByteBuffer s, final ByteBuffer i) {
+
+ final byte zero = 0;
+ i.put(zero);
+ i.put(zero);
+ i.put(s.get());
+ i.put(s.get());
+ }
+
+ public static final void SHORT_TO_LONG(final ByteBuffer s, final ByteBuffer l) {
+
+ final byte zero = 0;
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(s.get());
+ l.put(s.get());
+ }
+
+ public static final void SHORT_TO_FLOAT(final ByteBuffer s, final ByteBuffer f) {
+
+ f.putFloat((float)s.getShort());
+ }
+
+ public static final void SHORT_TO_DOUBLE(final ByteBuffer s, final ByteBuffer d) {
+
+ d.putDouble((double)s.getShort());
+ }
+
+ public static final void INT_TO_BYTE(final ByteBuffer i, final ByteBuffer b) {
+
+ b.put(i.get(3));
+ }
+
+ public static final void INT_TO_SHORT(final ByteBuffer i, final ByteBuffer s) {
+
+ s.put(i.get(2));
+ s.put(i.get(3));
+ }
+
+ public static final void INT_TO_LONG(final ByteBuffer i, final ByteBuffer l) {
+
+ final byte zero = 0;
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(zero);
+ l.put(i.get());
+ l.put(i.get());
+ l.put(i.get());
+ l.put(i.get());
+ }
+
+ public static final void INT_TO_FLOAT(final ByteBuffer i, final ByteBuffer f) {
+
+ f.putFloat((float)i.getInt());
+ }
+
+ public static final void INT_TO_DOUBLE(final ByteBuffer i, final ByteBuffer f) {
+
+ f.putDouble((float)i.getInt());
+ }
+
+ public static final void LONG_TO_BYTE(final ByteBuffer l, final ByteBuffer b) {
+
+ b.put((byte)l.getLong());
+ }
+
+ public static final void LONG_TO_SHORT(final ByteBuffer l, final ByteBuffer s) {
+
+ s.putShort((short)l.getLong());
+ }
+
+ public static final void LONG_TO_INT(final ByteBuffer l, final ByteBuffer i) {
+
+ i.putInt((int)l.getLong());
+ }
+
+ public static final void LONG_TO_FLOAT(final ByteBuffer l, final ByteBuffer f) {
+
+ f.putFloat((float)l.getLong());
+ }
+
+ public static final void LONG_TO_DOUBLE(final ByteBuffer l, final ByteBuffer f) {
+
+ f.putDouble((float)l.getLong());
+ }
+
+ public static final void FLOAT_TO_BYTE(final ByteBuffer f, final ByteBuffer b) {
+
+ b.put((byte)f.getFloat());
+ }
+
+ public static final void FLOAT_TO_SHORT(final ByteBuffer f, final ByteBuffer s) {
+
+ s.putShort((short)f.getFloat());
+ }
+
+ public static final void FLOAT_TO_INT(final ByteBuffer f, final ByteBuffer i) {
+
+ i.putInt((int)f.getFloat());
+ }
+
+ public static final void FLOAT_TO_LONG(final ByteBuffer f, final ByteBuffer l) {
+
+ l.putLong((long)f.getFloat());
+ }
+
+ public static final void FLOAT_TO_DOUBLE(final ByteBuffer f, final ByteBuffer d) {
+
+ d.putDouble((double)f.getFloat());
+ }
+
+ public static final void DOUBLE_TO_BYTE(final ByteBuffer d, final ByteBuffer b) {
+
+ b.put((byte)d.getDouble());
+ }
+
+ public static final void DOUBLE_TO_SHORT(final ByteBuffer d, final ByteBuffer s) {
+
+ s.putShort((short)d.getDouble());
+ }
+
+ public static final void DOUBLE_TO_INT(final ByteBuffer d, final ByteBuffer i) {
+
+ i.putInt((int)d.getDouble());
+ }
+
+ public static final void DOUBLE_TO_LONG(final ByteBuffer d, final ByteBuffer l) {
+
+ l.putLong((long)d.getDouble());
+ }
+
+ public static final void DOUBLE_TO_FLOAT(final ByteBuffer d, final ByteBuffer f) {
+
+ f.putFloat((float)d.getDouble());
+ }
+
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/Codec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/Codec.java
new file mode 100644
index 00000000..ed544257
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/Codec.java
@@ -0,0 +1,180 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.Serializable;
+import java.io.UncheckedIOException;
+import java.util.Arrays;
+
+import org.apache.commons.io.input.ProxyInputStream;
+import org.apache.commons.io.output.ProxyOutputStream;
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.KeyValueAccess;
+import org.janelia.saalfeldlab.n5.LockedChannel;
+import org.janelia.saalfeldlab.n5.N5Exception;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
+
+/**
+ * Interface representing a filter can encode a {@link OutputStream}s when writing data, and decode
+ * the {@link InputStream}s when reading data.
+ *
+ * Modeled after Filters in
+ * Zarr.
+ */
+@NameConfig.Prefix("codec")
+public interface Codec extends Serializable {
+
+ static OutputStream encode(OutputStream out, Codec.BytesCodec... bytesCodecs) throws IOException {
+ OutputStream stream = out;
+ for (final BytesCodec codec : bytesCodecs)
+ stream = codec.encode(stream);
+
+ return stream;
+ }
+
+ static InputStream decode(InputStream out, Codec.BytesCodec... bytesCodecs) throws IOException {
+ InputStream stream = out;
+ for (final BytesCodec codec : bytesCodecs)
+ stream = codec.decode(stream);
+
+ return stream;
+ }
+
+ interface BytesCodec extends Codec {
+
+ /**
+ * Decode an {@link InputStream}.
+ *
+ * @param in input stream
+ * @return the decoded input stream
+ */
+ InputStream decode(final InputStream in) throws IOException;
+
+ /**
+ * Encode an {@link OutputStream}.
+ *
+ * @param out the output stream
+ * @return the encoded output stream
+ */
+ OutputStream encode(final OutputStream out) throws IOException;
+ }
+
+ interface ArrayCodec extends DeterministicSizeCodec {
+
+ default long[] getPositionForBlock(final DatasetAttributes attributes, final DataBlock> datablock) {
+
+ return datablock.getGridPosition();
+ }
+
+ default long[] getPositionForBlock(final DatasetAttributes attributes, final long... blockPosition) {
+
+ return blockPosition;
+ }
+ /**
+ * Decode an {@link InputStream}.
+ *
+ * @param in input stream
+ * @return the DataBlock corresponding to the input stream
+ */
+ DataBlockInputStream decode(
+ final DatasetAttributes attributes,
+ final long[] gridPosition,
+ final InputStream in) throws IOException;
+
+ /**
+ * Encode a {@link DataBlock}.
+ *
+ * @param datablock the datablock to encode
+ */
+ DataBlockOutputStream encode(
+ final DatasetAttributes attributes,
+ final DataBlock> datablock,
+ final OutputStream out) throws IOException;
+
+ @Override default long encodedSize(long size) {
+
+ return size;
+ }
+
+ @Override default long decodedSize(long size) {
+
+ return size;
+ }
+ default void writeBlock(
+ final KeyValueAccess kva,
+ final String keyPath,
+ final DatasetAttributes datasetAttributes,
+ final DataBlock dataBlock) {
+
+ try (final LockedChannel lock = kva.lockForWriting(keyPath)) {
+ try (final OutputStream out = lock.newOutputStream()) {
+ final DataBlockOutputStream dataBlockOutput = encode(datasetAttributes, dataBlock, out);
+ try (final OutputStream stream = Codec.encode(dataBlockOutput, datasetAttributes.getCodecs())) {
+ dataBlock.writeData(dataBlockOutput.getDataOutput(stream));
+ }
+ }
+ } catch (final IOException | UncheckedIOException e) {
+ final String msg = "Failed to write block " + Arrays.toString(dataBlock.getGridPosition()) + " into dataset " + keyPath;
+ throw new N5Exception.N5IOException( msg, e);
+ }
+ }
+
+ default DataBlock readBlock(
+ final KeyValueAccess kva,
+ final String keyPath,
+ final DatasetAttributes datasetAttributes,
+ final long[] gridPosition) {
+
+ try (final LockedChannel lockedChannel = kva.lockForReading(keyPath)) {
+ try(final InputStream in = lockedChannel.newInputStream()) {
+
+ final BytesCodec[] codecs = datasetAttributes.getCodecs();
+ final ArrayCodec arrayCodec = datasetAttributes.getArrayCodec();
+ final DataBlockInputStream dataBlockStream = arrayCodec.decode(datasetAttributes, gridPosition, in);
+ InputStream stream = Codec.decode(dataBlockStream, codecs);
+
+ final DataBlock dataBlock = dataBlockStream.allocateDataBlock();
+ dataBlock.readData(dataBlockStream.getDataInput(stream));
+ stream.close();
+
+ return dataBlock;
+ }
+ } catch (final N5Exception.N5NoSuchKeyException e) {
+ return null;
+ } catch (final IOException | UncheckedIOException e) {
+ final String msg = "Failed to read block " + Arrays.toString(gridPosition) + " from dataset " + keyPath;
+ throw new N5Exception.N5IOException( msg, e);
+ }
+ }
+ }
+
+ abstract class DataBlockInputStream extends ProxyInputStream {
+
+
+ protected DataBlockInputStream(InputStream in) {
+
+ super(in);
+ }
+
+ public abstract DataBlock allocateDataBlock() throws IOException;
+
+ public abstract DataInput getDataInput(final InputStream inputStream);
+ }
+
+ abstract class DataBlockOutputStream extends ProxyOutputStream {
+
+ protected DataBlockOutputStream(final OutputStream out) {
+
+ super(out);
+ }
+
+ public abstract DataOutput getDataOutput(final OutputStream outputStream);
+ }
+
+ String getType();
+}
+
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/DeterministicSizeCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/DeterministicSizeCodec.java
new file mode 100644
index 00000000..9ac0a1fe
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/DeterministicSizeCodec.java
@@ -0,0 +1,13 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+/**
+ * A {@link Codec} that can deterministically determine the size of encoded data from the size of the raw data and vice versa from the data length alone (i.e. encoding is data
+ * independent).
+ */
+public interface DeterministicSizeCodec extends Codec {
+
+ public abstract long encodedSize(long size);
+
+ public abstract long decodedSize(long size);
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/FixedLengthConvertedInputStream.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/FixedLengthConvertedInputStream.java
new file mode 100644
index 00000000..78d6313a
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/FixedLengthConvertedInputStream.java
@@ -0,0 +1,71 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.util.function.BiConsumer;
+
+/**
+ * An {@link InputStream} that converts between two fixed-length types.
+ */
+public class FixedLengthConvertedInputStream extends InputStream {
+
+ private final int numBytes;
+ private final int numBytesAfterDecoding;
+
+ private final byte[] raw;
+ private final byte[] decoded;
+
+ private final ByteBuffer rawBuffer;
+ private final ByteBuffer decodedBuffer;
+
+ private final InputStream src;
+
+ private BiConsumer converter;
+
+ private int incrememntalBytesRead;
+
+ public FixedLengthConvertedInputStream(
+ final int numBytes,
+ final int numBytesAfterDecoding,
+ BiConsumer converter,
+ final InputStream src ) {
+
+ this.numBytes = numBytes;
+ this.numBytesAfterDecoding = numBytesAfterDecoding;
+ this.converter = converter;
+
+ raw = new byte[numBytes];
+ decoded = new byte[numBytesAfterDecoding];
+ incrememntalBytesRead = 0;
+
+ rawBuffer = ByteBuffer.wrap(raw);
+ decodedBuffer = ByteBuffer.wrap(decoded);
+
+ this.src = src;
+ }
+
+ @Override
+ public int read() throws IOException {
+
+ // TODO not sure if this always reads enough bytes
+ // int n = src.read(toEncode);
+ if (incrememntalBytesRead == 0) {
+
+ rawBuffer.rewind();
+ decodedBuffer.rewind();
+
+ for (int i = 0; i < numBytes; i++)
+ raw[i] = (byte)src.read();
+
+ converter.accept(rawBuffer, decodedBuffer);
+ }
+
+ final int out = decoded[incrememntalBytesRead++];
+ if (incrememntalBytesRead == numBytesAfterDecoding)
+ incrememntalBytesRead = 0;
+
+ return out;
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/FixedLengthConvertedOutputStream.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/FixedLengthConvertedOutputStream.java
new file mode 100644
index 00000000..87544fc7
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/FixedLengthConvertedOutputStream.java
@@ -0,0 +1,64 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.function.BiConsumer;
+
+/*
+ * An {@link OutputStream} that converts between two fixed-length types.
+ */
+public class FixedLengthConvertedOutputStream extends OutputStream {
+
+ private final int numBytes;
+
+ private final byte[] raw;
+ private final byte[] encoded;
+
+ private final ByteBuffer rawBuffer;
+ private final ByteBuffer encodedBuffer;
+
+ private final OutputStream src;
+
+ private BiConsumer converter;
+
+ private int incrememntalBytesWritten;
+
+ public FixedLengthConvertedOutputStream(
+ final int numBytes,
+ final int numBytesAfterEncoding,
+ final BiConsumer converter,
+ final OutputStream src ) {
+
+ this.numBytes = numBytes;
+ this.converter = converter;
+
+ raw = new byte[numBytes];
+ encoded = new byte[numBytesAfterEncoding];
+
+ rawBuffer = ByteBuffer.wrap(raw);
+ encodedBuffer = ByteBuffer.wrap(encoded);
+
+ incrememntalBytesWritten = 0;
+
+ this.src = src;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+
+ raw[incrememntalBytesWritten++] = (byte)b;
+
+ // write out the encoded bytes after writing numBytes bytes
+ if (incrememntalBytesWritten == numBytes) {
+
+ rawBuffer.rewind();
+ encodedBuffer.rewind();
+
+ converter.accept(rawBuffer, encodedBuffer);
+ src.write(encoded);
+ incrememntalBytesWritten = 0;
+ }
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/FixedScaleOffsetCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/FixedScaleOffsetCodec.java
new file mode 100644
index 00000000..e6c83163
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/FixedScaleOffsetCodec.java
@@ -0,0 +1,114 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.function.BiConsumer;
+
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
+
+@NameConfig.Name(FixedScaleOffsetCodec.TYPE)
+public class FixedScaleOffsetCodec extends AsTypeCodec {
+
+ private static final long serialVersionUID = 8024945290803548528L;
+
+ public static transient final String TYPE = "fixedscaleoffset";
+
+ @NameConfig.Parameter
+ protected final double scale;
+
+ @NameConfig.Parameter
+ protected final double offset;
+
+ private transient ByteBuffer tmpEncoder;
+ private transient ByteBuffer tmpDecoder;
+
+ public transient BiConsumer encoder;
+ public transient BiConsumer encoderPre;
+ public transient BiConsumer encoderPost;
+ public transient BiConsumer decoder;
+ public transient BiConsumer decoderPre;
+ public transient BiConsumer decoderPost;
+
+ private FixedScaleOffsetCodec() {
+
+ this(1, 0, null, null);
+ }
+
+ public FixedScaleOffsetCodec(final double scale, final double offset, DataType type, DataType encodedType) {
+
+ super(type, encodedType);
+ this.scale = scale;
+ this.offset = offset;
+
+ tmpEncoder = ByteBuffer.wrap(new byte[Double.BYTES]);
+ tmpDecoder = ByteBuffer.wrap(new byte[Double.BYTES]);
+
+ // encoder goes from type to encoded type
+ encoderPre = converter(type, DataType.FLOAT64);
+ encoderPost = converter(DataType.FLOAT64, encodedType);
+
+ // decoder goes from encoded type to type
+ decoderPre = converter(encodedType, DataType.FLOAT64);
+ decoderPost = converter(DataType.FLOAT64, type);
+
+ // convert from i type to double, apply scale and offset, then convert to type o
+ encoder = (i, o) -> {
+ tmpEncoder.rewind();
+ encoderPre.accept(i, tmpEncoder);
+ tmpEncoder.rewind();
+ final double x = tmpEncoder.getDouble();
+ tmpEncoder.rewind();
+ tmpEncoder.putDouble(scale * x + offset);
+ tmpEncoder.rewind();
+ encoderPost.accept(tmpEncoder, o);
+ };
+
+ // convert from i type to double, apply scale and offset, then convert to type o
+ decoder = (i, o) -> {
+ tmpDecoder.rewind();
+ decoderPre.accept(i, tmpDecoder);
+ tmpDecoder.rewind();
+ final double x = tmpDecoder.getDouble();
+ tmpDecoder.rewind();
+ tmpDecoder.putDouble((x - offset) / scale);
+ tmpDecoder.rewind();
+ decoderPost.accept(tmpDecoder, o);
+ };
+ }
+
+ public double getScale() {
+
+ return scale;
+ }
+
+ public double getOffset() {
+
+ return offset;
+ }
+
+ @Override
+ public String getType() {
+
+ return TYPE;
+ }
+
+ @Override
+ public InputStream decode(InputStream in) throws IOException {
+
+ numBytes = bytes(dataType);
+ numEncodedBytes = bytes(encodedType);
+ return new FixedLengthConvertedInputStream(numEncodedBytes, numBytes, this.decoder, in);
+ }
+
+ @Override
+ public OutputStream encode(OutputStream out) throws IOException {
+
+ numBytes = bytes(dataType);
+ numEncodedBytes = bytes(encodedType);
+ return new FixedLengthConvertedOutputStream(numBytes, numEncodedBytes, this.encoder, out);
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/IdentityCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/IdentityCodec.java
new file mode 100644
index 00000000..93a384dd
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/IdentityCodec.java
@@ -0,0 +1,34 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
+
+@NameConfig.Name(IdentityCodec.TYPE)
+public class IdentityCodec implements Codec.BytesCodec {
+
+ private static final long serialVersionUID = 8354269325800855621L;
+
+ public static final String TYPE = "id";
+
+ @Override
+ public InputStream decode(InputStream in) throws IOException {
+
+ return in;
+ }
+
+ @Override
+ public OutputStream encode(OutputStream out) throws IOException {
+
+ return out;
+ }
+
+ @Override
+ public String getType() {
+
+ return TYPE;
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/N5BlockCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/N5BlockCodec.java
new file mode 100644
index 00000000..6b83f466
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/N5BlockCodec.java
@@ -0,0 +1,202 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteOrder;
+
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
+
+import com.google.common.io.LittleEndianDataInputStream;
+import com.google.common.io.LittleEndianDataOutputStream;
+
+import javax.annotation.CheckForNull;
+import javax.annotation.Nullable;
+
+@NameConfig.Name(value = N5BlockCodec.TYPE)
+public class N5BlockCodec implements Codec.ArrayCodec {
+
+ private static final long serialVersionUID = 3523505403978222360L;
+
+ public static final String TYPE = "n5bytes";
+ public static final int MODE_DEFAULT = 0;
+ public static final int MODE_VARLENGTH = 1;
+ public static final int MODE_OBJECT = 2;
+
+ @Nullable
+ @NameConfig.Parameter(value = "endian", optional = true)
+ protected final ByteOrder byteOrder;
+
+ public N5BlockCodec() {
+
+ this(ByteOrder.BIG_ENDIAN);
+ }
+
+ public N5BlockCodec(@Nullable final ByteOrder byteOrder) {
+
+ this.byteOrder = byteOrder;
+ }
+
+ /**
+ * ByteOrder used to encode/decode this block of data.
+ * Will be `null` when {@link DatasetAttributes#getDataType()} refers to a single-byte type,
+ *
+ * @return the byte order for this codec
+ */
+ @CheckForNull
+ public ByteOrder getByteOrder() {
+ return byteOrder;
+ }
+
+ @Override public DataBlockInputStream decode(final DatasetAttributes attributes, final long[] gridPosition, InputStream in) throws IOException {
+
+ return new N5DataBlockInputStream(in, attributes, gridPosition, byteOrder);
+ }
+
+
+ @Override
+ public DataBlockOutputStream encode(final DatasetAttributes attributes, final DataBlock> dataBlock,
+ final OutputStream out)
+ throws IOException {
+
+ return new N5DataBlockOutputStream(out, attributes, dataBlock, byteOrder);
+ }
+
+ @Override
+ public String getType() {
+
+ return TYPE;
+ }
+
+ private static class N5DataBlockOutputStream extends DataBlockOutputStream {
+
+ private final DatasetAttributes attributes;
+ private final DataBlock> dataBlock;
+ private final ByteOrder byteOrder;
+ boolean start = true;
+
+
+ public N5DataBlockOutputStream(final OutputStream out, final DatasetAttributes attributes, final DataBlock> dataBlock, ByteOrder byteOrder) {
+ super(out);
+ this.attributes = attributes;
+ this.dataBlock = dataBlock;
+ this.byteOrder = byteOrder;
+ }
+
+ @Override
+ protected void beforeWrite(int n) throws IOException {
+
+ if (start) {
+ writeHeader();
+ start = false;
+ }
+ }
+
+ private void writeHeader() throws IOException {
+ final DataOutput dos = getDataOutput(out);
+
+ final int mode;
+ if (attributes.getDataType() == DataType.OBJECT || dataBlock.getSize() == null)
+ mode = MODE_OBJECT;
+ else if (dataBlock.getNumElements() == DataBlock.getNumElements(dataBlock.getSize()))
+ mode = MODE_DEFAULT;
+ else
+ mode = MODE_VARLENGTH;
+
+ dos.writeShort(mode);
+
+ if (mode != MODE_OBJECT) {
+ dos.writeShort(attributes.getNumDimensions());
+ for (final int size : dataBlock.getSize())
+ dos.writeInt(size);
+ }
+
+ if (mode != MODE_DEFAULT)
+ dos.writeInt(dataBlock.getNumElements());
+ }
+
+ @Override
+ public DataOutput getDataOutput(final OutputStream outputStream) {
+
+ if (byteOrder.equals(ByteOrder.BIG_ENDIAN))
+ return new DataOutputStream(outputStream);
+ else
+ return new LittleEndianDataOutputStream(outputStream);
+ }
+ }
+
+ private static class N5DataBlockInputStream extends DataBlockInputStream {
+ private final DatasetAttributes attributes;
+ private final long[] gridPosition;
+ private final ByteOrder byteOrder;
+
+ private short mode = -1;
+ private int[] blockSize = null;
+ private int numElements = -1;
+
+ private boolean start = true;
+
+ N5DataBlockInputStream(final InputStream in, final DatasetAttributes attributes, final long[] gridPosition, ByteOrder byteOrder) {
+ super(in);
+ this.attributes = attributes;
+ this.gridPosition = gridPosition;
+ this.byteOrder = byteOrder;
+ }
+ @Override protected void beforeRead(int n) throws IOException {
+
+ if (start) {
+ readHeader();
+ start = false;
+ }
+ }
+
+ @Override
+ public DataBlock allocateDataBlock() throws IOException {
+ if (start) {
+ readHeader();
+ start = false;
+ }
+ if (mode == MODE_OBJECT) {
+ return (DataBlock) attributes.getDataType().createDataBlock(null, gridPosition, numElements);
+ }
+ return (DataBlock) attributes.getDataType().createDataBlock(blockSize, gridPosition, numElements);
+ }
+
+ private void readHeader() throws IOException {
+ final DataInput dis = getDataInput(in);
+ mode = dis.readShort();
+ if (mode == MODE_OBJECT) {
+ numElements = dis.readInt();
+ return;
+ }
+
+ final int nDim = dis.readShort();
+ blockSize = new int[nDim];
+ for (int d = 0; d < nDim; ++d)
+ blockSize[d] = dis.readInt();
+ if (mode == MODE_DEFAULT) {
+ numElements = DataBlock.getNumElements(blockSize);
+ } else {
+ numElements = dis.readInt();
+ }
+ }
+
+ @Override
+ public DataInput getDataInput(final InputStream inputStream) {
+
+ if (byteOrder.equals(ByteOrder.BIG_ENDIAN))
+ return new DataInputStream(inputStream);
+ else
+ return new LittleEndianDataInputStream(inputStream);
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/RawBytes.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/RawBytes.java
new file mode 100644
index 00000000..bb3232e4
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/RawBytes.java
@@ -0,0 +1,139 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import java.io.DataInput;
+import java.io.DataInputStream;
+import java.io.DataOutput;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteOrder;
+import java.util.Arrays;
+
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
+
+import com.google.common.io.LittleEndianDataInputStream;
+import com.google.common.io.LittleEndianDataOutputStream;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+
+import javax.annotation.Nullable;
+
+@NameConfig.Name(value = RawBytes.TYPE)
+public class RawBytes implements Codec.ArrayCodec {
+
+ private static final long serialVersionUID = 3282569607795127005L;
+
+ public static final String TYPE = "bytes";
+
+ @NameConfig.Parameter(value = "endian", optional = true)
+ protected final ByteOrder byteOrder;
+
+ public RawBytes() {
+
+ this(ByteOrder.LITTLE_ENDIAN);
+ }
+
+ public RawBytes(final ByteOrder byteOrder) {
+
+ this.byteOrder = byteOrder;
+ }
+
+ @Nullable
+ public ByteOrder getByteOrder() {
+ return byteOrder;
+ }
+
+ @Override
+ public DataBlockInputStream decode(final DatasetAttributes attributes, final long[] gridPosition, InputStream in)
+ throws IOException {
+
+ return new DataBlockInputStream(in) {
+
+ private int[] blockSize = attributes.getBlockSize();
+ private int numElements = Arrays.stream(blockSize).reduce(1, (x, y) -> x * y);
+
+ @Override
+ protected void beforeRead(int n) {}
+
+ @Override
+ public DataBlock> allocateDataBlock() {
+
+ return attributes.getDataType().createDataBlock(blockSize, gridPosition, numElements);
+ }
+
+ @Override
+ public DataInput getDataInput(final InputStream inputStream) {
+
+ if (byteOrder.equals(ByteOrder.BIG_ENDIAN))
+ return new DataInputStream(inputStream);
+
+ return new LittleEndianDataInputStream(inputStream);
+ }
+
+ };
+ }
+
+ @Override
+ public DataBlockOutputStream encode(final DatasetAttributes attributes, final DataBlock> dataBlock,
+ final OutputStream out)
+ throws IOException {
+
+ return new DataBlockOutputStream(out) {
+
+ @Override
+ protected void beforeWrite(int n) throws IOException {}
+
+ @Override
+ public DataOutput getDataOutput(OutputStream outputStream) {
+
+ if (byteOrder.equals(ByteOrder.BIG_ENDIAN))
+ return new DataOutputStream(outputStream);
+ else
+ return new LittleEndianDataOutputStream(outputStream);
+ }
+ };
+ }
+
+ @Override
+ public String getType() {
+
+ return TYPE;
+ }
+
+ public static final ByteOrderAdapter byteOrderAdapter = new ByteOrderAdapter();
+
+ public static class ByteOrderAdapter implements JsonDeserializer, JsonSerializer {
+
+ @Override
+ public JsonElement serialize(ByteOrder src, java.lang.reflect.Type typeOfSrc,
+ JsonSerializationContext context) {
+
+ if (src.equals(ByteOrder.LITTLE_ENDIAN))
+ return new JsonPrimitive("little");
+ else
+ return new JsonPrimitive("big");
+ }
+
+ @Override
+ public ByteOrder deserialize(JsonElement json, java.lang.reflect.Type typeOfT,
+ JsonDeserializationContext context) throws JsonParseException {
+
+ if (json.getAsString().equals("little"))
+ return ByteOrder.LITTLE_ENDIAN;
+ if (json.getAsString().equals("big"))
+ return ByteOrder.BIG_ENDIAN;
+
+ return null;
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumCodec.java
new file mode 100644
index 00000000..7d7a58fb
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumCodec.java
@@ -0,0 +1,119 @@
+package org.janelia.saalfeldlab.n5.codec.checksum;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+import java.util.zip.CheckedInputStream;
+import java.util.zip.CheckedOutputStream;
+import java.util.zip.Checksum;
+
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.Codec.BytesCodec;
+import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodec;
+
+/**
+ * A {@link Codec} that appends a checksum to data when encoding and can validate against that checksum when decoding.
+ */
+public abstract class ChecksumCodec implements BytesCodec, DeterministicSizeCodec {
+
+ private static final long serialVersionUID = 3141427377277375077L;
+
+ private int numChecksumBytes;
+
+ private Checksum checksum;
+
+ public ChecksumCodec(Checksum checksum, int numChecksumBytes) {
+
+ this.checksum = checksum;
+ this.numChecksumBytes = numChecksumBytes;
+ }
+
+ public Checksum getChecksum() {
+
+ return checksum;
+ }
+
+ public int numChecksumBytes() {
+
+ return numChecksumBytes;
+ }
+
+ @Override
+ public CheckedOutputStream encode(final OutputStream out) throws IOException {
+
+ // when do we validate?
+ return new CheckedOutputStream(out, getChecksum()) {
+
+ private boolean closed = false;
+ @Override public void close() throws IOException {
+
+ if (!closed) {
+ writeChecksum(out);
+ closed = true;
+ out.close();
+ }
+ }
+ };
+ }
+
+ @Override
+ public CheckedInputStream decode(final InputStream in) throws IOException {
+
+ // TODO get the correct expected checksum
+ // TODO write a test with nested checksum codecs
+
+ // has to know the number of it needs to read?
+ return new CheckedInputStream(in, getChecksum());
+ }
+
+ public ByteBuffer decodeAndValidate(final InputStream in, int numBytes) throws IOException, ChecksumException {
+
+ final CheckedInputStream cin = decode(in);
+ final byte[] data = new byte[numBytes];
+ cin.read(data);
+
+ if (!valid(in))
+ throw new ChecksumException("Invalid checksum");
+
+ return ByteBuffer.wrap(data);
+ }
+
+ @Override
+ public long encodedSize(final long size) {
+
+ return size + numChecksumBytes();
+ }
+
+ @Override
+ public long decodedSize(final long size) {
+
+ return size - numChecksumBytes();
+ }
+
+ protected boolean valid(InputStream in) throws IOException {
+
+ return readChecksum(in) == getChecksum().getValue();
+ }
+
+ protected long readChecksum(InputStream in) throws IOException {
+
+ final byte[] checksum = new byte[numChecksumBytes()];
+ in.read(checksum);
+ return ByteBuffer.wrap(checksum).getLong();
+ }
+
+ /**
+ * Return the value of the checksum as a {@link ByteBuffer} to be serialized.
+ *
+ * @return a ByteBuffer representing the checksum value
+ */
+ public abstract ByteBuffer getChecksumValue();
+
+ public void writeChecksum(OutputStream out) throws IOException {
+
+ out.write(getChecksumValue().array());
+ }
+
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumException.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumException.java
new file mode 100644
index 00000000..034343c4
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumException.java
@@ -0,0 +1,12 @@
+package org.janelia.saalfeldlab.n5.codec.checksum;
+
+public class ChecksumException extends Exception {
+
+ private static final long serialVersionUID = 905130066386622561L;
+
+ public ChecksumException(final String message) {
+
+ super(message);
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/Crc32cChecksumCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/Crc32cChecksumCodec.java
new file mode 100644
index 00000000..f7c03608
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/Crc32cChecksumCodec.java
@@ -0,0 +1,46 @@
+package org.janelia.saalfeldlab.n5.codec.checksum;
+
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
+
+import java.nio.ByteBuffer;
+import java.util.zip.CRC32;
+
+@NameConfig.Name(Crc32cChecksumCodec.TYPE)
+public class Crc32cChecksumCodec extends ChecksumCodec {
+
+ private static final long serialVersionUID = 7424151868725442500L;
+
+ public static final String TYPE = "crc32c";
+
+ public Crc32cChecksumCodec() {
+
+ super(new CRC32(), 4);
+ }
+
+ @Override
+ public long encodedSize(final long size) {
+
+ return size + numChecksumBytes();
+ }
+
+ @Override
+ public long decodedSize(final long size) {
+
+ return size - numChecksumBytes();
+ }
+
+ @Override
+ public ByteBuffer getChecksumValue() {
+
+ final ByteBuffer buf = ByteBuffer.allocate(numChecksumBytes());
+ buf.putInt((int)getChecksum().getValue());
+ return buf;
+ }
+
+ @Override
+ public String getType() {
+
+ return TYPE;
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/serialization/JsonArrayUtils.java b/src/main/java/org/janelia/saalfeldlab/n5/serialization/JsonArrayUtils.java
new file mode 100644
index 00000000..b65fbb6c
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/serialization/JsonArrayUtils.java
@@ -0,0 +1,20 @@
+package org.janelia.saalfeldlab.n5.serialization;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+
+public class JsonArrayUtils {
+
+ public static void reverse(final JsonArray array) {
+
+ JsonElement a;
+ final int max = array.size() - 1;
+ for (int i = (max - 1) / 2; i >= 0; --i) {
+ final int j = max - i;
+ a = array.get(i);
+ array.set(i, array.get(j));
+ array.set(j, a);
+ }
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/serialization/N5Annotations.java b/src/main/java/org/janelia/saalfeldlab/n5/serialization/N5Annotations.java
new file mode 100644
index 00000000..500f139f
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/serialization/N5Annotations.java
@@ -0,0 +1,18 @@
+package org.janelia.saalfeldlab.n5.serialization;
+
+import java.io.Serializable;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+public interface N5Annotations extends Serializable {
+
+ @Inherited
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.FIELD)
+ @interface ReverseArray {
+ }
+}
+
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/serialization/NameConfig.java b/src/main/java/org/janelia/saalfeldlab/n5/serialization/NameConfig.java
new file mode 100644
index 00000000..2ccb122e
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/serialization/NameConfig.java
@@ -0,0 +1,44 @@
+package org.janelia.saalfeldlab.n5.serialization;
+
+import org.scijava.annotations.Indexable;
+
+import java.io.Serializable;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+public interface NameConfig extends Serializable {
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Inherited
+ @Target(ElementType.TYPE)
+ @interface Prefix {
+ String value();
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Inherited
+ @Target(ElementType.TYPE)
+ @Indexable
+ @interface Name {
+ String value();
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Inherited
+ @Target(ElementType.FIELD)
+ @interface Parameter {
+ String value() default "";
+ boolean optional() default false;
+ }
+
+ default String getType() {
+
+ final Name type = getClass().getAnnotation(Name.class);
+ return type == null ? null : type.value();
+
+ }
+}
+
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/AbstractShard.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/AbstractShard.java
new file mode 100644
index 00000000..4e2ac831
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/AbstractShard.java
@@ -0,0 +1,52 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+
+
+public abstract class AbstractShard implements Shard {
+
+ protected final DatasetAttributes datasetAttributes;
+
+ protected ShardIndex index;
+
+ private final long[] gridPosition;
+
+ public AbstractShard(final DatasetAttributes datasetAttributes, final long[] gridPosition,
+ final ShardIndex index) {
+
+ this.datasetAttributes = datasetAttributes;
+ this.gridPosition = gridPosition;
+ this.index = index;
+ }
+
+ @Override
+ public DatasetAttributes getDatasetAttributes() {
+
+ return datasetAttributes;
+ }
+
+ @Override
+ public int[] getSize() {
+
+ return getDatasetAttributes().getShardSize();
+ }
+
+ @Override
+ public int[] getBlockSize() {
+
+ return datasetAttributes.getBlockSize();
+ }
+
+ @Override
+ public long[] getGridPosition() {
+
+ return gridPosition;
+ }
+
+ @Override
+ public ShardIndex getIndex() {
+
+ return index;
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/InMemoryShard.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/InMemoryShard.java
new file mode 100644
index 00000000..c7274d85
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/InMemoryShard.java
@@ -0,0 +1,312 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import org.apache.commons.io.input.BoundedInputStream;
+import org.apache.commons.io.output.ByteArrayOutputStream;
+import org.apache.commons.io.output.CountingOutputStream;
+import org.apache.commons.io.output.ProxyOutputStream;
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.DefaultBlockReader;
+import org.janelia.saalfeldlab.n5.DefaultBlockWriter;
+import org.janelia.saalfeldlab.n5.KeyValueAccess;
+import org.janelia.saalfeldlab.n5.LockedChannel;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec.IndexLocation;
+import org.janelia.saalfeldlab.n5.util.GridIterator;
+import org.janelia.saalfeldlab.n5.util.Position;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class InMemoryShard extends AbstractShard {
+
+ /* Map of a hash of the DataBlocks `gridPosition` to the block */
+ private final Map> blocks;
+ private ShardIndexBuilder indexBuilder;
+
+ /*
+ * TODO:
+ * Use morton- or c-ording instead of writing blocks out in the order they're added?
+ * (later)
+ */
+ public InMemoryShard(final DatasetAttributes datasetAttributes, final long[] shardPosition) {
+
+ this( datasetAttributes, shardPosition, null);
+ indexBuilder = new ShardIndexBuilder(this);
+ final IndexLocation indexLocation = ((ShardingCodec)datasetAttributes.getArrayCodec()).getIndexLocation();
+ indexBuilder.indexLocation(indexLocation);
+ }
+
+ public InMemoryShard(final DatasetAttributes datasetAttributes, final long[] gridPosition,
+ ShardIndex index) {
+
+ super(datasetAttributes, gridPosition, index);
+ blocks = new TreeMap<>();
+ }
+
+ private void storeBlock(DataBlock block) {
+
+ blocks.put(Position.wrap(block.getGridPosition()), block);
+ }
+
+ /*
+ * Returns the {@link DataBlock} given a block grid position.
+ *
+ * The block grid position is relative to the image, not relative to this shard.
+ */
+ @Override public DataBlock getBlock(long... blockGridPosition) {
+
+ return blocks.get(Position.wrap(blockGridPosition));
+ }
+
+ @Override
+ public void writeBlock(DataBlock block) {
+
+ addBlock(block);
+ }
+
+ public void addBlock(DataBlock block) {
+
+ storeBlock(block);
+ }
+
+ public int numBlocks() {
+
+ return blocks.size();
+ }
+
+ @Override
+ public List> getBlocks() {
+
+ return new ArrayList<>(blocks.values());
+ }
+
+ public List> getBlocks( int[] blockIndexes ) {
+
+ final ArrayList> out = new ArrayList<>();
+ final int[] blocksPerShard = getDatasetAttributes().getBlocksPerShard();
+
+ long[] position = new long[ getSize().length ];
+ for( int idx : blockIndexes ) {
+ GridIterator.indexToPosition(idx, blocksPerShard, position);
+ DataBlock blk = getBlock(position);
+ if( blk != null )
+ out.add(blk);
+ }
+ return out;
+ }
+
+ protected IndexLocation indexLocation() {
+
+ if (index != null)
+ return index.getLocation();
+ else
+ return indexBuilder.getLocation();
+ }
+
+ @Override
+ public ShardIndex getIndex() {
+
+ if( index != null )
+ return index;
+ else
+ return indexBuilder.build();
+ }
+
+ public void write(final KeyValueAccess keyValueAccess, final String path) throws IOException {
+
+ try (final LockedChannel lockedChannel = keyValueAccess.lockForWriting(path)) {
+ try (final OutputStream os = lockedChannel.newOutputStream()) {
+ write(os);
+ }
+ }
+ }
+
+ public void write(final OutputStream out) throws IOException {
+
+ if (indexLocation() == IndexLocation.END)
+ writeShardEndStream(out, this);
+ else
+ writeShardStart(out, this);
+ }
+
+ public static InMemoryShard readShard(
+ final KeyValueAccess kva, final String key, final long[] gridPosition, final DatasetAttributes attributes)
+ throws IOException {
+
+ try (final LockedChannel lockedChannel = kva.lockForReading(key)) {
+ try (final InputStream is = lockedChannel.newInputStream()) {
+ return readShard(is, gridPosition, attributes);
+ }
+ }
+
+ // Another possible implementation
+// return fromShard(new VirtualShard<>(attributes, gridPosition, kva, key));
+ }
+
+ @SuppressWarnings("hiding")
+ public static InMemoryShard readShard(
+ final InputStream inputStream, final long[] gridPosition, final DatasetAttributes attributes) throws IOException {
+
+ try (ByteArrayOutputStream result = new ByteArrayOutputStream()) {
+ byte[] buffer = new byte[1024];
+ for (int length; (length = inputStream.read(buffer)) != -1;) {
+ result.write(buffer, 0, length);
+ }
+ return readShard(result.toByteArray(), gridPosition, attributes);
+ }
+ }
+
+ public static InMemoryShard readShard(
+ final byte[] data,
+ long[] shardPosition, final DatasetAttributes attributes) throws IOException {
+
+ final ShardIndex index = ((ShardingCodec)attributes.getArrayCodec()).createIndex(attributes);
+ ShardIndex.read(data, index);
+
+ final InMemoryShard shard = new InMemoryShard(attributes, shardPosition, index);
+ final GridIterator it = new GridIterator(attributes.getBlocksPerShard());
+ while (it.hasNext()) {
+
+ final long[] p = it.next();
+ final int[] pInt = GridIterator.long2int(p);
+
+ if (index.exists(pInt)) {
+
+ final ByteArrayInputStream is = new ByteArrayInputStream(data);
+ is.skip(index.getOffset(pInt));
+ BoundedInputStream bIs = BoundedInputStream.builder().setInputStream(is)
+ .setMaxCount(index.getNumBytes(pInt)).get();
+
+ final long[] blockGridPosition = attributes.getBlockPositionFromShardPosition(shardPosition, p);
+ @SuppressWarnings("unchecked")
+ final DataBlock blk = (DataBlock) DefaultBlockReader.readBlock(bIs, attributes,
+ blockGridPosition);
+ shard.addBlock(blk);
+ bIs.close();
+ }
+ }
+
+ return shard;
+ }
+
+ public static void writeShard(final KeyValueAccess keyValueAccess, final String path, final InMemoryShard shard) throws IOException {
+
+ try (final LockedChannel lockedChannel = keyValueAccess.lockForWriting(path)) {
+ try (final OutputStream os = lockedChannel.newOutputStream()) {
+ writeShard(os, shard);
+ }
+ }
+ }
+
+ public static void writeShard(final OutputStream out, final Shard shard) throws IOException {
+
+ fromShard(shard).write(out);
+ }
+
+ public static InMemoryShard fromShard(Shard shard) {
+
+ if (shard instanceof InMemoryShard)
+ return (InMemoryShard) shard;
+
+ final InMemoryShard inMemoryShard = new InMemoryShard(
+ shard.getDatasetAttributes(),
+ shard.getGridPosition());
+
+ shard.forEach(blk -> inMemoryShard.addBlock(blk));
+ return inMemoryShard;
+ }
+
+ protected static void writeShardEndStream(
+ final OutputStream out,
+ InMemoryShard shard ) throws IOException {
+
+ final DatasetAttributes datasetAttributes = shard.getDatasetAttributes();
+
+ final ShardIndexBuilder indexBuilder = new ShardIndexBuilder(shard);
+ indexBuilder.indexLocation(IndexLocation.END);
+ final ShardingCodec shardingCodec = (ShardingCodec)datasetAttributes.getArrayCodec();
+ indexBuilder.setCodecs(shardingCodec.getIndexCodecs());
+
+ // Necessary to stop `close()` when writing blocks from closing out base OutputStream
+ final ProxyOutputStream nop = new ProxyOutputStream(out) {
+ @Override public void close() {
+ //nop
+ }
+ };
+
+ final CountingOutputStream cout = new CountingOutputStream(nop);
+
+ long bytesWritten = 0;
+ for (DataBlock block : shard.getBlocks()) {
+ DefaultBlockWriter.writeBlock(cout, datasetAttributes, block);
+ final long size = cout.getByteCount() - bytesWritten;
+ bytesWritten = cout.getByteCount();
+
+ indexBuilder.addBlock( block.getGridPosition(), size);
+ }
+
+ ShardIndex.write(indexBuilder.build(), out);
+ }
+
+ protected static void writeShardEnd(
+ final OutputStream out,
+ InMemoryShard shard ) throws IOException {
+
+ final ShardIndexBuilder indexBuilder = new ShardIndexBuilder(shard);
+ indexBuilder.indexLocation(IndexLocation.END);
+ final DatasetAttributes datasetAttributes = shard.getDatasetAttributes();
+ final ShardingCodec shardingCodec = (ShardingCodec)datasetAttributes.getArrayCodec();
+ indexBuilder.setCodecs(shardingCodec.getIndexCodecs());
+
+ for (DataBlock block : shard.getBlocks()) {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ DefaultBlockWriter.writeBlock(os, datasetAttributes, block);
+
+ indexBuilder.addBlock(block.getGridPosition(), os.size());
+ out.write(os.toByteArray());
+ }
+
+ ShardIndex.write(indexBuilder.build(), out);
+ }
+
+ protected static void writeShardStart(
+ final OutputStream out,
+ InMemoryShard shard ) throws IOException {
+
+ final DatasetAttributes datasetAttributes = shard.getDatasetAttributes();
+ final ShardingCodec shardingCodec = (ShardingCodec)datasetAttributes.getArrayCodec();
+
+ final ShardIndexBuilder indexBuilder = new ShardIndexBuilder(shard);
+ indexBuilder.indexLocation(IndexLocation.START);
+ indexBuilder.setCodecs(shardingCodec.getIndexCodecs());
+
+ final List blockData = new ArrayList<>(shard.numBlocks());
+ for (DataBlock block : shard.getBlocks()) {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ DefaultBlockWriter.writeBlock(os, datasetAttributes, block);
+
+ blockData.add(os.toByteArray());
+ indexBuilder.addBlock(block.getGridPosition(), os.size());
+ }
+
+ try {
+ final ByteArrayOutputStream os = new ByteArrayOutputStream();
+ ShardIndex.write(indexBuilder.build(), os);
+ out.write(os.toByteArray());
+
+ for( byte[] data : blockData )
+ out.write(data);
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/Shard.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/Shard.java
new file mode 100644
index 00000000..3f55dfbc
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/Shard.java
@@ -0,0 +1,182 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.util.GridIterator;
+
+public interface Shard extends Iterable> {
+
+ /**
+ * Returns the number of blocks this shard contains along all dimensions.
+ *
+ * The size of a shard expected to be smaller than or equal to the spacing of the shard grid. The dimensionality of size is expected to be equal to the dimensionality of the
+ * dataset. Consistency is not enforced.
+ *
+ * @return size of the shard in units of blocks
+ */
+ default int[] getBlockGridSize() {
+
+ return getDatasetAttributes().getBlocksPerShard();
+ }
+
+ DatasetAttributes getDatasetAttributes();
+
+ /**
+ * Returns the size of shards in pixel units.
+ *
+ * @return shard size
+ */
+ default int[] getSize() {
+ return getDatasetAttributes().getShardSize();
+ }
+
+ /**
+ * Returns the size of blocks in pixel units.
+ *
+ * @return block size
+ */
+ default int[] getBlockSize() {
+ return getDatasetAttributes().getBlockSize();
+ }
+
+ /**
+ * Returns the position of this shard on the shard grid.
+ *
+ * The dimensionality of the grid position is expected to be equal to the dimensionality of the dataset. Consistency is not enforced.
+ *
+ * @return position on the shard grid
+ */
+ public long[] getGridPosition();
+
+ /**
+ * Returns of the block at the given position relative to this shard, or null if this shard does not contain the given block.
+ *
+ * @return the shard position
+ */
+ default int[] getBlockPosition(long... blockPosition) {
+
+ final long[] shardPos = getDatasetAttributes().getShardPositionForBlock(blockPosition);
+ return getDatasetAttributes().getBlockPositionInShard(shardPos, blockPosition);
+ }
+
+ /**
+ * Returns the position in pixels of the
+ *
+ * @return the min
+ */
+ default long[] getShardMinPosition(long... shardPosition) {
+
+ final int[] shardSize = getSize();
+ final long[] shardMin = new long[shardSize.length];
+ for (int i = 0; i < shardSize.length; i++) {
+ shardMin[i] = shardPosition[i] * shardSize[i];
+ }
+ return shardMin;
+ }
+
+ /**
+ * Returns the position of the shard containing the block with the given block position.
+ *
+ * @return the shard position
+ */
+ default long[] getShardPosition(long... blockPosition) {
+
+ final int[] shardBlockDimensions = getBlockGridSize();
+ final long[] shardGridPosition = new long[shardBlockDimensions.length];
+ for (int i = 0; i < shardGridPosition.length; i++) {
+ shardGridPosition[i] = (long)Math.floor((double)(blockPosition[i]) / shardBlockDimensions[i]);
+ }
+
+ return shardGridPosition;
+ }
+
+ public DataBlock getBlock(long... blockGridPosition);
+
+ public void writeBlock(DataBlock block);
+
+ //TODO Caleb: add writeBlocks that does NOT always expect to overwrite the entire existing Shard
+
+ default Iterator> iterator() {
+
+ return new DataBlockIterator<>(this);
+ }
+
+ default int getNumBlocks() {
+
+ return Arrays.stream(getBlockGridSize()).reduce(1, (x, y) -> x * y);
+ }
+
+ default List> getBlocks() {
+
+ final List> blocks = new ArrayList<>();
+ for (DataBlock block : this) {
+ blocks.add(block);
+ }
+ return blocks;
+ }
+
+ /**
+ * Returns an {@link Iterator} over block positions contained in this shard.
+ *
+ * @return
+ */
+ default Iterator blockPositionIterator() {
+
+ final int nd = getSize().length;
+ long[] min = getDatasetAttributes().getBlockPositionFromShardPosition( getGridPosition(), new long[nd]);
+ return new GridIterator(GridIterator.int2long(getBlockGridSize()), min);
+ }
+
+ ShardIndex getIndex();
+
+ static Shard createEmpty(final A attributes, long... shardPosition) {
+
+ final long[] emptyIndex = new long[(int)(2 * attributes.getNumBlocks())];
+ Arrays.fill(emptyIndex, ShardIndex.EMPTY_INDEX_NBYTES);
+ final ShardIndex shardIndex = new ShardIndex(attributes.getBlocksPerShard(), emptyIndex, ShardingCodec.IndexLocation.END);
+ return new InMemoryShard(attributes, shardPosition, shardIndex);
+ }
+
+ class DataBlockIterator implements Iterator> {
+
+ private final GridIterator it;
+ private final Shard shard;
+ private final ShardIndex index;
+ // TODO ShardParameters is deprecated?
+ private final ShardParameters attributes;
+ private int blockIndex = 0;
+
+ public DataBlockIterator(final Shard shard) {
+
+ this.shard = shard;
+ this.index = shard.getIndex();
+ this.attributes = shard.getDatasetAttributes();
+ this.blockIndex = 0;
+ it = new GridIterator(shard.getBlockGridSize());
+ }
+
+ @Override
+ public boolean hasNext() {
+
+ for (int i = blockIndex; i < attributes.getNumBlocks(); i++) {
+ if (index.exists(i))
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public DataBlock next() {
+ while (!index.exists(blockIndex++))
+ it.fwd();
+
+ return shard.getBlock(it.next());
+ }
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardException.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardException.java
new file mode 100644
index 00000000..d208c62e
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardException.java
@@ -0,0 +1,14 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import org.janelia.saalfeldlab.n5.N5Exception;
+
+public class ShardException extends N5Exception {
+
+ private static final long serialVersionUID = -77907634621557855L;
+
+ public static class IndexException extends ShardException {
+
+ private static final long serialVersionUID = 3924426352575114063L;
+
+ }
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardIndex.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardIndex.java
new file mode 100644
index 00000000..655bf62c
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardIndex.java
@@ -0,0 +1,323 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import org.apache.commons.io.input.BoundedInputStream;
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.DefaultBlockReader;
+import org.janelia.saalfeldlab.n5.DefaultBlockWriter;
+import org.janelia.saalfeldlab.n5.KeyValueAccess;
+import org.janelia.saalfeldlab.n5.LockedChannel;
+import org.janelia.saalfeldlab.n5.LongArrayDataBlock;
+import org.janelia.saalfeldlab.n5.N5Exception;
+import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodec;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec.IndexLocation;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.nio.channels.Channels;
+import java.nio.channels.FileChannel;
+import java.util.Arrays;
+import java.util.stream.IntStream;
+
+public class ShardIndex extends LongArrayDataBlock {
+
+ public static final long EMPTY_INDEX_NBYTES = 0xFFFFFFFFFFFFFFFFL;
+ private static final int BYTES_PER_LONG = 8;
+ private static final int LONGS_PER_BLOCK = 2;
+ private static final long[] DUMMY_GRID_POSITION = null;
+
+ private final IndexLocation location;
+
+ private final DeterministicSizeCodec[] codecs;
+
+ public ShardIndex(int[] shardBlockGridSize, long[] data, IndexLocation location, final DeterministicSizeCodec... codecs) {
+
+ super(prepend(LONGS_PER_BLOCK, shardBlockGridSize), DUMMY_GRID_POSITION, data);
+ this.codecs = codecs;
+ this.location = location;
+ }
+
+ public ShardIndex(int[] shardBlockGridSize, IndexLocation location, DeterministicSizeCodec... codecs) {
+
+ this(shardBlockGridSize, emptyIndexData(shardBlockGridSize), location, codecs);
+ }
+
+ public ShardIndex(int[] shardBlockGridSize, DeterministicSizeCodec... codecs) {
+
+ this(shardBlockGridSize, emptyIndexData(shardBlockGridSize), IndexLocation.END, codecs);
+ }
+
+ public boolean exists(int[] gridPosition) {
+
+ return getOffset(gridPosition) != EMPTY_INDEX_NBYTES ||
+ getNumBytes(gridPosition) != EMPTY_INDEX_NBYTES;
+ }
+
+ public boolean exists(int blockNum) {
+
+ return data[blockNum * 2] != EMPTY_INDEX_NBYTES ||
+ data[blockNum * 2 + 1] != EMPTY_INDEX_NBYTES;
+ }
+
+ public int getNumBlocks() {
+
+ return Arrays.stream(getSize()).reduce(1, (x, y) -> x * y);
+ }
+
+ public boolean isEmpty() {
+
+ return !IntStream.range(0, getNumBlocks()).anyMatch(this::exists);
+ }
+
+ public IndexLocation getLocation() {
+
+ return location;
+ }
+
+ public long getOffset(int... gridPosition) {
+
+ return data[getOffsetIndex(gridPosition)];
+ }
+
+ public long getOffsetByBlockIndex(int index) {
+ return data[index * 2];
+ }
+
+ public long getNumBytes(int... gridPosition) {
+
+ return data[getNumBytesIndex(gridPosition)];
+ }
+
+ public long getNumBytesByBlockIndex(int index) {
+
+ return data[index * 2 + 1];
+ }
+
+ public void set(long offset, long nbytes, int[] gridPosition) {
+
+ final int i = getOffsetIndex(gridPosition);
+ data[i] = offset;
+ data[i + 1] = nbytes;
+ }
+
+ protected int getOffsetIndex(int... gridPosition) {
+
+ int idx = (int) gridPosition[0];
+ int cumulativeSize = 1;
+ for (int i = 1; i < gridPosition.length; i++) {
+ cumulativeSize *= size[i];
+ idx += gridPosition[i] * cumulativeSize;
+ }
+ return idx * 2;
+ }
+
+ protected int getNumBytesIndex(int... gridPosition) {
+
+ return getOffsetIndex(gridPosition) + 1;
+ }
+
+ public long numBytes() {
+
+ final int numEntries = Arrays.stream(getSize()).reduce(1, (x, y) -> x * y);
+ final int numBytesFromBlocks = numEntries * BYTES_PER_LONG;
+ long totalNumBytes = numBytesFromBlocks;
+ for (Codec codec : codecs) {
+ if (codec instanceof DeterministicSizeCodec) {
+ totalNumBytes = ((DeterministicSizeCodec)codec).encodedSize(totalNumBytes);
+ }
+ }
+ return totalNumBytes;
+ }
+
+ public static boolean read(byte[] data, final ShardIndex index) {
+
+ final IndexByteBounds byteBounds = byteBounds(index, data.length);
+ final ByteArrayInputStream is = new ByteArrayInputStream(data);
+ is.skip(byteBounds.start);
+ try {
+ BoundedInputStream bIs = BoundedInputStream.builder()
+ .setInputStream(is)
+ .setMaxCount(index.numBytes()).get();
+
+ read(bIs, index);
+ return true;
+ } catch (IOException e) {
+ return false;
+ }
+ }
+
+ public static void read(InputStream in, final ShardIndex index) throws IOException {
+
+ @SuppressWarnings("unchecked")
+ final DataBlock indexBlock = (DataBlock) DefaultBlockReader.readBlock(in,
+ index.getIndexAttributes(), index.gridPosition);
+ final long[] indexData = indexBlock.getData();
+ System.arraycopy(indexData, 0, index.data, 0, index.data.length);
+ }
+
+ public static boolean read(
+ final KeyValueAccess keyValueAccess,
+ final String key,
+ final ShardIndex index
+ ) {
+
+ try {
+ final IndexByteBounds byteBounds = byteBounds(index, keyValueAccess.size(key));
+ try (final LockedChannel lockedChannel = keyValueAccess.lockForReading(key, byteBounds.start, byteBounds.end)) {
+ try (final InputStream in = lockedChannel.newInputStream()) {
+ read(in,index);
+ return true;
+ }
+ } catch (final IOException | UncheckedIOException e) {
+ throw new N5IOException("Failed to read shard index from " + key, e);
+ }
+ } catch (final IOException | N5Exception.N5NoSuchKeyException e) {
+ return false;
+ }
+ }
+
+ public static void write(
+ final ShardIndex index,
+ final KeyValueAccess keyValueAccess,
+ final String key
+ ) throws IOException {
+
+ final long start = index.location == IndexLocation.START ? 0 : sizeOrZero( keyValueAccess, key) ;
+ try (final LockedChannel lockedChannel = keyValueAccess.lockForWriting(key, start, index.numBytes())) {
+ try (final OutputStream os = lockedChannel.newOutputStream()) {
+ write(index, os);
+ }
+ } catch (final IOException | UncheckedIOException e) {
+ throw new N5IOException("Failed to write shard index to " + key, e);
+ }
+ }
+
+ private static long sizeOrZero(final KeyValueAccess keyValueAccess, final String key) {
+ try {
+ return keyValueAccess.size(key);
+ } catch (Exception e) {
+ return 0;
+ }
+ }
+
+ public static void write(final ShardIndex index, OutputStream out) throws IOException {
+
+ DefaultBlockWriter.writeBlock(out, index.getIndexAttributes(), index);
+ }
+
+ private DatasetAttributes getIndexAttributes() {
+
+ final DatasetAttributes indexAttributes =
+ new DatasetAttributes(
+ Arrays.stream(getSize()).mapToLong(it -> it).toArray(),
+ getSize(),
+ DataType.UINT64,
+ codecs
+ );
+ return indexAttributes;
+ }
+
+ public static IndexByteBounds byteBounds(DatasetAttributes datasetAttributes, final long objectSize) {
+
+ ShardingCodec shardCodec = (ShardingCodec)datasetAttributes.getArrayCodec();
+ final ShardIndex index = shardCodec.createIndex(datasetAttributes);
+
+ final long indexSize = index.numBytes();
+ return byteBounds(indexSize, index.location, objectSize);
+ }
+
+ public static IndexByteBounds byteBounds(final ShardIndex index, long objectSize) {
+ return byteBounds(index.numBytes(), index.location, objectSize);
+ }
+
+ public static IndexByteBounds byteBounds(final long indexSize, final IndexLocation indexLocation, final long objectSize) {
+
+ if (indexLocation == IndexLocation.START) {
+ return new IndexByteBounds(0L, indexSize);
+ } else {
+ return new IndexByteBounds(objectSize - indexSize, objectSize - 1);
+ }
+ }
+
+ public static class IndexByteBounds {
+
+ public final long start;
+ public final long end;
+ public final long size;
+
+ public IndexByteBounds(long start, long end) {
+
+ this.start = start;
+ this.end = end;
+ this.size = end - start + 1;
+ }
+ }
+
+ //TODO Caleb: Probably don't need to keep this eventually
+ public static ShardIndex read(FileChannel channel, DatasetAttributes datasetAttributes) throws IOException {
+
+ // TODO need codecs
+ // TODO FileChannel is too specific - generalize
+ ShardingCodec shardingCodec = (ShardingCodec)datasetAttributes.getArrayCodec();
+ final int[] indexShape = prepend(2, datasetAttributes.getBlocksPerShard());
+ final int indexSize = (int)Arrays.stream(indexShape).reduce(1, (x, y) -> x * y);
+ final int indexBytes = BYTES_PER_LONG * indexSize;
+
+ if (shardingCodec.getIndexLocation() == IndexLocation.END) {
+ channel.position(channel.size() - indexBytes);
+ }
+
+ final InputStream is = Channels.newInputStream(channel);
+ final DataInputStream dis = new DataInputStream(is);
+
+ final long[] indexes = new long[indexSize];
+ for (int i = 0; i < indexSize; i++) {
+ indexes[i] = dis.readLong();
+ }
+
+ return new ShardIndex(indexShape, indexes, IndexLocation.END);
+ }
+
+ private static long[] emptyIndexData(final int[] size) {
+
+ final int N = 2 * Arrays.stream(size).reduce(1, (x, y) -> x * y);
+ final long[] data = new long[N];
+ Arrays.fill(data, EMPTY_INDEX_NBYTES);
+ return data;
+ }
+
+ private static int[] prepend(final int value, final int[] array) {
+
+ final int[] indexBlockSize = new int[array.length + 1];
+ indexBlockSize[0] = value;
+ System.arraycopy(array, 0, indexBlockSize, 1, array.length);
+ return indexBlockSize;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+
+ if (other instanceof ShardIndex) {
+
+ final ShardIndex index = (ShardIndex) other;
+ if (this.location != index.location)
+ return false;
+
+ if (!Arrays.equals(this.size, index.size))
+ return false;
+
+ if (!Arrays.equals(this.data, index.data))
+ return false;
+
+ }
+ return true;
+ }
+}
+
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardIndexBuilder.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardIndexBuilder.java
new file mode 100644
index 00000000..41d505af
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardIndexBuilder.java
@@ -0,0 +1,86 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import java.util.Arrays;
+
+import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodec;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec.IndexLocation;
+
+public class ShardIndexBuilder {
+
+ private final Shard> shard;
+
+ private ShardIndex temporaryIndex;
+
+ private IndexLocation location = IndexLocation.END;
+
+ private DeterministicSizeCodec[] codecs;
+
+ private long currentOffset = 0;
+
+ public ShardIndexBuilder(Shard> shard) {
+
+ this.shard = shard;
+ this.temporaryIndex = new ShardIndex(shard.getBlockGridSize(), location);
+ }
+
+ public ShardIndex build() {
+
+ return new ShardIndex(
+ shard.getBlockGridSize(),
+ temporaryIndex.getData(),
+ location,
+ codecs);
+ }
+
+ public ShardIndexBuilder indexLocation(IndexLocation location) {
+
+ this.location = location;
+ this.temporaryIndex = new ShardIndex(shard.getBlockGridSize(), location);
+ updateInitialOffset();
+ return this;
+ }
+
+ public IndexLocation getLocation() {
+
+ return this.location;
+ }
+
+ public ShardIndexBuilder setCodecs(DeterministicSizeCodec... codecs) {
+
+ this.codecs = codecs;
+ final ShardIndex newIndex = new ShardIndex(shard.getBlockGridSize(), temporaryIndex.getLocation(), codecs);
+ this.temporaryIndex = newIndex;
+ updateInitialOffset();
+ return this;
+ }
+
+ public ShardIndexBuilder addBlock(long[] blockPosition, long numBytes) {
+ //TODO Caleb: Maybe move to ShardIndex?
+ final int[] blockPositionInShard = shard.getDatasetAttributes().getBlockPositionInShard(
+ shard.getGridPosition(),
+ blockPosition);
+
+ if (blockPositionInShard == null) {
+ throw new IllegalArgumentException(String.format(
+ "The block at position %s is not contained in the shard at position : %s and size : %s )",
+ Arrays.toString(blockPosition),
+ Arrays.toString(shard.getGridPosition()),
+ Arrays.toString(shard.getSize())));
+ }
+
+ temporaryIndex.set(currentOffset, numBytes, blockPositionInShard);
+ currentOffset += numBytes;
+
+ return this;
+ }
+
+ private void updateInitialOffset() {
+
+ if (location == IndexLocation.END)
+ currentOffset = 0;
+ else
+ currentOffset = temporaryIndex.numBytes();
+
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardParameters.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardParameters.java
new file mode 100644
index 00000000..1791c944
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardParameters.java
@@ -0,0 +1,227 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.TreeMap;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.janelia.saalfeldlab.n5.BlockParameters;
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.util.GridIterator;
+import org.janelia.saalfeldlab.n5.util.Position;
+
+import javax.annotation.CheckForNull;
+
+@Deprecated
+public interface ShardParameters extends BlockParameters {
+
+
+ /**
+ * The size of the blocks in pixel units.
+ *
+ * @return the number of pixels per dimension for this shard.
+ */
+ @CheckForNull
+ int[] getShardSize();
+
+
+ /**
+ * Returns the number of blocks per dimension for a shard.
+ *
+ * @return the size of the block grid of a shard
+ */
+ default int[] getBlocksPerShard() {
+
+ final int[] shardSize = getShardSize();
+ Objects.requireNonNull(shardSize, "getShardSize() must not be null");
+ final int nd = getNumDimensions();
+ final int[] blocksPerShard = new int[nd];
+ final int[] blockSize = getBlockSize();
+ for (int i = 0; i < nd; i++)
+ blocksPerShard[i] = shardSize[i] / blockSize[i];
+
+ return blocksPerShard;
+ }
+
+ /**
+ * Returns the number of blocks per dimension that tile the image.
+ *
+ * @return blocks per image
+ */
+ default long[] blocksPerImage() {
+ return IntStream.range(0, getNumDimensions())
+ .mapToLong(i -> (long) Math.ceil(getDimensions()[i] / getBlockSize()[i]))
+ .toArray();
+ }
+
+ /**
+ * Returns the number of shards per dimension that tile the image.
+ *
+ * @return shards per image
+ */
+ default long[] shardsPerImage() {
+ return IntStream.range(0, getNumDimensions())
+ .mapToLong(i -> (long)Math.ceil(getDimensions()[i] / getShardSize()[i]))
+ .toArray();
+ }
+
+ /**
+ * Given a block's position relative to the array, returns the position of the shard containing that block relative to the shard grid.
+ *
+ * @param blockGridPosition
+ * position of a block relative to the array
+ * @return the position of the containing shard in the shard grid
+ */
+ default long[] getShardPositionForBlock(final long... blockGridPosition) {
+
+ final int[] blocksPerShard = getBlocksPerShard();
+ final long[] shardGridPosition = new long[blockGridPosition.length];
+ for (int i = 0; i < shardGridPosition.length; i++) {
+ shardGridPosition[i] = (int)Math.floor((double)blockGridPosition[i] / blocksPerShard[i]);
+ }
+
+ return shardGridPosition;
+ }
+
+ /**
+ * Returns the number of shards per dimension for the dataset.
+ *
+ * @return the size of the shard grid of a dataset
+ */
+ default int[] getShardBlockGridSize() {
+
+ final int nd = getNumDimensions();
+ final int[] shardBlockGridSize = new int[nd];
+ final int[] blockSize = getBlockSize();
+ for (int i = 0; i < nd; i++)
+ shardBlockGridSize[i] = (int)(Math.ceil((double)getDimensions()[i] / blockSize[i]));
+
+ return shardBlockGridSize;
+ }
+
+ /**
+ * Returns the block at the given position relative to this shard, or null if this shard does not contain the given block.
+ *
+ * @return the block position
+ */
+ default int[] getBlockPositionInShard(final long[] shardPosition, final long[] blockPosition) {
+
+ // TODO check correctness
+ final long[] shardPos = getShardPositionForBlock(blockPosition);
+ if (!Arrays.equals(shardPosition, shardPos))
+ return null;
+
+ final int[] shardSize = getBlocksPerShard();
+ final int[] blockShardPos = new int[shardSize.length];
+ for (int i = 0; i < shardSize.length; i++) {
+ blockShardPos[i] = (int)(blockPosition[i] % shardSize[i]);
+ }
+
+ return blockShardPos;
+ }
+
+ /**
+ * Given a block's position relative to a shard, returns its position in pixels
+ * relative to the image.
+ *
+ * @param shardPosition shard position in the shard grid
+ * @param blockPosition block position the
+ * @return the block's min pixel coordinate
+ */
+ default long[] getBlockMinFromShardPosition(final long[] shardPosition, final long[] blockPosition) {
+
+ // is this useful?
+ final int[] blockSize = getBlockSize();
+ final int[] shardSize = getShardSize();
+ Objects.requireNonNull(shardSize, "getShardSize() must not be null");
+ final long[] blockImagePos = new long[shardSize.length];
+ for (int i = 0; i < shardSize.length; i++) {
+ blockImagePos[i] = (shardPosition[i] * shardSize[i]) + (blockPosition[i] * blockSize[i]);
+ }
+
+ return blockImagePos;
+ }
+
+ /**
+ * Given a block's position relative to a shard, returns its position relative
+ * to the image.
+ *
+ * @param shardPosition shard position in the shard grid
+ * @param blockPosition block position relative to the shard
+ * @return the block position in the block grid
+ */
+ default long[] getBlockPositionFromShardPosition(final long[] shardPosition, final long[] blockPosition) {
+
+ // is this useful?
+ final int[] shardBlockSize = getBlocksPerShard();
+ final long[] blockImagePos = new long[getNumDimensions()];
+ for (int i = 0; i < getNumDimensions(); i++) {
+ blockImagePos[i] = (shardPosition[i] * shardBlockSize[i]) + (blockPosition[i]);
+ }
+
+ return blockImagePos;
+ }
+
+ default Map> groupBlockPositions(final List blockPositions) {
+
+ final TreeMap> map = new TreeMap<>();
+ for( final long[] blockPos : blockPositions ) {
+ Position shardPos = Position.wrap(getShardPositionForBlock(blockPos));
+ if( !map.containsKey(shardPos)) {
+ map.put(shardPos, new ArrayList<>());
+ }
+ map.get(shardPos).add(blockPos);
+ }
+
+ return map;
+ }
+
+ default Map>> groupBlocks(final List> blocks) {
+
+ // figure out how to re-use groupBlockPositions here?
+ final TreeMap>> map = new TreeMap<>();
+ for (final DataBlock block : blocks) {
+ Position shardPos = Position.wrap(getShardPositionForBlock(block.getGridPosition()));
+ if (!map.containsKey(shardPos)) {
+ map.put(shardPos, new ArrayList<>());
+ }
+ map.get(shardPos).add(block);
+ }
+
+ return map;
+ }
+
+ /**
+ * @return the number of blocks per shard
+ */
+ default long getNumBlocks() {
+
+ return Arrays.stream(getBlocksPerShard()).reduce(1, (x, y) -> x * y);
+ }
+
+ default Stream blockPositions() {
+
+ final int[] blocksPerShard = getBlocksPerShard();
+ return toStream( new GridIterator(shardsPerImage()))
+ .flatMap( shardPosition -> {
+ final int nd = getNumDimensions();
+ final long[] min = getBlockPositionFromShardPosition(shardPosition, new long[nd]);
+ return toStream(new GridIterator(GridIterator.int2long(blocksPerShard), min));
+ });
+ }
+
+ static Stream toStream( final Iterator it ) {
+ return StreamSupport.stream( Spliterators.spliteratorUnknownSize(
+ it, Spliterator.ORDERED),
+ false);
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardingCodec.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardingCodec.java
new file mode 100644
index 00000000..4da31eff
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/ShardingCodec.java
@@ -0,0 +1,163 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.JsonSerializationContext;
+import com.google.gson.JsonSerializer;
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.KeyValueAccess;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodec;
+import org.janelia.saalfeldlab.n5.serialization.N5Annotations;
+import org.janelia.saalfeldlab.n5.serialization.NameConfig;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.reflect.Type;
+
+@NameConfig.Name(ShardingCodec.TYPE)
+public class ShardingCodec implements Codec.ArrayCodec {
+
+ private static final long serialVersionUID = -5879797314954717810L;
+
+ public static final String TYPE = "sharding_indexed";
+
+ public static final String CHUNK_SHAPE_KEY = "chunk_shape";
+ public static final String INDEX_LOCATION_KEY = "index_location";
+ public static final String CODECS_KEY = "codecs";
+ public static final String INDEX_CODECS_KEY = "index_codecs";
+
+ public enum IndexLocation {
+ START, END;
+ }
+
+ @N5Annotations.ReverseArray // TODO need to reverse for zarr, not for n5
+ @NameConfig.Parameter(CHUNK_SHAPE_KEY)
+ private final int[] blockSize;
+
+ @NameConfig.Parameter(CODECS_KEY)
+ private final Codec[] codecs;
+
+ @NameConfig.Parameter(INDEX_CODECS_KEY)
+ private final DeterministicSizeCodec[] indexCodecs;
+
+ @NameConfig.Parameter(value = INDEX_LOCATION_KEY, optional = true)
+ private final IndexLocation indexLocation;
+
+ @SuppressWarnings("unused")
+ private ShardingCodec() {
+
+ blockSize = null;
+ codecs = null;
+ indexCodecs = null;
+ indexLocation = IndexLocation.END;
+ }
+
+ public ShardingCodec(
+ final int[] blockSize,
+ final Codec[] codecs,
+ final DeterministicSizeCodec[] indexCodecs,
+ final IndexLocation indexLocation) {
+
+ this.blockSize = blockSize;
+ this.codecs = codecs;
+ this.indexCodecs = indexCodecs;
+ this.indexLocation = indexLocation;
+ }
+
+ public int[] getBlockSize() {
+
+ return blockSize;
+ }
+
+ public IndexLocation getIndexLocation() {
+
+ return indexLocation;
+ }
+
+ public ArrayCodec getArrayCodec() {
+
+ return (Codec.ArrayCodec)codecs[0];
+ }
+
+ public BytesCodec[] getCodecs() {
+
+ if (codecs.length == 1)
+ return new BytesCodec[]{};
+
+ final BytesCodec[] bytesCodecs = new BytesCodec[codecs.length - 1];
+ System.arraycopy(codecs, 1, bytesCodecs, 0, bytesCodecs.length);
+ return bytesCodecs;
+ }
+
+ public DeterministicSizeCodec[] getIndexCodecs() {
+
+ return indexCodecs;
+ }
+
+ @Override public long[] getPositionForBlock(DatasetAttributes attributes, DataBlock> datablock) {
+
+ final long[] blockPosition = datablock.getGridPosition();
+ return attributes.getShardPositionForBlock(blockPosition);
+ }
+
+ @Override public long[] getPositionForBlock(DatasetAttributes attributes, final long... blockPosition) {
+
+ return attributes.getShardPositionForBlock(blockPosition);
+ }
+ @Override public DataBlockInputStream decode(DatasetAttributes attributes, long[] gridPosition, InputStream in) throws IOException {
+
+ return getArrayCodec().decode(attributes, gridPosition, in);
+ }
+
+ @Override public DataBlockOutputStream encode(DatasetAttributes attributes, DataBlock> dataBlock, OutputStream out) throws IOException {
+
+ return getArrayCodec().encode(attributes, dataBlock, out);
+ }
+
+ @Override public void writeBlock(KeyValueAccess kva, String keyPath, DatasetAttributes datasetAttributes, DataBlock dataBlock) {
+
+ final long[] shardPos = datasetAttributes.getShardPositionForBlock(dataBlock.getGridPosition());
+ new VirtualShard(datasetAttributes, shardPos, kva, keyPath).writeBlock(dataBlock);
+ }
+
+ @Override public DataBlock readBlock(final KeyValueAccess kva, final String keyPath, final DatasetAttributes datasetAttributes, final long... gridPosition) {
+
+ final long[] shardPosition = datasetAttributes.getShardPositionForBlock(gridPosition);
+ return new VirtualShard(datasetAttributes, shardPosition, kva, keyPath).getBlock(gridPosition);
+ }
+
+ public ShardIndex createIndex(final DatasetAttributes attributes) {
+ return new ShardIndex(attributes.getBlocksPerShard(), getIndexLocation(), getIndexCodecs());
+ }
+
+ @Override
+ public String getType() {
+
+ return TYPE;
+ }
+
+ public static IndexLocationAdapter indexLocationAdapter = new IndexLocationAdapter();
+
+ public static class IndexLocationAdapter implements JsonSerializer, JsonDeserializer {
+
+ @Override public IndexLocation deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
+
+ if (!json.isJsonPrimitive())
+ return null;
+
+ return IndexLocation.valueOf(json.getAsString().toUpperCase());
+ }
+
+ @Override public JsonElement serialize(IndexLocation src, Type typeOfSrc, JsonSerializationContext context) {
+
+ return new JsonPrimitive(src.name().toLowerCase());
+ }
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/shard/VirtualShard.java b/src/main/java/org/janelia/saalfeldlab/n5/shard/VirtualShard.java
new file mode 100644
index 00000000..3b115a90
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/shard/VirtualShard.java
@@ -0,0 +1,265 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.apache.commons.io.input.BoundedInputStream;
+import org.apache.commons.io.input.ProxyInputStream;
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.KeyValueAccess;
+import org.janelia.saalfeldlab.n5.LockedChannel;
+import org.janelia.saalfeldlab.n5.N5Exception;
+import org.janelia.saalfeldlab.n5.N5Exception.N5IOException;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.util.GridIterator;
+
+public class VirtualShard extends AbstractShard {
+
+ final private KeyValueAccess keyValueAccess;
+ final private String path;
+
+ public VirtualShard(final DatasetAttributes datasetAttributes, long[] gridPosition,
+ final KeyValueAccess keyValueAccess, final String path) {
+
+ super(datasetAttributes, gridPosition, null);
+ this.keyValueAccess = keyValueAccess;
+ this.path = path;
+ }
+
+ public VirtualShard(final DatasetAttributes datasetAttributes, long[] gridPosition) {
+
+ this(datasetAttributes, gridPosition, null, null);
+ }
+
+ @SuppressWarnings("unchecked")
+ public DataBlock getBlock(InputStream in, long... blockGridPosition) throws IOException {
+
+ ShardingCodec shardingCodec = (ShardingCodec)datasetAttributes.getArrayCodec();
+ final Codec.BytesCodec[] codecs = shardingCodec.getCodecs();
+ final Codec.ArrayCodec arrayCodec = shardingCodec.getArrayCodec();
+
+ final ProxyInputStream proxyIn = new ProxyInputStream(in) {
+ @Override
+ public void close() {
+ //nop
+ }
+ };
+ final Codec.DataBlockInputStream dataBlockStream = arrayCodec.decode(datasetAttributes, blockGridPosition, proxyIn);
+
+ final InputStream stream = Codec.decode(in, codecs);
+ final DataBlock dataBlock = dataBlockStream.allocateDataBlock();
+ dataBlock.readData(dataBlockStream.getDataInput(stream));
+ stream.close();
+
+ return dataBlock;
+
+
+ }
+
+ @Override
+ public List> getBlocks() {
+ return getBlocks(IntStream.range(0, getNumBlocks()).toArray());
+ }
+
+ public List> getBlocks(final int[] blockIndexes) {
+
+ // will not contain nulls
+ final ShardIndex index = getIndex();
+ final ArrayList> blocks = new ArrayList<>();
+
+ if (index.isEmpty())
+ return blocks;
+
+ // sort index offsets
+ // and keep track of relevant positions
+ final long[] indexData = index.getData();
+ List sortedOffsets = Arrays.stream(blockIndexes)
+ .mapToObj(i -> new long[]{indexData[i * 2], i})
+ .filter(x -> x[0] != ShardIndex.EMPTY_INDEX_NBYTES)
+ .sorted(Comparator.comparingLong(a -> ((long[])a)[0]))
+ .collect(Collectors.toList());
+
+ final int nd = getDatasetAttributes().getNumDimensions();
+ long[] position = new long[nd];
+
+ final int[] blocksPerShard = getDatasetAttributes().getBlocksPerShard();
+ final long[] blockGridMin = IntStream.range(0, nd)
+ .mapToLong(i -> blocksPerShard[i] * getGridPosition()[i])
+ .toArray();
+
+ long streamPosition = 0;
+ try (final LockedChannel lockedChannel = keyValueAccess.lockForReading(path)) {
+ try (final InputStream channelIn = lockedChannel.newInputStream()) {
+
+ for (long[] offsetIndex : sortedOffsets) {
+
+ final long offset = offsetIndex[0];
+ if (offset < 0)
+ continue;
+
+ final long idx = offsetIndex[1];
+ GridIterator.indexToPosition(idx, blocksPerShard, blockGridMin, position);
+
+ channelIn.skip(offset - streamPosition);
+ final long numBytes = index.getNumBytesByBlockIndex((int) idx);
+ final BoundedInputStream bIs = BoundedInputStream.builder().setInputStream(channelIn)
+ .setMaxCount(numBytes).get();
+
+ blocks.add(getBlock(bIs, position.clone()));
+ streamPosition = offset + numBytes;
+ }
+ }
+ } catch (final N5Exception.N5NoSuchKeyException e) {
+ return blocks;
+ } catch (final IOException | UncheckedIOException e) {
+ throw new N5IOException("Failed to read block from " + path, e);
+ }
+
+ return blocks;
+ }
+
+ @Override
+ public DataBlock getBlock(long... blockGridPosition) {
+
+ final int[] relativePosition = getBlockPosition(blockGridPosition);
+ if (relativePosition == null)
+ throw new N5IOException("Attempted to read a block from the wrong shard.");
+
+ final ShardIndex idx = getIndex();
+ if (!idx.exists(relativePosition))
+ return null;
+
+ final long blockOffset = idx.getOffset(relativePosition);
+ final long blockSize = idx.getNumBytes(relativePosition);
+
+ try (final LockedChannel lockedChannel = keyValueAccess.lockForReading(path, blockOffset, blockSize)) {
+ try ( final InputStream in = lockedChannel.newInputStream()) {
+ final long[] blockPosInImg = getDatasetAttributes().getBlockPositionFromShardPosition(getGridPosition(), blockGridPosition);
+ return getBlock( in, blockPosInImg );
+ }
+ } catch (final N5Exception.N5NoSuchKeyException e) {
+ return null;
+ } catch (final IOException | UncheckedIOException e) {
+ throw new N5IOException("Failed to read block from " + path, e);
+ }
+ }
+
+ @Override
+ public void writeBlock(final DataBlock block) {
+
+ final int[] relativePosition = getBlockPosition(block.getGridPosition());
+ if (relativePosition == null)
+ throw new N5IOException("Attempted to write block in the wrong shard.");
+
+ final ShardIndex index = getIndex();
+ long startByte = 0;
+ try {
+ startByte = keyValueAccess.size(path);
+ } catch (N5Exception.N5NoSuchKeyException e) {
+ startByte = index.getLocation() == ShardingCodec.IndexLocation.START ? index.numBytes() : 0;
+ } catch (IOException e) {
+ throw new N5IOException(e);
+ }
+ final long size = Long.MAX_VALUE - startByte;
+
+ try (final LockedChannel lockedChannel = keyValueAccess.lockForWriting(path, startByte, size)) {
+ try ( final OutputStream channelOut = lockedChannel.newOutputStream()) {
+ try (final CountingOutputStream out = new CountingOutputStream(channelOut)) {;
+ writeBlock(out, datasetAttributes, block);
+
+ /* Update and write the index to the shard*/
+ index.set(startByte, out.getNumBytes(), relativePosition);
+ }
+ }
+ } catch (final IOException | UncheckedIOException e) {
+ throw new N5IOException("Failed to write block to shard " + path, e);
+ }
+
+ try {
+ ShardIndex.write(index, keyValueAccess, path);
+ } catch (IOException e) {
+ throw new N5IOException("Failed to write index to shard " + path, e);
+ }
+ }
+
+ void writeBlock(
+ final OutputStream out,
+ final DatasetAttributes datasetAttributes,
+ final DataBlock dataBlock) throws IOException {
+
+ ShardingCodec shardingCodec = (ShardingCodec)datasetAttributes.getArrayCodec();
+ final Codec.BytesCodec[] codecs = shardingCodec.getCodecs();
+ final Codec.ArrayCodec arrayCodec = shardingCodec.getArrayCodec();
+ final Codec.DataBlockOutputStream dataBlockOutput = arrayCodec.encode(datasetAttributes, dataBlock, out);
+ final OutputStream stream = Codec.encode(dataBlockOutput, codecs);
+
+ dataBlock.writeData(dataBlockOutput.getDataOutput(stream));
+ stream.close();
+ }
+ public ShardIndex createIndex() {
+
+ // Empty index of the correct size
+ return ((ShardingCodec)getDatasetAttributes().getArrayCodec()).createIndex(getDatasetAttributes());
+ }
+
+ @Override
+ public ShardIndex getIndex() {
+
+ index = createIndex();
+ ShardIndex.read(keyValueAccess, path, index);
+
+ return index;
+ }
+
+ static class CountingOutputStream extends OutputStream {
+ private final OutputStream out;
+ private long numBytes;
+
+ public CountingOutputStream(OutputStream out) {
+ this.out = out;
+ this.numBytes = 0;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ out.write(b);
+ numBytes++;
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ out.write(b);
+ numBytes += b.length;
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ out.write(b, off, len);
+ numBytes += len;
+ }
+
+ @Override
+ public void flush() throws IOException {
+ out.flush();
+ }
+
+ @Override
+ public void close() throws IOException {
+
+ }
+
+ public long getNumBytes() {
+ return numBytes;
+ }
+ }
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/util/FinalPosition.java b/src/main/java/org/janelia/saalfeldlab/n5/util/FinalPosition.java
new file mode 100644
index 00000000..1b7076d5
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/util/FinalPosition.java
@@ -0,0 +1,38 @@
+package org.janelia.saalfeldlab.n5.util;
+
+/*
+ * An immutable {@Position}.
+ */
+public class FinalPosition implements Position {
+
+ public final long[] position;
+
+ public FinalPosition(long[] position) {
+ this.position = position;
+ }
+
+ public FinalPosition(Position p) {
+ this.position = p.get().clone();
+ }
+
+ @Override
+ public long[] get() {
+ return position;
+ }
+
+ @Override
+ public long get(int i) {
+ return position[i];
+ }
+
+ @Override
+ public String toString() {
+ return Position.toString(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return Position.equals(this, obj);
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/util/GridIterator.java b/src/main/java/org/janelia/saalfeldlab/n5/util/GridIterator.java
new file mode 100644
index 00000000..9880e7b3
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/util/GridIterator.java
@@ -0,0 +1,169 @@
+package org.janelia.saalfeldlab.n5.util;
+
+import java.util.Iterator;
+
+/**
+ * Essentially imglib2's IntervalIterator, but N5 does not depend on imglib2.
+ */
+public class GridIterator implements Iterator {
+
+ final protected long[] dimensions;
+
+ final protected long[] steps;
+
+ final protected long[] position;
+
+ final protected long[] min;
+
+ final protected int lastIndex;
+
+ protected int index = -1;
+
+ public GridIterator(final long[] dimensions, final long[] min) {
+
+ final int n = dimensions.length;
+ this.dimensions = new long[n];
+ this.position = new long[n];
+ this.min = min;
+ steps = new long[n];
+
+ final int m = n - 1;
+ long k = steps[0] = 1;
+ for (int d = 0; d < m;) {
+ final long dimd = dimensions[d];
+ this.dimensions[d] = dimd;
+ k *= dimd;
+ steps[++d] = k;
+ }
+ final long dimm = dimensions[m];
+ this.dimensions[m] = dimm;
+ lastIndex = (int)(k * dimm - 1);
+ }
+
+ public GridIterator(final long[] dimensions) {
+
+ this(dimensions, new long[dimensions.length]);
+ }
+
+ public GridIterator(final int[] dimensions) {
+
+ this(int2long(dimensions));
+ }
+
+ public void fwd() {
+ ++index;
+ }
+
+ public void reset() {
+ index = -1;
+ }
+
+ @Override
+ public boolean hasNext() {
+ return index < lastIndex;
+ }
+
+ @Override
+ public long[] next() {
+ fwd();
+ indexToPosition(index, dimensions, min, position);
+ return position;
+ }
+
+ public int[] nextAsInt() {
+ return long2int(next());
+ }
+
+ public int getIndex() {
+ return index;
+ }
+
+ final static public void indexToPosition(long index, final long[] dimensions, final long[] offset,
+ final long[] position) {
+ for (int dim = 0; dim < dimensions.length; dim++) {
+ position[dim] = (index % dimensions[dim]) + offset[dim];
+ index /= dimensions[dim];
+ }
+ }
+
+ final static public void indexToPosition(long index, final long[] dimensions, final long[] position) {
+ for (int dim = 0; dim < dimensions.length; dim++) {
+ position[dim] = index % dimensions[dim];
+ index /= dimensions[dim];
+ }
+ }
+
+ final static public void indexToPosition(long index, final int[] dimensions, final long[] offset,
+ final long[] position) {
+ for (int dim = 0; dim < dimensions.length; dim++) {
+ position[dim] = (index % dimensions[dim]) + offset[dim];
+ index /= dimensions[dim];
+ }
+ }
+
+ final static public void indexToPosition(long index, final int[] dimensions, final long[] position) {
+ for (int dim = 0; dim < dimensions.length; dim++) {
+ position[dim] = index % dimensions[dim];
+ index /= dimensions[dim];
+ }
+ }
+
+ final static public long positionToIndex(final long[] dimensions, final long[] position) {
+ long idx = position[0];
+ int cumulativeSize = 1;
+ for (int i = 0; i < position.length; i++) {
+ idx += position[i] * cumulativeSize;
+ cumulativeSize *= dimensions[i];
+ }
+ return idx;
+ }
+
+ final static public long positionToIndex(final long[] dimensions, final int[] position) {
+ long idx = position[0];
+ int cumulativeSize = 1;
+ for (int i = 0; i < position.length; i++) {
+ idx += position[i] * cumulativeSize;
+ cumulativeSize *= dimensions[i];
+ }
+ return idx;
+ }
+
+ final static public long positionToIndex(final int[] dimensions, final int[] position) {
+ long idx = position[0];
+ int cumulativeSize = 1;
+ for (int i = 0; i < position.length; i++) {
+ idx += position[i] * cumulativeSize;
+ cumulativeSize *= dimensions[i];
+ }
+ return idx;
+ }
+
+ final static public long positionToIndex(final int[] dimensions, final long[] position) {
+ long idx = position[0];
+ int cumulativeSize = 1;
+ for (int i = 0; i < position.length; i++) {
+ idx += position[i] * cumulativeSize;
+ cumulativeSize *= dimensions[i];
+ }
+ return idx;
+ }
+
+ final static public int[] long2int(final long[] a) {
+ final int[] i = new int[a.length];
+
+ for (int d = 0; d < a.length; ++d)
+ i[d] = (int) a[d];
+
+ return i;
+ }
+
+ final static public long[] int2long(final int[] i) {
+ final long[] l = new long[i.length];
+
+ for (int d = 0; d < l.length; ++d)
+ l[d] = i[d];
+
+ return l;
+ }
+
+}
diff --git a/src/main/java/org/janelia/saalfeldlab/n5/util/Position.java b/src/main/java/org/janelia/saalfeldlab/n5/util/Position.java
new file mode 100644
index 00000000..5ddb8cf0
--- /dev/null
+++ b/src/main/java/org/janelia/saalfeldlab/n5/util/Position.java
@@ -0,0 +1,62 @@
+package org.janelia.saalfeldlab.n5.util;
+
+import java.util.Arrays;
+
+/*
+ * A wrapper around a primitive long array that is lexicographically {@link Comparable}
+ * and for which we can test equality.
+ */
+public interface Position extends Comparable {
+
+ public long[] get();
+
+ public long get(int i);
+
+ default int numDimensions() {
+ return get().length;
+ }
+
+ @Override
+ default int compareTo(Position other) {
+
+ // use Arrays.compare when we update to Java 9+
+ final int N = numDimensions() > other.numDimensions() ? numDimensions() : other.numDimensions();
+ for (int i = 0; i < N; i++) {
+ final long diff = get(i) - other.get(i);
+ if (diff != 0)
+ return (int) diff;
+ }
+ return 0;
+ }
+
+ public static boolean equals(final Position a, final Object b) {
+
+ if (a == null && b == null)
+ return true;
+
+ if (b == null)
+ return false;
+
+ if (!(b instanceof Position))
+ return false;
+
+ final Position other = (Position) b;
+ if (other.numDimensions() != a.numDimensions())
+ return false;
+
+ for (int i = 0; i < a.numDimensions(); i++)
+ if (other.get(i) != a.get(i))
+ return false;
+
+ return true;
+ }
+
+ public static String toString(Position p) {
+ return "Position: " + Arrays.toString(p.get());
+ }
+
+ public static Position wrap(final long[] p) {
+ return new FinalPosition(p);
+ }
+
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/AbstractN5Test.java b/src/test/java/org/janelia/saalfeldlab/n5/AbstractN5Test.java
index 6e2e9e8f..9b1e0831 100644
--- a/src/test/java/org/janelia/saalfeldlab/n5/AbstractN5Test.java
+++ b/src/test/java/org/janelia/saalfeldlab/n5/AbstractN5Test.java
@@ -50,6 +50,9 @@
import org.janelia.saalfeldlab.n5.N5Exception.N5ClassCastException;
import org.janelia.saalfeldlab.n5.N5Reader.Version;
+import org.janelia.saalfeldlab.n5.codec.AsTypeCodec;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.N5BlockCodec;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
@@ -89,7 +92,7 @@ public abstract class AbstractN5Test {
protected final HashSet tempWriters = new HashSet<>();
- protected final N5Writer createTempN5Writer() {
+ public final N5Writer createTempN5Writer() {
try {
return createTempN5Writer(tempN5Location());
@@ -117,6 +120,7 @@ protected final N5Writer createTempN5Writer(String location, GsonBuilder gson) {
@After
public void removeTempWriters() {
+
synchronized (tempWriters) {
for (final N5Writer writer : tempWriters) {
try {
@@ -208,11 +212,11 @@ public void testSetAttributeDoesntCreateGroup() {
}
@Test
- public void testCreateDataset() {
+ public void testCreateDataset() {
final DatasetAttributes info;
try (N5Writer writer = createTempN5Writer()) {
- writer.createDataset(datasetName, dimensions, blockSize, DataType.UINT64, new RawCompression());
+ writer.createDataset(datasetName, dimensions, blockSize, DataType.UINT64);
assertTrue("Dataset does not exist", writer.exists(datasetName));
@@ -246,6 +250,33 @@ public void testWriteReadByteBlock() {
}
}
+ @Test
+ public void testWriteReadByteBlockMultipleCodecs() {
+
+ /*TODO: this tests "passes" in the sense that we get the correct output, but it
+ * maybe is not the behavior we actually want*/
+ try (final N5Writer n5 = createTempN5Writer()) {
+ final Codec[] codecs = {
+ new N5BlockCodec(),
+ new AsTypeCodec(DataType.INT32, DataType.INT8),
+ new AsTypeCodec(DataType.INT64, DataType.INT32),
+ };
+ final long[] longBlock1 = new long[]{1,2,3,4,5,6,7,8};
+ final long[] dimensions1 = new long[]{2,2,2};
+ final int[] blockSize1 = new int[]{2,2,2};
+ n5.createDataset(datasetName, dimensions1, blockSize1, DataType.INT8, codecs);
+ final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName);
+ final LongArrayDataBlock dataBlock = new LongArrayDataBlock(blockSize1, new long[]{0, 0, 0}, longBlock1);
+ n5.writeBlock(datasetName, attributes, dataBlock);
+
+ final DatasetAttributes fakeAttributes = new DatasetAttributes(dimensions1, blockSize1, DataType.INT64, codecs);
+ final DataBlock> loadedDataBlock = n5.readBlock(datasetName, fakeAttributes, 0, 0, 0);
+ assertArrayEquals(longBlock1, (long[])loadedDataBlock.getData());
+ assertTrue(n5.remove(datasetName));
+
+ }
+ }
+
@Test
public void testWriteReadStringBlock() {
@@ -305,7 +336,7 @@ public void testWriteReadIntBlock() {
DataType.INT32}) {
try (final N5Writer n5 = createTempN5Writer()) {
- n5.createDataset(datasetName, dimensions, blockSize, dataType, compression);
+ n5.createDataset(datasetName, dimensions, blockSize, dataType, (Codec)compression);
final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName);
final IntArrayDataBlock dataBlock = new IntArrayDataBlock(blockSize, new long[]{0, 0, 0}, intBlock);
n5.writeBlock(datasetName, attributes, dataBlock);
@@ -465,7 +496,7 @@ public void testOverwriteBlock() {
}
@Test
- public void testAttributeParsingPrimitive() {
+ public void testAttributeParsingPrimitive() {
try (final N5Writer n5 = createTempN5Writer()) {
@@ -541,7 +572,7 @@ public void testAttributeParsingPrimitive() {
}
@Test
- public void testAttributes() {
+ public void testAttributes() {
try (final N5Writer n5 = createTempN5Writer()) {
assertNull(n5.getAttribute(groupName, "test", String.class));
@@ -607,7 +638,6 @@ public void testAttributes() {
}
}
-
@Test
public void testNullAttributes() throws URISyntaxException, IOException {
@@ -831,7 +861,7 @@ public void testUri() throws IOException, URISyntaxException {
}
@Test
- public void testRemoveGroup() {
+ public void testRemoveGroup() {
try (final N5Writer n5 = createTempN5Writer()) {
n5.createDataset(datasetName, dimensions, blockSize, DataType.UINT64, new RawCompression());
@@ -880,7 +910,7 @@ public void testDeepList() throws ExecutionException, InterruptedException {
for (final String subGroup : subGroupNames)
assertTrue("deepList contents", Arrays.asList(n5.deepList("")).contains(groupName.replaceFirst("/", "") + "/" + subGroup));
- final DatasetAttributes datasetAttributes = new DatasetAttributes(dimensions, blockSize, DataType.UINT64, new RawCompression());
+ final DatasetAttributes datasetAttributes = new DatasetAttributes(dimensions, blockSize, DataType.UINT64);
final LongArrayDataBlock dataBlock = new LongArrayDataBlock(blockSize, new long[]{0, 0, 0}, new long[blockNumElements]);
n5.createDataset(datasetName, datasetAttributes);
n5.writeBlock(datasetName, datasetAttributes, dataBlock);
@@ -982,7 +1012,7 @@ public void testDeepList() throws ExecutionException, InterruptedException {
}
@Test
- public void testExists() {
+ public void testExists() {
final String groupName2 = groupName + "-2";
final String datasetName2 = datasetName + "-2";
@@ -1003,7 +1033,7 @@ public void testExists() {
}
@Test
- public void testListAttributes() {
+ public void testListAttributes() {
try (N5Writer n5 = createTempN5Writer()) {
final String groupName2 = groupName + "-2";
@@ -1106,7 +1136,7 @@ public void testReaderCreation() throws IOException, URISyntaxException {
writer.setAttribute("/", N5Reader.VERSION_KEY, invalidVersion);
assertThrows("Incompatible version throws error", N5Exception.class, () -> {
try (final N5Reader ignored = createN5Reader(location)) {
- /*Only try with resource to ensure `close()` is called.*/
+ /*Only try with resource to ensure `close()` is called.*/
}
});
} finally {
@@ -1123,7 +1153,7 @@ public void testReaderCreation() throws IOException, URISyntaxException {
}
@Test
- public void testDelete() {
+ public void testDelete() {
try (N5Writer n5 = createTempN5Writer()) {
final String datasetName = AbstractN5Test.datasetName + "-test-delete";
@@ -1209,7 +1239,7 @@ protected static void runTests(final N5Writer writer, final ArrayList %s", i, testPaths[i], Arrays.toString(components)));
assertArrayEquals(testPathComponents[i], components);
}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/N5FSTest.java b/src/test/java/org/janelia/saalfeldlab/n5/N5FSTest.java
index bd1e43aa..da0a38ec 100644
--- a/src/test/java/org/janelia/saalfeldlab/n5/N5FSTest.java
+++ b/src/test/java/org/janelia/saalfeldlab/n5/N5FSTest.java
@@ -199,7 +199,6 @@ public void testWriteLock() throws IOException {
@Test
public void testLockReleaseByReader() throws IOException, ExecutionException, InterruptedException, TimeoutException {
- System.out.println("Testing lock release by Reader.");
final Path path = Paths.get(tempN5PathName(), "lock");
final LockedChannel lock = access.lockForWriting(path);
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/codec/AsTypeTests.java b/src/test/java/org/janelia/saalfeldlab/n5/codec/AsTypeTests.java
new file mode 100644
index 00000000..59aa3298
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/codec/AsTypeTests.java
@@ -0,0 +1,73 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.ByteBuffer;
+
+import org.janelia.saalfeldlab.n5.DataType;
+import org.junit.Test;
+
+public class AsTypeTests {
+
+ @Test
+ public void testInt2Byte() throws IOException {
+
+ final int N = 16;
+ final ByteBuffer intsAsBuffer = ByteBuffer.allocate(Integer.BYTES * N);
+ final byte[] encodedBytes = new byte[N];
+ for (int i = 0; i < N; i++) {
+ intsAsBuffer.putInt(i);
+ encodedBytes[i] = (byte)i;
+ }
+
+ final byte[] decodedInts = intsAsBuffer.array();
+ testEncodingAndDecoding(new AsTypeCodec(DataType.INT32, DataType.INT8), encodedBytes, decodedInts);
+ testEncodingAndDecoding(new AsTypeCodec(DataType.INT8, DataType.INT32), decodedInts, encodedBytes);
+ }
+
+ @Test
+ public void testDouble2Byte() throws IOException {
+
+ final int N = 16;
+ final ByteBuffer doublesAsBuffer = ByteBuffer.allocate(Double.BYTES * N);
+ final byte[] encodedBytes = new byte[N];
+ for (int i = 0; i < N; i++) {
+ doublesAsBuffer.putDouble(i);
+ encodedBytes[i] = (byte)i;
+ }
+ final byte[] decodedDoubles = doublesAsBuffer.array();
+
+ testEncodingAndDecoding(new AsTypeCodec(DataType.FLOAT64, DataType.INT8), encodedBytes, decodedDoubles);
+ testEncodingAndDecoding(new AsTypeCodec(DataType.INT8, DataType.FLOAT64), decodedDoubles, encodedBytes);
+ }
+
+ public static void testEncodingAndDecoding(Codec.BytesCodec codec, byte[] encodedBytes, byte[] decodedBytes) throws IOException {
+
+ testEncoding(codec, encodedBytes, decodedBytes);
+ testDecoding(codec, decodedBytes, encodedBytes);
+ }
+
+ public static void testDecoding(final Codec.BytesCodec codec, final byte[] expected, final byte[] input) throws IOException {
+
+ final InputStream result = codec.decode(new ByteArrayInputStream(input));
+ for (int i = 0; i < expected.length; i++)
+ assertEquals(expected[i], (byte)result.read());
+ }
+
+ public static void testEncoding(final Codec.BytesCodec codec, final byte[] expected, final byte[] data) throws IOException {
+
+ final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(expected.length);
+ final OutputStream encodedStream = codec.encode(outputStream);
+ encodedStream.write(data);
+ encodedStream.flush();
+ assertArrayEquals(expected, outputStream.toByteArray());
+ encodedStream.close();
+ }
+
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/codec/BytesTests.java b/src/test/java/org/janelia/saalfeldlab/n5/codec/BytesTests.java
new file mode 100644
index 00000000..ea24a7d9
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/codec/BytesTests.java
@@ -0,0 +1,53 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.nio.ByteOrder;
+
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.N5Writer;
+import org.janelia.saalfeldlab.n5.NameConfigAdapter;
+import org.janelia.saalfeldlab.n5.RawCompression;
+import org.janelia.saalfeldlab.n5.universe.N5Factory;
+import org.junit.Test;
+
+import com.google.gson.GsonBuilder;
+
+public class BytesTests {
+
+ @Test
+ public void testSerialization() {
+
+ final N5Factory factory = new N5Factory();
+ factory.cacheAttributes(false);
+ final GsonBuilder gsonBuilder = new GsonBuilder();
+ gsonBuilder.registerTypeHierarchyAdapter(Codec.class, NameConfigAdapter.getJsonAdapter(Codec.class));
+ gsonBuilder.registerTypeAdapter(ByteOrder.class, RawBytes.byteOrderAdapter);
+ factory.gsonBuilder(gsonBuilder);
+
+ final N5Writer reader = factory.openWriter("n5:src/test/resources/shardExamples/test.n5");
+ final Codec bytes = reader.getAttribute("mid_sharded", "codecs[0]/configuration/codecs[0]", Codec.class);
+ assertTrue("as RawBytes", bytes instanceof RawBytes);
+
+ final N5Writer writer = factory.openWriter("n5:src/test/resources/shardExamples/test.n5");
+
+ final DatasetAttributes datasetAttributes = new DatasetAttributes(
+ new long[]{8, 8},
+ new int[]{4, 4},
+ DataType.UINT8,
+ new N5BlockCodec(ByteOrder.LITTLE_ENDIAN),
+ new IdentityCodec()
+ );
+ writer.createGroup("shard"); //Should already exist, but this will ensure.
+ writer.setAttribute("shard", "/", datasetAttributes);
+ final DatasetAttributes deserialized = writer.getAttribute("shard", "/", DatasetAttributes.class);
+
+ assertEquals("1 codecs", 1, deserialized.getCodecs().length);
+ assertTrue("Identity", deserialized.getCodecs()[0] instanceof IdentityCodec);
+ assertTrue("Bytes", deserialized.getArrayCodec() instanceof N5BlockCodec);
+ assertEquals("LittleEndian", ByteOrder.LITTLE_ENDIAN,
+ ((N5BlockCodec)deserialized.getArrayCodec()).byteOrder);
+ }
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/codec/FixedConvertedInputStreamTest.java b/src/test/java/org/janelia/saalfeldlab/n5/codec/FixedConvertedInputStreamTest.java
new file mode 100644
index 00000000..27c744fa
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/codec/FixedConvertedInputStreamTest.java
@@ -0,0 +1,86 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.stream.IntStream;
+
+import org.junit.Test;
+
+public class FixedConvertedInputStreamTest {
+
+ @Test
+ public void testLengthOne() throws IOException
+ {
+
+ final byte expected = 5;
+ final byte[] data = new byte[32];
+ Arrays.fill(data, expected);
+
+ final FixedLengthConvertedInputStream convertedId = new FixedLengthConvertedInputStream(1, 1,
+ AsTypeCodec::IDENTITY_ONE,
+ new ByteArrayInputStream(data));
+
+ final FixedLengthConvertedInputStream convertedPlusOne = new FixedLengthConvertedInputStream(1, 1,
+ (x, y) -> {
+ y.put((byte)(x.get() + 1));
+ },
+ new ByteArrayInputStream(data));
+
+ for (int i = 0; i < 32; i++) {
+ assertEquals(expected, convertedId.read());
+ assertEquals(expected + 1, convertedPlusOne.read());
+ }
+
+ convertedId.close();
+ convertedPlusOne.close();
+ }
+
+ @Test
+ public void testIntToByte() throws IOException
+ {
+
+ final int N = 16;
+ final ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES * N);
+ IntStream.range(0, N).forEach( x -> {
+ buf.putInt(x);
+ });
+
+ final byte[] data = buf.array();
+ final FixedLengthConvertedInputStream intToByte = new FixedLengthConvertedInputStream(
+ 4, 1,
+ AsTypeCodec::INT_TO_BYTE,
+ new ByteArrayInputStream(data));
+
+ for( int i = 0; i < N; i++ )
+ assertEquals((byte)i, intToByte.read());
+
+ intToByte.close();
+ }
+
+ @Test
+ public void testByteToInt() throws IOException
+ {
+
+ final int N = 16;
+ final byte[] data = new byte[16];
+ for( int i = 0; i < N; i++ )
+ data[i] = (byte)i;
+
+ final FixedLengthConvertedInputStream byteToInt = new FixedLengthConvertedInputStream(
+ 1, 4, AsTypeCodec::BYTE_TO_INT,
+ new ByteArrayInputStream(data));
+
+ final DataInputStream dataStream = new DataInputStream(byteToInt);
+ for( int i = 0; i < N; i++ )
+ assertEquals(i, dataStream.readInt());
+
+ dataStream.close();
+ byteToInt.close();
+ }
+
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/codec/FixedConvertedOutputStreamTest.java b/src/test/java/org/janelia/saalfeldlab/n5/codec/FixedConvertedOutputStreamTest.java
new file mode 100644
index 00000000..f8cf5215
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/codec/FixedConvertedOutputStreamTest.java
@@ -0,0 +1,96 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.stream.IntStream;
+
+import org.junit.Test;
+
+public class FixedConvertedOutputStreamTest {
+
+ @Test
+ public void testLengthOne() throws IOException
+ {
+ final int N = 2;
+ final byte expected = 5;
+ final byte expectedPlusOne = 6;
+ final byte[] expectedData = new byte[N];
+ Arrays.fill(expectedData, expected);
+
+ final byte[] expectedPlusOneData = new byte[N];
+ Arrays.fill(expectedPlusOneData, expectedPlusOne);
+
+ final ByteArrayOutputStream outId = new ByteArrayOutputStream(N);
+ final FixedLengthConvertedOutputStream convertedId = new FixedLengthConvertedOutputStream(1, 1,
+ AsTypeCodec::IDENTITY_ONE,
+ outId);
+
+ convertedId.write(expectedData);
+ convertedId.flush();
+ convertedId.close();
+
+ assertArrayEquals(expectedData, outId.toByteArray());
+
+
+ final ByteArrayOutputStream outPlusOne = new ByteArrayOutputStream(N);
+ final FixedLengthConvertedOutputStream convertedPlusOne = new FixedLengthConvertedOutputStream(1, 1,
+ (x, y) -> y.put((byte)(x.get() + 1)),
+ outPlusOne);
+
+ convertedPlusOne.write(expectedData);
+ convertedPlusOne.close();
+ assertArrayEquals(expectedPlusOneData, outPlusOne.toByteArray());
+ }
+
+ @Test
+ public void testIntToByte() throws IOException
+ {
+
+ final int N = 16;
+ final ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES * N);
+ IntStream.range(0, N).forEach(buf::putInt);
+
+ final ByteBuffer expected = ByteBuffer.allocate(N);
+ IntStream.range(0, N).forEach( x -> expected.put((byte)x));
+
+ final ByteArrayOutputStream outStream = new ByteArrayOutputStream(N);
+ final FixedLengthConvertedOutputStream intToByte = new FixedLengthConvertedOutputStream(
+ 4, 1,
+ AsTypeCodec::INT_TO_BYTE,
+ outStream);
+
+ intToByte.write(buf.array());
+ intToByte.close();
+
+ assertArrayEquals(expected.array(), outStream.toByteArray());
+ }
+ @Test
+ public void testByteToInt() throws IOException
+ {
+
+ final int N = 16;
+ final byte[] data = new byte[16];
+ for( int i = 0; i < N; i++ )
+ data[i] = (byte)i;
+
+ FixedLengthConvertedInputStream byteToInt = new FixedLengthConvertedInputStream(
+ 1, 4,
+ (input, output) -> output.putInt(input.get()),
+ new ByteArrayInputStream(data));
+
+ final DataInputStream dataStream = new DataInputStream(byteToInt);
+ for( int i = 0; i < N; i++ )
+ assertEquals(i, dataStream.readInt());
+
+ dataStream.close();
+ byteToInt.close();
+ }
+
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/codec/FixedScaleOffsetTests.java b/src/test/java/org/janelia/saalfeldlab/n5/codec/FixedScaleOffsetTests.java
new file mode 100644
index 00000000..135a7f2b
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/codec/FixedScaleOffsetTests.java
@@ -0,0 +1,60 @@
+package org.janelia.saalfeldlab.n5.codec;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.stream.DoubleStream;
+
+import org.janelia.saalfeldlab.n5.DataType;
+import org.junit.Test;
+
+public class FixedScaleOffsetTests {
+
+ @Test
+ public void testDouble2Byte() throws IOException {
+
+ final int N = 16;
+ final double[] doubles = DoubleStream.iterate(0.0, x -> x + 1).limit(N).toArray();
+ final ByteBuffer encodedDoubles = ByteBuffer.allocate(Double.BYTES * N);
+ final byte[] bytes = new byte[N];
+
+ final double scale = 2;
+ final double offset = 1;
+
+ for (int i = 0; i < N; i++) {
+ final double val = (scale * doubles[i] + offset);
+ bytes[i] = (byte)val;
+ encodedDoubles.putDouble(i);
+ }
+
+ final FixedScaleOffsetCodec double2Byte = new FixedScaleOffsetCodec(scale, offset, DataType.FLOAT64, DataType.INT8);
+ AsTypeTests.testEncoding(double2Byte, bytes, encodedDoubles.array());
+ AsTypeTests.testDecoding(double2Byte, encodedDoubles.array(), bytes);
+ }
+
+ @Test
+ public void testLong2Short() throws IOException {
+
+ final int N = 16;
+ final ByteBuffer encodedLongs = ByteBuffer.allocate(Long.BYTES * N);
+ final ByteBuffer encodedShorts = ByteBuffer.allocate(Short.BYTES * N);
+
+ final long scale = 2;
+ final long offset = 1;
+
+ for (int i = 0; i < N; i++) {
+ final long val = (scale * i + offset);
+ encodedShorts.putShort((short)val);
+ encodedLongs.putLong(i);
+ }
+
+ final byte[] shortBytes = encodedShorts.array();
+ final byte[] longBytes = encodedLongs.array();
+
+ final FixedScaleOffsetCodec long2short = new FixedScaleOffsetCodec(scale, offset, DataType.INT64, DataType.INT16);
+ AsTypeTests.testEncoding(long2short, shortBytes, longBytes);
+ AsTypeTests.testDecoding(long2short, longBytes, shortBytes);
+ }
+
+
+
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/demo/BlockIterators.java b/src/test/java/org/janelia/saalfeldlab/n5/demo/BlockIterators.java
new file mode 100644
index 00000000..c96edc07
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/demo/BlockIterators.java
@@ -0,0 +1,95 @@
+package org.janelia.saalfeldlab.n5.demo;
+
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.Spliterator;
+import java.util.Spliterators;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
+
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.RawCompression;
+import org.janelia.saalfeldlab.n5.ShardedDatasetAttributes;
+import org.janelia.saalfeldlab.n5.codec.RawBytes;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodec;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec.IndexLocation;
+import org.janelia.saalfeldlab.n5.util.GridIterator;
+
+public class BlockIterators {
+
+ public static void main(String[] args) {
+
+// blockIterator();
+ shardBlockIterator();
+ }
+
+ public static void shardBlockIterator() {
+
+ final ShardedDatasetAttributes attrs = new ShardedDatasetAttributes(
+ new long[] {12, 8}, // image size
+ new int[] {6, 4}, // shard size
+ new int[] {2, 2}, // block size
+ DataType.UINT8,
+ new Codec[] { new RawBytes() },
+ new DeterministicSizeCodec[] { new RawBytes() },
+ IndexLocation.END);
+
+ shardPositions(attrs)
+ .forEach(x -> System.out.println(Arrays.toString(x)));
+ }
+
+ public static void blockIterator() {
+
+ final DatasetAttributes attrs = new DatasetAttributes(
+ new long[] {12, 8},
+ new int[] {2, 2},
+ DataType.UINT8,
+ new RawCompression());
+
+ blockPositions(attrs).forEach(x -> System.out.println(Arrays.toString(x)));
+ }
+
+ public static long[] blockGridSize(final DatasetAttributes attrs ) {
+ // this could be a nice method for DatasetAttributes
+
+ return IntStream.range(0, attrs.getNumDimensions()).mapToLong(i -> {
+ return (long)Math.ceil(attrs.getDimensions()[i] / attrs.getBlockSize()[i]);
+ }).toArray();
+
+ }
+
+ public static long[] shardGridSize(final ShardedDatasetAttributes attrs ) {
+ // this could be a nice method for DatasetAttributes
+
+ return IntStream.range(0, attrs.getNumDimensions()).mapToLong(i -> {
+ return (long)Math.ceil(attrs.getDimensions()[i] / attrs.getShardSize()[i]);
+ }).toArray();
+
+ }
+
+ public static Stream blockPositions( DatasetAttributes attrs ) {
+ return toStream(new GridIterator(blockGridSize(attrs)));
+ }
+
+ public static Stream shardPositions( ShardedDatasetAttributes attrs ) {
+
+ final int[] blocksPerShard = attrs.getBlocksPerShard();
+ return toStream( new GridIterator(shardGridSize(attrs)))
+ .flatMap( shardPosition -> {
+
+ final int nd = attrs.getNumDimensions();
+ final long[] min = attrs.getBlockPositionFromShardPosition(shardPosition, new long[nd]);
+ return toStream(new GridIterator(GridIterator.int2long(blocksPerShard), min));
+ });
+ }
+
+ public static Stream toStream( final Iterator it ) {
+ return StreamSupport.stream( Spliterators.spliteratorUnknownSize(
+ it, Spliterator.ORDERED),
+ false);
+ }
+
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/serialization/CodecSerialization.java b/src/test/java/org/janelia/saalfeldlab/n5/serialization/CodecSerialization.java
new file mode 100644
index 00000000..0610c7c5
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/serialization/CodecSerialization.java
@@ -0,0 +1,91 @@
+package org.janelia.saalfeldlab.n5.serialization;
+
+import static org.janelia.saalfeldlab.n5.NameConfigAdapter.getJsonAdapter;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.GsonUtils;
+import org.janelia.saalfeldlab.n5.GzipCompression;
+import org.janelia.saalfeldlab.n5.NameConfigAdapter;
+import org.janelia.saalfeldlab.n5.codec.AsTypeCodec;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.FixedScaleOffsetCodec;
+import org.janelia.saalfeldlab.n5.codec.IdentityCodec;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+
+public class CodecSerialization {
+
+ private Gson gson;
+
+ @Before
+ public void before() {
+
+
+ final GsonBuilder gsonBuilder = new GsonBuilder();
+ GsonUtils.registerGson(gsonBuilder);
+ gson = gsonBuilder.create();
+ }
+
+ @Test
+ public void testSerializeIdentity() {
+
+ final IdentityCodec id = new IdentityCodec();
+ final JsonObject jsonId = gson.toJsonTree(id).getAsJsonObject();
+ final JsonElement expected = gson.fromJson("{\"name\":\"id\"}", JsonElement.class);
+ assertEquals("identity", expected, jsonId.getAsJsonObject());
+ }
+
+ @Test
+ public void testSerializeAsType() {
+
+ final AsTypeCodec asTypeCodec = new AsTypeCodec(DataType.FLOAT64, DataType.INT16);
+ final JsonObject jsonAsType = gson.toJsonTree(asTypeCodec).getAsJsonObject();
+ final JsonElement expected = gson.fromJson(
+ "{\"name\":\"astype\",\"configuration\":{\"dataType\":\"float64\",\"encodedType\":\"int16\"}}",
+ JsonElement.class);
+ assertEquals("asType", expected, jsonAsType.getAsJsonObject());
+ }
+
+ @Test
+ public void testSerializeCodecArray() {
+
+ Codec[] codecs = new Codec[]{
+ new IdentityCodec(),
+ new AsTypeCodec(DataType.FLOAT64, DataType.INT16)
+ };
+ JsonArray jsonCodecArray = gson.toJsonTree(codecs).getAsJsonArray();
+ JsonElement expected = gson.fromJson(
+ "[{\"name\":\"id\"},{\"name\":\"astype\",\"configuration\":{\"dataType\":\"float64\",\"encodedType\":\"int16\"}}]",
+ JsonElement.class);
+ assertEquals("codec array", expected, jsonCodecArray.getAsJsonArray());
+
+ Codec[] codecsDeserialized = gson.fromJson(expected, Codec[].class);
+ assertEquals("codecs length not 2", 2, codecsDeserialized.length);
+ assertTrue("first codec not identity", codecsDeserialized[0] instanceof IdentityCodec);
+ assertTrue("second codec not asType", codecsDeserialized[1] instanceof AsTypeCodec);
+
+ codecs = new Codec[]{
+ new AsTypeCodec(DataType.FLOAT64, DataType.INT16),
+ new GzipCompression()
+ };
+ jsonCodecArray = gson.toJsonTree(codecs).getAsJsonArray();
+ expected = gson.fromJson(
+ "[{\"name\":\"astype\",\"configuration\":{\"dataType\":\"float64\",\"encodedType\":\"int16\"}},{\"name\":\"gzip\",\"configuration\":{\"level\":-1,\"useZlib\":false}}]",
+ JsonElement.class);
+ assertEquals("codec array", expected, jsonCodecArray.getAsJsonArray());
+
+ codecsDeserialized = gson.fromJson(expected, Codec[].class);
+ assertEquals("codecs length not 2", 2, codecsDeserialized.length);
+ assertTrue("first codec not asType", codecsDeserialized[0] instanceof AsTypeCodec);
+ assertTrue("second codec not gzip", codecsDeserialized[1] instanceof GzipCompression);
+ }
+
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardIndexTest.java b/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardIndexTest.java
new file mode 100644
index 00000000..0c8ee24a
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardIndexTest.java
@@ -0,0 +1,124 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Paths;
+
+import org.janelia.saalfeldlab.n5.KeyValueAccess;
+import org.janelia.saalfeldlab.n5.LockedChannel;
+import org.janelia.saalfeldlab.n5.N5FSTest;
+import org.janelia.saalfeldlab.n5.N5KeyValueWriter;
+import org.janelia.saalfeldlab.n5.codec.RawBytes;
+import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodec;
+import org.janelia.saalfeldlab.n5.codec.checksum.Crc32cChecksumCodec;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec.IndexLocation;
+import org.janelia.saalfeldlab.n5.util.GridIterator;
+import org.junit.After;
+import org.junit.Test;
+
+public class ShardIndexTest {
+
+ private static final N5FSTest tempN5Factory = new N5FSTest();
+
+ @After
+ public void removeTempWriters() {
+ tempN5Factory.removeTempWriters();
+ }
+
+ @Test
+ public void testOffsetIndex() throws IOException {
+
+ int[] shardBlockGridSize = new int[]{5,4,3};
+ ShardIndex index = new ShardIndex(
+ shardBlockGridSize,
+ IndexLocation.END, new RawBytes());
+
+ GridIterator it = new GridIterator(shardBlockGridSize);
+ int i = 0;
+ while( it.hasNext()) {
+ int j = index.getOffsetIndex(GridIterator.long2int(it.next()));
+ assertEquals(i, j);
+ i+=2;
+ }
+
+
+ shardBlockGridSize = new int[]{5,4,3,13};
+ index = new ShardIndex(
+ shardBlockGridSize,
+ IndexLocation.END, new RawBytes());
+
+ it = new GridIterator(shardBlockGridSize);
+ i = 0;
+ while( it.hasNext()) {
+ int j = index.getOffsetIndex(GridIterator.long2int(it.next()));
+ assertEquals(i, j);
+ i+=2;
+ }
+
+ }
+
+ @Test
+ public void testReadVirtual() throws IOException {
+
+ final N5KeyValueWriter writer = (N5KeyValueWriter) tempN5Factory.createTempN5Writer();
+ final KeyValueAccess kva = writer.getKeyValueAccess();
+
+ final int[] shardBlockGridSize = new int[] { 6, 5 };
+ final IndexLocation indexLocation = IndexLocation.END;
+ final DeterministicSizeCodec[] indexCodecs = new DeterministicSizeCodec[] { new RawBytes(),
+ new Crc32cChecksumCodec() };
+
+ final String path = Paths.get(Paths.get(writer.getURI()).toAbsolutePath().toString(), "0").toString();
+
+ final ShardIndex index = new ShardIndex(shardBlockGridSize, indexLocation, indexCodecs);
+ index.set(0, 6, new int[] { 0, 0 });
+ index.set(19, 32, new int[] { 1, 0 });
+ index.set(93, 111, new int[] { 3, 0 });
+ index.set(143, 1, new int[] { 1, 2 });
+ ShardIndex.write(index, kva, path);
+
+ final ShardIndex other = new ShardIndex(shardBlockGridSize, indexLocation, indexCodecs);
+ ShardIndex.read(kva, path, other);
+
+ assertEquals(index, other);
+ }
+
+ @Test
+ public void testReadInMemory() throws IOException {
+
+ final N5KeyValueWriter writer = (N5KeyValueWriter) tempN5Factory.createTempN5Writer();
+ final KeyValueAccess kva = writer.getKeyValueAccess();
+
+ final int[] shardBlockGridSize = new int[] { 6, 5 };
+ final IndexLocation indexLocation = IndexLocation.END;
+ final DeterministicSizeCodec[] indexCodecs = new DeterministicSizeCodec[] {
+ new RawBytes(),
+ new Crc32cChecksumCodec() };
+ final String path = Paths.get(Paths.get(writer.getURI()).toAbsolutePath().toString(), "indexTest").toString();
+
+ final ShardIndex index = new ShardIndex(shardBlockGridSize, indexLocation, indexCodecs);
+ index.set(0, 6, new int[] { 0, 0 });
+ index.set(19, 32, new int[] { 1, 0 });
+ index.set(93, 111, new int[] { 3, 0 });
+ index.set(143, 1, new int[] { 1, 2 });
+ ShardIndex.write(index, kva, path);
+
+ final ShardIndex indexRead = new ShardIndex(shardBlockGridSize, indexLocation, indexCodecs);
+ ShardIndex.read(rawBytes(kva, path), indexRead);
+
+ assertEquals(index, indexRead);
+ }
+
+ private static byte[] rawBytes(KeyValueAccess kva, String path) throws IOException {
+
+ final byte[] rawBytes = new byte[(int) kva.size(path)];
+ try (final LockedChannel lockedChannel = kva.lockForReading(path)) {
+ try (final InputStream is = lockedChannel.newInputStream()) {
+ is.read(rawBytes);
+ }
+ }
+ return rawBytes;
+ }
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardPropertiesTests.java b/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardPropertiesTests.java
new file mode 100644
index 00000000..eb0d6de4
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardPropertiesTests.java
@@ -0,0 +1,128 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodec;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec.IndexLocation;
+import org.janelia.saalfeldlab.n5.util.Position;
+import org.junit.Test;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+
+public class ShardPropertiesTests {
+
+ @Test
+ public void testShardProperties() throws Exception {
+
+ final long[] arraySize = new long[]{16, 16};
+ final int[] shardSize = new int[]{16, 16};
+ final long[] shardPosition = new long[]{1, 1};
+ final int[] blkSize = new int[]{4, 4};
+
+ final DatasetAttributes dsetAttrs = new DatasetAttributes(
+ arraySize,
+ shardSize,
+ blkSize,
+ DataType.UINT8,
+ new ShardingCodec(
+ blkSize,
+ new Codec[]{},
+ new DeterministicSizeCodec[]{},
+ IndexLocation.END
+ )
+ );
+
+ @SuppressWarnings({"rawtypes", "unchecked"}) final InMemoryShard shard = new InMemoryShard(dsetAttrs, shardPosition, null);
+
+ assertArrayEquals(new int[]{4, 4}, shard.getBlockGridSize());
+
+ assertArrayEquals(new long[]{0, 0}, shard.getShardPosition(0, 0));
+ assertArrayEquals(new long[]{1, 1}, shard.getShardPosition(5, 5));
+ assertArrayEquals(new long[]{1, 0}, shard.getShardPosition(5, 0));
+ assertArrayEquals(new long[]{0, 1}, shard.getShardPosition(0, 5));
+
+ // assertNull(shard.getBlockPosition(0, 0));
+ // assertNull(shard.getBlockPosition(3, 3));
+
+ assertArrayEquals(new int[]{0, 0}, shard.getBlockPosition(4, 4));
+ assertArrayEquals(new int[]{1, 1}, shard.getBlockPosition(5, 5));
+ assertArrayEquals(new int[]{2, 2}, shard.getBlockPosition(6, 6));
+ assertArrayEquals(new int[]{3, 3}, shard.getBlockPosition(7, 7));
+ }
+
+ @Test
+ public void testShardBlockPositionIterator() throws Exception {
+
+ final long[] arraySize = new long[]{16, 16};
+ final int[] shardSize = new int[]{16, 16};
+ final long[] shardPosition = new long[]{1, 1};
+ final int[] blkSize = new int[]{4, 4};
+
+ final DatasetAttributes dsetAttrs = new DatasetAttributes(
+ arraySize,
+ shardSize,
+ blkSize,
+ DataType.UINT8,
+ new ShardingCodec(
+ blkSize,
+ new Codec[]{},
+ new DeterministicSizeCodec[]{},
+ IndexLocation.END
+ )
+ );
+
+ @SuppressWarnings({"rawtypes", "unchecked"}) final InMemoryShard shard = new InMemoryShard(dsetAttrs, shardPosition, null);
+
+ int i = 0;
+ Iterator it = shard.blockPositionIterator();
+ long[] p = null;
+ while (it.hasNext()) {
+
+ p = it.next();
+ if (i == 0)
+ assertArrayEquals(new long[]{4, 4}, p);
+
+ i++;
+ }
+ assertEquals(16, i);
+ assertArrayEquals(new long[]{7, 7}, p);
+ }
+
+ @Test
+ public void testShardGrouping() {
+
+ final long[] arraySize = new long[]{8, 12};
+ final int[] shardSize = new int[]{4, 6};
+ final int[] blkSize = new int[]{2, 3};
+
+ final DatasetAttributes attrs = new DatasetAttributes(
+ arraySize,
+ shardSize,
+ blkSize,
+ DataType.UINT8,
+ new ShardingCodec(
+ blkSize,
+ new Codec[]{},
+ new DeterministicSizeCodec[]{},
+ IndexLocation.END
+ )
+ );
+
+ List blockPositions = attrs.blockPositions().collect(Collectors.toList());
+ final Map> result = attrs.groupBlockPositions(blockPositions);
+
+ // there are four shards in this image
+ assertEquals(4, result.keySet().size());
+
+ // there are four blocks per shard in this image
+ result.values().stream().forEach(x -> assertEquals(4, x.size()));
+ }
+
+}
diff --git a/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardTest.java b/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardTest.java
new file mode 100644
index 00000000..b19e07e5
--- /dev/null
+++ b/src/test/java/org/janelia/saalfeldlab/n5/shard/ShardTest.java
@@ -0,0 +1,319 @@
+package org.janelia.saalfeldlab.n5.shard;
+
+import org.janelia.saalfeldlab.n5.ByteArrayDataBlock;
+import org.janelia.saalfeldlab.n5.DataBlock;
+import org.janelia.saalfeldlab.n5.DataType;
+import org.janelia.saalfeldlab.n5.DatasetAttributes;
+import org.janelia.saalfeldlab.n5.KeyValueAccess;
+import org.janelia.saalfeldlab.n5.N5FSTest;
+import org.janelia.saalfeldlab.n5.N5KeyValueWriter;
+import org.janelia.saalfeldlab.n5.N5Writer;
+import org.janelia.saalfeldlab.n5.ShardedDatasetAttributes;
+import org.janelia.saalfeldlab.n5.codec.Codec;
+import org.janelia.saalfeldlab.n5.codec.DeterministicSizeCodec;
+import org.janelia.saalfeldlab.n5.codec.N5BlockCodec;
+import org.janelia.saalfeldlab.n5.codec.RawBytes;
+import org.janelia.saalfeldlab.n5.codec.checksum.Crc32cChecksumCodec;
+import org.janelia.saalfeldlab.n5.shard.ShardingCodec.IndexLocation;
+import org.janelia.saalfeldlab.n5.util.GridIterator;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.junit.Assert.assertArrayEquals;
+
+@RunWith(Parameterized.class)
+public class ShardTest {
+
+ private static final N5FSTest tempN5Factory = new N5FSTest();
+
+ @Parameterized.Parameters(name = "IndexLocation({0}), Block ByteOrder({1}), Index ByteOrder({2})")
+ public static Collection