Repository: saalfeldlab/n5 Branch: master Commit: 273be1512b62 Files: 200 Total size: 931.3 KB Directory structure: gitextract_yquuu4ly/ ├── .github/ │ ├── build.sh │ ├── setup.sh │ └── workflows/ │ └── build.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── doc/ │ ├── LICENSE.md │ ├── README.md │ └── n5-eclipse-style.xml ├── pom.xml ├── scripts/ │ ├── fsLockValidation │ └── writeLockTest.sh └── src/ ├── main/ │ └── java/ │ └── org/ │ └── janelia/ │ └── saalfeldlab/ │ └── n5/ │ ├── AbstractDataBlock.java │ ├── BufferedKvaLockedChannel.java │ ├── ByteArrayDataBlock.java │ ├── Bzip2Compression.java │ ├── CachedGsonKeyValueN5Reader.java │ ├── CachedGsonKeyValueN5Writer.java │ ├── ChannelLock.java │ ├── Compression.java │ ├── CompressionAdapter.java │ ├── DataBlock.java │ ├── DataType.java │ ├── DatasetAttributes.java │ ├── DoubleArrayDataBlock.java │ ├── FileKeyLockManager.java │ ├── FileSystemKeyValueAccess.java │ ├── FloatArrayDataBlock.java │ ├── FsIoPolicy.java │ ├── GsonKeyValueN5Reader.java │ ├── GsonKeyValueN5Writer.java │ ├── GsonN5Reader.java │ ├── GsonN5Writer.java │ ├── GsonUtils.java │ ├── GzipCompression.java │ ├── HttpKeyValueAccess.java │ ├── IntArrayDataBlock.java │ ├── IoPolicy.java │ ├── KeyLockState.java │ ├── KeyValueAccess.java │ ├── LinkedAttributePathToken.java │ ├── LockedChannel.java │ ├── LockedFileChannel.java │ ├── LockingPolicy.java │ ├── LongArrayDataBlock.java │ ├── Lz4Compression.java │ ├── N5Exception.java │ ├── N5FSReader.java │ ├── N5FSWriter.java │ ├── N5KeyValueReader.java │ ├── N5KeyValueWriter.java │ ├── N5Reader.java │ ├── N5URI.java │ ├── N5Writer.java │ ├── NameConfigAdapter.java │ ├── RawCompression.java │ ├── ReflectionUtils.java │ ├── ShortArrayDataBlock.java │ ├── StringDataBlock.java │ ├── XzCompression.java │ ├── cache/ │ │ ├── N5JsonCache.java │ │ └── N5JsonCacheableContainer.java │ ├── codec/ │ │ ├── BlockCodec.java │ │ ├── BlockCodecInfo.java │ │ ├── CodecInfo.java │ │ ├── CodecParser.java │ │ ├── ConcatenatedDataCodec.java │ │ ├── ConcatenatedDeterministicSizeDataCodec.java │ │ ├── DataCodec.java │ │ ├── DataCodecInfo.java │ │ ├── DatasetCodec.java │ │ ├── DatasetCodecInfo.java │ │ ├── DeterministicSizeCodecInfo.java │ │ ├── DeterministicSizeDataCodec.java │ │ ├── FlatArrayCodec.java │ │ ├── IdentityCodec.java │ │ ├── IndexCodecAdapter.java │ │ ├── N5BlockCodecInfo.java │ │ ├── N5BlockCodecs.java │ │ ├── RawBlockCodecInfo.java │ │ ├── RawBlockCodecs.java │ │ ├── checksum/ │ │ │ ├── ChecksumCodec.java │ │ │ ├── ChecksumException.java │ │ │ └── Crc32cChecksumCodec.java │ │ └── transpose/ │ │ ├── Transpose.java │ │ ├── TransposeCodec.java │ │ └── TransposeCodecInfo.java │ ├── http/ │ │ ├── ApacheListResponseParser.java │ │ ├── CandidateListResponseParser.java │ │ ├── ListResponseParser.java │ │ ├── MicrosoftListResponseParser.java │ │ ├── PatternListResponseParser.java │ │ └── PythonListResponseParser.java │ ├── readdata/ │ │ ├── ByteArrayReadData.java │ │ ├── InputStreamReadData.java │ │ ├── LazyGeneratedReadData.java │ │ ├── LazyRead.java │ │ ├── LazyReadData.java │ │ ├── Range.java │ │ ├── ReadData.java │ │ ├── VolatileReadData.java │ │ ├── prefetch/ │ │ │ ├── AggregatingPrefetchLazyRead.java │ │ │ ├── EnclosingPrefetchLazyRead.java │ │ │ ├── SliceTrackingLazyRead.java │ │ │ └── Slices.java │ │ └── segment/ │ │ ├── ConcatenatedReadData.java │ │ ├── DefaultSegmentedReadData.java │ │ ├── Segment.java │ │ └── SegmentedReadData.java │ ├── serialization/ │ │ ├── JsonArrayUtils.java │ │ ├── N5Annotations.java │ │ └── NameConfig.java │ ├── shard/ │ │ ├── DatasetAccess.java │ │ ├── DefaultDatasetAccess.java │ │ ├── DefaultShardCodecInfo.java │ │ ├── Nesting.java │ │ ├── PositionValueAccess.java │ │ ├── RawShard.java │ │ ├── RawShardCodec.java │ │ ├── RawShardDataBlock.java │ │ ├── Region.java │ │ ├── ShardCodecInfo.java │ │ └── ShardIndex.java │ └── util/ │ ├── FloatValueParser.java │ ├── MemCopy.java │ └── SubArrayCopy.java └── test/ ├── java/ │ └── org/ │ └── janelia/ │ └── saalfeldlab/ │ └── n5/ │ ├── AbstractN5Test.java │ ├── DatasetAttributesTest.java │ ├── FileKeyLockManagerTest.java │ ├── FsLockTest.java │ ├── N5Benchmark.java │ ├── N5CachedFSTest.java │ ├── N5FSTest.java │ ├── N5ReadBenchmark.java │ ├── N5URITest.java │ ├── TrackingN5Writer.java │ ├── UriTest.java │ ├── WriteLockExp.java │ ├── backward/ │ │ ├── CompatibilityTest.java │ │ └── CreateSampleData.java │ ├── benchmarks/ │ │ ├── N5BlockWriteBenchmarks.java │ │ └── ReadDataBenchmarks.java │ ├── cache/ │ │ └── N5CacheTest.java │ ├── codec/ │ │ ├── BlockCodecTests.java │ │ ├── BytesCodecTests.java │ │ ├── ChecksumCodecTests.java │ │ └── DatasetCodecTests.java │ ├── compression/ │ │ └── CompressionTypesTest.java │ ├── demo/ │ │ └── AttributePathDemo.java │ ├── http/ │ │ ├── HttpKeyValueAccessTest.java │ │ ├── HttpReaderFsWriter.java │ │ ├── N5HttpTest.java │ │ └── RunnerWithHttpServer.java │ ├── kva/ │ │ ├── AbstractKeyValueAccessTest.java │ │ ├── DelegateKeyValueAccess.java │ │ ├── FileSystemKeyValueAccessTest.java │ │ ├── FsLockingValidation.java │ │ ├── HttpKeyValueAccessTest.java │ │ └── TrackingKeyValueAccess.java │ ├── locking/ │ │ ├── JustFileChannels.java │ │ └── JustFileChannelsThreaded.java │ ├── readdata/ │ │ ├── RangeTests.java │ │ ├── ReadDataTests.java │ │ ├── prefetch/ │ │ │ ├── SliceTrackingLazyReadTests.java │ │ │ └── SlicesTest.java │ │ └── segment/ │ │ ├── ConcatenatedReadDataTest.java │ │ └── SegmentTest.java │ ├── serialization/ │ │ └── CodecSerializationTest.java │ ├── shard/ │ │ ├── DatasetAccessTest.java │ │ ├── NestedGridTest.java │ │ ├── ShardTest.java │ │ ├── TestPositionValueAccess.java │ │ ├── WriteRegionTest.java │ │ ├── WriteShardTest.java │ │ ├── WriteShardTest2.java │ │ └── WriteShardTestTruncate.java │ └── url/ │ └── UriAttributeTest.java └── resources/ ├── backward/ │ ├── data-1.5.0.n5/ │ │ ├── attributes.json │ │ └── raw/ │ │ ├── 0/ │ │ │ ├── 0 │ │ │ └── 1 │ │ ├── 1/ │ │ │ ├── 0 │ │ │ └── 1 │ │ └── attributes.json │ ├── data-2.5.1.n5/ │ │ ├── attributes.json │ │ └── raw/ │ │ ├── 0/ │ │ │ ├── 0 │ │ │ └── 1 │ │ ├── 1/ │ │ │ ├── 0 │ │ │ └── 1 │ │ └── attributes.json │ └── data-3.1.3.n5/ │ ├── attributes.json │ └── raw/ │ ├── 0/ │ │ ├── 0 │ │ └── 1 │ ├── 1/ │ │ ├── 0 │ │ └── 1 │ └── attributes.json └── url/ └── urlAttributes.n5/ ├── a/ │ ├── aa/ │ │ ├── aaa/ │ │ │ └── attributes.json │ │ └── attributes.json │ └── attributes.json ├── attributes.json └── objs/ └── attributes.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/build.sh ================================================ #!/bin/sh curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/main/ci-build.sh sh ci-build.sh ================================================ FILE: .github/setup.sh ================================================ #!/bin/sh curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/main/ci-setup-github-actions.sh sh ci-setup-github-actions.sh # Let the Linux build handle artifact deployment. if [ "$(uname)" != Linux ] then echo "No deploy -- non-Linux build" echo "NO_DEPLOY=1" >> $GITHUB_ENV fi ================================================ FILE: .github/workflows/build.yml ================================================ name: build on: push: branches: - master - development tags: - "*-[0-9]+.*" pull_request: branches: - master - development jobs: build: strategy: matrix: os: [ubuntu-latest, windows-latest] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - name: Setup Python uses: actions/setup-python@v5 with: python-version: '3.10' - name: Set up Java uses: actions/setup-java@v4 with: java-version: '8' distribution: 'zulu' cache: 'maven' - name: Set up CI environment run: .github/setup.sh shell: bash - name: Execute the build run: .github/build.sh shell: bash env: GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} MAVEN_USER: ${{ secrets.MAVEN_USER }} MAVEN_PASS: ${{ secrets.MAVEN_PASS }} OSSRH_PASS: ${{ secrets.OSSRH_PASS }} SIGNING_ASC: ${{ secrets.SIGNING_ASC }} ================================================ FILE: .gitignore ================================================ *.class # Mobile Tools for Java (J2ME) .mtj.tmp/ # Package Files # *.jar *.war *.ear # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* /target/ .classpath .project .settings .idea *.iml ================================================ FILE: LICENSE.md ================================================ ## BSD 2-Clause License Copyright (c) 2017-2026, 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: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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 HOLDER 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. ================================================ FILE: README.md ================================================ # N5 [![Build Status](https://github.com/saalfeldlab/n5/actions/workflows/build.yml/badge.svg)](https://github.com/saalfeldlab/n5/actions/workflows/build.yml) The N5 API specifies the primitive operations needed to store large chunked n-dimensional tensors, and arbitrary meta-data in a hierarchy of groups similar to HDF5. Other than HDF5, N5 is not bound to a specific backend. This repository includes a simple [file-system backend](#file-system-specification). There are also an [HDF5 backend](https://github.com/saalfeldlab/n5-hdf5), a [Zarr backend](https://github.com/saalfeldlab/n5-zarr), a [Google Cloud backend](https://github.com/saalfeldlab/n5-google-cloud), and an [AWS-S3 backend](https://github.com/saalfeldlab/n5-aws-s3). At this time, N5 supports: * arbitrary group hierarchies * arbitrary meta-data (stored as JSON or HDF5 attributes) * chunked n-dimensional tensor datasets * value-datatypes: [u]int8, [u]int16, [u]int32, [u]int64, float32, float64 * compression: raw, gzip, zlib, bzip2, xz, and lz4 are included in this repository, custom compression schemes can be added Chunked datasets can be sparse, i.e. empty chunks do not need to be stored. ## File-system specification *version 4.0.0* N5 group is not a single file but simply a directory on the file system. Meta-data is stored as a JSON file per each group/ directory. Tensor datasets can be chunked and chunks are stored as individual files. This enables parallel reading and writing on a cluster. 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 "/". 4. A dataset is a group with the mandatory attributes: * dimensions (e.g. [100, 200, 300]), * blockSize (e.g. [64, 64, 64]), * dataType (one of {uint8, uint16, uint32, uint64, int8, int16, int32, int64, float32, float64, object}) * compression as a struct with the mandatory attribute type that specifies the compression scheme, currently available are: * raw (no parameters), * bzip2 with parameters * blockSize ([1-9], default 9) * gzip with parameters * level (integer, default -1) * lz4 with parameters * blockSize (integer, default 65536) * 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/). 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). 8. All chunks of a chunked dataset have the same size except for end-chunks that may be smaller, therefore 9. Chunks are stored in the following binary format: * mode (uint16 big endian, default = 0x0000, varlength = 0x0001, object = 0x0002) * number of dimensions (uint16 big endian) * dimension 1[,...,n] (uint32 big endian) * [ mode == varlength ? number of elements (uint32 big endian) ] * compressed data (big endian) Example: A 3-dimensional `uint16` datablock of 1×2×3 pixels with raw compression storing the values (1,2,3,4,5,6) starts with: ```hexdump 00000000: 00 00 .. # 0 (default mode) 00000002: 00 03 .. # 3 (number of dimensions) 00000004: 00 00 00 01 .... # 1 (dimensions) 00000008: 00 00 00 02 .... # 2 0000000c: 00 00 00 03 .... # 3 ``` followed by data stored as raw or compressed big endian values. For raw: ```hexdump 00000010: 00 01 .. # 1 00000012: 00 02 .. # 2 00000014: 00 03 .. # 3 00000016: 00 04 .. # 4 00000018: 00 05 .. # 5 0000001a: 00 06 .. # 6 ``` for bzip2 compression: ```hexdump 00000010: 42 5a 68 39 BZh9 00000014: 31 41 59 26 1AY& 00000018: 53 59 02 3e SY.> 0000001c: 0d d2 00 00 .... 00000020: 00 40 00 7f .@.. 00000024: 00 20 00 31 . .1 00000028: 0c 01 0d 31 ...1 0000002c: a8 73 94 33 .s.3 00000030: 7c 5d c9 14 |].. 00000034: e1 42 40 08 .B@. 00000038: f8 37 48 .7H ``` for gzip compression: ```hexdump 00000010: 1f 8b 08 00 .... 00000014: 00 00 00 00 .... 00000018: 00 00 63 60 ..c` 0000001c: 64 60 62 60 d`b` 00000020: 66 60 61 60 f`a` 00000024: 65 60 03 00 e`.. 00000028: aa ea 6d bf ..m. 0000002c: 0c 00 00 00 .... ``` for xz compression: ```hexdump 00000010: fd 37 7a 58 .7zX 00000014: 5a 00 00 04 Z... 00000018: e6 d6 b4 46 ...F 0000001c: 02 00 21 01 ..!. 00000020: 16 00 00 00 .... 00000024: 74 2f e5 a3 t/.. 00000028: 01 00 0b 00 .... 0000002c: 01 00 02 00 .... 00000030: 03 00 04 00 .... 00000034: 05 00 06 00 .... 00000038: 0d 03 09 ca .... 0000003c: 34 ec 15 a7 4... 00000040: 00 01 24 0c ..$. 00000044: a6 18 d8 d8 .... 00000048: 1f b6 f3 7d ...} 0000004c: 01 00 00 00 .... 00000050: 00 04 59 5a ..YZ ``` ## Extensible compression schemes Custom compression schemes can be implemented using the annotation discovery mechanism of SciJava. Implement the [`BlockReader`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/BlockReader.java) and [`BlockWriter`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/BlockWriter.java) interfaces for the compression scheme and create a parameter class implementing the [`Compression`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/Compression.java) interface that is annotated with the [`CompressionType`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/Compression.java#L51) and [`CompressionParameter`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/Compression.java#L63) annotations. Typically, all this can happen in a single class such as in [`GzipCompression`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/GzipCompression.java). ## Disclaimer HDF5 is a great format that provides a wealth of conveniences that I do not want to miss. Its inefficiency for parallel writing, however, limits its applicability for handling of very large n-dimensional data. 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. ================================================ FILE: doc/LICENSE.md ================================================ ## BSD 2-Clause License Copyright (c) @project.inceptionYear@-@current.year@, 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: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * 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 HOLDER 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. ================================================ FILE: doc/README.md ================================================ # N5 [![Build Status](https://github.com/saalfeldlab/n5/actions/workflows/build.yml/badge.svg)](https://github.com/saalfeldlab/n5/actions/workflows/build.yml) The N5 API specifies the primitive operations needed to store large chunked n-dimensional tensors, and arbitrary meta-data in a hierarchy of groups similar to HDF5. Other than HDF5, N5 is not bound to a specific backend. This repository includes a simple [file-system backend](#file-system-specification). There are also an [HDF5 backend](https://github.com/saalfeldlab/n5-hdf5), a [Zarr backend](https://github.com/saalfeldlab/n5-zarr), a [Google Cloud backend](https://github.com/saalfeldlab/n5-google-cloud), and an [AWS-S3 backend](https://github.com/saalfeldlab/n5-aws-s3). At this time, N5 supports: * arbitrary group hierarchies * arbitrary meta-data (stored as JSON or HDF5 attributes) * chunked n-dimensional tensor datasets * value-datatypes: [u]int8, [u]int16, [u]int32, [u]int64, float32, float64 * compression: raw, gzip, zlib, bzip2, xz, and lz4 are included in this repository, custom compression schemes can be added Chunked datasets can be sparse, i.e. empty chunks do not need to be stored. ## File-system specification *version @n5-spec.version@* N5 group is not a single file but simply a directory on the file system. Meta-data is stored as a JSON file per each group/ directory. Tensor datasets can be chunked and chunks are stored as individual files. This enables parallel reading and writing on a cluster. 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 @n5-spec.version@ 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]), * dataType (one of {uint8, uint16, uint32, uint64, int8, int16, int32, int64, float32, float64, object}) * compression as a struct with the mandatory attribute type that specifies the compression scheme, currently available are: * raw (no parameters), * bzip2 with parameters * blockSize ([1-9], default 9) * gzip with parameters * level (integer, default -1) * lz4 with parameters * blockSize (integer, default 65536) * 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/). 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). 8. All chunks of a chunked dataset have the same size except for end-chunks that may be smaller, therefore 9. Chunks are stored in the following binary format: * mode (uint16 big endian, default = 0x0000, varlength = 0x0001, object = 0x0002) * number of dimensions (uint16 big endian) * dimension 1[,...,n] (uint32 big endian) * [ mode == varlength ? number of elements (uint32 big endian) ] * compressed data (big endian) Example: A 3-dimensional `uint16` datablock of 1×2×3 pixels with raw compression storing the values (1,2,3,4,5,6) starts with: ```hexdump 00000000: 00 00 .. # 0 (default mode) 00000002: 00 03 .. # 3 (number of dimensions) 00000004: 00 00 00 01 .... # 1 (dimensions) 00000008: 00 00 00 02 .... # 2 0000000c: 00 00 00 03 .... # 3 ``` followed by data stored as raw or compressed big endian values. For raw: ```hexdump 00000010: 00 01 .. # 1 00000012: 00 02 .. # 2 00000014: 00 03 .. # 3 00000016: 00 04 .. # 4 00000018: 00 05 .. # 5 0000001a: 00 06 .. # 6 ``` for bzip2 compression: ```hexdump 00000010: 42 5a 68 39 BZh9 00000014: 31 41 59 26 1AY& 00000018: 53 59 02 3e SY.> 0000001c: 0d d2 00 00 .... 00000020: 00 40 00 7f .@.. 00000024: 00 20 00 31 . .1 00000028: 0c 01 0d 31 ...1 0000002c: a8 73 94 33 .s.3 00000030: 7c 5d c9 14 |].. 00000034: e1 42 40 08 .B@. 00000038: f8 37 48 .7H ``` for gzip compression: ```hexdump 00000010: 1f 8b 08 00 .... 00000014: 00 00 00 00 .... 00000018: 00 00 63 60 ..c` 0000001c: 64 60 62 60 d`b` 00000020: 66 60 61 60 f`a` 00000024: 65 60 03 00 e`.. 00000028: aa ea 6d bf ..m. 0000002c: 0c 00 00 00 .... ``` for xz compression: ```hexdump 00000010: fd 37 7a 58 .7zX 00000014: 5a 00 00 04 Z... 00000018: e6 d6 b4 46 ...F 0000001c: 02 00 21 01 ..!. 00000020: 16 00 00 00 .... 00000024: 74 2f e5 a3 t/.. 00000028: 01 00 0b 00 .... 0000002c: 01 00 02 00 .... 00000030: 03 00 04 00 .... 00000034: 05 00 06 00 .... 00000038: 0d 03 09 ca .... 0000003c: 34 ec 15 a7 4... 00000040: 00 01 24 0c ..$. 00000044: a6 18 d8 d8 .... 00000048: 1f b6 f3 7d ...} 0000004c: 01 00 00 00 .... 00000050: 00 04 59 5a ..YZ ``` ## Extensible compression schemes Custom compression schemes can be implemented using the annotation discovery mechanism of SciJava. Implement the [`BlockReader`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/BlockReader.java) and [`BlockWriter`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/BlockWriter.java) interfaces for the compression scheme and create a parameter class implementing the [`Compression`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/Compression.java) interface that is annotated with the [`CompressionType`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/Compression.java#L51) and [`CompressionParameter`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/Compression.java#L63) annotations. Typically, all this can happen in a single class such as in [`GzipCompression`](https://github.com/saalfeldlab/n5/blob/master/src/main/java/org/janelia/saalfeldlab/n5/GzipCompression.java). ## Disclaimer HDF5 is a great format that provides a wealth of conveniences that I do not want to miss. Its inefficiency for parallel writing, however, limits its applicability for handling of very large n-dimensional data. 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. ================================================ FILE: doc/n5-eclipse-style.xml ================================================ ================================================ FILE: pom.xml ================================================ 4.0.0 org.scijava pom-scijava 43.0.0 org.janelia.saalfeldlab n5 4.0.1-SNAPSHOT N5 Not HDF5 https://github.com/saalfeldlab/n5 2017 Saalfeld Lab http://saalfeldlab.janelia.org/ Simplified BSD License repo axtimwalde Stephan Saalfeld http://imagej.net/User:Saalfeld founder lead developer debugger reviewer support maintainer bogovicj John Bogovic http://imagej.net/User:Bogovic lead developer debugger reviewer support maintainer Caleb Hulbert lead developer debugger reviewer support maintainer Stephan Saalfeld axtimwalde John Bogovic bogovicj Igor Pisarev igorpisarev Neil Thistlethwaite Andrew Champion Philipp Hanslovsky hanslovsky Caleb Hulbert Tobias Pietzsch tpietzsch Mark Kittisopikul Image.sc Forum https://forum.image.sc/tag/n5 scm:git:git://github.com/saalfeldlab/n5 scm:git:git@github.com:saalfeldlab/n5 HEAD https://github.com/saalfeldlab/n5 GitHub https://github.com/saalfedlab/n5/issues GitHub Actions https://github.com/saalfeldlab/n5/actions org.janelia.saalfeldlab.n5 bsd_2 Not HDF5 Saalfeld Lab Stephan Saalfeld true true sign,deploy-to-scijava 4.0.0 2.4.0-alpha-9 4.4.0-alpha-9 2.0.0-alpha-4 5.2.0-alpha-7 2.3.0-alpha-7 7.1.0-alpha-8 2.0.0-alpha-8 2.0.0-alpha-4 org.tukaani xz org.lz4 lz4-java com.google.code.gson gson org.scijava scijava-common org.apache.commons commons-compress commons-io commons-io commons-codec commons-codec junit junit test org.janelia.saalfeldlab n5-universe org.janelia.saalfeldlab n5 test net.imagej ij test net.imglib2 imglib2 test net.imglib2 imglib2-ij test cisd jhdf5 test org.apache.commons commons-collections4 ${commons-collections4.version} test info.picocli picocli 4.3.2 test org.openjdk.jmh jmh-core test org.openjdk.jmh jmh-generator-annprocess test scijava.public https://maven.scijava.org/content/groups/public org.codehaus.mojo build-helper-maven-plugin timestamp-property timestamp-property validate current.year yyyy maven-resources-plugin copy compile copy-resources ${basedir} doc *.md true ================================================ FILE: scripts/fsLockValidation ================================================ #!/bin/bash # # Run FsLockingValidation N times in parallel. # # Usage: # [N=] ./fsLockValidation [FsLockingValidation options...] # # Example: # N=4 ./fsLockValidation --file /tmp/shard-test --num-repeats 5000 N=${N:-2} OWN_DIR="$(readlink -f "$(dirname "${BASH_SOURCE[0]}")")" POM="$OWN_DIR/../pom.xml" TARGET="$OWN_DIR/../target" if [ ! -d "$TARGET/test-classes" ]; then echo "test-classes not found -- run 'mvn test-compile' in n5/ first" >&2 exit 1 fi echo "Building classpath..." CP=$(mvn -q -f "$POM" dependency:build-classpath \ -DincludeScope=test \ -Dmdep.outputFile=/dev/stdout \ 2>/dev/null) CP="$TARGET/test-classes:$TARGET/classes:$CP" cleanup() { kill "${pids[@]}" 2>/dev/null wait "${pids[@]}" 2>/dev/null } trap cleanup INT TERM EXIT echo "Launching $N processes..." pids=() for i in $(seq 1 $N); do java -cp "$CP" org.janelia.saalfeldlab.n5.kva.FsLockingValidation "$@" & pids+=($!) done # Wait for all processes and collect exit codes all_ok=true for i in "${!pids[@]}"; do pid=${pids[$i]} wait "$pid" code=$? if [ $code -ne 0 ]; then echo "Process $((i+1)) (pid $pid) exited with code $code" >&2 all_ok=false fi done if $all_ok; then echo "All $N processes completed without errors." else echo "One or more processes reported errors." >&2 exit 1 fi ================================================ FILE: scripts/writeLockTest.sh ================================================ #!/usr/bin/env bash set -euo pipefail TEST_DIR=/tmp/write-lock-test.zarr [ -d "$TEST_DIR" ] && rm -r -- "$TEST_DIR" if [ ! -d "test-classes" ]; then mvn test-compile fi mvn -e -q exec:java \ -Dexec.mainClass=org.janelia.saalfeldlab.n5.WriteLockExp \ -Dexec.classpathScope=test \ -Dexec.args="/tmp/write-lock-test.zarr 0" & pid1=$! mvn -e -q exec:java \ -Dexec.mainClass=org.janelia.saalfeldlab.n5.WriteLockExp \ -Dexec.classpathScope=test \ -Dexec.args="/tmp/write-lock-test.zarr 1" & pid2=$! trap 'kill "$pid1" "$pid2" 2>/dev/null || true' EXIT INT TERM wait "$pid1" wait "$pid2" ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/AbstractDataBlock.java ================================================ package org.janelia.saalfeldlab.n5; import java.util.function.ToIntFunction; /** * Abstract base class for {@link DataBlock} implementations. * * @param * the block data type * * @author Stephan Saalfeld */ public abstract class AbstractDataBlock implements DataBlock { protected final int[] size; protected final long[] gridPosition; protected final T data; private final ToIntFunction numElements; public AbstractDataBlock( final int[] size, final long[] gridPosition, final T data, final ToIntFunction numElements) { this.size = size; this.gridPosition = gridPosition; this.data = data; this.numElements = numElements; } @Override public int[] getSize() { return size; } @Override public long[] getGridPosition() { return gridPosition; } @Override public T getData() { return data; } @Override public int getNumElements() { return numElements.applyAsInt(data); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/BufferedKvaLockedChannel.java ================================================ package org.janelia.saalfeldlab.n5; import org.apache.commons.io.input.ProxyInputStream; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import java.io.*; class BufferedKvaLockedChannel implements LockedChannel { private final KeyValueAccess kva; private final String key; private ByteArrayOutputStream baos = null; BufferedKvaLockedChannel(final KeyValueAccess kva, final String key) { this.kva = kva; this.key = key; } @Override public Reader newReader() throws N5Exception.N5IOException { return new InputStreamReader(newInputStream()); } @Override public InputStream newInputStream() throws N5Exception.N5IOException { VolatileReadData volatileReadData = kva.createReadData(key); return new ProxyInputStream(volatileReadData.inputStream()) { @Override public void close() throws IOException { super.close(); volatileReadData.close(); } }; } @Override public Writer newWriter() throws N5Exception.N5IOException { return new BufferedWriter(new OutputStreamWriter(newOutputStream())); } @Override public OutputStream newOutputStream() throws N5Exception.N5IOException { if (baos == null) baos = new ByteArrayOutputStream(); return baos; } @Override public void close() throws IOException { if (baos != null && baos.size() > 0) kva.write(key, ReadData.from(baos.toByteArray())); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/ByteArrayDataBlock.java ================================================ package org.janelia.saalfeldlab.n5; public class ByteArrayDataBlock extends AbstractDataBlock { public ByteArrayDataBlock(final int[] size, final long[] gridPosition, final byte[] data) { super(size, gridPosition, data, a -> a.length); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/Bzip2Compression.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.IOException; 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.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.serialization.NameConfig; @CompressionType("bzip2") @NameConfig.Name("bzip2") public class Bzip2Compression implements Compression { private static final long serialVersionUID = -4873117458390529118L; @CompressionParameter @NameConfig.Parameter private final int blockSize; public Bzip2Compression(final int blockSize) { this.blockSize = blockSize; } public Bzip2Compression() { this(BZip2CompressorOutputStream.MAX_BLOCKSIZE); } @Override public boolean equals(final Object other) { if (other == null || other.getClass() != Bzip2Compression.class) return false; else return blockSize == ((Bzip2Compression)other).blockSize; } @Override public ReadData decode(final ReadData readData) throws N5IOException { try { return ReadData.from(new BZip2CompressorInputStream(readData.inputStream())); } catch (IOException e) { throw new N5IOException(e); } } @Override public ReadData encode(final ReadData readData) { return readData.encode(out -> new BZip2CompressorOutputStream(out, blockSize)); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Reader.java ================================================ package org.janelia.saalfeldlab.n5; 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; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonObject; /** * {@link N5Reader} implementation through {@link KeyValueAccess} with JSON * attributes parsed with {@link Gson}. * */ public interface CachedGsonKeyValueN5Reader extends GsonKeyValueN5Reader, N5JsonCacheableContainer { default N5JsonCache newCache() { return new N5JsonCache(this); } boolean cacheMeta(); N5JsonCache getCache(); @Override default JsonElement getAttributesFromContainer(final String normalPathName, final String normalCacheKey) { // this implementation doesn't use cache key, but rather depends on // attributesPath being implemented return GsonKeyValueN5Reader.super.getAttributes(normalPathName); } @Override default DatasetAttributes getDatasetAttributes(final String pathName) { final String normalPath = N5URI.normalizeGroupPath(pathName); final JsonElement attributes; if (!datasetExists(pathName)) return null; if (cacheMeta()) { attributes = getCache().getAttributes(normalPath, getAttributesKey()); } else { attributes = GsonKeyValueN5Reader.super.getAttributes(normalPath); } return createDatasetAttributes(attributes); } default DatasetAttributes normalGetDatasetAttributes(final String pathName) throws N5IOException { final String normalPath = N5URI.normalizeGroupPath(pathName); final JsonElement attributes = GsonKeyValueN5Reader.super.getAttributes(normalPath); return createDatasetAttributes(attributes); } @Override default T getAttribute( final String pathName, final String key, final Class clazz) throws N5Exception { final String normalPathName = N5URI.normalizeGroupPath(pathName); final String normalizedAttributePath = N5URI.normalizeAttributePath(key); final JsonElement attributes; if (cacheMeta()) { attributes = getCache().getAttributes(normalPathName, getAttributesKey()); } else { attributes = GsonKeyValueN5Reader.super.getAttributes(normalPathName); } try { return GsonUtils.readAttribute(attributes, normalizedAttributePath, clazz, getGson()); } catch (JsonSyntaxException | NumberFormatException | ClassCastException e) { throw new N5Exception.N5ClassCastException(e); } } @Override default T getAttribute( final String pathName, final String key, final Type type) throws N5Exception { final String normalPathName = N5URI.normalizeGroupPath(pathName); final String normalizedAttributePath = N5URI.normalizeAttributePath(key); JsonElement attributes; if (cacheMeta()) { attributes = getCache().getAttributes(normalPathName, getAttributesKey()); } else { attributes = GsonKeyValueN5Reader.super.getAttributes(normalPathName); } try { return GsonUtils.readAttribute(attributes, normalizedAttributePath, type, getGson()); } catch (JsonSyntaxException | NumberFormatException | ClassCastException e) { throw new N5Exception.N5ClassCastException(e); } } @Override default boolean exists(final String pathName) { final String normalPathName = N5URI.normalizeGroupPath(pathName); if (cacheMeta()) return getCache().isGroup(normalPathName, getAttributesKey()); else { return existsFromContainer(normalPathName, null); } } @Override default boolean existsFromContainer(final String normalPathName, final String normalCacheKey) { final KeyValueAccess kva = getKeyValueAccess(); if (normalCacheKey == null) return kva.isDirectory(kva.compose(getURI(), normalPathName)); else return kva.isFile(kva.compose(getURI(), normalPathName, normalCacheKey)); } @Override default boolean groupExists(final String pathName) { final String normalPathName = N5URI.normalizeGroupPath(pathName); if (cacheMeta()) return getCache().isGroup(normalPathName, null); else { return isGroupFromContainer(normalPathName); } } @Override default boolean isGroupFromContainer(final String normalPathName) { return GsonKeyValueN5Reader.super.groupExists(normalPathName); } @Override default boolean isGroupFromAttributes(final String normalCacheKey, final JsonElement attributes) { return true; } @Override default boolean datasetExists(final String pathName) throws N5IOException { final String normalPathName = N5URI.normalizeGroupPath(pathName); if (cacheMeta()) { return getCache().isDataset(normalPathName, getAttributesKey()); } return isDatasetFromContainer(normalPathName); } @Override default boolean isDatasetFromContainer(final String normalPathName) throws N5IOException { return normalGetDatasetAttributes(normalPathName) != null; } @Override default boolean isDatasetFromAttributes(final String normalCacheKey, final JsonElement attributes) { return isGroupFromAttributes(normalCacheKey, attributes) && createDatasetAttributes(attributes) != null; } /** * Reads or creates the attributes map of a group or dataset. * * @param pathName * group path * @return the attribute * @throws N5IOException if an IO error occurs while reading the attribute */ @Override default JsonElement getAttributes(final String pathName) throws N5IOException { final String groupPath = N5URI.normalizeGroupPath(pathName); /* If cached, return the cache */ if (cacheMeta()) { return getCache().getAttributes(groupPath, getAttributesKey()); } else { return GsonKeyValueN5Reader.super.getAttributes(groupPath); } } @Override default String[] list(final String pathName) throws N5IOException { final String normalPath = N5URI.normalizeGroupPath(pathName); if (cacheMeta()) { return getCache().list(normalPath); } else { return GsonKeyValueN5Reader.super.list(normalPath); } } @Override default String[] listFromContainer(final String normalPathName) { // this implementation doesn't use cache key, but rather depends on return GsonKeyValueN5Reader.super.list(normalPathName); } /** * Check for attributes that are required for a group to be a dataset. * * @param attributes * to check for dataset attributes * @return if {@link DatasetAttributes#DIMENSIONS_KEY} and * {@link DatasetAttributes#DATA_TYPE_KEY} are present */ static boolean hasDatasetAttributes(final JsonElement attributes) { if (attributes == null || !attributes.isJsonObject()) { return false; } final JsonObject metadataCache = attributes.getAsJsonObject(); return metadataCache.has(DatasetAttributes.DIMENSIONS_KEY) && metadataCache.has(DatasetAttributes.DATA_TYPE_KEY); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/CachedGsonKeyValueN5Writer.java ================================================ package org.janelia.saalfeldlab.n5; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import com.google.gson.Gson; import com.google.gson.JsonElement; /** * Cached default implementation of {@link N5Writer} with JSON attributes parsed * with {@link Gson}. */ public interface CachedGsonKeyValueN5Writer extends CachedGsonKeyValueN5Reader, GsonKeyValueN5Writer { @Override default void setVersion(final String path) throws N5Exception { final Version version = getVersion(); if (!VERSION.isCompatible(version)) throw new N5IOException("Incompatible version " + version + " (this is " + VERSION + ")."); if (!VERSION.equals(version)) setAttribute("/", VERSION_KEY, VERSION.toString());; } @Override default void createGroup(final String path) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(path); // avoid hitting the backend if this path is already a group according to the cache // else if exists is true (then a dataset is present) so throw an exception to avoid // overwriting / invalidating existing data if (groupExists(normalPath)) return; else if (datasetExists(normalPath)) throw new N5Exception("Can't make a group on existing dataset."); getKeyValueAccess().createDirectories(absoluteGroupPath(normalPath)); if (cacheMeta()) { // check all nodes that are parents of the added node, if they have // a children set, add the new child to it getKeyValueAccess().parent(normalPath); String[] pathParts = getKeyValueAccess().components(normalPath); String parent = N5URI.normalizeGroupPath("/"); if (pathParts.length == 0) { pathParts = new String[]{""}; } for (final String child : pathParts) { final String childPath = parent.isEmpty() ? child : parent + "/" + child; getCache().initializeNonemptyCache(childPath, getAttributesKey()); getCache().updateCacheInfo(childPath, getAttributesKey()); // only add if the parent exists and has children cached already if (parent != null && !child.isEmpty()) getCache().addChildIfPresent(parent, child); parent = childPath; } } } @Override default void writeAttributes( final String normalGroupPath, final JsonElement attributes) throws N5Exception { writeAndCacheAttributes(normalGroupPath, attributes); } default void writeAndCacheAttributes( final String normalGroupPath, final JsonElement attributes) throws N5Exception { GsonKeyValueN5Writer.super.writeAttributes(normalGroupPath, attributes); if (cacheMeta()) { JsonElement nullRespectingAttributes = attributes; /* * Gson only filters out nulls when you write the JsonElement. This * means it doesn't filter them out when caching. * To handle this, we explicitly writer the existing JsonElement to * a new JsonElement. * The output is identical to the input if: * - serializeNulls is true * - no null values are present * - caching is turned off */ if (!getGson().serializeNulls()) { nullRespectingAttributes = getGson().toJsonTree(attributes); } /* Update the cache, and write to the writer */ getCache().updateCacheInfo(normalGroupPath, getAttributesKey(), nullRespectingAttributes); } } @Override default boolean remove(final String path) throws N5Exception { // GsonKeyValueN5Writer.super.remove(path) /* * the lines below duplicate the single line above but would have to call * normalizeGroupPath again the below duplicates code, but avoids extra work */ final String normalPath = N5URI.normalizeGroupPath(path); final String groupPath = absoluteGroupPath(normalPath); if (getKeyValueAccess().isDirectory(groupPath)) getKeyValueAccess().delete(groupPath); if (cacheMeta()) { final String parentPath = getKeyValueAccess().parent(normalPath); getCache().removeCache(parentPath, normalPath); } /* an IOException should have occurred if anything had failed midway */ return true; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/ChannelLock.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.Closeable; import java.io.IOException; import java.io.UncheckedIOException; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; /** * Holds a channel and system-level file lock (shared for writing, non-shared * for reading) and keeps it open until this {@code ChannelLock} is {@link * #close() closed}. */ class ChannelLock implements Closeable { private final FileChannel channel; /** * Hold a hard reference to the {@code FileLock} to make sure it is not * prematurely released. *

* NB: We do not call {@code lock.release()} in {@link #close}, because at * this point the channel might be already closed (by an external writer). * {@code lock.release()} will throw an exception if the channel is already * closed. Instead, we just close the channel which will automatically * release the lock. */ @SuppressWarnings({"unused", "FieldCanBeLocal"}) private final FileLock lock; private ChannelLock(final FileChannel channel, final FileLock lock) { this.channel = channel; this.lock = lock; } public void close() throws IOException { // NB: We do not call lock.release() here, because it may throw an // exception if the channel is already closed. Instead, we just close // the channel. This will automatically release the lock. (And it is ok // to close an already closed channel.) channel.close(); } FileChannel getChannel() { return channel; } /** * Create a {@link FileChannel} on the given {@code path} and lock it with a * system-level {@link FileLock}. If there is an existing overlapping file * lock, this method will block until the existing lock is released and the * channel could be locked (by us). *

* The {@code FileLock} is exclusive if the {@code path} is locked {@code * forWriting}, and shared otherwise. *

* If the {@code path} is locked {@code forWriting} non-existing file and * the parent directories are created as needed. * * @throws IOException if an error occurs while opening the channel, or if * the calling thread is interrupted while waiting for the {@code FileLock}. */ static ChannelLock lock(final Path path, final boolean forWriting, final LockingPolicy policy) throws IOException { final FileChannel channel = openFileChannel(path, forWriting); if (policy == LockingPolicy.UNSAFE) { return new ChannelLock(channel, null); } try { while (true) { try { final FileLock lock = channel.lock(0, Long.MAX_VALUE, !forWriting); return new ChannelLock(channel, lock); } catch (final OverlappingFileLockException e) { try { Thread.sleep(100); } catch (final InterruptedException ie) { Thread.currentThread().interrupt(); throw new IOException("Interrupted while waiting for file lock", ie); } } } } catch (Exception e) { if (policy == LockingPolicy.STRICT) { closeQuietly(channel); throw e; } else { return new ChannelLock(channel, null); } } } /** * Opens a file channel. If the channel is opened {@code forWriting}, * then this may create the file and the parent directories as needed. * * @throws IOException * if the channel cannot be opened */ private static FileChannel openFileChannel(final Path path, final boolean forWriting) throws IOException { if (forWriting) { final Path parent = path.getParent(); if (parent != null) { Files.createDirectories(parent); } return FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); } else { return FileChannel.open(path, StandardOpenOption.READ); } } private static void closeQuietly(final FileChannel fileChannel) { if (fileChannel != null) { try { fileChannel.close(); } catch (final IOException | UncheckedIOException ignored) { } } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/Compression.java ================================================ package org.janelia.saalfeldlab.n5; 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; import org.janelia.saalfeldlab.n5.codec.DataCodec; import org.janelia.saalfeldlab.n5.codec.CodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.scijava.annotations.Indexable; /** * This interface is used to indicate that a {@link DataCodec} can be * serialized as a "compression" for the N5 format (using the N5 API). *

* N5Readers and N5Writers for the N5 format can declare DataCodecs that * implement this interface so that the {@link CompressionAdapter} is used for * serialization. *

* See also: an alternative method for serializing general {@link CodecInfo}s is * with the {@link NameConfigAdapter}. This interface remains for legacy * (de)serialization. * * @author Stephan Saalfeld */ public interface Compression extends Serializable, DataCodec, DataCodecInfo { /** * Annotation for runtime discovery of compression schemes. * */ @Retention(RetentionPolicy.RUNTIME) @Inherited @Target(ElementType.TYPE) @Indexable @interface CompressionType { String value(); } /** * Annotation for runtime discovery of compression schemes. * */ @Retention(RetentionPolicy.RUNTIME) @Inherited @Target(ElementType.FIELD) @interface CompressionParameter {} default String getType() { final CompressionType compressionType = getClass().getAnnotation(CompressionType.class); if (compressionType == null) return null; else return compressionType.value(); } @Override default DataCodec create() { return this; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/CompressionAdapter.java ================================================ package org.janelia.saalfeldlab.n5; 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; import org.janelia.saalfeldlab.n5.Compression.CompressionParameter; import org.janelia.saalfeldlab.n5.Compression.CompressionType; import org.scijava.annotations.Index; import org.scijava.annotations.IndexItem; 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; /** * Compression adapter, auto-discovers annotated compression implementations * in the classpath. * * @author Stephan Saalfeld */ public class CompressionAdapter implements JsonDeserializer, JsonSerializer { private static CompressionAdapter instance = null; private final HashMap> compressionConstructors = new HashMap<>(); private final HashMap>> compressionParameters = 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 boolean override) { if (override || instance == null) { final CompressionAdapter newInstance = new CompressionAdapter(); final ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); final Index annotationIndex = Index.load(CompressionType.class, classLoader); for (final IndexItem item : annotationIndex) { Class clazz; try { clazz = (Class)Class.forName(item.className()); final String type = clazz.getAnnotation(CompressionType.class).value(); final Constructor constructor = clazz.getDeclaredConstructor(); final HashMap> parameters = new HashMap<>(); final ArrayList fields = getDeclaredFields(clazz); for (final Field field : fields) { if (field.getAnnotation(CompressionParameter.class) != null) { parameters.put(field.getName(), field.getType()); } } newInstance.compressionConstructors.put(type, constructor); newInstance.compressionParameters.put(type, parameters); } catch (final NoClassDefFoundError | ClassNotFoundException | NoSuchMethodException | ClassCastException | UnsatisfiedLinkError e) { System.err.println("Compression '" + item.className() + "' could not be registered"); } } instance = newInstance; } } public static void update() { update(false); } @Override public JsonElement serialize( final Compression compression, final Type typeOfSrc, final JsonSerializationContext context) { final String type = compression.getType(); final Class clazz = compression.getClass(); final JsonObject json = new JsonObject(); json.addProperty("type", type); final HashMap> parameterTypes = compressionParameters.get(type); try { for (final Entry> parameterType : parameterTypes.entrySet()) { final String name = parameterType.getKey(); final Field field = clazz.getDeclaredField(name); final boolean isAccessible = field.isAccessible(); field.setAccessible(true); final Object value = field.get(compression); field.setAccessible(isAccessible); json.add(parameterType.getKey(), context.serialize(value)); } } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { e.printStackTrace(System.err); return null; } return json; } @Override public Compression deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { final JsonObject jsonObject = json.getAsJsonObject(); final JsonElement jsonType = jsonObject.get("type"); if (jsonType == null) return null; final String type = jsonType.getAsString(); final Constructor constructor = compressionConstructors.get(type); final Compression compression; try { compression = constructor.newInstance(); final HashMap> parameterTypes = compressionParameters.get(type); for (final Entry> parameterType : parameterTypes.entrySet()) { final String name = parameterType.getKey(); if (jsonObject.has(name)) { final Object parameter = context.deserialize(jsonObject.get(name), parameterType.getValue()); ReflectionUtils.setFieldValue(compression, name, parameter); } } } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | SecurityException | NoSuchFieldException e) { e.printStackTrace(System.err); return null; } return compression; } public static CompressionAdapter getJsonAdapter() { if (instance == null) update(); return instance; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/DataBlock.java ================================================ package org.janelia.saalfeldlab.n5; /** * Interface for data blocks. A data block has data, a position on the block * grid, and a size. * * @param type of the data contained in the DataBlock * * @author Stephan Saalfeld */ public interface DataBlock { /** * Returns the size of this data block. *

* The size of a data block is expected to be smaller than or equal to the * spacing of the block grid. The dimensionality of size is expected to be * equal to the dimensionality of the dataset. Consistency is not enforced. * * @return size of the data block */ int[] getSize(); /** * Returns the position of this data block on the block grid relative to dataset. *

* 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 block grid */ long[] getGridPosition(); /** * Returns the data object held by this data block. * * @return data object */ T getData(); /** * Returns the number of elements in this {@link DataBlock}. This number is * not necessarily equal {@link #getNumElements(int[]) * getNumElements(getSize())}. * * @return the number of elements */ int getNumElements(); /** * Returns the number of elements in a box of given size. * * @param size * the size * @return the number of elements */ static int getNumElements(final int[] size) { int n = size[0]; for (int i = 1; i < size.length; ++i) n *= size[i]; return n; } /** * Factory for creating {@code DataBlock}. * * @param * type of the data contained in the DataBlock */ interface DataBlockFactory { /** * Create a new {@link DataBlock} with the given {@code blockSize}, {@code gridPosition}, and {@code data} content. * * @param blockSize * the block size * @param gridPosition * the grid position * @param data * the data object * * @return a new DataBlock */ DataBlock createDataBlock(int[] blockSize, long[] gridPosition, T data); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/DataType.java ================================================ package org.janelia.saalfeldlab.n5; import java.lang.reflect.Type; 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; /** * Enumerates available data types. * * @author Stephan Saalfeld */ public enum DataType { UINT8( "uint8", (blockSize, gridPosition, numElements) -> new ByteArrayDataBlock( blockSize, gridPosition, new byte[numElements])), UINT16( "uint16", (blockSize, gridPosition, numElements) -> new ShortArrayDataBlock( blockSize, gridPosition, new short[numElements])), UINT32( "uint32", (blockSize, gridPosition, numElements) -> new IntArrayDataBlock( blockSize, gridPosition, new int[numElements])), UINT64( "uint64", (blockSize, gridPosition, numElements) -> new LongArrayDataBlock( blockSize, gridPosition, new long[numElements])), INT8( "int8", (blockSize, gridPosition, numElements) -> new ByteArrayDataBlock( blockSize, gridPosition, new byte[numElements])), INT16( "int16", (blockSize, gridPosition, numElements) -> new ShortArrayDataBlock( blockSize, gridPosition, new short[numElements])), INT32( "int32", (blockSize, gridPosition, numElements) -> new IntArrayDataBlock( blockSize, gridPosition, new int[numElements])), INT64( "int64", (blockSize, gridPosition, numElements) -> new LongArrayDataBlock( blockSize, gridPosition, new long[numElements])), FLOAT32( "float32", (blockSize, gridPosition, numElements) -> new FloatArrayDataBlock( blockSize, gridPosition, new float[numElements])), FLOAT64( "float64", (blockSize, gridPosition, numElements) -> new DoubleArrayDataBlock( blockSize, gridPosition, new double[numElements])), STRING( "string", (blockSize, gridPosition, numElements) -> new StringDataBlock( blockSize, gridPosition, new String[numElements])), OBJECT( "object", (blockSize, gridPosition, numElements) -> new ByteArrayDataBlock( blockSize, gridPosition, new byte[numElements])); private final String label; private final DataBlockFactory dataBlockFactory; DataType(final String label, final DataBlockFactory dataBlockFactory) { this.label = label; this.dataBlockFactory = dataBlockFactory; } @Override public String toString() { return label; } public static DataType fromString(final String string) { for (final DataType value : values()) if (value.toString().equals(string)) return value; return null; } /** * Factory for {@link DataBlock DataBlocks}. * * @param blockSize * the block size * @param gridPosition * the grid position * @param numElements * the number of elements (not necessarily one element per block * element) * @return the data block */ public DataBlock createDataBlock(final int[] blockSize, final long[] gridPosition, final int numElements) { return dataBlockFactory.createDataBlock(blockSize, gridPosition, numElements); } /** * Factory for {@link DataBlock DataBlocks} with one data element for each * block element (e.g. pixel image). * * @param blockSize * the block size * @param gridPosition * the grid position * @return the data block */ public DataBlock createDataBlock(final int[] blockSize, final long[] gridPosition) { return dataBlockFactory.createDataBlock(blockSize, gridPosition, DataBlock.getNumElements(blockSize)); } private interface DataBlockFactory { DataBlock createDataBlock(final int[] blockSize, final long[] gridPosition, final int numElements); } static public class JsonAdapter implements JsonDeserializer, JsonSerializer { @Override public DataType deserialize( final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { return DataType.fromString(json.getAsString()); } @Override public JsonElement serialize( final DataType src, final Type typeOfSrc, final JsonSerializationContext context) { return new JsonPrimitive(src.toString()); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/DatasetAttributes.java ================================================ package org.janelia.saalfeldlab.n5; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonNull; 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.codec.BlockCodec; import org.janelia.saalfeldlab.n5.codec.BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.CodecInfo; import org.janelia.saalfeldlab.n5.codec.N5BlockCodecInfo; import org.janelia.saalfeldlab.n5.shard.DatasetAccess; import org.janelia.saalfeldlab.n5.shard.DefaultDatasetAccess; import org.janelia.saalfeldlab.n5.shard.ShardCodecInfo; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedGrid; import java.io.Serializable; import java.lang.reflect.Type; import java.util.Arrays; import java.util.HashMap; import java.util.stream.Collectors; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.DatasetCodec; import org.janelia.saalfeldlab.n5.codec.DatasetCodecInfo; /** * Mandatory dataset attributes: * *

    *
  1. long[] : dimensions
  2. *
  3. int[] : blockSize
  4. *
  5. {@link DataType} : dataType
  6. *
  7. {@link CodecInfo}... : encode/decode routines
  8. *
* * @author Stephan Saalfeld */ public class DatasetAttributes implements 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"; private final long[] dimensions; // number of samples per chunk per dimension private final int[] chunkSize; // number of samples per block per dimension // identical to chunkSize for non-sharded datasets private final int[] blockSize; private final DataType dataType; private final JsonElement defaultValue; private final BlockCodecInfo blockCodecInfo; private final DataCodecInfo[] dataCodecInfos; private final DatasetCodecInfo[] datasetCodecInfos; private transient final DatasetAccess access; public DatasetAttributes( final long[] dimensions, final int[] blockSize, final DataType dataType, final JsonElement defaultValue, final BlockCodecInfo blockCodecInfo, final DatasetCodecInfo[] datasetCodecInfos, final DataCodecInfo... dataCodecInfos) { this.dimensions = dimensions; this.dataType = dataType; this.blockSize = blockSize; this.defaultValue = defaultValue == null ? JsonNull.INSTANCE : defaultValue; this.blockCodecInfo = blockCodecInfo == null ? defaultBlockCodecInfo() : blockCodecInfo; this.datasetCodecInfos = datasetCodecInfos; if (dataCodecInfos == null) this.dataCodecInfos = new DataCodecInfo[0]; else this.dataCodecInfos = Arrays.stream(dataCodecInfos) .filter(it -> it != null && !(it instanceof RawCompression)) .toArray(DataCodecInfo[]::new); access = createDatasetAccess(); chunkSize = access.getGrid().getBlockSize(0); } public DatasetAttributes( final long[] dimensions, final int[] outerBlockSize, final DataType dataType, final BlockCodecInfo blockCodecInfo, final DatasetCodecInfo[] datasetCodecInfos, final DataCodecInfo... dataCodecInfos) { this(dimensions, outerBlockSize, dataType, JsonNull.INSTANCE, blockCodecInfo, datasetCodecInfos, dataCodecInfos); } public DatasetAttributes( final long[] dimensions, final int[] outerBlockSize, final DataType dataType, final BlockCodecInfo blockCodecInfo, final DataCodecInfo... dataCodecInfos) { this(dimensions, outerBlockSize, dataType, blockCodecInfo, null, dataCodecInfos); } /** * Constructs a DatasetAttributes instance with specified dimensions, block size, data type, * and single compressor with default codec. * * @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 dataCodecInfos the codecs used encode/decode the data */ public DatasetAttributes( final long[] dimensions, final int[] blockSize, final DataType dataType, final DataCodecInfo... dataCodecInfos) { this(dimensions, blockSize, dataType, null, dataCodecInfos); } /** * Constructs a DatasetAttributes instance with specified dimensions, block size, data type, and default 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 */ public DatasetAttributes( final long[] dimensions, final int[] blockSize, final DataType dataType) { this(dimensions, blockSize, dataType, new DataCodecInfo[0]); } private DatasetAccess createDatasetAccess() { final int m = nestingDepth(blockCodecInfo); // There are m codecs: 1 DataBlock codecs, and m-1 shard codecs. // The inner-most codec (the DataBlock codec) is at index 0. final int[][] blockSizes = new int[m][]; // NestedGrid validates block sizes, so instantiate it before creating the blockCodecs // blockCodecInfo.create below could fail unexpecedly with invalid // blockSizes so validate first blockSizes[m - 1] = blockSize; BlockCodecInfo tmpInfo = blockCodecInfo; for (int l = m - 1; l > 0; --l) { final ShardCodecInfo info = (ShardCodecInfo)tmpInfo; blockSizes[l - 1] = info.getInnerBlockSize(); tmpInfo = info.getInnerBlockCodecInfo(); } BlockCodecInfo currentBlockCodecInfo = blockCodecInfo; DataCodecInfo[] currentDataCodecInfos = dataCodecInfos; DatasetCodecInfo[] datasetCodecInfos = this.datasetCodecInfos; final NestedGrid grid = new NestedGrid(blockSizes, dimensions); final BlockCodec[] blockCodecs = new BlockCodec[m]; for (int l = m - 1; l >= 0; --l) { blockCodecs[l] = currentBlockCodecInfo.create(dataType, blockSizes[l], currentDataCodecInfos); if (l > 0) { final ShardCodecInfo info = (ShardCodecInfo) currentBlockCodecInfo; currentBlockCodecInfo = info.getInnerBlockCodecInfo(); currentDataCodecInfos = info.getInnerDataCodecInfos(); if (info.getInnerDataCodecInfos() != null) { if (datasetCodecInfos != null && datasetCodecInfos.length > 0) { throw new N5Exception.N5JsonParseException("Found DatasetCodecs both inside and outside of shards. Not handled"); } else datasetCodecInfos = info.getInnerDatasetCodecInfos(); } } } // add dataset codecs blockCodecs[0] = blockCodecWithDatasetCodecs(this, blockCodecs[0], datasetCodecInfos); return new DefaultDatasetAccess<>(grid, blockCodecs); } @SuppressWarnings("unchecked") private static BlockCodec blockCodecWithDatasetCodecs(final DatasetAttributes attributes, final BlockCodec blockCodec, final DatasetCodecInfo[] datasetCodecInfos) { BlockCodec result = blockCodec; if (datasetCodecInfos != null) { for (final DatasetCodecInfo info : datasetCodecInfos) { result = DatasetCodec.concatenate(info.create(attributes), (BlockCodec)result); } } return result; } private static int nestingDepth(BlockCodecInfo info) { if (info instanceof ShardCodecInfo) { return 1 + nestingDepth(((ShardCodecInfo)info).getInnerBlockCodecInfo()); } else { return 1; } } protected BlockCodecInfo defaultBlockCodecInfo() { return new N5BlockCodecInfo(); } public long[] getDimensions() { return dimensions; } public int getNumDimensions() { return dimensions.length; } public int[] getChunkSize() { return chunkSize; } public int[] getBlockSize() { return blockSize; } public JsonElement getDefaultValue() { return defaultValue; } public boolean isSharded() { return blockCodecInfo instanceof ShardCodecInfo; } /** * Only used for deserialization for N5 backwards compatibility. * {@link Compression} is no longer a special case. Prefer to reference {@link #getDataCodecInfos()} * Will return {@link RawCompression} if no compression is otherwise provided, for legacy compatibility. *

* Deprecated in favor of {@link #getDataCodecInfos()}. * * @return compression CodecInfo, if one was present, or else RawCompression */ @Deprecated public Compression getCompression() { return Arrays.stream(dataCodecInfos) .filter(it -> it instanceof Compression) .map(it -> (Compression)it) .findFirst() .orElse(new RawCompression()); } public DataType getDataType() { return dataType; } /** * Get the {@link DatasetAccess} for this dataset. * * @return the {@code DatasetAccess} for this dataset */ protected DatasetAccess getDatasetAccess() { return (DatasetAccess) access; } /** * Returns the {@code NestedGrid} for this dataset, from which block and * shard sizes are accessible. * * @return the NestedGrid */ public NestedGrid getNestedBlockGrid() { return getDatasetAccess().getGrid(); } public BlockCodecInfo getBlockCodecInfo() { return blockCodecInfo; } public DataCodecInfo[] getDataCodecInfos() { return dataCodecInfos; } public DatasetCodecInfo[] getDatasetCodecInfos() { return datasetCodecInfos; } public String relativeBlockPath(long... position) { return Arrays.stream(position).mapToObj(Long::toString).collect(Collectors.joining("/")); } public HashMap asMap() { final HashMap map = new HashMap<>(); map.put(DIMENSIONS_KEY, dimensions); map.put(BLOCK_SIZE_KEY, chunkSize); map.put(DATA_TYPE_KEY, dataType); map.put(COMPRESSION_KEY, getCompression()); return map; } public static Builder builder(final long[] dimensions, final DataType dataType) { return new Builder(dimensions, dataType); } public static Builder builder(final DatasetAttributes attributes) { return new Builder(attributes); } private static final int[] DEFAULT_1D_BLOCK_SIZE = new int[]{65536}; private static final int[] DEFAULT_2D_BLOCK_SIZE = new int[]{512,512}; private static final int[] DEFAULT_3D_BLOCK_SIZE = new int[]{128,128,128}; private static final int DEFAULT_ND_DIM_LEN = 64; protected static int[] defaultBlockSize(final long[] dimensions) { final int[] blockSize; if (dimensions.length == 1) blockSize = DEFAULT_1D_BLOCK_SIZE.clone(); else if (dimensions.length == 2) blockSize = DEFAULT_2D_BLOCK_SIZE.clone(); else if (dimensions.length == 3) blockSize = DEFAULT_3D_BLOCK_SIZE.clone(); else { blockSize = new int[dimensions.length]; Arrays.fill(blockSize, DEFAULT_ND_DIM_LEN); } for (int i = 0; i < blockSize.length; i++) blockSize[i] = (int)Math.min(blockSize[i], dimensions[i]); return blockSize; } public static class Builder { private final long[] dimensions; private final DataType dataType; private int[] blockSize; private DataCodecInfo[] dataCodecInfos = new DataCodecInfo[0]; public Builder(final long[] dimensions, final DataType dataType) { this.dimensions = dimensions.clone(); this.dataType = dataType; this.blockSize = defaultBlockSize(dimensions); } public Builder(final DatasetAttributes attributes) { this.dimensions = attributes.getDimensions(); this.dataType = attributes.getDataType(); this.blockSize = attributes.getBlockSize(); this.dataCodecInfos = attributes.getDataCodecInfos(); } public Builder blockSize(final int[] blockSize) { this.blockSize = blockSize.clone(); return this; } /** * Sets the compression codec. Has no effect if {@code compression} is * null or {@link RawCompression}. * * @param compression the compression to use * @return this builder */ public Builder compression(final Compression compression) { if (compression != null && !(compression instanceof RawCompression)) this.dataCodecInfos = new DataCodecInfo[]{compression}; return this; } public DatasetAttributes build() { final int[] resolvedBlockSize = blockSize != null ? blockSize : defaultBlockSize(dimensions); return new DatasetAttributes(dimensions, resolvedBlockSize, dataType, new N5BlockCodecInfo(), null, dataCodecInfos); } } private static DatasetAttributesAdapter adapter = null; public static DatasetAttributesAdapter getJsonAdapter() { if (adapter == null) { adapter = new DatasetAttributesAdapter(); } return adapter; } 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); final DataType dataType = context.deserialize(obj.get(DATA_TYPE_KEY), DataType.class); final BlockCodecInfo blockCodecInfo; final DataCodecInfo[] dataCodecs; if (obj.has(CODEC_KEY)) { final CodecInfo[] codecs = context.deserialize(obj.get(CODEC_KEY), CodecInfo[].class); blockCodecInfo = (BlockCodecInfo)codecs[0]; dataCodecs = new DataCodecInfo[codecs.length - 1]; for (int i = 1; i < codecs.length; i++) { dataCodecs[i - 1] = (DataCodecInfo)codecs[i]; } } else if (obj.has(COMPRESSION_KEY)) { final Compression compression = CompressionAdapter.getJsonAdapter().deserialize(obj.get(COMPRESSION_KEY), Compression.class, context); dataCodecs = new DataCodecInfo[]{compression}; blockCodecInfo = new N5BlockCodecInfo(); } else if (obj.has(compressionTypeKey)) { final Compression compression = getCompressionVersion0(obj.get(compressionTypeKey).getAsString()); dataCodecs = new DataCodecInfo[]{compression}; blockCodecInfo = new N5BlockCodecInfo(); } else { return null; } return new DatasetAttributes(dimensions, blockSize, dataType, blockCodecInfo, dataCodecs); } //FIXME // this implements multi-codec serialization for N5. We probably don't want this now @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.chunkSize)); obj.add(DATA_TYPE_KEY, context.serialize(src.dataType)); final DataCodecInfo[] codecs = src.dataCodecInfos; // length > 1 is actually invalid, but this is checked on construction if (codecs.length == 0) obj.add(COMPRESSION_KEY, context.serialize(new RawCompression())); else obj.add(COMPRESSION_KEY, context.serialize(codecs[0])); return obj; } private static Compression getCompressionVersion0(final String compressionVersion0Name) { switch (compressionVersion0Name) { case "raw": return new RawCompression(); case "gzip": return new GzipCompression(); case "bzip2": return new Bzip2Compression(); case "lz4": return new Lz4Compression(); case "xz": return new XzCompression(); } return null; } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/DoubleArrayDataBlock.java ================================================ package org.janelia.saalfeldlab.n5; public class DoubleArrayDataBlock extends AbstractDataBlock { public DoubleArrayDataBlock(final int[] size, final long[] gridPosition, final double[] data) { super(size, gridPosition, data, a -> a.length); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/FileKeyLockManager.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.IOException; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.nio.channels.FileLock; import java.nio.file.Path; import java.util.Collections; import java.util.EnumMap; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import static org.janelia.saalfeldlab.n5.LockingPolicy.STRICT; /** * Provides thread-safe and process-safe read/write locking for filesystem paths. * Uses thread locks for JVM coordination and file locks for inter-process coordination. */ class FileKeyLockManager { private static final Map managers = Collections.synchronizedMap(new EnumMap<>(LockingPolicy.class)); static FileKeyLockManager forPolicy(final LockingPolicy policy) { return managers.computeIfAbsent(policy, FileKeyLockManager::new); } /** * @deprecated use {@link FileKeyLockManager#forPolicy(LockingPolicy)} */ @Deprecated static final FileKeyLockManager FILE_LOCK_MANAGER = forPolicy(STRICT); private final LockingPolicy policy; /** * Create a new {@link FileKeyLockManager} with the specified locking policy. *

* The given locking {@link LockingPolicy policy} applies to OS-level locking. * For both the {@code STRICT} and {@code PERMISSIVE} policy, a {@link * FileLock} is obtained. If this fails, {@code STRICT} will throw an {@code * IOException}. {@code PERMISSIVE} will proceed without locking. {@code * UNSAFE} will not attempt OS-level locking, however will still manage * mutual exclusion of readers and writers in the same JVM. Trying to lock * the same path with different locking policies will throw an {@code * IOException}. * * @param policy * the locking policy */ private FileKeyLockManager(final LockingPolicy policy) { this.policy = policy; } private final ConcurrentHashMap locks = new ConcurrentHashMap<>(); private final ReferenceQueue refQueue = new ReferenceQueue<>(); private static class WeakValue extends WeakReference { final String key; WeakValue( final String key, final KeyLockState value, final ReferenceQueue queue) { super(value, queue); this.key = key; } } /** * Remove entries from the cache whose references have been * garbage-collected. */ private void cleanUp() { while (true) { final WeakValue ref = (WeakValue) refQueue.poll(); if (ref == null) break; locks.remove(ref.key, ref); } } private KeyLockState keyLockState(final Path path, final LockingPolicy policy) throws IOException { final String key = path.toAbsolutePath().toString(); cleanUp(); final WeakValue existingRef = locks.get(key); KeyLockState state = existingRef == null ? null : existingRef.get(); if (state != null) { return state; } final KeyLockState newState = new KeyLockState(path, policy); while (state == null) { final WeakValue ref = locks.compute(key, (k, v) -> (v != null && v.get() != null) ? v : new WeakValue(k, newState, refQueue)); state = ref.get(); } return state; } /** * Acquires a read lock for the specified key. Multiple threads can hold * read locks for the same key simultaneously. *

* The first reader will acquire a shared file lock. Subsequent readers * only acquire the thread-level lock. * * @param path * the key (file path) to lock for reading * * @return a {@link LockedChannel} that must be closed when done * * @throws IOException * if acquiring the file lock fails */ public LockedFileChannel lockForReading(final Path path) throws IOException { return keyLockState(path, policy).acquireRead(); } /** * Acquires a write lock for the specified key. Only one thread can hold a * write lock for a key at a time, and no readers can hold locks. * * @param path * the file path to lock for writing * * @return a {@link LockedChannel} that must be closed when done * * @throws IOException * if acquiring the file lock fails */ public LockedFileChannel lockForWriting(final Path path) throws IOException { return keyLockState(path, policy).acquireWrite(); } /** * Returns the number of keys currently being tracked. * * @return the number of keys with associated locks */ int size() { return locks.size(); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/FileSystemKeyValueAccess.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.file.DirectoryNotEmptyException; import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileSystemException; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.attribute.FileAttribute; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.stream.Stream; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.N5Exception.N5NoSuchKeyException; import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; /** * Filesystem {@link KeyValueAccess}. * * @author Stephan Saalfeld * @author Igor Pisarev * @author Philipp Hanslovsky */ public class FileSystemKeyValueAccess implements KeyValueAccess { private final FileKeyLockManager fileKeyLockManager; public FileSystemKeyValueAccess() { final LockingPolicy policy = LockingPolicy.fromString(System.getProperty("n5.ioPolicy", "permissive")); this.fileKeyLockManager = FileKeyLockManager.forPolicy(policy); } private LockedFileChannel lockForReading(final Path path) throws N5IOException { try { return fileKeyLockManager.lockForReading(path); } catch (final NoSuchFileException e) { throw new N5NoSuchKeyException("No such file", e); } catch (IOException | UncheckedIOException e) { throw new N5IOException("Failed to lock file for reading: " + path, e); } } private LockedFileChannel lockForWriting(final Path path) throws N5IOException { try { return fileKeyLockManager.lockForWriting(path); } catch (final NoSuchFileException e) { throw new N5NoSuchKeyException("No such file", e); } catch (IOException | UncheckedIOException e) { throw new N5IOException("Failed to lock file for writing: " + path, e); } } @Override public VolatileReadData createReadData(final String normalPath) { return VolatileReadData.from(new FileLazyRead(Paths.get(normalPath))); } @Override public void write(final String normalPath, final ReadData data) throws N5IOException { final Path path = Paths.get(normalPath); try (final LockedFileChannel channel = lockForWriting(path)) { data.writeTo(channel.asOutputStream()); } catch (IOException e) { throw new N5IOException(e); } } @Override public boolean isDirectory(final String normalPath) { final Path path = Paths.get(normalPath); return Files.isDirectory(path); } @Override public boolean isFile(final String normalPath) { final Path path = Paths.get(normalPath); return Files.isRegularFile(path); } @Override public boolean exists(final String normalPath) { final Path path = Paths.get(normalPath); return Files.exists(path); } @Override public long size(final String normalPath) { return size(Paths.get(normalPath)); } private static long size(final Path path) { try { return Files.size(path); } catch (NoSuchFileException e) { throw new N5NoSuchKeyException("No such file", e); } catch (IOException | UncheckedIOException e) { throw new N5IOException(e); } } @Override public String[] listDirectories(final String normalPath) throws N5IOException { final Path path = Paths.get(normalPath); try (final Stream pathStream = Files.list(path)) { return pathStream .filter(Files::isDirectory) .map(a -> path.relativize(a).toString()) .toArray(String[]::new); } catch (NoSuchFileException e) { throw new N5NoSuchKeyException("No such file", e); } catch (IOException | UncheckedIOException e) { throw new N5IOException("Failed to list directories", e); } } @Override public String[] list(final String normalPath) throws N5IOException { final Path path = Paths.get(normalPath); try (final Stream pathStream = Files.list(path)) { return pathStream .map(a -> path.relativize(a).toString()) .toArray(String[]::new); } catch (NoSuchFileException e) { throw new N5NoSuchKeyException("No such file", e); } catch (IOException | UncheckedIOException e) { throw new N5IOException("Failed to list files", e); } } @Override public String[] components(final String path) { final Path fsPath = Paths.get(path); final Path root = fsPath.getRoot(); final String separator = fsPath.getFileSystem().getSeparator(); final String[] components; int o; if (root == null) { components = new String[fsPath.getNameCount()]; o = 0; } else { components = new String[fsPath.getNameCount() + 1]; components[0] = root.toString(); o = 1; } for (int i = o; i < components.length; ++i) { String name = fsPath.getName(i - o).toString(); /* Preserve trailing slash on final component if present*/ if (i == components.length - 1) { final String trailingSeparator = path.endsWith(separator) ? separator : path.endsWith("/") ? "/" : ""; name += trailingSeparator; } components[i] = name; } return components; } @Override public String parent(final String path) { final Path parent = Paths.get(path).getParent(); if (parent == null) return null; else return parent.toString(); } @Override public String relativize(final String path, final String base) { final Path basePath = Paths.get(base); return basePath.relativize(Paths.get(path)).toString(); } /** * Returns a normalized path. It ensures correctness on both Unix and * Windows, * otherwise {@code pathName} is treated as UNC path on Windows, and * {@code Paths.get(pathName, ...)} fails with {@code InvalidPathException}. * * @param path the path * @return the normalized path, without leading slash */ @Override public String normalize(final String path) { return Paths.get(path).normalize().toString(); } @Override public URI uri(final String normalPath) throws URISyntaxException { // normalize make absolute the scheme specific part only try { final URI normalUri = URI.create(normalPath); if (normalUri.isAbsolute()) return normalUri.normalize(); } catch (final IllegalArgumentException e) { return new File(normalPath).toURI().normalize(); } return new File(normalPath).toURI().normalize(); } @Override public String compose(final String... components) { if (components == null || components.length == 0) return null; if (components.length == 1) return Paths.get(components[0]).toString(); return Paths.get(components[0], Arrays.copyOfRange(components, 1, components.length)).normalize().toString(); } @Override public String compose(URI uri, String... components) { Path composedPath; if (uri.isAbsolute()) composedPath = Paths.get(uri); else composedPath = Paths.get(uri.toString()); for (String component : components) { if (component == null || component.isEmpty()) continue; composedPath = composedPath.resolve(component); } return composedPath.toAbsolutePath().toString(); } @Override public void createDirectories(final String normalPath) throws N5IOException { try { createDirectories(Paths.get(normalPath)); } catch (NoSuchFileException e) { throw new N5NoSuchKeyException("No such file", e); } catch (IOException | UncheckedIOException e) { throw new N5IOException("Failed to create directories", e); } } @Override public void delete(final String normalPath) throws N5IOException { try { final Path path = Paths.get(normalPath); if (Files.isRegularFile(path)) try (final LockedFileChannel channel = lockForWriting(path)) { Files.delete(path); } else { try (final Stream pathStream = Files.walk(path)) { for (final Iterator i = pathStream.sorted(Comparator.reverseOrder()).iterator(); i.hasNext();) { final Path childPath = i.next(); if (Files.isRegularFile(childPath)) try (final LockedFileChannel channel = lockForWriting(childPath)) { Files.delete(childPath); } else tryDelete(childPath); } } } } catch (NoSuchFileException ignore) { /* It doesn't exist; that's sufficient for us to not complain on a `delete` call */ } catch (IOException | UncheckedIOException e) { throw new N5IOException("Failed to delete file at " + normalPath, e); } } protected static void tryDelete(final Path path) throws IOException { try { Files.delete(path); } catch (final DirectoryNotEmptyException e) { /* * Even though path is expected to be an empty directory, sometimes * deletion fails on network filesystems when lock files are not * cleared immediately after the leaves have been removed. */ try { /* wait and reattempt */ Thread.sleep(100); Files.delete(path); } catch (final InterruptedException ex) { e.printStackTrace(); Thread.currentThread().interrupt(); } } } /** * This is a copy of {@link Files#createDirectories(Path, FileAttribute...)} * that follows symlinks. * * Workaround for https://bugs.openjdk.java.net/browse/JDK-8130464 * * Creates a directory by creating all nonexistent parent directories first. * Unlike the {@link Files#createDirectories} method, an exception * is not thrown if the directory could not be created because it already * exists. * *

* The {@code attrs} parameter is optional {@link FileAttribute * file-attributes} to set atomically when creating the nonexistent * directories. Each file attribute is identified by its {@link * FileAttribute#name name}. If more than one attribute of the same name is * included in the array then all but the last occurrence is ignored. * *

* If this method fails, then it may do so after creating some, but not * all, of the parent directories. * * @param dir * the directory to create * * @param attrs * an optional list of file attributes to set atomically when * creating the directory * * @return the directory * * @throws UnsupportedOperationException * if the array contains an attribute that cannot be set * atomically * when creating the directory * @throws FileAlreadyExistsException * if {@code dir} exists but is not a directory (optional * specific * exception) * @throws IOException * if an I/O error occurs * @throws SecurityException * in the case of the default provider, and a security manager * is * installed, the {@link SecurityManager#checkWrite(String) * checkWrite} * method is invoked prior to attempting to create a directory * and * its {@link SecurityManager#checkRead(String) checkRead} is * invoked for each parent directory that is checked. If {@code * dir} is not an absolute path then its {@link Path#toAbsolutePath * toAbsolutePath} may need to be invoked to get its absolute * path. * This may invoke the security manager's {@link * SecurityManager#checkPropertyAccess(String) * checkPropertyAccess} * method to check access to the system property * {@code user.dir} */ protected static Path createDirectories(Path dir, final FileAttribute... attrs) throws IOException { // attempt to create the directory try { createAndCheckIsDirectory(dir, attrs); return dir; } catch (final FileAlreadyExistsException x) { // file exists and is not a directory throw x; } catch (final IOException x) { // parent may not exist or other reason } SecurityException se = null; try { dir = dir.toAbsolutePath(); } catch (final SecurityException x) { // don't have permission to get absolute path se = x; } // find a descendant that exists Path parent = dir.getParent(); while (parent != null) { try { parent.getFileSystem().provider().checkAccess(parent); break; } catch (final NoSuchFileException x) { // does not exist } parent = parent.getParent(); } if (parent == null) { // unable to find existing parent if (se == null) { throw new FileSystemException( dir.toString(), null, "Unable to determine if root directory exists"); } else { throw se; } } // create directories Path child = parent; for (final Path name : parent.relativize(dir)) { child = child.resolve(name); createAndCheckIsDirectory(child, attrs); } return dir; } /** * This is a copy of a previous Files#createAndCheckIsDirectory(Path, * FileAttribute...) method that follows symlinks. * * Workaround for https://bugs.openjdk.java.net/browse/JDK-8130464 * * Used by createDirectories to attempt to create a directory. A no-op if the * directory already exists. * * @param dir directory path * @param attrs file attributes * @throws IOException the exception */ protected static void createAndCheckIsDirectory( final Path dir, final FileAttribute... attrs) throws IOException { try { Files.createDirectory(dir, attrs); } catch (final FileAlreadyExistsException x) { if (!Files.isDirectory(dir)) throw x; } } /** * Verify that the range {@code [offset, offset+length)} is fully contained in {@code [0, channelSize)}. * * @throws IndexOutOfBoundsException * if range is not fully contained */ private static void validBounds(final long channelSize, final long offset, final long length) throws IndexOutOfBoundsException { if (offset < 0) throw new IndexOutOfBoundsException("offset must be > 0, but was: " + offset); else if (channelSize > 0 && offset >= channelSize) // offset == 0 and channelSize == 0 is okay throw new IndexOutOfBoundsException("offset (" + offset + ") must be less than channel size (" + channelSize + ")"); else if (length >= 0 && offset + length > channelSize) throw new IndexOutOfBoundsException("offset + length (" + (offset + length) + ") must be less than channel size (" + channelSize + ")"); } private class FileLazyRead implements LazyRead { private final Path path; private LockedFileChannel lock; // TODO rename FileLazyRead(final Path path) { this.path = path; lock = lockForReading(path); } @Override public long size() throws N5IOException { if (lock == null) { throw new N5IOException("FileLazyRead is already closed."); } try { return Files.size(path); } catch (NoSuchFileException e) { throw new N5NoSuchKeyException("No such file", e); } catch (IOException | UncheckedIOException e) { throw new N5IOException(e); } } @Override public ReadData materialize(final long offset, final long length) { if (lock == null) { throw new N5IOException("FileLazyRead is already closed."); } try { final long channelSize = lock.size(); validBounds(channelSize, offset, length); final long size = length < 0 ? (channelSize - offset) : length; if (size > Integer.MAX_VALUE) { throw new IndexOutOfBoundsException("Attempt to materialize too large data"); } final byte[] data = new byte[(int) size]; lock.read(ByteBuffer.wrap(data), offset); return ReadData.from(data); } catch (IOException | UncheckedIOException e) { throw new N5Exception.N5IOException(e); } } @Override public void close() throws IOException { if (lock != null) { lock.close(); lock = null; } } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/FloatArrayDataBlock.java ================================================ package org.janelia.saalfeldlab.n5; public class FloatArrayDataBlock extends AbstractDataBlock { public FloatArrayDataBlock(final int[] size, final long[] gridPosition, final float[] data) { super(size, gridPosition, data, a -> a.length); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/FsIoPolicy.java ================================================ package org.janelia.saalfeldlab.n5; import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.file.*; import static org.janelia.saalfeldlab.n5.FileKeyLockManager.FILE_LOCK_MANAGER; public class FsIoPolicy { static final IoPolicy atomicWithFallback = IoPolicy.withFallback(new Atomic(), new Unsafe()); private static boolean validBounds(long channelSize, long offset, long length) { if (offset < 0) throw new N5Exception("offset must be > 0, but was: " + offset); else if (channelSize > 0 && offset >= channelSize) // offset == 0 and channelSize == 0 is okay throw new N5Exception("offset (" + offset + ") must be less than channel size (" + channelSize + ")"); else if (length >= 0 && offset + length > channelSize) throw new N5Exception("offset + length (" + (offset + length) + ") must be less than channel size (" + channelSize + ")"); return true; } /** * Opens a file channel. If the channel is opened {@code forWriting}, * then this may create the file and the parent directories as needed. * * @throws IOException * if the channel cannot be opened */ static FileChannel openFileChannel(final Path path, final boolean forWriting) throws IOException { if (forWriting) { final Path parent = path.getParent(); /* if not null and not directory, it will call `createDirectories` but we expect it to throw an IOException */ if (parent != null && !parent.toFile().isDirectory()) { Files.createDirectories(parent); } return FileChannel.open(path, StandardOpenOption.READ, StandardOpenOption.WRITE, StandardOpenOption.CREATE); } else { return FileChannel.open(path, StandardOpenOption.READ); } } /** * This method is necessary to handle the situtation where writing is successful, but `close` fails on the file channel. * This has been observed to happen fairly consistently on MacOS when writing to a file mounted over SMB. * * @param readData to write to the {@code Path} * @param path to write to * @throws IOException if writing failed. */ private static void writeToPathIgnoreCloseException(ReadData readData, Path path) throws IOException { FileChannel channel = openFileChannel(path, true); OutputStream os = Channels.newOutputStream(channel); try { readData.writeTo(os); os.flush(); channel.force(true); } catch (Throwable e) { os.close(); channel.close(); throw e; } /* if we get here, the write succeeded, and the os/channel may not be closed yet */ try { os.close(); channel.close(); } catch (IOException | UncheckedIOException ignore) { /* Ignore; we know the data was written already. */ } } public static class Unsafe implements IoPolicy { @Override public void write(String key, ReadData readData) throws IOException { final Path path = Paths.get(key); writeToPathIgnoreCloseException(readData, path); } @Override public VolatileReadData read(final String key) throws IOException { final Path path = Paths.get(key); FileLazyRead fileLazyRead = new FileLazyRead(path, false); return VolatileReadData.from(fileLazyRead); } @Override public void delete(final String key) throws IOException { final Path path = Paths.get(key); Files.deleteIfExists(path); } } public static class Atomic implements IoPolicy { @Override public void write(String key, ReadData readData) throws IOException { final Path path = Paths.get(key); try (LockedFileChannel channel = FILE_LOCK_MANAGER.lockForWriting(path)) { readData.writeTo(channel.asOutputStream()); } } @Override public VolatileReadData read(String key) throws IOException { final Path path = Paths.get(key); FileLazyRead fileLazyRead = new FileLazyRead(path, true); return VolatileReadData.from(fileLazyRead); } @Override public void delete(final String key) throws IOException { final Path path = Paths.get(key); if (!Files.isRegularFile(path)) Files.delete(path); try (LockedFileChannel ignore = FILE_LOCK_MANAGER.lockForWriting(path)) { Files.delete(path); } } } static class FileLazyRead implements LazyRead { private static final Closeable NO_OP = () -> { }; private final Path path; private Closeable lock; FileLazyRead(final Path path) throws IOException { this(path, true); } FileLazyRead(final Path path, final boolean requireLock ) throws IOException { this.path = path; if (requireLock) lock = FILE_LOCK_MANAGER.lockForReading(path); else lock = NO_OP; } @Override public long size() throws N5Exception.N5IOException { if (lock == null) { throw new N5Exception.N5IOException("FileLazyRead is already closed."); } try { return Files.size(path); } catch (NoSuchFileException e) { throw new N5Exception.N5NoSuchKeyException("No such file", e); } catch (IOException | UncheckedIOException e) { throw new N5Exception.N5IOException(e); } } @Override public ReadData materialize(final long offset, final long length) { if (lock == null) { throw new N5Exception.N5IOException("FileLazyRead is already closed."); } ReadData readData = null; try (final FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) { channel.position(offset); final long channelSize = channel.size(); if (!validBounds(channelSize, offset, length)) { throw new IndexOutOfBoundsException(); } final long size = length < 0 ? (channelSize - offset) : length; if (size > Integer.MAX_VALUE) { throw new IndexOutOfBoundsException("Attempt to materialize too large data"); } final byte[] data = new byte[(int) size]; final ByteBuffer buf = ByteBuffer.wrap(data); channel.read(buf); readData = ReadData.from(data); } catch (final NoSuchFileException e) { throw new N5Exception.N5NoSuchKeyException("No such file", e); } catch (IOException | UncheckedIOException e) { /* Occasionally (frequently for some source remote mounted file systems) this can throw exceptions during * `channel.close()` which is called automatically in the try-with-resources block. In this case, we have * successfully read the data, and we can return it, and ignore the exception. * */ if (readData == null) throw new N5Exception.N5IOException(e); } return readData; } @Override public void close() throws IOException { if (lock != null) { lock.close(); lock = null; } } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Reader.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.InputStreamReader; import java.io.UncheckedIOException; import java.util.List; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import org.janelia.saalfeldlab.n5.shard.PositionValueAccess; import com.google.gson.Gson; import com.google.gson.JsonElement; /** * {@link N5Reader} implementation through {@link KeyValueAccess} with JSON * attributes parsed with {@link Gson}. * */ public interface GsonKeyValueN5Reader extends GsonN5Reader { KeyValueAccess getKeyValueAccess(); default boolean groupExists(final String normalPath) { return getKeyValueAccess().isDirectory(absoluteGroupPath(normalPath)); } @Override default boolean exists(final String pathName) { final String normalPath = N5URI.normalizeGroupPath(pathName); return groupExists(normalPath) || datasetExists(normalPath); } @Override default boolean datasetExists(final String pathName) throws N5Exception { // for n5, every dataset must be a group return getDatasetAttributes(pathName) != null; } /** * Reads or creates the attributes map of a group or dataset. * * @param pathName * group path * @return the attribute * @throws N5Exception if the attributes cannot be read */ @Override default JsonElement getAttributes(final String pathName) throws N5Exception { final String groupPath = N5URI.normalizeGroupPath(pathName); final String attributesPath = absoluteAttributesPath(groupPath); try (final VolatileReadData readData = getKeyValueAccess().createReadData(attributesPath);) { if (readData == null) { return null; } return GsonUtils.readAttributes(new InputStreamReader(readData.inputStream()), getGson()); } catch (final N5Exception.N5NoSuchKeyException e) { return null; } catch (final UncheckedIOException | N5IOException e) { throw new N5IOException("Failed to read attributes from dataset " + pathName, e); } } @Override default DataBlock readChunk( final String pathName, final DatasetAttributes datasetAttributes, final long... gridPosition) throws N5Exception { DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); try { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(pathName), convertedDatasetAttributes); return convertedDatasetAttributes. getDatasetAccess().readChunk(posKva, gridPosition); } catch (N5Exception.N5NoSuchKeyException e) { return null; } } @Override default List> readChunks( final String pathName, final DatasetAttributes datasetAttributes, final List blockPositions) throws N5Exception { DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(pathName), convertedDatasetAttributes); return convertedDatasetAttributes. getDatasetAccess().readChunks(posKva, blockPositions); } @Override default DataBlock readBlock( final String pathName, final DatasetAttributes datasetAttributes, final long... gridPosition) throws N5Exception { final DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); final int shardLevel = convertedDatasetAttributes.getNestedBlockGrid().numLevels() - 1; try { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(pathName), convertedDatasetAttributes); return convertedDatasetAttributes. getDatasetAccess().readBlock(posKva, gridPosition, shardLevel); } catch (N5Exception.N5NoSuchKeyException e) { return null; } } @Override default String[] list(final String pathName) throws N5Exception { return getKeyValueAccess().listDirectories(absoluteGroupPath(pathName)); } /** * Constructs the absolute path (in terms of this store) for the group or * dataset. * * @param normalGroupPath * normalized group path without leading slash * @return the absolute path to the group */ default String absoluteGroupPath(final String normalGroupPath) { return getKeyValueAccess().compose(getURI(), normalGroupPath); } /** * Constructs the absolute path (in terms of this store) for the attributes * file of a group or dataset. * * @param normalPath * normalized group path without leading slash * @return the absolute path to the attributes */ default String absoluteAttributesPath(final String normalPath) { return getKeyValueAccess().compose(getURI(), normalPath, getAttributesKey()); } @Override default boolean blockExists( final String pathName, final DatasetAttributes datasetAttributes, final long... gridPosition) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(pathName); final String blockPath = getKeyValueAccess().compose(getURI(), normalPath, datasetAttributes.relativeBlockPath(gridPosition)); return getKeyValueAccess().isFile(blockPath); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/GsonKeyValueN5Writer.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.OutputStreamWriter; import java.io.UncheckedIOException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.List; import java.util.Map; import com.google.gson.JsonSyntaxException; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.shard.PositionValueAccess; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; /** * Default implementation of {@link N5Writer} with JSON attributes parsed with * {@link Gson}. */ public interface GsonKeyValueN5Writer extends GsonN5Writer, GsonKeyValueN5Reader { /** * TODO This overrides the version even if incompatible, check * if this is the desired behavior or if it is always overridden, e.g. as by * the caching version. If this is true, delete this implementation. * * @param path to the group to write the version into */ default void setVersion(final String path) { if (!VERSION.equals(getVersion())) setAttribute("/", VERSION_KEY, VERSION.toString()); } static String initializeContainer( final KeyValueAccess keyValueAccess, final String basePath) throws N5IOException { final String normBasePath = keyValueAccess.normalize(basePath); keyValueAccess.createDirectories(normBasePath); return normBasePath; } @Override default void createGroup(final String path) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(path); getKeyValueAccess().createDirectories(absoluteGroupPath(normalPath)); } /** * Helper method that writes an attributes tree into the store *

* TODO This method is not part of the public API and should be protected * in Java versions greater than 8 * * @param normalGroupPath * to write the attributes to * @param attributes * to write * @throws N5Exception * if unable to write the attributes at {@code normalGroupPath} */ default void writeAttributes( final String normalGroupPath, final JsonElement attributes) throws N5Exception { final ReadData newAttributesReadData = ReadData.from(os -> { final OutputStreamWriter writer = new OutputStreamWriter(os, StandardCharsets.UTF_8); GsonUtils.writeAttributes(writer, attributes, getGson()); }); try { getKeyValueAccess().write(absoluteAttributesPath(normalGroupPath), newAttributesReadData); } catch (UncheckedIOException | N5IOException e) { throw new N5Exception.N5IOException("Failed to write attributes into " + normalGroupPath, e); } } @Override default void setAttributes( final String path, final JsonElement attributes) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(path); if (!exists(normalPath)) throw new N5IOException("" + normalPath + " is not a group or dataset."); writeAttributes(normalPath, attributes); } /** * Helper method that reads the existing map of attributes, JSON encodes, * inserts and overrides the provided attributes, and writes them back into * the attributes store. * * TODO This method is not part of the public API and should be protected * in Java greater than 8 * * @param normalGroupPath * to write the attributes to * @param attributes * to write * @throws N5Exception * if unable to read or write the attributes at * {@code normalGroupPath} */ default void writeAttributes( final String normalGroupPath, final Map attributes) throws N5Exception { if (attributes != null && !attributes.isEmpty()) { JsonElement root = getAttributes(normalGroupPath); root = root != null && root.isJsonObject() ? root.getAsJsonObject() : new JsonObject(); root = GsonUtils.insertAttributes(root, attributes, getGson()); writeAttributes(normalGroupPath, root); } } @Override default void setAttributes( final String path, final Map attributes) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(path); if (!exists(normalPath)) throw new N5IOException("" + normalPath + " is not a group or dataset."); writeAttributes(normalPath, attributes); } @Override default boolean removeAttribute(final String groupPath, final String attributePath) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(groupPath); final String absoluteNormalPath = getKeyValueAccess().compose(getURI(), normalPath); final String normalKey = N5URI.normalizeAttributePath(attributePath); if (!getKeyValueAccess().isDirectory(absoluteNormalPath)) return false; if (attributePath.equals("/")) { setAttributes(normalPath, JsonNull.INSTANCE); return true; } final JsonElement attributes = getAttributes(normalPath); if (GsonUtils.removeAttribute(attributes, normalKey) != null) { setAttributes(normalPath, attributes); return true; } return false; } @Override default T removeAttribute(final String pathName, final String key, final Class cls) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(pathName); final String normalKey = N5URI.normalizeAttributePath(key); final JsonElement attributes = getAttributes(normalPath); final T obj; try { obj = GsonUtils.removeAttribute(attributes, normalKey, cls, getGson()); } catch (JsonSyntaxException | NumberFormatException | ClassCastException e) { throw new N5Exception.N5ClassCastException(e); } if (obj != null) { setAttributes(normalPath, attributes); } return obj; } @Override default boolean removeAttributes(final String pathName, final List attributes) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(pathName); boolean removed = false; for (final String attribute : attributes) { final String normalKey = N5URI.normalizeAttributePath(attribute); removed |= removeAttribute(normalPath, normalKey); } return removed; } @Override default void writeRegion( final String datasetPath, final DatasetAttributes datasetAttributes, final long[] min, final long[] size, final DataBlockSupplier chunkSupplier, final boolean writeFully) throws N5Exception { DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); try { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(datasetPath), convertedDatasetAttributes); convertedDatasetAttributes.getDatasetAccess().writeRegion(posKva, min, size, chunkSupplier, writeFully); } catch (final UncheckedIOException e) { throw new N5IOException( "Failed to write blocks into dataset " + datasetPath, e); } } @Override default void writeRegion( final String datasetPath, final DatasetAttributes datasetAttributes, final long[] min, final long[] size, final DataBlockSupplier chunkSupplier, final boolean writeFully, final ExecutorService exec) throws N5Exception, InterruptedException, ExecutionException { DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); try { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(datasetPath), convertedDatasetAttributes); convertedDatasetAttributes.getDatasetAccess().writeRegion(posKva, min, size, chunkSupplier, writeFully, exec); } catch (final UncheckedIOException e) { throw new N5IOException( "Failed to write blocks into dataset " + datasetPath, e); } } @Override default void writeChunks( final String datasetPath, final DatasetAttributes datasetAttributes, final DataBlock... chunks) throws N5Exception { DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); try { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(datasetPath), convertedDatasetAttributes); convertedDatasetAttributes.getDatasetAccess().writeChunks(posKva, Arrays.asList(chunks)); } catch (final UncheckedIOException e) { throw new N5IOException( "Failed to write chunks into dataset " + datasetPath, e); } } @Override default void writeChunk( final String path, final DatasetAttributes datasetAttributes, final DataBlock chunk) throws N5Exception { DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); try { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(path), convertedDatasetAttributes); convertedDatasetAttributes. getDatasetAccess().writeChunk(posKva, chunk); } catch (final UncheckedIOException e) { throw new N5IOException( "Failed to write chunk " + Arrays.toString(chunk.getGridPosition()) + " into dataset " + path, e); } } @Override default void writeBlock( final String path, final DatasetAttributes datasetAttributes, final DataBlock dataBlock) throws N5Exception { final DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); final int shardLevel = convertedDatasetAttributes.getNestedBlockGrid().numLevels() - 1; try { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(path), convertedDatasetAttributes); convertedDatasetAttributes. getDatasetAccess().writeBlock(posKva, dataBlock, shardLevel); } catch (final UncheckedIOException e) { throw new N5IOException( "Failed to write block " + Arrays.toString(dataBlock.getGridPosition()) + " into dataset " + path, e); } } @Override default boolean remove(final String path) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(path); final String groupPath = absoluteGroupPath(normalPath); if (getKeyValueAccess().isDirectory(groupPath)) getKeyValueAccess().delete(groupPath); /* an IOException should have occurred if anything had failed midway */ return true; } @Override default boolean deleteBlock( final String path, final DatasetAttributes datasetAttributes, final long... gridPosition) throws N5Exception { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(path), datasetAttributes); return posKva.remove(gridPosition); } @Override default boolean deleteChunk( final String path, final DatasetAttributes datasetAttributes, final long... gridPosition) throws N5Exception { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(path), datasetAttributes); return datasetAttributes.getDatasetAccess().deleteChunk(posKva, gridPosition); } @Override default boolean deleteChunks( final String path, final DatasetAttributes datasetAttributes, final List gridPositions) throws N5Exception { final PositionValueAccess posKva = PositionValueAccess.fromKva(getKeyValueAccess(), getURI(), N5URI.normalizeGroupPath(path), datasetAttributes); return datasetAttributes.getDatasetAccess().deleteChunks(posKva, gridPositions); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/GsonN5Reader.java ================================================ package org.janelia.saalfeldlab.n5; import java.lang.reflect.Type; import java.util.Map; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonParseException; import com.google.gson.Gson; import com.google.gson.JsonElement; import com.google.gson.JsonSyntaxException; /** * {@link N5Reader} with JSON attributes parsed with {@link Gson}. * */ public interface GsonN5Reader extends N5Reader { Gson getGson(); /** * Get the key for the {@link KeyValueAccess}, that is used for storing attributes. * The N5 format uses "attributes.json". * * @return the attributes key */ String getAttributesKey(); @Override default Map> listAttributes(final String pathName) throws N5Exception { return GsonUtils.listAttributes(getAttributes(pathName)); } @Override default DatasetAttributes getDatasetAttributes(final String pathName) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(pathName); final JsonElement attributes = getAttributes(normalPath); return createDatasetAttributes(attributes); } default DatasetAttributes createDatasetAttributes(final JsonElement attributes) { final JsonDeserializationContext context = new JsonDeserializationContext() { @Override public T deserialize(JsonElement json, Type typeOfT) throws JsonParseException { return getGson().fromJson(json, typeOfT); } }; return DatasetAttributes.getJsonAdapter().deserialize(attributes, DatasetAttributes.class, context); } @Override default T getAttribute(final String pathName, final String key, final Class clazz) throws N5Exception { final String normalPathName = N5URI.normalizeGroupPath(pathName); final String normalizedAttributePath = N5URI.normalizeAttributePath(key); final JsonElement attributes = getAttributes(normalPathName); try { return GsonUtils.readAttribute(attributes, normalizedAttributePath, clazz, getGson()); } catch (JsonSyntaxException | NumberFormatException | ClassCastException e) { throw new N5Exception.N5ClassCastException(e); } } @Override default T getAttribute(final String pathName, final String key, final Type type) throws N5Exception { final String normalPathName = N5URI.normalizeGroupPath(pathName); final String normalizedAttributePath = N5URI.normalizeAttributePath(key); final JsonElement attributes = getAttributes(normalPathName); try { return GsonUtils.readAttribute(attributes, normalizedAttributePath, type, getGson()); } catch (JsonSyntaxException | NumberFormatException | ClassCastException e) { throw new N5Exception.N5ClassCastException(e); } } /** * Reads or the attributes of a group or dataset. * * @param pathName * group path * @return the attributes identified by pathName * @throws N5Exception if the attribute cannot be returned */ JsonElement getAttributes(final String pathName) throws N5Exception; } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/GsonN5Writer.java ================================================ package org.janelia.saalfeldlab.n5; import com.google.gson.Gson; import com.google.gson.JsonElement; /** * {@link N5Writer} with JSON attributes parsed with {@link Gson}. * */ public interface GsonN5Writer extends GsonN5Reader, N5Writer { /** * Set the attributes of a group. This result of this method is equivalent * with {@link N5Writer#setAttribute(String, String, Object) N5Writer#setAttribute(groupPath, "/", attributes)}. * * @param groupPath * to write the attributes to * @param attributes * to write * @throws N5Exception if the attributes cannot be set */ void setAttributes( final String groupPath, final JsonElement attributes) throws N5Exception; } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/GsonUtils.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.lang.reflect.Array; import java.lang.reflect.Type; import java.util.HashMap; import java.util.Map; import java.util.regex.Matcher; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; import com.google.gson.JsonSyntaxException; import com.google.gson.reflect.TypeToken; import org.janelia.saalfeldlab.n5.N5Exception.N5JsonParseException; /** * Utility class for working with JSON. * * @author Stephan Saalfeld */ public interface GsonUtils { /** * Reads the attributes json from a given {@link Reader}. * * @param reader * the reader * @param gson * to parse Json from the {@code reader} * @return the root {@link JsonObject} of the attributes */ static JsonElement readAttributes(final Reader reader, final Gson gson) { return gson.fromJson(reader, JsonElement.class); } static T readAttribute( final JsonElement root, final String normalizedAttributePath, final Class cls, final Gson gson) throws JsonSyntaxException, NumberFormatException, ClassCastException { return readAttribute(root, normalizedAttributePath, TypeToken.get(cls).getType(), gson); } static T readAttribute( final JsonElement root, final String normalizedAttributePath, final Type type, final Gson gson) throws JsonSyntaxException, NumberFormatException, ClassCastException { final JsonElement attribute = getAttribute(root, normalizedAttributePath); return parseAttributeElement(attribute, gson, type); } /** * Deserialize the {@code attribute} as {@link Type type} {@code T}. * * @param attribute * to deserialize as {@link Type type} * @param gson * used to deserialize {@code attribute} * @param type * to deserialize {@code attribute} as * @param * return type represented by {@link Type type} * @return the deserialized attribute object, or {@code null} if * {@code attribute} cannot deserialize to {@code T} */ @SuppressWarnings("unchecked") static T parseAttributeElement(final JsonElement attribute, final Gson gson, final Type type) throws JsonSyntaxException, NumberFormatException, ClassCastException { if (attribute == null) return null; final Class clazz = (type instanceof Class) ? ((Class)type) : null; if (clazz != null && clazz.isAssignableFrom(HashMap.class)) { final Type mapType = new TypeToken>() { }.getType(); final Map retMap = gson.fromJson(attribute, mapType); // noinspection unchecked return (T)retMap; } if (attribute instanceof JsonArray) { final JsonArray array = attribute.getAsJsonArray(); try { final T retArray = GsonUtils.getJsonAsArray(gson, array, type); if (retArray != null) return retArray; } catch (final JsonSyntaxException e) { if (type == String.class) //noinspection unchecked return (T)gson.toJson(attribute); } } try { final T parsedResult = gson.fromJson(attribute, type); if (parsedResult == null) throw new ClassCastException("Cannot parse json as type " + type.getTypeName()); return parsedResult; } catch (final JsonSyntaxException e) { if (type == String.class) //noinspection unchecked return (T)gson.toJson(attribute); throw e; } } /** * Return the attribute at {@code normalizedAttributePath} as a * {@link JsonElement}. Does not attempt to parse the attribute. * to search for the {@link JsonElement} at location * {@code normalizedAttributePath} * * @param root * containing an attribute at normalizedAttributePath * @param normalizedAttributePath * to the attribute * @return the attribute as a {@link JsonElement}. */ static JsonElement getAttribute(JsonElement root, final String normalizedAttributePath) { final String[] pathParts = normalizedAttributePath.split("(?= jsonArray.size()) { return null; } root = jsonArray.get(index); } else { return null; } } } return root; } /** * Best effort implementation of {@link N5Reader#listAttributes(String)} * with limited type resolution. Possible return types are *

    *
  • null
  • *
  • boolean
  • *
  • double
  • *
  • String
  • *
  • Object
  • *
  • boolean[]
  • *
  • double[]
  • *
  • String[]
  • *
  • Object[]
  • *
* * @param root * the json element * @return the attribute map */ static Map> listAttributes(final JsonElement root) throws N5JsonParseException { if (root == null) { return new HashMap<>(); } if (!root.isJsonObject()) { throw new N5JsonParseException("JsonElement found, but was not JsonObject"); } final HashMap> attributes = new HashMap<>(); root.getAsJsonObject().entrySet().forEach(entry -> { final Class clazz; final String key = entry.getKey(); final JsonElement jsonElement = entry.getValue(); if (jsonElement.isJsonNull()) clazz = null; else if (jsonElement.isJsonPrimitive()) clazz = classForJsonPrimitive((JsonPrimitive)jsonElement); else if (jsonElement.isJsonArray()) { final JsonArray jsonArray = (JsonArray)jsonElement; Class arrayElementClass = Object.class; if (jsonArray.size() > 0) { final JsonElement firstElement = jsonArray.get(0); if (firstElement.isJsonPrimitive()) { arrayElementClass = classForJsonPrimitive(firstElement.getAsJsonPrimitive()); for (int i = 1; i < jsonArray.size() && arrayElementClass != Object.class; ++i) { final JsonElement element = jsonArray.get(i); if (element.isJsonPrimitive()) { final Class nextArrayElementClass = classForJsonPrimitive( element.getAsJsonPrimitive()); if (nextArrayElementClass != arrayElementClass) if (nextArrayElementClass == double.class && arrayElementClass == long.class) arrayElementClass = double.class; else { arrayElementClass = Object.class; break; } } else { arrayElementClass = Object.class; break; } } } clazz = Array.newInstance(arrayElementClass, 0).getClass(); } else clazz = Object[].class; } else clazz = Object.class; attributes.put(key, clazz); }); return attributes; } static T getJsonAsArray(final Gson gson, final JsonArray array, final Class cls) { return getJsonAsArray(gson, array, TypeToken.get(cls).getType()); } @SuppressWarnings("unchecked") static T getJsonAsArray(final Gson gson, final JsonArray array, final Type type) { final Class clazz = (type instanceof Class) ? ((Class)type) : null; if (type == boolean[].class) { final boolean[] retArray = new boolean[array.size()]; for (int i = 0; i < array.size(); i++) { final Boolean value = gson.fromJson(array.get(i), boolean.class); retArray[i] = value; } return (T)retArray; } else if (type == double[].class) { final double[] retArray = new double[array.size()]; for (int i = 0; i < array.size(); i++) { final double value = gson.fromJson(array.get(i), double.class); retArray[i] = value; } return (T)retArray; } else if (type == float[].class) { final float[] retArray = new float[array.size()]; for (int i = 0; i < array.size(); i++) { final float value = gson.fromJson(array.get(i), float.class); retArray[i] = value; } return (T)retArray; } else if (type == long[].class) { final long[] retArray = new long[array.size()]; for (int i = 0; i < array.size(); i++) { final long value = gson.fromJson(array.get(i), long.class); retArray[i] = value; } return (T)retArray; } else if (type == short[].class) { final short[] retArray = new short[array.size()]; for (int i = 0; i < array.size(); i++) { final short value = gson.fromJson(array.get(i), short.class); retArray[i] = value; } return (T)retArray; } else if (type == int[].class) { final int[] retArray = new int[array.size()]; for (int i = 0; i < array.size(); i++) { final int value = gson.fromJson(array.get(i), int.class); retArray[i] = value; } return (T)retArray; } else if (type == byte[].class) { final byte[] retArray = new byte[array.size()]; for (int i = 0; i < array.size(); i++) { final byte value = gson.fromJson(array.get(i), byte.class); retArray[i] = value; } return (T)retArray; } else if (type == char[].class) { final char[] retArray = new char[array.size()]; for (int i = 0; i < array.size(); i++) { final char value = gson.fromJson(array.get(i), char.class); retArray[i] = value; } return (T)retArray; } else if (clazz != null && clazz.isArray()) { final Class componentCls = clazz.getComponentType(); final Object[] clsArray = (Object[])Array.newInstance(componentCls, array.size()); for (int i = 0; i < array.size(); i++) { clsArray[i] = gson.fromJson(array.get(i), componentCls); } // noinspection unchecked return (T)clsArray; } return null; } /** * Return a reasonable class for a {@link JsonPrimitive}. Possible return * types are *
    *
  • boolean
  • *
  • double
  • *
  • String
  • *
  • Object
  • *
* * @param jsonPrimitive * the json primitive * @return the class */ static Class classForJsonPrimitive(final JsonPrimitive jsonPrimitive) { if (jsonPrimitive.isBoolean()) return boolean.class; else if (jsonPrimitive.isNumber()) { final Number number = jsonPrimitive.getAsNumber(); if (number.longValue() == number.doubleValue()) return long.class; else return double.class; } else if (jsonPrimitive.isString()) return String.class; else return Object.class; } /** * If there is an attribute in {@code root} such that it can be parsed and * deserialized as {@code T}, * then remove it from {@code root}, write {@code root} to the * {@code writer}, and return the removed attribute. *

* If there is an attribute at the location specified by * {@code normalizedAttributePath} but it cannot be deserialized to * {@code T}, then it is not removed. *

* If nothing is removed, then {@code root} is not written to the * {@code writer}. * to write the modified {@code root} to after removal of the attribute to * remove the attribute from * * * @param the type of the attribute to be removed * @param writer to write the modified JsonElement, which no longer contains the removed attribute * @param root element to remove the attribute from * @param normalizedAttributePath * to the attribute location of the attribute to remove to * deserialize the attribute with of the removed attribute * @param cls of the type of the attribute to be removed * @param gson used to deserialize the JsonElement as {@code T} * @return the removed attribute, or null if nothing removed * @throws IOException * if unable to write to the {@code writer} */ static T removeAttribute( final Writer writer, final JsonElement root, final String normalizedAttributePath, final Class cls, final Gson gson) throws JsonSyntaxException, NumberFormatException, ClassCastException, IOException { final T removed = removeAttribute(root, normalizedAttributePath, cls, gson); if (removed != null) { writeAttributes(writer, root, gson); } return removed; } /** * If there is an attribute in {@code root} at location * {@code normalizedAttributePath} then remove it from {@code root}.. * to write the modified {@code root} to after removal of the attribute to * remove the attribute from * * @param writer to write the modified JsonElement, which no longer contains the removed attribute * @param root to remove the attribute from * @param normalizedAttributePath * to the attribute location to deserialize the attribute with * @param gson to write the attribute to the {@code writer} * @return if the attribute was removed or not * @throws IOException if an error occurs while writing to the {@code writer} */ static boolean removeAttribute( final Writer writer, final JsonElement root, final String normalizedAttributePath, final Gson gson) throws JsonSyntaxException, NumberFormatException, ClassCastException, IOException { final JsonElement removed = removeAttribute(root, normalizedAttributePath, JsonElement.class, gson); if (removed != null) { writeAttributes(writer, root, gson); return true; } return false; } /** * If there is an attribute in {@code root} such that it can be parsed and * desrialized as {@code T}, * then remove it from {@code root} and return the removed attribute. *

* If there is an attribute at the location specified by * {@code normalizedAttributePath} but it cannot be deserialized to * {@code T}, then it is not removed. * to remove the attribute from * * @param the type of the attribute to be removed * @param root element to remove the attribute from * @param normalizedAttributePath * to the attribute location of the attribute to remove to * deserialize the attribute with of the removed attribute * @param cls of the type of the attribute to be removed * @param gson used to deserialize the JsonElement as {@code T} * @return the removed attribute, or null if nothing removed * @throws JsonSyntaxException if the attribute is not valid json * @throws NumberFormatException if {@code T} is a {@link Number} but the attribute cannot be parsed as {@code T} * @throws ClassCastException if an attribute exists at this path, but is not of type {@code T} */ static T removeAttribute( final JsonElement root, final String normalizedAttributePath, final Class cls, final Gson gson) throws JsonSyntaxException, NumberFormatException, ClassCastException { final T attribute = GsonUtils.readAttribute(root, normalizedAttributePath, cls, gson); if (attribute != null) { removeAttribute(root, normalizedAttributePath); } return attribute; } /** * Remove and return the attribute at {@code normalizedAttributePath} as a * {@link JsonElement}. Does not attempt to parse the attribute. to search * for the {@link JsonElement} at location {@code normalizedAttributePath} * * @param root * the root JsonElement * @param normalizedAttributePath * to the attribute * @return the attribute as a {@link JsonElement}. */ static JsonElement removeAttribute(JsonElement root, final String normalizedAttributePath) { final String[] pathParts = normalizedAttributePath.split("(?= jsonArray.size()) { return null; } root = jsonArray.get(index); if (i == pathParts.length - 1) { jsonArray.remove(index); } } else { return null; } } } return root; } /** * Inserts {@code attribute} into {@code root} at location * {@code normalizedAttributePath} and write the resulting {@code root}. *

* If {@code root} is not a {@link JsonObject}, then it is overwritten with * an object containing {@code "normalizedAttributePath": attribute } * * @param writer * the writer * @param root * the root json element * @param normalizedAttributePath * the attribute path * @param attribute * the attribute * @param gson * the gson * @param * the attribute type * @throws IOException * the exception */ static void writeAttribute( final Writer writer, JsonElement root, final String normalizedAttributePath, final T attribute, final Gson gson) throws IOException { root = insertAttribute(root, normalizedAttributePath, attribute, gson); writeAttributes(writer, root, gson); } /** * Writes the attributes JsonElement to a given {@link Writer}. * This will overwrite any existing attributes. * * @param writer * the writer * @param root * the root json element * @param gson * the gson * @param * the attribute type * @throws IOException * the exception */ static void writeAttributes( final Writer writer, final JsonElement root, final Gson gson) throws IOException { gson.toJson(root, writer); writer.flush(); } static JsonElement insertAttributes(JsonElement root, final Map attributes, final Gson gson) { for (final Map.Entry attribute : attributes.entrySet()) { root = insertAttribute(root, N5URI.normalizeAttributePath(attribute.getKey()), attribute.getValue(), gson); } return root; } static JsonElement insertAttribute( JsonElement root, final String normalizedAttributePath, final T attribute, final Gson gson) { LinkedAttributePathToken pathToken = N5URI.getAttributePathTokens(normalizedAttributePath); /* No path to traverse or build; just return the value */ if (pathToken == null) return gson.toJsonTree(attribute); JsonElement json = root; while (pathToken != null) { final JsonElement parent = pathToken.setAndCreateParentElement(json); /* * We may need to create or override the existing root if it is * non-existent or incompatible. */ final boolean rootOverriden = json == root && parent != json; if (root == null || rootOverriden) { root = parent; } json = pathToken.writeChild(gson, attribute); pathToken = pathToken.next(); } return root; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/GzipCompression.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.IOException; import java.io.InputStream; import java.util.zip.Deflater; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; 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.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.serialization.NameConfig; @CompressionType("gzip") @NameConfig.Name("gzip") public class GzipCompression implements Compression { private static final long serialVersionUID = 8630847239813334263L; /** * Explicit equivalent of {@link java.util.zip.Deflater#DEFAULT_COMPRESSION}: zlib defines * level 6 as "a default compromise between speed and compression." An explicit value is used * instead of {@code DEFAULT_COMPRESSION} (-1) because -1 is not a valid level for Zarr codecs. * * @see zlib Manual */ private static final int N5_DEFAULT_GZIP_LEVEL = 6; @CompressionParameter @NameConfig.Parameter private final int level; /** * This is not a NameConfig.Parameter because this parameter must not be * serialized for zarr */ @CompressionParameter private final boolean useZlib; private final transient GzipParameters parameters = new GzipParameters(); public GzipCompression() { this(N5_DEFAULT_GZIP_LEVEL); } public GzipCompression(final int level) { this(level, false); } public GzipCompression(final int level, final boolean useZlib) { this.level = level; this.useZlib = useZlib; } @Override public boolean equals(final Object other) { if (other == null || other.getClass() != GzipCompression.class) return false; else { final GzipCompression gz = ((GzipCompression)other); return useZlib == gz.useZlib && level == gz.level; } } private InputStream decode(final InputStream in) throws IOException { if (useZlib) { return new InflaterInputStream(in); } else { return GzipCompressorInputStream.builder() .setInputStream(in) .setDecompressConcatenated(true) .get(); } } @Override public ReadData decode(final ReadData readData) throws N5IOException { try { return ReadData.from(decode(readData.inputStream())); } catch (IOException e) { throw new N5IOException(e); } } @Override public ReadData encode(final ReadData readData) { if (useZlib) { return readData.encode(out -> new DeflaterOutputStream(out, new Deflater(level))); } else { return readData.encode(out -> { parameters.setCompressionLevel(level); return new GzipCompressorOutputStream(out, parameters); }); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/HttpKeyValueAccess.java ================================================ package org.janelia.saalfeldlab.n5; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.function.TriFunction; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.http.ListResponseParser; import org.janelia.saalfeldlab.n5.readdata.ReadData; import java.io.Closeable; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; import java.net.HttpURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.nio.channels.NonWritableChannelException; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; /** * A read-only {@link KeyValueAccess} implementation using HTTP. As a result, calling lockForWriting, createDirectories, or delete will throw an {@link N5Exception}. *

* The behavior of list, listDirectories, and isDirectory will depend on the server configuration. See the documentation of those methods for details. *

* Methods that take a "normalPath" as an argument expect absolute URIs. */ public class HttpKeyValueAccess implements KeyValueAccess { public static final String HEAD = "HEAD"; public static final String GET = "GET"; public static final String RANGE = "Range"; public static final String ACCEPT_RANGE = "Accept-Range"; public static final String BYTES = "bytes"; private int readTimeoutMilliseconds; private int connectionTimeoutMilliseconds; private ListResponseParser listResponseParser = ListResponseParser.defaultListParser(); private ListResponseParser listDirectoryResponseParser = ListResponseParser.defaultDirectoryListParser(); /** * Opens an {@link HttpKeyValueAccess} * * @throws N5IOException if the access could not be created */ public HttpKeyValueAccess() { readTimeoutMilliseconds = 5000; connectionTimeoutMilliseconds = 5000; } public void setReadTimeout(int readTimeoutMilliseconds) { this.readTimeoutMilliseconds = readTimeoutMilliseconds; } public void setConnectionTimeout(int connectionTimeoutMilliseconds) { this.connectionTimeoutMilliseconds = connectionTimeoutMilliseconds; } public void setListParser(final ListResponseParser parser) { listResponseParser = parser; } public void setListDirectoryParser(final ListResponseParser parser) { listDirectoryResponseParser = parser; } @Override public String normalize(final String path) { return N5URI.normalizeGroupPath(path); } @Override public URI uri(final String normalPath) throws URISyntaxException { return new URI(normalPath); } /** * Test whether the {@code normalPath} exists. *

* Removes leading slash from {@code normalPath}, and then checks whether * either {@code path} or {@code path + "/"} is a key. * * @param normalPath is expected to be in normalized form, no further efforts are * made to normalize it. * @return {@code true} if {@code path} exists, {@code false} otherwise */ @Override public boolean exists(final String normalPath) { try { requireValidHttpResponse(normalPath, "HEAD", "Error checking existence: " + normalPath, true); return true; } catch (N5Exception.N5NoSuchKeyException e) { return false; } } @Override public long size(String normalPath) { final HttpURLConnection head = requireValidHttpResponse(normalPath, "HEAD", "Error checking existence: " + normalPath, true); return head.getContentLengthLong(); } /** * Test whether the path is a directory. *

* Appends trailing "/" to {@code normalPath} if there is none, removes * leading "/", and then checks whether resulting {@code path} is a key. * * @param normalPath is expected to be in normalized form, no further efforts are * made to normalize it. * @return {@code true} if {@code path} (with trailing "/") exists as a key, * {@code false} otherwise */ @Override public boolean isDirectory(final String normalPath) { try { requireValidHttpResponse(getDirectoryPath(normalPath), HEAD, false, (code, msg,http) -> { final N5Exception cause = validExistsResponse(code, "Error checking directory: " + normalPath, msg, true); if (code >= 300 && code < 400) { final String redirectLocation = http.getHeaderField("Location"); if (!(redirectLocation.endsWith("/") || redirectLocation.endsWith("index.html"))) return new N5Exception.N5NoSuchKeyException("Found File at " + normalPath + " but was not directory"); return null; } return cause; }); return true; } catch (N5Exception e) { return false; } } private static String getDirectoryPath(String normalPath) { final String directoryNormalPath; if (normalPath.endsWith("/")) directoryNormalPath = normalPath; else directoryNormalPath = normalPath + "/"; return directoryNormalPath; } /** * Test whether the path is a file. *

* Checks whether {@code normalPath} has no trailing "/", then removes * leading "/" and checks whether the resulting {@code path} is a key. * * @param normalPath is expected to be in normalized form, no further efforts are * made to normalize it. * @return {@code true} if {@code path} exists as a key and has no trailing * slash, {@code false} otherwise */ @Override public boolean isFile(final String normalPath) { /* Files must not end in `/` And Don't accept a redirect to a location ending in `/` */ try { requireValidHttpResponse(getFilePath(normalPath), HEAD, false, (code, msg, http) -> { final N5Exception cause = validExistsResponse(code, "Error accessing file: " + normalPath, msg, true); if (code >= 300 && code < 400) { final String redirectLocation = http.getHeaderField("Location"); if (redirectLocation.endsWith("/") || redirectLocation.endsWith("index.html")) return new N5Exception.N5NoSuchKeyException("Found key at " + normalPath + " but was directory"); } return cause; }); return true; } catch (N5Exception e) { return false; } } private static String getFilePath(String normalPath) { final String fileNormalPath = normalPath.replaceAll("/+$", ""); return fileNormalPath; } private HttpURLConnection httpRequest(String normalPath, String method) throws IOException { final URL url = URI.create(normalPath).toURL(); final HttpURLConnection connection = (HttpURLConnection)url.openConnection(); connection.setReadTimeout(readTimeoutMilliseconds); connection.setConnectTimeout(connectionTimeoutMilliseconds); connection.setRequestMethod(method); return connection; } @Override public VolatileReadData createReadData(final String normalPath) { return VolatileReadData.from(new HttpLazyRead(normalPath)); } public LockedChannel lockForReading(final String normalPath) throws N5IOException { //TODO Caleb: Maybe check exists lazily when attempting to read try { if (!exists(normalPath)) throw new N5Exception.N5NoSuchKeyException("Key does not exist: " + normalPath); return new HttpObjectChannel(uri(normalPath), 0, -1); } catch (URISyntaxException e) { throw new N5Exception("Invalid URI Syntax", e); } } @Override public LockedChannel lockForWriting(final String normalPath) throws N5IOException { throw new N5Exception("HttpKeyValueAccess is read-only"); } @Override public void write(final String normalPath, final ReadData data) throws N5IOException { throw new N5Exception("HttpKeyValueAccess is read-only"); } /** * List all 'directory'-like children of a path. *

* Will throw an N5IOException both if a connection to the server can not be established, or the server does not allow listing. * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @return the directories * @throws N5IOException * if an error occurs during listing */ @Override public String[] listDirectories(final String normalPath) throws N5IOException { return queryListEntries(normalPath, listDirectoryResponseParser, true); } /** * List all children of a path. *

* Will throw an N5IOException both if a connection to the server can not be * established, or the server does not allow listing. * * @param normalPath * is expected to be in normalized form, no further efforts are * made to normalize it. * @return the the child paths * @throws N5IOException * if an error occurs during listing */ @Override public String[] list(final String normalPath) throws N5IOException { return queryListEntries(normalPath, listResponseParser, true); } private String[] queryListEntries(String normalPath, ListResponseParser parser, boolean allowRedirect) throws N5IOException{ final HttpURLConnection http = requireValidHttpResponse(normalPath, GET, "Error listing directory at " + normalPath, allowRedirect); try { final String listResponse = responseToString(http.getInputStream()); return parser.parseListResponse(listResponse); } catch (IOException e) { throw new N5IOException("Error listing directory at " + normalPath, e); } } private static N5Exception validExistsResponse(int code, String responseMsg, String message, boolean allowRedirect) { if (code >= 200 && code < (allowRedirect ? 400 : 300)) return null; final RuntimeException cause = new RuntimeException(message + "( "+ responseMsg + ")(" + code + ")"); if (code == 404 | code == 410) return new N5Exception.N5NoSuchKeyException(message, cause); return new N5Exception(message, cause); } private HttpURLConnection requireValidHttpResponse(String uri, String method, String message, boolean allowRedirect) throws N5Exception { return requireValidHttpResponse(uri, method, (code, msg, http) -> validExistsResponse(code, msg, message, allowRedirect)); } private HttpURLConnection requireValidHttpResponse(String uri, String method, TriFunction filterCode) throws N5Exception { return requireValidHttpResponse(uri, method, true, filterCode); } private HttpURLConnection requireValidHttpResponse(String uri, String method, boolean followRedirects, TriFunction filterCode) throws N5Exception { final int code; final HttpURLConnection http; final String responseMsg; try { http = httpRequest(uri, method); http.setInstanceFollowRedirects(followRedirects); code = http.getResponseCode(); responseMsg = http.getResponseMessage(); } catch (IOException e) { throw new N5IOException("Could not validate HTTP Response", e); } final N5Exception cause = filterCode.apply(code, responseMsg, http); if (cause != null) throw cause; return http; } private String responseToString(InputStream inputStream) throws IOException { return IOUtils.toString(inputStream, StandardCharsets.UTF_8.name()); } @Override public void createDirectories(final String normalPath) { throw new N5Exception("HttpKeyValueAccess is read-only"); } @Override public void delete(final String normalPath) { throw new N5Exception("HttpKeyValueAccess is read-only"); } private class HttpObjectChannel implements LockedChannel { protected final URI uri; private final long startByte; private final long size; private final ArrayList resources = new ArrayList<>(); protected HttpObjectChannel(final URI uri, long startByte, long size) { this.uri = uri; this.startByte = startByte; this.size = size; } private boolean isPartialRead() { return startByte > 0 || (size >= 0 && size != Long.MAX_VALUE); } @Override public InputStream newInputStream() throws N5IOException { try { HttpURLConnection conn = (HttpURLConnection)uri.toURL().openConnection(); if (isPartialRead()) { conn.setRequestProperty(RANGE, rangeString()); final String acceptRanges = conn.getHeaderField(ACCEPT_RANGE); if (acceptRanges == null || !acceptRanges.equals(BYTES)) { conn.disconnect(); conn = (HttpURLConnection)uri.toURL().openConnection(); return ReadData.from(conn.getInputStream()).materialize().slice(startByte, size).inputStream(); } } return conn.getInputStream(); } catch (FileNotFoundException e) { /*default HttpURLConnection throws FileNotFoundException on 404 or 410 */ throw new N5Exception.N5NoSuchKeyException("Could not open stream for " + uri, e); } catch (IOException e) { throw new N5IOException("Could not open stream for " + uri, e); } } private String rangeString() { final String lastByte = (size > 0) ? Long.toString(startByte + size - 1) : ""; return String.format("%s=%d-%s", BYTES, startByte, lastByte); } @Override public Reader newReader() { final InputStreamReader reader = new InputStreamReader(newInputStream(), StandardCharsets.UTF_8); synchronized (resources) { resources.add(reader); } return reader; } @Override public OutputStream newOutputStream() { throw new NonWritableChannelException(); } @Override public Writer newWriter() { throw new NonWritableChannelException(); } @Override public void close() throws IOException { synchronized (resources) { for (final Closeable resource : resources) { resource.close(); } resources.clear(); } } } private class HttpLazyRead implements LazyRead { private final String normalKey; HttpLazyRead(String normalKey) { this.normalKey = normalKey; } @Override public long size() { return HttpKeyValueAccess.this.size(normalKey); } @Override public ReadData materialize(long offset, long length) { try (final HttpObjectChannel ch = new HttpObjectChannel(uri(normalKey), offset, length)) { return ReadData.from(ch.newInputStream()).materialize(); } catch (IOException e) { throw new N5IOException(e); } catch (URISyntaxException e) { throw new N5Exception(e); } } @Override public void close() { } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/IntArrayDataBlock.java ================================================ package org.janelia.saalfeldlab.n5; public class IntArrayDataBlock extends AbstractDataBlock { public IntArrayDataBlock(final int[] size, final long[] gridPosition, final int[] data) { super(size, gridPosition, data, a -> a.length); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/IoPolicy.java ================================================ package org.janelia.saalfeldlab.n5; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import java.io.IOException; public interface IoPolicy { void write(String key, ReadData readData) throws IOException; VolatileReadData read(String key) throws IOException; void delete(String key) throws IOException; static IoPolicy withFallback(IoPolicy primary, IoPolicy fallback) { return new IoPolicy() { @Override public void write(String key, ReadData readData) throws IOException { try { primary.write(key, readData); } catch (IOException e) { fallback.write(key, readData); } } @Override public VolatileReadData read(String key) throws IOException { try { return primary.read(key); } catch (IOException e) { return fallback.read(key); } } @Override public void delete(String key) throws IOException { try { primary.delete(key); } catch (IOException e) { fallback.delete(key); } } }; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/KeyLockState.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.IOException; import java.nio.channels.FileChannel; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.concurrent.Semaphore; /** * Per-key state that tracks both thread locks and file locks. */ class KeyLockState { private final Path path; private final LockingPolicy policy; public KeyLockState(final Path path, LockingPolicy policy) { this.path = path; this.policy = policy; } /** * The current system-level file lock (shared for reading, exclusive for * writing). Is {@link ChannelLock#close closed} when the last {@link * #releaseRead() Reader is released}, or the (one and only) {@link * #releaseWrite() Writer is released}. */ private volatile ChannelLock channelLock; /** * Multiple Readers coordinate via this mutex. {@code numReaders} may only * be modified when {@code readerMutex} is held. */ private final Semaphore readerMutex = new Semaphore(1); private int numReaders = 0; /** * This coordinates mutual exclusion between one writer and (the first of) * multiple readers. {@code channelLock} may only be created or closed when * {@code channelLockMutex} is held. */ private final Semaphore channelLockMutex = new Semaphore(1); LockedFileChannel acquireRead() throws IOException { try { readerMutex.acquire(); try { if (numReaders == 0) { // We are the first Reader, and are responsible for creating the channelLock // (Other concurrent Readers will still be blocked in readerMutex.) // If a Writer is still open, this will block us until the Writer is closed. channelLockMutex.acquire(); try { channelLock = ChannelLock.lock(path, false, policy); } catch (IOException e) { // Something went wrong. Back off. channelLockMutex.release(); throw e; } } // We have a FileChannel. // Create a LockedFileChannel that will releaseRead() when it is closed. ++numReaders; return new LockedFileChannel(channelLock.getChannel(), this::releaseRead); } finally { readerMutex.release(); } } catch (InterruptedException e) { throw new IOException(e); } } void releaseRead() throws IOException { try { readerMutex.acquire(); try { --numReaders; if (numReaders == 0) { // We were the last Reader, and are responsible for releasing the channelLock releaseChannelLock(); } } finally { readerMutex.release(); } } catch (InterruptedException e) { throw new IOException(e); } } private void releaseChannelLock() throws IOException { try { channelLock.close(); } finally { channelLockMutex.release(); } } LockedFileChannel acquireWrite() throws IOException { try { // If another Writer or Reader is still open, this will block until it is closed. channelLockMutex.acquire(); try { channelLock = ChannelLock.lock(path, true, policy); } catch (IOException e) { // Something went wrong. Back off. channelLockMutex.release(); throw e; } // We have a WRITE ChannelLock. // Create a LockedFileChannel that will releaseWrite() when it is closed. return new LockedFileChannel(channelLock.getChannel(), this::releaseWrite); } catch (InterruptedException e) { throw new IOException(e); } } void releaseWrite() throws IOException { releaseChannelLock(); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/KeyValueAccess.java ================================================ package org.janelia.saalfeldlab.n5; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystem; import java.util.Arrays; import java.util.stream.Collectors; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; /** * Key value read primitives used by {@link N5KeyValueReader} * implementations. This interface implements a subset of access primitives * provided by {@link FileSystem} to reduce the implementation burden for * backends lacking a {@link FileSystem} implementation (such as AWS-S3). * * @author Stephan Saalfeld */ public interface KeyValueAccess { /** * Split a path string into its components. * * @param path * the path * @return the path components */ default String[] components( final String path ) { String[] components = Arrays.stream(path.split("/")) .filter(x -> !x.isEmpty()) .toArray(String[]::new); if (components.length == 0) return path.startsWith("/") ? new String[]{"/"} : new String[]{""}; if (path.startsWith("/") && !components[0].equals("/")) { final String[] prependRoot = new String[components.length + 1]; prependRoot[0] = "/"; System.arraycopy(components, 0, prependRoot, 1, components.length); components = prependRoot; } if (path.endsWith("/") && !components[components.length - 1].endsWith("/")) { components[components.length - 1] = components[components.length - 1] + "/"; } return components; } /** * Compose a path from a base uri and subsequent components. * * @param uri the base path uri * @param components the path components * @return the path */ default String compose( final URI uri, final String... components ) { int firstNonEmptyIdx = 0; while (firstNonEmptyIdx < components.length && (components[firstNonEmptyIdx] == null || components[firstNonEmptyIdx].isEmpty())) { firstNonEmptyIdx++; } /*If there are no non-empty components, there is nothing to compose against; return the uri. */ if (components.length == firstNonEmptyIdx) return uri.toString(); /* allocate space for the initial path and the new components, skipping empty strings */ final int nonEmptysize = components.length - firstNonEmptyIdx; final String[] allComponents = new String[1 + nonEmptysize]; if (uri.getPath().isEmpty()) //TODO Caleb: This `isEmpty()` check is only necessary for Java 8. In newer versions // URI resolution is updated so that resolving and empty path with a new path adds // a leading `/` between the rest of the URI and the path part. In Java 8 it doesn't // add the `/` so it ends up directly concatenating the path part with URI allComponents[0] = "/"; else allComponents[0] = uri.getPath(); System.arraycopy(components, firstNonEmptyIdx, allComponents, 1, nonEmptysize); URI composedUri = uri; for (int i = 0; i < allComponents.length; i++) { final String component = allComponents[i]; if (component == null || component.isEmpty()) continue; else if (component.endsWith("/") || i == allComponents.length - 1) composedUri = composedUri.resolve(N5URI.encodeAsUriPath(component)); else composedUri = composedUri.resolve(N5URI.encodeAsUriPath(component + "/")); } return composedUri.toString(); } @Deprecated default String compose( final String... components ) { return normalize( Arrays.stream(components) .filter(x -> !x.isEmpty()) .collect(Collectors.joining("/")) ); } /** * Get the parent of a path string. * * @param path * the path * @return the parent path or null if the path has no parent */ default String parent( final String path ) { final String removeTrailingSlash = path.replaceAll("/+$", ""); return normalize(N5URI.getAsUri(removeTrailingSlash).resolve("").toString()); } /** * Relativize path relative to base. * * @param path * the path * @param base * the base path * @return the result or null if the path has no parent */ default String relativize( final String path, final String base ) { try { /* * Must pass absolute path to `uri`. if it already is, this is * redundant, and has no impact on the result. It's not true that * the inputs are always referencing absolute paths, but it doesn't * matter in this case, since we only care about the relative * portion of `path` to `base`, so the result always ignores the * absolute prefix anyway. */ return normalize(uri("/" + base).relativize(uri("/" + path)).toString()); } catch (final URISyntaxException e) { throw new N5Exception("Cannot relativize path (" + path + ") with base (" + base + ")", e); } } /** * Normalize a path to canonical form. All paths pointing to the same * location return the same output. This is most important for cached * data pointing at the same location getting the same key. * * @param path * the path * @return the normalized path */ String normalize( final String path ); /** * Get the absolute (including scheme) {@link URI} of the given path * * @param uriString * is expected to be in normalized form, no further * efforts are made to normalize it. * @return absolute URI * @throws URISyntaxException if the given path is not a proper URI */ default URI uri(final String uriString) throws URISyntaxException { try { return URI.create(uriString); } catch (Exception ignore) { return N5URI.encodeAsUri(uriString); } } /** * Test whether the path exists. * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @return true if the path exists */ boolean exists( final String normalPath ); /** * Returns the size in bytes of the object at the given normalPath if it exists. * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @return the size of the object in bytes. * @throws N5Exception.N5NoSuchKeyException if the given key does not exist */ long size( final String normalPath ) throws N5Exception.N5NoSuchKeyException; /** * Test whether the path is a directory. * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @return true if the path is a directory */ boolean isDirectory( String normalPath ); /** * Test whether the path is a file. * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @return true if the path is a file */ boolean isFile( String normalPath ); // TODO: Looks un-used. Remove? /** * Create a {@link VolatileReadData} through which data at the normal key * can be read. *

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

* If supported by this KeyValueAccess implementation, partial reads are * possible by {@link ReadData#slice slicing} the returned {@code ReadData}. *

* The resulting {@code VolatileReadData} is potentially lazy. If the requested * key does not exist, it will throw {@code N5NoSuchKeyException}. Whether * the exception is thrown when {@link KeyValueAccess#createReadData(String)}] is called, * or when trying to materialize the {@code VolatileReadData} is implementation dependent. * * @param normalPath * is expected to be in normalized form, no further efforts are made to normalize it * * @return a ReadData * * @throws N5IOException * if an error occurs */ VolatileReadData createReadData(final String normalPath) throws N5IOException; /** * Write {@code data} to the given {@code normalPath}. *

* Existing data at {@code normalPath} will be overridden. * * @param normalPath * is expected to be in normalized form, no further efforts are made to normalize it * @param data * the data to write * * @throws N5IOException * if an error occurs */ void write(String normalPath, ReadData data) throws N5IOException; /** * Create a lock on a path for reading. This isn't meant to be kept * around. Create, use, [auto]close, e.g. * * try (final lock = store.lockForReading()) { * ... * } * * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @return the locked channel * @throws N5IOException * if a locked channel could not be created * @deprecated migrate to {@link KeyValueAccess#createReadData(String)} */ @Deprecated default LockedChannel lockForReading( final String normalPath ) throws N5IOException { return new BufferedKvaLockedChannel(this, normalPath); } /** * 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 it. This * lock isn't meant to be kept around. Create, use, [auto]close, e.g. * * try (final lock = store.lockForWriting()) { * ... * } * * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @return the locked channel * @throws N5IOException * if a locked channel could not be created * @deprecated migrate to {@link KeyValueAccess#write(String, ReadData)} */ @Deprecated default LockedChannel lockForWriting( final String normalPath ) throws N5IOException { return new BufferedKvaLockedChannel(this, normalPath); } /** * List all 'directory'-like children of a path. * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @return the directories * @throws N5IOException * if an error occurs during listing */ String[] listDirectories( final String normalPath ) throws N5IOException; /** * List all children of a path. * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @return the the child paths * @throws N5IOException if an error occurs during listing */ String[] list( final String normalPath ) throws N5IOException; /** * Create a directory and all parent paths along the way. The directory * and parent paths are discoverable. On a filesystem, this usually means * that the directories exist, on a key value store that is unaware of * directories, this may be implemented as creating an object for each path. * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @throws N5IOException * if an error occurs during creation */ void createDirectories( final String normalPath ) throws N5IOException; /** * Delete a path. If the path is a directory, delete it recursively. * * @param normalPath * is expected to be in normalized form, no further * efforts are made to normalize it. * @throws N5IOException * if an error occurs during deletion */ void delete( final String normalPath ) throws N5IOException; } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/LinkedAttributePathToken.java ================================================ package org.janelia.saalfeldlab.n5; import java.util.Iterator; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; public abstract class LinkedAttributePathToken implements Iterator> { /** * The JsonElement which contains the mapping that this token represents. */ protected T parentJson; /** * The token representing the subsequent path element. */ protected LinkedAttributePathToken childToken; /** * @return a reference object of type {@code T} */ public abstract T getJsonType(); /** * This method will return the child element, if the {@link #parentJson} * contains a child that * is valid for this token. If the {@link #parentJson} does not have a * child, this method will * create it. If the {@link #parentJson} does have a child, but it is not * {@link #jsonCompatible(JsonElement)} * with our {@link #childToken}, then this method will also create a new * (compatible) child, and replace * the existing incompatible child. * * @return the resulting child element */ public abstract JsonElement getOrCreateChildElement(); /** * This method will write into {@link #parentJson} the subsequent * {@link JsonElement}. *
* The written JsonElement will EITHER be: *

    *
  • The result of serializing {@code value} ( if {@link #childToken} is * {@code null} )
  • *
  • The {@link JsonElement} which represents our {@link #childToken} (See * {@link #getOrCreateChildElement()} )
  • *
* * @param gson * instance used to serialize {@code value } * @param value * to write * @return the object that was written. */ public JsonElement writeChild(final Gson gson, final Object value) { if (childToken != null) /* * If we have a child, get/create it and set current json element to * it */ return getOrCreateChildElement(); else { /* We are done, no token remaining */ writeValue(gson, value); return getChildElement(); } } /** * Check if the provided {@code json} is compatible with this token. *
* Compatibility means that the provided {@code JsonElement} does not * explicitly conflict with this token. That means that {@code null} is * always compatible. The only real incompatibility is when the * {@code JsonElement} * that we receive is different that we expect. * * @param json * element that we are testing for compatibility. * @return false if {@code json} is not null and is not of type {@code T} */ @SuppressWarnings("BooleanMethodIsAlwaysInverted") public boolean jsonCompatible(final JsonElement json) { return json == null || json.getClass() == getJsonType().getClass(); } /** * Write {@code value} into {@link #parentJson}. *
* Should only be called when {@link #childToken} is null (i.e. this is the * last token in the attribute path). * * @param gson * instance used to serialize {@code value} * @param value * to serialize */ protected abstract void writeValue(Gson gson, Object value); /** * Write the {@link JsonElement} the corresponds to {@link #childToken} into * {@link #parentJson}. */ protected abstract void writeChildElement(); /** * @return the element that is represented by {@link #childToken} if * present. */ protected abstract JsonElement getChildElement(); /** * If {@code json} is compatible with the type or token that we are, then * set {@link #parentJson} to {@code json}. *
* However, if {@code json} is either {@code null} or not * {@link #jsonCompatible(JsonElement)} then * {@link #parentJson} will be set to a new instance of {@code T}. * * @param json * to attempt to set to {@link #parentJson} * @return the value set to {@link #parentJson}. */ @SuppressWarnings("unchecked") protected JsonElement setAndCreateParentElement(final JsonElement json) { if (json == null || !jsonCompatible(json)) { // noinspection unchecked parentJson = (T)getJsonType().deepCopy(); } else { // noinspection unchecked parentJson = (T)json; } return parentJson; } /** * @return if we have a {@link #childToken}. */ @Override public boolean hasNext() { return childToken != null; } /** * @return {@link #childToken} */ @Override public LinkedAttributePathToken next() { return childToken; } public static class ObjectAttributeToken extends LinkedAttributePathToken { private static final JsonObject JSON_OBJECT = new JsonObject(); private final String key; public ObjectAttributeToken(final String key) { this.key = key; } /** * @return the {@link #key} this token maps it's * {@link #getChildElement()} to. */ public String getKey() { return key; } @Override public String toString() { return getKey(); } @Override public JsonObject getJsonType() { return JSON_OBJECT; } @Override public JsonElement getOrCreateChildElement() { final JsonElement childElement = parentJson.getAsJsonObject().get(key); if (!parentJson.has(key) || childToken != null && !childToken.jsonCompatible(childElement)) { writeChildElement(); } return getChildElement(); } @Override protected JsonElement getChildElement() { return parentJson.get(key); } @Override protected void writeValue(final Gson gson, final Object value) { parentJson.add(key, gson.toJsonTree(value)); } @Override protected void writeChildElement() { if (childToken != null) parentJson.add(key, childToken.getJsonType().deepCopy()); } } public static class ArrayAttributeToken extends LinkedAttributePathToken { private static final JsonArray JSON_ARRAY = new JsonArray(); private final int index; public ArrayAttributeToken(final int index) { this.index = index; } /** * @return the {@link #index} this token maps it's * {@link #getChildElement()} to. */ public int getIndex() { return index; } @Override public String toString() { return "[" + getIndex() + "]"; } @Override public JsonArray getJsonType() { return JSON_ARRAY; } @Override public JsonElement getOrCreateChildElement() { if (index >= parentJson.size() || childToken != null && !childToken.jsonCompatible(parentJson.get(index))) writeChildElement(); return parentJson.get(index); } @Override protected void writeChildElement() { if (childToken != null) { fillArrayToIndex(parentJson, index, null); parentJson.set(index, childToken.getJsonType().deepCopy()); } } @Override protected JsonElement getChildElement() { return parentJson.get(index); } @Override protected void writeValue(final Gson gson, final Object value) { fillArrayToIndex(parentJson, index, value); parentJson.set(index, gson.toJsonTree(value)); } /** * Fill {@code array} up to, and including, {@code index} with a default * value, determined by the type of {@code value}. * Importantly, this does NOT set {@code array[index]=value}. * * @param array * to fill * @param index * to fill the array to (inclusive) * @param value * used to determine the default array fill value */ private static void fillArrayToIndex(final JsonArray array, final int index, final Object value) { final JsonElement fillValue; if (valueRepresentsANumber(value)) { fillValue = new JsonPrimitive(0); } else { fillValue = JsonNull.INSTANCE; } for (int i = array.size(); i <= index; i++) { array.add(fillValue); } } /** * Check if {@value} represents a {@link Number}. * True if {@code value} either: *
    *
  • is a {@code Number}, or;
  • *
  • is a {@link JsonPrimitive} where * {@link JsonPrimitive#isNumber()}
  • *
* * @param value * we wish to check if represents a {@link Number} or not. * @return true if {@code value} represents a {@link Number} */ private static boolean valueRepresentsANumber(final Object value) { return value instanceof Number || (value instanceof JsonPrimitive && ((JsonPrimitive)value).isNumber()); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/LockedChannel.java ================================================ package org.janelia.saalfeldlab.n5; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import java.io.Closeable; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; /** * A lock on a path that can create a {@link Reader}, {@link Writer}, * {@link InputStream}, or {@link OutputStream}. * * @author Stephan Saalfeld */ public interface LockedChannel extends Closeable { /** * Create a UTF-8 {@link Reader}. * * @return the reader * @throws N5IOException * if the reader could not be created */ Reader newReader() throws N5IOException; /** * Create a new {@link InputStream}. * * @return the input stream * @throws N5IOException * if an input stream could not be created */ InputStream newInputStream() throws N5IOException; /** * Create a new UTF-8 {@link Writer}. * * @return the writer * @throws N5IOException * if a writer could not be created */ Writer newWriter() throws N5IOException; /** * Create a new {@link OutputStream}. * * @return the output stream * @throws N5IOException * if an output stream could not be created */ OutputStream newOutputStream() throws N5IOException; } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/LockedFileChannel.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.WritableByteChannel; /** * LockedFileChannel implementation for both read and write operations. *

* When closing this {@code LockedFileChannel}, {@code releaseLock} is called, * but the {@code channel} is not closed. The channel may be shared among * multiple {@code LockedFileChannel} for concurrent readers. {@code * releaseLock} should take care of closing the channel when the last reader (or * the only writer) is closed. */ class LockedFileChannel implements Closeable { // TODO: Consider splitting LockedFileChannel into read-only and write-only part. // TODO: Consider removing LockedFileChannel and having // FileKeyLockManager.acquireRead() and FileKeyLockManager.acquireWrite() // return appropriately wrapped SeekableByteChannel. private final FileChannel channel; private ReleaseLock releaseLock; @FunctionalInterface public interface ReleaseLock { void release() throws IOException; } LockedFileChannel(final FileChannel channel, final ReleaseLock releaseLock) { this.channel = channel; this.releaseLock = releaseLock; } /** * Returns the size of this channel's file. *

* See {@link FileChannel#size()}. */ public long size() throws IOException { return channel.size(); } /** * Reads a sequence of bytes from this channel into the given buffer, * starting at the given file position. *

* See {@link FileChannel#read(ByteBuffer, long)}. */ public int read(final ByteBuffer dst, final long position) throws IOException { return channel.read(dst, position); } /** * Return an {@link OutputStream} that writes into this channel. * Closing the OutputStream will close this channel. */ public OutputStream asOutputStream() { return Channels.newOutputStream(new ClosingChannelWrapper()); } volatile boolean firstWrite = true; private class ClosingChannelWrapper implements WritableByteChannel { @Override public int write(final ByteBuffer src) throws IOException { if (!firstWrite) return channel.write(src); try { channel.truncate(0); return channel.write(src); } finally { firstWrite = false; } } @Override public boolean isOpen() { return channel.isOpen(); } @Override public void close() throws IOException { channel.close(); LockedFileChannel.this.close(); } } @Override public void close() throws IOException { if (releaseLock != null) { releaseLock.release(); releaseLock = null; // Mote that setting releaseLock=null here drops the (method) // reference to LockKeyState, which potentially allows clearing the // WeakReference that FileLockManager holds. } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/LockingPolicy.java ================================================ package org.janelia.saalfeldlab.n5; /** * File locking policy. *

* Usually, we want to coordinate reads and writs to a container such that *

    *
  • multiple readers can access a key simultaneously (blocking all writers).
  • *
  • A writer should have exclusive access to a key (blocking all other readers and writers).
  • *
* However, this cannot always be enforced for all backends: * For SMB on macOS OS-level file locking is broken. * For AWS S3 and Google Cloud we can detect (but not prevent) concurrent modifications. *

* Sometimes we know that we can disregard locking, for example in read-only settings. *

* {@code IoPolicy} can be used to configure locking policy for backends ({@link * KeyValueAccess}) that support various locking strategies (potentially coming * with performance trade-offs). *

* The policy values ({@link #STRICT}, {@link #UNSAFE}, {@link #PERMISSIVE}) * specify intent. Detailed interpretation is up to the backend implementation. */ public enum LockingPolicy { /** * Protect all reads and writes by locks. * Fail if locking is not possible. */ STRICT, /** * Reads and writes are unprotected. */ UNSAFE, /** * Try to lock for all reads and writes. * Fall back to unprotected reads and writes if locking is not possible. * This is the default. */ PERMISSIVE; static LockingPolicy fromString(final String s) { if ("strict".equalsIgnoreCase(s)) return STRICT; else if ("unsafe".equalsIgnoreCase(s)) return UNSAFE; // else if ("permissive".equalsIgnoreCase(s)) // return PERMISSIVE; else return PERMISSIVE; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/LongArrayDataBlock.java ================================================ package org.janelia.saalfeldlab.n5; public class LongArrayDataBlock extends AbstractDataBlock { public LongArrayDataBlock(final int[] size, final long[] gridPosition, final long[] data) { super(size, gridPosition, data, a -> a.length); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/Lz4Compression.java ================================================ package org.janelia.saalfeldlab.n5; import net.jpountz.lz4.LZ4BlockInputStream; import net.jpountz.lz4.LZ4BlockOutputStream; import org.janelia.saalfeldlab.n5.Compression.CompressionType; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.serialization.NameConfig; @CompressionType("lz4") @NameConfig.Name("lz4") public class Lz4Compression implements Compression { private static final long serialVersionUID = -9071316415067427256L; @CompressionParameter @NameConfig.Parameter private final int blockSize; public Lz4Compression(final int blockSize) { this.blockSize = blockSize; } public Lz4Compression() { this(1 << 16); } @Override public boolean equals(final Object other) { if (other == null || other.getClass() != Lz4Compression.class) return false; else return blockSize == ((Lz4Compression)other).blockSize; } @Override public ReadData decode(final ReadData readData) { return ReadData.from(new LZ4BlockInputStream(readData.inputStream())); } @Override public ReadData encode(final ReadData readData) { return readData.encode(out -> new LZ4BlockOutputStream(out, blockSize)); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/N5Exception.java ================================================ package org.janelia.saalfeldlab.n5; public class N5Exception extends RuntimeException { public N5Exception() { super(); } public N5Exception(final String message) { super(message); } public N5Exception(final String message, final Throwable cause) { super(message, cause); } public N5Exception(final Throwable cause) { super(cause); } protected N5Exception( final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } public static class N5IOException extends N5Exception{ public N5IOException(final String message) { super(message); } public N5IOException(final String message, final Throwable cause) { super(message, cause); } public N5IOException(final Throwable cause) { super(cause); } protected N5IOException( final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } /** * This excpetion represents the situation when an attribute is requested by key as a specific Class, * that attribute key does exist, but is not parseable as the desired Class */ public static class N5ClassCastException extends N5Exception { public N5ClassCastException(final Class cls) { super("Cannot cast as class " + cls.getName()); } public N5ClassCastException(final String message) { super(message); } public N5ClassCastException(final String message, final Throwable cause) { super(message, cause); } public N5ClassCastException(final Throwable cause) { super(cause); } protected N5ClassCastException( final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } public static class N5NoSuchKeyException extends N5IOException { public N5NoSuchKeyException(final String message) { super(message); } public N5NoSuchKeyException(final String message, final Throwable cause) { super(message, cause); } public N5NoSuchKeyException(final Throwable cause) { super(cause); } protected N5NoSuchKeyException( final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } /** * Exception to represent an error when attempting to parse json attributes */ public static class N5JsonParseException extends N5Exception { public N5JsonParseException(final String message) { super(message); } public N5JsonParseException(final String message, final Throwable cause) { super(message, cause); } public N5JsonParseException(final Throwable cause) { super(cause); } protected N5JsonParseException( final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } public static class N5ConcurrentModificationException extends N5IOException { public N5ConcurrentModificationException(final String message) { super(message); } public N5ConcurrentModificationException(final String message, final Throwable cause) { super(message, cause); } public N5ConcurrentModificationException(final Throwable cause) { super(cause); } protected N5ConcurrentModificationException( final String message, final Throwable cause, final boolean enableSuppression, final boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/N5FSReader.java ================================================ package org.janelia.saalfeldlab.n5; import java.nio.file.FileSystems; import com.google.gson.Gson; import com.google.gson.GsonBuilder; /** * {@link N5Reader} implementation through {@link KeyValueAccess} with JSON * attributes parsed with {@link Gson}. * * @author Stephan Saalfeld * @author Igor Pisarev * @author Philipp Hanslovsky */ //public class N5FSReader extends N5KeyValueReader { public class N5FSReader extends N5KeyValueReader { /** * Opens an {@link N5FSReader} at a given base path with a custom * {@link GsonBuilder} to support custom attributes. * * @param basePath * N5 base path * @param gsonBuilder * the gson builder * @param cacheMeta * cache attributes and meta data * Setting this to true avoids frequent reading and parsing of * JSON encoded attributes and other meta data that requires * accessing the store. This is most interesting for high latency * backends. Changes of cached attributes and meta data by an * independent writer on the same container will not be tracked. * * @throws N5Exception * if the base path cannot be read or does not exist, if the N5 * version of the container is not compatible with this * implementation. */ public N5FSReader(final String basePath, final GsonBuilder gsonBuilder, final boolean cacheMeta) throws N5Exception { super( new FileSystemKeyValueAccess(), basePath, gsonBuilder, cacheMeta); if (!exists("/")) throw new N5Exception.N5IOException("No container exists at " + basePath); } /** * Opens an {@link N5FSReader} at a given base path. * * @param basePath * N5 base path * @param cacheMeta * cache attributes and meta data * Setting this to true avoids frequent reading and parsing of * JSON encoded attributes and other meta data that requires * accessing the store. This is most interesting for high latency * backends. Changes of cached attributes and meta data by an * independent writer on the same container will not be tracked. * * @throws N5Exception * if the base path cannot be read or does not exist, if the N5 * version of the container is not compatible with this * implementation. */ public N5FSReader(final String basePath, final boolean cacheMeta) throws N5Exception { this(basePath, new GsonBuilder(), cacheMeta); } /** * Opens an {@link N5FSReader} at a given base path with a custom * {@link GsonBuilder} to support custom attributes. * * @param basePath * N5 base path * @param gsonBuilder * the gson builder * @throws N5Exception * if the base path cannot be read or does not exist, if the N5 * version of the container is not compatible with this * implementation. */ public N5FSReader(final String basePath, final GsonBuilder gsonBuilder) throws N5Exception { this(basePath, gsonBuilder, false); } /** * Opens an {@link N5FSReader} at a given base path. * * @param basePath * N5 base path * @throws N5Exception * if the base path cannot be read or does not exist, if the N5 * version of the container is not compatible with this * implementation. */ public N5FSReader(final String basePath) throws N5Exception { this(basePath, new GsonBuilder(), false); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/N5FSWriter.java ================================================ package org.janelia.saalfeldlab.n5; import java.nio.file.FileSystems; import com.google.gson.GsonBuilder; /** * Filesystem {@link N5Writer} implementation with version compatibility check. * * @author Stephan Saalfeld */ public class N5FSWriter extends N5KeyValueWriter { /** * Opens an {@link N5FSWriter} at a given base path with a custom * {@link GsonBuilder} to support custom attributes. * * If the base path does not exist, it will be created. * * If the base path exists and if the N5 version of the container is * compatible with this implementation, the N5 version of this container * will be set to the current N5 version of this implementation. * * @param basePath * n5 base path * @param gsonBuilder * the gson builder * @param cacheAttributes * cache attributes and meta data * Setting this to true avoidsfrequent reading and parsing of * JSON encoded attributes andother meta data that requires * accessing the store. This ismost interesting for high latency * backends. Changes of cachedattributes and meta data by an * independent writer on the samecontainer will not be tracked. * * @throws N5Exception * if the base path cannot be written to or cannot be created, * if the N5 version of the container is not compatible with * this implementation. */ public N5FSWriter(final String basePath, final GsonBuilder gsonBuilder, final boolean cacheAttributes) throws N5Exception { super( new FileSystemKeyValueAccess(), basePath, gsonBuilder, cacheAttributes); } /** * Opens an {@link N5FSWriter} at a given base path. * * If the base path does not exist, it will be created. * * If the base path exists and if the N5 version of the container is * compatible with this implementation, the N5 version of this container * will be set to the current N5 version of this implementation. * * @param basePath * base path * @param cacheAttributes * attributes and meta data * Setting this to true avoidsfrequent reading and parsing of * JSON encoded attributes andother meta data that requires * accessing the store. This ismost interesting for high latency * backends. Changes of cachedattributes and meta data by an * independent writer on the samecontainer will not be tracked. * * @throws N5Exception * if the base path cannot be written to or cannot be created, * if the N5 version of the container is not compatible with * this implementation. */ public N5FSWriter(final String basePath, final boolean cacheAttributes) throws N5Exception { this(basePath, new GsonBuilder(), cacheAttributes); } /** * Opens an {@link N5FSWriter} at a given base path with a custom * {@link GsonBuilder} to support custom attributes. *

* If the base path does not exist, it will be created. *

*

* If the base path exists and if the N5 version of the container is * compatible with this implementation, the N5 version of this container * will be set to the current N5 version of this implementation. *

* * @param basePath * base path * @param gsonBuilder * gson builder * * @throws N5Exception * if the base path cannot be written to or cannot be created, * if the N5 version of the container is not compatible with * this implementation. */ public N5FSWriter(final String basePath, final GsonBuilder gsonBuilder) throws N5Exception { this(basePath, gsonBuilder, false); } /** * Opens an {@link N5FSWriter} at a given base path. *

* If the base path does not exist, it will be created. *

*

* If the base path exists and if the N5 version of the container is * compatible with this implementation, the N5 version of this container * will be set to the current N5 version of this implementation. *

* * @param basePath * n5 base path * * @throws N5Exception * if the base path cannot be written to or cannot be created, * if the N5 version of the container is not compatible with * this implementation. */ public N5FSWriter(final String basePath) throws N5Exception { this(basePath, new GsonBuilder()); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/N5KeyValueReader.java ================================================ package org.janelia.saalfeldlab.n5; import java.net.URI; import java.net.URISyntaxException; import com.google.gson.JsonElement; import org.janelia.saalfeldlab.n5.cache.N5JsonCache; import com.google.gson.Gson; import com.google.gson.GsonBuilder; /** * {@link N5Reader} implementation through {@link KeyValueAccess} with JSON * attributes parsed with {@link Gson}. * * @author Stephan Saalfeld * @author Igor Pisarev * @author Philipp Hanslovsky */ public class N5KeyValueReader implements CachedGsonKeyValueN5Reader { public static final String ATTRIBUTES_JSON = "attributes.json"; protected final KeyValueAccess keyValueAccess; protected final Gson gson; protected final boolean cacheMeta; protected URI uri; private final N5JsonCache cache; /** * Opens an {@link N5KeyValueReader} at a given base path with a custom * {@link GsonBuilder} to support custom attributes. * * @param keyValueAccess * the KeyValueAccess backend used * @param basePath * N5 base path * @param gsonBuilder * the GsonBuilder * @param cacheMeta * cache attributes and meta data * Setting this to true avoidsfrequent reading and parsing of * JSON encoded attributes andother meta data that requires * accessing the store. This ismost interesting for high latency * backends. Changes of cachedattributes and meta data by an * independent writer will not betracked. * * @throws N5Exception * if the base path cannot be read or does not exist, if the N5 * version of the container is not compatible with this * implementation. */ public N5KeyValueReader( final KeyValueAccess keyValueAccess, final String basePath, final GsonBuilder gsonBuilder, final boolean cacheMeta) throws N5Exception { this(true, keyValueAccess, basePath, gsonBuilder, cacheMeta, true); } /** * Opens an {@link N5KeyValueReader} at a given base path with a custom * {@link GsonBuilder} to support custom attributes. * * @param checkVersion * the version check * @param keyValueAccess * the backend KeyValueAccess used * @param basePath * base path * @param gsonBuilder * the GsonBuilder * @param cacheMeta * cache attributes and meta data Setting this to true avoids * frequent reading and parsing of JSON encoded attributes and * other meta data that requires accessing the store. This is * most interesting for high latency backends. Changes of cached * attributes and meta data by an independent writer will not be * tracked. * @param checkExists * if true, an N5IOException will be thrown if a container does * not exist at the specified location * @throws N5Exception * if the base path cannot be read or does not exist, if the N5 * version of the container is not compatible with this * implementation. */ protected N5KeyValueReader( final boolean checkVersion, final KeyValueAccess keyValueAccess, final String basePath, final GsonBuilder gsonBuilder, final boolean cacheMeta, final boolean checkExists) throws N5Exception { this.keyValueAccess = keyValueAccess; this.gson = registerGson(gsonBuilder).create(); this.cacheMeta = cacheMeta; if (this.cacheMeta) this.cache = newCache(); else this.cache = null; try { uri = keyValueAccess.uri(basePath); } catch (final URISyntaxException e) { throw new N5Exception(e); } boolean versionFound = false; if (checkVersion) { /* Existence checks, if any, go in subclasses */ /* Check that version (if there is one) is compatible. */ final Version version = getVersion(); versionFound = !version.equals(NO_VERSION); if (!VERSION.isCompatible(version)) throw new N5Exception.N5IOException( "Incompatible version " + version + " (this is " + VERSION + ")."); } // if a version was found, the container exists - don't need to check again if (checkExists && (!versionFound && !inferExistence("/"))) throw new N5Exception.N5IOException("No container exists at " + basePath); } private boolean inferExistence(String path) { final JsonElement attributes = getAttributes(path); return attributes != null || exists(path); } protected GsonBuilder registerGson(final GsonBuilder gsonBuilder) { gsonBuilder.registerTypeAdapter(DataType.class, new DataType.JsonAdapter()); gsonBuilder.registerTypeHierarchyAdapter(Compression.class, CompressionAdapter.getJsonAdapter()); gsonBuilder.registerTypeHierarchyAdapter(DatasetAttributes.class, DatasetAttributes.getJsonAdapter()); gsonBuilder.disableHtmlEscaping(); return gsonBuilder; } @Override public String getAttributesKey() { return ATTRIBUTES_JSON; } @Override public Gson getGson() { return gson; } @Override public KeyValueAccess getKeyValueAccess() { return keyValueAccess; } @Override public URI getURI() { return uri; } @Override public boolean cacheMeta() { return cacheMeta; } @Override public N5JsonCache getCache() { return this.cache; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/N5KeyValueWriter.java ================================================ package org.janelia.saalfeldlab.n5; import com.google.gson.GsonBuilder; /** * Filesystem {@link N5Writer} implementation with version compatibility check. * * @author Stephan Saalfeld */ public class N5KeyValueWriter extends N5KeyValueReader implements CachedGsonKeyValueN5Writer { /** * Opens an {@link N5KeyValueWriter} at a given base path with a custom * {@link GsonBuilder} to support custom attributes. * *

* If the base path does not exist, it will be created. *

* If the base path exists and if the N5 version of the container is * compatible with this implementation, the N5 version of this container * will be set to the current N5 version of this implementation. * * @param keyValueAccess * the backend key value access to use * @param basePath * n5 base path * @param gsonBuilder * the gson builder * @param cacheAttributes * Setting this to true avoids frequent reading and parsing of * JSON encoded attributes, this is most interesting for high * latency file systems. Changes of attributes by an independent * writer will not be tracked. * @throws N5Exception * if the base path cannot be written to or cannot be created, * if the N5 version of the container is not compatible with * this implementation. */ public N5KeyValueWriter( final KeyValueAccess keyValueAccess, final String basePath, final GsonBuilder gsonBuilder, final boolean cacheAttributes) throws N5Exception { super(false, keyValueAccess, basePath, gsonBuilder, cacheAttributes, false); Version version = null; try { version = getVersion(); if (!VERSION.isCompatible(version)) throw new N5Exception.N5IOException("Incompatible version " + version + " (this is " + VERSION + ")."); } catch (final NullPointerException e) {} if (version == null || version.equals(new Version(0, 0, 0, ""))) { createGroup("/"); setVersion("/"); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/N5Reader.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Serializable; import java.io.UncheckedIOException; import java.lang.reflect.Type; import java.net.URI; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.LinkedBlockingQueue; import java.util.function.Predicate; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedGrid; /** * A simple structured container for hierarchies of chunked * n-dimensional datasets and attributes. * * @author Stephan Saalfeld * @see "https://github.com/axtimwalde/n5" */ public interface N5Reader extends AutoCloseable { class Version { private final int major; private final int minor; private final int patch; private final String suffix; public Version( final int major, final int minor, final int patch, final String rest) { this.major = major; this.minor = minor; this.patch = patch; this.suffix = rest; } public Version( final int major, final int minor, final int patch) { this(major, minor, patch, ""); } /** * Creates a version from a SemVer compatible version string. *

* If the version string is null or not a SemVer version, this * version will be "0.0.0" *

* * @param versionString * the string representation of the version */ public Version(final String versionString) { boolean isSemVer = false; if (versionString != null) { final Matcher matcher = Pattern.compile("(\\d+)(\\.(\\d+))?(\\.(\\d+))?(.*)").matcher(versionString); isSemVer = matcher.find(); if (isSemVer) { major = Integer.parseInt(matcher.group(1)); final String minorString = matcher.group(3); if (!minorString.equals("")) minor = Integer.parseInt(minorString); else minor = 0; final String patchString = matcher.group(5); if (!patchString.equals("")) patch = Integer.parseInt(patchString); else patch = 0; suffix = matcher.group(6); } else { major = 0; minor = 0; patch = 0; suffix = ""; } } else { major = 0; minor = 0; patch = 0; suffix = ""; } } public final int getMajor() { return major; } public final int getMinor() { return minor; } public final int getPatch() { return patch; } public final String getSuffix() { return suffix; } @Override public String toString() { final StringBuilder s = new StringBuilder(); s.append(major); s.append("."); s.append(minor); s.append("."); s.append(patch); s.append(suffix); return s.toString(); } @Override public boolean equals(final Object other) { if (other instanceof Version) { final Version otherVersion = (Version)other; return (major == otherVersion.major) & (minor == otherVersion.minor) & (patch == otherVersion.patch) & (suffix.equals(otherVersion.suffix)); } else return false; } /** * Returns true if this implementation is compatible with a given * version. * * Currently, this means that the version is less than or equal to * 1.X.X. * * @param version * the version * @return true if this version is compatible */ public boolean isCompatible(final Version version) { return version.getMajor() <= major; } } /** * SemVer version of this N5 spec. */ Version NO_VERSION = new Version(0, 0, 0); /** * SemVer version of this N5 spec. */ Version VERSION = new Version(4, 0, 0); /** * Version attribute key. */ String VERSION_KEY = "n5"; /** * Get the SemVer version of this container as specified in the 'version' * attribute of the root group. * * If no version is specified or the version string does not conform to * the SemVer format, 0.0.0 will be returned. For incomplete versions, * such as 1.2, the missing elements are filled with 0, i.e. 1.2.0 in this * case. * * @return the version * @throws N5Exception * the exception */ default Version getVersion() throws N5Exception { return new Version(getAttribute("/", VERSION_KEY, String.class)); } /** * Returns the URI of the container's root. * * @return the base path URI */ URI getURI(); /** * Reads an attribute. * * @param pathName * group path * @param key * the key * @param clazz * attribute class * @param * the attribute type * @return the attribute * @throws N5Exception * the exception */ T getAttribute( String pathName, String key, Class clazz) throws N5Exception; /** * Reads an attribute. * * @param pathName * group path * @param key * the key * @param type * attribute Type (use this for specifying generic types) * @param * the attribute type * @return the attribute * @throws N5Exception * the exception */ T getAttribute( String pathName, String key, Type type) throws N5Exception; /** * Get mandatory dataset attributes. * * @param pathName * dataset path * @return dataset attributes or null if either dimensions or dataType are * not set * @throws N5Exception * the exception */ DatasetAttributes getDatasetAttributes(String pathName) throws N5Exception; /** * Some implementations may need to convert arbitrary DatasetAttributes to their specific equivalent variant. * Ideally, this method would be `protected`, but that's not valid for the interface. The default implementation * is the identity (returns the input DatasetAttributes unchanged). *

* The returned DatasetAttributes is not guaranteed to be a unique instance. * * @param attributes * to convert * @return the converted attributes */ default DatasetAttributes getConvertedDatasetAttributes(final DatasetAttributes attributes) { return attributes; } /** * Reads a chunk as a {@link DataBlock}. * * @param * the DataBlock data type * @param pathName * dataset path * @param datasetAttributes * the dataset attributes * @param gridPosition * the grid position * @return the data block * @throws N5Exception * the exception */ DataBlock readChunk( String pathName, DatasetAttributes datasetAttributes, long... gridPosition) throws N5Exception; /** * Reads multiple chunks as {@link DataBlock}s. *

* Implementations may optimize / batch read operations when possible, e.g. * in the case that the datasets are sharded. * * @param * the DataBlock data type * @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> readChunks( final String pathName, final DatasetAttributes datasetAttributes, final List gridPositions) throws N5Exception { DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); final ArrayList> blocks = new ArrayList<>(); for( final long[] p : gridPositions ) blocks.add(readChunk(pathName, convertedDatasetAttributes, p)); return blocks; } /** * Reads a block, returning a {@link DataBlock}. Will be a chunk or shard (if the dataset is sharded). *

* A block is the highest (coarsest) level of the dataset's {@link NestedGrid} * This method's behavior is identical to {@link #readChunk} for un-sharded datasets. * * @param * the DataBlock data type * @param pathName * dataset path * @param datasetAttributes * the dataset attributes * @param gridPosition * the position in the block grid * @return the data block * @throws N5Exception * the exception * * @see DatasetAttributes#getNestedBlockGrid() */ DataBlock readBlock( String pathName, DatasetAttributes datasetAttributes, long... gridPosition) throws N5Exception; /** * Checks if a block exists at the given grid position without reading the data. *

* A block is the highest (coarsest) level of the dataset's {@link * NestedGrid}, that is, a shard if the dataset is sharded, or a chunk * otherwise. *

* This method only checks for the presence of the key value for the gridPosition, it does not * read or validate the contents. As a result, this method refers to chunks in un-sharded datasets * or shards in sharded datasets. * * @param pathName * dataset path * @param datasetAttributes * the dataset attributes * @param gridPosition * the block grid position * @return true if the block file exists * @throws N5Exception * the exception */ boolean blockExists( String pathName, DatasetAttributes datasetAttributes, long... gridPosition) throws N5Exception; /** * Load a {@link DataBlock} as a {@link Serializable}. The offset is given * in * {@link DataBlock} grid coordinates. * * @param dataset * the dataset path * @param attributes * the dataset attributes * @param * the data block type * @param gridPosition * the grid position * @return the data block * @throws N5Exception * the exception * @throws ClassNotFoundException * the class not found exception */ default T readSerializedBlock( final String dataset, final DatasetAttributes attributes, final long... gridPosition) throws N5Exception, ClassNotFoundException { DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(attributes); final DataBlock block = readChunk(dataset, convertedDatasetAttributes, gridPosition); if (block == null) return null; final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(block.getData()); try (ObjectInputStream in = new ObjectInputStream(byteArrayInputStream)) { return (T) in.readObject(); } catch (final IOException | UncheckedIOException e) { throw new N5Exception.N5IOException(e); } } /** * Test whether a group or dataset exists at a given path. * * @param pathName * group path * @return true if the path exists */ boolean exists(String pathName); /** * Test whether a dataset exists at a given path. * * @param pathName * dataset path * @return true if a dataset exists * @throws N5Exception * an exception is thrown if the existence of a dataset at the given path cannot be determined. */ default boolean datasetExists(final String pathName) throws N5Exception { return exists(pathName) && getDatasetAttributes(pathName) != null; } /** * List all groups (including datasets) in a group. * * @param pathName * group path * @return list of children * @throws N5Exception * an exception is thrown if pathName is not a valid group */ String[] list(String pathName) throws N5Exception; /** * Recursively list all groups and datasets in the given path. * Only paths that satisfy the provided filter will be included, but the * children of paths that were excluded may be included (filter does not * apply to the subtree). * * @param pathName * base group path * @param filter * filter for children to be included * @return list of child groups and datasets * @throws N5Exception * an exception is thrown if pathName is not a valid group */ default String[] deepList( final String pathName, final Predicate filter) throws N5Exception { final String groupSeparator = getGroupSeparator(); final String normalPathName = pathName .replaceAll( "(^" + groupSeparator + "*)|(" + groupSeparator + "*$)", ""); final List absolutePaths = deepList(this, normalPathName, false, filter); return absolutePaths .stream() .map(a -> a.replaceFirst(normalPathName + "(" + groupSeparator + "?)", "")) .filter(a -> !a.isEmpty()) .toArray(String[]::new); } /** * Recursively list all groups and datasets in the given path. * * @param pathName * base group path * @return list of groups and datasets * @throws N5Exception * an exception is thrown if pathName is not a valid group */ default String[] deepList(final String pathName) throws N5Exception { return deepList(pathName, a -> true); } /** * Recursively list all datasets in the given path. Only paths that satisfy the * provided filter will be included, but the children of paths that were * excluded may be included (filter does not apply to the subtree). * *

* This method delivers the same results as *

* *
	 * {@code
	 * n5.deepList(prefix, a -> {
	 * 	try {
	 * 		return n5.datasetExists(a) && filter.test(a);
	 * 	} catch (final N5Exception e) {
	 * 		return false;
	 * 	}
	 * });
	 * }
	 * 
*

* but will execute {@link #datasetExists(String)} only once per node. This can * be relevant for performance on high latency backends such as cloud stores. *

* * @param pathName base group path * @param filter filter for datasets to be included * @return list of groups * @throws N5Exception * an exception is thrown if pathName is not a valid group */ default String[] deepListDatasets( final String pathName, final Predicate filter) throws N5Exception { final String groupSeparator = getGroupSeparator(); final String normalPathName = pathName .replaceAll( "(^" + groupSeparator + "*)|(" + groupSeparator + "*$)", ""); final List absolutePaths = deepList(this, normalPathName, true, filter); return absolutePaths .stream() .map(a -> a.replaceFirst(normalPathName + "(" + groupSeparator + "?)", "")) .filter(a -> !a.isEmpty()) .toArray(String[]::new); } /** * Recursively list all including datasets in the given path. * *

* This method delivers the same results as *

* *
	 * {@code
	 * n5.deepList(prefix, a -> {
	 * 	try {
	 * 		return n5.datasetExists(a);
	 * 	} catch (final N5Exception e) {
	 * 		return false;
	 * 	}
	 * });
	 * }
	 * 
*

* but will execute {@link #datasetExists(String)} only once per node. This * can * be relevant for performance on high latency backends such as cloud * stores. *

* * @param pathName * base group path * @return list of groups * @throws N5Exception * an exception is thrown if pathName is not a valid group */ default String[] deepListDatasets(final String pathName) throws N5Exception { return deepListDatasets(pathName, a -> true); } /** * Helper method to recursively list all groups and datasets. This method is not part of the * public API and is accessible only because Java 8 does not support private * interface methods yet. *

* TODO make private when committing to Java versions newer than 8 * * @param n5 the n5 reader * @param pathName the base group path * @param datasetsOnly true if only dataset paths should be returned * @param filter a dataset filter * @return the list of all children * @throws N5Exception * an exception is thrown if pathName is not a valid group */ static ArrayList deepList( final N5Reader n5, final String pathName, final boolean datasetsOnly, final Predicate filter) throws N5Exception { final ArrayList children = new ArrayList<>(); final boolean isDataset = n5.datasetExists(pathName); final boolean passDatasetTest = datasetsOnly && !isDataset; if (!passDatasetTest && filter.test(pathName)) children.add(pathName); if (!isDataset) { final String groupSeparator = n5.getGroupSeparator(); final String[] baseChildren = n5.list(pathName); for (final String child : baseChildren) children.addAll(deepList(n5, pathName + groupSeparator + child, datasetsOnly, filter)); } return children; } /** * Recursively list all groups (including datasets) in the given group, in * parallel, using the given {@link ExecutorService}. Only paths that * satisfy * the provided filter will be included, but the children of paths that were * excluded may be included (filter does not apply to the subtree). * * @param pathName * base group path * @param filter * filter for children to be included * @param executor * executor service * @return list of datasets * @throws N5Exception * an exception is thrown if pathName is not a valid group * @throws ExecutionException * the execution exception * @throws InterruptedException * the interrupted exception */ default String[] deepList( final String pathName, final Predicate filter, final ExecutorService executor) throws N5Exception, InterruptedException, ExecutionException { final String groupSeparator = getGroupSeparator(); final String normalPathName = pathName.replaceAll("(^" + groupSeparator + "*)|(" + groupSeparator + "*$)", ""); final ArrayList results = new ArrayList(); final LinkedBlockingQueue> datasetFutures = new LinkedBlockingQueue<>(); deepListHelper(this, normalPathName, false, filter, executor, datasetFutures); while (!datasetFutures.isEmpty()) { final String result = datasetFutures.poll().get(); if (result != null && !result.equals(normalPathName)) results.add(result.substring(normalPathName.length() + groupSeparator.length())); } return results.toArray(new String[0]); } /** * Recursively list all groups (including datasets) in the given group, in * parallel, using the given {@link ExecutorService}. * * @param pathName * base group path * @param executor * executor service * @return list of groups * @throws N5Exception * an exception is thrown if pathName is not a valid group * @throws ExecutionException * the execution exception * @throws InterruptedException * this exception is thrown if execution is interrupted */ default String[] deepList( final String pathName, final ExecutorService executor) throws N5Exception, InterruptedException, ExecutionException { return deepList(pathName, a -> true, executor); } /** * Recursively list all datasets in the given group, in parallel, using the * given {@link ExecutorService}. Only paths that satisfy the provided * filter * will be included, but the children of paths that were excluded may be * included (filter does not apply to the subtree). * *

* This method delivers the same results as *

* *
	 * {@code
	 * n5.deepList(prefix, a -> {
	 * 	try {
	 * 		return n5.datasetExists(a) && filter.test(a);
	 * 	} catch (final N5Exception e) {
	 * 		return false;
	 * 	}
	 * }, exec);
	 * }
	 * 
*

* but will execute {@link #datasetExists(String)} only once per node. This * can * be relevant for performance on high latency backends such as cloud * stores. *

* * @param pathName * base group path * @param filter * filter for datasets to be included * @param executor * executor service * @return list of datasets * @throws N5Exception * an exception is thrown if pathName is not a valid group * @throws ExecutionException * the execution exception * @throws InterruptedException * this exception is thrown if execution is interrupted */ default String[] deepListDatasets( final String pathName, final Predicate filter, final ExecutorService executor) throws N5Exception, InterruptedException, ExecutionException { final String groupSeparator = getGroupSeparator(); final String normalPathName = pathName.replaceAll("(^" + groupSeparator + "*)|(" + groupSeparator + "*$)", ""); final ArrayList results = new ArrayList(); final LinkedBlockingQueue> datasetFutures = new LinkedBlockingQueue<>(); deepListHelper(this, normalPathName, true, filter, executor, datasetFutures); datasetFutures.poll().get(); // skip self while (!datasetFutures.isEmpty()) { final String result = datasetFutures.poll().get(); if (result != null) results.add(result.substring(normalPathName.length() + groupSeparator.length())); } return results.toArray(new String[0]); } /** * Recursively list all datasets in the given group, in parallel, using the * given {@link ExecutorService}. * *

* This method delivers the same results as *

* *
	 * {@code
	 * n5.deepList(prefix, a -> {
	 * 	try {
	 * 		return n5.datasetExists(a);
	 * 	} catch (final N5Exception e) {
	 * 		return false;
	 * 	}
	 * }, exec);
	 * }
	 * 
*

* but will execute {@link #datasetExists(String)} only once per node. This * can * be relevant for performance on high latency backends such as cloud * stores. *

* * @param pathName * base group path * @param executor * executor service * @return list of groups * @throws N5Exception * an exception is thrown if pathName is not a valid group * @throws ExecutionException * the execution exception * @throws InterruptedException * this exception is thrown if execution is interrupted */ default String[] deepListDatasets( final String pathName, final ExecutorService executor) throws N5Exception, InterruptedException, ExecutionException { return deepListDatasets(pathName, a -> true, executor); } /** * Helper method for parallel deep listing. This method is not part of the * public API and is accessible only because Java 8 does not support private * interface methods yet. * * TODO make private when committing to Java versions newer than 8 * * @param n5 * the n5 reader * @param path * the base path * @param datasetsOnly * true if only dataset paths should be returned * @param filter * filter for datasets to be included * @param executor * the executor service * @param datasetFutures * result futures */ static void deepListHelper( final N5Reader n5, final String path, final boolean datasetsOnly, final Predicate filter, final ExecutorService executor, final LinkedBlockingQueue> datasetFutures) { final String groupSeparator = n5.getGroupSeparator(); datasetFutures.add(executor.submit(() -> { boolean isDataset = false; try { isDataset = n5.datasetExists(path); } catch (final N5Exception e) {} if (!isDataset) { String[] children = null; try { children = n5.list(path); for (final String child : children) { final String fullChildPath = path + groupSeparator + child; deepListHelper(n5, fullChildPath, datasetsOnly, filter, executor, datasetFutures); } } catch (final N5Exception e) {} } final boolean passDatasetTest = datasetsOnly && !isDataset; return !passDatasetTest && filter.test(path) ? path : null; })); } /** * List all attributes and their class of a group. * * @param pathName * group path * @return a map of attribute keys to their inferred class * @throws N5Exception if an error occurred during listing */ Map> listAttributes(String pathName) throws N5Exception; /** * Returns the symbol that is used to separate nodes in a group path. * * @return the group separator */ default String getGroupSeparator() { return "/"; } /** * Creates a group path by concatenating all nodes with the node separator * defined by {@link #getGroupSeparator()}. The string will not have a * leading or trailing node separator symbol. * * @param nodes a collection of child node names * @return the full group path */ default String groupPath(final String... nodes) { if (nodes == null || nodes.length == 0) return ""; final String groupSeparator = getGroupSeparator(); final StringBuilder builder = new StringBuilder(nodes[0]); for (int i = 1; i < nodes.length; ++i) { builder.append(groupSeparator); builder.append(nodes[i]); } return builder.toString(); } /** * Default implementation of {@link AutoCloseable#close()} for all * implementations that do not hold any closable resources. */ @Override default void close() {} } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/N5URI.java ================================================ package org.janelia.saalfeldlab.n5; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.charset.Charset; import java.nio.charset.CharsetDecoder; import java.nio.charset.CoderResult; import java.nio.charset.CodingErrorAction; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A {@link URI} for N5 containers, groups, datasets, and attributes. *

* Container paths are stored in the URI path. Group / dataset paths are stored in the URI query, * and attribute paths are stored in the URI fragment. */ public class N5URI { private static final Charset UTF8 = StandardCharsets.UTF_8; public static final Pattern ARRAY_INDEX = Pattern.compile("\\[([0-9]+)]"); final URI uri; private final String scheme; private final String container; private final String group; private final String attribute; public N5URI(final String uri) throws URISyntaxException { this(encodeAsUri(uri)); } public N5URI(final URI uri) { this.uri = uri; scheme = uri.getScheme() == null ? null : uri.getScheme(); final String schemeSpecificPartWithoutQuery = getSchemeSpecificPartWithoutQuery(); if (uri.getScheme() == null) { container = schemeSpecificPartWithoutQuery.replaceFirst("//", ""); } else { container = uri.getScheme() + ":" + schemeSpecificPartWithoutQuery; } group = uri.getQuery(); attribute = decodeFragment(uri.getRawFragment()); } /** * @return the container path */ public String getContainerPath() { return container; } public URI getURI() { return uri; } /** * @return the group path, or root ("/") if none was provided */ public String getGroupPath() { return group != null ? group : "/"; } /** * @return the normalized group path */ public String normalizeGroupPath() { return normalizeGroupPath(getGroupPath()); } /** * @return the attribute path, or root ("/") if none was provided */ public String getAttributePath() { return attribute != null ? attribute : "/"; } /** * @return the normalized attribute path */ public String normalizeAttributePath() { return normalizeAttributePath(getAttributePath()); } /** * Parse this {@link N5URI} as a {@link LinkedAttributePathToken}. * * @see N5URI#getAttributePathTokens(String) * @return the linked attribute path token */ public LinkedAttributePathToken getAttributePathTokens() { return getAttributePathTokens(normalizeAttributePath()); } /** * Parses the {@link String normalizedAttributePath} to a list of * {@link LinkedAttributePathToken}. * This is useful for traversing or constructing a json representation of * the provided {@link String normalizedAttributePath}. * Note that {@link String normalizedAttributePath} should be normalized * prior to generating this list * * @param normalizedAttributePath * to parse into {@link LinkedAttributePathToken}s * @return the head of the {@link LinkedAttributePathToken}s */ public static LinkedAttributePathToken getAttributePathTokens(final String normalizedAttributePath) { final String[] attributePathParts = normalizedAttributePath.replaceAll("^/", "").split("(?> firstTokenRef = new AtomicReference<>(); final AtomicReference> currentTokenRef = new AtomicReference<>(); final Consumer> updateCurrentToken = (newToken) -> { if (firstTokenRef.get() == null) { firstTokenRef.set(newToken); currentTokenRef.set(firstTokenRef.get()); } else { final LinkedAttributePathToken currentToken = currentTokenRef.get(); currentToken.childToken = newToken; currentTokenRef.set(newToken); } }; for (final String pathPart : attributePathParts) { final Matcher matcher = ARRAY_INDEX.matcher(pathPart); final LinkedAttributePathToken newToken; if (matcher.matches()) { final int index = Integer.parseInt(matcher.group().replace("[", "").replace("]", "")); newToken = new LinkedAttributePathToken.ArrayAttributeToken(index); } else { final String pathPartUnEscaped = pathPart.replaceAll("\\\\/", "/").replaceAll("\\\\\\[", "["); newToken = new LinkedAttributePathToken.ObjectAttributeToken(pathPartUnEscaped); } updateCurrentToken.accept(newToken); } return firstTokenRef.get(); } private String getSchemePart() { return scheme == null ? "" : scheme + "://"; } private String getContainerPart() { return container; } private String getGroupPart() { return group == null ? "" : "?" + group; } private String getAttributePart() { return attribute == null ? "" : "#" + attribute; } @Override public String toString() { return getContainerPart() + getGroupPart() + getAttributePart(); } private String getSchemeSpecificPartWithoutQuery() { /* Why not substring "?"? */ return uri.getSchemeSpecificPart().replace("?" + uri.getQuery(), ""); } /** * N5URI is always considered absolute if a scheme is provided. * If no scheme is provided, the N5URI is absolute if it starts with either * "/" or "[A-Z]:" * * @return if the path for this N5URI is absolute */ public boolean isAbsolute() { if (scheme != null) return true; final String path = uri.getPath(); if (!path.isEmpty()) { final char char0 = path.charAt(0); return char0 == '/' || (path.length() >= 2 && path.charAt(1) == ':' && char0 >= 'A' && char0 <= 'Z'); } return false; } /** * Generate a new N5URI which is the result of resolving {@link N5URI * relativeN5Url} to this {@link N5URI}. * If relativeN5Url is not relative to this N5URI, then the resulting N5URI * is equivalent to relativeN5Url. * * @param relativeN5Url * N5URI to resolve against ourselves * @return the result of the resolution. * @throws URISyntaxException * if the uri is malformed */ public N5URI resolve(final N5URI relativeN5Url) throws URISyntaxException { final URI thisUri = uri; final URI relativeUri = relativeN5Url.uri; final StringBuilder newUri = new StringBuilder(); if (relativeUri.getScheme() != null) { return relativeN5Url; } final String thisScheme = thisUri.getScheme(); if (thisScheme != null) { newUri.append(thisScheme).append(":"); } if (relativeUri.getAuthority() != null) { newUri .append(relativeUri.getAuthority()) .append(relativeUri.getPath()) .append(relativeN5Url.getGroupPart()) .append(relativeN5Url.getAttributePart()); return new N5URI(newUri.toString()); } final String thisAuthority = thisUri.getAuthority(); if (thisAuthority != null) { newUri.append("//").append(thisAuthority); } final String path = relativeUri.getPath(); if (!path.isEmpty()) { if (!relativeN5Url.isAbsolute()) { newUri.append(thisUri.getPath()).append('/'); } newUri .append(path) .append(relativeN5Url.getGroupPart()) .append(relativeN5Url.getAttributePart()); return new N5URI(newUri.toString()); } newUri.append(thisUri.getPath()); final String query = relativeUri.getQuery(); if (query != null) { if (query.charAt(0) != '/' && thisUri.getQuery() != null) { newUri.append(this.getGroupPart()).append('/'); newUri.append(relativeUri.getQuery()); } else { newUri.append(relativeN5Url.getGroupPart()); } newUri.append(relativeN5Url.getAttributePart()); return new N5URI(newUri.toString()); } newUri.append(this.getGroupPart()); final String fragment = relativeUri.getFragment(); if (fragment != null) { if (fragment.charAt(0) != '/' && thisUri.getFragment() != null) { newUri.append(this.getAttributePart()).append('/'); } else { newUri.append(relativeN5Url.getAttributePart()); } return new N5URI(newUri.toString()); } newUri.append(this.getAttributePart()); return new N5URI(newUri.toString()); } /** * Generate a new N5URI which is the result of resolving {@link URI * relativeUri} to this {@link N5URI}. * If relativeUri is not relative to this N5URI, then the resulting N5URI is * equivalent to relativeUri. * * @param relativeUri * URI to resolve against ourselves * @return the result of the resolution. * @throws URISyntaxException * if the uri is malformed */ public N5URI resolve(final URI relativeUri) throws URISyntaxException { return resolve(new N5URI(relativeUri)); } /** * Generate a new N5URI which is the result of resolving {@link String * relativeString} to this {@link N5URI} * If relativeString is not relative to this N5URI, then the resulting N5URI * is equivalent to relativeString. * * @param relativeString * String to resolve against ourselves * @return the result of the resolution. * @throws URISyntaxException * if the uri is malformed */ public N5URI resolve(final String relativeString) throws URISyntaxException { return resolve(new N5URI(relativeString)); } /** * Normalize a POSIX path, resulting in removal of redundant "/", "./", and * resolution of relative "../". *

* NOTE: currently a private helper method only used by {@link N5URI#normalizeGroupPath(String)}. * It's safe to do in that case since relative group paths should always be POSIX compliant. * A new helper method to understand other path types (e.g. Windows) may be necessary eventually. * * @param path * to normalize * @return the normalized path */ private static String normalizePath(String path) { path = path == null ? "" : path; final char[] pathChars = path.toCharArray(); final List tokens = new ArrayList<>(); final StringBuilder curToken = new StringBuilder(); boolean escape = false; for (final char character : pathChars) { /* Skip if we last saw escape */ if (escape) { escape = false; curToken.append(character); continue; } /* Check if we are escape character */ if (character == '\\') { escape = true; } else if (character == '/') { if (tokens.isEmpty() && curToken.length() == 0) { /* If we are root, and the first token, then add the '/' */ curToken.append(character); } /* * The current token is complete, add it to the list, if it * isn't empty */ final String newToken = curToken.toString(); if (!newToken.isEmpty()) { /* * If our token is '..' then remove the last token instead * of adding a new one */ if (newToken.equals("..")) { tokens.remove(tokens.size() - 1); } else { tokens.add(newToken); } } /* reset for the next token */ curToken.setLength(0); } else { curToken.append(character); } } final String lastToken = curToken.toString(); if (!lastToken.isEmpty()) { if (lastToken.equals("..")) { tokens.remove(tokens.size() - 1); } else { tokens.add(lastToken); } } if (tokens.isEmpty()) return ""; String root = ""; if (tokens.get(0).equals("/")) { tokens.remove(0); root = "/"; } return root + tokens .stream() .filter(it -> !it.equals(".")) .filter(it -> !it.isEmpty()) .reduce((l, r) -> l + "/" + r) .orElse(""); } /** * Normalize a group path relative to a container's root, resulting in * removal of redundant "/", "./", resolution of relative "../", * and removal of leading slashes. * * @param path * to normalize * @return the normalized path */ public static String normalizeGroupPath(final String path) { /* * Alternatively, could do something like the below in every * KeyValueReader implementation * * return keyValueAccess.relativize( N5URI.normalizeGroupPath(path), * basePath); * * has to be in the implementations, since KeyValueAccess doesn't have a * basePath. */ return normalizePath(path.startsWith("/") || path.startsWith("\\") ? path.substring(1) : path); } private enum N5UriPattern { /** * matches any `/` or `[N]` where `N` is non-negative */ MULTI_PART_ATTRIBUTE(Pattern.compile(".*((?\\[[0-9]+])")), /** * matches `A[N]` where A is some non-empty preceding path */ ATTRIBUTE_ARRAY_EXCEPT_START(Pattern.compile("((?\\[[0-9]+]))")), /** * The following Pattern has 4 possible matches. * It is intended to be used to remove matching portions iteratively until no further matches are found: *

* The first 3 matches can remove redundant separators of the form: *

    *
  • (?<=/)/+ : `a///b` -> `a/b`
  • *
  • (?<=(/|^))(\./)+ : `a/./b` -> `a/b`
  • *
  • ((/|(?<=/))\.)$ : `a/b/` -> `a/b`
  • *
* The next match avoids removing `/` when it is NOT redundant (e.g. only character, or escaped): *
    *
  • (? `/ , `/a/b/\\/` -> `/a/b/\\/`
  • *
* The last match resolves relative paths: *
    *
  • 5. ((?<=^/)|^|(?<=(/|^))[^/]+(? *
      *
    • `a/../b` -> `b`
    • *
    • `/a/../b` -> `/b`
    • *
    • `../a/../b` -> `b`
    • *
    • `/../a/../b` -> `/b`
    • *
    • `/../a/../../b` -> `/b`
    • *
    *
*

*/ RELATIVE_ATTRIBUTE_PARTS(Pattern.compile( "((?<=/)/+|(?<=(/|^))(\\./)+|((/|(?<=/))\\.)$|(? * Attribute paths have a few special characters: *

    *
  • "." which represents the current element
  • *
  • ".." which represent the previous elemnt
  • *
  • "/" which is used to separate elements in the json tree
  • *
  • [N] where N is an integer, refer to an index in the previous element * in the tree; the previous element must be an array. *

    * Note: [N] also separates the previous and following elements, regardless * of whether it is preceded by "/" or not. *

  • *
  • "\" which is an escape character, which indicates the subquent '/' or * '[N]' should not be interpreted as a path delimeter, * but as part of the current path name.
  • * *
*

* When normalizing: *

    *
  • "/" are added before and after any indexing brackets [N]
  • *
  • any redundant "/" are removed
  • *
  • any relative ".." and "." are resolved
  • *
*

* Examples of valid attribute paths, and their normalizations *

    *
  • /a/b/c becomes /a/b/c
  • *
  • /a/b/c/ becomes /a/b/c
  • *
  • ///a///b///c becomes /a/b/c
  • *
  • /a/././b/c becomes /a/b/c
  • *
  • /a/b[1]c becomes /a/b/[1]/c
  • *
  • /a/b/[1]c becomes /a/b/[1]/c
  • *
  • /a/b[1]/c becomes /a/b/[1]/c
  • *
  • /a/b[1]/c/.. becomes /a/b/[1]
  • *
* * @param attributePath * to normalize * @return the normalized attribute path */ public static String normalizeAttributePath(final String attributePath) { /* * Short circuit if there are no non-escaped `/` or array indices (e.g. * [N] where N is a non-negative integer) */ if (!N5UriPattern.MULTI_PART_ATTRIBUTE.matches(attributePath)) return attributePath; /* Add separator after arrays at the beginning `[10]b` -> `[10]/b` */ final String attrPathPlusFirstIndexSeparator = N5UriPattern.appendSlashAfterArrayStart(attributePath); /* * Add separator before and after arrays not at the beginning `a[10]b` * -> `a/[10]/b` */ final String attrPathPlusIndexSeparators = N5UriPattern.addSlashAroundArrayExceptStart(attrPathPlusFirstIndexSeparator); /* remove relative path parts like `./`, `../`, `a//b, `a/b/` */ return N5UriPattern.removeRelativePathParts(attrPathPlusIndexSeparators); } /** * If uri is a valid URI, just return it as a URI. Else, encode if possible. * * @param uri as String to get as URI * @return URI from input. Encoded if necessary */ public static URI getAsUri(final String uri) throws N5Exception { try { return URI.create(uri); } catch (Exception ignore) { try { return N5URI.encodeAsUri(uri); } catch (URISyntaxException e) { throw new N5Exception("Could not encode as URI: " + uri, e); } } } public static URI encodeAsUriPath(final String path) { try { return new URI(null, null, path, null); } catch (Exception e) { throw new IllegalArgumentException("Could not encode as URI path component: " + path, e); } } /** * Encode the inpurt {@link String uri} so that illegal characters are * properly escaped prior to generating the resulting {@link URI}. * * @param uri * to encode * @return the {@link URI} created from encoding the {@link String uri} * @throws URISyntaxException * if the provided String is not a valid URI */ public static URI encodeAsUri(final String uri) throws URISyntaxException { if (uri.trim().length() == 0) { //TODO Caleb: ??? return new URI(uri); } /* * find last # symbol to split fragment on. If we don't remove it first, * then it will encode it, and not parse it separately * after we remove the temporary _N5 scheme */ final int fragmentIdx = uri.lastIndexOf('#'); final String uriWithoutFragment; final String fragment; if (fragmentIdx >= 0) { uriWithoutFragment = uri.substring(0, fragmentIdx); fragment = uri.substring(fragmentIdx + 1); } else { uriWithoutFragment = uri; fragment = null; } /* Edge case to handle when uriWithoutFragment is empty */ final URI _n5Uri; if (uriWithoutFragment.length() == 0 && fragment != null && fragment.length() > 0) { _n5Uri = new URI("N5Internal", "//STAND_IN", fragment); } else { _n5Uri = new URI("N5Internal", uriWithoutFragment, fragment); } final URI n5Uri; if (fragment == null) { n5Uri = new URI(_n5Uri.getRawSchemeSpecificPart()); } else { if (Objects.equals(_n5Uri.getPath(), "") && Objects.equals(_n5Uri.getAuthority(), "STAND_IN")) { n5Uri = new URI("#" + _n5Uri.getRawFragment()); } else { n5Uri = new URI(_n5Uri.getRawSchemeSpecificPart() + "#" + _n5Uri.getRawFragment()); } } return n5Uri; } /** * Generate an {@link N5URI} from a container, group, and attribute * * @param container * of the N5Url * @param group * of the N5Url * @param attribute * of the N5Url * @return the {@link N5URI} * @throws URISyntaxException * if the uri is malformed */ public static N5URI from( final String container, final String group, final String attribute) throws URISyntaxException { final String containerPart = container != null ? container : ""; final String groupPart = group != null ? "?" + group : ""; final String attributePart = attribute != null ? "#" + attribute : ""; return new N5URI(containerPart + groupPart + attributePart); } /** * Intentionally copied from {@link URI} for internal use * * @see URI#decode(char) */ @SuppressWarnings("JavadocReference") private static int decode(final char c) { if ((c >= '0') && (c <= '9')) return c - '0'; if ((c >= 'a') && (c <= 'f')) return c - 'a' + 10; if ((c >= 'A') && (c <= 'F')) return c - 'A' + 10; assert false; return -1; } /** * Intentionally copied from {@link URI} for internal use * * @see URI#decode(char, char) */ @SuppressWarnings("JavadocReference") private static byte decode(final char c1, final char c2) { return (byte)(((decode(c1) & 0xf) << 4) | ((decode(c2) & 0xf) << 0)); } /** * Modified from {@link URI#decode(String)} to ignore the listed exception, * where it doesn't decode escape values inside square braces. *

* As an example of the original implementation, a backslash inside a square * brace would be encoded to "[%5C]", and * when calling {@code decode("[%5C]")} it would not decode to "[\]" since * the encode escape sequence is inside square braces. *

* We keep all the decoding logic in this modified version, EXCEPT, that we * don't check for and ignore encoded sequences inside square braces. *

* Thus, {@code decode("[%5C]")} -> "[\]". * * @see URI#decode(char, char) */ @SuppressWarnings("JavadocReference") private static String decodeFragment(final String rawFragment) { if (rawFragment == null) return rawFragment; final int n = rawFragment.length(); if (n == 0) return rawFragment; if (rawFragment.indexOf('%') < 0) return rawFragment; final StringBuffer sb = new StringBuffer(n); final ByteBuffer bb = ByteBuffer.allocate(n); final CharBuffer cb = CharBuffer.allocate(n); final CharsetDecoder dec = UTF8 .newDecoder() .onMalformedInput(CodingErrorAction.REPLACE) .onUnmappableCharacter(CodingErrorAction.REPLACE); // This is not horribly efficient, but it will do for now char c = rawFragment.charAt(0); for (int i = 0; i < n;) { assert c == rawFragment.charAt(i); // Loop invariant if (c != '%') { sb.append(c); if (++i >= n) break; c = rawFragment.charAt(i); continue; } bb.clear(); for (;;) { assert (n - i >= 2); bb.put(decode(rawFragment.charAt(++i), rawFragment.charAt(++i))); if (++i >= n) break; c = rawFragment.charAt(i); if (c != '%') break; } bb.flip(); cb.clear(); dec.reset(); CoderResult cr = dec.decode(bb, cb, true); assert cr.isUnderflow(); cr = dec.flush(cb); assert cr.isUnderflow(); sb.append(cb.flip()); } return sb.toString(); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/N5Writer.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectOutputStream; import java.io.Serializable; import java.io.UncheckedIOException; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedGrid; /** * A simple structured container API for hierarchies of chunked * n-dimensional datasets and attributes. * * @author Stephan Saalfeld * @see "https://github.com/axtimwalde/n5" */ public interface N5Writer extends N5Reader { /** * Sets an attribute. * * @param groupPath group path * @param attributePath the key * @param attribute the attribute * @param the attribute type * @throws N5Exception the exception */ default void setAttribute( final String groupPath, final String attributePath, final T attribute) throws N5Exception { setAttributes(groupPath, Collections.singletonMap(attributePath, attribute)); } /** * Sets a map of attributes. The passed attributes are inserted into the * existing attribute tree. New attributes, including their parent * objects will be added, existing attributes whose paths are not included * will remain unchanged, those whose paths are included will be overridden. * * @param groupPath group path * @param attributes the attribute map of attribute paths and values * @throws N5Exception the exception */ void setAttributes( String groupPath, Map attributes) throws N5Exception; /** * Remove the attribute from group {@code pathName} with key {@code key}. * * @param groupPath group path * @param attributePath of attribute to remove * @return true if attribute removed, else false * @throws N5Exception the exception */ boolean removeAttribute(String groupPath, String attributePath) throws N5Exception; /** * Remove the attribute from group {@code pathName} with key {@code key} and * type {@code T}. *

* If an attribute at {@code pathName} and {@code key} exists, but is not of * type {@code T}, it is not removed. * * @param groupPath group path * @param attributePath of attribute to remove * @param clazz of the attribute to remove * @param of the attribute * @return the removed attribute, as {@code T}, or {@code null} if no * matching attribute * @throws N5Exception if removing he attribute failed, parsing the attribute failed, or the attribute cannot be interpreted as T */ T removeAttribute(String groupPath, String attributePath, Class clazz) throws N5Exception; /** * Remove attributes as provided by {@code attributes}. *

* If any element of {@code attributes} does not exist, it will be ignored. * If at least one attribute from {@code attributes} is removed, this will * return {@code true}. * * * @param groupPath group path * @param attributePaths to remove * @return true if any of the listed attributes were removed * @throws N5Exception the exception */ default boolean removeAttributes(final String groupPath, final List attributePaths) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(groupPath); boolean removed = false; for (final String attribute : attributePaths) { removed |= removeAttribute(normalPath, N5URI.normalizeAttributePath(attribute)); } return removed; } /** * Sets mandatory dataset attributes. * * @param datasetPath dataset path * @param datasetAttributes the dataset attributes * @throws N5Exception the exception */ default void setDatasetAttributes( final String datasetPath, final DatasetAttributes datasetAttributes) throws N5Exception { setAttributes(datasetPath, getConvertedDatasetAttributes(datasetAttributes).asMap()); } /** * Set the SemVer version of this container as specified in the * {@link N5Reader#VERSION_KEY} attribute of the root group. This default * implementation writes the version only if the current version is not * equal {@link N5Reader#VERSION}. * * @throws N5Exception the exception */ default void setVersion() throws N5Exception { if (!VERSION.equals(getVersion())) setAttribute("/", VERSION_KEY, VERSION.toString()); } /** * Creates a group (directory) * * @param groupPath the path * @throws N5Exception the exception */ void createGroup(String groupPath) throws N5Exception; /** * Removes a group or dataset (directory and all contained files). * *

* {@link #remove(String) remove("")} or * {@link #remove(String) remove("")} will delete this N5 * container. Please note that no checks for safety will be performed, * e.g. {@link #remove(String) remove("..")} will try to * recursively delete the parent directory of this N5 container which * only fails because it attempts to delete the parent directory before it * is empty. * * @param groupPath group path * @return true if removal was successful, false otherwise * @throws N5Exception the exception */ boolean remove(String groupPath) throws N5Exception; /** * Removes the N5 container. * * @return true if removal was successful, false otherwise * @throws N5Exception the exception */ default boolean remove() throws N5Exception { return remove("/"); } /** * Creates a dataset. This does not create any data but the path and * mandatory attributes only. The returned DatasetAttributes should be used * for future read/write operations on this dataset. It may not be the same * DatasetAttributes object that was provided, depending on the implementation. * * @param datasetPath dataset path * @param datasetAttributes the dataset attributes * @return DatasetAttributes optimal attributes object to be used for read/write operations * @throws N5Exception */ default DatasetAttributes createDataset( final String datasetPath, final DatasetAttributes datasetAttributes) throws N5Exception { final String normalPath = N5URI.normalizeGroupPath(datasetPath); createGroup(normalPath); final DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); setDatasetAttributes(normalPath, convertedDatasetAttributes); return convertedDatasetAttributes; } /** * Creates a dataset. This does not create any data but the path and * mandatory attributes only. Returns the DatasetAttributes object to be * used for future read/write operations on this dataset. * * @param datasetPath dataset path * @param dimensions the dataset dimensions * @param blockSize the block size * @param dataType the data type * @param compression the compression * @return DatasetAttributes optimal attributes object to be used for read/write operations * @throws N5Exception */ default DatasetAttributes createDataset( final String datasetPath, final long[] dimensions, final int[] blockSize, final DataType dataType, final Compression compression) throws N5Exception { return createDataset(datasetPath, new DatasetAttributes(dimensions, blockSize, dataType, compression)); } /** * Writes a chunk represented by a {@link DataBlock}. * * @param datasetPath dataset path * @param datasetAttributes the dataset attributes * @param chunk the chunk as a DataBlock * @param the data block data type * @throws N5Exception the exception */ void writeChunk( String datasetPath, DatasetAttributes datasetAttributes, DataBlock chunk) throws N5Exception; /** * Write multiple chunks represented by {@link DataBlock}s, useful for aggregation. * * @param datasetPath dataset path * @param datasetAttributes the dataset attributes * @param chunks the chunks * @param the data block data type * @throws N5Exception the exception */ default void writeChunks( final String datasetPath, final DatasetAttributes datasetAttributes, final DataBlock... chunks) throws N5Exception { // default method is naive DatasetAttributes convertedAttributes = getConvertedDatasetAttributes(datasetAttributes); for (DataBlock block : chunks) { writeChunk(datasetPath, convertedAttributes, block); } } /** * Writes a block stored as a {@link DataBlock}. *

* A block is the highest (coarsest) level of the dataset's {@link NestedGrid}, * that is, a shard if the dataset is sharded, or a chunk otherwise. *

* This method's behavior is identical to {@link #writeChunk} for un-sharded datasets. * * @param pathName dataset path * @param datasetAttributes the dataset attributes * @param dataBlock the data block * @param the data block data type * @throws N5Exception the exception * * @see DatasetAttributes#getNestedBlockGrid() */ void writeBlock( String pathName, DatasetAttributes datasetAttributes, DataBlock dataBlock) throws N5Exception; @FunctionalInterface interface DataBlockSupplier { /** * * @param gridPos * @param existingDataBlock * existing data to be merged into the new data block (may be {@code null}) * * @return data block at the given gridPos */ DataBlock get(long[] gridPos, DataBlock existingDataBlock); } /** * @param datasetPath the dataset path * @param datasetAttributes the dataset attributes * @param min min pixel coordinate of region to write * @param size size in pixels of region to write * @param chunkSupplier is asked to create chunks within the given region * @param writeFully if false, merge existing data in shards/blocks that overlap the region boundary. if true, override everything. * @throws N5Exception the exception */ void writeRegion( String datasetPath, DatasetAttributes datasetAttributes, long[] min, long[] size, DataBlockSupplier chunkSupplier, boolean writeFully) throws N5Exception; /** * @param datasetPath the dataset path * @param datasetAttributes the dataset attributes * @param min min pixel coordinate of region to write * @param size size in pixels of region to write * @param chunkSupplier is asked to create chunks within the given region * @param writeFully if false, merge existing data in shards/chunks that overlap the region boundary. if true, override everything. * @param exec used to parallelize over blocks (chunks and shards) * @throws N5Exception the exception */ void writeRegion( String datasetPath, DatasetAttributes datasetAttributes, long[] min, long[] size, DataBlockSupplier chunkSupplier, boolean writeFully, ExecutorService exec) throws N5Exception, InterruptedException, ExecutionException; /** * Deletes the block at {@code gridPosition}. *

* A block is the highest (coarsest) level of the dataset's {@link NestedGrid}, * that is, a shard if the dataset is sharded, or a chunk otherwise. * Note that {@code gridPosition} is in units of blocks at the highest * (coarsest) level, too. *

* This method's behavior is identical to {@link #deleteChunk} for un-sharded datasets. * * @param datasetPath dataset path * @param gridPosition position of block to be deleted * @throws N5Exception if the block exists but could not be deleted * * @return {@code true} if the block at {@code gridPosition} existed and was deleted. */ default boolean deleteBlock( final String datasetPath, final long... gridPosition) throws N5Exception { final DatasetAttributes datasetAttributes = getDatasetAttributes(datasetPath); return deleteBlock(datasetPath, datasetAttributes, gridPosition); } /** * Deletes the block at {@code gridPosition}. *

* A block is the highest (coarsest) level of the dataset's {@link NestedGrid}, * that is, a shard if the dataset is sharded, or a chunk otherwise. * Note that {@code gridPosition} is in units of blocks at the highest * (coarsest) level, too. *

* This method's behavior is identical to {@link #deleteChunk} for un-sharded datasets. * * @param datasetPath the dataset path * @param datasetAttributes the dataset attributes * @param gridPosition position of block to be deleted * @throws N5Exception if the block exists but could not be deleted * * @return {@code true} if the block at {@code gridPosition} existed and was deleted. */ boolean deleteBlock( String datasetPath, DatasetAttributes datasetAttributes, long... gridPosition) throws N5Exception; /** * Deletes the chunk at {@code gridPosition}. *

* Note that {@code gridPosition} is in units of chunks. * * @param datasetPath dataset path * @param gridPosition position of chunk to be deleted * @throws N5Exception if the chunk exists but could not be deleted * * @return {@code true} if the chunk at {@code gridPosition} existed and was deleted. */ default boolean deleteChunk( final String datasetPath, final long... gridPosition) throws N5Exception { final DatasetAttributes datasetAttributes = getDatasetAttributes(datasetPath); return deleteChunk(datasetPath, datasetAttributes, gridPosition); } /** * Deletes the chunk at {@code gridPosition}. *

* Note that {@code gridPosition} is in units of chunks. * * @param datasetPath the dataset path * @param datasetAttributes the dataset attributes * @param gridPosition position of chunk to be deleted * @throws N5Exception if the chunk exists but could not be deleted * * @return {@code true} if the chunk at {@code gridPosition} existed and was deleted. */ boolean deleteChunk( String datasetPath, DatasetAttributes datasetAttributes, long... gridPosition) throws N5Exception; /** * Deletes the chunks at the given {@code gridPositions}. *

* Note that {@code gridPositions} are in units of chunks. * * @param datasetPath dataset path * @param gridPositions a list of grid positions * @return {@code true} if any of the specified chunks existed and was deleted * @throws N5Exception if any of the chunks did exist but could not be deleted */ default boolean deleteChunks( final String datasetPath, final DatasetAttributes datasetAttributes, final List gridPositions) throws N5Exception { boolean deleted = false; for (long[] pos : gridPositions) { deleted |= deleteChunk(datasetPath, datasetAttributes, pos); } return deleted; } /** * Save a {@link Serializable} as an N5 {@link DataBlock} at a given offset. * The offset is given in {@link DataBlock} grid coordinates. * * @param object the object to serialize * @param datasetPath the dataset path * @param datasetAttributes the dataset attributes * @param gridPosition the grid position * @throws N5Exception the exception */ default void writeSerializedBlock( final Serializable object, final String datasetPath, final DatasetAttributes datasetAttributes, final long... gridPosition) throws N5Exception { final ByteArrayOutputStream byteOutputStream = new ByteArrayOutputStream(); try (ObjectOutputStream out = new ObjectOutputStream(byteOutputStream)) { out.writeObject(object); } catch (final IOException | UncheckedIOException e) { throw new N5Exception.N5IOException(e); } final byte[] bytes = byteOutputStream.toByteArray(); final DataBlock dataBlock = new ByteArrayDataBlock(null, gridPosition, bytes); writeChunk(datasetPath, datasetAttributes, dataBlock); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/NameConfigAdapter.java ================================================ 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 * * @param * the class this adapter (de)serializes */ 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 NameConfig.Serialize serialize = clazz.getAnnotation(NameConfig.Serialize.class); if (serialize != null && !serialize.value()) continue; 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"); e.printStackTrace(System.err); } } } 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) { new RuntimeException("Could not serialize " + clazz.getName(), 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 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); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/RawCompression.java ================================================ package org.janelia.saalfeldlab.n5; import org.janelia.saalfeldlab.n5.Compression.CompressionType; import org.janelia.saalfeldlab.n5.codec.DeterministicSizeDataCodec; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.serialization.NameConfig; @CompressionType("raw") @NameConfig.Name("raw") public class RawCompression implements Compression, DeterministicSizeDataCodec { private static final long serialVersionUID = 7526445806847086477L; @Override public boolean equals(final Object other) { return other != null && other.getClass() == RawCompression.class; } @Override public ReadData encode(final ReadData readData) { return readData; } @Override public ReadData decode(final ReadData readData) { return readData; } @Override public long encodedSize(final long size) { return size; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/ReflectionUtils.java ================================================ package org.janelia.saalfeldlab.n5; import java.lang.reflect.Field; import java.lang.reflect.Modifier; class ReflectionUtils { static void setFieldValue( final Object object, final String fieldName, final T value) throws NoSuchFieldException, IllegalAccessException { Field modifiersField; boolean isModifiersAccessible; try { modifiersField = Field.class.getDeclaredField("modifiers"); isModifiersAccessible = modifiersField.isAccessible(); modifiersField.setAccessible(true); } catch (final NoSuchFieldException e) { // Java 11+ does not allow to access modifiers modifiersField = null; isModifiersAccessible = false; } final Field field = object.getClass().getDeclaredField(fieldName); final boolean isFieldAccessible = field.isAccessible(); field.setAccessible(true); if (modifiersField != null) { final int modifiers = field.getModifiers(); modifiersField.setInt(field, modifiers & ~Modifier.FINAL); field.set(object, value); modifiersField.setInt(field, modifiers); } else { field.set(object, value); } field.setAccessible(isFieldAccessible); if (modifiersField != null) { modifiersField.setAccessible(isModifiersAccessible); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/ShortArrayDataBlock.java ================================================ package org.janelia.saalfeldlab.n5; public class ShortArrayDataBlock extends AbstractDataBlock { public ShortArrayDataBlock(final int[] size, final long[] gridPosition, final short[] data) { super(size, gridPosition, data, a -> a.length); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/StringDataBlock.java ================================================ package org.janelia.saalfeldlab.n5; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; public class StringDataBlock extends AbstractDataBlock { protected static final Charset ENCODING = StandardCharsets.UTF_8; protected static final String NULLCHAR = "\0"; protected byte[] serializedData = null; protected String[] actualData = null; public StringDataBlock(final int[] size, final long[] gridPosition, final String[] data) { super(size, gridPosition, new String[0], a -> a.length); actualData = data; } public StringDataBlock(final int[] size, final long[] gridPosition, final byte[] data) { super(size, gridPosition, new String[0], a -> a.length); serializedData = data; } public void readData(final ByteBuffer buffer) { if (buffer.hasArray()) { if (buffer.array() != serializedData) buffer.get(serializedData); actualData = deserialize(buffer.array()); } else actualData = ENCODING.decode(buffer).toString().split(NULLCHAR); } protected byte[] serialize(String[] strings) { final String flattenedArray = String.join(NULLCHAR, strings) + NULLCHAR; return flattenedArray.getBytes(ENCODING); } protected String[] deserialize(byte[] rawBytes) { final String rawChars = new String(rawBytes, ENCODING); return rawChars.split(NULLCHAR); } @Override public int getNumElements() { if (serializedData == null) serializedData = serialize(actualData); return serializedData.length; } @Override public String[] getData() { if (actualData == null) actualData = deserialize(serializedData); return actualData; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/XzCompression.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.IOException; 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.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.serialization.NameConfig; @CompressionType("xz") @NameConfig.Name("xz") public class XzCompression implements Compression { private static final long serialVersionUID = -7272153943564743774L; @CompressionParameter @NameConfig.Parameter private final int preset; public XzCompression(final int preset) { this.preset = preset; } public XzCompression() { this(6); } @Override public boolean equals(final Object other) { if (other == null || other.getClass() != XzCompression.class) return false; else return preset == ((XzCompression)other).preset; } @Override public ReadData decode(final ReadData readData) throws N5IOException { try { return ReadData.from(new XZCompressorInputStream(readData.inputStream())); } catch (IOException e) { throw new N5IOException(e); } } @Override public ReadData encode(final ReadData readData) { return readData.encode(out -> new XZCompressorOutputStream(out, preset)); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/cache/N5JsonCache.java ================================================ package org.janelia.saalfeldlab.n5.cache; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashSet; import org.janelia.saalfeldlab.n5.N5Exception; import com.google.gson.JsonElement; /* * A cache containing JSON attributes and children for groups and * datasets stored in N5 containers. Used by {@link CachedGsonKeyValueN5Reader} * and {@link CachedGsonKeyValueN5Writer}. * */ public class N5JsonCache { public static final N5CacheInfo emptyCacheInfo = new N5CacheInfo(); public static final EmptyJson emptyJson = new EmptyJson(); protected final N5JsonCacheableContainer container; /** * Data object for caching meta data. Elements that are null are not yet * cached. */ protected static class N5CacheInfo { protected final HashMap attributesCache = new HashMap<>(); protected LinkedHashSet children = null; protected boolean isDataset = false; protected boolean isGroup = false; public JsonElement getCache(final String normalCacheKey) { // synchronize the method instead? synchronized (attributesCache) { return attributesCache.get(normalCacheKey); } } public boolean containsKey(final String normalCacheKey) { // synchronize the method instead? synchronized (attributesCache) { return attributesCache.containsKey(normalCacheKey); } } public boolean isDataset() { return isDataset; } public boolean isGroup() { return isGroup; } } @SuppressWarnings("deprecation") protected static class EmptyJson extends JsonElement { @Override public JsonElement deepCopy() { throw new N5Exception("Do not copy EmptyJson, you naughty person"); } } private final HashMap containerPathToCache = new HashMap<>(); public N5JsonCache(final N5JsonCacheableContainer container) { this.container = container; } public JsonElement getAttributes(final String normalPathKey, final String normalCacheKey) { N5CacheInfo cacheInfo = getCacheInfo(normalPathKey); if (cacheInfo == null) { addNewCacheInfo(normalPathKey, normalCacheKey, null); cacheInfo = getCacheInfo(normalPathKey); } if (cacheInfo == emptyCacheInfo || cacheInfo.getCache(normalCacheKey) == emptyJson) { return null; } synchronized (cacheInfo) { if (!cacheInfo.containsKey(normalCacheKey)) { updateCacheInfo(normalPathKey, normalCacheKey, null); } } final JsonElement output = cacheInfo.getCache(normalCacheKey); return output == null ? null : output.deepCopy(); } public boolean isDataset(final String normalPathKey, final String normalCacheKey) { N5CacheInfo cacheInfo = getCacheInfo(normalPathKey); if (cacheInfo == null) { addNewCacheInfo(normalPathKey, normalCacheKey, null); cacheInfo = getCacheInfo(normalPathKey); } return cacheInfo.isDataset; } public boolean isGroup(final String normalPathKey, final String cacheKey) { N5CacheInfo cacheInfo = getCacheInfo(normalPathKey); if (cacheInfo == null) { addNewCacheInfo(normalPathKey, cacheKey, null); cacheInfo = getCacheInfo(normalPathKey); } return cacheInfo.isGroup; } /** * Returns true if a resource exists. * * @param normalPathKey * the container path * @param normalCacheKey * the cache key / resource (may be null) * @return true if exists */ public boolean exists(final String normalPathKey, final String normalCacheKey) { N5CacheInfo cacheInfo = getCacheInfo(normalPathKey); if (cacheInfo == null) { addNewCacheInfo(normalPathKey, normalCacheKey, null); cacheInfo = getCacheInfo(normalPathKey); } return cacheInfo != emptyCacheInfo; } public String[] list(final String normalPathKey) { N5CacheInfo cacheInfo = getCacheInfo(normalPathKey); if (cacheInfo == null) { addNewCacheInfo(normalPathKey); cacheInfo = getCacheInfo(normalPathKey); } if (cacheInfo == emptyCacheInfo) throw new N5Exception.N5IOException(normalPathKey + " is not a valid group"); if (cacheInfo.children == null) addChild(cacheInfo, normalPathKey); final String[] children = new String[cacheInfo.children.size()]; int i = 0; for (final String child : cacheInfo.children) { children[i++] = child; } Arrays.sort(children); return children; } public N5CacheInfo addNewCacheInfo( final String normalPathKey, final String normalCacheKey, final JsonElement uncachedAttributes) { N5CacheInfo cacheInfo; JsonElement attrsFromContainer = null; boolean groupExistsFromContainer = false; try { attrsFromContainer = container.getAttributesFromContainer(normalPathKey, normalCacheKey); if (attrsFromContainer == null) groupExistsFromContainer = container.existsFromContainer(normalPathKey, null); if (groupExistsFromContainer || attrsFromContainer != null) cacheInfo = newCacheInfo(); else cacheInfo = emptyCacheInfo; } catch (N5Exception.N5NoSuchKeyException e) { cacheInfo = emptyCacheInfo; } if (cacheInfo != emptyCacheInfo) { if (normalCacheKey != null) { final JsonElement attributes = (uncachedAttributes == null) ? attrsFromContainer : uncachedAttributes; updateCacheAttributes(cacheInfo, normalCacheKey, attributes); updateCacheIsGroup(cacheInfo, container.isGroupFromAttributes(normalCacheKey, attributes)); updateCacheIsDataset(cacheInfo, container.isDatasetFromAttributes(normalCacheKey, attributes)); } else { updateCacheIsGroup(cacheInfo, container.isGroupFromContainer(normalPathKey)); updateCacheIsDataset(cacheInfo, container.isDatasetFromContainer(normalPathKey)); } } updateCache(normalPathKey, cacheInfo); return cacheInfo; } private N5CacheInfo addNewCacheInfo(final String normalPathKey) { return addNewCacheInfo(normalPathKey, null, null); } private void addChild(final N5CacheInfo cacheInfo, final String normalPathKey) { if (cacheInfo.children == null) cacheInfo.children = new LinkedHashSet<>(); final String[] children = container.listFromContainer(normalPathKey); Collections.addAll(cacheInfo.children, children); } protected N5CacheInfo getOrMakeCacheInfo(final String normalPathKey) { N5CacheInfo cacheInfo = getCacheInfo(normalPathKey); if (cacheInfo == null) { return addNewCacheInfo(normalPathKey, null, null); } if (cacheInfo == emptyCacheInfo) cacheInfo = newCacheInfo(); return cacheInfo; } /** * Updates the cache attributes for the given normalPathKey * adding the appropriate node to the cache if necessary. * * @param normalPathKey * the normalized path key * @param normalCacheKey * the normalize key to cache */ public void updateCacheInfo(final String normalPathKey, final String normalCacheKey) { final N5CacheInfo cacheInfo = getOrMakeCacheInfo(normalPathKey); final JsonElement attrs = cacheInfo.attributesCache.get(normalCacheKey); updateCacheInfo(normalPathKey, normalCacheKey, attrs); } /** * Updates the cache attributes for the given normalPathKey and * normalCacheKey, * adding the appropriate node to the cache if necessary. * * @param normalPathKey * the normalized path key * @param normalCacheKey * the normalized cache key * @param uncachedAttributes * attributes to be cached */ public void updateCacheInfo( final String normalPathKey, final String normalCacheKey, final JsonElement uncachedAttributes) { final N5CacheInfo cacheInfo = getOrMakeCacheInfo(normalPathKey); if (normalCacheKey != null) { final JsonElement attributesToCache = uncachedAttributes == null ? container.getAttributesFromContainer(normalPathKey, normalCacheKey) : uncachedAttributes; updateCacheAttributes(cacheInfo, normalCacheKey, attributesToCache); updateCacheIsGroup(cacheInfo, container.isGroupFromAttributes(normalCacheKey, attributesToCache)); updateCacheIsDataset(cacheInfo, container.isDatasetFromAttributes(normalCacheKey, attributesToCache)); } else { updateCacheIsGroup(cacheInfo, container.isGroupFromContainer(normalPathKey)); updateCacheIsDataset(cacheInfo, container.isDatasetFromContainer(normalPathKey)); } updateCache(normalPathKey, cacheInfo); } public void initializeNonemptyCache(final String normalPathKey, final String normalCacheKey) { final N5CacheInfo cacheInfo = getCacheInfo(normalPathKey); if (cacheInfo == null || cacheInfo == emptyCacheInfo) { final N5CacheInfo info = newCacheInfo(); if (normalCacheKey != null) info.attributesCache.put(normalCacheKey, emptyJson); updateCache(normalPathKey, info); } } public void setAttributes(final String normalPathKey, final String normalCacheKey, final JsonElement attributes) { N5CacheInfo cacheInfo = getCacheInfo(normalPathKey); boolean update = false; if (cacheInfo == null) { return; } if (cacheInfo == emptyCacheInfo) { cacheInfo = newCacheInfo(); update = true; } updateCacheAttributes(cacheInfo, normalCacheKey, attributes); if (update) updateCache(normalPathKey, cacheInfo); } /** * Adds child to the parent's children list, only if the parent has been * cached, and its children list already exists. * * @param parent * parent path * @param child * child path */ public void addChildIfPresent(final String parent, final String child) { final N5CacheInfo cacheInfo = getCacheInfo(parent); if (cacheInfo == null) return; if (cacheInfo.children != null) cacheInfo.children.add(child); } /** * Adds child to the parent's children list, only if the parent has been * cached, creating a children list if it does not already exist. * * @param parent * parent path * @param child * child path */ public void addChild(final String parent, final String child) { final N5CacheInfo cacheInfo = getCacheInfo(parent); if (cacheInfo == null) return; if (cacheInfo.children == null) cacheInfo.children = new LinkedHashSet<>(); cacheInfo.children.add(child); } public void removeCache(final String normalParentPathKey, final String normalPathKey) { // this path and all children should be removed = set to emptyCacheInfo synchronized (containerPathToCache) { containerPathToCache.put(normalPathKey, emptyCacheInfo); containerPathToCache.keySet().stream().filter(x -> { return x.startsWith(normalPathKey + "/"); }).forEach(x -> { containerPathToCache.put(x, emptyCacheInfo); }); } // update the parent's children, if present (remove the normalPathKey) final N5CacheInfo parentCache = containerPathToCache.get(normalParentPathKey); if (parentCache != null && parentCache.children != null) { parentCache.children.remove(normalPathKey.replaceFirst(normalParentPathKey + "/", "")); } } protected N5CacheInfo getCacheInfo(final String pathKey) { synchronized (containerPathToCache) { return containerPathToCache.get(pathKey); } } protected N5CacheInfo newCacheInfo() { return new N5CacheInfo(); } protected void updateCache(final String normalPathKey, final N5CacheInfo cacheInfo) { synchronized (containerPathToCache) { containerPathToCache.put(normalPathKey, cacheInfo); } } protected void updateCacheAttributes( final N5CacheInfo cacheInfo, final String normalCacheKey, final JsonElement attributes) { synchronized (cacheInfo.attributesCache) { cacheInfo.attributesCache.put(normalCacheKey, attributes); } } protected void updateCacheIsGroup(final N5CacheInfo cacheInfo, final boolean isGroup) { cacheInfo.isGroup = isGroup; } protected void updateCacheIsDataset(final N5CacheInfo cacheInfo, final boolean isDataset) { cacheInfo.isDataset = isDataset; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/cache/N5JsonCacheableContainer.java ================================================ package org.janelia.saalfeldlab.n5.cache; import org.janelia.saalfeldlab.n5.CachedGsonKeyValueN5Reader; import org.janelia.saalfeldlab.n5.GsonKeyValueN5Reader; import org.janelia.saalfeldlab.n5.N5Reader; import com.google.gson.JsonElement; /** * An N5 container whose structure and attributes can be cached. *

* Implementations of interface methods must explicitly query the backing * storage unless noted otherwise. Cached implementations (e.g {@link CachedGsonKeyValueN5Reader}) call * these methods to update their {@link N5JsonCache}. Corresponding * {@link N5Reader} methods should use the cache, if present. */ public interface N5JsonCacheableContainer { /** * Returns a {@link JsonElement} containing attributes at a given path, * for a given cache key. * * @param normalPathName * the normalized path name * @param normalCacheKey * the cache key * @return the attributes as a json element. * @see GsonKeyValueN5Reader#getAttributes */ JsonElement getAttributesFromContainer(final String normalPathName, final String normalCacheKey); /** * Query whether a resource exists in this container. * * @param normalPathName * the normalized path name * @param normalCacheKey * the normalized resource name (may be null). * @return true if the resouce exists */ boolean existsFromContainer(final String normalPathName, final String normalCacheKey); /** * Query whether a path in this container is a group. * * @param normalPathName * the normalized path name * @return true if the path is a group */ boolean isGroupFromContainer(final String normalPathName); /** * Query whether a path in this container is a dataset. * * @param normalPathName * the normalized path name * @return true if the path is a dataset * @see N5Reader#datasetExists */ boolean isDatasetFromContainer(final String normalPathName); /** * * Returns true if a path is a group, given that the the given attributes exist * for the given cache key. *

* Should not call the backing storage. * * @param normalCacheKey * the cache key * @param attributes * the attributes * @return true if the path is a group */ boolean isGroupFromAttributes(final String normalCacheKey, final JsonElement attributes); /** * Returns true if a path is a dataset, given that the the given attributes exist * for the given cache key. *

* Should not call the backing storage. * * @param normalCacheKey * the cache key * @param attributes * the attributes * @return true if the path is a dataset */ boolean isDatasetFromAttributes(final String normalCacheKey, final JsonElement attributes); /** * List the children of a path for this container. * * @param normalPathName * the normalized path name * @return list of children * @see N5Reader#list */ String[] listFromContainer(final String normalPathName); } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/BlockCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * De/serialize {@link DataBlock} from/to {@link ReadData}. * * @param * type of the data contained in the DataBlock */ public interface BlockCodec { /** * Serializes a {@link DataBlock} into a {@link ReadData} representation for * storage. *

* The encoding process serializes the block's data, applies any configured * compression, and may include metadata depending on the codec * implementation. * * @param dataBlock * the data block to encode * * @return serialized representation of the data block * * @throws N5IOException * if encoding or compression fails * * @see #decode(ReadData, long[]) */ ReadData encode(DataBlock dataBlock) throws N5IOException; /** * Deserializes a {@link DataBlock} from its {@link ReadData} * representation. *

* Reverses the encoding process by decompressing (if needed) and * deserializing the data. * * @param readData * the serialized data to decode * @param gridPosition * position of this block on the block grid (level 0 coordinates) * * @return reconstructed data block with deserialized data and grid position * * @throws N5IOException * if decoding, decompression, or data validation fails * * @see #encode(DataBlock) */ DataBlock decode(ReadData readData, long[] gridPosition) throws N5IOException; /** * Given the {@code blockSize} of a {@code DataBlock} return the size of * the encoded block in bytes. *

* A {@code UnsupportedOperationException} is thrown, if this {@code * BlockCodec} cannot determine encoded size independent of block content. * For example, if the block type contains var-length elements or if the * serializer uses a non-deterministic {@code DataCodec}. * * @param blockSize * size of the block to be encoded * * @return size of the encoded block in bytes * * @throws UnsupportedOperationException * if this {@code DataBlockSerializer} cannot determine encoded size independent of block content */ default long encodedSize(int[] blockSize) throws UnsupportedOperationException { throw new UnsupportedOperationException(); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/BlockCodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.codec; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * Metadata and factory for a particular family of {@code BlockCodec}. *

* {@code BlockCodec}s encode {@link DataBlock}s into {@link ReadData} and * decode {@link ReadData} into {@link DataBlock}s. */ public interface BlockCodecInfo extends CodecInfo, DeterministicSizeCodecInfo { default long[] getKeyPositionForBlock(final DatasetAttributes attributes, final DataBlock datablock) { return datablock.getGridPosition(); } default long[] getKeyPositionForBlock(final DatasetAttributes attributes, final long... blockPosition) { return blockPosition; } @Override default long encodedSize(long size) { return size; } @Override default long decodedSize(long size) { return size; } BlockCodec create(DataType dataType, int[] blockSize, DataCodecInfo... codecs); default BlockCodec create(final DatasetAttributes attributes, final DataCodecInfo... codecInfos) { return create(attributes.getDataType(), attributes.getBlockSize(), codecInfos); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/CodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.codec; import java.io.Serializable; import org.janelia.saalfeldlab.n5.serialization.NameConfig; /** * {@code CodecInfo}s are an untyped semantic layer for {@link BlockCodec}s, {@link DataCodec}s, and {@link DatasetCodec}s. *

* Modeled after Codecs in * Zarr. */ @NameConfig.Prefix("codec") public interface CodecInfo extends Serializable { String getType(); } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/CodecParser.java ================================================ package org.janelia.saalfeldlab.n5.codec; import java.util.ArrayList; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5Exception; public class CodecParser { public DatasetCodecInfo[] datasetCodecInfos; public BlockCodecInfo blockCodecInfo; public DataCodecInfo[] dataCodecInfos; public CodecParser(CodecInfo[] codecs) { parse(codecs); } private void parse(CodecInfo[] codecs) { final ArrayList dataCodecList = new ArrayList<>(); final ArrayList datasetCodecList = new ArrayList<>(); boolean foundBlockCodec = false; int i = 0; int blockCodecIndex = -1; for (CodecInfo codec : codecs) { if (!foundBlockCodec) { if (codec instanceof BlockCodecInfo) { blockCodecInfo = (BlockCodecInfo)codec; foundBlockCodec = true; blockCodecIndex = i; } else if (codec instanceof DatasetCodecInfo) datasetCodecList.add((DatasetCodecInfo)codec); else throw new N5Exception("Codec at index " + i + " is a DataCodec, but came before a BlockCodec."); } else if (codec instanceof BlockCodecInfo) throw new N5Exception("Codec at index " + i + " is a BlockCodec, but came after a BlockCodec at position " + blockCodecIndex); else if (codec instanceof DatasetCodecInfo) throw new N5Exception("Codec at index " + i + " is a DatasetCodec, but came after a BlockCodec at position " + blockCodecIndex); else dataCodecList.add((DataCodecInfo)codec); i++; } datasetCodecInfos = datasetCodecList.stream().toArray(n -> new DatasetCodecInfo[n]); dataCodecInfos = dataCodecList.stream().toArray(n -> new DataCodecInfo[n]); } private static CodecInfo[] concatenateCodecs(DatasetAttributes attributes) { final CodecInfo[] codecs = new CodecInfo[attributes.getDataCodecInfos().length + 1]; codecs[0] = attributes.getBlockCodecInfo(); System.arraycopy(attributes.getDataCodecInfos(), 0, codecs, 1, attributes.getDataCodecInfos().length); return codecs; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/ConcatenatedDataCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec; import org.janelia.saalfeldlab.n5.readdata.ReadData; class ConcatenatedDataCodec implements DataCodec { private final DataCodec[] codecs; ConcatenatedDataCodec(final DataCodec[] codecs) { if (codecs == null) { throw new NullPointerException(); } this.codecs = codecs; } @Override public ReadData encode(ReadData readData) { for (DataCodec codec : codecs) { readData = codec.encode(readData); } return readData; } @Override public ReadData decode(ReadData readData) { for (int i = codecs.length - 1; i >= 0; i--) { final DataCodec codec = codecs[i]; readData = codec.decode(readData); } return readData; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/ConcatenatedDeterministicSizeDataCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec; class ConcatenatedDeterministicSizeDataCodec extends ConcatenatedDataCodec implements DeterministicSizeDataCodec { private final DeterministicSizeDataCodec[] codecs; ConcatenatedDeterministicSizeDataCodec(final DeterministicSizeDataCodec[] codecs) { super(codecs); this.codecs = codecs; } @Override public long encodedSize(long size) { for (DeterministicSizeDataCodec codec : codecs) { size = codec.encodedSize(size); } return size; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/DataCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec; import java.util.Arrays; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * {@code DataCodec}s transform one {@link ReadData} into another, * for example, compressing it. */ public interface DataCodec { /** * Decode the given {@link ReadData}. *

* The returned decoded {@code ReadData} reports {@link ReadData#length() * length()}{@code == decodedLength}. Decoding may be lazy or eager, * depending on the {@code DataCodec} implementation. * * @param readData * data to decode * * @return decoded ReadData * * @throws N5IOException * if any I/O error occurs */ ReadData decode(ReadData readData) throws N5IOException; /** * Encode the given {@link ReadData}. *

* Encoding may be lazy or eager, depending on the {@code DataCodec} * implementation. * * @param readData * data to encode * * @return encoded ReadData * * @throws N5IOException * if any I/O error occurs */ ReadData encode(ReadData readData) throws N5IOException; /** * Create a {@code DataCodec} that sequentially applies {@code codecs} in * the given order for encoding, and in reverse order for decoding. *

* If all {@code codecs} implement {@code DeterministicSizeDataCodec}, the * returned {@code DataCodec} will also be a {@code DeterministicSizeDataCodec}. * * @param codecs * a list of DataCodecs * @return the concatenated DataCodec */ static DataCodec concatenate(final DataCodec... codecs) { if (codecs == null) throw new NullPointerException(); if (codecs.length == 1) return codecs[0]; if (Arrays.stream(codecs).allMatch(DeterministicSizeDataCodec.class::isInstance)) return new ConcatenatedDeterministicSizeDataCodec(Arrays.copyOf(codecs, codecs.length, DeterministicSizeDataCodec[].class)); else return new ConcatenatedDataCodec(codecs); } static DataCodec create(final DataCodecInfo... codecInfos) { if (codecInfos == null) throw new NullPointerException(); final DataCodec[] codecs = new DataCodec[codecInfos.length]; Arrays.setAll(codecs, i -> codecInfos[i].create()); return DataCodec.concatenate(codecs); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/DataCodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.codec; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.serialization.NameConfig; /** * Used to create {@code DataCodec}s, which transform one {@link ReadData} into another, * for example, applying compression. */ @NameConfig.Prefix("data-codec") public interface DataCodecInfo extends CodecInfo { DataCodec create(); } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/DatasetCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * A Codec that transforms the contents of a {@link DataBlock}. *

* This class is N5's analogue to Zarr's array-to-array codec. * * @param source data type (data contained in decoded blocks) * @param target data type (data contained in encoded blocks) */ public interface DatasetCodec { // TODO Name ideas: // "ImageCodec"? // BlockTransformationCodec DataBlock encode(DataBlock block) throws N5IOException; DataBlock decode(DataBlock dataBlock) throws N5IOException; /** * Create a {@code BlockCodec} that, for encoding, first applies {@code * datasetCodec} and then {@code blockCodec} (and does the same in reverse * order for decoding). * * @param * the source type of this codec * @param * the target type of this codec * @param datasetCodec * the DatasetCodec to apply * @param blockCodec * the wrapped BlockCodec * @return the concatenated BlockCodec */ static BlockCodec concatenate(final DatasetCodec datasetCodec, final BlockCodec blockCodec) { return new BlockCodec() { @Override public ReadData encode(final DataBlock dataBlock) throws N5IOException { return blockCodec.encode(datasetCodec.encode(dataBlock)); } @Override public DataBlock decode(final ReadData readData, final long[] gridPosition) throws N5IOException { return datasetCodec.decode(blockCodec.decode(readData, gridPosition)); } }; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/DatasetCodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.codec; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.codec.transpose.TransposeCodec; import org.janelia.saalfeldlab.n5.serialization.NameConfig; /** * Used to create typed {@code DatasetCodec}s, which transform one {@link DataBlock} into another, * for example, by applying a transposition (@link {@link TransposeCodec}. */ @NameConfig.Prefix("data-codec") // TODO: is this Prefix correct? public interface DatasetCodecInfo extends CodecInfo { DatasetCodec create(final DatasetAttributes attributes); } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/DeterministicSizeCodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.codec; /** * A {@link CodecInfo} 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 DeterministicSizeCodecInfo extends CodecInfo { long encodedSize(long size); long decodedSize(long size); } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/DeterministicSizeDataCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec; /** * A {@link DataCodec} that can deterministically determine the size of encoded * data from the size of the raw data (i.e. encoding is data independent). */ public interface DeterministicSizeDataCodec extends DataCodec { /** * Given {@code size} bytes of raw data, how many bytes will the encoded * data have. * * @param size in bytes * @return encoded size in bytes */ long encodedSize(long size); /** * Create a {@code DeterministicSizeDataCodec} that sequentially applies * {@code codecs} in the given order for encoding, and in reverse order for * decoding. * * @param codecs * a list of DeterministicSizeDataCodec * @return the concatenated DeterministicSizeDataCodec */ static DeterministicSizeDataCodec concatenate(final DeterministicSizeDataCodec... codecs) { if (codecs == null) throw new NullPointerException(); if (codecs.length == 1) return codecs[0]; return new ConcatenatedDeterministicSizeDataCodec(codecs); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/FlatArrayCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.nio.IntBuffer; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.function.IntFunction; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * De/serialize the {@link DataBlock#getData() data} contained in a {@code * DataBlock} from/to a sequence of bytes. *

* Static fields {@code BYTE}, {@code SHORT_BIG_ENDIAN}, {@code * SHORT_LITTLE_ENDIAN}, etc. contain {@code FlatArrayCodec}s for all primitive * array types and big-endian / little-endian byte order. * * @param * type of the data contained in the DataBlock */ public abstract class FlatArrayCodec { public abstract ReadData encode(T data) throws N5IOException; public abstract T decode(ReadData readData, int numElements) throws N5IOException; public int bytesPerElement() { return bytesPerElement; } public T newArray(final int numElements) { return dataFactory.apply(numElements); } // ------------------- instances -------------------- // public static final FlatArrayCodec BYTE = new ByteArrayCodec(); public static final FlatArrayCodec SHORT_BIG_ENDIAN = new ShortArrayCodec(ByteOrder.BIG_ENDIAN); public static final FlatArrayCodec INT_BIG_ENDIAN = new IntArrayCodec(ByteOrder.BIG_ENDIAN); public static final FlatArrayCodec LONG_BIG_ENDIAN = new LongArrayCodec(ByteOrder.BIG_ENDIAN); public static final FlatArrayCodec FLOAT_BIG_ENDIAN = new FloatArrayCodec(ByteOrder.BIG_ENDIAN); public static final FlatArrayCodec DOUBLE_BIG_ENDIAN = new DoubleArrayCodec(ByteOrder.BIG_ENDIAN); public static final FlatArrayCodec SHORT_LITTLE_ENDIAN = new ShortArrayCodec(ByteOrder.LITTLE_ENDIAN); public static final FlatArrayCodec INT_LITTLE_ENDIAN = new IntArrayCodec(ByteOrder.LITTLE_ENDIAN); public static final FlatArrayCodec LONG_LITTLE_ENDIAN = new LongArrayCodec(ByteOrder.LITTLE_ENDIAN); public static final FlatArrayCodec FLOAT_LITTLE_ENDIAN = new FloatArrayCodec(ByteOrder.LITTLE_ENDIAN); public static final FlatArrayCodec DOUBLE_LITTLE_ENDIAN = new DoubleArrayCodec(ByteOrder.LITTLE_ENDIAN); public static final FlatArrayCodec STRING = new N5StringArrayCodec(); public static final FlatArrayCodec ZARR_STRING = new ZarrStringArrayCodec(); public static final FlatArrayCodec OBJECT = new ObjectArrayCodec(); public static FlatArrayCodec SHORT(ByteOrder order) { return order == ByteOrder.BIG_ENDIAN ? SHORT_BIG_ENDIAN : SHORT_LITTLE_ENDIAN; } public static FlatArrayCodec INT(ByteOrder order) { return order == ByteOrder.BIG_ENDIAN ? INT_BIG_ENDIAN : INT_LITTLE_ENDIAN; } public static FlatArrayCodec LONG(ByteOrder order) { return order == ByteOrder.BIG_ENDIAN ? LONG_BIG_ENDIAN : LONG_LITTLE_ENDIAN; } public static FlatArrayCodec FLOAT(ByteOrder order) { return order == ByteOrder.BIG_ENDIAN ? FLOAT_BIG_ENDIAN : FLOAT_LITTLE_ENDIAN; } public static FlatArrayCodec DOUBLE(ByteOrder order) { return order == ByteOrder.BIG_ENDIAN ? DOUBLE_BIG_ENDIAN : DOUBLE_LITTLE_ENDIAN; } // ---------------- implementations ----------------- // private final int bytesPerElement; private final IntFunction dataFactory; private FlatArrayCodec(int bytesPerElement, IntFunction dataFactory) { this.bytesPerElement = bytesPerElement; this.dataFactory = dataFactory; } private static final class ByteArrayCodec extends FlatArrayCodec { private ByteArrayCodec() { super(Byte.BYTES, byte[]::new); } @Override public ReadData encode(final byte[] data) { return ReadData.from(data); } @Override public byte[] decode(final ReadData readData, int numElements) throws N5IOException { final byte[] data = newArray(numElements); try (final InputStream is = readData.limit(numElements).inputStream()) { new DataInputStream(is).readFully(data); } catch (IOException e) { throw new N5IOException(e); } return data; } } private static final class ShortArrayCodec extends FlatArrayCodec { private final ByteOrder order; ShortArrayCodec(ByteOrder order) { super(Short.BYTES, short[]::new); this.order = order; } @Override public ReadData encode(final short[] data) throws N5IOException { final ByteBuffer serialized = ByteBuffer.allocate(Short.BYTES * data.length); serialized.order(order).asShortBuffer().put(data); return ReadData.from(serialized); } @Override public short[] decode(final ReadData readData, int numElements) throws N5IOException { final short[] data = newArray(numElements); readData.limit(2 * numElements).toByteBuffer().order(order).asShortBuffer().get(data); return data; } } private static final class IntArrayCodec extends FlatArrayCodec { private final ByteOrder order; IntArrayCodec(ByteOrder order) { super(Integer.BYTES, int[]::new); this.order = order; } @Override public ReadData encode(final int[] data) throws N5IOException { final ByteBuffer serialized = ByteBuffer.allocate(Integer.BYTES * data.length); serialized.order(order).asIntBuffer().put(data); return ReadData.from(serialized); } @Override public int[] decode(final ReadData readData, int numElements) throws N5IOException { final int[] data = newArray(numElements); final ByteBuffer byteBuffer = readData.limit(4 * numElements).toByteBuffer(); final IntBuffer intBuffer = byteBuffer.order(order).asIntBuffer(); intBuffer.get(data); return data; } } private static final class LongArrayCodec extends FlatArrayCodec { private final ByteOrder order; LongArrayCodec(ByteOrder order) { super(Long.BYTES, long[]::new); this.order = order; } @Override public ReadData encode(final long[] data) throws N5IOException { final ByteBuffer serialized = ByteBuffer.allocate(Long.BYTES * data.length); serialized.order(order).asLongBuffer().put(data); return ReadData.from(serialized); } @Override public long[] decode(final ReadData readData, int numElements) throws N5IOException { final long[] data = newArray(numElements); readData.limit(8 * numElements).toByteBuffer().order(order).asLongBuffer().get(data); return data; } } private static final class FloatArrayCodec extends FlatArrayCodec { private final ByteOrder order; FloatArrayCodec(ByteOrder order) { super(Float.BYTES, float[]::new); this.order = order; } @Override public ReadData encode(final float[] data) throws N5IOException { final ByteBuffer serialized = ByteBuffer.allocate(Float.BYTES * data.length); serialized.order(order).asFloatBuffer().put(data); return ReadData.from(serialized); } @Override public float[] decode(final ReadData readData, int numElements) throws N5IOException { final float[] data = newArray(numElements); readData.limit(4 * numElements).toByteBuffer().order(order).asFloatBuffer().get(data); return data; } } private static final class DoubleArrayCodec extends FlatArrayCodec { private final ByteOrder order; DoubleArrayCodec(ByteOrder order) { super(Double.BYTES, double[]::new); this.order = order; } @Override public ReadData encode(final double[] data) throws N5IOException { final ByteBuffer serialized = ByteBuffer.allocate(Double.BYTES * data.length); serialized.order(order).asDoubleBuffer().put(data); return ReadData.from(serialized); } @Override public double[] decode(final ReadData readData, int numElements) throws N5IOException { final double[] data = newArray(numElements); readData.limit(8 * numElements).toByteBuffer().order(order).asDoubleBuffer().get(data); return data; } } private static final class N5StringArrayCodec extends FlatArrayCodec { private static final Charset ENCODING = StandardCharsets.UTF_8; private static final String NULLCHAR = "\0"; N5StringArrayCodec() { super( -1, String[]::new); } @Override public ReadData encode(String[] data) throws N5IOException { final String flattenedArray = String.join(NULLCHAR, data) + NULLCHAR; return ReadData.from(flattenedArray.getBytes(ENCODING)); } @Override public String[] decode(ReadData readData, int numElements) throws N5IOException { final byte[] serializedData = readData.allBytes(); final String rawChars = new String(serializedData, ENCODING); return rawChars.split(NULLCHAR); } } private static final class ZarrStringArrayCodec extends FlatArrayCodec { private static final Charset ENCODING = StandardCharsets.UTF_8; ZarrStringArrayCodec() { super( -1, String[]::new); } @Override public ReadData encode(String[] data) throws N5IOException { final int N = data.length; final byte[][] encodedStrings = Arrays.stream(data).map(str -> str.getBytes(ENCODING)).toArray(byte[][]::new); final int[] lengths = Arrays.stream(encodedStrings).mapToInt(a -> a.length).toArray(); final int totalLength = Arrays.stream(lengths).sum(); final ByteBuffer buf = ByteBuffer.wrap(new byte[totalLength + 4 * N + 4]); buf.order(ByteOrder.LITTLE_ENDIAN); buf.putInt(N); for (int i = 0; i < N; ++i) { buf.putInt(lengths[i]); buf.put(encodedStrings[i]); } return ReadData.from(buf.array()); } @Override public String[] decode(ReadData readData, int numElements) throws N5IOException { final ByteBuffer serialized = readData.toByteBuffer(); serialized.order(ByteOrder.LITTLE_ENDIAN); // sanity check to avoid out of memory errors if (serialized.limit() < 4) throw new RuntimeException("Corrupt buffer, data seems truncated."); final int n = serialized.getInt(); if (serialized.limit() < n) throw new RuntimeException("Corrupt buffer, data seems truncated."); final String[] actualData = new String[n]; for (int i = 0; i < n; ++i) { final int length = serialized.getInt(); final byte[] encodedString = new byte[length]; serialized.get(encodedString); actualData[i] = new String(encodedString, ENCODING); } return actualData; } } private static final class ObjectArrayCodec extends FlatArrayCodec { ObjectArrayCodec() { super(-1, byte[]::new); } @Override public ReadData encode(byte[] data) throws N5IOException { return ReadData.from(data); } @Override public byte[] decode(ReadData readData, int numElements) throws N5IOException { final byte[] data = newArray(numElements); try (final InputStream is = readData.inputStream()) { new DataInputStream(is).readFully(data); } catch (IOException e) { throw new N5IOException(e); } return data; } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/IdentityCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.serialization.NameConfig; @NameConfig.Name(IdentityCodec.TYPE) public class IdentityCodec implements DeterministicSizeDataCodec, DataCodecInfo { private static final long serialVersionUID = 8354269325800855621L; public static final String TYPE = "id"; @Override public String getType() { return TYPE; } @Override public ReadData decode(ReadData readData) { return readData; } @Override public ReadData encode(ReadData readData) { return readData; } @Override public DataCodec create() { return this; } @Override public long encodedSize(long size) { return size; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/IndexCodecAdapter.java ================================================ package org.janelia.saalfeldlab.n5.codec; public class IndexCodecAdapter { private final BlockCodecInfo blockCodecInfo; private final DeterministicSizeCodecInfo[] dataCodecs; public IndexCodecAdapter(final BlockCodecInfo blockCodecInfo, final DeterministicSizeCodecInfo... dataCodecs) { this.blockCodecInfo = blockCodecInfo; this.dataCodecs = dataCodecs; } public BlockCodecInfo getBlockCodecInfo() { return blockCodecInfo; } public DataCodecInfo[] getDataCodecs() { final DataCodecInfo[] dataCodecs = new DataCodecInfo[this.dataCodecs.length]; System.arraycopy(this.dataCodecs, 0, dataCodecs, 0, this.dataCodecs.length); return dataCodecs; } public long encodedSize(long initialSize) { long totalNumBytes = initialSize; for (DeterministicSizeCodecInfo codec : dataCodecs) { totalNumBytes = codec.encodedSize(totalNumBytes); } return totalNumBytes; } public static IndexCodecAdapter create(final CodecInfo... codecs) { if (codecs == null || codecs.length == 0) return new IndexCodecAdapter(new RawBlockCodecInfo()); if (codecs[0] instanceof BlockCodecInfo) return new IndexCodecAdapter((BlockCodecInfo)codecs[0]); final DeterministicSizeCodecInfo[] indexCodecs = new DeterministicSizeCodecInfo[codecs.length]; System.arraycopy(codecs, 0, indexCodecs, 0, codecs.length); return new IndexCodecAdapter(new RawBlockCodecInfo(), indexCodecs); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/N5BlockCodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.codec; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.serialization.NameConfig; @NameConfig.Name(value = N5BlockCodecInfo.TYPE) public class N5BlockCodecInfo implements BlockCodecInfo { private static final long serialVersionUID = 3523505403978222360L; public static final String TYPE = "n5bytes"; private transient DatasetAttributes attributes; @Override public long[] getKeyPositionForBlock(DatasetAttributes attributes, DataBlock datablock) { return datablock.getGridPosition(); } @Override public long[] getKeyPositionForBlock(DatasetAttributes attributes, long... blockPosition) { return blockPosition; } @Override public long encodedSize(long size) { final int[] blockSize = attributes.getBlockSize(); int headerSize = new N5BlockCodecs.BlockHeader(blockSize, DataBlock.getNumElements(blockSize)).getSize(); return headerSize + size; } @Override public String getType() { return TYPE; } @Override public BlockCodec create(final DataType dataType, final int[] blockSize, final DataCodecInfo... codecInfos) { return N5BlockCodecs.create(dataType, DataCodec.create(codecInfos)); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/N5BlockCodecs.java ================================================ package org.janelia.saalfeldlab.n5.codec; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.janelia.saalfeldlab.n5.ByteArrayDataBlock; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DataBlock.DataBlockFactory; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DoubleArrayDataBlock; import org.janelia.saalfeldlab.n5.FloatArrayDataBlock; import org.janelia.saalfeldlab.n5.IntArrayDataBlock; import org.janelia.saalfeldlab.n5.LongArrayDataBlock; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.ShortArrayDataBlock; import org.janelia.saalfeldlab.n5.StringDataBlock; import org.janelia.saalfeldlab.n5.readdata.ReadData; import static org.janelia.saalfeldlab.n5.N5Exception.*; import static org.janelia.saalfeldlab.n5.codec.N5BlockCodecs.BlockHeader.MODE_DEFAULT; import static org.janelia.saalfeldlab.n5.codec.N5BlockCodecs.BlockHeader.MODE_OBJECT; import static org.janelia.saalfeldlab.n5.codec.N5BlockCodecs.BlockHeader.MODE_VARLENGTH; import static org.janelia.saalfeldlab.n5.codec.N5BlockCodecs.BlockHeader.headerSizeInBytes; public class N5BlockCodecs { private static final BlockCodecFactory BYTE = c -> new DefaultBlockCodec<>(FlatArrayCodec.BYTE, ByteArrayDataBlock::new, c); private static final BlockCodecFactory SHORT = c -> new DefaultBlockCodec<>(FlatArrayCodec.SHORT_BIG_ENDIAN, ShortArrayDataBlock::new, c); private static final BlockCodecFactory INT = c -> new DefaultBlockCodec<>(FlatArrayCodec.INT_BIG_ENDIAN, IntArrayDataBlock::new, c); private static final BlockCodecFactory LONG = c -> new DefaultBlockCodec<>(FlatArrayCodec.LONG_BIG_ENDIAN, LongArrayDataBlock::new, c); private static final BlockCodecFactory FLOAT = c -> new DefaultBlockCodec<>(FlatArrayCodec.FLOAT_BIG_ENDIAN, FloatArrayDataBlock::new, c); private static final BlockCodecFactory DOUBLE = c -> new DefaultBlockCodec<>(FlatArrayCodec.DOUBLE_BIG_ENDIAN, DoubleArrayDataBlock::new, c); private static final BlockCodecFactory STRING = c -> new StringBlockCodec(c); private static final BlockCodecFactory OBJECT = c -> new ObjectBlockCodec(c); private N5BlockCodecs() {} public static BlockCodec create( final DataType dataType, final DataCodec codec) { final BlockCodecFactory factory; switch (dataType) { case UINT8: case INT8: factory = N5BlockCodecs.BYTE; break; case UINT16: case INT16: factory = N5BlockCodecs.SHORT; break; case UINT32: case INT32: factory = N5BlockCodecs.INT; break; case UINT64: case INT64: factory = N5BlockCodecs.LONG; break; case FLOAT32: factory = N5BlockCodecs.FLOAT; break; case FLOAT64: factory = N5BlockCodecs.DOUBLE; break; case STRING: factory = N5BlockCodecs.STRING; break; case OBJECT: factory = N5BlockCodecs.OBJECT; break; default: throw new IllegalArgumentException("Unsupported data type: " + dataType); } @SuppressWarnings("unchecked") final BlockCodecFactory tFactory = (BlockCodecFactory)factory; return tFactory.create(codec); } private interface BlockCodecFactory { /** * Create a {@link BlockCodec} that uses the specified {@code DataCodec} * and de/serializes {@code DataBlock} to N5 format. * * @return N5 {@code BlockCodec} using the specified {@code DataCodec} */ BlockCodec create(DataCodec dataCodec); } abstract static class N5AbstractBlockCodec implements BlockCodec { final FlatArrayCodec dataCodec; private final DataBlockFactory dataBlockFactory; final DataCodec codec; N5AbstractBlockCodec(FlatArrayCodec dataCodec, DataBlockFactory dataBlockFactory, DataCodec codec) { this.dataCodec = dataCodec; this.dataBlockFactory = dataBlockFactory; this.codec = codec; } abstract BlockHeader createBlockHeader(final DataBlock dataBlock, ReadData blockData) throws N5IOException; @Override public ReadData encode(DataBlock dataBlock) throws N5IOException { return ReadData.from(out -> { final ReadData dataReadData = dataCodec.encode(dataBlock.getData()); final BlockHeader header = createBlockHeader(dataBlock, dataReadData); header.writeTo(out); final ReadData encodedData = codec.encode(dataReadData); encodedData.writeTo(out); }); } abstract BlockHeader decodeBlockHeader(final InputStream in) throws N5IOException; @Override public DataBlock decode(final ReadData readData, final long[] gridPosition) throws N5IOException { // read block header with input stream since header is variable length final BlockHeader header; try(final InputStream in = readData.inputStream()) { header = decodeBlockHeader(in); } catch (IOException e) { throw new N5IOException(e); } // determine length // and slice original read data so that bodyReadData is known length final int numElements = header.numElements(); final long bodyLength = readData.length() - header.getSize(); final ReadData bodyReadData = bodyLength > 0 ? readData.slice(header.getSize(), bodyLength) : ReadData.empty(); final ReadData decodeData = codec.decode(bodyReadData); // the dataCodec knows the number of bytes per element final T data = dataCodec.decode(decodeData, numElements); return dataBlockFactory.createDataBlock(header.blockSize(), gridPosition, data); } } /** * DataBlockCodec for all N5 data types, except STRING and OBJECT */ private static class DefaultBlockCodec extends N5AbstractBlockCodec { DefaultBlockCodec( final FlatArrayCodec dataCodec, final DataBlockFactory dataBlockFactory, final DataCodec codec) { super(dataCodec, dataBlockFactory, codec); } @Override protected BlockHeader createBlockHeader(final DataBlock dataBlock, ReadData blockData) throws N5IOException { return new BlockHeader(dataBlock.getSize(), dataBlock.getNumElements()); } @Override protected BlockHeader decodeBlockHeader(final InputStream in) throws N5IOException { return BlockHeader.readFrom(in, MODE_DEFAULT, MODE_VARLENGTH); } @Override public long encodedSize(final int[] blockSize) throws UnsupportedOperationException { if (codec instanceof DeterministicSizeDataCodec) { final int bytesPerElement = dataCodec.bytesPerElement(); final int numElements = DataBlock.getNumElements(blockSize); final int headerSize = headerSizeInBytes(MODE_DEFAULT, blockSize.length); return headerSize + ((DeterministicSizeDataCodec) codec).encodedSize((long) numElements * bytesPerElement); } else { throw new UnsupportedOperationException(); } } } /** * DataBlockCodec for N5 data type STRING */ private static class StringBlockCodec extends N5AbstractBlockCodec { StringBlockCodec(final DataCodec codec) { super(FlatArrayCodec.STRING, StringDataBlock::new, codec); } @Override protected BlockHeader createBlockHeader(final DataBlock dataBlock, ReadData blockData) throws N5IOException { return new BlockHeader(MODE_VARLENGTH, dataBlock.getSize(), (int)blockData.length()); } @Override protected BlockHeader decodeBlockHeader(final InputStream in) throws N5IOException { return BlockHeader.readFrom(in, MODE_DEFAULT, MODE_VARLENGTH); } } /** * DataBlockCodec for N5 data type OBJECT */ private static class ObjectBlockCodec extends N5AbstractBlockCodec { ObjectBlockCodec(final DataCodec codec) { super(FlatArrayCodec.OBJECT, ByteArrayDataBlock::new, codec); } @Override protected BlockHeader createBlockHeader(DataBlock dataBlock, ReadData blockData) { return new BlockHeader(null, dataBlock.getNumElements()); } @Override protected BlockHeader decodeBlockHeader(final InputStream in) throws N5IOException { return BlockHeader.readFrom(in, MODE_OBJECT); } } static class BlockHeader { public static final short MODE_DEFAULT = 0; public static final short MODE_VARLENGTH = 1; public static final short MODE_OBJECT = 2; private final short mode; private final int[] blockSize; private final int numElements; BlockHeader(final short mode, final int[] blockSize, final int numElements) { this.mode = mode; this.blockSize = blockSize; this.numElements = numElements; } BlockHeader(final int[] blockSize, final int numElements) { if (blockSize == null) { this.mode = MODE_OBJECT; } else if (DataBlock.getNumElements(blockSize) == numElements) { this.mode = MODE_DEFAULT; } else { this.mode = MODE_VARLENGTH; } this.blockSize = blockSize; this.numElements = numElements; } public int getSize() { return headerSizeInBytes(mode, blockSize == null ? 0 : blockSize.length); } public int[] blockSize() { return blockSize; } public int numElements() { return numElements; } private static int[] readBlockSize(final DataInputStream dis) throws N5IOException { try { final int nDim = dis.readShort(); final int[] blockSize = new int[nDim]; for (int d = 0; d < nDim; ++d) blockSize[d] = dis.readInt(); return blockSize; } catch (IOException e) { throw new N5IOException(e); } } private static void writeBlockSize(final int[] blockSize, final DataOutputStream dos) throws N5IOException { try { dos.writeShort(blockSize.length); for (final int size : blockSize) dos.writeInt(size); } catch (IOException e) { throw new N5IOException(e); } } static int headerSizeInBytes(final short mode, final int numDimensions) { switch (mode) { case MODE_DEFAULT: return 2 + // 1 short for mode 2 + // 1 short for blockSize.length 4 * numDimensions; // 1 int for each blockSize element case MODE_VARLENGTH: return 2 +// 1 short for mode 2 + // 1 short for blockSize.length 4 * numDimensions + // 1 int for each blockSize dimension 4; // 1 int for numElements case MODE_OBJECT: return 2 + // 1 short for mode 4; // 1 int for numElements default: throw new N5Exception("unexpected mode: " + mode); } } void writeTo(final OutputStream out) throws N5IOException { try { final DataOutputStream dos = new DataOutputStream(out); dos.writeShort(mode); switch (mode) { case MODE_DEFAULT:// default writeBlockSize(blockSize, dos); break; case MODE_VARLENGTH:// varlength writeBlockSize(blockSize, dos); dos.writeInt(numElements); break; case MODE_OBJECT: // object dos.writeInt(numElements); break; default: throw new N5Exception("unexpected mode: " + mode); } dos.flush(); } catch (IOException e) { throw new N5IOException(e); } } static BlockHeader readFrom(final InputStream in, short... allowedModes) throws N5IOException, N5Exception { try { final DataInputStream dis = new DataInputStream(in); final short mode = dis.readShort(); final int[] blockSize; final int numElements; switch (mode) { case MODE_DEFAULT:// default blockSize = readBlockSize(dis); numElements = DataBlock.getNumElements(blockSize); break; case MODE_VARLENGTH:// varlength blockSize = readBlockSize(dis); numElements = dis.readInt(); break; case MODE_OBJECT: // object blockSize = null; numElements = dis.readInt(); break; default: throw new N5Exception("Unexpected mode: " + mode); } boolean modeIsOk = allowedModes == null || allowedModes.length == 0; for (int i = 0; !modeIsOk && i < allowedModes.length; ++i) { if (mode == allowedModes[i]) { modeIsOk = true; break; } } if (!modeIsOk) { throw new N5Exception("Unexpected mode: " + mode); } return new BlockHeader(mode, blockSize, numElements); } catch (IOException e) { throw new N5IOException(e); } } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/RawBlockCodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.codec; import java.nio.ByteOrder; 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.DataType; import org.janelia.saalfeldlab.n5.serialization.NameConfig; @NameConfig.Name(value = RawBlockCodecInfo.TYPE) public class RawBlockCodecInfo implements BlockCodecInfo { private static final long serialVersionUID = 3282569607795127005L; public static final String TYPE = "raw-bytes"; @NameConfig.Parameter(value = "endian", optional = true) private final ByteOrder byteOrder; public RawBlockCodecInfo() { this(ByteOrder.BIG_ENDIAN); } public RawBlockCodecInfo(final ByteOrder byteOrder) { this.byteOrder = byteOrder; } @Override public String getType() { return TYPE; } public ByteOrder getByteOrder() { return byteOrder; } @Override public BlockCodec create(final DataType dataType, final int[] blockSize, final DataCodecInfo... codecInfos) { ensureValidByteOrder(dataType, getByteOrder()); return RawBlockCodecs.create(dataType, byteOrder, blockSize, DataCodec.create(codecInfos)); } public static void ensureValidByteOrder(final DataType dataType, final ByteOrder byteOrder) { switch (dataType) { case INT8: case UINT8: case STRING: case OBJECT: return; } if (byteOrder == null) throw new IllegalArgumentException("DataType (" + dataType + ") requires ByteOrder, but was null"); } public static 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; } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/RawBlockCodecs.java ================================================ package org.janelia.saalfeldlab.n5.codec; import org.janelia.saalfeldlab.n5.ByteArrayDataBlock; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DoubleArrayDataBlock; import org.janelia.saalfeldlab.n5.FloatArrayDataBlock; import org.janelia.saalfeldlab.n5.IntArrayDataBlock; import org.janelia.saalfeldlab.n5.LongArrayDataBlock; import org.janelia.saalfeldlab.n5.ShortArrayDataBlock; import org.janelia.saalfeldlab.n5.StringDataBlock; import org.janelia.saalfeldlab.n5.readdata.ReadData; import java.nio.ByteOrder; public class RawBlockCodecs { private static final BlockCodecFactory BYTE = (byteOrder, blockSize, codec) -> new RawBlockCodec<>(FlatArrayCodec.BYTE, ByteArrayDataBlock::new, blockSize, codec); private static final BlockCodecFactory SHORT = (byteOrder, blockSize, codec) -> new RawBlockCodec<>(FlatArrayCodec.SHORT(byteOrder), ShortArrayDataBlock::new, blockSize, codec); private static final BlockCodecFactory INT = (byteOrder, blockSize, codec) -> new RawBlockCodec<>(FlatArrayCodec.INT(byteOrder), IntArrayDataBlock::new, blockSize, codec); private static final BlockCodecFactory LONG = (byteOrder, blockSize, codec) -> new RawBlockCodec<>(FlatArrayCodec.LONG(byteOrder), LongArrayDataBlock::new, blockSize, codec); private static final BlockCodecFactory FLOAT = (byteOrder, blockSize, codec) -> new RawBlockCodec<>(FlatArrayCodec.FLOAT(byteOrder), FloatArrayDataBlock::new, blockSize, codec); private static final BlockCodecFactory DOUBLE = (byteOrder, blockSize, codec) -> new RawBlockCodec<>(FlatArrayCodec.DOUBLE(byteOrder), DoubleArrayDataBlock::new, blockSize, codec); private static final BlockCodecFactory STRING = (byteOrder, blockSize, codec) -> new RawBlockCodec<>(FlatArrayCodec.ZARR_STRING, StringDataBlock::new, blockSize, codec); private static final BlockCodecFactory OBJECT = (byteOrder, blockSize, codec) -> new RawBlockCodec<>(FlatArrayCodec.OBJECT, ByteArrayDataBlock::new, blockSize, codec); private RawBlockCodecs() {} public static BlockCodec create( final DataType dataType, final ByteOrder byteOrder, final int[] blockSize, final DataCodec codec) { final BlockCodecFactory factory; switch (dataType) { case UINT8: case INT8: factory = RawBlockCodecs.BYTE; break; case UINT16: case INT16: factory = RawBlockCodecs.SHORT; break; case UINT32: case INT32: factory = RawBlockCodecs.INT; break; case UINT64: case INT64: factory = RawBlockCodecs.LONG; break; case FLOAT32: factory = RawBlockCodecs.FLOAT; break; case FLOAT64: factory = RawBlockCodecs.DOUBLE; break; case STRING: factory = RawBlockCodecs.STRING; break; // TODO: What about OBJECT? default: throw new IllegalArgumentException("Unsupported data type: " + dataType); } final BlockCodecFactory tFactory = (BlockCodecFactory) factory; return tFactory.create(byteOrder, blockSize, codec); } private interface BlockCodecFactory { /** * Create a {@link BlockCodec} that uses the specified {@code ByteOrder} * and {@code DataCodec} and de/serializes {@code DataBlock} of the * specified {@code blockSize} to raw format. * * @return Raw {@code BlockCodec} */ BlockCodec create(ByteOrder byteOrder, int[] blockSize, DataCodec dataCodec); } private static class RawBlockCodec implements BlockCodec { private final FlatArrayCodec dataCodec; private final DataBlock.DataBlockFactory dataBlockFactory; private final int[] blockSize; private final int numElements; private final DataCodec codec; RawBlockCodec( final FlatArrayCodec dataCodec, final DataBlock.DataBlockFactory dataBlockFactory, final int[] blockSize, final DataCodec codec) { this.dataCodec = dataCodec; this.dataBlockFactory = dataBlockFactory; this.blockSize = blockSize; this.numElements = DataBlock.getNumElements(blockSize); this.codec = codec; } @Override public ReadData encode(DataBlock dataBlock) { return ReadData.from(out -> { final ReadData blockData = dataCodec.encode(dataBlock.getData()); codec.encode(blockData).writeTo(out); }); } @Override public DataBlock decode(ReadData readData, long[] gridPosition) { final ReadData decodeData = codec.decode(readData); final T data = dataCodec.decode(decodeData, numElements); return dataBlockFactory.createDataBlock(blockSize, gridPosition, data); } @Override public long encodedSize(final int[] blockSize) throws UnsupportedOperationException { if (codec instanceof DeterministicSizeDataCodec) { final int bytesPerElement = dataCodec.bytesPerElement(); final int numElements = DataBlock.getNumElements(blockSize); return ((DeterministicSizeDataCodec) codec).encodedSize((long) numElements * bytesPerElement); } else { throw new UnsupportedOperationException(); } } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec.checksum; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.function.Supplier; import java.util.zip.CheckedOutputStream; import java.util.zip.Checksum; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.codec.CodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodec; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.DeterministicSizeDataCodec; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * A {@link CodecInfo} that appends a checksum to data when encoding and can * validate against that checksum when decoding. *

* Checksum codec instances are expected to be thread safe, but {@link Checksum} * implementations may not be. As a result, subclasses of this implementation * provide a {@link Supplier} for an appropriate Checksum type, a new instance * of which is created by {@link #getChecksum()} for each * {@link #encode(ReadData)} and {@link #decode(ReadData)} call. */ public abstract class ChecksumCodec implements DataCodec, DataCodecInfo, DeterministicSizeDataCodec { private static final long serialVersionUID = 3141427377277375077L; private int numChecksumBytes; private Supplier checksumSupplier; public ChecksumCodec(Supplier checksumSupplier, int numChecksumBytes) { this.checksumSupplier = checksumSupplier; this.numChecksumBytes = numChecksumBytes; } /** * Returns a new {@link Checksum} instance. * * @return the checksum */ public Checksum getChecksum() { return checksumSupplier.get(); } public int numChecksumBytes() { return numChecksumBytes; } private CheckedOutputStream createStream(OutputStream out) { final Checksum checksum = getChecksum(); return new CheckedOutputStream(out, checksum) { private boolean closed = false; @Override public void close() throws IOException { if (!closed) { writeChecksum(checksum, out); closed = true; out.close(); } } }; } @Override public ReadData encode(ReadData readData) { return readData.encode(this::createStream); } @Override public ReadData decode(ReadData readData) throws N5IOException { final ReadData rdm = readData.materialize(); final long N = rdm.requireLength(); final ReadData data = rdm.slice(0, N - numChecksumBytes); final long calculatedChecksum = computeChecksum(data); final ReadData checksumRd = rdm.slice(N - numChecksumBytes, numChecksumBytes); final long storedChecksum = readChecksum(checksumRd); if( calculatedChecksum != storedChecksum) throw new N5Exception(String.format("Calculated checksum (%d) does not match stored checksum (%d).", calculatedChecksum, storedChecksum)); return data; } @Override public long encodedSize(final long size) { return size + numChecksumBytes(); } protected long readChecksum(ReadData checksumData) { // the computed checksum is a long that can take values in [0, 2^32 - 1] // so convert the four bytes to an appropriate long ByteBuffer buf = ByteBuffer.allocate(8); buf.order(ByteOrder.LITTLE_ENDIAN); buf.put(checksumData.allBytes()); buf.putInt(0); buf.rewind(); return buf.getLong(); } protected long computeChecksum(ReadData data) { final Checksum checksum = getChecksum(); checksum.update(data.allBytes(), 0, (int)data.requireLength()); return checksum.getValue(); } /** * Return the value of the checksum as a {@link ByteBuffer} to be serialized. * * @return a ByteBuffer representing the checksum value */ public abstract ByteBuffer getChecksumValue(Checksum checksum); protected void writeChecksum(Checksum checksum, OutputStream out) throws IOException { out.write(getChecksumValue(checksum).array()); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/ChecksumException.java ================================================ 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); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/checksum/Crc32cChecksumCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec.checksum; import org.apache.commons.codec.digest.PureJavaCrc32C; import org.janelia.saalfeldlab.n5.codec.DataCodec; import org.janelia.saalfeldlab.n5.serialization.NameConfig; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.zip.Checksum; @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 PureJavaCrc32C(), 4); } @Override public ByteBuffer getChecksumValue(Checksum checksum) { final ByteBuffer buf = ByteBuffer.allocate(numChecksumBytes()); buf.order(ByteOrder.LITTLE_ENDIAN).putInt((int)checksum.getValue()); return buf; } @Override public String getType() { return TYPE; } @Override public DataCodec create() { return this; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/Transpose.java ================================================ package org.janelia.saalfeldlab.n5.codec.transpose; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.util.MemCopy; class Transpose { // TODO: detect when 1-sized dimensions are permuted. This should allow to // simplify (or completely avoid) copying under certain conditions. public static int[] encode(final int[] decodedPos, final int[] order) { final int[] encodedPos = new int[decodedPos.length]; encode(decodedPos, order, encodedPos); return encodedPos; } public static int[] decode(final int[] encodedPos, final int[] order) { final int[] decodedPos = new int[encodedPos.length]; decode(encodedPos, order, decodedPos); return decodedPos; } public static void encode(int[] decodedPos, int[] order, int[] encodedPos) { for (int d = 0; d < order.length; d++) encodedPos[d] = decodedPos[order[d]]; } public static void decode(int[] encodedPos, int[] order, int[] decodedPos) { for (int d = 0; d < order.length; d++) decodedPos[order[d]] = encodedPos[d]; } @SuppressWarnings("unchecked") public static Transpose of(final DataType dataType, final int numDimensions) { return (Transpose) new Transpose<>(MemCopy.forDataType(dataType), numDimensions); } private final MemCopy memCopy; private final int[] ssize; private final int[] tsize; private final int[] ssteps; private final int[] tsteps; private final int[] csteps; Transpose(final MemCopy memCopy, final int n) { this.memCopy = memCopy; ssize = new int[n]; tsize = new int[n]; ssteps = new int[n]; tsteps = new int[n]; csteps = new int[n]; } public void encode(final T decoded, final T encoded, final int[] decodedSize, final int[] order) { final int n = ssize.length; for (int d = 0; d < n; ++d) ssize[d] = decodedSize[d]; for (int d = 0; d < n; ++d) tsize[d] = decodedSize[order[d]]; ssteps[0] = 1; for (int d = 0; d < n - 1; ++d) ssteps[d + 1] = ssteps[d] * ssize[d]; tsteps[0] = 1; for (int d = 0; d < n - 1; ++d) tsteps[d + 1] = tsteps[d] * tsize[d]; for (int d = 0; d < n; ++d) csteps[order[d]] = tsteps[d]; copyRecursively(decoded, 0, encoded, 0, n - 1); } public void decode(final T encoded, final T decoded, final int[] decodedSize, final int[] order) { final int n = ssize.length; for (int d = 0; d < n; ++d) ssize[d] = decodedSize[order[d]]; ssteps[0] = 1; for (int d = 0; d < n - 1; ++d) ssteps[d + 1] = ssteps[d] * ssize[d]; tsteps[0] = 1; for (int d = 0; d < n - 1; ++d) tsteps[d + 1] = tsteps[d] * decodedSize[d]; for (int d = 0; d < n; ++d) csteps[d] = tsteps[order[d]]; copyRecursively(encoded, 0, decoded, 0, n - 1); } private void copyRecursively(final T src, final int srcPos, final T dest, final int destPos, final int d) { if (d == 0) { final int length = ssize[d]; final int stride = csteps[d]; memCopy.copyStrided(src, srcPos, dest, destPos, stride, length); } else { final int length = ssize[d]; final int srcStride = ssteps[d]; final int destStride = csteps[d]; for (int i = 0; i < length; ++i) copyRecursively(src, srcPos + i * srcStride, dest, destPos + i * destStride, d - 1); } } Transpose newInstance() { return new Transpose<>(memCopy, ssize.length); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposeCodec.java ================================================ package org.janelia.saalfeldlab.n5.codec.transpose; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.codec.DatasetCodec; public class TransposeCodec implements DatasetCodec { private final DataType dataType; private final int[] order; private final Transpose transpose; public TransposeCodec(final DataType dataType, final int[] order) { this.order = order; this.dataType = dataType; transpose = Transpose.of(dataType, order.length); } @Override public DataBlock encode(final DataBlock dataBlock) { DataBlock encodedBlock = (DataBlock)dataType.createDataBlock(dataBlock.getSize(), dataBlock.getGridPosition(), dataBlock.getNumElements()); transpose.encode(dataBlock.getData(), encodedBlock.getData(), dataBlock.getSize(), order); return encodedBlock; } @Override public DataBlock decode(final DataBlock dataBlock) { DataBlock decodedBlock = (DataBlock)dataType.createDataBlock(dataBlock.getSize(), dataBlock.getGridPosition(), dataBlock.getNumElements()); transpose.decode((T)dataBlock.getData(), decodedBlock.getData(), dataBlock.getSize(), order); return decodedBlock; } public static boolean isIdentity(final int[] permutation) { for (int i = 0; i < permutation.length; i++) if (permutation[i] != i) return false; return true; } public static boolean isReversal(final int[] permutation) { for (int i = 0; i < permutation.length; i++) if (permutation[i] != i) return false; return true; } public static int[] invertPermutation(final int[] p) { final int[] inv = new int[p.length]; for (int i = 0; i < p.length; i++) inv[p[i]] = i; return inv; } /** * Composes two permutations: result[i] = first[second[i]]. * * @param first * the first permutation * @param second * the second permutation * @return the composition of first and second */ public static int[] concatenatePermutations(final int[] first, final int[] second) { int n = first.length; final int[] result = new int[n]; for (int i = 0; i < n; i++) { result[i] = first[second[i]]; } return result; } /** * Conjugates a permutation with the reversal permutation: rev * p * rev^-1, * where rev is the permutation that reverses the elements. * * @param p * the permutation to conjugate * @return the conjugated permutation */ public static int[] conjugateWithReverse(final int[] p) { final int n = p.length; final int[] rev = new int[n]; for (int i = 0; i < n; i++) rev[i] = n - i - 1; // note that rev is its own inverse int[] result = concatenatePermutations(rev, p); // result = rev * p result = concatenatePermutations(result, rev); // result = rev * p * // rev^-1 return result; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/codec/transpose/TransposeCodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.codec.transpose; import java.util.Arrays; import java.util.stream.IntStream; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.codec.DatasetCodecInfo; import org.janelia.saalfeldlab.n5.serialization.NameConfig; /** * Describes a permutation of the dimensions of a block. *

* The {@code order} parameter parameterizes the permutation. * The ith element of the order array gives the destination index of the ith element of the input. * Example: * order = [1, 2, 0] * input = [7, 8, 9] // interpret as a block size * result = [9, 7, 8] // permuted block size * *

* See the specification of Zarr's Transpose codec. */ @NameConfig.Name(value = TransposeCodecInfo.TYPE) public class TransposeCodecInfo implements DatasetCodecInfo { public static final String TYPE = "n5-transpose"; @NameConfig.Parameter private int[] order; public TransposeCodecInfo() { // for serialization } public TransposeCodecInfo(int[] order) { this.order = order; } @Override public String getType() { return TYPE; } public int[] getOrder() { return order; } @Override public TransposeCodec create(DatasetAttributes datasetAttributes) { validate(); return new TransposeCodec<>(datasetAttributes.getDataType(), getOrder()); } @Override public boolean equals(Object obj) { if (obj instanceof TransposeCodecInfo) return Arrays.equals(order, ((TransposeCodecInfo)obj).getOrder()); return false; } private void validate() { final boolean[] indexFound = new boolean[order.length]; for( int i : order ) indexFound[i] = true; final int[] missingIndexes = IntStream.range(0, order.length).filter(i -> !indexFound[i]).toArray(); if( missingIndexes.length > 0 ) throw new N5Exception("Invalid order for TransposeCodec. Missing indexes: " + Arrays.toString(missingIndexes)); } public static TransposeCodecInfo concatenate(TransposeCodecInfo[] infos) { if( infos == null || infos.length == 0) return null; else if (infos.length == 1) return infos[0]; // copy the initial order so we don't modify to the original int[] order = new int[infos[0].order.length]; System.arraycopy(infos[0].order, 0, order, 0, order.length); for( int i = 1; i < infos.length; i++ ) order = TransposeCodec.concatenatePermutations(order, infos[i].order); return new TransposeCodecInfo(order); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/http/ApacheListResponseParser.java ================================================ package org.janelia.saalfeldlab.n5.http; import java.util.regex.Pattern; /** * {@link ListResponseParser}s for Apache HTTP Servers. */ abstract class ApacheListResponseParser extends PatternListResponseParser { private static final Pattern LIST_ENTRY = Pattern.compile("alt=\"\\[(\\s*|DIR)\\]\".*href=[^>]+>(?[^<]+)"); private static final Pattern LIST_DIR_ENTRY = Pattern.compile("alt=\"\\[DIR\\]\".*href=[^>]+>(?[^<]+)"); ApacheListResponseParser(Pattern pattern) { super(pattern); } static class ListDirectories extends ApacheListResponseParser { public ListDirectories() { super(LIST_DIR_ENTRY); } } static class ListAll extends ApacheListResponseParser { public ListAll() { super(LIST_ENTRY); } } public static ListResponseParser directoryParser() { return new ListDirectories(); } public static ListResponseParser parser() { return new ListAll(); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/http/CandidateListResponseParser.java ================================================ package org.janelia.saalfeldlab.n5.http; class CandidateListResponseParser implements ListResponseParser { private ListResponseParser[] candidateParsers; private ListResponseParser successfulParser; CandidateListResponseParser(ListResponseParser[] candidateParsers) { this.candidateParsers = candidateParsers; } @Override public String[] parseListResponse(String response) { if (successfulParser != null) return successfulParser.parseListResponse(response); String[] result = new String[0]; for (ListResponseParser parser : candidateParsers) { result = parser.parseListResponse(response); if (result.length > 1) { successfulParser = parser; return result; } } return result; } public ListResponseParser successfulParser() { return successfulParser; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/http/ListResponseParser.java ================================================ package org.janelia.saalfeldlab.n5.http; public interface ListResponseParser { /** * Parse a String response for a list call, and return the results as a * String array. * * @param response * an (http) response * @return the list elements it contains */ String[] parseListResponse(final String response); public static ListResponseParser defaultListParser() { return new CandidateListResponseParser( new ListResponseParser[]{ MicrosoftListResponseParser.parser(), ApacheListResponseParser.parser(), PythonListResponseParser.parser() }); } public static ListResponseParser defaultDirectoryListParser() { return new CandidateListResponseParser( new ListResponseParser[]{ MicrosoftListResponseParser.directoryParser(), ApacheListResponseParser.directoryParser(), PythonListResponseParser.directoryParser() }); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/http/MicrosoftListResponseParser.java ================================================ package org.janelia.saalfeldlab.n5.http; import java.util.regex.Pattern; /** * {@link ListResponseParser}s for Microsoft-IIS Servers. */ abstract class MicrosoftListResponseParser extends PatternListResponseParser { private static final Pattern LIST_ENTRY = Pattern.compile( "HREF=[^>]+>(?!\\[To Parent Directory])(?[^<]+)"); private static final Pattern LIST_DIR_ENTRY = Pattern.compile( "<dir>[^H]+HREF=[^>]+>(?!\\[To Parent Directory])(?[^<]+)"); MicrosoftListResponseParser(Pattern pattern) { super(pattern); } static class ListDirectories extends MicrosoftListResponseParser { public ListDirectories() { super(LIST_DIR_ENTRY); } } static class ListAll extends MicrosoftListResponseParser { public ListAll() { super(LIST_ENTRY); } } public static ListResponseParser directoryParser() { return new ListDirectories(); } public static ListResponseParser parser() { return new ListAll(); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/http/PatternListResponseParser.java ================================================ package org.janelia.saalfeldlab.n5.http; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * A {@link ListResponseParser} that uses a {@link Pattern}. * * This implementation of parseListResponse returns all the groups named "entry" * for all matches of the given pattern. */ class PatternListResponseParser implements ListResponseParser { protected Pattern pattern; public PatternListResponseParser(Pattern pattern) { this.pattern = pattern; } @Override public String[] parseListResponse(final String response) { final Matcher matcher = pattern.matcher(response); final List matches = new ArrayList<>(); while (matcher.find()) { matches.add(matcher.group("entry")); } return matches.toArray(new String[0]); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/http/PythonListResponseParser.java ================================================ package org.janelia.saalfeldlab.n5.http; import java.util.regex.Pattern; /** * {@link ListResponseParser}s for Python's SimpleHTTP Server. */ abstract class PythonListResponseParser extends PatternListResponseParser { private static final Pattern LIST_ENTRY = Pattern.compile("href=\"[^\"]+\">(?[^<]+)"); private static final Pattern LIST_DIR_ENTRY = Pattern.compile("href=\"[^\"]+\">(?[^<]+)/"); PythonListResponseParser(Pattern pattern) { super(pattern); } static class ListDirectories extends PythonListResponseParser { public ListDirectories() { super(LIST_DIR_ENTRY); } } static class ListAll extends PythonListResponseParser { public ListAll() { super(LIST_ENTRY); } } public static ListResponseParser directoryParser() { return new ListDirectories(); } public static ListResponseParser parser() { return new ListAll(); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/ByteArrayReadData.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.util.Arrays; class ByteArrayReadData implements ReadData { static final ReadData EMPTY = new ByteArrayReadData(new byte[0]); private final byte[] data; private final int offset; private final int length; ByteArrayReadData(final byte[] data) { this(data, 0, data.length); } ByteArrayReadData(final byte[] data, final int offset, final int length) { if (!validBounds(data.length, offset, length)) throw new IndexOutOfBoundsException(); this.data = data; this.offset = offset; if( length < 0 ) this.length = data.length - offset; else this.length = length; } @Override public long length() { return length; } @Override public long requireLength() { return length; } @Override public InputStream inputStream() { return new ByteArrayInputStream(data, offset, length); } @Override public byte[] allBytes() { if (offset == 0 && data.length == length) { return data; } else { return Arrays.copyOfRange(data, offset, offset + length); } } @Override public ReadData materialize() { return this; } @Override public ReadData slice(final long offset, final long length) { final int o = this.offset + (int)offset; return new ByteArrayReadData(data, o, (int)length); } private static boolean validBounds(int arrayLength, int offset, int length) { if (offset < 0) return false; else if (arrayLength > 0 && offset >= arrayLength) // offset == 0 and arrayLength == 0 is okay return false; else if (length >= 0 && offset + length > arrayLength) return false; return true; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/InputStreamReadData.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import java.io.DataInputStream; import java.io.IOException; import java.io.InputStream; import org.apache.commons.io.IOUtils; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; // not thread-safe class InputStreamReadData implements ReadData { private final InputStream inputStream; private final int length; InputStreamReadData(final InputStream inputStream, final int length) { this.inputStream = inputStream; this.length = length; } @Override public long length() { return (bytes != null) ? bytes.length() : length; } @Override public long requireLength() throws N5IOException { long l = length(); if ( l >= 0 ) { return l; } else { return materialize().length(); } } @Override public ReadData slice(final long offset, final long length) throws N5IOException { materialize(); return bytes.slice(offset, length); } @Override public ReadData limit(final long length) { return new InputStreamReadData(inputStream, (int)length); } private boolean inputStreamCalled = false; @Override public InputStream inputStream() throws IllegalStateException { if (bytes != null) { return bytes.inputStream(); } else if (!inputStreamCalled) { inputStreamCalled = true; return inputStream; } else { throw new IllegalStateException("InputStream() already called"); } } @Override public byte[] allBytes() throws N5IOException, IllegalStateException { materialize(); return bytes.allBytes(); } private ByteArrayReadData bytes; @Override public ReadData materialize() throws N5IOException { if (bytes == null) { final byte[] data; if (length >= 0) { data = new byte[length]; try (InputStream is = inputStream()) { new DataInputStream(is).readFully(data); } catch (IOException e) { throw new N5IOException(e); } } else { try (InputStream is = inputStream()) { data = IOUtils.toByteArray(is); } catch (IOException e) { throw new N5IOException(e); } } bytes = new ByteArrayReadData(data); } return this; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyGeneratedReadData.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import org.apache.commons.io.output.CountingOutputStream; import org.apache.commons.io.output.ProxyOutputStream; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; class LazyGeneratedReadData implements ReadData { LazyGeneratedReadData(final ReadData.Generator generator) { this.generator = generator; } /** * Construct a {@code LazyReadData} that uses the given {@code OutputStreamOperator} to * encode the given {@code ReadData}. * * @param data * the ReadData to encode * @param encoder * OutputStreamOperator to use for encoding */ LazyGeneratedReadData(final ReadData data, final OutputStreamOperator encoder) { this(outputStream -> { try (final OutputStream deflater = encoder.apply(interceptClose.apply(outputStream))) { data.writeTo(deflater); } }); } private final ReadData.Generator generator; private ByteArrayReadData bytes; private long length = -1; @Override public ReadData materialize() throws N5IOException { if (bytes == null) { try { final ByteArrayOutputStream baos = new ByteArrayOutputStream(8192); generator.writeTo(baos); bytes = new ByteArrayReadData(baos.toByteArray()); } catch (IOException e) { throw new N5IOException(e); } } return this; } @Override public long length() { return (bytes != null) ? bytes.length() : length; } @Override public long requireLength() throws N5IOException { long l = length(); if ( l >= 0 ) { return l; } else { return materialize().length(); } } @Override public ReadData slice(final long offset, final long length) throws N5IOException { materialize(); return bytes.slice(offset, length); } @Override public InputStream inputStream() throws N5IOException, IllegalStateException { materialize(); return bytes.inputStream(); } @Override public byte[] allBytes() throws N5IOException, IllegalStateException { materialize(); return bytes.allBytes(); } @Override public void writeTo(final OutputStream outputStream) throws N5IOException, IllegalStateException { try { if (bytes != null) { outputStream.write(bytes.allBytes()); } else { final CountingOutputStream cos = new CountingOutputStream(outputStream); generator.writeTo(cos); length = cos.getByteCount(); } } catch (IOException e) { throw new N5IOException(e); } } /** * {@code UnaryOperator} that wraps {@code OutputStream} to intercept {@code * close()} and call {@code flush()} instead */ private static final OutputStreamOperator interceptClose = o -> new ProxyOutputStream(o) { @Override public void close() throws IOException { out.flush(); } }; } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyRead.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import java.io.Closeable; import java.util.Collection; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; /** * A lazy reading strategy for lazy, partial reading of data from some source. *

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

* This method performs the actual read operation from the underlying * source, loading only the requested portion of data. The implementation * should handle bounds checking and throw appropriate exceptions for * invalid ranges. * * @param offset * the starting position in the data source * @param length * the number of bytes to read, or -1 to read from offset to end * * @return a materialized {@link ReadData} instance containing the requested * data * * @throws N5IOException * if any I/O error occurs */ ReadData materialize(long offset, long length) throws N5IOException; /** * Returns the total size of the data source in bytes. * * @return the size of the data source in bytes * * @throws N5IOException * if an I/O error occurs while trying to get the length */ long size() throws N5IOException; /** * Indicates that the given slices will be subsequently read. * {@code LazyRead} implementations (optionally) may take steps to prepare * for these subsequent slices. * * @param ranges * slice ranges to prefetch * * @throws N5IOException * if any I/O error occurs */ default void prefetch(final Collection ranges) throws N5IOException { } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/LazyReadData.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; /** * A {@link VolatileReadData} that is backed by a {@link LazyRead}. *

* The {@code LazyRead} is closed when this {@code LazyReadData} is closed. */ class LazyReadData implements VolatileReadData { private final LazyRead lazyRead; private ReadData materialized; private final long offset; private long length; LazyReadData(final LazyRead lazyRead) { this(lazyRead, 0, -1); } private LazyReadData(final LazyRead lazyRead, final long offset, final long length) { this.lazyRead = lazyRead; this.offset = offset; this.length = length; } @Override public ReadData materialize() throws N5IOException { if (materialized == null) { materialized = lazyRead.materialize(offset, length); length = materialized.length(); } return this; } /** * Returns a {@link ReadData} whose length is limited to the given value. *

* This implementation defers a material read operation if allowed * by the {@link LazyRead}. * * @param length * the length of the resulting ReadData * @return a length-limited ReadData * @throws N5IOException * if an I/O error occurs while trying to get the length */ @Override public ReadData slice(final long offset, final long length) { if (offset < 0) throw new IndexOutOfBoundsException("Negative offset: " + offset); if (materialized != null) return materialized.slice(offset, length); // if a slice of indeterminate length is requested, but the // length is already known, use the known length; final long lengthArg; if (this.length > 0 && length < 0) lengthArg = this.length - offset; else lengthArg = length; return new LazyReadData(lazyRead, this.offset + offset, lengthArg); } @Override public InputStream inputStream() throws N5IOException, IllegalStateException { materialize(); return materialized.inputStream(); } @Override public byte[] allBytes() throws N5IOException, IllegalStateException { materialize(); return materialized.allBytes(); } @Override public long length() { return length; } @Override public long requireLength() throws N5IOException { if (length < 0) { length = lazyRead.size() - offset; } return length; } @Override public void prefetch(final Collection ranges) throws N5IOException { lazyRead.prefetch(ranges); } @Override public void close() throws N5IOException { try { lazyRead.close(); } catch (IOException e) { throw new N5IOException(e); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/Range.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; /** * A range specified as a {@link #offset}, {@link #length} pair. */ public interface Range { /** * Order {@code Range}s by {@link #offset}. * Ranges with the same offset are ordered by {@link #length}. */ Comparator COMPARATOR = Comparator .comparingLong(Range::offset) .thenComparingLong(Range::length); /** * @return start index (inclusive) */ long offset(); /** * @return number of elements */ long length(); /** * @return end index (exclusive) */ default long end() { return offset() + length(); } static boolean equals(final Range r0, final Range r1) { if (r0 == null && r1 == null) { return true; } else if (r0 == null || r1 == null) { return false; } else { return r0.offset() == r1.offset() && r0.length() == r1.length(); } } static Range at(final long offset, final long length) { class DefaultRange implements Range { private final long offset; private final long length; public DefaultRange(final long offset, final long length) { this.offset = offset; this.length = length; } @Override public long offset() { return offset; } @Override public long length() { return length; } @Override public final boolean equals(final Object o) { if (!(o instanceof DefaultRange)) return false; final DefaultRange that = (DefaultRange) o; return offset == that.offset && length == that.length; } @Override public int hashCode() { int result = Long.hashCode(offset); result = 31 * result + Long.hashCode(length); return result; } @Override public String toString() { return "Range{" + "offset=" + offset + ", length=" + length + '}'; } } return new DefaultRange(offset, length); } /** * Returns a potentially new collection of {@link Range}s such that * adjacent or overlapping Ranges are combined. *

* If the input ranges are non-adjacent the input instance is returned, but if aggregation * occurs, the result will be a new, sorted List. * * @param ranges * a collection of Ranges * * @return collection with adjacent or overlapping ranges merged */ static Collection aggregate(final Collection ranges) { if (ranges.size() == 0) return ranges; final ArrayList sortedRanges = new ArrayList<>(ranges); sortedRanges.sort(Range.COMPARATOR); final ArrayList result = new ArrayList<>(); Range lo = null; for (Range hi : sortedRanges) { if (lo == null) lo = hi; else if (lo.end() >= hi.offset()) { // merge lo = Range.at(lo.offset(), Math.max(lo.end(), hi.end()) - lo.offset()); } else { result.add(lo); lo = hi; } } result.add(lo); final boolean wereMerges = ranges.size() != result.size(); return wereMerges ? result : ranges; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/ReadData.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.Collection; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.codec.DataCodec; /** * An abstraction over {@code byte[]} data. *

* The data may come from a {@code byte[]} array, a {@code ByteBuffer}, an * {@code InputStream}, a {@code KeyValueAccess}. *

* {@code ReadData} instances can be created via one of the static {@link #from} * methods. For example, use {@link #from(InputStream, int)} to wrap an {@code * InputStream}. *

* {@code ReadData} may be lazy-loaded. For example, for {@code InputStream} and * {@code KeyValueAccess} sources, loading is deferred until the data is * accessed (e.g., {@link #allBytes()}, {@link #writeTo(OutputStream)}), or * explicitly {@link #materialize() materialized}. *

* {@code ReadData} can be {@link DataCodec#encode encoded} and {@link * DataCodec#decode decoded} by a {@link DataCodec}, which will also be lazy if * possible. */ public interface ReadData { /** * Returns number of bytes in this {@link ReadData}, if known. Otherwise * {@code -1}. * * @return number of bytes, if known, or -1 */ default long length() { return -1; } /** * Returns number of bytes in this {@link ReadData}. If the length is not * currently know, this method may retrieve the length using I/O operations, * {@link #materialize} this {@code ReadData}, or perform any other steps * necessary to obtain the length. * * @return number of bytes * * @throws N5IOException * if an I/O error occurs while trying to get the length */ long requireLength() throws N5IOException; /** * Returns a {@link ReadData} whose length is limited to the given value. * * @param length * the length of the resulting ReadData * @return a length-limited ReadData * @throws N5IOException * if an I/O error occurs while trying to get the length */ default ReadData limit(final long length) throws N5IOException { return slice(0, length); } /** * Returns a new {@link ReadData} representing a slice, or subset * of this ReadData. * * @param offset the offset relative to this ReadData * @param length length of the returned ReadData * @return a slice * @throws N5IOException an exception */ default ReadData slice(final long offset, final long length) throws N5IOException { return materialize().slice(offset, length); } /** * Returns a new {@link ReadData} representing a slice, or subset * of this ReadData. * * @param range a range in this ReadData * @return a slice * @throws N5IOException an exception */ default ReadData slice(final Range range) throws N5IOException { return slice(range.offset(), range.length()); } /** * Open a {@code InputStream} on this data. *

* Repeatedly calling this method may or may not work, depending on how * the underlying data is stored. For example, if the underlying data is * stored as a {@code byte[]} array, multiple streams can be opened. If * the underlying data is just an {@code InputStream} then this will be * returned on the first call. * * @return an InputStream on this data * * @throws N5IOException * if any I/O error occurs * @throws IllegalStateException * if this method was already called once and cannot be called again. */ InputStream inputStream() throws N5IOException, IllegalStateException; /** * Return the contained data as a {@code byte[]} array. *

* This may use {@link #inputStream()} to read the data. * Because repeatedly calling {@link #inputStream()} may not work, *

    *
  1. this method may fail with {@code IllegalStateException} if {@code inputStream()} was already called
  2. *
  3. subsequent {@code inputStream()} calls may fail with {@code IllegalStateException}
  4. *
* * @return all contained data as a byte[] array * * @throws N5IOException * if any I/O error occurs * @throws IllegalStateException * if {@link #inputStream()} was already called once and cannot be called again. */ byte[] allBytes() throws N5IOException, IllegalStateException; /** * Return the contained data as a {@code ByteBuffer}. *

* This may use {@link #inputStream()} to read the data. * Because repeatedly calling {@link #inputStream()} may not work, *

    *
  1. this method may fail with {@code IllegalStateException} if {@code inputStream()} was already called
  2. *
  3. subsequent {@code inputStream()} calls may fail with {@code IllegalStateException}
  4. *
* The byte order of the returned {@code ByteBuffer} is {@code BIG_ENDIAN}. * * @return all contained data as a ByteBuffer * * @throws N5IOException * if any I/O error occurs * @throws IllegalStateException * if {@link #inputStream()} was already called once and cannot be called again. */ default ByteBuffer toByteBuffer() throws N5IOException, IllegalStateException { return ByteBuffer.wrap(allBytes()); } /** * Read the underlying data into a {@code byte[]} array, and return it as a {@code ReadData}. * (If this {@code ReadData} is already in a {@code byte[]} array or {@code * ByteBuffer}, just return {@code this}.) *

* The returned {@code ReadData} has a known {@link #length} and multiple * {@link #inputStream InputStreams} can be opened on it. *

* Implementation note: This should be preferably implemented to return * {@code this}. For example, materialize into a new {@code byte[]}, {@code * ReadData}, or similar and then delegate to this materialized version * internally. * * @return * a materialized ReadData. * @throws N5IOException * if any I/O error occurs */ ReadData materialize() throws N5IOException; /** * Write the contained data into an {@code OutputStream}. *

* This may use {@link #inputStream()} to read the data. * Because repeatedly calling {@link #inputStream()} may not work, *

    *
  1. this method may fail with {@code IllegalStateException} if {@code inputStream()} was already called
  2. *
  3. subsequent {@code inputStream()} calls may fail with {@code IllegalStateException}
  4. *
* * @param outputStream * destination to write to * * @throws N5IOException * if any I/O error occurs * @throws IllegalStateException * if {@link #inputStream()} was already called once and cannot be called again. */ default void writeTo(OutputStream outputStream) throws N5IOException, IllegalStateException { try { outputStream.write(allBytes()); } catch (IOException e) { throw new N5IOException(e); } } /** * Indicates that the given slices will be subsequently read. * {@code ReadData} implementations (optionally) may take steps to prepare * for these subsequent slices. * * @param ranges * slice ranges to prefetch * * @throws N5IOException * if any I/O error occurs */ default void prefetch(final Collection ranges) throws N5IOException { } // ------------- Encoding / Decoding ---------------- // /** * Returns a new ReadData that uses the given {@code OutputStreamOperator} to * encode this ReadData. * * @param encoder * OutputStreamOperator to use for encoding * * @return encoded ReadData */ default ReadData encode(OutputStreamOperator encoder) { return new LazyGeneratedReadData(this, encoder); } /** * {@code OutputStreamOperator} is {@link #apply applied} to an {@code * OutputStream} to transform it into another(e.g., compressed) {@code * OutputStream}. *

* This is basically {@code UnaryOperator}, but {@link #apply} * throws {@code IOException}. */ @FunctionalInterface interface OutputStreamOperator { OutputStream apply(OutputStream o) throws IOException; } // --------------- Factory Methods ------------------ // /** * Create a new {@code ReadData} that loads lazily from {@code inputStream} * and {@link #length() reports} the given {@code length}. *

* No effort is made to ensure that the {@code inputStream} in fact contains * exactly {@code length} bytes. * * @param inputStream * InputStream to read from * @param length * reported length of the ReadData * * @return a new ReadData */ static ReadData from(final InputStream inputStream, final int length) { return new InputStreamReadData(inputStream, length); } /** * Create a new {@code ReadData} that loads lazily from {@code inputStream} * and reports {@link #length() length() == -1} (i.e., unknown length). * * @param inputStream * InputStream to read from * * @return a new ReadData */ static ReadData from(final InputStream inputStream) { return from(inputStream, -1); } /** * Create a new {@code ReadData} that wraps the specified portion of a * {@code byte[]} array. * * @param data * array containing the data * @param offset * start offset of the ReadData in the data array * @param length * length of the ReadData (in bytes) * * @return a new ReadData */ static ReadData from(final byte[] data, final int offset, final int length) { return new ByteArrayReadData(data, offset, length); } /** * Create a new {@code ReadData} that wraps the given {@code byte[]} array. * * @param data * array containing the data * * @return a new ReadData */ static ReadData from(final byte[] data) { return from(data, 0, data.length); } /** * Create a new {@code ReadData} that wraps the given {@code ByteBuffer}. * * @param data * buffer containing the data * * @return a new ReadData */ static ReadData from(final ByteBuffer data) { if (data.hasArray()) { return from(data.array(), 0, data.limit()); } else { throw new UnsupportedOperationException("TODO. Direct ByteBuffer not supported yet."); } } /** * Generator supplies the content of a ReadData by writing it to an {@link * OutputStream} on demand. *

* An example of a {@code Generator} is applying a compressor (an {@link * OutputStreamOperator}) to an existing ReadData. */ @FunctionalInterface interface Generator { void writeTo(OutputStream outputStream) throws IOException, IllegalStateException; } /** * Create a new {@code ReadData} that is lazily generated by the given {@link Generator}. * * @param generator * generates the data * * @return a new ReadData */ static ReadData from(Generator generator) { return new LazyGeneratedReadData(generator); } /** * Returns an empty {@code ReadData}. * * @return an empty ReadData */ static ReadData empty() { return ByteArrayReadData.EMPTY; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/VolatileReadData.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.prefetch.AggregatingPrefetchLazyRead; /** * During its life-time, the content of a {@code VolatileReadData} should not be * mutated. *

* Implementations can enforce this by *

    *
  • locking underlying resources until the {@code VolatileReadData} is {@link #close() closed}), or
  • *
  • failing with {@link N5IOException} if such modifications are detected.
  • *
*/ public interface VolatileReadData extends ReadData, AutoCloseable { @Override void close() throws N5IOException; /** * Create a new {@code VolatileReadData} that loads (lazily) from {@code lazyRead}. *

* The returned {@code VolatileReadData} is responsible for {@link LazyRead#close() closing} * * @param lazyRead * provides data * * @return a new VolatileReadData */ static VolatileReadData from(final LazyRead lazyRead) { final LazyRead aggregatingLazyRead = new AggregatingPrefetchLazyRead(lazyRead); return new LazyReadData(aggregatingLazyRead); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/prefetch/AggregatingPrefetchLazyRead.java ================================================ package org.janelia.saalfeldlab.n5.readdata.prefetch; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.Range; /** * A {@link SliceTrackingLazyRead} that implements {@link #prefetch} to * aggregate overlapping / adjacent ranges and then materialize each aggregated * range. */ public class AggregatingPrefetchLazyRead extends SliceTrackingLazyRead { public AggregatingPrefetchLazyRead(final LazyRead delegate) { super(delegate); } /** * Indicates that the given slices will be subsequently read. *

* This implementation groups overlapping / adjacent {@link Range}s into single read requests. * * @param ranges * slice ranges to prefetch * * @throws N5IOException * if any I/O error occurs */ @Override public void prefetch(final Collection ranges) throws N5IOException { final List filteredRanges = new ArrayList<>(ranges); filteredRanges.removeIf(this::isCovered); final Collection aggregatedRanges = Range.aggregate(filteredRanges); for (final Range slice : aggregatedRanges) { materialize(slice.offset(), slice.length()); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/prefetch/EnclosingPrefetchLazyRead.java ================================================ package org.janelia.saalfeldlab.n5.readdata.prefetch; import java.util.Collection; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.Range; /** * A {@link SliceTrackingLazyRead} that implements {@link #prefetch} to * materialize the bounding range of all requested ranges. */ public class EnclosingPrefetchLazyRead extends SliceTrackingLazyRead { public EnclosingPrefetchLazyRead(final LazyRead delegate) { super(delegate); } /** * Indicates that the given slices will be subsequently read. * {@code LazyRead} implementations (optionally) may take steps to prepare * for these subsequent slices. *

* Minimal implementation: Find offset and length covering all ranges that * are not yet fully covered by existing slices. Then materialize the slice * covering that range. * * @param ranges * slice ranges to prefetch * * @throws N5IOException * if any I/O error occurs */ @Override public void prefetch(final Collection ranges) throws N5IOException { long fromIndex = Long.MAX_VALUE; long toIndex = Long.MIN_VALUE; for (final Range slice : ranges) { if (!isCovered(slice)) { fromIndex = Math.min(fromIndex, slice.offset()); toIndex = Math.max(toIndex, slice.end()); } } if (fromIndex < toIndex) { materialize(fromIndex, toIndex - fromIndex); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/prefetch/SliceTrackingLazyRead.java ================================================ package org.janelia.saalfeldlab.n5.readdata.prefetch; import java.io.IOException; import java.util.ArrayList; import java.util.List; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * A {@link LazyRead} that wraps a delegate {@code LazyRead} and keeps track of * all slices that have been {@link #materialize materialized}. *

* When materializing a new slice, we first check whether it is completely * covered by a materialized slice that we already track. If so, then we just * return a slice on the existing materialized slice. If not, we materialize the * slice from the delegate track it. */ public class SliceTrackingLazyRead implements LazyRead { protected static class Slice implements Range { // Offset and length in the delegate private final long offset; private final long length; // Data of this slice private final ReadData data; Slice(final long offset, final long length, final ReadData data) { this.offset = offset; this.length = length; this.data = data; } @Override public long offset() { return offset; } @Override public long length() { return length; } @Override public String toString() { return "{" + offset + ", " + length + '}'; } } protected final List slices = new ArrayList<>(); /** * The {@code LazyRead} providing our data. */ private final LazyRead delegate; public SliceTrackingLazyRead(final LazyRead delegate) { this.delegate = delegate; } @Override public void close() throws IOException { delegate.close(); } @Override public ReadData materialize(final long offset, final long length) throws N5IOException { final Slice containing = Slices.findContainingSlice(slices, offset, length); if (containing != null) { return containing.data.slice(offset - containing.offset, length); } else { final ReadData data = delegate.materialize(offset, length); Slices.addSlice(slices, new Slice(offset, length, data)); return data; } } @Override public long size() throws N5IOException { return delegate.size(); } protected boolean isCovered(final Range slice) { return Slices.findContainingSlice(slices, slice) != null; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/prefetch/Slices.java ================================================ package org.janelia.saalfeldlab.n5.readdata.prefetch; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.janelia.saalfeldlab.n5.readdata.Range; class Slices { private Slices() { // utility class. should not be instantiated. } /** * In an ordered list of {@code slices}, find a slice that completely contains the given range. *

* Pre-conditions: *

    *
  1. Slices are ordered by offset.
  2. *
  3. If two slices overlap, no slice is fully contained within the other. * (Therefore, if {@code a.offset < b.offset} then {@code a.end < b.end}.)
  4. *
* * @param slices * ordered list of slices * @param offset * start of the range to cover * @param length * length of the range to cover * * @return a slice that completely contains the requested range, or {@code null} if no such slice exists */ static T findContainingSlice(final List slices, final long offset, final long length) { // Find the slice with the largest slice.offset <= offset. final int i = Collections.binarySearch(slices, Range.at(offset, 0), Comparator.comparingLong(Range::offset)); // Largest index of a slice with slice.offset <= offset. final int index = i < 0 ? -i - 2 : i; if (index < 0) { // We find no overlapping slice, because // slices[0].offset is already too large. return null; } final T slice = slices.get(index); if (slice.end() < offset + length) { return null; } return slice; } /** * In an ordered list of {@code slices}, find a slice that completely contains the given range. *

* Pre-conditions: *

    *
  1. Slices are ordered by offset.
  2. *
  3. If two slices overlap, no slice is fully contained within the other. * (Therefore, if {@code a.offset < b.offset} then {@code a.end < b.end}.)
  4. *
* * @param slices * ordered list of slices * @param range * range to cover * * @return a slice that completely contains the requested range, or {@code null} if no such slice exists */ static T findContainingSlice(final List slices, final Range range) { return findContainingSlice(slices, range.offset(), range.length()); } /** * Add a new {@code slice} to the {@code slice} list. *

* Note, that the new {@code slice} is expected to not be fully contained in * an existing slice! *

* Pre/post-conditions: *

    *
  1. Slices are ordered by offset.
  2. *
  3. If two slices overlap, no slice is fully contained within the other. * (Therefore, if {@code a.offset < b.offset} then {@code a.end < b.end}.)
  4. *
*

* The new {@code slice} will be inserted into the list at the correct position * (such that {@code slices} remains ordered by slice offset), and all existing * slices that are fully contained in the new {@code slice} will be removed. * * @param slices * ordered list of slices * @param slice * slice to be inserted */ static void addSlice(final List slices, final T slice) { final int i = Collections.binarySearch(slices, slice, Comparator.comparingLong(Range::offset)); final int from = i < 0 ? -i - 1 : i; int to = from; while (to < slices.size() && slices.get(to).end() <= slice.end()) { ++to; } if (from == to) { // empty range: just insert slices.add(from, slice); } else { // overwrite the first element in range, remove the rest slices.set(from, slice); slices.subList(from + 1, to).clear(); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/segment/ConcatenatedReadData.java ================================================ package org.janelia.saalfeldlab.n5.readdata.segment; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * Implementation of a {@link SegmentedReadData} representing the concatenation * of several {@code SegmentedReadData}s. *

* {@code ConcatenatedReadData} contains the segments of all concatenated {@code * SegmentedReadData}s with appropriately offset locations. *

* In particular, it is also possible to concatenate {@code SegmentedReadData}s * with (yet) unknown length. (This is useful for postponing compression of * DataBlocks until they are actually written.) In that case, segment locations * are only available after all lengths become known. This happens when this * {@code ConcatenatedReadData} (or all its constituents) is * {@link #materialize() materialized} or {@link #writeTo(OutputStream) written}. */ class ConcatenatedReadData implements SegmentedReadData { private final List content; private final ReadData delegate; private final List segments; private final List locations; private final Map segmentToLocation; private boolean locationsBuilt; private long length; ConcatenatedReadData(final List content) { this.content = content; delegate = ReadData.from(os -> content.forEach(d -> d.writeTo(os))); segments = new ArrayList<>(); locations = new ArrayList<>(); segmentToLocation = new HashMap<>(); locationsBuilt = false; length = -1; } // constructor for slices private ConcatenatedReadData(final ReadData delegate, final List segments, final List locations) { content = null; this.delegate = delegate; this.segments = segments; this.locations = locations; segmentToLocation = new HashMap<>(); for (int i = 0; i < segments.size(); i++) { segmentToLocation.put(segments.get(i), locations.get(i)); } locationsBuilt = true; } /** * Verify that all {@code content} elements have known length. * Builds {@code segments} and {@code locations} if they have not been built yet. * * @throws IllegalStateException if any of the concatenated ReadData don't know their length yet */ private void ensureKnownSize() throws IllegalStateException { if (!locationsBuilt) { long offset = 0; for (int i = 0; i < content.size(); i++) { final SegmentedReadData data = content.get(i); if (data.length() < 0) { throw new IllegalStateException("Some of concatenated ReadData don't know their length yet."); } segments.addAll(data.segments()); for (Segment segment : data.segments()) { final Range l = data.location(segment); locations.add(Range.at(l.offset() + offset, l.length())); } offset += data.length(); } length = offset; for (int i = 0; i < segments.size(); i++) { segmentToLocation.put(segments.get(i), locations.get(i)); } locationsBuilt = true; } } @Override public Range location(final Segment segment) throws IllegalArgumentException { ensureKnownSize(); final Range location = segmentToLocation.get(segment); if (location == null) { throw new IllegalArgumentException(); } return location; } @Override public List segments() { return segments; } @Override public long length() { if (length < 0) { length = 0; for (final ReadData data : content) { final long l = data.length(); if (l < 0) { length = -1; break; } length += l; } } return length; } @Override public long requireLength() throws N5IOException { if (length < 0) { length = 0; for (final ReadData data : content) { length += data.requireLength(); } } return length; } @Override public SegmentedReadData slice(final Segment segment) throws IllegalArgumentException, N5IOException { ensureKnownSize(); final Range l = location(segment); return slice(l.offset(), l.length()); } @Override public SegmentedReadData slice(final long offset, final long length) throws N5IOException { ensureKnownSize(); final ReadData delegateSlice = delegate.slice(offset, length); final long sliceLength = delegateSlice.length(); // fromIndex: find first segment with offset >= sourceOffset int fromIndex = Collections.binarySearch(locations, Range.at(offset, -1), Range.COMPARATOR); if (fromIndex < 0) { fromIndex = -fromIndex - 1; } // toIndex: find first segment with offset >= sourceOffset + length int toIndex = Collections.binarySearch(locations, Range.at(offset + sliceLength, -1), Range.COMPARATOR); if (toIndex < 0) { toIndex = -toIndex - 1; } // contained: find segments in [fromIndex, toIndex) with s.offset() + s.length() <= sourceOffset + length final List containedSegments = new ArrayList<>(); final List containedSegmentLocations = new ArrayList<>(); for (int i = fromIndex; i < toIndex; ++i) { final Range l = locations.get(i); if (l.offset() + l.length() <= offset + sliceLength) { containedSegments.add(segments.get(i)); containedSegmentLocations.add(Range.at(l.offset() - offset, l.length())); } } return new ConcatenatedReadData(delegateSlice, containedSegments, containedSegmentLocations); } @Override public InputStream inputStream() throws N5IOException, IllegalStateException { return delegate.inputStream(); } @Override public byte[] allBytes() throws N5IOException, IllegalStateException { return delegate.allBytes(); } @Override public ByteBuffer toByteBuffer() throws N5IOException, IllegalStateException { return delegate.toByteBuffer(); } @Override public SegmentedReadData materialize() throws N5IOException { delegate.materialize(); return this; } @Override public void writeTo(final OutputStream outputStream) throws N5IOException, IllegalStateException { delegate.writeTo(outputStream); } @Override public ReadData encode(final OutputStreamOperator encoder) { return delegate.encode(encoder); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/segment/DefaultSegmentedReadData.java ================================================ package org.janelia.saalfeldlab.n5.readdata.segment; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * Implementation of a {@link SegmentedReadData} wrapper around an existing {@code ReadData} delegate. *

* It is used for *

    *
  • adding segment information to vanilla ReadData (see {@link SegmentedReadData#wrap(ReadData, Range...)}),
  • *
  • representing slices into other {@code DefaultSegmentedReadData}.
  • *
segments; private static class SegmentImpl implements Segment, Range { private final SegmentedReadData source; private final long offset; private final long length; public SegmentImpl(final SegmentedReadData source, final Range location) { this(source, location.offset(), location.length()); } public SegmentImpl(final SegmentedReadData source, final long offset, final long length) { this.source = source; this.offset = offset; this.length = length; } @Override public long offset() { return offset; } @Override public long length() { return length; } @Override public SegmentedReadData source() { return source; } } private static class EnclosingSegmentImpl extends SegmentImpl { public EnclosingSegmentImpl(final SegmentedReadData source) { super(source, 0, -1); } @Override public long length() { return source().length(); } } // assumes segments are ordered by location private DefaultSegmentedReadData(final ReadData delegate, final ReadData segmentSource, final long offset, final List segments) { this.delegate = delegate; this.segmentSource = segmentSource; this.offset = offset; this.segments = segments; } // assumes segments are ordered by location private DefaultSegmentedReadData(final ReadData delegate, final List segments) { this.delegate = delegate; this.segmentSource = this; this.offset = 0; this.segments = segments; } /** * Wrap {@code readData} and create segments at the given locations. The * order of segments in the returned {@link SegmentsAndData#segments()} list * matches the order of the given {@code locations} (while the {@link * #segments} in the {@link SegmentsAndData#data()} are ordered by offset). */ static SegmentsAndData wrap(final ReadData readData, final List locations) { final List sortedSegments = new ArrayList<>(locations.size()); final DefaultSegmentedReadData data = new DefaultSegmentedReadData(readData, sortedSegments); for (Range l : locations) { sortedSegments.add(new SegmentImpl(data, l)); } final List segments = new ArrayList<>(sortedSegments); sortedSegments.sort(Range.COMPARATOR); return new SegmentsAndData() { @Override public List segments() { return segments; } @Override public SegmentedReadData data() { return data; } }; } /** * Wrap the given {@code delegate} with a single segment fully containing it. */ DefaultSegmentedReadData(final ReadData delegate) { this.delegate = delegate; this.segmentSource = this; this.offset = 0; this.segments = Collections.singletonList(new EnclosingSegmentImpl(this)); } @Override public Range location(final Segment segment) { if (segmentSource.equals(segment.source()) && segment instanceof Range) { final Range l = (Range) segment; return offset == 0 ? l : Range.at(l.offset() - offset, l.length()); } else { throw new IllegalArgumentException(); } } @Override public List segments() { return segments; } @Override public long length() { return delegate.length(); } @Override public long requireLength() throws N5IOException { return delegate.requireLength(); } @Override public SegmentedReadData slice(final Segment segment) throws IllegalArgumentException, N5IOException { if (segmentSource.equals(segment.source())) { if (segment instanceof EnclosingSegmentImpl) { return this; } else if (segment instanceof SegmentImpl) { final SegmentImpl s = (SegmentImpl) segment; return new DefaultSegmentedReadData( delegate.slice(s.offset(), s.length()), segmentSource, this.offset + s.offset(), Collections.singletonList(s)); } } throw new IllegalArgumentException(); } @Override public SegmentedReadData slice(final long offset, final long length) throws N5IOException { final ReadData delegateSlice = delegate.slice(offset, length); final long sourceOffset = this.offset + offset; final long sliceLength = delegateSlice.length(); // fromIndex: find first segment with offset >= sourceOffset int fromIndex = Collections.binarySearch(segments, Range.at(sourceOffset, -1), Range.COMPARATOR); if (fromIndex < 0) { fromIndex = -fromIndex - 1; } // toIndex: find first segment with offset >= sourceOffset + length int toIndex; if (sliceLength < 0) { toIndex = segments.size(); } else { toIndex = Collections.binarySearch(segments, Range.at(sourceOffset + sliceLength, -1), Range.COMPARATOR); if (toIndex < 0) { toIndex = -toIndex - 1; } } // contained: find segments in [fromIndex, toIndex) with s.offset() + s.length() <= sourceOffset + length final List candidates = segments.subList(fromIndex, toIndex); final List sliceSegments; if (sliceLength < 0) { sliceSegments = candidates; } else { final ArrayList contained = new ArrayList<>(candidates.size()); candidates.forEach(s -> { if (s.offset() + s.length() <= sourceOffset + sliceLength) { contained.add(s); } }); contained.trimToSize(); sliceSegments = contained; } return new DefaultSegmentedReadData( delegateSlice, segmentSource, sourceOffset, sliceSegments); } @Override public InputStream inputStream() throws N5IOException, IllegalStateException { return delegate.inputStream(); } @Override public byte[] allBytes() throws N5IOException, IllegalStateException { return delegate.allBytes(); } @Override public ByteBuffer toByteBuffer() throws N5IOException, IllegalStateException { return delegate.toByteBuffer(); } @Override public SegmentedReadData materialize() throws N5IOException { delegate.materialize(); return this; } @Override public void writeTo(final OutputStream outputStream) throws N5IOException, IllegalStateException { delegate.writeTo(outputStream); } @Override public void prefetch(final Collection ranges) throws N5IOException { delegate.prefetch(ranges); } /** * Returns a new ReadData that uses the given {@code OutputStreamOperator} to * encode this SegmentedReadData. *

* Note that segments are lost by encoding. * * @param encoder * OutputStreamOperator to use for encoding * * @return encoded ReadData */ @Override public ReadData encode(final OutputStreamOperator encoder) { return delegate.encode(encoder); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/segment/Segment.java ================================================ package org.janelia.saalfeldlab.n5.readdata.segment; /** * A particular segment in a source {@link SegmentedReadData}. */ public interface Segment { /** * Returns the {@code SegmentedReadData} on which this segment is originally * defined. (The segment is tracked through slices and concatenations, but * the source will remain the same.) *

* This is mostly just used internally to make {@code SegmentedReadData} * implementations easier. The only real use for {@code source()} outside of * that is to get a {@code ReadData} containing exactly this segment, using * {@code segment.source().slice(segment)}. * * @return the {@code SegmentedReadData} on which this segment is originally defined */ SegmentedReadData source(); } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/readdata/segment/SegmentedReadData.java ================================================ package org.janelia.saalfeldlab.n5.readdata.segment; import java.io.OutputStream; import java.util.Arrays; import java.util.List; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.ReadData; /** * A {@code ReadData} which keeps track of contained {@link Segment}s. *

* A {@code Segment} refers to the data in a particular {@link Range}. * (That range can be obtained using {@link #location(Segment)}). Segments are * pulled along when {@link #slice(long, long)} slicing} or {@link #concatenate * concatenating} {@code SegmentedReadData} (and will have appropriately offset * {@link #location locations} in the slice/concatenation). */ public interface SegmentedReadData extends ReadData { interface SegmentsAndData { List segments(); SegmentedReadData data(); } /** * Wrap a {@link ReadData} and create one segment comprising the entire * {@code * readData}. The segment can be retrieved as the first (and only) element * of {@link SegmentedReadData#segments()}. * * @param readData * the ReadData to wrap * @return the SegmentedReadData */ static SegmentedReadData wrap(ReadData readData) { return new DefaultSegmentedReadData(readData); } /** * Wrap {@code readData} and create segments at the given locations. The * order of segments in the returned {@link SegmentsAndData#segments()} list * matches the order of the given {@code locations} (while the * {@link #segments} in the {@link SegmentsAndData#data()} are ordered by * offset). * * @param readData * the ReadData to wrap * @param locations * the ranges for segments * @return the SegmentsAndData */ static SegmentsAndData wrap(ReadData readData, Range... locations) { return wrap(readData, Arrays.asList(locations)); } /** * Wrap {@code readData} and create segments at the given locations. The * order of segments in the returned {@link SegmentsAndData#segments()} list * matches the order of the given {@code locations} (while the * {@link #segments} in the {@link SegmentsAndData#data()} are ordered by * offset). * * @param readData * the ReadData to wrap * @param locations * the ranges for segments * @return the SegmentsAndData */ static SegmentsAndData wrap(ReadData readData, List locations) { return DefaultSegmentedReadData.wrap(readData, locations); } /** * Return a {@link SegmentedReadData} representing the concatenation of the * given {@code readDatas}. The concatenation contains the segments of all * concatenated {@code readData}s with appropriately offset locations. *

* In particular, it is also possible to concatenate * {@code SegmentedReadData}s with (yet) unknown length. (This is useful for * postponing compression of DataBlocks until they are actually written.) In * that case, segment locations are only available after all lengths become * known. This happens when concatenation (or all its constituents) is * {@link #materialize() materialized} or {@link #writeTo(OutputStream) * written}. * * @param readDatas * a list of ReadDatra to concatenate * @return the SegmentedReadData comprising all the input readDatas */ static SegmentedReadData concatenate(List readDatas) { return new ConcatenatedReadData(readDatas); } /** * Returns the location of {@code segment} in this {@code ReadData}. *

* Note that this {@code ReadData} is not necessarily the source of the * segment. *

* The returned {@code Range} may be {@code {offset=0, length=-1}}, which * means that the segment comprises this whole {@code ReadData} (and the * length of this {@code ReadData} is not yet known). * * @param segment * the segment id * * @return location of the segment, or null * * @throws IllegalArgumentException * if the segment is not contained in this ReadData */ Range location(Segment segment) throws IllegalArgumentException; /** * Return all segments (fully) contained in this {@code ReadData}, ordered * by location (that is, sorted by {@link Range#COMPARATOR}). * * @return all segments contained in this {@code ReadData}. */ List segments(); @Override default SegmentedReadData limit(final long length) throws N5Exception.N5IOException { return slice(0, length); } /** * Return a {@code SegmentedReadData} wrapping a slice containing exactly * the given segment. *

* The {@link #location} of the given {@code segment} in this ReadData * specifies the range to slice. * * @param segment segment to slice * @return a slice * @throws IllegalArgumentException * if the segment is not contained in this ReadData * @throws N5Exception.N5IOException */ SegmentedReadData slice(Segment segment) throws IllegalArgumentException, N5Exception.N5IOException; /** * Returns a new {@link SegmentedReadData} representing a slice, or subset * of this ReadData. *

* The {@link #segments} of the returned SegmentedReadData are all segments * fully contained in the requested range. * * @param offset the offset relative to this ReadData * @param length length of the returned ReadData * @return a slice * @throws N5Exception.N5IOException an exception */ @Override SegmentedReadData slice(long offset, long length) throws N5Exception.N5IOException; /** * Returns a new {@link SegmentedReadData} representing a slice, or subset * of this ReadData. *

* The {@link #segments} of the returned SegmentedReadData are all segments * fully contained in the requested range. * * @param range a range in this ReadData * @return a slice * @throws N5Exception.N5IOException an exception */ @Override default SegmentedReadData slice(final Range range) throws N5Exception.N5IOException { return slice(range.offset(), range.length()); } @Override SegmentedReadData materialize() throws N5Exception.N5IOException; } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/serialization/JsonArrayUtils.java ================================================ package org.janelia.saalfeldlab.n5.serialization; import com.google.gson.JsonArray; import com.google.gson.JsonElement; public class JsonArrayUtils { /** * Reverses the order of elements in a JSON array in-place. * * @param array the JSON array to reverse; must not be null * @see N5Annotations.ReverseArray */ 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); } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/serialization/N5Annotations.java ================================================ 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; /** * Provides specialized annotations for N5 serialization behaviors. *

* This interface defines annotations that control specific serialization * transformations needed for N5 compatibility across different storage * formats, for example, when dealing with dimension ordering conventions. * * @see ReverseArray */ public interface N5Annotations extends Serializable { /** * Indicates that an array field should be reversed during serialization/deserialization. *

* This annotation is used to handle dimension ordering differences between storage formats. * For example, Zarr uses C-order (row-major) dimension ordering [Z, Y, X], while N5 uses * F-order (column-major) dimension ordering [X, Y, Z]. *

* This ensures that dimension-related arrays maintain the correct semantic meaning * across different storage format conventions. */ @Inherited @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @interface ReverseArray { } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/serialization/NameConfig.java ================================================ 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; /** * Configuration interface for N5 serialization naming and parameter annotations. *

* This interface provides a standardized way to configure serialization names and parameters * for N5 components such as compression algorithms and codecs. It defines annotations that * control how classes and their fields are serialized and deserialized for the N5 API. *

* Classes implementing this interface can use the provided annotations to: *

    *
  • Define a serialization type name with {@link Name @Name}
  • *
  • Specify a namespace prefix with {@link Prefix @Prefix}
  • *
  • Mark fields as serialization parameters with {@link Parameter @Parameter}
  • *
* * @see Name * @see Prefix * @see Parameter */ public interface NameConfig extends Serializable { /** * Defines a namespace prefix for serialization. *

* This annotation specifies a prefix that is prepended to the serialization * type name, creating a namespaced identifier. This is useful for organizing * related components into logical groups, usually all the components implementing * a particular interface. * * @see Name */ @Retention(RetentionPolicy.RUNTIME) @Inherited @Target(ElementType.TYPE) @interface Prefix { String value(); } /** * Specifies the serialization type name for a class. *

* This annotation defines the string identifier used during serialization and * deserialization to identify the type. The name should be unique within its * namespace and is typically a short, descriptive identifier. * * @see Prefix */ @Retention(RetentionPolicy.RUNTIME) @Inherited @Target(ElementType.TYPE) @Indexable @interface Name { String value(); } /** * Controls whether a class should be serializable as a {@code NameConfig}. *

* This annotation allows explicitly enabling or disabling serialization for a class. *

* By default, classes are serialized. */ @Retention(RetentionPolicy.RUNTIME) @Inherited @Target(ElementType.TYPE) @Indexable @interface Serialize { boolean value() default true; } /** * Marks a field as a parameter to be serialized. *

* This annotation identifies fields that should be included during serialization * and deserialization. It supports both required and optional parameters. *

* The {@code value} attribute can be used to specify an alternative name for * the parameter during serialization. If not specified, the field name is used. * * @see Name */ @Retention(RetentionPolicy.RUNTIME) @Inherited @Target(ElementType.FIELD) @interface Parameter { /** * Alternative name for the parameter during serialization. * If empty, the field name is used. * * @return the parameter name, or empty string for field name */ String value() default ""; /** * Whether this parameter is optional. * Optional parameters may be omitted during deserialization. * * @return {@code true} if the parameter is optional, {@code false} otherwise */ boolean optional() default false; } /** * Returns the serialization type name for this instance. *

* This method retrieves the value from the {@link Name @Name} annotation * if present on the class. * * @return the type name from the {@code @Name} annotation, or {@code null} if not annotated */ default String getType() { final Name type = getClass().getAnnotation(Name.class); return type == null ? null : type.value(); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/DatasetAccess.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.N5Writer.DataBlockSupplier; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedGrid; /** * Wrap an instantiated DataBlock/block codec hierarchy to implement (single and * batch) DataBlock read/write methods. * * @param * type of the data contained in the DataBlock */ public interface DatasetAccess { /** * Read the chunk at the given {@code gridPosition}. *

* If the requested chunk doesn't exist, then this method will return {@code * null}. {@code N5IOException} will be thrown if something goes wrong * reading or decoding data for an existing key. * * @param pva * dataset storage * @param gridPosition * grid position of the chunks to read * * @return the DataBlock or {@code null} * * @throws N5IOException * if any error occurs while reading or decoding the block */ DataBlock readChunk(PositionValueAccess pva, long[] gridPosition) throws N5IOException; /** * Read the chunks at the given {@code gridPositions}. *

* The returned {@code List>} is in the same order as the * requested {@code gridPositions}. That is, the {@code DataBlock} at index * {@code i} has grid coordinates {@code gridPositions.get(i)}. *

* If a requested chunk doesn't exist, then the corresponding element in the * result list will be {@code null}. ({@code N5IOException} will only be * thrown if something goes wrong reading or decoding data for an existing * key.) * * @param pva * dataset storage * @param gridPositions * list of grid positions of the chunks to read * @return list of DataBlocks * * @throws N5IOException * if any error occurs while reading or decoding blocks */ List> readChunks(PositionValueAccess pva, List gridPositions) throws N5IOException; /** * Writes a chunk to the {@link DataBlock#getGridPosition() grid position} * specified by {@code chunk}. */ void writeChunk(PositionValueAccess pva, DataBlock chunk) throws N5IOException; /** * Writes multiple chunks to the {@link DataBlock#getGridPosition() grid * positions} specified by the respective {@code chunks}. */ void writeChunks(PositionValueAccess pva, List> chunks) throws N5IOException; /** * Deletes the chunk at {@code gridPosition}. * * @return true if it existed and was deleted. */ boolean deleteChunk(PositionValueAccess pva, long[] gridPosition) throws N5IOException; /** * Deletes the chunks at the given {@code positions}. * * @return true if any existed and were deleted. */ boolean deleteChunks(PositionValueAccess pva, List gridPositions) throws N5IOException; /** * @param pva * @param min * min pixel coordinate of region to write * @param size * size in pixels of region to write * @param chunkSupplier * is asked to create chunks within the given region * @param writeFully * if false, merge existing data in blocks/chunks that overlap the region boundary. if true, override everything. * * @throws N5IOException */ void writeRegion( PositionValueAccess pva, long[] min, long[] size, DataBlockSupplier chunkSupplier, boolean writeFully ) throws N5IOException; /** * * @param pva * @param min * min pixel coordinate of region to write * @param size * size in pixels of region to write * @param chunkSupplier * is asked to create chunks within the given region. must be thread-safe. * @param writeFully * if false, merge existing data in blocks/chunks that overlap the region boundary. if true, override everything. * @param exec * used to parallelize over chunks and blocks * * @throws N5Exception * @throws InterruptedException * @throws ExecutionException */ void writeRegion( PositionValueAccess pva, long[] min, long[] size, DataBlockSupplier chunkSupplier, boolean writeFully, ExecutorService exec ) throws N5Exception, InterruptedException, ExecutionException; /** * Read a block at {@code shardGridPosition} at the given nesting {@code * level}. The data is read as chunks and then rearranged and assembled into * a (large) {@code DataBlock}. *

* The {@code getGridPosition()} of the returned {@code DataBlock} is the * grid position with respect to {@code level}. For example, if {@code * level==1}, then this refers to the position on the shard grid. * * @param pva * dataset storage * @param shardGridPosition * position of the shard to read (on the shard grid at {@code level}) * @param level * grid level of the shard/block to write. * @return * @throws N5IOException */ DataBlock readBlock(PositionValueAccess pva, long[] shardGridPosition, int level) throws N5IOException; /** * Write a full block at the given nesting {@code level}. The block data is * given as a (large) {@code DataBlock} that will be sliced, rearranged, and * written as chunks (level-0 DataBlocks). *

* {@code dataBlock.getGridPosition()} is the grid position with respect to * {@code level}. For example, if {@code level==1}, then this refers to the * position on the level-1 shard grid. * * @param pva * dataset storage * @param dataBlock * block to write * @param level * grid level of the block to write. * * @throws N5IOException */ void writeBlock(PositionValueAccess pva, DataBlock dataBlock, int level) throws N5IOException; NestedGrid getGrid(); } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultDatasetAccess.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.ArrayList; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; import java.util.List; import java.util.ListIterator; import java.util.Objects; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; import org.janelia.saalfeldlab.n5.ByteArrayDataBlock; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DoubleArrayDataBlock; import org.janelia.saalfeldlab.n5.FloatArrayDataBlock; import org.janelia.saalfeldlab.n5.IntArrayDataBlock; import org.janelia.saalfeldlab.n5.LongArrayDataBlock; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.N5Exception.N5NoSuchKeyException; import org.janelia.saalfeldlab.n5.N5Writer.DataBlockSupplier; import org.janelia.saalfeldlab.n5.ShortArrayDataBlock; import org.janelia.saalfeldlab.n5.StringDataBlock; import org.janelia.saalfeldlab.n5.codec.BlockCodec; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedGrid; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedPosition; import org.janelia.saalfeldlab.n5.util.SubArrayCopy; public class DefaultDatasetAccess implements DatasetAccess { private final NestedGrid grid; private final BlockCodec[] codecs; public DefaultDatasetAccess(final NestedGrid grid, final BlockCodec[] codecs) { this.grid = grid; this.codecs = codecs; } public NestedGrid getGrid() { return grid; } @Override public DataBlock readChunk(final PositionValueAccess pva, final long[] gridPosition) throws N5IOException { final NestedPosition position = grid.nestedPosition(gridPosition); try (final VolatileReadData readData = pva.get(position.key())) { return readChunkRecursive(readData, position, grid.numLevels() - 1); } catch (N5NoSuchKeyException ignored) { return null; } } private DataBlock readChunkRecursive( final ReadData readData, final NestedPosition position, final int level) { if (readData == null) { return null; } else if (level == 0) { @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[0]; return codec.decode(readData, position.absolute(0)); } else { @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[level]; final RawShard shard = codec.decode(readData, position.absolute(level)).getData(); return readChunkRecursive(shard.getElementData(position.relative(level - 1)), position, level - 1); } } @Override public List> readChunks(final PositionValueAccess pva, final List gridPositions) throws N5IOException { // for non-sharded datasets, just read the chunks individually if (grid.numLevels() == 1) { return gridPositions.stream().map(pos -> readChunk(pva, pos)).collect(Collectors.toList()); } // Create a list of ChunkRequests and sort it such that requests // from the same (nested) shard are grouped contiguously. final ChunkRequests requests = createReadRequests(gridPositions); final List> duplicates = requests.removeDuplicates(); final List> split = requests.split(); for (final ChunkRequests subRequests : split) { final long[] key = subRequests.relativeGridPosition(); try (final VolatileReadData readData = pva.get(key)) { readChunksRecursive(readData, subRequests); } catch (N5NoSuchKeyException ignored) { // the key didn't exist (as we found out when lazy-reading the index). // we don't have to do anything: all subRequest blocks remain null. // on to the next shard. } } return requests.chunks(duplicates); } /** * Bulk Read operation on a shard. * * @param readData for the corresponding shard * @param requests for chunks within the shard to be read */ private void readChunksRecursive( final ReadData readData, final ChunkRequests requests ) { assert !requests.requests.isEmpty(); assert requests.level > 0; if (readData == null) { return; } final int level = requests.level(); @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[level]; final RawShard shard = codec.decode(readData, requests.gridPosition()).getData(); if (level == 1 ) { //Base case; read the chunks // TODO: collect all the elementPos that we will need and prefetch // Probably best to add a prefetch method to RawShard? // Here's an attempt at that. // Don't love that we have to build a list of positions // Consider making DataBlockRequest package private and passing them directly final ArrayList positions = new ArrayList<>(); for (final ChunkRequest request : requests) { positions.add(request.position.relative(0)); } shard.prefetch(positions); for (final ChunkRequest request : requests) { final long[] elementPos = request.position.relative(0); final ReadData elementData = shard.getElementData(elementPos); request.chunk = readChunkRecursive(elementData, request.position, 0); } } else { // level > 1 final List> split = requests.split(); for (final ChunkRequests subRequests : split) { final long[] subShardPosition = subRequests.relativeGridPosition(); final ReadData elementData = shard.getElementData(subShardPosition); readChunksRecursive(elementData, subRequests); } } } @Override public void writeChunk(final PositionValueAccess pva, final DataBlock chunk) throws N5IOException { if (grid.numLevels() == 1) { @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[0]; pva.set(chunk.getGridPosition(), codec.encode(chunk)); } else { final NestedPosition position = grid.nestedPosition(chunk.getGridPosition()); final long[] key = position.key(); final ReadData modifiedData; try (final VolatileReadData existingData = pva.get(key)) { modifiedData = writeChunkRecursive(existingData, chunk, position, grid.numLevels() - 1); // Here, we are about to write the shard data, but with the new block modified. // Need to make sure that the read operations happen now before pva.set acquires a write lock modifiedData.materialize(); } pva.set(key, modifiedData); } } private ReadData writeChunkRecursive( final ReadData existingReadData, final DataBlock chunk, final NestedPosition position, final int level) { if (level == 0) { @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[0]; return codec.encode(chunk); } else { @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[level]; final long[] gridPos = position.absolute(level); final RawShard shard = getRawShard(existingReadData, codec, gridPos, level); final long[] elementPos = position.relative(level - 1); final ReadData existingElementData = (level == 1) ? null // if level == 1, we don't need to extract the nested (DataBlock) ReadData because it will be overridden anyway : shard.getElementData(elementPos); final ReadData modifiedElementData = writeChunkRecursive(existingElementData, chunk, position, level - 1); shard.setElementData(modifiedElementData, elementPos); return codec.encode(new RawShardDataBlock(gridPos, shard)); } } @Override public void writeChunks(final PositionValueAccess pva, final List> chunks) throws N5IOException { if (grid.numLevels() == 1) { @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[0]; chunks.forEach(chunk -> pva.set(chunk.getGridPosition(), codec.encode(chunk))); } else { // Create a list of ChunkRequests, sorted such that requests from // the same (nested) shard are grouped contiguously. final ChunkRequests requests = createWriteRequests(chunks); requests.removeDuplicates(); final List> split = requests.split(); for (final ChunkRequests subRequests : split) { final boolean writeFully = subRequests.coversShard(); final long[] shardKey = subRequests.relativeGridPosition(); final ReadData modifiedData; try (final VolatileReadData existingData = writeFully ? null : pva.get(shardKey)) { modifiedData = writeChunksRecursive(existingData, subRequests); // Here, we are about to write the shard data, but with the new blocks modified. // Need to make sure that the read operations happen now before pva.set acquires a write lock modifiedData.materialize(); } pva.set(shardKey, modifiedData); } } } /** * Bulk Write operation on a shard. * * @param existingReadData encoded existing shard data (to decode and partially override) * @param requests for chunks within the shard to be written */ private ReadData writeChunksRecursive( final ReadData existingReadData, // may be null final ChunkRequests requests ) { assert !requests.requests.isEmpty(); assert requests.level > 0; final boolean writeFully = existingReadData == null; final int level = requests.level(); @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[level]; final long[] gridPos = requests.gridPosition(); final RawShard shard = getRawShard(existingReadData, codec, gridPos, level); if ( level == 1 ) { // Base case, write the blocks for (final ChunkRequest request : requests) { final ReadData elementData = writeChunkRecursive(null, request.chunk, request.position, 0); final long[] elementPos = request.position.relative(0); shard.setElementData(elementData, elementPos); } } else { // level > 1 final List> split = requests.split(); for (final ChunkRequests subRequests : split) { final boolean nestedWriteFully = writeFully || subRequests.coversShard(); final long[] elementPos = subRequests.relativeGridPosition(); final ReadData existingElementData = nestedWriteFully ? null : shard.getElementData(elementPos); final ReadData modifiedElementData = writeChunksRecursive(existingElementData, subRequests); shard.setElementData(modifiedElementData, elementPos); } } return codec.encode(new RawShardDataBlock(gridPos, shard)); } @Override public void writeRegion( final PositionValueAccess pva, final long[] min, final long[] size, final DataBlockSupplier chunkSupplier, final boolean writeFully ) throws N5IOException { final Region region = new Region(min, size, grid); for (long[] key : Region.gridPositions(region.minPos().key(), region.maxPos().key())) { final NestedPosition pos = grid.nestedPosition(key, grid.numLevels() - 1); final boolean nestedWriteFully = writeFully || region.fullyContains(pos); final ReadData modifiedData; try (final VolatileReadData existingData = nestedWriteFully ? null : pva.get(key)) { modifiedData = writeRegionRecursive(existingData, region, chunkSupplier, pos); // Here, we are about to write the shard data, but with the new shard modified. // Need to make sure that the read operations happen now before pva.set acquires a write lock if (existingData != null && modifiedData != null) { modifiedData.materialize(); } } pva.set(key, modifiedData); } } @Override public void writeRegion( final PositionValueAccess pva, final long[] min, final long[] size, final DataBlockSupplier chunkSupplier, final boolean writeFully, final ExecutorService exec) throws N5Exception, InterruptedException, ExecutionException { final Region region = new Region(min, size, grid); for (long[] key : Region.gridPositions(region.minPos().key(), region.maxPos().key())) { exec.submit(() -> { final NestedPosition pos = grid.nestedPosition(key, grid.numLevels() - 1); final boolean nestedWriteFully = writeFully || region.fullyContains(pos); final ReadData modifiedData; try (final VolatileReadData existingData = nestedWriteFully ? null : pva.get(key)) { modifiedData = writeRegionRecursive(existingData, region, chunkSupplier, pos); // Here, we are about to write the shard data, but with the new block modified. // Need to make sure that the read operations happen now before pva.set acquires a write lock if (existingData != null && modifiedData != null) { modifiedData.materialize(); } } pva.set(key, modifiedData); }); } } private ReadData writeRegionRecursive( final ReadData existingReadData, // may be null final Region region, final DataBlockSupplier chunkSupplier, final NestedPosition position ) { final boolean writeFully = existingReadData == null; final int level = position.level(); if ( level == 0 ) { @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[0]; final long[] gridPosition = position.absolute(0); // If the DataBlock is not fully contained in the region, we will // get existingReadData != null. In that case, we try to decode the // existing DataBlock and pass it to the BlockSupplier for modification. // (This might fail with N5NoSuchKeyException if existingReadData // lazily points to non-existent data.) DataBlock existingChunk = null; if (existingReadData != null) { try { existingChunk = codec.decode(existingReadData, gridPosition); } catch (N5NoSuchKeyException ignored) { } } final DataBlock chunk = chunkSupplier.get(gridPosition, existingChunk); // null chunks may be provided when they contain only the fill value // and only non-empty chunks should be written, for example if (chunk == null) return null; return codec.encode(chunk); } else { @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[level]; final long[] gridPos = position.absolute(level); final RawShard shard = getRawShard(existingReadData, codec, gridPos, level); for (NestedPosition pos : region.containedNestedPositions(position)) { final boolean nestedWriteFully = writeFully || region.fullyContains(pos); final long[] elementPos = pos.relative(); final ReadData existingElementData = nestedWriteFully ? null : shard.getElementData(elementPos); final ReadData modifiedElementData = writeRegionRecursive(existingElementData, region, chunkSupplier, pos); shard.setElementData(modifiedElementData, elementPos); } // do not write empty shards if (shard.isEmpty()) return null; return codec.encode(new RawShardDataBlock(gridPos, shard)); } } // // -- deleteChunk --------------------------------------------------------- @Override public boolean deleteChunk(final PositionValueAccess pva, final long[] gridPosition) throws N5IOException { if (grid.numLevels() == 1) { // for non-sharded dataset, don't bother getting the value, just remove the key. return pva.remove(gridPosition); } else { final NestedPosition position = grid.nestedPosition(gridPosition); final long[] key = position.key(); final ReadData modifiedData; try (final VolatileReadData existingData = pva.get(key)) { modifiedData = deleteChunkRecursive(existingData, position, grid.numLevels() - 1); if (modifiedData == existingData) { // nothing changed, the blocks we wanted to delete didn't exist anyway return false; } else if (modifiedData != null) { // Here, we are about to write the shard data, but without the chunk to be deleted. // Need to make sure that the read operations happen now before pva.set acquires a write lock modifiedData.materialize(); } } catch (final N5NoSuchKeyException e) { // the key didn't exist (as we found out when lazy-reading the index) // so nothing changed, the chunks we wanted to delete didn't exist anyway return false; } if (modifiedData == null) { return pva.remove(key); } else { pva.set(key, modifiedData); return true; } } } private ReadData deleteChunkRecursive( final ReadData existingReadData, final NestedPosition position, final int level) throws N5NoSuchKeyException { if (level == 0 || existingReadData == null) { return null; } else { @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[level]; final long[] gridPos = position.absolute(level); final RawShard shard = codec.decode(existingReadData, gridPos).getData(); final long[] elementPos = position.relative(level - 1); final ReadData existingElementData = shard.getElementData(elementPos); if (existingElementData == null) { // The chunk (or the whole nested shard containing it) does not exist. // This shard remains unchanged. return existingReadData; } else { final ReadData modifiedElementData = deleteChunkRecursive(existingElementData, position, level - 1); if (modifiedElementData == existingElementData) { // The nested shard was not modified. // This shard remains unchanged. return existingReadData; } shard.setElementData(modifiedElementData, elementPos); if (modifiedElementData == null) { // The chunk or nested shard was removed. // Check whether this shard becomes empty. if (shard.isEmpty()) { // This shard is empty and should be removed. return null; } } return codec.encode(new RawShardDataBlock(gridPos, shard)); } } } // // -- deleteChunks -------------------------------------------------------- @Override public boolean deleteChunks(final PositionValueAccess pva, final List gridPositions) throws N5IOException { // for non-sharded datasets, just delete the chunks individually if (grid.numLevels() == 1) { boolean deleted = false; for (long[] pos : gridPositions) { deleted |= pva.remove(pos); } return deleted; } else { // Create a list of ChunkRequests and sort it such that requests // from the same (nested) shard are grouped contiguously. // Despite the name, createReadRequests() works for delete requests as well ... final ChunkRequests requests = createReadRequests(gridPositions); requests.removeDuplicates(); boolean deleted = false; final List> split = requests.split(); for (final ChunkRequests subRequests : split) { final boolean writeFully = subRequests.coversShard(); final long[] key = subRequests.relativeGridPosition(); final ReadData modifiedData; try (final VolatileReadData existingData = writeFully ? null : pva.get(key)) { modifiedData = deleteChunksRecursive(existingData, subRequests);; if (modifiedData == existingData) { // nothing changed, the chunks we wanted to delete didn't exist anyway continue; } else if (existingData != null && modifiedData != null) { // Here, we are about to write the shard data, but without the chunk to be deleted. // Need to make sure that the read operations happen now before pva.set acquires a write lock modifiedData.materialize(); } } catch (final N5NoSuchKeyException e) { // the key didn't exist (as we found out when lazy-reading the index) // so nothing changed, the chunks we wanted to delete didn't exist anyway continue; } if (modifiedData == null) { deleted |= pva.remove(key); } else { pva.set(key, modifiedData); deleted = true; } } return deleted; } } /** * Bulk Delete operation on a shard. * * @param existingReadData encoded existing shard data (to decode and partially override) * @param requests for chunks within the shard to be deleted */ private ReadData deleteChunksRecursive( final ReadData existingReadData, // may be null final ChunkRequests requests ) { assert !requests.requests.isEmpty(); assert requests.level > 0; if (existingReadData == null) { return null; } final int level = requests.level(); @SuppressWarnings("unchecked") final BlockCodec codec = (BlockCodec) codecs[level]; final long[] gridPos = requests.gridPosition(); final RawShard shard = codec.decode(existingReadData, gridPos).getData(); boolean modified = false; boolean shardElementSetToNull = false; if ( level == 1 ) { // Base case, delete the chunks for (final ChunkRequest request : requests) { final long[] elementPos = request.position.relative(0); if (shard.getElementData(elementPos) != null) { shard.setElementData(null, elementPos); modified = true; shardElementSetToNull = true; } } } else { // level > 1 final List> split = requests.split(); for (final ChunkRequests subRequests : split) { final boolean writeFully = subRequests.coversShard(); final long[] elementPos = subRequests.relativeGridPosition(); final ReadData existingElementData = writeFully ? null : shard.getElementData(elementPos); final ReadData modifiedElementData = deleteChunksRecursive(existingElementData, subRequests); if (modifiedElementData != existingElementData) { shard.setElementData(modifiedElementData, elementPos); modified = true; shardElementSetToNull |= (modifiedElementData == null); } } } if (!modified) { // No nested shard or chunk was modified. // This shard remains unchanged. return existingReadData; } if (shardElementSetToNull) { // At least one chunk or nested shard was removed. // Check whether this shard becomes empty. if (shard.index().allElementsNull()) { // This shard is empty and should be removed. return null; } } return codec.encode(new RawShardDataBlock(gridPos, shard)); } // // -- readShard ----------------------------------------------------------- // NB: How to handle the dataset borders? // // N5 format uses truncated DataBlocks at the dataset border. // // For the Zarr format, when a truncated DataBlock is written, it is // padded with zero values to the default DataBlock size. This will // happen also when writing DataBlocks into a Shard. // // However, Zarr will not fill up a Shard with empty DataBlocks to pad // it to the default Shard size. Instead, these blocks will be missing // in the Shard index. // // When we write a full Shard as a "big DataBlock", what do we expect? // // For N5 format probably we expect the big DataBlock to be truncated at // the dataset border. (N5 format doesn't have shards yet, so we are // relatively free what to do here. But this would be consistent.) // // For Zarr format, we either expect // * a big DataBlock that is the default Shard size, or // * a big DataBlock that is truncated after the last DataBlock that is // (partially) in the dataset borders (but truncated at multiple of // default DataBlock size, so "slightly padded"). // // I'm not sure which, so we handle both cases for now. In any case, we // do not want to write DataBlocks that are completely outside the // dataset (even if the "big DataBlock" covers this area.) // // This works for writing. For reading, we'll have to decide what to return, // though... Potentially, there is no valid block at the border, so we // cannot determine where to put the border just from the data. We need to // rely on external input or heuristics. // // For now, we decided to always truncate the at the dataset border when we // read full shards. This will hopefully work where we need it, but it // introduces inconsistencies. There is a readShardInternal() implementation // which takes the expected shardSizeInPixels as an argument, so that we can // easily revisit and change this heuristic. @Override public DataBlock readBlock( final PositionValueAccess pva, final long[] shardGridPosition, final int level ) throws N5IOException { if (level == 0) { return readChunk(pva, shardGridPosition); } final long[] shardPixelPos = grid.pixelPosition(shardGridPosition, level); final int[] defaultShardSize = grid.getBlockSize(level); final long[] datasetSize = grid.getDatasetSize(); final int n = grid.numDimensions(); final int[] shardSizeInPixels = new int[n]; for (int d = 0; d < n; ++d) { shardSizeInPixels[d] = Math.min(defaultShardSize[d], (int) (datasetSize[d] - shardPixelPos[d])); } return readBlockInternal(pva, shardGridPosition, shardSizeInPixels, level); } private DataBlock readBlockInternal( final PositionValueAccess pva, final long[] shardGridPosition, final int[] shardSizeInPixels, // expected size of this shard in pixels final int level ) throws N5IOException { final int n = grid.numDimensions(); final int[] defaultShardSize = grid.getBlockSize(level); for (int d = 0; d < n; d++) { if (shardSizeInPixels[d] > defaultShardSize[d]) { throw new IllegalArgumentException("Requested shard size is larger than the default shard size"); } } // level-0 block-grid position of the min chunk in the shard final long[] gridMin = grid.absolutePosition(shardGridPosition, level, 0); // level-0 block-grid position of the max chunk in the shard that we need to read. // (the shard might go beyond the dataset border, and we don't need to read anything there) final long[] gridMax = new long[n]; final long[] datasetSizeInChunks = grid.getDatasetSizeInChunks(); final int[] chunkSize = grid.getBlockSize(0); for (int d = 0; d < n; ++d) { final int shardSizeInChunks = (shardSizeInPixels[d] + chunkSize[d] - 1) / chunkSize[d]; final int gridSize = Math.min(shardSizeInChunks, (int) (datasetSizeInChunks[d] - gridMin[d])); gridMax[d] = gridMin[d] + gridSize - 1; } // read all chunks in (gridMin, gridMax) and filter out missing chunks final List chunkPositions = Region.gridPositions(gridMin, gridMax); final List> chunks = readChunks(pva, chunkPositions) .stream().filter(Objects::nonNull).collect(Collectors.toList()); if (chunks.isEmpty()) { return null; } // allocate shard and copy data from chunks final DataBlock shard = DataBlockFactory.of(chunks.get(0).getData()).createDataBlock(shardSizeInPixels, shardGridPosition); final long[] shardPixelPos = grid.pixelPosition(shardGridPosition, level); final long[] chunkPixelPos = new long[n]; final int[] srcPos = new int[n]; final int[] destPos = new int[n]; final int[] size = new int[n]; for (final DataBlock chunk : chunks) { // copy chunk data that overlaps the shard grid.pixelPosition(chunk.getGridPosition(), 0, chunkPixelPos); final int[] bsize = chunk.getSize(); for (int d = 0; d < n; d++) { destPos[d] = (int) (chunkPixelPos[d] - shardPixelPos[d]); size[d] = Math.min(bsize[d], shardSizeInPixels[d] - destPos[d]); } SubArrayCopy.copy(chunk.getData(), bsize, srcPos, shard.getData(), shardSizeInPixels, destPos, size); } return shard; } // // -- writeBlock ---------------------------------------------------------- @Override public void writeBlock( final PositionValueAccess pva, final DataBlock dataBlock, final int level ) throws N5IOException { if (level == 0) { writeChunk(pva, dataBlock); return; } final T shardData = dataBlock.getData(); final DataBlockFactory blockFactory = DataBlockFactory.of(shardData); final int n = grid.numDimensions(); final int[] chunkSize = grid.getBlockSize(0); // size of a standard (non-truncated) chunk final long[] datasetChunkSize = grid.getDatasetSizeInChunks(); final long[] shardPixelMin = grid.pixelPosition(dataBlock.getGridPosition(), level); final int[] shardPixelSize = dataBlock.getSize(); // the max chunk + 1 in the shard, if it isn't truncated by the dataset border final long[] shardChunkTo = new long[n]; Arrays.setAll(shardChunkTo, d -> (shardPixelMin[d] + shardPixelSize[d] + chunkSize[d] - 1) / chunkSize[d]); // level 0 grid positions of all chunks we want to extract final long[] gridMin = grid.absolutePosition(dataBlock.getGridPosition(), level, 0); final long[] gridMax = new long[n]; Arrays.setAll(gridMax, d -> Math.min(shardChunkTo[d], datasetChunkSize[d]) - 1); final List chunkPositions = Region.gridPositions(gridMin, gridMax); // Max pixel coordinates + 1, of the region we want to copy. This should // always be shardPixelMin + shardPixelSize, except at the dataset // border, where we truncate to the smallest multiple of chunkSize still // overlapping the dataset. final long[] regionBound = new long[n]; Arrays.setAll(regionBound, d -> Math.min(shardPixelMin[d] + shardPixelSize[d], datasetChunkSize[d] * chunkSize[d])); final List> chunks = new ArrayList<>(chunkPositions.size()); final int[] srcPos = new int[n]; final int[] destPos = new int[n]; final int[] destSize = new int[n]; for ( long[] chunkPos : chunkPositions) { final long[] pixelMin = grid.pixelPosition(chunkPos, 0); for (int d = 0; d < n; d++) { srcPos[d] = (int) (pixelMin[d] - shardPixelMin[d]); destSize[d] = Math.min(chunkSize[d], (int) (regionBound[d] - pixelMin[d])); } // This extracting chunks will not work if num_array_elements != num_block_elements. // But we'll deal with that later if it becomes a problem... final DataBlock chunk = blockFactory.createDataBlock(destSize, chunkPos); SubArrayCopy.copy(shardData, shardPixelSize, srcPos, chunk.getData(), destSize, destPos, destSize); chunks.add(chunk); } writeChunks(pva, chunks); } // // -- helpers ------------------------------------------------------------- /** * If {@code existingReadData != null} try to decode it into a RawShard. * Otherwise, or if this fails because we find that {@code existingReadData} * lazily points to non-existent data, return a new empty RawShard. * * @param existingReadData data to decode or null * @param codec shard codec * @param gridPos position of the shard on the shard grid of the given level * @param level level of the shard * @return the decode shard (or a new empty shard) */ private RawShard getRawShard( final ReadData existingReadData, final BlockCodec codec, final long[] gridPos, final int level) { if (existingReadData != null) { try { return codec.decode(existingReadData, gridPos).getData(); } catch (N5NoSuchKeyException ignored) { } } return new RawShard(grid.relativeBlockSize(level)); } /** * A request to read or write a chunk (level-0 DataBlock) at a given {@link #position}. *

* Write requests are constructed with {@link #position} and {@link #chunk}. *

* Read requests are constructed with only a {@link #position}, and * initially {@link #chunk chunk=null}. When the DataBlock is read, it will * be put into {@link #chunk}. *

* {@code ChunkRequest} are used for reading/writing a list of chunks * with {@link #readChunks} and {@link #writeChunks}. The {@link #index} * field is the position in the list of positions/chunks to read/write. For * processing, requests are re-ordered such that all requests from the same * (sub-)shard are grouped together. The {@link #index} field is used to * re-establish the order of results (only important for {@link #readChunks}). * * @param * type of the data contained in the DataBlock */ private static final class ChunkRequest { final NestedPosition position; final int index; DataBlock chunk; // read request ChunkRequest(final NestedPosition position, final int index) { this.position = position; this.index = index; this.chunk = null; } // write request ChunkRequest(final NestedPosition position, final DataBlock chunk) { this.position = position; this.index = -1; this.chunk = chunk; } @Override public String toString() { return "ChunkRequest{position=" + position + ", index=" + index + '}'; } } /** * A list of {@code ChunkRequest}, ordered by {@code NestedPosition}. * All requests lie in the same shard at the given {@code level}. *

* {@code ChunkRequests} should be constructed using {@link * #createReadRequests} (for reading) or {@link #createWriteRequests} (for * writing). *

* When recursing into nested shard levels, {@code ChunkRequests} should * be {@link #split} to partition into sub-{@code ChunkRequests} that * each cover one shard. * * @param * type of the data contained in the DataBlocks */ private static final class ChunkRequests implements Iterable> { private final NestedGrid grid; private final List> requests; private final int level; private ChunkRequests(final List> requests, final int level, final NestedGrid grid) { this.requests = requests; this.level = level; this.grid = grid; } /** * Returns a map of duplicate requests. Each pair of consecutive * elements (A,B) of the list means that request A is a duplicate of * request B. A has been removed from the {@code requests} list, and B * remains in the {@code requests} list. After the (read) requests have * been processed, elements corresponding to A can be added into the * result list by using the {@code DataBlock} of B. *

* If {@code n} duplicates occur in {@code requests}, the resulting list * will have {@code 2*n} elements. */ public List> removeDuplicates() { List> duplicates = new ArrayList<>(); ChunkRequest previous = null; final ListIterator> iter = requests.listIterator(); while (iter.hasNext()) { final ChunkRequest current = iter.next(); if (previous != null) { if (previous.position.equals(current.position)) { iter.remove(); duplicates.add(current); duplicates.add(previous); continue; } } previous = current; } return duplicates; } @Override public Iterator> iterator() { return requests.iterator(); } /** * All chunks contained in this {@code ChunkRequests} are in the * same shard at this nesting level. *

* Use {@link #split()} to partition into {@code ChunkRequests} with * nesting level {@link #level()}{@code -1}. * * @return nesting level */ public int level() { return level; } /** * Position on the shard grid at the level of this ChunkRequests * (of the one shard containing all the requested blocks). */ public long[] gridPosition() { return position().absolute(level); } /** * Relative grid position at the level of this ChunkRequests, * that is, relative offset within containing the (level+1) element. */ public long[] relativeGridPosition() { return position().relative(level); } private NestedPosition position() { if (requests.isEmpty()) throw new IllegalArgumentException(); return requests.get(0).position; } /** * Split into sub-requests, grouping by same position at nesting level {@link #level()}{@code -1}. */ public List> split() { final int subLevel = level - 1; final List> subRequests = new ArrayList<>(); for (int i = 0; i < requests.size(); ) { final long[] ilpos = requests.get(i).position.absolute(subLevel); int j = i + 1; for (; j < requests.size(); ++j) { final long[] jlpos = requests.get(j).position.absolute(subLevel); if (!Arrays.equals(ilpos, jlpos)) { break; } } subRequests.add(new ChunkRequests<>(requests.subList(i, j), subLevel, grid)); i = j; } return subRequests; } /** * Returns {@code true} if this {@code ChunkRequests} completely * fills its containing shard at nesting level {@link #level()}. * (This can be used to avoid reading a shard that will be completely * overwritten). *

* Note that this method only works correctly if the requests list * contains no duplicates. See {@link #removeDuplicates()}. */ public boolean coversShard() { final long[] gridMin = grid.absolutePosition(position().absolute(level), level, 0); final long[] datasetSize = grid.getDatasetSizeInChunks(); // in units of DataBlocks final int[] defaultShardSize = grid.relativeToBaseBlockSize(level); // in units of DataBlocks int numElements = 1; for (int d = 0; d < defaultShardSize.length; d++) { numElements *= Math.min(defaultShardSize[d], (int) (datasetSize[d] - gridMin[d])); } return requests.size() >= numElements;// NB: It should never be (requests.size() > numElements), unless there are duplicate blocks. } /** * Extract {@link ChunkRequest#chunk chunk}s from the requests, * in the order of {@link ChunkRequest#index indices}. *

* (This is used in {@link #readChunks} to collect chunks in the * requested order.) */ public List> chunks(final List> duplicates) { final int size = requests.size() + duplicates.size() / 2; final DataBlock[] blocks = new DataBlock[size]; requests.forEach(r -> blocks[r.index] = r.chunk); for (int i = 0; i < duplicates.size(); i += 2) { final ChunkRequest a = duplicates.get(i * 2); final ChunkRequest b = duplicates.get(i * 2 + 1); blocks[a.index] = b.chunk; } return Arrays.asList(blocks); } } /** * Construct {@code ChunkRequests} from a list of level-0 grid positions * for reading. *

* The nesting level of the returned {@code ChunkRequests} is {@code * grid.numLevels()}, that is level of the highest-order shard + 1. This * implies that the requests are not guaranteed to be in the same shard (at * any level. {@link ChunkRequests#split() Splitting} the {@code * ChunkRequests} once will return a list of {@code ChunkRequests} * that each contain chunks from one highest-order shard. */ private ChunkRequests createReadRequests(final List gridPositions) { final List> requests = new ArrayList<>(gridPositions.size()); for (int i = 0; i < gridPositions.size(); i++) { final NestedPosition pos = grid.nestedPosition(gridPositions.get(i)); requests.add(new ChunkRequest<>(pos, i)); } requests.sort(Comparator.comparing(r -> r.position)); return new ChunkRequests<>(requests, grid.numLevels(), grid); } /** * Construct {@code ChunkRequests} from a list of chunks (level-0 * DataBlocks) for writing. *

* The nesting level ot the returned {@code ChunkRequests} is {@code * grid.numLevels()}, that is level of the highest-order shard + 1. This * implies that the requests are not guaranteed to be in the same shard (at * any level. {@link ChunkRequests#split() Splitting} the {@code * ChunkRequests} once will return a list of {@code ChunkRequests} * that each contain chunks from one highest-order shard. */ private ChunkRequests createWriteRequests(final List> dataBlocks) { final List> requests = new ArrayList<>(dataBlocks.size()); for (final DataBlock dataBlock : dataBlocks) { final NestedPosition pos = grid.nestedPosition(dataBlock.getGridPosition()); requests.add(new ChunkRequest<>(pos, dataBlock)); } requests.sort(Comparator.comparing(r -> r.position)); return new ChunkRequests<>(requests, grid.numLevels(), grid); } /** * Factory for the standard {@code DataBlock}, where {@code T} is an * array type and the number of elements in a block corresponds to the * {@link DataBlock#getSize()}. *

* This is used by {@link #readBlock} and {@link #writeBlock} which * internally need to allocate new DataBlocks to split or merge a shard. */ private interface DataBlockFactory { DataBlock createDataBlock(final int[] blockSize, final long[] gridPosition); @SuppressWarnings("unchecked") static DataBlockFactory of(T array) { if (array instanceof byte[]) { return (size, pos) -> (DataBlock) new ByteArrayDataBlock(size, pos, new byte[DataBlock.getNumElements(size)]); } else if (array instanceof short[]) { return (size, pos) -> (DataBlock) new ShortArrayDataBlock(size, pos, new short[DataBlock.getNumElements(size)]); } else if (array instanceof int[]) { return (size, pos) -> (DataBlock) new IntArrayDataBlock(size, pos, new int[DataBlock.getNumElements(size)]); } else if (array instanceof long[]) { return (size, pos) -> (DataBlock) new LongArrayDataBlock(size, pos, new long[DataBlock.getNumElements(size)]); } else if (array instanceof float[]) { return (size, pos) -> (DataBlock) new FloatArrayDataBlock(size, pos, new float[DataBlock.getNumElements(size)]); } else if (array instanceof double[]) { return (size, pos) -> (DataBlock) new DoubleArrayDataBlock(size, pos, new double[DataBlock.getNumElements(size)]); } else if (array instanceof String[]) { return (size, pos) -> (DataBlock) new StringDataBlock(size, pos, new String[DataBlock.getNumElements(size)]); } else { throw new IllegalArgumentException("unsupported array type: " + array.getClass().getSimpleName()); } } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/DefaultShardCodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.Arrays; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.codec.BlockCodec; import org.janelia.saalfeldlab.n5.codec.BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.CodecInfo; import org.janelia.saalfeldlab.n5.codec.CodecParser; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.DatasetCodecInfo; import org.janelia.saalfeldlab.n5.serialization.N5Annotations; import org.janelia.saalfeldlab.n5.serialization.NameConfig; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; /** * Default (and probably only) implementation of {@link ShardCodecInfo}. */ @NameConfig.Name(value = "sharding_indexed") public class DefaultShardCodecInfo implements ShardCodecInfo { @Override public String getType() { return "sharding_indexed"; } @N5Annotations.ReverseArray @NameConfig.Parameter(value = "chunk_shape") private final int[] innerBlockSize; @NameConfig.Parameter(value = "index_location", optional = true) private final IndexLocation indexLocation; @NameConfig.Parameter private CodecInfo[] codecs; @NameConfig.Parameter(value = "index_codecs") private CodecInfo[] indexCodecs; private transient DatasetCodecInfo[] innerDatasetCodecInfos; private transient BlockCodecInfo innerBlockCodecInfo; private transient DataCodecInfo[] innerDataCodecInfos; private transient BlockCodecInfo indexBlockCodecInfo; private transient DataCodecInfo[] indexDataCodecInfos; DefaultShardCodecInfo() { // for serialization this(null, null, null, null, null, IndexLocation.END); } public DefaultShardCodecInfo( final int[] innerBlockSize, final DatasetCodecInfo[] innerDatasetCodecInfos, final BlockCodecInfo innerBlockCodecInfo, final DataCodecInfo[] innerDataCodecInfos, final BlockCodecInfo indexBlockCodecInfo, final DataCodecInfo[] indexDataCodecInfos, final IndexLocation indexLocation) { this.innerBlockSize = innerBlockSize; this.innerDatasetCodecInfos = innerDatasetCodecInfos; this.innerBlockCodecInfo = innerBlockCodecInfo; this.innerDataCodecInfos = innerDataCodecInfos; this.indexBlockCodecInfo = indexBlockCodecInfo; this.indexDataCodecInfos = indexDataCodecInfos; this.indexLocation = indexLocation; codecs = concatenateCodecs(innerBlockCodecInfo, innerDataCodecInfos); indexCodecs = concatenateCodecs(indexBlockCodecInfo, indexDataCodecInfos); } public DefaultShardCodecInfo( final int[] innerBlockSize, final BlockCodecInfo innerBlockCodecInfo, final DataCodecInfo[] innerDataCodecInfos, final BlockCodecInfo indexBlockCodecInfo, final DataCodecInfo[] indexDataCodecInfos, final IndexLocation indexLocation) { this(innerBlockSize, null, innerBlockCodecInfo, innerDataCodecInfos, indexBlockCodecInfo, indexDataCodecInfos, indexLocation); } private void build() { if (innerBlockCodecInfo != null) return; // sets // innerBlockCodecInfo, innerDataCodecInfos // indexBlockCodecInfo, indexDataCodecInfos // from // codecs and indexCodecs final CodecParser parser = new CodecParser(codecs); innerDatasetCodecInfos = parser.datasetCodecInfos; innerBlockCodecInfo = parser.blockCodecInfo; innerDataCodecInfos = parser.dataCodecInfos; if (indexCodecs[0] instanceof BlockCodecInfo) indexBlockCodecInfo = (BlockCodecInfo)indexCodecs[0]; else throw new N5Exception("Codec at index " + 0 + " must be a BlockCodec."); indexDataCodecInfos = new DataCodecInfo[indexCodecs.length - 1]; for (int i = 1; i < indexCodecs.length; i++) indexDataCodecInfos[i - 1] = (DataCodecInfo)indexCodecs[i]; } @Override public int[] getInnerBlockSize() { return innerBlockSize; } @Override public DatasetCodecInfo[] getInnerDatasetCodecInfos() { return innerDatasetCodecInfos; } @Override public BlockCodecInfo getInnerBlockCodecInfo() { return innerBlockCodecInfo; } @Override public DataCodecInfo[] getInnerDataCodecInfos() { return innerDataCodecInfos; } @Override public BlockCodecInfo getIndexBlockCodecInfo() { return indexBlockCodecInfo; } @Override public DataCodecInfo[] getIndexDataCodecInfos() { return indexDataCodecInfos; } @Override public IndexLocation getIndexLocation() { return indexLocation; } public CodecInfo[] getCodecs() { return codecs; } public CodecInfo[] getIndexCodecs() { return indexCodecs; } @Override public RawShardCodec create(final int[] blockSize, final DataCodecInfo... codecs) { build(); // Number of elements (DataBlocks, nested shards) in each dimension per shard. final int[] size = new int[blockSize.length]; // blockSize argument is number of pixels in the shard // innerBlockSize is number of pixels in each shard element (nested shard or DataBlock) Arrays.setAll(size, d -> blockSize[d] / innerBlockSize[d]); final BlockCodec indexCodec = indexBlockCodecInfo.create( DataType.UINT64, ShardIndex.blockSizeFromIndexSize(size), indexDataCodecInfos); return new RawShardCodec(size, indexLocation, indexCodec); } private static CodecInfo[] concatenateCodecs(BlockCodecInfo blkInfo, DataCodecInfo[] dataInfos) { if (dataInfos == null) { return new CodecInfo[]{blkInfo}; } final CodecInfo[] allCodecs = new CodecInfo[dataInfos.length + 1]; allCodecs[0] = blkInfo; System.arraycopy(dataInfos, 0, allCodecs, 1, dataInfos.length); return allCodecs; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/Nesting.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.Arrays; import java.util.Objects; /** * Container for classes that coordinate hierarchical grid structures for * sharded N5 datasets. *

* This class provides classes for representing and navigating nested/sharded * dataset layouts for the N5 API. In a sharded dataset, level-0 data blocks * (called chunks) are grouped into higher-level containers called * shards, which can themselves be nested within parent shards, * creating a multi-level hierarchy. *

* Nesting levels: *

    *
  • Level 0: The finest granularity - individual chunks (level-0 data * blocks) containing actual pixel data *
  • Level 1: First-level shards, which contain multiple level-0 data * blocks *
  • Level 2+: Higher-level shards (if they exist), which contain * multiple lower-level shards *
  • Nesting depth: The total number of hierarchical levels in the * structure *
*

* Contained Classes: *

    *
  • {@link NestedGrid} - Defines the hierarchical grid structure with block * sizes at each nesting level. This is the grid "schema" that describes how the * hierarchy is organized. *
  • {@link NestedPosition} - Represents a specific position within a * {@code NestedGrid} at a particular nesting level, providing coordinate * transformations between levels. *
* */ public class Nesting { /** * Represents the position of a block at a particular level of a nested hierarchy, * where the hierarchy is defined by a given {@link NestedGrid}. */ public static class NestedPosition implements Comparable { private final NestedGrid grid; private final long[] position; private final int level; protected NestedPosition(final NestedGrid grid, final long[] position, final int level) { this.grid = grid; this.position = position; this.level = level; } /** * Get the nesting level of this position. *

* Positions with {@code level=0} refer to chunks, positions with * {@code level=1} refer to first-level shards (containing chunks), * and so on. * * @return nesting level */ public int level() { return level; } public int numDimensions() { return grid.numDimensions(); } /** * Get the relative grid position at {@code level}, that is, relative * offset within containing the {@code (level+1)} element. * * @param level * requested nesting level * * @return relative grid position */ public long[] relative(final int level) { return grid.relativePosition(position, this.level, level); } /** * Get the relative grid position at this positions {@link #level()}, * that is, relative offset within containing the {@code (level+1)} * element. * * @return relative grid position */ public long[] relative() { return relative(level()); } /** * Get the absolute grid position at {@code level}. * * @param level * requested nesting level * * @return absolute grid position */ public long[] absolute(final int level) { return grid.absolutePosition(position, this.level, level); } public long[] key() { return relative(grid.numLevels() - 1); } public long[] pixelPosition() { return grid.pixelPosition(position, level); } public long[] maxPixelPosition() { return grid.maxPixelPosition(position, level); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append('{'); for (int l = level; l < grid.numLevels(); ++l) { if (l > level) { sb.append(" / "); } sb.append(Arrays.toString(relative(l))); } sb.append(" (level ").append(level).append(")}"); return sb.toString(); } // TODO: Consider making Comparable, equals, and hashCode assume that // everything is on the same NestedGrid. This is how we use it in // practice, and it would simplify things a little bit. @Override public int compareTo(NestedPosition o) { if (o.grid != grid) throw new IllegalArgumentException("NestedPositions of different NestedGrids are not comparable"); final int levelInequality = Integer.compare(level, o.level); if (levelInequality != 0) return levelInequality; final int[] sk = grid.relativeToBase[level]; for (int l = grid.numLevels() - 1; l >= 0; --l) { final int[] si = grid.relativeToBase[l]; final boolean maxLevel = l < grid.numLevels - 1; final int[] rj = maxLevel ? grid.relativeToAdjacent[l + 1] : null; for (int d = grid.numDimensions - 1; d >= 0; --d) { long relative = position[d] * sk[d] / si[d]; long orelative = o.position[d] * sk[d] / si[d]; if (maxLevel) { relative %= rj[d]; orelative %= rj[d]; } final int posInequality = Long.compare(relative, orelative); if (posInequality != 0) return posInequality; } } return 0; } @Override public boolean equals(final Object o) { if (o == this) return true; if (!(o instanceof NestedPosition)) return false; final NestedPosition that = (NestedPosition) o; return level == that.level && Objects.equals(grid, that.grid) && Objects.deepEquals(position, that.position); } @Override public int hashCode() { return Objects.hash(grid, Arrays.hashCode(position), level); } // TODO: should we have prefix()? suffix()? head()? tail()? } /** * A nested grid of blocks used to coordinate the relationships of shards * and the blocks (chunks / sub-shards) they contain. *

* The nesting depth ({@link #numLevels()}) of the {@code NestedGrid} is 1 * for non-sharded datasets, 2 for simple sharded datasets (where shards * contain chunks), and ≥3 for nested sharded datasets. *

* Positions with {@code level=0} refer to the DataBlock grid, positions * with {@code level=1} refer to first-level Shard grid, and so on. */ public static class NestedGrid { private final int numLevels; private final int numDimensions; /** * relativeToBase[i][d] is block size (in dimension d) at level i relative to level 0 */ private final int[][] relativeToBase; /** * relativeToAdjacent[i][d] is block size (in dimension d) at level i relative to level i-1 */ private final int[][] relativeToAdjacent; /** * blockSizes[l][d] is the block size in pixels at level l in dimension d */ private final int[][] blockSizes; /** * dimensions of the dataset in pixels. */ private final long[] datasetSize; /** * dimensions of the dataset in level-0 blocks. */ private final long[] datasetSizeInChunks; /** * {@code blockSizes[l][d]} is the block size at level {@code l} in dimension {@code d}. * Level 0 contains the smallest blocks. blockSizes[l+1][d] must be a multiple of blockSizes[l][d]. * * @param blockSizes * block sizes for all levels and dimensions. * @param datasetSize * size of the dataset. */ public NestedGrid(int[][] blockSizes, long[] datasetSize) { if (blockSizes == null) throw new IllegalArgumentException("blockSizes is null"); if (blockSizes[0] == null) throw new IllegalArgumentException("blockSizes[0] is null"); this.blockSizes = blockSizes; this.datasetSize = datasetSize; numLevels = blockSizes.length; numDimensions = blockSizes[0].length; relativeToBase = new int[numLevels][numDimensions]; relativeToAdjacent = new int[numLevels][numDimensions]; for (int l = 0; l < numLevels; ++l) { final int k = Math.max(0, l - 1); if (blockSizes[l] == null) throw new IllegalArgumentException("blockSizes[" + l + "] null"); if (blockSizes[l].length != numDimensions) throw new IllegalArgumentException( String.format("Block size at level %d has a different length (%d vs %d)", l, numDimensions, blockSizes[l].length)); for (int d = 0; d < numDimensions; ++d) { if (blockSizes[l][d] <= 0) { throw new IllegalArgumentException( String.format("Block sizes at level %d (%d) is negative for dimension %d.", l, blockSizes[l][d], d)); } if (blockSizes[l][d] < blockSizes[k][d]) { throw new IllegalArgumentException( String.format("Block sizes at level %d (%d) is smaller than previous level (%d) " + " for dimension %d.", l, blockSizes[l][d], blockSizes[k][d], d)); } if (blockSizes[l][d] % blockSizes[k][d] != 0) { throw new IllegalArgumentException( String.format("Block sizes at level %d (%d) not a multiple of previous level (%d) " + " for dimension %d.", l, blockSizes[l][d], blockSizes[k][d], d)); } relativeToBase[l][d] = blockSizes[l][d] / blockSizes[0][d]; relativeToAdjacent[l][d] = blockSizes[l][d] / blockSizes[k][d]; } } if (datasetSize == null) { datasetSizeInChunks = null; } else { datasetSizeInChunks = new long[numDimensions]; Arrays.setAll(datasetSizeInChunks, d -> (datasetSize[d] + blockSizes[0][d] - 1) / blockSizes[0][d]); } } public NestedGrid(int[][] blockSizes) { this(blockSizes, null); } /** * Create a {@code NestedPosition} at the specified nesting {@code * level} grid {@code position}. *

* Note that {@code position} is in units of grid elements at {@code * level}. Positions with {@code level=0} refer to the Chunk grid, * positions with {@code level=1} refer to first-level Shard grid, and * so on. *

* The returned {@code NestedPosition} will have * {@link NestedPosition#level() level()==level}. * * @param position * position at {@code level} * @param level * nesting level of {@code position} * * @return a NestedPosition representation of the specified grid position and nesting level */ public NestedPosition nestedPosition(final long[] position, final int level) { return new NestedPosition(this, position, level); } /** * Create a {@code NestedPosition} at the specified chunk grid {@code * position} (that is, at nesting level 0). *

* Note that {@code position} is in units of chunks. *

* The returned {@code NestedPosition} will have * {@link NestedPosition#level() level()==0}. * * @param position * position at level 0 (chunk grid) * * @return a NestedPosition representation of the specified chunk grid position */ public NestedPosition nestedPosition(final long[] position) { return nestedPosition(position, 0); } public int numLevels() { return numLevels; } public int numDimensions() { return numDimensions; } /** * Get the block size in pixels at the given {@code level}. */ public int[] getBlockSize(final int level) { return blockSizes[level]; } /** * Computes the pixel position for the given {@code sourcePos} grid * position at {@code sourceLevel}. * * @param sourcePos * a grid position at {@code sourceLevel} * @param sourceLevel * nesting level of {@code sourcePos} * @param targetPos * the pixel position will be stored here */ public void pixelPosition( final long[] sourcePos, final int sourceLevel, final long[] targetPos) { final int[] s = blockSizes[sourceLevel]; for (int d = 0; d < numDimensions; ++d) { targetPos[d] = sourcePos[d] * s[d]; } } /** * Get the pixel position for the given {@code sourcePos} grid position * at {@code sourceLevel}. * * @param sourcePos * a grid position at {@code sourceLevel} * @param sourceLevel * nesting level of {@code sourcePos} * * @return the pixel position */ public long[] pixelPosition( final long[] sourcePos, final int sourceLevel) { final long[] targetPos = new long[numDimensions]; pixelPosition(sourcePos, sourceLevel, targetPos); return targetPos; } /** * Get the maximum pixel position in the block (chunk/shard) at the * given {@code sourcePos} grid position at {@code sourceLevel}. *

* Note that this does not take into account {@link #getDatasetSize() * dataset dimensions}. That is, it is always assumed that the * chunk/shard has the default size. * * @param sourcePos * a grid position at {@code sourceLevel} * @param sourceLevel * nesting level of {@code sourcePos} * @param targetPos * the pixel position will be stored here */ public void maxPixelPosition( final long[] sourcePos, final int sourceLevel, final long[] targetPos) { final int[] s = blockSizes[sourceLevel]; for (int d = 0; d < numDimensions; ++d) { targetPos[d] = (sourcePos[d] + 1) * s[d] - 1; } } /** * Get the maximum pixel position in the block (chunk/shard) at the * given {@code sourcePos} grid position at {@code sourceLevel}. *

* Note that this does not take into account {@link #getDatasetSize() * dataset dimensions}. That is, it is always assumed that the * chunk/shard has the default size. * * @param sourcePos * a grid position at {@code sourceLevel} * @param sourceLevel * nesting level of {@code sourcePos} * * @return the pixel position */ public long[] maxPixelPosition( final long[] sourcePos, final int sourceLevel) { final long[] targetPos = new long[numDimensions]; maxPixelPosition(sourcePos, sourceLevel, targetPos); return targetPos; } /** * Computes the absolute {@code targetPos} grid position at {@code * targetLevel} for the given {@code sourcePos} grid position at {@code * sourceLevel}. *

* For example, this can be used to compute the coordinates on the shard * grid ({@code targetLevel==1}) of the shard containing a given * chunk ({@code sourcePos} at {@code sourceLevel==0}). * * @param sourcePos * a grid position at {@code sourceLevel} * @param sourceLevel * nesting level of {@code sourcePos} * @param targetPos * the grid position at {@code targetLevel} will be stored here * @param targetLevel * nesting level of {@code targetPos} */ public void absolutePosition( final long[] sourcePos, final int sourceLevel, final long[] targetPos, final int targetLevel) { final int[] sk = relativeToBase[sourceLevel]; final int[] si = relativeToBase[targetLevel]; for (int d = 0; d < numDimensions; ++d) { targetPos[d] = sourcePos[d] * sk[d] / si[d]; } } /** * Get the absolute grid position at {@code targetLevel} for the given * {@code sourcePos} grid position at {@code sourceLevel}. *

* For example, this can be used to compute the coordinates on the shard * grid ({@code targetLevel==1}) of the shard containing a given * chunk ({@code sourcePos} at {@code sourceLevel==0}). * * @param sourcePos * the source position j * @param sourceLevel * the source level * @param targetLevel * the target level * * @return absolute position at the target level */ public long[] absolutePosition( final long[] sourcePos, final int sourceLevel, final int targetLevel) { if (sourceLevel == targetLevel) { return sourcePos; } final long[] targetPos = new long[numDimensions]; absolutePosition(sourcePos, sourceLevel, targetPos, targetLevel); return targetPos; } /** * Get the absolute grid position at {@code targetLevel} for the given * {@code sourcePos} chunk grid position (level 0). *

* For example, this can be used to compute the coordinates on the shard * grid ({@code targetLevel==1}) of the shard containing a given * chunk ({@code sourcePos}. * * @param sourcePos * the source position j * @param targetLevel * the target level * * @return absolute position at the target level */ public long[] absolutePosition( final long[] sourcePos, final int targetLevel) { return absolutePosition(sourcePos, 0, targetLevel); } /** * Computes the {@code targetPos} grid position at {@code targetLevel} * for the given {@code sourcePos} grid position at {@code sourceLevel}, * relative to the containing element at {@code targetLevel+1}. * (The containing element is a shard for {@code targetLevel+1 < * numLevels} or the dataset for {@code targetLevel+1 == numLevels}.) *

* For example, this can be used to compute the grid coordinates {@code * targetLevel==0} of a given chunk ({@code sourcePos} at {@code * sourceLevel==0}) within a shard (containing element at level {@code * targetLevel+1==1}). *

* * @param sourcePos * a grid position at {@code sourceLevel} * @param sourceLevel * nesting level of {@code sourcePos} * @param targetPos * the grid position at {@code targetLevel} will be stored here * @param targetLevel * nesting level of {@code targetPos} */ public void relativePosition( final long[] sourcePos, final int sourceLevel, final long[] targetPos, final int targetLevel) { absolutePosition(sourcePos, sourceLevel, targetPos, targetLevel); if (targetLevel < numLevels - 1) { final int[] rj = relativeToAdjacent[targetLevel + 1]; for (int d = 0; d < numDimensions; ++d) { targetPos[d] %= rj[d]; } } } public long[] relativePosition( final long[] sourcePos, final int sourceLevel, final int targetLevel) { final long[] targetPos = new long[numDimensions]; relativePosition(sourcePos, sourceLevel, targetPos, targetLevel); return targetPos; } /** * Get size of a block at the given {@code level} relative to {@code * level-1} (that is, in units of {@code level-1} blocks). *

* For example {@code relativeBlockSize(1)} returns the number of * chunks in a (non-nested) shard. */ public int[] relativeBlockSize(final int level) { return relativeToAdjacent[level]; } /** * Get size of a block at the given {@code level} relative to level {@code * 0} (that is, in units of chunks). *

* For example {@code relativeToBaseBlockSize(1)} returns the number of * chunks in a (non-nested) shard. */ public int[] relativeToBaseBlockSize(final int level) { return relativeToBase[level]; } /** * Get the size of the dataset in pixels. *

* This might return {@code null}, if this {@code NestedGrid} was not * constructed with dataset dimensions. * * @return size of the dataset in pixels */ public long[] getDatasetSize() { return datasetSize; } /** * Get the size of the dataset in units of chunks. *

* This might return {@code null}, if this {@code NestedGrid} was not * constructed with dataset dimensions. * * @return size of the dataset in chunks */ public long[] getDatasetSizeInChunks() { return datasetSizeInChunks; } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/PositionValueAccess.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.net.URI; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.KeyValueAccess; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; /** * Wrap a KeyValueAccess and a dataset URI to be able to get/set values (ReadData) by {@code long[]} key * indicating the position of a top-level DataBlock (a top-level shard for sharded datasets, * a chunk for non-sharded datasets). */ public interface PositionValueAccess { /** * Gets the {@link VolatileReadData} for the DataBlock at the given position * in the block grid. *

* If the requested key does not exist, either {@code null} is returned or a * lazy {@code VolatileReadData} that will throw {@code N5NoSuchKeyException} * when trying to materialize. * * @param key * The position of the block * @return ReadData for the given key or {@code null} if the key doesn't exist * @throws N5Exception.N5IOException * if an error occurs while reading */ VolatileReadData get(long[] key) throws N5Exception.N5IOException; /** * Write the {@code data} for a DataBlock to the given position in the block * grid. * * @param key * The grid position of the DataBlock to write * @param data * The data to write * * @throws N5Exception.N5IOException * if an error occurs while writing */ void set(long[] key, ReadData data) throws N5Exception.N5IOException; boolean exists(long[] key) throws N5Exception.N5IOException; boolean remove(long[] key) throws N5Exception.N5IOException; static PositionValueAccess fromKva( final KeyValueAccess kva, final URI uri, final String normalPath, final DatasetAttributes attributes) { return new KvaPositionValueAccess(kva, uri, normalPath, attributes); } class KvaPositionValueAccess implements PositionValueAccess { private final KeyValueAccess kva; private final URI uri; private final String normalPath; private final DatasetAttributes attributes; KvaPositionValueAccess(final KeyValueAccess kva, final URI uri, final String normalPath, final DatasetAttributes attributes) { this.kva = kva; this.uri = uri; this.normalPath = normalPath; this.attributes = attributes; } /** * Constructs the absolute path for a DataBlock at a given grid * position. * * @param gridPosition * to the target data block * @return the absolute path to the data block ad gridPosition */ protected String absolutePath(final long... gridPosition) { return kva.compose(uri, normalPath, attributes.relativeBlockPath(gridPosition)); } @Override public VolatileReadData get(final long[] key) throws N5IOException { try { return kva.createReadData(absolutePath(key)); } catch (N5Exception.N5NoSuchKeyException e) { return null; } } @Override public boolean exists(final long[] key) throws N5IOException { return kva.isFile(absolutePath(key)); } @Override public void set(final long[] key, final ReadData data) throws N5IOException { if (data == null) { remove(key); } else { kva.write(absolutePath(key), data); } } @Override public boolean remove(final long[] gridPosition) throws N5IOException { final String key = absolutePath(gridPosition); if (!kva.isFile(key)) return false; kva.delete(key); return true; } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/RawShard.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.ArrayList; import java.util.Collection; import java.util.List; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.segment.Segment; import org.janelia.saalfeldlab.n5.readdata.segment.SegmentedReadData; import org.janelia.saalfeldlab.n5.shard.ShardIndex.NDArray; public class RawShard { private final SegmentedReadData sourceData; private final NDArray index; RawShard(final int[] size) { sourceData = null; index = new NDArray<>(size, Segment[]::new); } RawShard(final SegmentedReadData sourceData, final NDArray index) { this.sourceData = sourceData; this.index = index; } RawShard(final ShardIndex.SegmentIndexAndData segmentIndexAndData) { this(segmentIndexAndData.data(), segmentIndexAndData.index()); } /** * The ReadData from which the shard was constructed, or {@code null} for a * new empty shard. * * @return this shard's source ReadData, or null. */ public SegmentedReadData sourceData() { return sourceData; } /** * Maps grid position of shard elements to {@link Segment}s that give the * byte range for the blocks in this shard. * * @return an NDArray of segments */ public NDArray index() { return index; } public boolean isEmpty() { return index().allElementsNull(); } public ReadData getElementData(final long[] pos) { final Segment segment = index.get(pos); return segment == null ? null : segment.source().slice(segment); } public void setElementData(final ReadData data, final long[] pos) { final Segment segment = data == null ? null : SegmentedReadData.wrap(data).segments().get(0); index.set(segment, pos); } public void prefetch(List positions) { final List ranges = new ArrayList<>(positions.size()); for (long[] pos : positions) { final Segment seg = index.get(pos); if (seg != null) ranges.add(sourceData.location(seg)); } sourceData.prefetch(ranges); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/RawShardCodec.java ================================================ package org.janelia.saalfeldlab.n5.shard; import static org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation.START; import java.util.ArrayList; import java.util.List; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.codec.BlockCodec; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.segment.Segment; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.segment.SegmentedReadData; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; import org.janelia.saalfeldlab.n5.shard.ShardIndex.NDArray; public class RawShardCodec implements BlockCodec { /** * Number of elements (chunks, nested shards) in each dimension per shard. */ private final int[] size; private final IndexLocation indexLocation; private final BlockCodec indexCodec; private final long indexBlockSizeInBytes; RawShardCodec(final int[] size, final IndexLocation indexLocation, final BlockCodec indexCodec) { this.size = size; this.indexLocation = indexLocation; this.indexCodec = indexCodec; indexBlockSizeInBytes = indexCodec.encodedSize(ShardIndex.blockSizeFromIndexSize(size)); } @Override public ReadData encode(final DataBlock shard) throws N5Exception.N5IOException { // concatenate slices for all non-null segments in shard.getData().index() final NDArray index = shard.getData().index(); final List readDatas = new ArrayList<>(); // TODO: Any clever ReadData grouping, slice merging, etc. should go here // This basic implementation just slices ReadData for all non-null // elements and concatenates in flat index order. for (Segment segment : index.data) { if (segment != null) { readDatas.add(segment.source().slice(segment)); } } final SegmentedReadData data = SegmentedReadData.concatenate(readDatas); final ReadData.Generator writer; if (indexLocation == START) { data.materialize(); final NDArray locations = ShardIndex.locations(index, data); final DataBlock indexDataBlock = ShardIndex.toDataBlock(locations, indexBlockSizeInBytes); final ReadData indexReadData = indexCodec.encode(indexDataBlock); writer = out -> { indexReadData.writeTo(out); data.writeTo(out); }; } else { // indexLocation == END writer = out -> { data.writeTo(out); final NDArray locations = ShardIndex.locations(index, data); final DataBlock indexDataBlock = ShardIndex.toDataBlock(locations, 0); final ReadData indexReadData = indexCodec.encode(indexDataBlock); indexReadData.writeTo(out); }; } return ReadData.from(writer); } @Override public DataBlock decode(final ReadData readData, final long[] gridPosition) throws N5Exception.N5IOException { final long indexOffset = (indexLocation == START) ? 0 : (readData.requireLength() - indexBlockSizeInBytes); final ReadData indexReadData = readData.slice(indexOffset, indexBlockSizeInBytes); final DataBlock indexDataBlock = indexCodec.decode(indexReadData, new long[size.length]); final NDArray locations = ShardIndex.fromDataBlock(indexDataBlock); final ShardIndex.SegmentIndexAndData segments = ShardIndex.segments(locations, readData); return new RawShardDataBlock(gridPosition, new RawShard(segments)); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/RawShardDataBlock.java ================================================ package org.janelia.saalfeldlab.n5.shard; import org.janelia.saalfeldlab.n5.DataBlock; /** * Wrap a RawShard as a DataBlock. * This basically just adds a gridPosition for the shard. */ public class RawShardDataBlock implements DataBlock { private final long[] gridPosition; private final RawShard shard; RawShardDataBlock(final long[] gridPosition, final RawShard shard) { this.gridPosition = gridPosition; this.shard = shard; } // TODO: should this be the number of elements in the Shard (number of // sub-shards / chunks) along each dimension, or the number of // pixels alon each dimension? @Override public int[] getSize() { return shard.index().size(); } @Override public long[] getGridPosition() { return gridPosition; } @Override public int getNumElements() { return shard.index().numElements(); } @Override public RawShard getData() { return shard; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/Region.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedGrid; /** * Bounds, in pixel coordinates, of a region in a dataset. *

* Provides methods to find which chunks and shards are contained in the * region, iterate sub-NestedPositions, etc. */ public class Region { /** * The dimensions of the full dataset. * This is used to decide whether DataBlocks are on the border (and therefore possibly truncated). */ private final long[] datasetDimensions; /** * The nested grid of the dataset */ private final NestedGrid grid; /** * min pixel position in the region */ private final long[] min; /** * size of the region in pixels */ private final long[] size; /** * {@code NestedPosition} of the chunk containing the min pixel position. */ private final Nesting.NestedPosition minPos; /** * {@code NestedPosition} of the chunk containing the max pixel position. */ private final Nesting.NestedPosition maxPos; public Region(final long[] min, final long[] size, final NestedGrid grid) { this.min = min; this.size = size; this.grid = grid; this.datasetDimensions = grid.getDatasetSize(); final int n = min.length; final int[] chunkSize = grid.getBlockSize(0); final long[] minChunk = new long[n]; Arrays.setAll(minChunk, d -> min[d] / chunkSize[d]); minPos = grid.nestedPosition(minChunk); final long[] maxChunk = new long[n]; Arrays.setAll(maxChunk, d -> (min[d] + size[d] - 1) / chunkSize[d]); maxPos = grid.nestedPosition(maxChunk); } /** * Get the {@code NestedPosition} of the minimum chunk touched by the region. */ public Nesting.NestedPosition minPos() { return minPos; } /** * Get the {@code NestedPosition} of the maximum chunk touched by the region. */ public Nesting.NestedPosition maxPos() { return maxPos; } /** * Check whether the shard or chunk corresponding to the given position is * fully contained inside the region. *

* The {@link Nesting.NestedPosition#level() level} of {@code position} is * used to determine whether it refers to a chunk or a (potentially nested) * shard. * * @param position * the NestedPosition to check * * @return true, if the given position is fully contained in this region */ public boolean fullyContains(final Nesting.NestedPosition position) { final long[] pmin = position.pixelPosition(); for (int d = 0; d < pmin.length; d++) { if (pmin[d] < min[d]) { return false; } } final long[] pmax = position.maxPixelPosition(); for (int d = 0; d < pmax.length; d++) { final long m = Math.min(pmax[d], datasetDimensions[d] - 1); if (m > min[d] + size[d] - 1) { return false; } } return true; } /** * Returns {@code NestedPosition}s of all nested elements in the given * {@code position} that are contained in this {@code Region}. *

* The returned {@code NestedPosition}s will all have level {@code * position.level()-1}. */ // TODO: Revise to accept Consumer for handling each position List containedNestedPositions(final Nesting.NestedPosition position) { final int level = position.level() - 1; final long[] gridMinOfRegion = minPos().absolute(level); final long[] gridMaxOfRegion = maxPos().absolute(level); final long[] gridMinOfPosition = position.absolute(level); final int[] gridSizeOfPosition = grid.relativeBlockSize(level + 1); final int n = grid.numDimensions(); final long[] gridMin = new long[n]; Arrays.setAll(gridMin, d -> Math.max(gridMinOfRegion[d], gridMinOfPosition[d])); final long[] gridMax = new long[n]; Arrays.setAll(gridMax, d -> Math.min(gridMaxOfRegion[d], gridMinOfPosition[d] + gridSizeOfPosition[d] - 1)); final List gridPositions = gridPositions(gridMin, gridMax); final List nestedPositions = new ArrayList<>(); gridPositions.forEach(p -> nestedPositions.add(grid.nestedPosition(p, level))); return nestedPositions; } // TODO: Revise to accept Consumer for handling each position public static List gridPositions(final long[] min, final long[] max) { final int n = min.length; final long[] pos = min.clone(); int numElements = 1; for (int d = 0; d < n; ++d) { numElements *= (int) (max[d] - min[d] + 1); } long[][] positions = new long[numElements][n]; Arrays.setAll(positions[0], j -> pos[j]); for (int i = 1; i < numElements; i++) { for (int d = 0; d < n; ++d) { if (++pos[d] <= max[d]) { Arrays.setAll(positions[i], j -> pos[j]); break; } pos[d] = min[d]; } } return Arrays.asList(positions); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/ShardCodecInfo.java ================================================ package org.janelia.saalfeldlab.n5.shard; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.codec.BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.DatasetCodecInfo; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; public interface ShardCodecInfo extends BlockCodecInfo { /** * Size in pixels of each shard element (either nested shard or chunk) * * @return the size of each shard element */ int[] getInnerBlockSize(); /** * * @return the collection of DatasetCodecInfo applied to data blocks for this shard */ DatasetCodecInfo[] getInnerDatasetCodecInfos(); /** * BlockCodecInfo for shard elements (either nested shard or DataBlock) * * @return the BlockCodecInfo for DataBlocks in this shard */ BlockCodecInfo getInnerBlockCodecInfo(); /** * @return the collection of DataCodecInfos applied to data blocks for this * shard. */ DataCodecInfo[] getInnerDataCodecInfos(); /** * BlockCodec for shard index * * @return the BlockCodecInfo for this shard's index */ BlockCodecInfo getIndexBlockCodecInfo(); /** * Deterministic-size DataCodecs for index BlockCodec * * @return the collection of DataCodecInfos for this shard's index */ DataCodecInfo[] getIndexDataCodecInfos(); IndexLocation getIndexLocation(); @SuppressWarnings("unchecked") @Override default RawShardCodec create(DataType dataType, int[] blockSize, DataCodecInfo... codecs) { return create(blockSize, codecs); } RawShardCodec create(int[] blockSize, DataCodecInfo... codecs); } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/shard/ShardIndex.java ================================================ 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.function.IntFunction; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.LongArrayDataBlock; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.segment.Segment; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.segment.SegmentedReadData; import org.janelia.saalfeldlab.n5.readdata.segment.SegmentedReadData.SegmentsAndData; import com.google.gson.annotations.SerializedName; public class ShardIndex { private ShardIndex() { // utility class. should not be instantiated. } public enum IndexLocation { @SerializedName("start") START, @SerializedName("end") END } /** * Access flat {@code T[]} array as n-dimensional array. * * @param * element type */ public static class NDArray { final int[] size; private final int[] stride; final T[] data; NDArray(final int[] size, final IntFunction createArray) { this.size = size; stride = getStrides(size); data = createArray.apply(getNumElements(size)); } NDArray(final int[] size, final T[] data) { this.size = size; stride = getStrides(size); this.data = data; } T get(long... position) { return data[index(position)]; } void set(T value, long... position) { data[index(position)] = value; } private int index(long... position) { int index = 0; for (int i = 0; i < stride.length; i++) { index += stride[i] * position[i]; } return index; } public int[] size() { return size; } public int numElements() { return data.length; } public boolean allElementsNull() { for (T t : data) { if (t != null) { return false; } } return true; } } static int getNumElements(final int[] size) { int numElements = 1; for (int s : size) { numElements *= s; } return numElements; } static int[] getStrides(final int[] size) { final int n = size.length; final int[] stride = new int[n]; stride[0] = 1; for (int i = 1; i < n; i++) { stride[i] = stride[i - 1] * size[i - 1]; } return stride; } /** * Special value indicating an empty block entry in the index. * Used for both offset and length when a block doesn't exist. */ static final long EMPTY_INDEX_NBYTES = 0xFFFFFFFFFFFFFFFFL; /** * Size of first dimension of the {@code DataBlock} representation of the shard index. */ private static final int LONGS_PER_BLOCK = 2; static NDArray fromDataBlock( final DataBlock block ) { final long[] blockData = block.getData(); final int[] size = indexSizeFromBlockSize(block.getSize()); final int n = getNumElements(size); final Range[] locations = new Range[n]; for (int i = 0; i < n; i++) { long offset = blockData[i * LONGS_PER_BLOCK]; long length = blockData[i * LONGS_PER_BLOCK + 1]; if (offset != EMPTY_INDEX_NBYTES && length != EMPTY_INDEX_NBYTES) { locations[i] = Range.at(offset, length); } } return new NDArray<>(size, locations); } static DataBlock toDataBlock( final NDArray locations, final long offset ) { final Range[] data = locations.data; final int[] blockSize = blockSizeFromIndexSize(locations.size); final long[] blockData = new long[data.length * 2]; for (int i = 0; i < data.length; ++i) { if (data[i] != null) { blockData[i * LONGS_PER_BLOCK] = data[i].offset() + offset; blockData[i * LONGS_PER_BLOCK + 1] = data[i].length(); } else { blockData[i * LONGS_PER_BLOCK] = EMPTY_INDEX_NBYTES; blockData[i * LONGS_PER_BLOCK + 1] = EMPTY_INDEX_NBYTES; } } return new LongArrayDataBlock(blockSize, new long[blockSize.length], blockData); } /** * Prepends a value to an array. * * @param value the value to prepend * @param array the original array * @return a new array with the value prepended */ 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; } /** * Prepends {@code LONGS_PER_BLOCK} to the {@code indexSize} array. */ static int[] blockSizeFromIndexSize(final int[] indexSize) { return prepend(LONGS_PER_BLOCK, indexSize); } /** * Strips first element (should be {@code LONGS_PER_BLOCK}) from the {@code blockSize} array. */ static int[] indexSizeFromBlockSize(final int[] blockSize) { assert blockSize[ 0 ] == LONGS_PER_BLOCK; return Arrays.copyOfRange(blockSize, 1, blockSize.length); } /** * Retrieves the {@code SegmentLocation} of each non-null {@code Segment} in * {@code segments}. Returns a {@code NDArray} with entries * corresponding tho the {@code segments} entries. */ static NDArray locations(final NDArray segments, final SegmentedReadData readData) { final Segment[] data = segments.data; final Range[] locations = new Range[data.length]; for (int i = 0; i < data.length; ++i) { final Segment segment = data[i]; if ( segment != null ) { locations[i] = readData.location(segment); } } return new NDArray<>(segments.size, locations); } interface SegmentIndexAndData { NDArray index(); SegmentedReadData data(); } /** * Puts a {@code Segment} at each non-null {@code SegmentLocation} in {@code * locations} on the given {@code readData}. Returns both the {@code * SegmentedReadData} with these segments and a {@code NDArray} * with segment entries corresponding to the {@code locations} entries. */ static SegmentIndexAndData segments(final NDArray locations, final ReadData readData) { final Range[] locationsData = locations.data; final Segment[] segmentsData = new Segment[locationsData.length]; final List presentLocations = new ArrayList<>(); for (int i = 0; i < locationsData.length; i++) { if (locationsData[i] != null) { presentLocations.add(locationsData[i]); } } final SegmentsAndData segmentsAndData = SegmentedReadData.wrap(readData, presentLocations); final Iterator presentSegments = segmentsAndData.segments().iterator(); for (int i = 0; i < locationsData.length; i++) { if (locationsData[i] != null) { segmentsData[i] = presentSegments.next(); } } final NDArray index = new NDArray<>(locations.size, segmentsData); final SegmentedReadData data = segmentsAndData.data(); return new SegmentIndexAndData() { @Override public NDArray index() {return index;} @Override public SegmentedReadData data() {return data;} }; } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/util/FloatValueParser.java ================================================ package org.janelia.saalfeldlab.n5.util; import org.apache.commons.codec.DecoderException; import org.apache.commons.codec.binary.Hex; import org.janelia.saalfeldlab.n5.N5Exception; /** * Parses {@link Float} and {@link Double} values from JSON hex strings. *

* This does not directly cover the Strings "NaN", "Infinity", and "-Infinity" * but they are parsable by the parseDouble and parseFloat methods. Rather, this * class handles converting to and from the hex representations of NaN, * -Infinity, Infinity, and all other allowable values. */ public class FloatValueParser { /** * Parses a hex string to a float value. * * @param hexString * hex string in format "0x" followed by 8 hex digits * @return the float value * @throws N5Exception * if the string format is invalid */ public static float parseFloat(String hexString) throws N5Exception { validateFloat(hexString); final int intValue = Integer.parseUnsignedInt(hexString.substring(2), 16); return Float.intBitsToFloat(intValue); } /** * Encodes a float value to a hex string. * * @param value * the float to encode * @return hex string in format "0x" followed by 8 hex digits */ public static String encodeFloat(float value) { return String.format("0x%08x", Float.floatToIntBits(value)); } private static void validateFloat(String hexString) { if (!hexString.startsWith("0x") || hexString.length() != 10) throw new N5Exception("Could not parse string " + hexString + " as float."); } /** * Parses a hex string to a double value. * * @param hexString * hex string in format "0x" followed by 16 hex digits * @return the double value * @throws N5Exception * if the string format is invalid */ public static double parseDouble(String hexString) throws N5Exception { validateDouble(hexString); final long longValue = Long.parseUnsignedLong(hexString.substring(2), 16); return Double.longBitsToDouble(longValue); } /** * Encodes a double value to a hex string. * * @param value * the double to encode * @return hex string in format "0x" followed by 16 hex digits */ public static String encodeDouble(double value) { return String.format("0x%016x", Double.doubleToLongBits(value)); } private static void validateDouble(String hexString) { if (!hexString.startsWith("0x") || hexString.length() != 18) throw new N5Exception("Could not parse string " + hexString + " as double."); } /** * Parses a hex string to a byte array. * * @param hexString * hex string in format "0x" followed by hex digits * @return the decoded byte array * @throws N5Exception * if the string format is invalid or decoding fails */ public static byte[] parseBytes(String hexString) throws N5Exception { validateBytes(hexString); try { return Hex.decodeHex(hexString.substring(2)); } catch (DecoderException e) { throw new N5Exception(e); } } private static void validateBytes(String hexString) { if (!hexString.startsWith("0x")) throw new N5Exception("Could not parse string " + hexString + " to bytes."); } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/util/MemCopy.java ================================================ package org.janelia.saalfeldlab.n5.util; import org.janelia.saalfeldlab.n5.DataType; /** * Low-level range copying methods between source and target primitve array * (type {@code T}, e.g., {@code double[]}). * * @param * the source/target type. Must be a primitive array type (e.g., {@code double[]}) */ public interface MemCopy { MemCopyByte BYTE = new MemCopyByte(); MemCopyShort SHORT = new MemCopyShort(); MemCopyInt INT = new MemCopyInt(); MemCopyLong LONG = new MemCopyLong(); MemCopyFloat FLOAT = new MemCopyFloat(); MemCopyDouble DOUBLE = new MemCopyDouble(); static MemCopy forDataType(final DataType dataType) { switch (dataType) { case UINT8: case INT8: return BYTE; case UINT16: case INT16: return SHORT; case UINT32: case INT32: return INT; case UINT64: case INT64: return LONG; case FLOAT32: return FLOAT; case FLOAT64: return DOUBLE; case STRING: case OBJECT: throw new UnsupportedOperationException("TODO?"); default: throw new IllegalArgumentException(); } } /** * Copy {@code length} components from the {@code src} array to the {@code * dest} array. The components at positions {@code srcPos} through {@code * srcPos+length-1} in the source array are copied into positions {@code * destPos}, {@code destPos+destStride}, {@code destPos + 2*destStride}, * etc., through {@code destPos+(length-1)*destStride} of the destination * array. */ void copyStrided(T src, int srcPos, T dest, int destPos, int destStride, int length); class MemCopyByte implements MemCopy { @Override public void copyStrided(final byte[] src, final int srcPos, final byte[] dest, final int destPos, final int destStride, final int length) { if (destStride == 1) System.arraycopy(src, srcPos, dest, destPos, length); else for (int i = 0; i < length; ++i) dest[destPos + i * destStride] = src[srcPos + i]; } } class MemCopyShort implements MemCopy { @Override public void copyStrided(final short[] src, final int srcPos, final short[] dest, final int destPos, final int destStride, final int length) { if (destStride == 1) System.arraycopy(src, srcPos, dest, destPos, length); else for (int i = 0; i < length; ++i) dest[destPos + i * destStride] = src[srcPos + i]; } } class MemCopyInt implements MemCopy { @Override public void copyStrided(final int[] src, final int srcPos, final int[] dest, final int destPos, final int destStride, final int length) { if (destStride == 1) System.arraycopy(src, srcPos, dest, destPos, length); else for (int i = 0; i < length; ++i) dest[destPos + i * destStride] = src[srcPos + i]; } } class MemCopyLong implements MemCopy { @Override public void copyStrided(final long[] src, final int srcPos, final long[] dest, final int destPos, final int destStride, final int length) { if (destStride == 1) System.arraycopy(src, srcPos, dest, destPos, length); else for (int i = 0; i < length; ++i) dest[destPos + i * destStride] = src[srcPos + i]; } } class MemCopyFloat implements MemCopy { @Override public void copyStrided(final float[] src, final int srcPos, final float[] dest, final int destPos, final int destStride, final int length) { if (destStride == 1) System.arraycopy(src, srcPos, dest, destPos, length); else for (int i = 0; i < length; ++i) dest[destPos + i * destStride] = src[srcPos + i]; } } class MemCopyDouble implements MemCopy { @Override public void copyStrided(final double[] src, final int srcPos, final double[] dest, final int destPos, final int destStride, final int length) { if (destStride == 1) System.arraycopy(src, srcPos, dest, destPos, length); else for (int i = 0; i < length; ++i) dest[destPos + i * destStride] = src[srcPos + i]; } } } ================================================ FILE: src/main/java/org/janelia/saalfeldlab/n5/util/SubArrayCopy.java ================================================ package org.janelia.saalfeldlab.n5.util; /** * Copy sub-region between flattened arrays (of different sizes). *

* The {@link #copy(Object, int[], int[], Object, int[], int[], int[]) SubArrayCopy.copy} * method requires the nD size of the flattened source and target arrays, the nD * starting position and nD size of the source region to copy, and the nD * starting position in the target to copy to. */ public interface SubArrayCopy { /** * Copy a nD region from {@code src} to {@code dest}, where {@code src} and * {@code dest} are flattened nD array of dimensions {@code srcSize} and * {@code destSize}, respectively. * * @param src * flattened nD source array * @param srcSize * dimensions of src * @param srcPos * starting position, in src, of the range to copy * @param dest * flattened nD destination array * @param destSize * dimensions of dest * @param destPos * starting position, in dest, of the range to copy * @param size * size of the range to copy */ // TODO: generic T instead of Object to make sure src and dest are the same primitive array type static void copy( Object src, int[] srcSize, int[] srcPos, Object dest, int[] destSize, int[] destPos, int[] size ) { final int n = srcSize.length; assert srcPos.length == n; assert destSize.length == n; assert destPos.length == n; assert size.length == n; final int[] srcStrides = createAllocationSteps( srcSize ); final int[] destStrides = createAllocationSteps( destSize ); final int oSrc = positionToIndex( srcPos, srcSize ); final int oDest = positionToIndex( destPos, destSize ); copyNDRangeRecursive( n - 1, src, srcStrides, oSrc, dest, destStrides, oDest, size ); } // TODO: maybe hide the implementation details below in an inner class static int positionToIndex( final int[] position, final int[] dimensions ) { final int maxDim = dimensions.length - 1; int i = position[ maxDim ]; for ( int d = maxDim - 1; d >= 0; --d ) i = i * dimensions[ d ] + position[ d ]; return i; } /** * Create allocation step array from the dimensions of an N-dimensional * array. * * @param dimensions * @param steps */ // TODO: rename to something with "stride" // TODO: inline into method below (or always use this one) static void createAllocationSteps( final int[] dimensions, final int[] steps ) { steps[ 0 ] = 1; for ( int d = 1; d < dimensions.length; ++d ) steps[ d ] = steps[ d - 1 ] * dimensions[ d - 1 ]; } // TODO: rename to something with "stride" // TODO: inline above (or always use that one) static int[] createAllocationSteps( final int[] dimensions ) { final int[] steps = new int[ dimensions.length ]; createAllocationSteps( dimensions, steps ); return steps; } /** * Recursively copy a {@code (d+1)} dimensional region from {@code src} to * {@code dest}, where {@code src} and {@code dest} are flattened nD array * with strides {@code srcStrides} and {@code destStrides}, respectively. *

* For {@code d=0}, a 1D line of length {@code size[0]} is copied * (equivalent to {@code System.arraycopy}). For {@code d=1}, a 2D plane of * size {@code size[0] * size[1]} is copied, by recursively copying 1D * lines, starting {@code srcStrides[1]} (respectively {@code * destStrides[1]}) apart. For {@code d=2}, a 3D box is copied by * recursively copying 2D planes, etc. * * @param d * current dimension * @param src * flattened nD source array * @param srcStrides * nD strides of src * @param srcPos * flattened index (in src) to start copying from * @param dest * flattened nD destination array * @param destStrides * nD strides of dest * @param destPos * flattened index (in dest) to start copying to * @param size * nD size of the range to copy */ static void copyNDRangeRecursive( final int d, final T src, final int[] srcStrides, final int srcPos, final T dest, final int[] destStrides, final int destPos, final int[] size ) { final int len = size[d]; if (d > 0) { final int stride_src = srcStrides[d]; final int stride_dst = destStrides[d]; for (int i = 0; i < len; ++i) copyNDRangeRecursive(d - 1, src, srcStrides, srcPos + i * stride_src, dest, destStrides, destPos + i * stride_dst, size); } else System.arraycopy(src, srcPos, dest, destPos, len); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/AbstractN5Test.java ================================================ package org.janelia.saalfeldlab.n5; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.function.Predicate; import org.janelia.saalfeldlab.n5.N5Exception.N5ClassCastException; import org.janelia.saalfeldlab.n5.N5Reader.Version; import org.janelia.saalfeldlab.n5.url.UriAttributeTest; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import com.google.gson.GsonBuilder; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import com.google.gson.reflect.TypeToken; /** * Abstract base class for testing N5 functionality. * Subclasses are expected to provide a specific N5 implementation to be tested by defining the {@link #createN5Writer()} method. *

* This class does not create sharded datasets. Its tests generally call read/writeBlock which are equivalent to read/writeChunk * for the cases being tested here. The test {@link #testReadChunkVsBlock} checks that the equivalence between these methods holds. * * @author Stephan Saalfeld <saalfelds@janelia.hhmi.org> * @author Igor Pisarev <pisarevi@janelia.hhmi.org> * @author John Bogovic <bogovicj@janelia.hhmi.org> * @author Caleb Hulbert <hulbertc@janelia.hhmi.org> */ public abstract class AbstractN5Test { static protected final String groupName = "/test/group"; static protected final String[] subGroupNames = new String[]{"a", "b", "c"}; static protected final String datasetName = "/test/group/dataset"; static protected final long[] dimensions = new long[]{6, 15, 35}; static protected final int[] blockSize = new int[]{3, 5, 7}; static protected final int blockNumElements = blockSize[0] * blockSize[1] * blockSize[2]; static protected byte[] byteBlock; static protected short[] shortBlock; static protected int[] intBlock; static protected long[] longBlock; static protected float[] floatBlock; static protected double[] doubleBlock; protected final HashSet tempWriters = new HashSet<>(); protected static Random random = new Random(); public static URI createTempUri(String prefix, String suffix, URI base) { long rand = random.nextLong(); String name = prefix + Long.toUnsignedString(rand) + (suffix == null ? "" : suffix); if (base != null) { String basePath = base.getPath().isEmpty() || base.getPath().endsWith("/") ? base.getPath() : base.getPath() + "/"; return base.resolve(basePath + name); } return N5URI.getAsUri(name); } public N5Writer createTempN5Writer() { try { return createTempN5Writer(tempN5Location()); } catch (URISyntaxException | IOException e) { throw new RuntimeException(e); } } public final N5Writer createTempN5Writer(String location) { return createTempN5Writer(location, new GsonBuilder()); } protected final N5Writer createTempN5Writer(String location, GsonBuilder gson) { final N5Writer tempWriter; try { tempWriter = createN5Writer(location, gson); } catch (IOException | URISyntaxException e) { throw new RuntimeException(e); } tempWriters.add(tempWriter); return tempWriter; } @After public void removeTempWriters() { synchronized (tempWriters) { for (final N5Writer writer : tempWriters) { try { writer.remove(); } catch (final Exception e) { } } tempWriters.clear(); } } protected abstract String tempN5Location() throws URISyntaxException, IOException; protected N5Writer createN5Writer() throws IOException, URISyntaxException { return createN5Writer(tempN5Location()); } protected N5Writer createN5Writer(final String location) throws IOException, URISyntaxException { return createN5Writer(location, new GsonBuilder()); } /* Tests that override this should ensure that the `N5Writer` created will remove its container on close() */ protected abstract N5Writer createN5Writer(String location, GsonBuilder gson) throws IOException, URISyntaxException; protected N5Reader createN5Reader(final String location) throws IOException, URISyntaxException { return createN5Reader(location, new GsonBuilder()); } protected abstract N5Reader createN5Reader(String location, GsonBuilder gson) throws IOException, URISyntaxException; protected Compression[] getCompressions() { return new Compression[]{ new RawCompression(), new Bzip2Compression(), new GzipCompression(), new GzipCompression(5, true), new Lz4Compression(), new XzCompression() }; } @Before public void setUpOnce() { final Random rnd = new Random(111); byteBlock = new byte[blockNumElements]; shortBlock = new short[blockNumElements]; intBlock = new int[blockNumElements]; longBlock = new long[blockNumElements]; floatBlock = new float[blockNumElements]; doubleBlock = new double[blockNumElements]; rnd.nextBytes(byteBlock); for (int i = 0; i < blockNumElements; ++i) { shortBlock[i] = (short)rnd.nextInt(); intBlock[i] = rnd.nextInt(); longBlock[i] = rnd.nextLong(); floatBlock[i] = Float.intBitsToFloat(rnd.nextInt()); doubleBlock[i] = Double.longBitsToDouble(rnd.nextLong()); } } @Test public void testCreateGroup() { try (N5Writer n5 = createTempN5Writer()) { n5.createGroup(groupName); assertTrue("Group does not exist: " + groupName, n5.exists(groupName)); final Path groupPath = Paths.get(groupName); String subGroup = ""; for (int i = 0; i < groupPath.getNameCount(); ++i) { subGroup = subGroup + "/" + groupPath.getName(i); assertTrue("Group does not exist: " + subGroup, n5.exists(subGroup)); } } } @Test public void testSetAttributeDoesntCreateGroup() { try (final N5Writer writer = createTempN5Writer()) { final String testGroup = "/group/should/not/exit"; assertFalse(writer.exists(testGroup)); assertThrows(N5Exception.N5IOException.class, () -> writer.setAttribute(testGroup, "test", "test")); assertFalse(writer.exists(testGroup)); } } @Test public void testCreateDataset() { final DatasetAttributes info; try (N5Writer writer = createTempN5Writer()) { writer.createDataset(datasetName, dimensions, blockSize, DataType.UINT64, new RawCompression()); assertTrue("Dataset does not exist", writer.exists(datasetName)); info = writer.getDatasetAttributes(datasetName); } assertArrayEquals(dimensions, info.getDimensions()); assertArrayEquals(blockSize, info.getBlockSize()); assertArrayEquals("blockSize == chunkSize when not sharded", blockSize, info.getChunkSize()); assertEquals(DataType.UINT64, info.getDataType()); } @Test public void testBlocksLargerThanDimensions() { // Test case where block size is larger than dataset dimensions final long[] smallDimensions = new long[]{2, 3, 4}; final int[] largeBlockSize = new int[]{5, 7, 10}; try (final N5Writer n5 = createTempN5Writer()) { final DatasetAttributes attributes = n5.createDataset( datasetName, smallDimensions, largeBlockSize, DataType.UINT8, new RawCompression()); // Create a block that is larger than the dataset dimensions final int numElements = largeBlockSize[0] * largeBlockSize[1] * largeBlockSize[2]; final byte[] data = new byte[numElements]; for (int i = 0; i < numElements; i++) { data[i] = (byte)(i % 256); } final ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(largeBlockSize, new long[]{0, 0, 0}, data); n5.writeBlock(datasetName, attributes, dataBlock); // Read the block back final DataBlock loadedDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertNotNull("Block should be readable", loadedDataBlock); assertArrayEquals("Block size should match", largeBlockSize, loadedDataBlock.getSize()); assertArrayEquals("Block data should match", data, (byte[])loadedDataBlock.getData()); } } @Test public void testUnalignedBlocksTruncatedAtEnd() { // Test case where dimensions don't evenly divide by block size final long[] unalignedDimensions = new long[]{5, 14, 33}; final int[] testBlockSize = new int[]{3, 5, 7}; try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, unalignedDimensions, testBlockSize, DataType.INT32, new RawCompression()); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); // Test writing to the last block in dimension 0 (should be truncated to size 2 instead of 3) final int[] truncatedBlockSize0 = new int[]{2, 5, 7}; // [3-4] in dim 0 final int numElements0 = truncatedBlockSize0[0] * truncatedBlockSize0[1] * truncatedBlockSize0[2]; final int[] data0 = new int[numElements0]; for (int i = 0; i < numElements0; i++) { data0[i] = i + 1000; } final IntArrayDataBlock dataBlock0 = new IntArrayDataBlock(truncatedBlockSize0, new long[]{1, 0, 0}, data0); n5.writeBlock(datasetName, attributes, dataBlock0); final DataBlock loadedBlock0 = n5.readBlock(datasetName, attributes, 1, 0, 0); assertNotNull("Truncated block should be readable", loadedBlock0); assertArrayEquals("Truncated block data should match", data0, (int[])loadedBlock0.getData()); // Test writing to the last block in dimension 1 (should be truncated to size 4 instead of 5) final int[] truncatedBlockSize1 = new int[]{3, 4, 7}; // [10-13] in dim 1 final int numElements1 = truncatedBlockSize1[0] * truncatedBlockSize1[1] * truncatedBlockSize1[2]; final int[] data1 = new int[numElements1]; for (int i = 0; i < numElements1; i++) { data1[i] = i + 2000; } final IntArrayDataBlock dataBlock1 = new IntArrayDataBlock(truncatedBlockSize1, new long[]{0, 2, 0}, data1); n5.writeBlock(datasetName, attributes, dataBlock1); final DataBlock loadedBlock1 = n5.readBlock(datasetName, attributes, 0, 2, 0); assertNotNull("Truncated block should be readable", loadedBlock1); assertArrayEquals("Truncated block data should match", data1, (int[])loadedBlock1.getData()); // Test writing to the last block in dimension 2 (should be truncated to size 5 instead of 7) final int[] truncatedBlockSize2 = new int[]{3, 5, 5}; // [28-32] in dim 2 final int numElements2 = truncatedBlockSize2[0] * truncatedBlockSize2[1] * truncatedBlockSize2[2]; final int[] data2 = new int[numElements2]; for (int i = 0; i < numElements2; i++) { data2[i] = i + 3000; } final IntArrayDataBlock dataBlock2 = new IntArrayDataBlock(truncatedBlockSize2, new long[]{0, 0, 4}, data2); n5.writeBlock(datasetName, attributes, dataBlock2); final DataBlock loadedBlock2 = n5.readBlock(datasetName, attributes, 0, 0, 4); assertNotNull("Truncated block should be readable", loadedBlock2); assertArrayEquals("Truncated block data should match", data2, (int[])loadedBlock2.getData()); } } @Test public void testWriteReadByteBlock() { for (final Compression compression : getCompressions()) { for (final DataType dataType : new DataType[]{ DataType.UINT8, DataType.INT8}) { try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, dataType, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(blockSize, new long[]{0, 0, 0}, byteBlock); n5.writeBlock(datasetName, attributes, dataBlock); final DataBlock loadedDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(byteBlock, (byte[])loadedDataBlock.getData()); } } } } @Test public void testWriteReadStringBlock() { // test dataset; all characters are valid UTF8 but may have different numbers of bytes! final DataType dataType = DataType.STRING; final int[] blockSize = new int[]{3, 2, 1}; final String[] stringBlock = new String[]{"", "a", "bc", "de", "fgh", ":-þ"}; for (final Compression compression : getCompressions()) { try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, dataType, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final StringDataBlock dataBlock = new StringDataBlock(blockSize, new long[]{0L, 0L, 0L}, stringBlock); n5.writeBlock(datasetName, attributes, dataBlock); final DataBlock loadedDataBlock = n5.readBlock(datasetName, attributes, 0L, 0L, 0L); assertArrayEquals(stringBlock, (String[])loadedDataBlock.getData()); } } } @Test public void testWriteReadShortBlock() { for (final Compression compression : getCompressions()) { for (final DataType dataType : new DataType[]{ DataType.UINT16, DataType.INT16}) { try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, dataType, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final ShortArrayDataBlock dataBlock = new ShortArrayDataBlock(blockSize, new long[]{0, 0, 0}, shortBlock); n5.writeBlock(datasetName, attributes, dataBlock); final DataBlock loadedDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(shortBlock, (short[])loadedDataBlock.getData()); } } } } @Test public void testWriteReadIntBlock() { for (final Compression compression : getCompressions()) { for (final DataType dataType : new DataType[]{ DataType.UINT32, DataType.INT32}) { try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, dataType, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final IntArrayDataBlock dataBlock = new IntArrayDataBlock(blockSize, new long[]{0, 0, 0}, intBlock); n5.writeBlock(datasetName, attributes, dataBlock); final DataBlock loadedDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(intBlock, (int[])loadedDataBlock.getData()); } } } } @Test public void testWriteReadLongBlock() { for (final Compression compression : getCompressions()) { for (final DataType dataType : new DataType[]{ DataType.UINT64, DataType.INT64}) { try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, dataType, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final LongArrayDataBlock dataBlock = new LongArrayDataBlock(blockSize, new long[]{0, 0, 0}, longBlock); n5.writeBlock(datasetName, attributes, dataBlock); final DataBlock loadedDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(longBlock, (long[])loadedDataBlock.getData()); } } } } @Test public void testWriteReadFloatBlock() { for (final Compression compression : getCompressions()) { try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, DataType.FLOAT32, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final FloatArrayDataBlock dataBlock = new FloatArrayDataBlock(blockSize, new long[]{0, 0, 0}, floatBlock); n5.writeBlock(datasetName, attributes, dataBlock); final DataBlock loadedDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(floatBlock, (float[])loadedDataBlock.getData(), 0.001f); } } } @Test public void testWriteReadDoubleBlock() { for (final Compression compression : getCompressions()) { try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, DataType.FLOAT64, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final DoubleArrayDataBlock dataBlock = new DoubleArrayDataBlock(blockSize, new long[]{0, 0, 0}, doubleBlock); n5.writeBlock(datasetName, attributes, dataBlock); final DataBlock loadedDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(doubleBlock, (double[])loadedDataBlock.getData(), 0.001); } } } @Test public void testReadChunkVsBlock() { // test that readBlock behaves the same as readChunk for unsharded datasets for (final Compression compression : getCompressions()) { try (final N5Writer n5 = createTempN5Writer()) { final short[] shortData1 = new short[shortBlock.length]; for( int i = 0; i < shortBlock.length; i++) shortData1[i] = (short)(2 * shortBlock[i] + 3); n5.createDataset(datasetName, dimensions, blockSize, DataType.INT16, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final ShortArrayDataBlock dataBlock0 = new ShortArrayDataBlock(blockSize, new long[]{0, 0, 0}, shortBlock); final ShortArrayDataBlock dataBlock1 = new ShortArrayDataBlock(blockSize, new long[]{1, 0, 0}, shortData1); n5.writeChunk(datasetName, attributes, dataBlock0); n5.writeBlock(datasetName, attributes, dataBlock1); // read with readBlock assertArrayEquals(shortBlock, (short[])n5.readBlock(datasetName, attributes, 0, 0, 0).getData()); assertArrayEquals(shortData1, (short[])n5.readBlock(datasetName, attributes, 1, 0, 0).getData()); // read with readChunk assertArrayEquals(shortBlock, (short[])n5.readChunk(datasetName, attributes, 0, 0, 0).getData()); assertArrayEquals(shortData1, (short[])n5.readChunk(datasetName, attributes, 1, 0, 0).getData()); } } } @Test public void testMode1WriteReadByteBlock() { final int[] differentBlockSize = new int[]{5, 10, 15}; for (final Compression compression : getCompressions()) { for (final DataType dataType : new DataType[]{ DataType.UINT8, DataType.INT8}) { try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, differentBlockSize, dataType, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(differentBlockSize, new long[]{0, 0, 0}, byteBlock); n5.writeBlock(datasetName, attributes, dataBlock); final DataBlock loadedDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(byteBlock, (byte[])loadedDataBlock.getData()); } } } } @Test public void testWriteReadSerializableBlock() throws ClassNotFoundException { for (final Compression compression : getCompressions()) { final DataType dataType = DataType.OBJECT; try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, dataType, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final HashMap> object = new HashMap<>(); object.put("one", new ArrayList<>()); object.put("two", new ArrayList<>()); object.get("one").add(new double[]{1, 2, 3}); object.get("two").add(new double[]{4, 5, 6, 7, 8}); n5.writeSerializedBlock(object, datasetName, attributes, 0, 0, 0); final HashMap> loadedObject = n5.readSerializedBlock(datasetName, attributes, new long[]{0, 0, 0}); object.forEach((key, value) -> assertArrayEquals(value.get(0), loadedObject.get(key).get(0), 0.01)); } } } @Test @Ignore // TODO public void testWriteInvalidBlock() { final Compression compression = getCompressions()[0]; final DataType dataType = DataType.UINT8; final int[] biggerBlockSize = Arrays.stream(blockSize).map(x -> x + 2).toArray(); int nBigger = Arrays.stream(biggerBlockSize).reduce(1, (x, y) -> x * y); final int[] smallerBlockSize = Arrays.stream(blockSize).map(x -> x - 2).toArray(); int nSmaller = Arrays.stream(smallerBlockSize).reduce(1, (x, y) -> x * y); int N = Arrays.stream(blockSize).reduce(1, (x, y) -> x * y); final Random rnd = new Random(7560); final byte[] biggerData = new byte[nBigger]; rnd.nextBytes(biggerData); final byte[] smallerData = new byte[nSmaller]; rnd.nextBytes(smallerData); final float[] floatData = new float[N]; try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, dataType, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); // write a block that is too large final ByteArrayDataBlock bigDataBlock = new ByteArrayDataBlock(biggerBlockSize, new long[]{0, 0, 0}, biggerData); n5.writeBlock(datasetName, attributes, bigDataBlock); final DataBlock loadedBigDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(biggerData, (byte[])loadedBigDataBlock.getData()); // write a block that is too small final ByteArrayDataBlock smallDataBlock = new ByteArrayDataBlock(smallerBlockSize, new long[]{0, 0, 0}, smallerData); n5.writeBlock(datasetName, attributes, smallDataBlock); final DataBlock loadedSmallDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(smallerData, (byte[])loadedSmallDataBlock.getData()); // write a block of the wrong type final FloatArrayDataBlock floatDataBlock = new FloatArrayDataBlock(blockSize, new long[]{0, 0, 0}, floatData); assertThrows(ClassCastException.class, () -> { n5.writeBlock(datasetName, attributes, floatDataBlock); }); } } @Test public void testOverwriteBlock() { final Compression compression = getCompressions()[0]; try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, DataType.INT32, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final IntArrayDataBlock randomDataBlock = new IntArrayDataBlock(blockSize, new long[]{0, 0, 0}, intBlock); n5.writeBlock(datasetName, attributes, randomDataBlock); final DataBlock loadedRandomDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(intBlock, (int[])loadedRandomDataBlock.getData()); // test the case where the resulting file becomes shorter (because the data compresses better) final int[] emptyBlock = new int[DataBlock.getNumElements(blockSize)]; final IntArrayDataBlock emptyDataBlock = new IntArrayDataBlock(blockSize, new long[]{0, 0, 0}, emptyBlock); n5.writeBlock(datasetName, attributes, emptyDataBlock); final DataBlock loadedEmptyDataBlock = n5.readBlock(datasetName, attributes, 0, 0, 0); assertArrayEquals(emptyBlock, (int[])loadedEmptyDataBlock.getData()); } } @Test public void testAttributeParsingPrimitive() { try (final N5Writer n5 = createTempN5Writer()) { n5.createGroup(groupName); /* Test parsing of int, int[], double, double[], String, and String[] types * * All types are parseable as JsonElements or String * * ints are also parseable as doubles and Strings * doubles are also parseable as ints and Strings * Strings should be parsable as Strings * * int[]s should be parseable as double[]s and String[]s * double[]s should be parseable as double[]s and String[]s * String[]s should be parsable as String[]s */ n5.setAttribute(groupName, "key", "value"); assertEquals("value", n5.getAttribute(groupName, "key", String.class)); assertNotNull(n5.getAttribute(groupName, "key", JsonElement.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", Integer.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", int[].class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", Double.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", double[].class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", String[].class)); n5.setAttribute(groupName, "key", new String[]{"value"}); assertArrayEquals(new String[]{"value"}, n5.getAttribute(groupName, "key", String[].class)); assertEquals(JsonParser.parseString("[\"value\"]"), JsonParser.parseString(n5.getAttribute(groupName, "key", String.class))); assertNotNull(n5.getAttribute(groupName, "key", JsonElement.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", Integer.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", int[].class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", Double.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", double[].class)); n5.setAttribute(groupName, "key", 1); assertEquals(1, (long)n5.getAttribute(groupName, "key", Integer.class)); assertEquals(1.0, n5.getAttribute(groupName, "key", Double.class), 1e-9); assertEquals("1", n5.getAttribute(groupName, "key", String.class)); assertNotNull(n5.getAttribute(groupName, "key", JsonElement.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", int[].class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", double[].class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", String[].class)); n5.setAttribute(groupName, "key", new int[]{2, 3}); assertArrayEquals(new int[]{2, 3}, n5.getAttribute(groupName, "key", int[].class)); assertArrayEquals(new double[]{2.0, 3.0}, n5.getAttribute(groupName, "key", double[].class), 1e-9); assertEquals(JsonParser.parseString("[2,3]"), JsonParser.parseString(n5.getAttribute(groupName, "key", String.class))); assertArrayEquals(new String[]{"2", "3"}, n5.getAttribute(groupName, "key", String[].class)); assertNotNull(n5.getAttribute(groupName, "key", JsonElement.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", Integer.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", Double.class)); n5.setAttribute(groupName, "key", 0.1); assertEquals(0, (long)n5.getAttribute(groupName, "key", Integer.class)); assertEquals(0.1, n5.getAttribute(groupName, "key", Double.class), 1e-9); assertEquals("0.1", n5.getAttribute(groupName, "key", String.class)); assertNotNull(n5.getAttribute(groupName, "key", JsonElement.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", int[].class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", double[].class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", String[].class)); n5.setAttribute(groupName, "key", new double[]{0.2, 0.3}); assertArrayEquals(new int[]{0, 0}, n5.getAttribute(groupName, "key", int[].class)); // TODO returns not null, is this right? assertArrayEquals(new double[]{0.2, 0.3}, n5.getAttribute(groupName, "key", double[].class), 1e-9); assertEquals(JsonParser.parseString("[0.2,0.3]"), JsonParser.parseString(n5.getAttribute(groupName, "key", String.class))); assertArrayEquals(new String[]{"0.2", "0.3"}, n5.getAttribute(groupName, "key", String[].class)); assertNotNull(n5.getAttribute(groupName, "key", JsonElement.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", Integer.class)); assertThrows(N5ClassCastException.class, () -> n5.getAttribute(groupName, "key", Double.class)); } } @Test public void testAttributes() { try (final N5Writer n5 = createTempN5Writer()) { assertNull(n5.getAttribute(groupName, "test", String.class)); assertEquals(0, n5.listAttributes(groupName).size()); n5.createGroup(groupName); assertNull(n5.getAttribute(groupName, "test", String.class)); assertEquals(0, n5.listAttributes(groupName).size()); n5.setAttribute(groupName, "key1", "value1"); assertEquals(1, n5.listAttributes(groupName).size()); /* class interface */ assertEquals("value1", n5.getAttribute(groupName, "key1", String.class)); /* type interface */ assertEquals("value1", n5.getAttribute(groupName, "key1", new TypeToken() { }.getType())); final Map newAttributes = new HashMap<>(); newAttributes.put("key2", "value2"); newAttributes.put("key3", "value3"); n5.setAttributes(groupName, newAttributes); assertEquals(3, n5.listAttributes(groupName).size()); /* class interface */ assertEquals("value1", n5.getAttribute(groupName, "key1", String.class)); assertEquals("value2", n5.getAttribute(groupName, "key2", String.class)); assertEquals("value3", n5.getAttribute(groupName, "key3", String.class)); /* type interface */ assertEquals("value1", n5.getAttribute(groupName, "key1", new TypeToken() { }.getType())); assertEquals("value2", n5.getAttribute(groupName, "key2", new TypeToken() { }.getType())); assertEquals("value3", n5.getAttribute(groupName, "key3", new TypeToken() { }.getType())); // test the case where the resulting file becomes shorter n5.setAttribute(groupName, "key1", 1); n5.setAttribute(groupName, "key2", 2); assertEquals(3, n5.listAttributes(groupName).size()); /* class interface */ assertEquals(Integer.valueOf(1), n5.getAttribute(groupName, "key1", Integer.class)); assertEquals(Integer.valueOf(2), n5.getAttribute(groupName, "key2", Integer.class)); assertEquals("value3", n5.getAttribute(groupName, "key3", String.class)); /* type interface */ assertEquals(Integer.valueOf(1), n5.getAttribute(groupName, "key1", new TypeToken() { }.getType())); assertEquals(Integer.valueOf(2), n5.getAttribute(groupName, "key2", new TypeToken() { }.getType())); assertEquals("value3", n5.getAttribute(groupName, "key3", new TypeToken() { }.getType())); n5.setAttribute(groupName, "key1", null); n5.setAttribute(groupName, "key2", null); n5.setAttribute(groupName, "key3", null); assertEquals(0, n5.listAttributes(groupName).size()); } } @Test public void testDatasetAttributes() { final String dset = ""; final String key = "user-attr"; final String value = "value"; try (final N5Writer n5 = createTempN5Writer()) { n5.setAttribute(dset, key, value); n5.setDatasetAttributes(dset, new DatasetAttributes(new long[]{5}, new int[]{5}, DataType.INT32)); assertNotNull(n5.getDatasetAttributes(dset)); assertEquals(value, n5.getAttribute(dset, key, String.class)); } } @Test public void testNullAttributes() throws URISyntaxException, IOException { /* serializeNulls*/ try (N5Writer writer = createTempN5Writer(tempN5Location(), new GsonBuilder().serializeNulls())) { writer.createGroup(groupName); writer.setAttribute(groupName, "nullValue", null); assertNull(writer.getAttribute(groupName, "nullValue", Object.class)); assertEquals(JsonNull.INSTANCE, writer.getAttribute(groupName, "nullValue", JsonElement.class)); final HashMap nulls = new HashMap<>(); nulls.put("anotherNullValue", null); nulls.put("structured/nullValue", null); nulls.put("implicitNulls[3]", null); writer.setAttributes(groupName, nulls); assertNull(writer.getAttribute(groupName, "anotherNullValue", Object.class)); assertEquals(JsonNull.INSTANCE, writer.getAttribute(groupName, "anotherNullValue", JsonElement.class)); assertNull(writer.getAttribute(groupName, "structured/nullValue", Object.class)); assertEquals(JsonNull.INSTANCE, writer.getAttribute(groupName, "structured/nullValue", JsonElement.class)); assertNull(writer.getAttribute(groupName, "implicitNulls[3]", Object.class)); assertEquals(JsonNull.INSTANCE, writer.getAttribute(groupName, "implicitNulls[3]", JsonElement.class)); assertNull(writer.getAttribute(groupName, "implicitNulls[1]", Object.class)); assertEquals(JsonNull.INSTANCE, writer.getAttribute(groupName, "implicitNulls[1]", JsonElement.class)); /* Negative test; a value that truly doesn't exist will still return `null` but will also return `null` when querying as a `JsonElement` */ assertNull(writer.getAttribute(groupName, "implicitNulls[10]", Object.class)); assertNull(writer.getAttribute(groupName, "implicitNulls[10]", JsonElement.class)); assertNull(writer.getAttribute(groupName, "keyDoesn'tExist", Object.class)); assertNull(writer.getAttribute(groupName, "keyDoesn'tExist", JsonElement.class)); /* check existing value gets overwritten */ writer.setAttribute(groupName, "existingValue", 1); assertEquals((Integer)1, writer.getAttribute(groupName, "existingValue", Integer.class)); writer.setAttribute(groupName, "existingValue", null); assertThrows(N5ClassCastException.class, () -> writer.getAttribute(groupName, "existingValue", Integer.class)); assertEquals(JsonNull.INSTANCE, writer.getAttribute(groupName, "existingValue", JsonElement.class)); } /* without serializeNulls*/ try (N5Writer writer = createTempN5Writer(tempN5Location(), new GsonBuilder())) { writer.createGroup(groupName); writer.setAttribute(groupName, "nullValue", null); assertNull(writer.getAttribute(groupName, "nullValue", Object.class)); assertNull(writer.getAttribute(groupName, "nullValue", JsonElement.class)); final HashMap nulls = new HashMap<>(); nulls.put("anotherNullValue", null); nulls.put("structured/nullValue", null); nulls.put("implicitNulls[3]", null); writer.setAttributes(groupName, nulls); assertNull(writer.getAttribute(groupName, "anotherNullValue", Object.class)); assertNull(writer.getAttribute(groupName, "anotherNullValue", JsonElement.class)); assertNull(writer.getAttribute(groupName, "structured/nullValue", Object.class)); assertNull(writer.getAttribute(groupName, "structured/nullValue", JsonElement.class)); /* Arrays are still filled with `null`, regardless of `serializeNulls()`*/ assertNull(writer.getAttribute(groupName, "implicitNulls[3]", Object.class)); assertEquals(JsonNull.INSTANCE, writer.getAttribute(groupName, "implicitNulls[3]", JsonElement.class)); assertNull(writer.getAttribute(groupName, "implicitNulls[1]", Object.class)); assertEquals(JsonNull.INSTANCE, writer.getAttribute(groupName, "implicitNulls[1]", JsonElement.class)); /* Negative test; a value that truly doesn't exist will still return `null` but will also return `null` when querying as a `JsonElement` */ assertNull(writer.getAttribute(groupName, "implicitNulls[10]", Object.class)); assertNull(writer.getAttribute(groupName, "implicitNulls[10]", JsonElement.class)); assertNull(writer.getAttribute(groupName, "keyDoesn'tExist", Object.class)); assertNull(writer.getAttribute(groupName, "keyDoesn'tExist", JsonElement.class)); /* check existing value gets overwritten */ writer.setAttribute(groupName, "existingValue", 1); assertEquals((Integer)1, writer.getAttribute(groupName, "existingValue", Integer.class)); writer.setAttribute(groupName, "existingValue", null); assertNull(writer.getAttribute(groupName, "existingValue", Integer.class)); assertNull(writer.getAttribute(groupName, "existingValue", JsonElement.class)); } } @Test public void testRemoveAttributes() throws IOException, URISyntaxException { try (N5Writer writer = createTempN5Writer(tempN5Location(), new GsonBuilder().serializeNulls())) { writer.setAttribute("", "a/b/c", 100); assertEquals((Integer)100, writer.getAttribute("", "a/b/c", Integer.class)); /* Remove Test without Type */ assertTrue(writer.removeAttribute("", "a/b/c")); assertNull(writer.getAttribute("", "a/b/c", Integer.class)); writer.setAttribute("", "a/b/c", 100); assertEquals((Integer)100, writer.getAttribute("", "a/b/c", Integer.class)); /* Remove Test with correct Type */ assertEquals((Integer)100, writer.removeAttribute("", "a/b/c", Integer.class)); assertNull(writer.getAttribute("", "a/b/c", Integer.class)); writer.setAttribute("", "a/b/c", 100); assertEquals((Integer)100, writer.getAttribute("", "a/b/c", Integer.class)); /* Remove Test with incorrect Type */ assertThrows(N5ClassCastException.class, () -> writer.removeAttribute("", "a/b/c", Boolean.class)); final Integer abcInteger = writer.removeAttribute("", "a/b/c", Integer.class); assertEquals((Integer)100, abcInteger); assertNull(writer.getAttribute("", "a/b/c", Integer.class)); writer.setAttribute("", "a/b/c", 100); assertEquals((Integer)100, writer.getAttribute("", "a/b/c", Integer.class)); /* Remove Test with non-leaf */ assertTrue(writer.removeAttribute("", "a/b")); assertNull(writer.getAttribute("", "a/b/c", Integer.class)); assertNull(writer.getAttribute("", "a/b", JsonObject.class)); writer.setAttribute("", "a\\b\\c/b\\[10]\\c/c", 100); assertEquals((Integer)100, writer.getAttribute("", "a\\b\\c/b\\[10]\\c/c", Integer.class)); /* Remove Test with escape-requiring key */ assertTrue(writer.removeAttribute("", "a\\b\\c/b\\[10]\\c/c")); assertNull(writer.getAttribute("", "a\\b\\c/b\\[10]\\c/c", Integer.class)); writer.setAttribute("", "a/b[9]", 10); assertEquals((Integer)10, writer.getAttribute("", "a/b[9]", Integer.class)); assertEquals((Integer)0, writer.getAttribute("", "a/b[8]", Integer.class)); /*Remove test with arrays */ assertTrue(writer.removeAttribute("", "a/b[5]")); assertEquals(9, writer.getAttribute("", "a/b", JsonArray.class).size()); assertEquals((Integer)0, writer.getAttribute("", "a/b[5]", Integer.class)); assertEquals((Integer)10, writer.getAttribute("", "a/b[8]", Integer.class)); assertTrue(writer.removeAttribute("", "a/b[8]")); assertEquals(8, writer.getAttribute("", "a/b", JsonArray.class).size()); assertNull(writer.getAttribute("", "a/b[8]", Integer.class)); assertTrue(writer.removeAttribute("", "a/b")); assertNull(writer.getAttribute("", "a/b[9]", Integer.class)); assertNull(writer.getAttribute("", "a/b", Integer.class)); /* ensure old remove behavior no longer works (i.e. set to null no longer should remove) */ writer.setAttribute("", "a/b/c", 100); assertEquals((Integer)100, writer.getAttribute("", "a/b/c", Integer.class)); writer.setAttribute("", "a/b/c", null); assertEquals(JsonNull.INSTANCE, writer.getAttribute("", "a/b/c", JsonNull.class)); writer.removeAttribute("", "a/b/c"); assertNull(writer.getAttribute("", "a/b/c", JsonNull.class)); /* remove multiple, all present */ writer.setAttribute("", "a/b/c", 100); writer.setAttribute("", "a/b/d", "test"); writer.setAttribute("", "a/c[9]", 10); writer.setAttribute("", "a/c[5]", 5); assertTrue(writer.removeAttributes("", Arrays.asList("a/b/c", "a/b/d", "a/c[5]"))); assertNull(writer.getAttribute("", "a/b/c", Integer.class)); assertNull(writer.getAttribute("", "a/b/d", String.class)); assertEquals(9, writer.getAttribute("", "a/c", JsonArray.class).size()); assertEquals((Integer)10, writer.getAttribute("", "a/c[8]", Integer.class)); assertEquals((Integer)0, writer.getAttribute("", "a/c[5]", Integer.class)); /* remove multiple, any present */ writer.setAttribute("", "a/b/c", 100); writer.setAttribute("", "a/b/d", "test"); writer.setAttribute("", "a/c[9]", 10); writer.setAttribute("", "a/c[5]", 5); assertTrue(writer.removeAttributes("", Arrays.asList("a/b/c", "a/b/d", "a/x[5]"))); assertNull(writer.getAttribute("", "a/b/c", Integer.class)); assertNull(writer.getAttribute("", "a/b/d", String.class)); assertEquals(10, writer.getAttribute("", "a/c", JsonArray.class).size()); assertEquals((Integer)10, writer.getAttribute("", "a/c[9]", Integer.class)); assertEquals((Integer)5, writer.getAttribute("", "a/c[5]", Integer.class)); /* remove multiple, none present */ writer.setAttribute("", "a/b/c", 100); writer.setAttribute("", "a/b/d", "test"); writer.setAttribute("", "a/c[9]", 10); writer.setAttribute("", "a/c[5]", 5); assertFalse(writer.removeAttributes("", Arrays.asList("X/b/c", "Z/b/d", "a/x[5]"))); assertEquals((Integer)100, writer.getAttribute("", "a/b/c", Integer.class)); assertEquals("test", writer.getAttribute("", "a/b/d", String.class)); assertEquals(10, writer.getAttribute("", "a/c", JsonArray.class).size()); assertEquals((Integer)10, writer.getAttribute("", "a/c[9]", Integer.class)); assertEquals((Integer)5, writer.getAttribute("", "a/c[5]", Integer.class)); /* Test path normalization */ writer.setAttribute("", "a/b/c", 100); assertEquals((Integer)100, writer.getAttribute("", "a/b/c", Integer.class)); assertEquals((Integer)100, writer.removeAttribute("", "a////b/x/../c", Integer.class)); assertNull(writer.getAttribute("", "a/b/c", Integer.class)); writer.createGroup("foo"); writer.setAttribute("foo", "a", 100); writer.removeAttribute("foo", "a"); assertNull(writer.getAttribute("foo", "a", Integer.class)); } } @Test public void testRemoveContainer() throws IOException, URISyntaxException { final String location = tempN5Location(); try (final N5Writer n5 = createTempN5Writer(location)) { try (N5Reader n5Reader = createN5Reader(location)) { assertNotNull(n5Reader); } assertTrue(n5.remove()); assertThrows(Exception.class, () -> createN5Reader(location).close()); } assertThrows(Exception.class, () -> createN5Reader(location).close()); } @Test public void testUri() throws IOException, URISyntaxException { try (final N5Writer writer = createTempN5Writer()) { try (final N5Reader reader = createN5Reader(writer.getURI().toString())) { assertEquals(writer.getURI(), reader.getURI()); } } } @Test public void testRemoveGroup() { try (final N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName, dimensions, blockSize, DataType.UINT64, new RawCompression()); n5.remove(groupName); assertFalse("Group still exists", n5.exists(groupName)); } } @Test public void testList() throws IOException, URISyntaxException { try (final N5Writer listN5 = createTempN5Writer()) { listN5.createGroup(groupName); assertArrayEquals("New Group should return empty array for list()", new String[0], listN5.list(groupName)); for (final String subGroup : subGroupNames) { final String childGroup = groupName + "/" + subGroup; listN5.createGroup(childGroup); assertArrayEquals("New Group should return empty array for list()", new String[0], listN5.list(childGroup)); } final String[] groupsList = listN5.list(groupName); Arrays.sort(groupsList); assertArrayEquals(subGroupNames, groupsList); /* test reading a container this reader didn't create. Ensures cache initialization works as expected. */ try (final N5Reader listN5_2 = createN5Reader(listN5.getURI().toString())) { final String[] groupsList_2 = listN5_2.list(groupName); Arrays.sort(groupsList_2); assertArrayEquals(subGroupNames, groupsList_2); } // test listing the root group ("" and "/" should give identical results) assertArrayEquals(new String[]{"test"}, listN5.list("")); assertArrayEquals(new String[]{"test"}, listN5.list("/")); // calling list on a non-existant group throws an exception assertThrows(N5Exception.class, () -> listN5.list("this-group-does-not-exist")); } } @Test public void testDeepList() throws ExecutionException, InterruptedException { try (final N5Writer n5 = createTempN5Writer()) { n5.createGroup(groupName); for (final String subGroup : subGroupNames) n5.createGroup(groupName + "/" + subGroup); final List groupsList = Arrays.asList(n5.deepList("/")); for (final String subGroup : subGroupNames) assertTrue("deepList contents", groupsList.contains(groupName.replaceFirst("/", "") + "/" + subGroup)); 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); final LongArrayDataBlock dataBlock = new LongArrayDataBlock(blockSize, new long[]{0, 0, 0}, new long[blockNumElements]); n5.createDataset(datasetName, datasetAttributes); n5.writeBlock(datasetName, datasetAttributes, dataBlock); final List datasetList = Arrays.asList(n5.deepList("/")); for (final String subGroup : subGroupNames) assertTrue("deepList contents", datasetList.contains(groupName.replaceFirst("/", "") + "/" + subGroup)); assertTrue("deepList contents", datasetList.contains(datasetName.replaceFirst("/", ""))); assertFalse("deepList stops at datasets", datasetList.contains(datasetName + "/0")); final List datasetList2 = Arrays.asList(n5.deepList("")); for (final String subGroup : subGroupNames) assertTrue("deepList contents", datasetList2.contains(groupName.replaceFirst("/", "") + "/" + subGroup)); assertTrue("deepList contents", datasetList2.contains(datasetName.replaceFirst("/", ""))); assertFalse("deepList stops at datasets", datasetList2.contains(datasetName + "/0")); final String prefix = "/test"; final List datasetList3 = Arrays.asList(n5.deepList(prefix)); for (final String subGroup : subGroupNames) assertTrue("deepList contents", datasetList3.contains("group/" + subGroup)); assertTrue("deepList contents", datasetList3.contains(datasetName.replaceFirst(prefix + "/", ""))); // parallel deepList tests final List datasetListP = Arrays.asList(n5.deepList("/", Executors.newFixedThreadPool(2))); for (final String subGroup : subGroupNames) assertTrue("deepList contents", datasetListP.contains(groupName.replaceFirst("/", "") + "/" + subGroup)); assertTrue("deepList contents", datasetListP.contains(datasetName.replaceFirst("/", ""))); assertFalse("deepList stops at datasets", datasetListP.contains(datasetName + "/0")); final List datasetListP2 = Arrays.asList(n5.deepList("", Executors.newFixedThreadPool(2))); for (final String subGroup : subGroupNames) assertTrue("deepList contents", datasetListP2.contains(groupName.replaceFirst("/", "") + "/" + subGroup)); assertTrue("deepList contents", datasetListP2.contains(datasetName.replaceFirst("/", ""))); assertFalse("deepList stops at datasets", datasetListP2.contains(datasetName + "/0")); final List datasetListP3 = Arrays.asList(n5.deepList(prefix, Executors.newFixedThreadPool(2))); for (final String subGroup : subGroupNames) assertTrue("deepList contents", datasetListP3.contains("group/" + subGroup)); assertTrue("deepList contents", datasetListP3.contains(datasetName.replaceFirst(prefix + "/", ""))); assertFalse("deepList stops at datasets", datasetListP3.contains(datasetName + "/0")); // test filtering final Predicate isCalledDataset = d -> d.endsWith("/dataset"); final Predicate isBorC = d -> d.matches(".*/[bc]$"); final List datasetListFilter1 = Arrays.asList(n5.deepList(prefix, isCalledDataset)); assertTrue( "deepList filter \"dataset\"", datasetListFilter1.stream().map(x -> prefix + x).allMatch(isCalledDataset)); final List datasetListFilter2 = Arrays.asList(n5.deepList(prefix, isBorC)); assertTrue( "deepList filter \"b or c\"", datasetListFilter2.stream().map(x -> prefix + x).allMatch(isBorC)); final List datasetListFilterP1 = Arrays.asList(n5.deepList(prefix, isCalledDataset, Executors.newFixedThreadPool(2))); assertTrue( "deepList filter \"dataset\"", datasetListFilterP1.stream().map(x -> prefix + x).allMatch(isCalledDataset)); final List datasetListFilterP2 = Arrays.asList(n5.deepList(prefix, isBorC, Executors.newFixedThreadPool(2))); assertTrue( "deepList filter \"b or c\"", datasetListFilterP2.stream().map(x -> prefix + x).allMatch(isBorC)); // test dataset filtering final List datasetListFilterD = Arrays.asList(n5.deepListDatasets(prefix)); assertTrue( "deepListDataset", datasetListFilterD.size() == 1 && (prefix + "/" + datasetListFilterD.get(0)).equals(datasetName)); assertArrayEquals( datasetListFilterD.toArray(), n5.deepList(prefix, n5::datasetExists)); final List datasetListFilterDandBC = Arrays.asList(n5.deepListDatasets(prefix, isBorC)); assertEquals("deepListDatasetFilter", 0, datasetListFilterDandBC.size()); assertArrayEquals( datasetListFilterDandBC.toArray(), n5.deepList(prefix, a -> n5.datasetExists(a) && isBorC.test(a))); final List datasetListFilterDP = Arrays.asList(n5.deepListDatasets(prefix, Executors.newFixedThreadPool(2))); assertTrue( "deepListDataset Parallel", datasetListFilterDP.size() == 1 && (prefix + "/" + datasetListFilterDP.get(0)).equals(datasetName)); assertArrayEquals( datasetListFilterDP.toArray(), n5.deepList(prefix, n5::datasetExists, Executors.newFixedThreadPool(2))); final List datasetListFilterDandBCP = Arrays.asList(n5.deepListDatasets(prefix, isBorC, Executors.newFixedThreadPool(2))); assertEquals("deepListDatasetFilter Parallel", 0, datasetListFilterDandBCP.size()); assertArrayEquals( datasetListFilterDandBCP.toArray(), n5.deepList(prefix, a -> n5.datasetExists(a) && isBorC.test(a), Executors.newFixedThreadPool(2))); } } @Test public void testExists() { final String groupName2 = groupName + "-2"; final String datasetName2 = datasetName + "-2"; final String notExists = groupName + "-notexists"; try (N5Writer n5 = createTempN5Writer()) { n5.createDataset(datasetName2, dimensions, blockSize, DataType.UINT64, new RawCompression()); assertTrue(n5.exists(datasetName2)); assertTrue(n5.datasetExists(datasetName2)); n5.createGroup(groupName2); assertTrue(n5.exists(groupName2)); assertFalse(n5.datasetExists(groupName2)); assertFalse(n5.exists(notExists)); assertFalse(n5.datasetExists(notExists)); } } @Test public void testListAttributes() { try (N5Writer n5 = createTempN5Writer()) { final String groupName2 = groupName + "-2"; final String datasetName2 = datasetName + "-2"; n5.createDataset(datasetName2, dimensions, blockSize, DataType.UINT64, new RawCompression()); n5.setAttribute(datasetName2, "attr1", new double[]{1.1, 2.1, 3.1}); n5.setAttribute(datasetName2, "attr2", new String[]{"a", "b", "c"}); n5.setAttribute(datasetName2, "attr3", 1.1); n5.setAttribute(datasetName2, "attr4", "a"); n5.setAttribute(datasetName2, "attr5", new long[]{1, 2, 3}); n5.setAttribute(datasetName2, "attr6", 1); n5.setAttribute(datasetName2, "attr7", new double[]{1, 2, 3.1}); n5.setAttribute(datasetName2, "attr8", new Object[]{"1", 2, 3.1}); Map> attributesMap = n5.listAttributes(datasetName2); assertEquals(attributesMap.get("attr1"), double[].class); assertEquals(attributesMap.get("attr2"), String[].class); assertEquals(attributesMap.get("attr3"), double.class); assertEquals(attributesMap.get("attr4"), String.class); assertEquals(attributesMap.get("attr5"), long[].class); assertEquals(attributesMap.get("attr6"), long.class); assertEquals(attributesMap.get("attr7"), double[].class); assertEquals(attributesMap.get("attr8"), Object[].class); n5.createGroup(groupName2); n5.setAttribute(groupName2, "attr1", new double[]{1.1, 2.1, 3.1}); n5.setAttribute(groupName2, "attr2", new String[]{"a", "b", "c"}); n5.setAttribute(groupName2, "attr3", 1.1); n5.setAttribute(groupName2, "attr4", "a"); n5.setAttribute(groupName2, "attr5", new long[]{1, 2, 3}); n5.setAttribute(groupName2, "attr6", 1); n5.setAttribute(groupName2, "attr7", new double[]{1, 2, 3.1}); n5.setAttribute(groupName2, "attr8", new Object[]{"1", 2, 3.1}); attributesMap = n5.listAttributes(groupName2); assertEquals(attributesMap.get("attr1"), double[].class); assertEquals(attributesMap.get("attr2"), String[].class); assertEquals(attributesMap.get("attr3"), double.class); assertEquals(attributesMap.get("attr4"), String.class); assertEquals(attributesMap.get("attr5"), long[].class); assertEquals(attributesMap.get("attr6"), long.class); assertEquals(attributesMap.get("attr7"), double[].class); assertEquals(attributesMap.get("attr8"), Object[].class); } } @Test public void testVersion() throws NumberFormatException, IOException, URISyntaxException { try (final N5Writer writer = createTempN5Writer()) { final Version n5Version = writer.getVersion(); assertEquals(n5Version, N5Reader.VERSION); final Version incompatibleVersion = new Version(N5Reader.VERSION.getMajor() + 1, N5Reader.VERSION.getMinor(), N5Reader.VERSION.getPatch()); writer.setAttribute("/", N5Reader.VERSION_KEY, incompatibleVersion.toString()); final Version version = writer.getVersion(); assertFalse(N5Reader.VERSION.isCompatible(version)); assertThrows(N5Exception.N5IOException.class, () -> createTempN5Writer(writer.getURI().toString())); final Version compatibleVersion = new Version(N5Reader.VERSION.getMajor(), N5Reader.VERSION.getMinor(), N5Reader.VERSION.getPatch()); writer.setAttribute("/", N5Reader.VERSION_KEY, compatibleVersion.toString()); } } @Test public void testReaderCreation() throws IOException, URISyntaxException { // non-existent location should fail final String location = tempN5Location() + "-" + UUID.randomUUID(); assertThrows("Non-existent location throws error", N5Exception.N5IOException.class, () -> { try (N5Reader test = createN5Reader(location)) { test.list("/"); } }); try (N5Writer writer = createTempN5Writer(location)) { try (N5Reader n5r = createN5Reader(location)) { assertNotNull(n5r); } // existing directory without attributes is okay; // Remove and create to remove attributes store writer.removeAttribute("/", "/"); try (N5Reader na = createN5Reader(location)) { assertNotNull(na); } // existing location with attributes, but no version writer.removeAttribute("/", "/"); writer.setAttribute("/", "mystring", "ms"); try (N5Reader wa = createN5Reader(location)) { assertNotNull(wa); } // existing directory with incompatible version should fail writer.removeAttribute("/", "/"); final String invalidVersion = new Version(N5Reader.VERSION.getMajor() + 1, N5Reader.VERSION.getMinor(), N5Reader.VERSION.getPatch()).toString(); 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.*/ } }); } } @Test public void testDelete() { try (N5Writer n5 = createTempN5Writer()) { final String datasetName = AbstractN5Test.datasetName + "-test-delete"; n5.createDataset(datasetName, dimensions, blockSize, DataType.UINT8, new RawCompression()); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final long[] position1 = {0, 0, 0}; final long[] position2 = {0, 1, 2}; // no blocks should exist to begin with assertNull(n5.readBlock(datasetName, attributes, position1)); final ByteArrayDataBlock dataBlock = new ByteArrayDataBlock(blockSize, position1, byteBlock); n5.writeBlock(datasetName, attributes, dataBlock); // block should exist at position1 but not at position2 final DataBlock readBlock = n5.readChunk(datasetName, attributes, position1); assertNotNull(readBlock); assertTrue(readBlock instanceof ByteArrayDataBlock); assertArrayEquals(byteBlock, ((ByteArrayDataBlock)readBlock).getData()); assertTrue("deleting existing chunk should return true", n5.deleteChunk(datasetName, position1)); assertFalse("deleting non-existing chunk should return false", n5.deleteChunk(datasetName, position1)); assertFalse("deleting non-existing chunk should return false", n5.deleteChunk(datasetName, position2)); // for an unsharded dataset, deleteChunk and deleteBlock behave identically assertFalse("deleting non-existing block should return false", n5.deleteBlock(datasetName, position1)); assertFalse("deleting non-existing block should return false", n5.deleteBlock(datasetName, position2)); n5.writeChunk(datasetName, attributes, dataBlock); assertTrue("deleting existing block should return true", n5.deleteBlock(datasetName, position1)); // no block should exist anymore assertNull(n5.readBlock(datasetName, attributes, position1)); assertNull(n5.readBlock(datasetName, attributes, position2)); } } public static class TestData { public String groupPath; public String attributePath; public T attributeValue; public Class attributeClass; @SuppressWarnings("unchecked") public TestData(final String groupPath, final String key, final T attributeValue) { this.groupPath = groupPath; this.attributePath = key; this.attributeValue = attributeValue; this.attributeClass = (Class)attributeValue.getClass(); } } protected static void testAttributePathEquivalence(final N5Writer writer, final String groupPath, final String[] equivalentPaths ) { if( equivalentPaths.length == 0 ) return; int i = 0; final String first = equivalentPaths[i]; writer.setAttribute(groupPath, first, i); assertEquals(i, writer.getAttribute(groupPath, first, Integer.class).intValue()); writer.getAttribute(groupPath, first, int.class); for (i = 1; i < equivalentPaths.length; i++) { final String path = equivalentPaths[i]; assertEquals(path + " not equivalent to " + first, i-1, (int)writer.getAttribute(groupPath, path, int.class)); writer.setAttribute(groupPath, path, i); assertEquals(path + " set behaved incorrectly", i, (int)writer.getAttribute(groupPath, path, int.class)); assertEquals(path + " not equivalent to " + first, i, (int)writer.getAttribute(groupPath, first, int.class)); } } protected static void addAndTest(final N5Writer writer, final ArrayList> existingTests, final TestData testData) { /* test a new value on existing path */ writer.setAttribute(testData.groupPath, testData.attributePath, testData.attributeValue); assertEquals(testData.attributeValue, writer.getAttribute(testData.groupPath, testData.attributePath, testData.attributeClass)); assertEquals(testData.attributeValue, writer.getAttribute(testData.groupPath, testData.attributePath, TypeToken.get(testData.attributeClass).getType())); /* previous values should still be there, but we remove first if the test we just added overwrites. */ existingTests.removeIf(test -> { try { final String normalizedTestKey = N5URI.from(null, "", test.attributePath).normalizeAttributePath().replaceAll("^/", ""); final String normalizedTestDataKey = N5URI.from(null, "", testData.attributePath).normalizeAttributePath().replaceAll("^/", ""); return normalizedTestKey.equals(normalizedTestDataKey); } catch (final URISyntaxException e) { throw new RuntimeException(e); } }); runTests(writer, existingTests); existingTests.add(testData); } protected static void runTests(final N5Writer writer, final ArrayList> existingTests) { for (final TestData test : existingTests) { assertEquals(test.attributeValue, writer.getAttribute(test.groupPath, test.attributePath, test.attributeClass)); assertEquals(test.attributeValue, writer.getAttribute(test.groupPath, test.attributePath, TypeToken.get(test.attributeClass).getType())); } } @Test public void customObjectTest() { final String testGroup = "test"; final ArrayList> existingTests = new ArrayList<>(); final UriAttributeTest.TestDoubles doubles1 = new UriAttributeTest.TestDoubles( "doubles", "doubles1", new double[]{5.7, 4.5, 3.4}); final UriAttributeTest.TestDoubles doubles2 = new UriAttributeTest.TestDoubles( "doubles", "doubles2", new double[]{5.8, 4.6, 3.5}); final UriAttributeTest.TestDoubles doubles3 = new UriAttributeTest.TestDoubles( "doubles", "doubles3", new double[]{5.9, 4.7, 3.6}); final UriAttributeTest.TestDoubles doubles4 = new UriAttributeTest.TestDoubles( "doubles", "doubles4", new double[]{5.10, 4.8, 3.7}); try (N5Writer n5 = createTempN5Writer()) { n5.createGroup(testGroup); addAndTest(n5, existingTests, new TestData<>(testGroup, "/doubles[1]", doubles1)); addAndTest(n5, existingTests, new TestData<>(testGroup, "/doubles[2]", doubles2)); addAndTest(n5, existingTests, new TestData<>(testGroup, "/doubles[3]", doubles3)); addAndTest(n5, existingTests, new TestData<>(testGroup, "/doubles[4]", doubles4)); /* Test overwrite custom */ addAndTest(n5, existingTests, new TestData<>(testGroup, "/doubles[1]", doubles4)); } } @Test public void testAttributePaths() { try (final N5Writer writer = createTempN5Writer()) { final String testGroup = "test"; writer.createGroup(testGroup); final ArrayList> existingTests = new ArrayList<>(); /* Test a new value by path */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/a/b/c/key1", "value1")); /* test a new value on existing path */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/a/b/key2", "value2")); /* test replacing an existing value */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/a/b/c/key1", "new_value1")); /* Test a new value with arrays */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/array[0]/b/c/key1", "array_value1")); /* test replacing an existing value */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/array[0]/b/c/key1", "new_array_value1")); /* test a new value on existing path with arrays */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/array[0]/d[3]/key2", "array_value2")); /* test a new value on existing path with nested arrays */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/array[1][2]/[3]key2", "array2_value2")); /* test with syntax variants */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/array[1][2]/[3]key2", "array3_value3")); addAndTest(writer, existingTests, new TestData<>(testGroup, "array[1]/[2][3]/key2", "array3_value4")); addAndTest(writer, existingTests, new TestData<>(testGroup, "/array/[1]/[2]/[3]/key2", "array3_value5")); /* test with whitespace*/ addAndTest(writer, existingTests, new TestData<>(testGroup, " ", "space")); addAndTest(writer, existingTests, new TestData<>(testGroup, "\n", "newline")); addAndTest(writer, existingTests, new TestData<>(testGroup, "\t", "tab")); addAndTest(writer, existingTests, new TestData<>(testGroup, "\r\n", "windows_newline")); addAndTest(writer, existingTests, new TestData<>(testGroup, " \n\t \t \n \r\n\r\n", "mixed")); /* test URI encoded characters inside square braces */ addAndTest(writer, existingTests, new TestData<>(testGroup, "[ ]", "space")); addAndTest(writer, existingTests, new TestData<>(testGroup, "[\n]", "newline")); addAndTest(writer, existingTests, new TestData<>(testGroup, "[\t]", "tab")); addAndTest(writer, existingTests, new TestData<>(testGroup, "[\r\n]", "windows_newline")); addAndTest(writer, existingTests, new TestData<>(testGroup, "[ ][\n][\t][ \t \n \r\n][\r\n]", "mixed")); addAndTest(writer, existingTests, new TestData<>(testGroup, "[ ][\\n][\\t][ \\t \\n \\r\\n][\\r\\n]", "mixed")); addAndTest(writer, existingTests, new TestData<>(testGroup, "[\\]", "backslash")); /* Non String tests */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/an/integer/test", 1)); addAndTest(writer, existingTests, new TestData<>(testGroup, "/a/double/test", 1.0)); addAndTest(writer, existingTests, new TestData<>(testGroup, "/a/float/test", 1.0F)); final TestData booleanTest = new TestData<>(testGroup, "/a/boolean/test", true); addAndTest(writer, existingTests, booleanTest); /* overwrite structure*/ existingTests.remove(booleanTest); addAndTest(writer, existingTests, new TestData<>(testGroup, "/a/boolean[2]/test", true)); /* Fill an array with number */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/double_array[5]", 5.0)); addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/double_array[1]", 1.0)); addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/double_array[2]", 2.0)); addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/double_array[4]", 4.0)); addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/double_array[0]", 0.0)); /* We intentionally skipped index 3, it should be `0` */ assertEquals((Integer)0, writer.getAttribute(testGroup, "/filled/double_array[3]", Integer.class)); /* Fill an array with Object */ addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/string_array[5]", "f")); addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/string_array[1]", "b")); addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/string_array[2]", "c")); addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/string_array[4]", "e")); addAndTest(writer, existingTests, new TestData<>(testGroup, "/filled/string_array[0]", "a")); /* path is relative to root */ testAttributePathEquivalence( writer, testGroup, new String[] { "/keyAtRoot", "keyAtRoot", "./keyAtRoot", "././keyAtRoot", "../keyAtRoot", "/../keyAtRoot", "/../../keyAtRoot", "/../bye/../keyAtRoot" }); /* the parent of the root is the root */ /* We intentionally skipped index 3, but it should have been pre-populated with JsonNull */ assertEquals(JsonNull.INSTANCE, writer.getAttribute(testGroup, "/filled/string_array[3]", JsonNull.class)); /* Ensure that escaping does NOT interpret the json path structure, but rather it adds the keys opaquely*/ final HashMap testAttributes = new HashMap<>(); testAttributes.put("\\/z\\/y\\/x", 10); testAttributes.put("q\\/r\\/t", 11); testAttributes.put("\\/l\\/m\\[10]\\/n", 12); testAttributes.put("\\/", 13); /* intentionally the same as above, but this time it should be added as an opaque key*/ testAttributes.put("\\/a\\/b/key2", "value2"); writer.setAttributes(testGroup, testAttributes); assertEquals((Integer)10, writer.getAttribute(testGroup, "\\/z\\/y\\/x", Integer.class)); assertEquals((Integer)11, writer.getAttribute(testGroup, "q\\/r\\/t", Integer.class)); assertEquals((Integer)12, writer.getAttribute(testGroup, "\\/l\\/m\\[10]\\/n", Integer.class)); assertEquals((Integer)13, writer.getAttribute(testGroup, "\\/", Integer.class)); /* We are passing a different type for the same key ("/"). * This means it will try ot grab the exact match first, but then fail, and continuat on * to try and grab the value as a json structure. I should grab the root, and match the empty string case */ assertEquals(writer.getAttribute(testGroup, "", JsonObject.class), writer.getAttribute(testGroup, "/", JsonObject.class)); /* Lastly, ensure grabbing nonsense returns null */ assertNull(writer.getAttribute(testGroup, "/this/key/does/not/exist", Object.class)); } } @Test public void testAttributePathEscaping() { final JsonObject emptyObj = new JsonObject(); final String slashKey = "/"; final String abcdefKey = "abc/def"; final String zeroKey = "[0]"; final String bracketsKey = "]] [] [["; final String doubleBracketsKey = "[[2][33]]"; final String doubleBackslashKey = "\\\\\\\\"; //Evaluates to `\\` through java and json final String dataString = "dataString"; final String rootSlash = jsonKeyVal(slashKey, dataString); final String abcdef = jsonKeyVal(abcdefKey, dataString); final String zero = jsonKeyVal(zeroKey, dataString); final String brackets = jsonKeyVal(bracketsKey, dataString); final String doubleBrackets = jsonKeyVal(doubleBracketsKey, dataString); final String doubleBackslash = jsonKeyVal(doubleBackslashKey, dataString); try (N5Writer n5 = createTempN5Writer()) { // "/" as key String grp = "a"; n5.createGroup(grp); n5.setAttribute(grp, "\\/", dataString); assertEquals(dataString, n5.getAttribute(grp, "\\/", String.class)); String jsonContents = n5.getAttribute(grp, "/", String.class); assertTrue(jsonContents.contains(rootSlash)); // "abc/def" as key grp = "b"; n5.createGroup(grp); n5.setAttribute(grp, "abc\\/def", dataString); assertEquals(dataString, n5.getAttribute(grp, "abc\\/def", String.class)); jsonContents = n5.getAttribute(grp, "/", String.class); assertTrue(jsonContents.contains(abcdef)); // "[0]" as a key grp = "c"; n5.createGroup(grp); n5.setAttribute(grp, "\\[0]", dataString); assertEquals(dataString, n5.getAttribute(grp, "\\[0]", String.class)); jsonContents = n5.getAttribute(grp, "/", String.class); assertTrue(jsonContents.contains(zero)); // "]] [] [[" as a key grp = "d"; n5.createGroup(grp); n5.setAttribute(grp, bracketsKey, dataString); assertEquals(dataString, n5.getAttribute(grp, bracketsKey, String.class)); jsonContents = n5.getAttribute(grp, "/", String.class); assertTrue(jsonContents.contains(brackets)); // "[[2][33]]" grp = "e"; n5.createGroup(grp); n5.setAttribute(grp, "[\\[2]\\[33]]", dataString); assertEquals(dataString, n5.getAttribute(grp, "[\\[2]\\[33]]", String.class)); jsonContents = n5.getAttribute(grp, "/", String.class); assertTrue(jsonContents.contains(doubleBrackets)); // "\\" as key grp = "f"; n5.createGroup(grp); n5.setAttribute(grp, "\\\\", dataString); assertEquals(dataString, n5.getAttribute(grp, "\\\\", String.class)); jsonContents = n5.getAttribute(grp, "/", String.class); assertTrue(jsonContents.contains(doubleBackslash)); // clear n5.setAttribute(grp, "/", emptyObj); jsonContents = n5.getAttribute(grp, "/", String.class); assertFalse(jsonContents.contains(doubleBackslash)); } } /* * For readability above */ private String jsonKeyVal(final String key, final String val) { return String.format("\"%s\":\"%s\"", key, val); } @Test public void testRootLeaves() { /* Test retrieving non-JsonObject root leaves */ try (final N5Writer n5 = createTempN5Writer()) { n5.createGroup(groupName); n5.setAttribute(groupName, "/", "String"); final JsonElement stringPrimitive = n5.getAttribute(groupName, "/", JsonElement.class); assertTrue(stringPrimitive.isJsonPrimitive()); assertEquals("String", stringPrimitive.getAsString()); n5.setAttribute(groupName, "/", 0); final JsonElement intPrimitive = n5.getAttribute(groupName, "/", JsonElement.class); assertTrue(intPrimitive.isJsonPrimitive()); assertEquals(0, intPrimitive.getAsInt()); n5.setAttribute(groupName, "/", true); final JsonElement booleanPrimitive = n5.getAttribute(groupName, "/", JsonElement.class); assertTrue(booleanPrimitive.isJsonPrimitive()); assertTrue(booleanPrimitive.getAsBoolean()); n5.setAttribute(groupName, "/", null); final JsonElement jsonNull = n5.getAttribute(groupName, "/", JsonElement.class); assertTrue(jsonNull.isJsonNull()); assertEquals(JsonNull.INSTANCE, jsonNull); n5.setAttribute(groupName, "[5]", "array"); final JsonElement rootJsonArray = n5.getAttribute(groupName, "/", JsonElement.class); assertTrue(rootJsonArray.isJsonArray()); final JsonArray rootArray = rootJsonArray.getAsJsonArray(); assertEquals("array", rootArray.get(5).getAsString()); assertEquals(JsonNull.INSTANCE, rootArray.get(3)); assertThrows(IndexOutOfBoundsException.class, () -> rootArray.get(10)); } /* Test with new root's each time */ final ArrayList> tests = new ArrayList<>(); tests.add(new TestData<>(groupName, "", "empty_root")); tests.add(new TestData<>(groupName, "/", "replace_empty_root")); tests.add(new TestData<>(groupName, "[0]", "array_root")); for (final TestData testData : tests) { try (final N5Writer writer = createTempN5Writer()) { writer.createGroup(testData.groupPath); writer.setAttribute(testData.groupPath, testData.attributePath, testData.attributeValue); assertEquals(testData.attributeValue, writer.getAttribute(testData.groupPath, testData.attributePath, testData.attributeClass)); assertEquals(testData.attributeValue, writer.getAttribute(testData.groupPath, testData.attributePath, TypeToken.get(testData.attributeClass).getType())); } } /* Test with replacing an existing root-leaf */ tests.clear(); tests.add(new TestData<>(groupName, "", "empty_root")); tests.add(new TestData<>(groupName, "/", "replace_empty_root")); tests.add(new TestData<>(groupName, "[0]", "array_root")); try (final N5Writer writer = createTempN5Writer()) { writer.createGroup(groupName); for (final TestData testData : tests) { writer.setAttribute(testData.groupPath, testData.attributePath, testData.attributeValue); assertEquals(testData.attributeValue, writer.getAttribute(testData.groupPath, testData.attributePath, testData.attributeClass)); assertEquals(testData.attributeValue, writer.getAttribute(testData.groupPath, testData.attributePath, TypeToken.get(testData.attributeClass).getType())); } } /* Test with replacing an existing root non-leaf*/ tests.clear(); final TestData rootAsObject = new TestData<>(groupName, "/some/non/leaf[3]/structure", 100); final TestData rootAsPrimitive = new TestData<>(groupName, "", 200); final TestData rootAsArray = new TestData<>(groupName, "/", 300); tests.add(rootAsPrimitive); tests.add(rootAsArray); try (final N5Writer writer = createTempN5Writer()) { writer.createGroup(groupName); for (final TestData test : tests) { /* Set the root as Object*/ writer.setAttribute(rootAsObject.groupPath, rootAsObject.attributePath, rootAsObject.attributeValue); assertEquals(rootAsObject.attributeValue, writer.getAttribute(rootAsObject.groupPath, rootAsObject.attributePath, rootAsObject.attributeClass)); /* Override the root with something else */ writer.setAttribute(test.groupPath, test.attributePath, test.attributeValue); /* Verify original root is gone */ assertNull(writer.getAttribute(rootAsObject.groupPath, rootAsObject.attributePath, rootAsObject.attributeClass)); /* verify new root exists */ assertEquals(test.attributeValue, writer.getAttribute(test.groupPath, test.attributePath, test.attributeClass)); } } } @Test public void testWriterSeparation() { try (N5Writer writer1 = createTempN5Writer()) { try (N5Writer writer2 = createTempN5Writer()) { assertTrue(writer1.exists("/")); assertTrue(writer2.exists("/")); assertTrue(writer1.remove()); assertTrue(writer2.exists("/")); assertFalse(writer1.exists("/")); assertTrue(writer2.remove()); assertFalse(writer2.exists("/")); } } } protected String[] illegalChars() { return new String[]{" ", "#", "%"}; } @Test public void testPathsWithIllegalUriCharacters() throws IOException, URISyntaxException { try (N5Writer writer = createTempN5Writer()) { try (N5Reader reader = createN5Reader(writer.getURI().toString())) { final String[] illegalChars = illegalChars(); for (final String illegalChar : illegalChars) { final String groupWithIllegalChar = "test" + illegalChar + "group"; assertThrows("list over group should throw prior to create", N5Exception.N5IOException.class, () -> writer.list(groupWithIllegalChar)); writer.createGroup(groupWithIllegalChar); assertTrue("Newly created group should exist", writer.exists(groupWithIllegalChar)); assertArrayEquals("list over empty group should be empty list", new String[0], writer.list(groupWithIllegalChar)); writer.setAttribute(groupWithIllegalChar, "/a/b/key1", "value1"); final String attrFromWriter = writer.getAttribute(groupWithIllegalChar, "/a/b/key1", String.class); final String attrFromReader = reader.getAttribute(groupWithIllegalChar, "/a/b/key1", String.class); assertEquals("value1", attrFromWriter); assertEquals("value1", attrFromReader); final String datasetWithIllegalChar = "test" + illegalChar + "dataset"; final DatasetAttributes datasetAttributes = new DatasetAttributes(dimensions, blockSize, DataType.UINT64, new RawCompression()); writer.createDataset(datasetWithIllegalChar, datasetAttributes); final DatasetAttributes datasetFromWriter = writer.getDatasetAttributes(datasetWithIllegalChar); final DatasetAttributes datasetFromReader = reader.getDatasetAttributes(datasetWithIllegalChar); assertDatasetAttributesEquals(datasetAttributes, datasetFromWriter); assertDatasetAttributesEquals(datasetAttributes, datasetFromReader); } } } } public static void assertBlockEquals(final DataBlock expected, final DataBlock actual) { assertEquals("Datablocks are different type", expected.getClass(), actual.getClass()); Assert.assertArrayEquals("read block position should be same as block position when unsharded", expected.getGridPosition(), actual.getGridPosition()); Assert.assertArrayEquals("read block size should equal block size when unsharded", expected.getSize(), actual.getSize()); final Object expectedData = expected.getData(); final Object actualData = actual.getData(); final String dataEqualsMsg = "block written through shard should be identical"; if (expectedData instanceof byte[]) assertArrayEquals(dataEqualsMsg, (byte[])expectedData, (byte[])expectedData); else if (expectedData instanceof short[]) assertArrayEquals(dataEqualsMsg, (short[])expectedData, (short[])actualData); else if (expectedData instanceof int[]) assertArrayEquals(dataEqualsMsg, (int[])expectedData, (int[])actualData); else if (expectedData instanceof long[]) assertArrayEquals(dataEqualsMsg, (long[])expectedData, (long[])actualData); else if (expectedData instanceof float[]) assertArrayEquals(dataEqualsMsg, (float[])expectedData, (float[])actualData, 0f); else if (expectedData instanceof double[]) assertArrayEquals(dataEqualsMsg, (double[])expectedData, (double[])actualData, 0d); else if (expectedData instanceof String[]) assertArrayEquals(dataEqualsMsg, (String[])expectedData, (String[])actualData); else fail("Unsupported data type for block data: " + expectedData.getClass()); } protected void assertDatasetAttributesEquals(final DatasetAttributes expected, final DatasetAttributes actual) { assertArrayEquals(expected.getDimensions(), actual.getDimensions()); assertArrayEquals(expected.getBlockSize(), actual.getBlockSize()); assertEquals(expected.getDataType(), actual.getDataType()); // TODO would be nice to check this somehow maybe make a DatasetAttributes.equals method? // assertArrayEquals(expected.getDataCodecInfos(), actual.getDataCodecInfos()); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/DatasetAttributesTest.java ================================================ package org.janelia.saalfeldlab.n5; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.N5BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.RawBlockCodecInfo; import org.janelia.saalfeldlab.n5.shard.DefaultShardCodecInfo; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedGrid; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; import org.junit.Test; /** * Unit tests for DatasetAttributes class. */ public class DatasetAttributesTest { /** * Test that validateBlockShardSizes method accepts valid shard and chunk size combinations. */ @Test public void testValidateBlockShardSizesValid() { // Test case 1: shard size equals block size long[] dimensions = new long[]{100, 200, 300}; int[] shardSize = new int[]{64, 64, 64}; int[] chunkSize = new int[]{64, 64, 64}; DataType dataType = DataType.UINT8; // This should not throw any exception DatasetAttributes attrs = shardDatasetAttributes(dimensions, shardSize, chunkSize, dataType); assertEquals(chunkSize, attrs.getChunkSize()); assertEquals(shardSize, attrs.getBlockSize()); NestedGrid grid = attrs.getNestedBlockGrid(); assertEquals(chunkSize, grid.getBlockSize(0)); assertEquals(shardSize, grid.getBlockSize(1)); // Test case 2: shard size is a multiple of block size shardSize = new int[]{128}; chunkSize = new int[]{64}; attrs = shardDatasetAttributes(new long[]{128}, shardSize, chunkSize, dataType); assertEquals(chunkSize, attrs.getChunkSize()); assertEquals(shardSize, attrs.getBlockSize()); grid = attrs.getNestedBlockGrid(); assertEquals(chunkSize, grid.getBlockSize(0)); assertEquals(shardSize, grid.getBlockSize(1)); // Test case 3: different multiples per dimension shardSize = new int[]{128, 256, 32, 2}; chunkSize = new int[]{32, 64, 32, 1}; attrs = shardDatasetAttributes(new long[]{128, 128, 128, 128}, shardSize, chunkSize, dataType ); assertEquals(chunkSize, attrs.getChunkSize()); assertEquals(shardSize, attrs.getBlockSize()); grid = attrs.getNestedBlockGrid(); assertEquals(chunkSize, grid.getBlockSize(0)); assertEquals(shardSize, grid.getBlockSize(1)); // Test case 4: large multiples shardSize = new int[]{1024, 2048, 512}; chunkSize = new int[]{32, 64, 16}; attrs = shardDatasetAttributes(dimensions, shardSize, chunkSize, dataType); assertEquals(chunkSize, attrs.getChunkSize()); assertEquals(shardSize, attrs.getBlockSize()); grid = attrs.getNestedBlockGrid(); assertEquals(chunkSize, grid.getBlockSize(0)); assertEquals(shardSize, grid.getBlockSize(1)); } private static DatasetAttributes shardDatasetAttributes( long[] dimensions, int[] shardSize, int[] chunkSize, DataType dataType) { DefaultShardCodecInfo blockCodecInfo = new DefaultShardCodecInfo( chunkSize, new N5BlockCodecInfo(), new DataCodecInfo[]{new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[]{new RawCompression()}, IndexLocation.END); return new DatasetAttributes(dimensions, shardSize, dataType, blockCodecInfo); } @Test public void builderTests() { final long[] dims = new long[]{100, 200, 300}; final int[] blk = new int[]{32, 32, 32}; // default blockSize uses defaultBlockSize, not full dimensions final DatasetAttributes defaultBlk = DatasetAttributes.builder(dims, DataType.FLOAT32).build(); assertArrayEquals(DatasetAttributes.defaultBlockSize(dims), defaultBlk.getBlockSize()); // blockSize is reflected in output final DatasetAttributes withBlk = DatasetAttributes.builder(dims, DataType.FLOAT32) .blockSize(blk).build(); assertArrayEquals(blk, withBlk.getBlockSize()); assertFalse(withBlk.isSharded()); // compression sets a data codec; RawCompression is a no-op final DatasetAttributes withGzip = DatasetAttributes.builder(dims, DataType.FLOAT32) .blockSize(blk).compression(new GzipCompression()).build(); assertEquals(1, withGzip.getDataCodecInfos().length); final DatasetAttributes withRaw = DatasetAttributes.builder(dims, DataType.FLOAT32) .blockSize(blk).compression(new RawCompression()).build(); assertEquals(0, withRaw.getDataCodecInfos().length); // round-trip through Builder(DatasetAttributes) final DatasetAttributes roundTrip = DatasetAttributes.builder(withGzip).build(); assertArrayEquals(withGzip.getDimensions(), roundTrip.getDimensions()); assertArrayEquals(withGzip.getBlockSize(), roundTrip.getBlockSize()); assertEquals(withGzip.getDataType(), roundTrip.getDataType()); assertEquals(withGzip.getDataCodecInfos().length, roundTrip.getDataCodecInfos().length); } /** * Test that validateBlockShardSizes method rejects invalid shard and block size combinations. */ @Test public void testValidateBlockShardSizesInvalid() { final long[] dimensions = new long[]{100, 200, 300}; final DataType dataType = DataType.UINT8; // Block size too smallcompression != null && !(compression instanceof RawCompression) IllegalArgumentException ex0 = assertThrows( IllegalArgumentException.class, () -> shardDatasetAttributes(dimensions, new int[]{1, 1, 1}, new int[]{1, 0, -1}, dataType)); assertTrue(ex0.getMessage().contains("negative")); // Different number of dimensions IllegalArgumentException ex1 = assertThrows( IllegalArgumentException.class, () -> shardDatasetAttributes(dimensions, new int[]{64, 64}, new int[]{32, 32, 32}, dataType)); assertTrue(ex1.getMessage().contains("different length")); // Shard size smaller than block size IllegalArgumentException ex2 = assertThrows( IllegalArgumentException.class, () -> shardDatasetAttributes(dimensions, new int[]{32, 64, 64}, new int[]{64, 64, 64}, dataType)); assertTrue(ex2.getMessage().contains("is smaller than previous")); // Shard size not a multiple of block size IllegalArgumentException ex3 = assertThrows( IllegalArgumentException.class, () -> shardDatasetAttributes(dimensions, new int[]{100, 100, 100}, new int[]{64, 64, 64}, dataType)); assertTrue(ex3.getMessage().contains("not a multiple of previous level")); // Multiple violations - shard smaller than block in one dimension IllegalArgumentException ex4 = assertThrows( IllegalArgumentException.class, () -> shardDatasetAttributes(dimensions, new int[]{128, 32, 128}, new int[]{64, 64, 64}, dataType)); assertTrue(ex4.getMessage().contains("is smaller than previous")); // Edge case - shard size of 0 assertThrows( IllegalArgumentException.class, () -> shardDatasetAttributes(dimensions, new int[]{0, 64, 64}, new int[]{64, 64, 64}, dataType)); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/FileKeyLockManagerTest.java ================================================ package org.janelia.saalfeldlab.n5; import java.io.Closeable; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import org.junit.After; import org.junit.Before; import org.junit.Test; import static org.janelia.saalfeldlab.n5.FileKeyLockManager.FILE_LOCK_MANAGER; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; public class FileKeyLockManagerTest { private Path tempDir; @Before public void setUp() throws IOException { tempDir = Files.createTempDirectory("fklm-test"); } @After public void tearDown() throws IOException { if (tempDir != null) { Files.walk(tempDir) .sorted((a, b) -> -a.compareTo(b)) .forEach(p -> { try { Files.deleteIfExists(p); } catch (IOException e) { // ignore } }); } } @Test public void testConcurrentReads() throws Exception { // create a test file final Path testFile = tempDir.resolve("test.txt"); final byte[] testContent = "test content for concurrent reads".getBytes(); Files.write(testFile, testContent); final int numReaders = 5; final ExecutorService executor = Executors.newFixedThreadPool(numReaders); final CountDownLatch startLatch = new CountDownLatch(1); final CountDownLatch readersReady = new CountDownLatch(numReaders); final CountDownLatch readersFinished = new CountDownLatch(numReaders); final AtomicInteger concurrentReaders = new AtomicInteger(0); final AtomicInteger maxConcurrentReaders = new AtomicInteger(0); final AtomicInteger successfulReads = new AtomicInteger(0); for (int i = 0; i < numReaders; i++) { executor.submit(() -> { try { readersReady.countDown(); startLatch.await(); try (final LockedFileChannel lock = FILE_LOCK_MANAGER.lockForReading(testFile)) { final int concurrent = concurrentReaders.incrementAndGet(); maxConcurrentReaders.updateAndGet(max -> Math.max(max, concurrent)); // actually read from the channel final byte[] buf = new byte[testContent.length]; final int bytesRead = lock.read(ByteBuffer.wrap(buf), 0); if (bytesRead > 0 && Arrays.equals(buf, testContent)) { successfulReads.incrementAndGet(); } Thread.sleep(100); concurrentReaders.decrementAndGet(); } } catch (Exception e) { e.printStackTrace(); } finally { readersFinished.countDown(); } }); } assertTrue("All readers should be ready", readersReady.await(5, TimeUnit.SECONDS)); final long startTime = System.currentTimeMillis(); startLatch.countDown(); assertTrue("All readers should finish", readersFinished.await(10, TimeUnit.SECONDS)); final long duration = System.currentTimeMillis() - startTime; executor.shutdown(); assertTrue("Executor should terminate", executor.awaitTermination(5, TimeUnit.SECONDS)); System.out.println("Test completed in " + duration + "ms"); System.out.println("Maximum concurrent readers: " + maxConcurrentReaders.get()); System.out.println("Successful reads: " + successfulReads.get()); assertEquals("All readers should have read successfully", numReaders, successfulReads.get()); assertTrue("Multiple readers should have been reading concurrently", maxConcurrentReaders.get() > 1); assertTrue("Concurrent execution should be faster than sequential", duration < numReaders * 100); } @Test public void testReadWriteExclusion() throws Exception { // create a test file final Path testFile = tempDir.resolve("test2.txt"); Files.write(testFile, "initial content".getBytes()); final String writtenContent = "written by writer"; // acquire a write lock and write to the file final OutputStream writeLock = FILE_LOCK_MANAGER.lockForWriting(testFile).asOutputStream(); writeLock.write(writtenContent.getBytes()); // try to acquire a read lock from another thread - should block final CountDownLatch readAttempted = new CountDownLatch(1); final CountDownLatch readAcquired = new CountDownLatch(1); final AtomicReference readContent = new AtomicReference<>(); new Thread(() -> { readAttempted.countDown(); try (final LockedFileChannel readLock = FILE_LOCK_MANAGER.lockForReading(testFile)) { // actually read from the channel final byte[] buf = new byte[writtenContent.getBytes().length]; final int charsRead = readLock.read(ByteBuffer.wrap(buf), 0); if (charsRead > 0) { readContent.set(new String(buf, 0, charsRead)); } readAcquired.countDown(); } catch (IOException e) { e.printStackTrace(); } }).start(); assertTrue(readAttempted.await(1, TimeUnit.SECONDS)); assertFalse("Read lock should not be acquired while write lock is held", readAcquired.await(200, TimeUnit.MILLISECONDS)); writeLock.close(); assertTrue("Read lock should be acquired after write lock is released", readAcquired.await(2, TimeUnit.SECONDS)); assertEquals("Reader should see written content", writtenContent, readContent.get()); } private class CleanUpHelper implements Closeable { private final Path path; private final LockedFileChannel channel; CleanUpHelper() throws IOException, InterruptedException { path = tempDir.resolve("trigger.txt"); Files.write(path, "trigger".getBytes()); channel = FILE_LOCK_MANAGER.lockForReading(path); tryWaitForSize(-1, 10); } private void tryWaitForSize(final int expectedSize) throws IOException, InterruptedException { tryWaitForSize(expectedSize, 100); } private void tryWaitForSize(final int expectedSize, final int numIterations) throws IOException, InterruptedException { // Wait a bit, trigger GC, loop a few times to try to trigger stale WeakReferences to be processed for (int i = 0; i < numIterations; ++i) { Thread.sleep(10); System.gc(); // FileKeyLockManager.cleanUp() is called on the side during // normal usage. We keep locking and unlocking "trigger.txt" for // which we already hold a read lock. This will keep the // FileKeyLockManager working, but will not lead to // removal/insertion of KeyLockState for "trigger.txt". try (LockedFileChannel temp = FILE_LOCK_MANAGER.lockForReading(path)) { } if (FILE_LOCK_MANAGER.size() == expectedSize) { break; } } } @Override public void close() throws IOException { channel.close(); } } @Test // TODO: Remove? This test relies on garbage collection behaviour and is inherently fragile. public void testLockCleanup() throws Exception { // create test files final Path testFile1 = tempDir.resolve("key1.txt"); final Path testFile2 = tempDir.resolve("key2.txt"); final Path testFile3 = tempDir.resolve("key3.txt"); final byte[] content = "content".getBytes(); Files.write(testFile1, content); Files.write(testFile2, content); Files.write(testFile3, content); final CleanUpHelper cleanup = new CleanUpHelper(); final int initialSize = FILE_LOCK_MANAGER.size(); final LockedFileChannel lock1 = FILE_LOCK_MANAGER.lockForReading(testFile1); final OutputStream lock2 = FILE_LOCK_MANAGER.lockForWriting(testFile2).asOutputStream(); final LockedFileChannel lock3 = FILE_LOCK_MANAGER.lockForReading(testFile3); // actually perform I/O on each lock final byte[] buf = new byte[content.length]; lock1.read(ByteBuffer.wrap(buf),0); lock2.write(content); lock3.read(ByteBuffer.wrap(buf),0); assertEquals("Should have 3 new keys", initialSize + 3, FILE_LOCK_MANAGER.size()); // close lock1 - entry should be auto-removed lock1.close(); cleanup.tryWaitForSize(initialSize + 2); assertEquals("key1 should be auto-removed on close", initialSize + 2, FILE_LOCK_MANAGER.size()); // close remaining locks - entries should be auto-removed lock2.close(); lock3.close(); cleanup.tryWaitForSize(initialSize); assertEquals("All entries should be auto-removed on close", initialSize, FILE_LOCK_MANAGER.size()); } @Test public void testWriteLockCreatesFile() throws Exception { // file does not exist - write lock creates it via CREATE option final Path testFile = tempDir.resolve("newfile.txt"); final String content = "written to new file"; assertFalse("File should not exist initially", Files.exists(testFile)); try (final LockedFileChannel writeLock = FILE_LOCK_MANAGER.lockForWriting(testFile)) { assertTrue("File should be created by write lock", Files.exists(testFile)); // actually write to the file writeLock.asOutputStream().write(content.getBytes()); } // verify written content assertEquals("Content should be written", content, new String(Files.readAllBytes(testFile))); } @Test public void testWriteLockCreatesParentDirectories() throws Exception { // parent directories do not exist - write lock creates them final Path testFile = tempDir.resolve("a/b/c/newfile.txt"); final String content = "written to nested file"; assertFalse("File should not exist initially", Files.exists(testFile)); assertFalse("Parent should not exist initially", Files.exists(testFile.getParent())); try (final LockedFileChannel writeLock = FILE_LOCK_MANAGER.lockForWriting(testFile)) { assertTrue("File should be created by write lock", Files.exists(testFile)); assertTrue("Parent directories should be created", Files.exists(testFile.getParent())); // actually write to the file writeLock.asOutputStream().write(content.getBytes()); } // verify written content assertEquals("Content should be written", content, new String(Files.readAllBytes(testFile))); } @Test public void testReadLockRequiresExistingFile() throws Exception { final Path testFile = tempDir.resolve("nonexistent.txt"); assertThrows("Should not acquire read lock for non-existent file", NoSuchFileException.class, () -> FILE_LOCK_MANAGER.lockForReading(testFile)); } @Test // TODO: Remove? This test relies on garbage collection behaviour and is inherently fragile. public void testLocksMapEmptyAfterProperClose() throws Exception { final Path testFile = tempDir.resolve("proper-close.txt"); Files.write(testFile, "content".getBytes()); final CleanUpHelper cleanup = new CleanUpHelper(); final int initialSize = FILE_LOCK_MANAGER.size(); try (final LockedFileChannel lock = FILE_LOCK_MANAGER.lockForWriting(testFile)) { lock.asOutputStream().write("new content".getBytes()); } cleanup.tryWaitForSize(initialSize); assertEquals("locks map should be back to initial size after proper close", initialSize, FILE_LOCK_MANAGER.size()); } @Test // TODO: Remove? This test relies on garbage collection behaviour and is inherently fragile. public void testLocksMapEmptyAfterLeakedChannelIsGCd() throws Exception { final Path testFile = tempDir.resolve("leaked-channel.txt"); Files.write(testFile, "content".getBytes()); final CleanUpHelper cleanup = new CleanUpHelper(); final int initialSize = FILE_LOCK_MANAGER.size(); // acquire lock in a separate scope so reference can be GC'd acquireAndLeakLock(testFile); cleanup.tryWaitForSize(initialSize); assertEquals("locks map should be back to initial size after leaked channel is GC'd", initialSize, FILE_LOCK_MANAGER.size()); } private void acquireAndLeakLock(final Path path) throws IOException { // acquire lock but don't close it - just let the reference go out of scope @SuppressWarnings("resource") LockedFileChannel leaked = FILE_LOCK_MANAGER.lockForWriting(path); leaked.asOutputStream().write("leaked content".getBytes()); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/FsLockTest.java ================================================ package org.janelia.saalfeldlab.n5; import org.junit.Test; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.*; import java.util.concurrent.atomic.AtomicBoolean; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; public class FsLockTest { private static final FileKeyLockManager LOCK_MANAGER = FileKeyLockManager.FILE_LOCK_MANAGER; private static String tempPathName() { try { final File tmpFile = Files.createTempDirectory("fs-key-lock-test-").toFile(); tmpFile.delete(); tmpFile.mkdir(); tmpFile.deleteOnExit(); return tmpFile.getCanonicalPath(); } catch (final Exception e) { throw new RuntimeException(e); } } @Test public void testReadLock() throws IOException { final Path path = Paths.get(tempPathName(), "lock"); path.toFile().createNewFile(); assertTrue("File Created", path.toFile().exists()); LockedFileChannel lock = LOCK_MANAGER.lockForReading(path); lock.close(); lock = LOCK_MANAGER.lockForReading(path); final ExecutorService exec = Executors.newSingleThreadExecutor(); final Future future = exec.submit(() -> { LOCK_MANAGER.lockForWriting(path).close(); return null; }); try { System.out.println("Trying to acquire locked readable channel..."); System.out.println(future.get(3, TimeUnit.SECONDS)); fail("Lock broken!"); } catch (final TimeoutException e) { System.out.println("Lock held!"); future.cancel(true); } catch (final InterruptedException | ExecutionException e) { future.cancel(true); System.out.println("Test was interrupted!"); } finally { lock.close(); Files.delete(path); } exec.shutdownNow(); } @Test public void testWriteLock() throws IOException { final Path path = Paths.get(tempPathName(), "lock"); final LockedFileChannel lock = LOCK_MANAGER.lockForWriting(path); System.out.println("locked"); final ExecutorService exec = Executors.newSingleThreadExecutor(); final Future future = exec.submit(() -> { LOCK_MANAGER.lockForReading(path).close(); return null; }); try { System.out.println("Trying to acquire locked writable channel..."); System.out.println(future.get(3, TimeUnit.SECONDS)); fail("Lock broken!"); } catch (final TimeoutException e) { System.out.println("Lock held!"); future.cancel(true); } catch (final InterruptedException | ExecutionException e) { future.cancel(true); System.out.println("Test was interrupted!"); } finally { lock.close(); Files.delete(path); } exec.shutdownNow(); } @Test public void testFSLockRelease() throws IOException, ExecutionException, InterruptedException, TimeoutException { final Path path = Paths.get(tempPathName(), "lock"); final ExecutorService exec = Executors.newFixedThreadPool(2); // first thread acquires the lock, waits for 200ms then should release it exec.submit(() -> { try { try(final LockedFileChannel lock = LOCK_MANAGER.lockForWriting(path)) { lock.size(); Thread.sleep(200); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }); // second thread waits for the lock. // it should get it within a few seconds. final Future future = exec.submit(() -> { LOCK_MANAGER.lockForWriting(path).close(); return null; }); future.get(3, TimeUnit.SECONDS); Files.delete(path); exec.shutdownNow(); } @Test public void testReadLockBehavior() throws IOException, InterruptedException, ExecutionException, TimeoutException { final Path path = Paths.get(tempPathName(), "read-lock"); path.toFile().createNewFile(); final ExecutorService exec = Executors.newFixedThreadPool(3); final AtomicBoolean v = new AtomicBoolean(false); // first thread acquires a read lock, waits for 200ms Future f = exec.submit(() -> { try { try(final LockedFileChannel lock = LOCK_MANAGER.lockForReading(path)) { lock.size(); Thread.sleep(200); // ensure that the other thread updated the value assertTrue(v.get()); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }); // second thread gets a read lock // and should not be blocked // this thread updates the boolean exec.submit(() -> { try( final LockedFileChannel lock = LOCK_MANAGER.lockForReading(path)) { lock.size(); v.set(true); } return null; }); f.get(3, TimeUnit.SECONDS); exec.shutdownNow(); Files.delete(path); } @Test public void testWriteLockBehavior() throws IOException, ExecutionException, InterruptedException, TimeoutException { final Path path = Paths.get(tempPathName(), "lock"); final ExecutorService exec = Executors.newFixedThreadPool(2); // first thread acquires the lock, waits for 200ms then should release it exec.submit(() -> { try { try(final LockedFileChannel lock = LOCK_MANAGER.lockForWriting(path)) { lock.size(); Thread.sleep(200); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } }); // second thread waits for the lock. // it should get it within a few seconds. final Future future = exec.submit(() -> { LOCK_MANAGER.lockForWriting(path).close(); return null; }); future.get(3, TimeUnit.SECONDS); Files.delete(path); exec.shutdownNow(); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/N5Benchmark.java ================================================ package org.janelia.saalfeldlab.n5; import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; import ch.systemsx.cisd.base.mdarray.MDShortArray; import ch.systemsx.cisd.hdf5.HDF5Factory; import ch.systemsx.cisd.hdf5.HDF5IntStorageFeatures; import ch.systemsx.cisd.hdf5.IHDF5ShortWriter; import ch.systemsx.cisd.hdf5.IHDF5Writer; import ij.IJ; import ij.ImagePlus; import ij.io.Opener; import ij.process.ShortProcessor; import net.imglib2.Cursor; import net.imglib2.img.imageplus.ImagePlusImg; import net.imglib2.img.imageplus.ImagePlusImgs; import net.imglib2.type.numeric.integer.UnsignedShortType; import net.imglib2.view.Views; /** * * * @author Stephan Saalfeld <saalfelds@janelia.hhmi.org> */ public class N5Benchmark { private static String testDirPath; static { try { testDirPath = Files.createTempDirectory("n5-benchmark-").toFile().getCanonicalPath(); } catch (final IOException e) { throw new RuntimeException(e); } } private static String datasetName = "/dataset"; private static N5Writer n5; private static short[] data; private static final Compression[] compressions = { new RawCompression(), new Bzip2Compression(), new GzipCompression(), new Lz4Compression(), new XzCompression() }; /** * @throws java.lang.Exception */ @BeforeClass public static void setUpBeforeClass() throws Exception { final File testDir = new File(testDirPath); testDir.mkdirs(); if (!(testDir.exists() && testDir.isDirectory())) throw new IOException("Could not create benchmark directory for HDF5Utils benchmark."); data = new short[64 * 64 * 64]; final ImagePlus imp = new Opener().openURL("https://imagej.net/ij/images/t1-head-raw.zip"); final ImagePlusImg img = (ImagePlusImg)(Object)ImagePlusImgs.from(imp); final Cursor cursor = Views.flatIterable(Views.interval(img, new long[]{100, 100, 30}, new long[]{163, 163, 93})).cursor(); for (int i = 0; i < data.length; ++i) data[i] = (short)cursor.next().get(); n5 = new N5FSWriter(testDirPath); } /** * @throws java.lang.Exception */ @AfterClass public static void rampDownAfterClass() throws Exception { n5.remove(""); } /** * @throws java.lang.Exception */ @Before public void setUp() throws Exception {} /** * Generates some files for documentation of the binary format. Not a test. */ // @Test public void testDocExample() { final short[] dataBlockData = new short[]{1, 2, 3, 4, 5, 6}; for (final Compression compression : compressions) { try { final String compressedDatasetName = datasetName + "." + compression.getType(); n5.createDataset(compressedDatasetName, new long[]{1, 2, 3}, new int[]{1, 2, 3}, DataType.UINT16, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(compressedDatasetName); final ShortArrayDataBlock dataBlock = new ShortArrayDataBlock(new int[]{1, 2, 3}, new long[]{0, 0, 0}, dataBlockData); n5.writeChunk(compressedDatasetName, attributes, dataBlock); } catch (final N5Exception e) { fail(e.getMessage()); } } } // @Test public void benchmarkWritingSpeed() { final int nBlocks = 5; for (int i = 0; i < 1; ++i) { for (final Compression compression : compressions) { final long t = System.currentTimeMillis(); try { final String compressedDatasetName = datasetName + "." + compression.getType(); n5.createDataset(compressedDatasetName, new long[]{64 * nBlocks, 64 * nBlocks, 64 * nBlocks}, new int[]{64, 64, 64}, DataType.UINT16, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(compressedDatasetName); for (int z = 0; z < nBlocks; ++z) for (int y = 0; y < nBlocks; ++y) for (int x = 0; x < nBlocks; ++x) { final ShortArrayDataBlock dataBlock = new ShortArrayDataBlock(new int[]{64, 64, 64}, new long[]{x, y, z}, data); n5.writeChunk(compressedDatasetName, attributes, dataBlock); } } catch (final N5Exception e) { fail(e.getMessage()); } System.out.println(String.format("%d : %s : %fs", i, compression.getType(), 0.001 * (System.currentTimeMillis() - t))); } /* TIF blocks */ long t = System.currentTimeMillis(); final String compressedDatasetName = testDirPath + "/" + datasetName + ".tif"; new File(compressedDatasetName).mkdirs(); for (int z = 0; z < nBlocks; ++z) for (int y = 0; y < nBlocks; ++y) for (int x = 0; x < nBlocks; ++x) { final ImagePlus impBlock = new ImagePlus("", new ShortProcessor(64, 64 * 64, data, null)); IJ.saveAsTiff(impBlock, compressedDatasetName + "/" + x + "-" + y + "-" + z + ".tif"); } System.out.println(String.format("%d : tif : %fs", i, 0.001 * (System.currentTimeMillis() - t))); /* HDF5 raw */ t = System.currentTimeMillis(); String hdf5Name = testDirPath + "/" + datasetName + ".h5"; IHDF5Writer hdf5Writer = HDF5Factory.open(hdf5Name); IHDF5ShortWriter uint16Writer = hdf5Writer.uint16(); uint16Writer.createMDArray( datasetName, new long[]{64 * nBlocks, 64 * nBlocks, 64 * nBlocks}, new int[]{64, 64, 64}, HDF5IntStorageFeatures.INT_NO_COMPRESSION); for (int z = 0; z < nBlocks; ++z) for (int y = 0; y < nBlocks; ++y) for (int x = 0; x < nBlocks; ++x) { final MDShortArray targetCell = new MDShortArray(data, new int[]{64, 64, 64}); uint16Writer.writeMDArrayBlockWithOffset(datasetName, targetCell, new long[]{64 * z, 64 * y, 64 * x}); } System.out.println(String.format("%d : hdf5 raw : %fs", i, 0.001 * (System.currentTimeMillis() - t))); new File(hdf5Name).delete(); /* HDF5 gzip */ t = System.currentTimeMillis(); hdf5Name = testDirPath + "/" + datasetName + ".gz.h5"; hdf5Writer = HDF5Factory.open(hdf5Name); uint16Writer = hdf5Writer.uint16(); uint16Writer.createMDArray( datasetName, new long[]{64 * nBlocks, 64 * nBlocks, 64 * nBlocks}, new int[]{64, 64, 64}, HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE); for (int z = 0; z < nBlocks; ++z) for (int y = 0; y < nBlocks; ++y) for (int x = 0; x < nBlocks; ++x) { final MDShortArray targetCell = new MDShortArray(data, new int[]{64, 64, 64}); uint16Writer.writeMDArrayBlockWithOffset(datasetName, targetCell, new long[]{64 * z, 64 * y, 64 * x}); } System.out.println(String.format("%d : hdf5 gzip : %fs", i, 0.001 * (System.currentTimeMillis() - t))); new File(hdf5Name).delete(); } } @Test @Ignore public void benchmarkParallelWritingSpeed() { final int nBlocks = 5; for (int i = 1; i <= 16; i *= 2 ) { System.out.println( i + " threads."); final ExecutorService exec = Executors.newFixedThreadPool(i); final ArrayList> futures = new ArrayList<>(); long t; for (final Compression compression : compressions) { t = System.currentTimeMillis(); try { final String compressedDatasetName = datasetName + "." + compression.getType(); n5.createDataset(compressedDatasetName, new long[]{64 * nBlocks, 64 * nBlocks, 64 * nBlocks}, new int[]{64, 64, 64}, DataType.UINT16, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(compressedDatasetName); for (int z = 0; z < nBlocks; ++z) { final int fz = z; for (int y = 0; y < nBlocks; ++y) { final int fy = y; for (int x = 0; x < nBlocks; ++x) { final int fx = x; futures.add( exec.submit( () -> { final ShortArrayDataBlock dataBlock = new ShortArrayDataBlock(new int[]{64, 64, 64}, new long[]{fx, fy, fz}, data); n5.writeChunk(compressedDatasetName, attributes, dataBlock); return true; })); } } } for (final Future f : futures) f.get(); System.out.println(String.format("%d : %s : %fs", i, compression.getType(), 0.001 * (System.currentTimeMillis() - t))); } catch (final N5Exception | InterruptedException | ExecutionException e) { fail(e.getMessage()); } } /* TIF blocks */ futures.clear(); t = System.currentTimeMillis(); final String compressedDatasetName = testDirPath + "/" + datasetName + ".tif"; try { new File(compressedDatasetName).mkdirs(); for (int z = 0; z < nBlocks; ++z) { final int fz = z; for (int y = 0; y < nBlocks; ++y) { final int fy = y; for (int x = 0; x < nBlocks; ++x) { final int fx = x; futures.add( exec.submit( () -> { final ImagePlus impBlock = new ImagePlus("", new ShortProcessor(64, 64 * 64, data, null)); IJ.saveAsTiff(impBlock, compressedDatasetName + "/" + fx + "-" + fy + "-" + fz + ".tif"); return true; })); } } } for (final Future f : futures) f.get(); System.out.println(String.format("%d : tif : %fs", i, 0.001 * (System.currentTimeMillis() - t))); } catch (final InterruptedException | ExecutionException e) { fail(e.getMessage()); } /* HDF5 raw */ futures.clear(); t = System.currentTimeMillis(); try { final String hdf5Name = testDirPath + "/" + datasetName + ".h5"; final IHDF5Writer hdf5Writer = HDF5Factory.open( hdf5Name ); final IHDF5ShortWriter uint16Writer = hdf5Writer.uint16(); uint16Writer.createMDArray( datasetName, new long[]{64 * nBlocks, 64 * nBlocks, 64 * nBlocks}, new int[]{64, 64, 64}, HDF5IntStorageFeatures.INT_NO_COMPRESSION); for (int z = 0; z < nBlocks; ++z) { final int fz = z; for (int y = 0; y < nBlocks; ++y) { final int fy = y; for (int x = 0; x < nBlocks; ++x) { final int fx = x; futures.add( exec.submit( () -> { final MDShortArray targetCell = new MDShortArray(data, new int[]{64, 64, 64}); uint16Writer.writeMDArrayBlockWithOffset(datasetName, targetCell, new long[]{64 * fz, 64 * fy, 64 * fx}); return true; })); } } } for (final Future f : futures) f.get(); hdf5Writer.close(); System.out.println(String.format("%d : hdf5 raw : %fs", i, 0.001 * (System.currentTimeMillis() - t))); new File(hdf5Name).delete(); } catch (final InterruptedException | ExecutionException e) { fail(e.getMessage()); } /* HDF5 gzip */ futures.clear(); t = System.currentTimeMillis(); try { final String hdf5Name = testDirPath + "/" + datasetName + ".gz.h5"; final IHDF5Writer hdf5Writer = HDF5Factory.open( hdf5Name ); final IHDF5ShortWriter uint16Writer = hdf5Writer.uint16(); uint16Writer.createMDArray( datasetName, new long[]{64 * nBlocks, 64 * nBlocks, 64 * nBlocks}, new int[]{64, 64, 64}, HDF5IntStorageFeatures.INT_AUTO_SCALING_DEFLATE); for (int z = 0; z < nBlocks; ++z) { final int fz = z; for (int y = 0; y < nBlocks; ++y) { final int fy = y; for (int x = 0; x < nBlocks; ++x) { final int fx = x; futures.add( exec.submit( () -> { final MDShortArray targetCell = new MDShortArray(data, new int[]{64, 64, 64}); uint16Writer.writeMDArrayBlockWithOffset(datasetName, targetCell, new long[]{64 * fz, 64 * fy, 64 * fx}); return true; })); } } } for (final Future f : futures) f.get(); hdf5Writer.close(); System.out.println(String.format("%d : hdf5 gzip : %fs", i, 0.001 * (System.currentTimeMillis() - t))); new File(hdf5Name).delete(); } catch (final InterruptedException | ExecutionException e) { fail(e.getMessage()); } exec.shutdown(); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/N5CachedFSTest.java ================================================ package org.janelia.saalfeldlab.n5; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.net.URISyntaxException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Arrays; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.Test; import com.google.gson.GsonBuilder; import com.google.gson.JsonElement; public class N5CachedFSTest extends N5FSTest { @Override protected N5Writer createN5Writer(final String location, final GsonBuilder gson) throws IOException, URISyntaxException { return createN5Writer(location, gson, true); } @Override protected N5Reader createN5Reader(final String location, final GsonBuilder gson) throws IOException, URISyntaxException { return createN5Reader(location, gson, true); } protected N5Writer createN5Writer(final String location, final GsonBuilder gson, final boolean cache) throws IOException, URISyntaxException { return new N5FSWriter(location, gson, cache); } protected N5Writer createN5Writer(final String location, final boolean cache) throws IOException, URISyntaxException { return createN5Writer(location, new GsonBuilder(), cache); } protected N5Reader createN5Reader(final String location, final GsonBuilder gson, final boolean cache) throws IOException, URISyntaxException { return new N5FSReader(location, gson, cache); } @Override protected N5Writer createN5Writer() throws IOException, URISyntaxException { return new N5FSWriter(tempN5Location(), new GsonBuilder(), true) { @Override public void close() { super.close(); remove(); } }; } @Test public void cacheTest() throws IOException, URISyntaxException { /* Test the cache by setting many attributes, then manually deleting the underlying file. * The only possible way for the test to succeed is if it never again attempts to read the file, and relies on the cache. */ try (N5KeyValueWriter n5 = (N5KeyValueWriter) createN5Writer()) { final String cachedGroup = "cachedGroup"; final String attributesPath = n5.absoluteAttributesPath(cachedGroup); final ArrayList> tests = new ArrayList<>(); n5.createGroup(cachedGroup); addAndTest(n5, tests, new TestData<>(cachedGroup, "a/b/c", 100)); addAndTest(n5, tests, new TestData<>(cachedGroup, "a/a[5]", "asdf")); addAndTest(n5, tests, new TestData<>(cachedGroup, "a/a[2]", 0)); Files.delete(Paths.get(attributesPath)); runTests(n5, tests); } try (N5KeyValueWriter n5 = (N5KeyValueWriter)createN5Writer(tempN5Location(), false)) { final String cachedGroup = "cachedGroup"; final String attributesPath = n5.absoluteAttributesPath(cachedGroup); final ArrayList> tests = new ArrayList<>(); n5.createGroup(cachedGroup); addAndTest(n5, tests, new TestData<>(cachedGroup, "a/b/c", 100)); addAndTest(n5, tests, new TestData<>(cachedGroup, "a/a[5]", "asdf")); addAndTest(n5, tests, new TestData<>(cachedGroup, "a/a[2]", 0)); Files.delete(Paths.get(attributesPath)); assertThrows(AssertionError.class, () -> runTests(n5, tests)); n5.remove(); } } @Test public void cacheGroupDatasetTest() throws IOException, URISyntaxException { final String datasetName = "dd"; final String groupName = "gg"; final String tmpLocation = tempN5Location(); try (GsonKeyValueN5Writer w1 = (GsonKeyValueN5Writer) createN5Writer(tmpLocation); GsonKeyValueN5Writer w2 = (GsonKeyValueN5Writer) createN5Writer(tmpLocation);) { // create a group, both writers know it exists w1.createGroup(groupName); assertTrue(w1.exists(groupName)); assertTrue(w2.exists(groupName)); // one writer removes the group w2.remove(groupName); assertTrue(w1.exists(groupName)); // w1's cache thinks group still exists assertFalse(w2.exists(groupName)); // w2 knows group has been removed // create a dataset w1.createDataset(datasetName, dimensions, blockSize, DataType.UINT8, new RawCompression()); assertTrue(w1.exists(datasetName)); assertTrue(w2.exists(datasetName)); assertNotNull(w1.getDatasetAttributes(datasetName)); assertNotNull(w2.getDatasetAttributes(datasetName)); // one writer removes the data w2.remove(datasetName); assertTrue(w1.exists(datasetName)); // w1's cache thinks group still exists assertFalse(w2.exists(datasetName)); // w2 knows group has been removed assertNotNull(w1.getDatasetAttributes(datasetName)); assertNull(w2.getDatasetAttributes(datasetName)); w1.remove(); } } @Test public void cacheBehaviorTest() throws IOException, URISyntaxException { final String loc = tempN5Location(); // make an uncached n5 writer try (final N5TrackingStorage n5 = new N5TrackingStorage(new FileSystemKeyValueAccess(), loc, new GsonBuilder(), true)) { cacheBehaviorHelper(n5); n5.remove(); } } public static void cacheBehaviorHelper(final TrackingStorage n5) throws IOException, URISyntaxException { // non existant group final String groupA = "groupA"; final String groupB = "groupB"; // expected backend method call counts int expectedExistCount = 0; final int expectedGroupCount = 0; final int expectedDatasetCount = 0; int expectedAttributeCount = 0; int expectedListCount = 0; int expectedWriteAttributeCount = 0; boolean exists = n5.exists(groupA); boolean groupExists = n5.groupExists(groupA); boolean datasetExists = n5.datasetExists(groupA); assertFalse(exists); // group does not exist assertFalse(groupExists); // group does not exist assertFalse(datasetExists); // dataset does not exist assertEquals(++expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(++expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.createGroup(groupA); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); // group B exists = n5.exists(groupB); groupExists = n5.groupExists(groupB); datasetExists = n5.datasetExists(groupB); assertFalse(exists); // group now exists assertFalse(groupExists); // group now exists assertFalse(datasetExists); // dataset does not exist assertEquals(++expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(++expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); exists = n5.exists(groupA); groupExists = n5.groupExists(groupA); datasetExists = n5.datasetExists(groupA); assertTrue(exists); // group now exists assertTrue(groupExists); // group now exists assertFalse(datasetExists); // dataset does not exist assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); final String cachedGroup = "cachedGroup"; // should not check existence when creating a group n5.createGroup(cachedGroup); n5.createGroup(cachedGroup); // be annoying assertEquals(++expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(++expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); // should not check existence when this instance created a group n5.exists(cachedGroup); n5.groupExists(cachedGroup); n5.datasetExists(cachedGroup); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); // should not read attributes from container when setting them n5.setAttribute(cachedGroup, "one", 1); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(++expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.setAttribute(cachedGroup, "two", 2); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(++expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.removeAttribute(cachedGroup, "one"); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(++expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.removeAttribute(cachedGroup, "one"); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.removeAttribute(cachedGroup, "cow"); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.removeAttribute(cachedGroup, "two", Integer.class); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(++expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.list(""); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(++expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.list(cachedGroup); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(++expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); /* * Check existence for groups that have not been made by this reader but isGroup * and isDatatset must be false if it does not exists so then should not be * called. * * Similarly, attributes can not exist for a non-existent group, so should not * attempt to get attributes from the container. * * Finally,listing on a non-existent group is pointless, so don't call the * backend storage */ final String nonExistentGroup = "doesNotExist"; n5.exists(nonExistentGroup); assertEquals(++expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(++expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.groupExists(nonExistentGroup); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.datasetExists(nonExistentGroup); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.getAttributes(nonExistentGroup); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); assertThrows(N5Exception.class, () -> n5.list(nonExistentGroup)); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); final String a = "a"; final String ab = "a/b"; final String abc = "a/b/c"; // create "a/b/c" n5.createGroup(abc); assertTrue(n5.exists(abc)); assertTrue(n5.groupExists(abc)); assertFalse(n5.datasetExists(abc)); assertEquals(++expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(++expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); // ensure that backend need not be checked when testing existence of "a/b" // TODO how does this work assertTrue(n5.exists(ab)); assertTrue(n5.groupExists(ab)); assertFalse(n5.datasetExists(ab)); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); // remove a nested group // checks for all children should not require a backend check n5.remove(a); assertFalse(n5.exists(a)); assertFalse(n5.groupExists(a)); assertFalse(n5.datasetExists(a)); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); assertFalse(n5.exists(ab)); assertFalse(n5.groupExists(ab)); assertFalse(n5.datasetExists(ab)); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); assertFalse(n5.exists(abc)); assertFalse(n5.groupExists(abc)); assertFalse(n5.datasetExists(abc)); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.createGroup("a"); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.createGroup("a/a"); assertEquals(++expectedExistCount, n5.getExistCallCount()); assertEquals(++expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.createGroup("a/b"); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); n5.createGroup("a/c"); assertEquals(++expectedExistCount, n5.getExistCallCount()); assertEquals(++expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); final Set abcListSet = Arrays.stream(n5.list("a")).collect(Collectors.toSet()); assertEquals(Stream.of("a", "b", "c").collect(Collectors.toSet()), abcListSet); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(++expectedListCount, n5.getListCallCount()); // list incremented assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); // remove a n5.remove("a/a"); final Set bc = Arrays.stream(n5.list("a")).collect(Collectors.toSet()); assertEquals(Stream.of("b", "c").collect(Collectors.toSet()), bc); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(expectedGroupCount, n5.getGroupCallCount()); assertEquals(expectedDatasetCount, n5.getDatasetCallCount()); assertEquals(expectedAttributeCount, n5.getAttrCallCount()); assertEquals(expectedListCount, n5.getListCallCount()); // list NOT incremented assertEquals(expectedWriteAttributeCount, n5.getWriteAttrCallCount()); /*Check exists should only increment exists if attributes do no exist. Create a new writer and inject * a new group with attributes unbeknownst to this writer */ try (N5Writer writer = new N5FSWriter(n5.getURI().toString(), false)) { writer.createGroup("sneaky_group"); writer.setAttribute("sneaky_group", "sneaky_attribute", "BOO!"); } n5.exists("sneaky_group"); assertEquals(expectedExistCount, n5.getExistCallCount()); assertEquals(++expectedAttributeCount, n5.getAttrCallCount()); // TODO repeat the above exercise when creating dataset } public static interface TrackingStorage extends CachedGsonKeyValueN5Writer { public int getAttrCallCount(); public int getExistCallCount(); public int getGroupCallCount(); public int getGroupAttrCallCount(); public int getDatasetCallCount(); public int getDatasetAttrCallCount(); public int getListCallCount(); public int getWriteAttrCallCount(); } public static class N5TrackingStorage extends N5KeyValueWriter implements TrackingStorage { public int attrCallCount = 0; public int existsCallCount = 0; public int groupCallCount = 0; public int groupAttrCallCount = 0; public int datasetCallCount = 0; public int datasetAttrCallCount = 0; public int listCallCount = 0; public int writeAttrCallCount = 0; public N5TrackingStorage(final KeyValueAccess keyValueAccess, final String basePath, final GsonBuilder gsonBuilder, final boolean cacheAttributes) throws IOException { super(keyValueAccess, basePath, gsonBuilder, cacheAttributes); } @Override public JsonElement getAttributesFromContainer(final String key, final String cacheKey) { attrCallCount++; return super.getAttributesFromContainer(key, cacheKey); } @Override public boolean existsFromContainer(final String path, final String cacheKey) { existsCallCount++; return super.existsFromContainer(path, cacheKey); } @Override public boolean isGroupFromContainer(final String key) { groupCallCount++; return super.isGroupFromContainer(key); } @Override public boolean isGroupFromAttributes(final String normalCacheKey, final JsonElement attributes) { groupAttrCallCount++; return super.isGroupFromAttributes(normalCacheKey, attributes); } @Override public boolean isDatasetFromContainer(final String key) { datasetCallCount++; return super.isDatasetFromContainer(key); } @Override public boolean isDatasetFromAttributes(final String normalCacheKey, final JsonElement attributes) { datasetAttrCallCount++; return super.isDatasetFromAttributes(normalCacheKey, attributes); } @Override public String[] listFromContainer(final String key) { listCallCount++; return super.listFromContainer(key); } @Override public void writeAttributes(String normalGroupPath, JsonElement attributes) throws N5Exception { writeAttrCallCount++; super.writeAttributes(normalGroupPath, attributes); } @Override public int getAttrCallCount() { return attrCallCount; } @Override public int getExistCallCount() { return existsCallCount; } @Override public int getGroupCallCount() { return groupCallCount; } @Override public int getGroupAttrCallCount() { return groupAttrCallCount; } @Override public int getDatasetCallCount() { return datasetCallCount; } @Override public int getDatasetAttrCallCount() { return datasetAttrCallCount; } @Override public int getListCallCount() { return listCallCount; } @Override public int getWriteAttrCallCount() { return writeAttrCallCount; } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/N5FSTest.java ================================================ package org.janelia.saalfeldlab.n5; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicBoolean; import org.junit.Test; import com.google.gson.GsonBuilder; /** * Initiates testing of the filesystem-based N5 implementation. * * @author Stephan Saalfeld * @author Igor Pisarev */ public class N5FSTest extends AbstractN5Test { private static String tempN5PathName() { try { final File tmpFile = Files.createTempDirectory("n5-test-").toFile(); tmpFile.delete(); tmpFile.mkdir(); tmpFile.deleteOnExit(); return tmpFile.getCanonicalPath(); } catch (final Exception e) { throw new RuntimeException(e); } } @Override protected String tempN5Location() throws URISyntaxException { final String basePath = new File(tempN5PathName()).toURI().normalize().getPath(); return new URI("file", null, basePath, null).toString(); } @Override protected N5Writer createN5Writer() throws IOException, URISyntaxException { return createN5Writer(tempN5Location(), new GsonBuilder()); } @Override protected N5Writer createN5Writer( final String location, final GsonBuilder gson) throws IOException, URISyntaxException { return new N5FSWriter(location, gson); } @Override protected N5Reader createN5Reader( final String location, final GsonBuilder gson) throws IOException, URISyntaxException { return new N5FSReader(location, gson); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/N5ReadBenchmark.java ================================================ package org.janelia.saalfeldlab.n5; import com.google.gson.GsonBuilder; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Random; import java.util.concurrent.TimeUnit; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import org.openjdk.jmh.runner.options.TimeValue; @State( Scope.Thread ) @Fork( 1 ) public class N5ReadBenchmark { private static String tempN5PathName() { try { final File tmpFile = Files.createTempDirectory("n5-test-").toFile(); tmpFile.deleteOnExit(); return tmpFile.getCanonicalPath(); } catch (final Exception e) { throw new RuntimeException(e); } } private static final String basePath = tempN5PathName(); private static final String datasetName = "/test/group/dataset"; private static final long[] dimensions = new long[]{640, 640, 640}; private static final int[] blockSize = new int[]{64, 64, 64}; private static byte[] byteBlock; private static short[] shortBlock; private static int[] intBlock; private static long[] longBlock; private static float[] floatBlock; private static double[] doubleBlock; private static void createData() { final Random rnd = new Random(); final int blockNumElements = DataBlock.getNumElements(blockSize); byteBlock = new byte[blockNumElements]; shortBlock = new short[blockNumElements]; intBlock = new int[blockNumElements]; longBlock = new long[blockNumElements]; floatBlock = new float[blockNumElements]; doubleBlock = new double[blockNumElements]; rnd.nextBytes(byteBlock); for (int i = 0; i < blockNumElements; ++i) { shortBlock[i] = (short)rnd.nextInt(); intBlock[i] = rnd.nextInt(); longBlock[i] = rnd.nextLong(); floatBlock[i] = Float.intBitsToFloat(rnd.nextInt()); doubleBlock[i] = Double.longBitsToDouble(rnd.nextLong()); } } private static N5Writer createTempN5Writer() { return new N5FSWriter(basePath, new GsonBuilder()); } private static N5Reader n5; private static DatasetAttributes attributes; @Setup(Level.Trial) public void setup() { createData(); System.out.println("basePath = " + basePath); try (final N5Writer n5 = createTempN5Writer()) { // final Compression compression = new Lz4Compression(); final Compression compression = new RawCompression(); n5.createDataset(datasetName, dimensions, blockSize, DataType.FLOAT64, compression); final DatasetAttributes attributes = n5.getDatasetAttributes(datasetName); final DoubleArrayDataBlock dataBlock = new DoubleArrayDataBlock(blockSize, new long[] {0, 0, 0}, doubleBlock); n5.writeChunk(datasetName, attributes, dataBlock); } n5 = new N5FSReader(basePath, new GsonBuilder()); attributes = n5.getDatasetAttributes(datasetName); } // @TearDown(Level.Trial) // public void teardown() { // } @Benchmark @BenchmarkMode( Mode.AverageTime ) @OutputTimeUnit( TimeUnit.MILLISECONDS ) public void bench() { n5.readChunk(datasetName, attributes, 0, 0, 0); } public static void main( final String... args ) throws RunnerException, IOException { final Options opt = new OptionsBuilder() .include( N5ReadBenchmark.class.getSimpleName() ) .warmupIterations( 8 ) .measurementIterations( 8 ) .warmupTime( TimeValue.milliseconds( 500 ) ) .measurementTime( TimeValue.milliseconds( 500 ) ) .build(); new Runner( opt ).run(); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/N5URITest.java ================================================ package org.janelia.saalfeldlab.n5; import org.junit.Test; import java.net.URISyntaxException; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; public class N5URITest { @Test public void testAttributePath() { assertEquals("/a/b/c/d/e", N5URI.normalizeAttributePath("/a/b/c/d/e")); assertEquals("/a/b/c/d/e", N5URI.normalizeAttributePath("/a/b/c/d/e/")); assertEquals("/", N5URI.normalizeAttributePath("/")); assertEquals("", N5URI.normalizeAttributePath("")); assertEquals("/", N5URI.normalizeAttributePath("/a/..")); assertEquals("/", N5URI.normalizeAttributePath("/a/../b/../c/d/../..")); assertEquals("/", N5URI.normalizeAttributePath("/a/../b/../c/d/../..")); assertEquals("/", N5URI.normalizeAttributePath("/a/../b/../c/d/../..")); assertEquals("/", N5URI.normalizeAttributePath("/./././././")); assertEquals("/", N5URI.normalizeAttributePath("/./././././.")); assertEquals("", N5URI.normalizeAttributePath("./././././")); assertEquals("a.", N5URI.normalizeAttributePath("./a././././")); assertEquals("\\\\.", N5URI.normalizeAttributePath("./a./../\\\\././.")); assertEquals("a./\\\\.", N5URI.normalizeAttributePath("./a./\\\\././.")); assertEquals("/a./\\\\.", N5URI.normalizeAttributePath("/./a./\\\\././.")); assertEquals("/a/[0]/b/[0]", N5URI.normalizeAttributePath("/a/[0]/b[0]/")); assertEquals("[0]", N5URI.normalizeAttributePath("[0]")); assertEquals("/[0]", N5URI.normalizeAttributePath("/[0]/")); assertEquals("/a/[0]", N5URI.normalizeAttributePath("/a[0]/")); assertEquals("/[0]/b", N5URI.normalizeAttributePath("/[0]b/")); assertEquals("[b]", N5URI.normalizeAttributePath("[b]")); assertEquals("a[b]c", N5URI.normalizeAttributePath("a[b]c")); assertEquals("a[bc", N5URI.normalizeAttributePath("a[bc")); assertEquals("ab]c", N5URI.normalizeAttributePath("ab]c")); assertEquals("a[b00]c", N5URI.normalizeAttributePath("a[b00]c")); assertEquals("[ ]", N5URI.normalizeAttributePath("[ ]")); assertEquals("[\n]", N5URI.normalizeAttributePath("[\n]")); assertEquals("[\t]", N5URI.normalizeAttributePath("[\t]")); assertEquals("[\r\n]", N5URI.normalizeAttributePath("[\r\n]")); assertEquals("[ ][\n][\t][ \t \n \r\n][\r\n]", N5URI.normalizeAttributePath("[ ][\n][\t][ \t \n \r\n][\r\n]")); assertEquals("[\\]", N5URI.normalizeAttributePath("[\\]")); assertEquals("let's/try/a/real/case/with spaces", N5URI.normalizeAttributePath("let's/try/a/real/case/with spaces/")); assertEquals("let's/try/a/real/case/with spaces", N5URI.normalizeAttributePath("let's/try/a/real/////case////with spaces/")); assertEquals("/first/relative/a/wd/.w/asd", N5URI.normalizeAttributePath("/../first/relative/test/../a/b/.././wd///.w/asd")); assertEquals("first/relative/a/wd/.w/asd", N5URI.normalizeAttributePath("../first/relative/test/../a/b/.././wd///.w/asd")); assertEquals("", N5URI.normalizeAttributePath("../result/../only/../single/..")); assertEquals("", N5URI.normalizeAttributePath("../result/../multiple/../..")); assertEquals("/", N5URI.normalizeAttributePath("/../result/../multiple/../..")); assertEquals("b", N5URI.normalizeAttributePath("a/../b")); assertEquals("/b", N5URI.normalizeAttributePath("/a/../b")); assertEquals("b", N5URI.normalizeAttributePath("../a/../b")); assertEquals("/b", N5URI.normalizeAttributePath("/../a/../b")); assertEquals("/b", N5URI.normalizeAttributePath("/../a/../../b")); String normalizedPath = N5URI.normalizeAttributePath("let's/try/a/some/////with/ / //white spaces/"); assertEquals("Normalizing a normal path should be the identity", normalizedPath, N5URI.normalizeAttributePath(normalizedPath)); } @Test public void testEscapedAttributePaths() { assertEquals("\\/a\\/b\\/c\\/d\\/e", N5URI.normalizeAttributePath("\\/a\\/b\\/c\\/d\\/e")); assertEquals("/a\\\\/b/c", N5URI.normalizeAttributePath("/a\\\\/b/c")); assertEquals("a[b]\\[10]", N5URI.normalizeAttributePath("a[b]\\[10]")); assertEquals("\\[10]", N5URI.normalizeAttributePath("\\[10]")); assertEquals("a/[0]/\\[10]b", N5URI.normalizeAttributePath("a[0]\\[10]b")); assertEquals("[\\[10]\\[20]]", N5URI.normalizeAttributePath("[\\[10]\\[20]]")); assertEquals("[\\[10]/[20]/]", N5URI.normalizeAttributePath("[\\[10][20]]")); assertEquals("\\/", N5URI.normalizeAttributePath("\\/")); } @Test public void testIsAbsolute() throws URISyntaxException { /* Always true if scheme provided */ assertTrue(new N5URI("file:///a/b/c").isAbsolute()); assertTrue(new N5URI("file://C:\\\\a\\\\b\\\\c").isAbsolute()); /* Unix Paths*/ assertTrue(new N5URI("/a/b/c").isAbsolute()); assertFalse(new N5URI("a/b/c").isAbsolute()); /* Windows Paths*/ assertTrue(new N5URI("C:\\\\a\\\\b\\\\c").isAbsolute()); assertFalse(new N5URI("a\\\\b\\\\c").isAbsolute()); } @Test public void testGetRelative() throws URISyntaxException { assertEquals( "/a/b/c/d?e#f", new N5URI("/a/b/c").resolve("d?e#f").toString()); assertEquals( "/d?e#f", new N5URI("/a/b/c").resolve("/d?e#f").toString()); assertEquals( "file:/a/b/c", new N5URI("s3://janelia-cosem-datasets/jrc_hela-3/jrc_hela-3.n5").resolve("file:/a/b/c").toString()); assertEquals( "s3://janelia-cosem-datasets/a/b/c", new N5URI("s3://janelia-cosem-datasets/jrc_hela-3/jrc_hela-3.n5").resolve("/a/b/c").toString()); assertEquals( "s3://janelia-cosem-datasets/a/b/c?d/e#f/g", new N5URI("s3://janelia-cosem-datasets/jrc_hela-3/jrc_hela-3.n5").resolve("/a/b/c?d/e#f/g").toString()); assertEquals( "s3://janelia-cosem-datasets/jrc_hela-3/jrc_hela-3.n5/a/b/c?d/e#f/g", new N5URI("s3://janelia-cosem-datasets/jrc_hela-3/jrc_hela-3.n5").resolve("a/b/c?d/e#f/g").toString()); assertEquals( "s3://janelia-cosem-datasets/jrc_hela-3/jrc_hela-3.n5?d/e#f/g", new N5URI("s3://janelia-cosem-datasets/jrc_hela-3/jrc_hela-3.n5").resolve("?d/e#f/g").toString()); assertEquals( "s3://janelia-cosem-datasets/jrc_hela-3/jrc_hela-3.n5#f/g", new N5URI("s3://janelia-cosem-datasets/jrc_hela-3/jrc_hela-3.n5").resolve("#f/g").toString()); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/TrackingN5Writer.java ================================================ package org.janelia.saalfeldlab.n5; import com.google.gson.GsonBuilder; import org.janelia.saalfeldlab.n5.kva.TrackingKeyValueAccess; /** * An N5Writer that tracks the number of materialize calls performed by * its underlying key value access. */ public class TrackingN5Writer extends N5KeyValueWriter { public final TrackingKeyValueAccess tkva; public TrackingN5Writer(String basePath, KeyValueAccess kva) { super(new TrackingKeyValueAccess(kva), basePath, new GsonBuilder(), false); this.tkva = (TrackingKeyValueAccess) getKeyValueAccess(); } public void resetNumMaterializeCalls() { tkva.numMaterializeCalls = 0; } public int getNumMaterializeCalls() { return tkva.numMaterializeCalls; } public void resetNumIsFileCalls() { tkva.numIsFileCalls = 0; } public int getNumIsFileCalls() { return tkva.numIsFileCalls; } public void resetTotalBytesRead() { tkva.totalBytesRead = 0; } public long getTotalBytesRead() { return tkva.totalBytesRead; } public void resetAllTracking() { tkva.numMaterializeCalls = 0; tkva.numIsFileCalls = 0; tkva.totalBytesRead = 0; } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/UriTest.java ================================================ package org.janelia.saalfeldlab.n5; import static org.junit.Assert.assertEquals; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.Paths; import org.junit.Before; import org.junit.Test; public class UriTest { private N5FSWriter n5; private KeyValueAccess kva; private String relativePath = "src/test/resources/url/urlAttributes.n5"; private String relativeAbnormalPath = "src/test/resources/./url/urlAttributes.n5"; private String relativeAbnormalPath2 = "src/test/resources/../resources/url/urlAttributes.n5"; @Before public void before() { n5 = new N5FSWriter(relativePath); kva = n5.getKeyValueAccess(); } @Test public void testUriParsing() throws URISyntaxException { final URI uri = n5.getURI(); assertEquals("Container URI must contain scheme", "file", uri.getScheme()); assertEquals("Container URI must be absolute", uri.getPath(), Paths.get(relativePath).toAbsolutePath().toUri().normalize().getPath()); assertEquals("Container URI must be normalized 1", uri, kva.uri(relativePath)); assertEquals("Container URI must be normalized 2", uri, kva.uri(relativeAbnormalPath)); assertEquals("Container URI must be normalized 3", uri, kva.uri(relativeAbnormalPath2)); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/WriteLockExp.java ================================================ package org.janelia.saalfeldlab.n5; import java.util.Arrays; import org.janelia.saalfeldlab.n5.N5Writer.DataBlockSupplier; import org.janelia.saalfeldlab.n5.universe.N5Factory; import org.janelia.saalfeldlab.n5.zarr.v3.ZarrV3DatasetAttributes; import org.janelia.scicomp.n5.zstandard.ZstandardCompression; public class WriteLockExp { long[] dimensions = new long[]{1024, 1024}; int[] shardSize = {1024, 1024}; int[] chunkSize = {64, 64}; private String root; private int shardIdx; int[] data; public WriteLockExp(String root, int shardIdx) { this.root = root; this.shardIdx = shardIdx; } public static void main(String[] args) { String root = args[0]; int shardIdx = Integer.parseInt(args[1]); WriteLockExp exp = new WriteLockExp( root, shardIdx ); exp.run(); } public void run() { System.out.println("start"); int N = 100_000; final int chunkN = chunkSize[0] * chunkSize[1]; data = data(chunkN, shardIdx+1); final IntArrayDataBlock block = new IntArrayDataBlock(chunkSize, new long[]{shardIdx, shardIdx}, data); try (N5Writer n5 = N5Factory.createWriter(root)) { final DatasetAttributes attrs = getOrCreateDataset(n5); for (int i = 0; i < N; i++) { if( i % 1000 == 0) System.out.println("iter: " + i); // n5.writeRegion("", attrs, new long[]{0,0}, dimensions, blockSupplier(shardIdx, chunkSize, chunkN), true); n5.writeChunks("", attrs, block); } } catch (N5Exception e) { e.printStackTrace(); } System.out.println("done"); } public DatasetAttributes getOrCreateDataset(N5Writer n5) { ZarrV3DatasetAttributes tmpAttrs = ZarrV3DatasetAttributes.builder(dimensions, DataType.INT32) .shardShape(shardSize) .blockSize(chunkSize) .compression(new ZstandardCompression()) .build(); final DatasetAttributes existingAttrs = n5.getDatasetAttributes(""); if( existingAttrs != null ) return existingAttrs; return n5.createDataset("", tmpAttrs); } DataBlockSupplier blockSupplier(final int i, final int[] chunkSize, final int chunkN) { return new DataBlockSupplier() { @Override public DataBlock get(long[] gridPos, DataBlock existingDataBlock) { if (gridPos[0] == i && gridPos[1] == i) return new IntArrayDataBlock(chunkSize, gridPos, data); else return null; } }; } static int[] data(final int chunkN, final int value) { final int[] data = new int[chunkN]; Arrays.fill(data, value); return data; } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/backward/CompatibilityTest.java ================================================ package org.janelia.saalfeldlab.n5.backward; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.util.Arrays; import org.janelia.saalfeldlab.n5.*; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import org.junit.Test; import com.google.gson.JsonElement; public class CompatibilityTest { String[][] readVersionsDataset = { {"data-1.5.0.n5", "raw"}, {"data-2.5.1.n5", "raw"}, {"data-3.1.3.n5", "raw"} }; String writeVersion = "data-3.1.3.n5"; String writeDataset = "raw"; String[] writePathsToTest = {"0/0", "0/1", "1/0", "1/1"}; @Test public void testBackwardReads() throws NumberFormatException, IOException { for (String[] versionDset : readVersionsDataset) backwardReadHelper(versionDset[0], versionDset[1]); } public void backwardReadHelper(final String base, final String dsetPath) throws NumberFormatException, IOException { final N5FSReader n5 = new N5FSReader("src/test/resources/backward/" + base); assertTrue(n5.datasetExists(dsetPath)); final DatasetAttributes attrs = n5.getDatasetAttributes(dsetPath); // equivalent to the assertTrue above, but be extra sure assertNotNull(attrs); byte value = 0; long[] p = new long[2]; DataBlock b00 = n5.readChunk(dsetPath, attrs, p); assertNotNull(b00); assertArrayEquals(new int[]{5,4}, b00.getSize()); assertArrayEquals(expectedData(20, value), b00.getData()); p[0] = 1; p[1] = 0; value++; DataBlock b10 = n5.readChunk(dsetPath, attrs, p); assertNotNull(b10); assertArrayEquals(new int[]{2,4}, b10.getSize()); assertArrayEquals(expectedData(8, value), b10.getData()); p[0] = 0; p[1] = 1; value++; DataBlock b01 = n5.readChunk(dsetPath, attrs, p); assertNotNull(b01); assertArrayEquals(new int[]{5,1}, b01.getSize()); assertArrayEquals(expectedData(5, value), b01.getData()); p[0] = 1; p[1] = 1; value++; DataBlock b11 = n5.readChunk(dsetPath, attrs, p); assertNotNull(b11); assertArrayEquals(new int[]{2,1}, b11.getSize()); assertArrayEquals(expectedData(2, value), b11.getData()); n5.close(); } @Test public void testBlockData() throws IOException { final N5FSReader n5Legacy = new N5FSReader("src/test/resources/backward/" + writeVersion); final URI uriLegacy = n5Legacy.getURI(); final File basePath = Files.createTempDirectory("n5-blockDataTest-").toFile(); basePath.delete(); basePath.mkdir(); basePath.deleteOnExit(); N5FSWriter n5My = CreateSampleData.createSampleData( basePath.getCanonicalPath(), writeDataset, new RawCompression()); URI uriMy = n5My.getURI(); // check attributes final JsonElement attrsLegacy = ((GsonKeyValueN5Reader)n5Legacy).getAttributes(writeDataset); final JsonElement attrsMy = ((GsonKeyValueN5Reader)n5My).getAttributes(writeDataset); assertEquals(attrsLegacy, attrsMy); final KeyValueAccess kva = n5My.getKeyValueAccess(); for (final String path : writePathsToTest) { final byte[] dataMy = read(kva, kva.compose(uriMy, writeDataset, path)); final byte[] dataLegacy = read(kva, kva.compose(uriLegacy, writeDataset, path)); assertArrayEquals(dataLegacy, dataMy); } n5My.remove(); n5My.close(); n5Legacy.close(); } private byte[] read(KeyValueAccess kva, String path) { byte[] data; try (VolatileReadData readData = kva.createReadData(path)) { data = readData.allBytes(); } catch (N5Exception.N5IOException e) { return null; } return data; } private static byte[] expectedData(int size, byte value ) { byte[] data = new byte[size]; Arrays.fill(data, value); return data; } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/backward/CreateSampleData.java ================================================ package org.janelia.saalfeldlab.n5.backward; import java.io.File; import java.io.IOException; import java.util.Arrays; import org.janelia.saalfeldlab.n5.ByteArrayDataBlock; import org.janelia.saalfeldlab.n5.Compression; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.N5FSWriter; import org.janelia.saalfeldlab.n5.RawCompression; public class CreateSampleData { public static void main(String[] args) throws IOException { File f = new File("src/test/resources/data-4.0.0-alpha-X.n5"); System.out.println(f.getCanonicalPath()); createSampleData(f.getCanonicalPath(), "raw", new RawCompression()); } public static N5FSWriter createSampleData(String baseDir, String dataset, Compression compression) throws IOException { N5FSWriter n5 = new N5FSWriter(baseDir); final String dsetPath = compression.getType(); long[] dimensions = new long[]{7, 5}; int[] blkSizeDset = new int[]{5, 4}; int[] blkSize = new int[]{5, 4}; final DatasetAttributes attrs = new DatasetAttributes(dimensions, blkSizeDset, DataType.UINT8, compression); n5.createDataset(dsetPath, attrs); byte val = 0; long[] pos = new long[]{0, 0}; n5.writeChunk(dsetPath, attrs, createDataBlock(blkSize, pos, val)); pos[0] = 1; pos[1] = 0; blkSize[0] = 2; blkSize[1] = 4; val++; n5.writeChunk(dsetPath, attrs, createDataBlock(blkSize, pos, val)); pos[0] = 0; pos[1] = 1; blkSize[0] = 5; blkSize[1] = 1; val++; n5.writeChunk(dsetPath, attrs, createDataBlock(blkSize, pos, val)); pos[0] = 1; pos[1] = 1; blkSize[0] = 2; blkSize[1] = 1; val++; n5.writeChunk(dsetPath, attrs, createDataBlock( blkSize, pos, val )); return n5; } public static ByteArrayDataBlock createDataBlock(int[] size, long[] gridPosition, byte value) throws IOException { int N = Arrays.stream(size).reduce(1, (x,y) -> x*y); final byte[] data = new byte[N]; Arrays.fill(data, value); return new ByteArrayDataBlock(size, gridPosition, data); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/benchmarks/N5BlockWriteBenchmarks.java ================================================ package org.janelia.saalfeldlab.n5.benchmarks; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; import java.util.Random; import java.util.concurrent.TimeUnit; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.FileSystemKeyValueAccess; import org.janelia.saalfeldlab.n5.GzipCompression; import org.janelia.saalfeldlab.n5.N5KeyValueWriter; import org.janelia.saalfeldlab.n5.N5Writer; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.TearDown; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; import com.google.gson.GsonBuilder; @State(Scope.Benchmark) @Warmup(iterations = 5, time = 100, timeUnit = TimeUnit.MICROSECONDS) @Measurement(iterations = 50, time = 100, timeUnit = TimeUnit.MICROSECONDS) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @Fork(1) public class N5BlockWriteBenchmarks { Random random = new Random(7777); final String writeGroup = "writeGroup"; final String readGroup = "readGroup"; N5Writer n5; DatasetAttributes dsetAttrs; ArrayList> blocks; @Param( value = { "int32" } ) protected String dataType; @Param( value = { "3" } ) protected int numDimensions; @Param( value = { "64" } ) protected int blockDim; @Param( value = { "5" } ) protected int numBlocks; public static void main( String[] args ) throws RunnerException { final Options options = new OptionsBuilder().include( N5BlockWriteBenchmarks.class.getSimpleName() + "\\." ).build(); new Runner(options).run(); } @TearDown(Level.Trial) public void teardown() { File d = new File(n5.getURI()); n5.remove(); d.delete(); } @Setup(Level.Trial) public void setup() { File tmpDir; try { tmpDir = Files.createTempDirectory("n5-blockWriteBenchmark-").toFile(); FileSystemKeyValueAccess kva = new FileSystemKeyValueAccess(); n5 = new N5KeyValueWriter(kva, tmpDir.getAbsolutePath(), new GsonBuilder(), true); int[] blockSize = new int[numDimensions]; Arrays.fill(blockSize, blockDim); long[] dims = new long[numDimensions]; Arrays.fill(dims, blockDim); dims[0] = blockDim * numBlocks; DataType dtype = DataType.fromString(dataType); dsetAttrs = new DatasetAttributes(dims, blockSize, dtype, new GzipCompression()); n5.createDataset("", dsetAttrs); blocks = new ArrayList<>(); for (int i = 0; i < numBlocks; i++) { long[] p = new long[numDimensions]; p[0] = i; DataBlock blk = dtype.createDataBlock(blockSize, p); fillBlock(dtype, blk); blocks.add(blk); // write data into the read group n5.writeBlock(readGroup, dsetAttrs, blk); } } catch (final IOException e) { e.printStackTrace(); } } @Benchmark public void writeBenchmark() throws IOException { blocks.forEach(blk -> { n5.writeBlock(writeGroup, dsetAttrs, blk); }); } @Benchmark public void readBenchmark(Blackhole hole) throws IOException { final long[] p = new long[numDimensions]; for (int i = 0; i < numBlocks; i++) { p[0] = i; hole.consume(n5.readBlock(readGroup, dsetAttrs, p)); } } private void fillBlock(DataType dtype, DataBlock blk) { switch (dtype) { case INT32: fill((int[])blk.getData()); break; case FLOAT32: fill((float[])blk.getData()); break; case FLOAT64: fill((double[])blk.getData()); break; case INT16: fill((short[])blk.getData()); break; case INT64: fill((long[])blk.getData()); break; case INT8: fill((byte[])blk.getData()); break; case OBJECT: break; case STRING: break; case UINT16: fill((short[])blk.getData()); break; case UINT32: fill((int[])blk.getData()); break; case UINT64: fill((long[])blk.getData()); break; case UINT8: fill((byte[])blk.getData()); break; default: break; } } private void fill(short[] arr) { for (int i = 0; i < arr.length; i++) arr[i] = (short)random.nextInt(); } private void fill(int[] arr) { for (int i = 0; i < arr.length; i++) arr[i] = random.nextInt(); } private void fill(long[] arr) { for (int i = 0; i < arr.length; i++) arr[i] = random.nextLong(); } private void fill(float[] arr) { for (int i = 0; i < arr.length; i++) arr[i] = random.nextFloat(); } private void fill(double[] arr) { for (int i = 0; i < arr.length; i++) arr[i] = random.nextDouble(); } private void fill(byte[] arr) { random.nextBytes(arr); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/benchmarks/ReadDataBenchmarks.java ================================================ package org.janelia.saalfeldlab.n5.benchmarks; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.Random; import java.util.concurrent.TimeUnit; import org.janelia.saalfeldlab.n5.FileSystemKeyValueAccess; import org.janelia.saalfeldlab.n5.KeyValueAccess; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import org.openjdk.jmh.annotations.Benchmark; import org.openjdk.jmh.annotations.BenchmarkMode; import org.openjdk.jmh.annotations.Fork; import org.openjdk.jmh.annotations.Level; import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; import org.openjdk.jmh.annotations.TearDown; import org.openjdk.jmh.annotations.Warmup; import org.openjdk.jmh.infra.Blackhole; import org.openjdk.jmh.runner.Runner; import org.openjdk.jmh.runner.RunnerException; import org.openjdk.jmh.runner.options.Options; import org.openjdk.jmh.runner.options.OptionsBuilder; @State(Scope.Benchmark) @Warmup(iterations = 10, time = 100, timeUnit = TimeUnit.MICROSECONDS) @Measurement(iterations = 100, time = 100, timeUnit = TimeUnit.MICROSECONDS) @BenchmarkMode(Mode.AverageTime) @OutputTimeUnit(TimeUnit.MICROSECONDS) @Fork(1) public class ReadDataBenchmarks { @Param(value = { "10000000" }) protected int objectSizeBytes; protected Path basePath; protected ArrayList tmpPaths; protected KeyValueAccess kva; protected Random random; public ReadDataBenchmarks() {} public static void main(String... args) throws RunnerException { final Options options = new OptionsBuilder().include(ReadDataBenchmarks.class.getSimpleName() + "\\.") .build(); new Runner(options).run(); } @Benchmark public void run(Blackhole hole) throws IOException { try (final VolatileReadData read = read()) { hole.consume(read.materialize()); } } public VolatileReadData read() throws IOException { return kva.createReadData(getPath().toString()); } protected Path getPath() { return basePath.resolve("tmp-" + objectSizeBytes); } @Setup(Level.Trial) public void setup() throws IOException { random = new Random(); kva = new FileSystemKeyValueAccess(); basePath = Files.createTempDirectory("ReadDataBenchmark-"); tmpPaths = new ArrayList<>(); for (final int sz : sizes()) { Path p = basePath.resolve("tmp-"+sz); write(p, sz); tmpPaths.add(p); } } protected void write(Path path, int numBytes) { final byte[] data = new byte[numBytes]; random.nextBytes(data); System.out.println(path.toAbsolutePath()); System.out.println(numBytes); ReadData readData = ReadData.from(os -> { os.write(data); }).materialize(); try { kva.write(path.toAbsolutePath().toString(), readData); } catch (N5Exception.N5IOException e) { e.printStackTrace(); } } @TearDown(Level.Trial) public void teardown() { for ( Path p : tmpPaths ) { p.toFile().delete(); } basePath.toFile().delete(); } public int[] sizes() { try { final Param ann = ReadDataBenchmarks.class.getDeclaredField("objectSizeBytes").getAnnotation(Param.class); System.out.println(Arrays.toString(ann.value())); return Arrays.stream(ann.value()).mapToInt(Integer::parseInt).toArray(); } catch (final NoSuchFieldException e) { e.printStackTrace(); } catch (final SecurityException e) { e.printStackTrace(); } return null; } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/cache/N5CacheTest.java ================================================ package org.janelia.saalfeldlab.n5.cache; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.util.Arrays; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import org.janelia.saalfeldlab.n5.N5Exception; import org.junit.Test; import com.google.gson.JsonElement; import com.google.gson.JsonObject; public class N5CacheTest { @Test public void cacheBackingTest() { final DummyBackingStorage backingStorage = new DummyBackingStorage(); final N5JsonCache cache = new N5JsonCache(backingStorage); int expectedAttrCallCount = 0; // check existence, ensure backing storage is only called once // this cache `exists` is overridden to write an attribute // which means the `exists` call checks attr existence first, // and since it finds some, it infers the existence without // an explicit check. Some backends don't support exists // so this is a way to handle those cases more elegantly assertEquals(0, backingStorage.existsCallCount); cache.exists("a", null); assertEquals(++expectedAttrCallCount, backingStorage.attrCallCount); assertEquals(0, backingStorage.existsCallCount); cache.exists("a", null); assertEquals(0, backingStorage.existsCallCount); assertEquals(expectedAttrCallCount, backingStorage.attrCallCount); // check existence of new group, ensure backing storage is only called one more time cache.exists("b", null); assertEquals(++expectedAttrCallCount, backingStorage.attrCallCount); assertEquals(0, backingStorage.existsCallCount); cache.exists("b", null); assertEquals(expectedAttrCallCount, backingStorage.attrCallCount); assertEquals(0, backingStorage.existsCallCount); // check isDataset, ensure backing storage is only called when expected // isDataset is called by exists, so should have been called twice here assertEquals(2, backingStorage.isDatasetCallCount); cache.isDataset("a", null); assertEquals(2, backingStorage.isDatasetCallCount); assertEquals(2, backingStorage.isDatasetCallCount); cache.isDataset("b", null); assertEquals(2, backingStorage.isDatasetCallCount); // check isGroup, ensure backing storage is only called when expected // isGroup is called by exists, so should have been called twice here assertEquals(2, backingStorage.isGroupCallCount); cache.isDataset("a", null); assertEquals(2, backingStorage.isGroupCallCount); assertEquals(2, backingStorage.isGroupCallCount); cache.isDataset("b", null); assertEquals(2, backingStorage.isGroupCallCount); // similarly check list, ensure backing storage is only called when expected // list is called by exists, so should have been called twice here assertEquals(0, backingStorage.listCallCount); cache.list("a"); assertEquals(1, backingStorage.listCallCount); assertEquals(1, backingStorage.listCallCount); cache.list("b"); assertEquals(2, backingStorage.listCallCount); // finally check getAttributes // it is not called by exists (since it needs the cache key) assertEquals(expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("a", "foo"); assertEquals(++expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("a", "foo"); assertEquals(expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("a", "bar"); assertEquals(++expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("a", "bar"); assertEquals(expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("a", "face"); assertEquals(++expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("b", "foo"); assertEquals(++expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("b", "foo"); assertEquals(expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("b", "bar"); assertEquals(++expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("b", "bar"); assertEquals(expectedAttrCallCount, backingStorage.attrCallCount); cache.getAttributes("b", "face"); assertEquals(++expectedAttrCallCount, backingStorage.attrCallCount); } @Test public void testCopyOnReadPreventsExternalModification() { final DummyBackingStorage backingStorage = new DummyBackingStorage(); final N5JsonCache cache = new N5JsonCache(backingStorage); // Get attributes and modify the returned object JsonElement attrs1 = cache.getAttributes("path", "key"); attrs1.getAsJsonObject().addProperty("modified", "value"); // Get attributes again - should not contain the modification JsonElement attrs2 = cache.getAttributes("path", "key"); assertFalse(attrs2.getAsJsonObject().has("modified")); // Verify both calls return different instances assertNotSame(attrs1, attrs2); } @Test public void testCacheManipulationMethods() { final DummyBackingStorage backingStorage = new DummyBackingStorage(); final N5JsonCache cache = new N5JsonCache(backingStorage); // First, ensure the path exists in cache assertTrue(cache.exists("path", null)); // Test setAttributes JsonObject newAttrs = new JsonObject(); newAttrs.addProperty("custom", "value"); cache.setAttributes("path", "key", newAttrs); JsonElement retrievedAttrs = cache.getAttributes("path", "key"); assertTrue(retrievedAttrs.getAsJsonObject().has("custom")); assertEquals("value", retrievedAttrs.getAsJsonObject().get("custom").getAsString()); // Test updateCacheInfo JsonObject updatedAttrs = new JsonObject(); updatedAttrs.addProperty("updated", "updated-value"); cache.updateCacheInfo("path", "key2", updatedAttrs); JsonElement retrievedUpdated = cache.getAttributes("path", "key2"); assertTrue(retrievedUpdated.getAsJsonObject().has("updated")); assertEquals("updated-value", retrievedUpdated.getAsJsonObject().get("updated").getAsString()); // Test initializeNonemptyCache cache.initializeNonemptyCache("newPath", "newKey"); assertTrue(cache.exists("newPath", null)); } @Test public void testChildManagement() { final DummyBackingStorage backingStorage = new DummyBackingStorage(); final N5JsonCache cache = new N5JsonCache( backingStorage ); // Initialize parent and children cache.exists("parent", null); cache.list("parent"); // Test addChild cache.addChild( "parent", "child1" ); String[] children = cache.list( "parent" ); assertTrue( Arrays.asList( children ).contains( "child1" ) ); // Note: addChildIfPresent doesn't check or create the parent, // it only adds to existing cache entries // Test addChildIfPresent on non-cached parent // This should not throw and should not create the parent cache.addChildIfPresent("nonexistent", "child"); children = cache.list("nonexistent"); assertFalse(Arrays.asList(children).contains("child")); // Test addChildIfPresent on cached parent without children list cache.exists("parent2", null); children = cache.list("parent2"); // create children array cache.addChildIfPresent("parent2", "child"); children = cache.list("parent2"); assertTrue(Arrays.asList(children).contains("child")); } @Test public void testRemoveCacheHierarchy() { final DummyBackingStorage backingStorage = new DummyBackingStorage(); final N5JsonCache cache = new N5JsonCache(backingStorage); // Setup hierarchy cache.exists("root", null); cache.exists("root/child1", null); cache.exists("root/child1/grandchild", null); cache.exists("root/child2", null); // Add children relationships cache.list("root"); cache.addChild("root", "child1"); cache.addChild("root", "child2"); // Remove child1 and its descendants cache.removeCache("root", "root/child1"); // Verify removal - paths should not exist anymore assertFalse(cache.exists("root/child1", null)); assertFalse(cache.exists("root/child1/grandchild", null)); // Verify parent's children list updated String[] remaining = cache.list("root"); assertFalse(Arrays.asList(remaining).contains("child1")); assertTrue(Arrays.asList(remaining).contains("child2")); // Verify child2 unaffected assertTrue(cache.exists("root/child2", null)); } @Test(expected = N5Exception.N5IOException.class) public void testListNonExistentGroupThrows() { final DummyNonExistentBackingStorage backingStorage = new DummyNonExistentBackingStorage(); final N5JsonCache cache = new N5JsonCache(backingStorage); cache.list("nonexistent"); } @Test public void testEmptyCacheInfoBehavior() { final DummyNonExistentBackingStorage backingStorage = new DummyNonExistentBackingStorage(); final N5JsonCache cache = new N5JsonCache(backingStorage); // Non-existent path should return emptyCacheInfo assertFalse(cache.exists("nonexistent", null)); assertFalse(cache.isGroup("nonexistent", null)); assertFalse(cache.isDataset("nonexistent", null)); assertNull(cache.getAttributes("nonexistent", "key")); } @Test(expected = N5Exception.class) public void testEmptyJsonDeepCopyThrows() { N5JsonCache.emptyJson.deepCopy(); } @Test public void testCacheStateTransitions() { final DummyBackingStorage backingStorage = new DummyBackingStorage(); final N5JsonCache cache = new N5JsonCache(backingStorage); // Start with emptyCacheInfo cache.addNewCacheInfo("path", null, null); // Transition to a nonempty cache cache.initializeNonemptyCache("path", "key"); assertTrue(cache.exists("path", null)); // Update existing cache JsonObject attrs = new JsonObject(); attrs.addProperty("version", "1"); cache.setAttributes("path", "key", attrs); attrs.addProperty("version", "2"); cache.updateCacheInfo("path", "key", attrs); assertEquals("2", cache.getAttributes("path", "key").getAsJsonObject().get("version").getAsString()); } protected static class DummyBackingStorage implements N5JsonCacheableContainer { int attrCallCount = 0; int existsCallCount = 0; int isGroupCallCount = 0; int isDatasetCallCount = 0; int isGroupFromAttrsCallCount = 0; int isDatasetFromAttrsCallCount = 0; int listCallCount = 0; public DummyBackingStorage() { } public JsonElement getAttributesFromContainer(final String path, final String cacheKey) { attrCallCount++; final JsonObject obj = new JsonObject(); obj.addProperty("key", "value"); return obj; } public boolean existsFromContainer(final String path, final String cacheKey) { existsCallCount++; return true; } public boolean isGroupFromContainer(final String path) { isGroupCallCount++; return true; } public boolean isDatasetFromContainer(final String path) { isDatasetCallCount++; return true; } public String[] listFromContainer(final String path) { listCallCount++; return new String[] { "list" }; } @Override public boolean isGroupFromAttributes(final String cacheKey, final JsonElement attributes) { isGroupFromAttrsCallCount++; return true; } @Override public boolean isDatasetFromAttributes(final String cacheKey, final JsonElement attributes) { isDatasetFromAttrsCallCount++; return true; } } // Helper class for non-existent paths protected static class DummyNonExistentBackingStorage extends DummyBackingStorage { @Override public JsonElement getAttributesFromContainer(String key, String cacheKey) { attrCallCount++; return null; } @Override public boolean existsFromContainer(String path, String cacheKey) { existsCallCount++; return false; } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/codec/BlockCodecTests.java ================================================ package org.janelia.saalfeldlab.n5.codec; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.nio.ByteOrder; import java.util.Arrays; import java.util.Random; 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.DoubleArrayDataBlock; import org.janelia.saalfeldlab.n5.FloatArrayDataBlock; import org.janelia.saalfeldlab.n5.GzipCompression; import org.janelia.saalfeldlab.n5.IntArrayDataBlock; import org.janelia.saalfeldlab.n5.LongArrayDataBlock; import org.janelia.saalfeldlab.n5.RawCompression; import org.janelia.saalfeldlab.n5.ShortArrayDataBlock; import org.janelia.saalfeldlab.n5.codec.BytesCodecTests.BitShiftBytesCodec; import org.janelia.saalfeldlab.n5.shard.DatasetAccess; import org.janelia.saalfeldlab.n5.shard.PositionValueAccess; import org.janelia.saalfeldlab.n5.shard.TestPositionValueAccess; import org.junit.Test; public class BlockCodecTests { static Random random = new Random(12345); final int[] blockSize = {11, 7, 5}; private final BitShiftBytesCodec shiftCodec = new BitShiftBytesCodec(3); private final GzipCompression compressor = new GzipCompression(); private final DataCodecInfo[][] dataCodecInfos = new DataCodecInfo[][]{ {}, // empty: "raw" compression {compressor}, {shiftCodec}, {shiftCodec, compressor} }; private final DataType[] dataTypes = { DataType.INT8, DataType.UINT8, DataType.INT16, DataType.UINT16, DataType.INT32, DataType.UINT32, DataType.INT64, DataType.UINT64, DataType.FLOAT32, DataType.FLOAT64 }; @Test public void testN5BlockCodec() throws Exception { for (DataType dataType : dataTypes) { for (DataCodecInfo[] dataCodecInfo : dataCodecInfos) { final DatasetAttributes attributes = new DatasetAttributes( new long[]{32, 32, 32}, blockSize, dataType, new N5BlockCodecInfo(), dataCodecInfo); testBlockCodecHelper(attributes); } } } @Test public void testRawBytesBlockCodec() throws Exception { // Test RawBlockCodecInfo codec with different byte orders and DataTypes final ByteOrder[] byteOrders = {ByteOrder.BIG_ENDIAN, ByteOrder.LITTLE_ENDIAN}; for (DataType dataType : dataTypes) { for (ByteOrder byteOrder : byteOrders) { for (DataCodecInfo[] codecs : dataCodecInfos) { final RawBlockCodecInfo codec = new RawBlockCodecInfo(byteOrder); final DatasetAttributes attributes = new DatasetAttributes( new long[]{32, 32, 32}, blockSize, dataType, codec, codecs); testBlockCodecHelper(attributes); } } } } private void testBlockCodecHelper(DatasetAttributes attributes) throws Exception { // TODO // final int[] blockSize = attributes.getBlockSize(); // final DataType dataType = attributes.getDataType(); // final long[] gridPosition = {3, 2, 1}; // // // Create appropriate data block based on type // DataBlock originalBlock = ((DataBlock)createRandomDataBlock(dataType, blockSize, gridPosition)); // final BlockCodec codec = attributes.getBlockCodec(); // // // Test encode/decode roundtrip // final ReadData encoded = codec.encode(originalBlock); // assertNotNull(encoded); // // final DataBlock decoded = codec.decode(encoded, gridPosition); // assertNotNull(decoded); // // assertArrayEquals("Block size should match", blockSize, decoded.getSize()); // assertArrayEquals("Grid position should match", gridPosition, decoded.getGridPosition()); // assertDataEquals(originalBlock, decoded); // verifyCompatibleDataType(dataType, decoded); } @SuppressWarnings("unchecked") @Test public void testEmptyBlock() throws Exception { // Test handling of empty blocks final int[] blockSize = {0, 0}; final long[] gridPosition = {0, 0}; final N5BlockCodecInfo blockCodecInfo = new N5BlockCodecInfo(); final TestDatasetAttributes attributes = new TestDatasetAttributes( new long[]{64, 64}, new int[]{8, 8}, DataType.UINT8, blockCodecInfo, new RawCompression()); final PositionValueAccess store = new TestPositionValueAccess(); DatasetAccess access = attributes.getDatasetAccess(); // Test encode/decode final ByteArrayDataBlock emptyBlock = new ByteArrayDataBlock(blockSize, gridPosition, new byte[0]); access.writeChunk(store, emptyBlock); final DataBlock decoded = access.readChunk(store, gridPosition); assertEquals("Empty block should have 0 elements", 0, decoded.getNumElements()); } @Test public void testEncodedSizeCalculation() throws Exception { // TODO // Test that encoded size calculations are correct // final int[] blockSize = {64, 64}; // final DatasetAttributes n5ArrayAttrs = new DatasetAttributes( // new long[]{512, 512}, // blockSize, // blockSize, // DataType.INT16, // new N5BlockCodecInfo()); // // // final DatasetAttributes rawArrayAttrs = new DatasetAttributes( // new long[]{512, 512}, // blockSize, // blockSize, // DataType.INT16, // new RawBlockCodecInfo()); // // // Calculate expected sizes // final long rawDataSize = blockSize[0] * blockSize[1] * 2; // INT16 has 2 bytes per element // // // N5BlockCodecInfo adds a header // // the estimate of the encoded size // final long n5EncodedSize = n5ArrayAttrs.getBlockCodecInfo().encodedSize(rawDataSize); // assertTrue("N5 encoded size should be larger than raw size", n5EncodedSize > rawDataSize); // // DataBlock dataBlock = ((DataBlock)createRandomDataBlock(n5ArrayAttrs.getDataType(), blockSize, new long[]{0, 0})); // ReadData n5EncodedDataBlock = n5ArrayAttrs.getBlockCodec().encode(dataBlock); // assertEquals("N5 actual encoded size should equal estimated size", n5EncodedSize, n5EncodedDataBlock.length()); // // // RawBlockCodecInfo should not change size // final long rawEncodedSize = rawArrayAttrs.getBlockCodecInfo().encodedSize(rawDataSize); // assertEquals("Raw encoded size should equal input size", rawDataSize, rawEncodedSize); // // ReadData rawEncodedDataBlock = rawArrayAttrs.getBlockCodec().encode(dataBlock); // assertEquals("Raw actual encoded size should equal estimated size", rawEncodedSize, rawEncodedDataBlock.length()); } private static DataBlock createRandomDataBlock(DataType dataType, int[] blockSize, long[] gridPosition) { final int numElements = Arrays.stream(blockSize).reduce(1, (a, b) -> a * b); switch (dataType) { case INT8: case UINT8: byte[] uint8Data = new byte[numElements]; for (int i = 0; i < numElements; i++) { uint8Data[i] = (byte) random.nextInt(256); } return new ByteArrayDataBlock(blockSize, gridPosition, uint8Data); case INT16: case UINT16: short[] uint16Data = new short[numElements]; for (int i = 0; i < numElements; i++) { uint16Data[i] = (short) random.nextInt(65536); } return new ShortArrayDataBlock(blockSize, gridPosition, uint16Data); case INT32: case UINT32: int[] uint32Data = new int[numElements]; for (int i = 0; i < numElements; i++) { uint32Data[i] = random.nextInt(); } return new IntArrayDataBlock(blockSize, gridPosition, uint32Data); case INT64: case UINT64: long[] uint64Data = new long[numElements]; for (int i = 0; i < numElements; i++) { uint64Data[i] = random.nextLong(); } return new LongArrayDataBlock(blockSize, gridPosition, uint64Data); case FLOAT32: float[] floatData = new float[numElements]; for (int i = 0; i < numElements; i++) { floatData[i] = random.nextFloat(); } return new FloatArrayDataBlock(blockSize, gridPosition, floatData); case FLOAT64: double[] doubleData = new double[numElements]; for (int i = 0; i < numElements; i++) { doubleData[i] = random.nextDouble(); } return new DoubleArrayDataBlock(blockSize, gridPosition, doubleData); default: throw new IllegalArgumentException("Unsupported data type: " + dataType); } } private static void verifyCompatibleDataType(DataType expectedType, DataBlock block) { Object data = block.getData(); switch (expectedType) { case INT8: case UINT8: assertTrue("Expected byte array for " + expectedType, data instanceof byte[]); break; case INT16: case UINT16: assertTrue("Expected short array for " + expectedType, data instanceof short[]); break; case INT32: case UINT32: assertTrue("Expected int array for " + expectedType, data instanceof int[]); break; case INT64: case UINT64: assertTrue("Expected long array for " + expectedType, data instanceof long[]); break; case FLOAT32: assertTrue("Expected float array for " + expectedType, data instanceof float[]); break; case FLOAT64: assertTrue("Expected double array for " + expectedType, data instanceof double[]); break; default: throw new IllegalArgumentException("Unsupported data type: " + expectedType); } } private static void assertDataEquals(DataBlock expected, DataBlock actual) { Object expectedData = expected.getData(); Object actualData = actual.getData(); if (expectedData instanceof byte[]) { assertArrayEquals((byte[]) expectedData, (byte[]) actualData); } else if (expectedData instanceof short[]) { assertArrayEquals((short[]) expectedData, (short[]) actualData); } else if (expectedData instanceof int[]) { assertArrayEquals((int[]) expectedData, (int[]) actualData); } else if (expectedData instanceof long[]) { assertArrayEquals((long[]) expectedData, (long[]) actualData); } else if (expectedData instanceof float[]) { assertArrayEquals((float[]) expectedData, (float[]) actualData, 0.0f); } else if (expectedData instanceof double[]) { assertArrayEquals((double[]) expectedData, (double[]) actualData, 0.0); } else { throw new IllegalArgumentException("Unknown data type"); } } public static class TestDatasetAttributes extends DatasetAttributes { public TestDatasetAttributes(long[] dimensions, int[] outerBlockSize, DataType dataType, BlockCodecInfo blockCodecInfo, DataCodecInfo... dataCodecInfos) { super(dimensions, outerBlockSize, dataType, blockCodecInfo, dataCodecInfos); } @Override // to make this accessible for the test protected DatasetAccess getDatasetAccess() { return super.getDatasetAccess(); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/codec/BytesCodecTests.java ================================================ package org.janelia.saalfeldlab.n5.codec; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Random; import java.util.function.IntUnaryOperator; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.ReadData.OutputStreamOperator; import org.janelia.saalfeldlab.n5.serialization.NameConfig; import org.junit.BeforeClass; import org.junit.Test; public class BytesCodecTests { static Random random; @BeforeClass public static void setup() { random = new Random(7777); } @Test public void testEncodeDecodeBytes() { // Create a BitShiftBytesCodec with shift value final BitShiftBytesCodec originalCodec = new BitShiftBytesCodec(3); // Test encode/decode roundtrip final byte[] testData = new byte[12]; random.nextBytes(testData); final ReadData original = ReadData.from(testData); final ReadData encoded = originalCodec.encode(original); final ReadData decoded = originalCodec.decode(encoded); final byte[] result = decoded.allBytes(); assertEquals("Length should match", testData.length, result.length); assertArrayEquals("encoded-decoded bytes should match original", testData, result); } @Test public void concatenatedBytesCodecTest() throws IOException { int N = 16; ReadData data = ReadData.from( new InputStream() { @Override public int read() throws IOException { return Math.abs(random.nextInt()) % 32; } }, N ).materialize(); final byte[] bytes = data.allBytes(); final byte[] expected = new byte[bytes.length]; for (int i = 0; i < bytes.length; i++) { expected[i] = (byte)(2 * bytes[i] + 3); } final DataCodec a = new ByteFunctionCodec(x -> 2 * x, x -> x / 2); final DataCodec b = new ByteFunctionCodec(x -> x + 3, x -> x - 3 ); final ConcatenatedDataCodec ab = new ConcatenatedDataCodec(new DataCodec[]{a, b}); final ReadData encodedData = ab.encode(data).materialize(); assertArrayEquals(expected, encodedData.allBytes()); final ReadData decodedData = ab.decode(encodedData).materialize(); assertArrayEquals(bytes, decodedData.allBytes()); } public static class ByteFunctionCodec implements DataCodec, DataCodecInfo { IntUnaryOperator encoder; IntUnaryOperator decoder; public ByteFunctionCodec( IntUnaryOperator encoder, IntUnaryOperator decoder ) { this.encoder = encoder; this.decoder = decoder; } @Override public String getType() { return "byteFunction"; } public ReadData decode(ReadData data) { return data.encode(new ByteFun(decoder)); } public ReadData encode(ReadData data) { return data.encode(new ByteFun(encoder)); } @Override public DataCodec create() { return this; } } private static class ByteFun implements OutputStreamOperator { IntUnaryOperator fun; public ByteFun(IntUnaryOperator fun) { this.fun = fun; } @Override public OutputStream apply(OutputStream o) { return new OutputStream() { @Override public void write(int b) throws IOException { o.write(fun.applyAsInt(b)); } }; } } @NameConfig.Name(BitShiftBytesCodec.TYPE) public static class BitShiftBytesCodec implements DataCodec, DataCodecInfo { @Override public DataCodec create() { return this; } private static final String TYPE = "bitshift"; @NameConfig.Parameter private int shift; public BitShiftBytesCodec() { shift = 0; } public BitShiftBytesCodec(int shift) { this.shift = shift; } @Override public String getType() { return TYPE; } @Override public ReadData decode(ReadData readData) throws N5IOException { if (shift == 0) { return readData; } final byte[] data = readData.allBytes(); final byte[] decoded = new byte[data.length]; // Apply inverse bit shift (right rotate) to decode for (int i = 0; i < data.length; i++) { int b = data[i] & 0xFF; decoded[i] = (byte)((b >>> shift) | (b << (8 - shift))); } return ReadData.from(decoded); } @Override public ReadData encode(ReadData readData) throws N5IOException { if (shift == 0) { return readData; } byte[] data = readData.allBytes(); byte[] encoded = new byte[data.length]; // Apply bit shift (left rotate) to encode for (int i = 0; i < data.length; i++) { int b = data[i] & 0xFF; encoded[i] = (byte)((b << shift) | (b >>> (8 - shift))); } return ReadData.from(encoded); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null || getClass() != obj.getClass()) { return false; } BitShiftBytesCodec other = (BitShiftBytesCodec)obj; return shift == other.shift; } @Override public int hashCode() { return Integer.hashCode(shift); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/codec/ChecksumCodecTests.java ================================================ package org.janelia.saalfeldlab.n5.codec; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.codec.checksum.Crc32cChecksumCodec; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.junit.Test; public class ChecksumCodecTests { @Test public void testCrc32cChecksumCodec() { final ReadData rd = ReadData.from(new byte[] {0,1,2,3,4,5,6,7,8,9}); final long N = rd.requireLength(); final Crc32cChecksumCodec codec = new Crc32cChecksumCodec(); final ReadData encoded = codec.encode(rd); // Crc32 adds 4 bytes to the data assertEquals(N+codec.numChecksumBytes(), encoded.requireLength()); final ReadData decoded = codec.decode(encoded); assertArrayEquals(rd.allBytes(), decoded.allBytes()); // attempting to decode perturbed data throws exception final byte[] encodedBytes = encoded.allBytes(); encodedBytes[1]++; final ReadData perturbed = ReadData.from(encodedBytes); assertThrows(N5Exception.class, () -> codec.decode(perturbed)); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/codec/DatasetCodecTests.java ================================================ package org.janelia.saalfeldlab.n5.codec; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import org.janelia.saalfeldlab.n5.codec.transpose.TransposeCodecInfo; import org.junit.Test; public class DatasetCodecTests { @Test public void testTransposeCodecSimplification() throws Exception { // 2d final TransposeCodecInfo id2 = new TransposeCodecInfo(new int[]{0, 1}); final TransposeCodecInfo rev2 = new TransposeCodecInfo(new int[]{1, 0}); assertNull(TransposeCodecInfo.concatenate(null)); assertEquals(id2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id2})); assertEquals(rev2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev2})); assertEquals(rev2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev2, id2})); assertEquals(rev2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id2, rev2, id2})); assertEquals(id2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev2, rev2})); assertEquals(id2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev2, rev2, rev2, rev2})); assertEquals(id2, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id2, rev2, id2, rev2, rev2, rev2})); // 3d final TransposeCodecInfo id3 = new TransposeCodecInfo(new int[]{0, 1, 2}); final TransposeCodecInfo rev3 = new TransposeCodecInfo(new int[]{2, 1, 0}); final TransposeCodecInfo t021 = new TransposeCodecInfo(new int[]{0, 2, 1}); final TransposeCodecInfo t102 = new TransposeCodecInfo(new int[]{1, 0, 2}); final TransposeCodecInfo t120 = new TransposeCodecInfo(new int[]{1, 2, 0}); final TransposeCodecInfo t201 = new TransposeCodecInfo(new int[]{2, 0, 1}); assertEquals(id3, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id3})); assertEquals(rev3, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev3})); assertEquals(rev3, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev3, id3})); assertEquals(rev3, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{id3, rev3, id3})); assertEquals(t102, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{rev3, t102, t021})); assertEquals(t201, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{t021, t102})); assertEquals(t120, TransposeCodecInfo.concatenate(new TransposeCodecInfo[]{t102, t021})); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/compression/CompressionTypesTest.java ================================================ package org.janelia.saalfeldlab.n5.compression; import java.lang.reflect.Field; import java.util.Map; import org.apache.commons.collections4.MapUtils; import org.janelia.saalfeldlab.n5.CompressionAdapter; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; /** * @author Stephan Saalfeld * */ public class CompressionTypesTest { /** * @throws java.lang.Exception */ @BeforeClass public static void setUpBeforeClass() throws Exception { } /** * @throws java.lang.Exception */ @AfterClass public static void tearDownAfterClass() throws Exception { } /** * @throws java.lang.Exception */ @Before public void setUp() throws Exception { } /** * @throws java.lang.Exception */ @After public void tearDown() throws Exception { } @Test public void test() throws NoSuchFieldException, SecurityException, IllegalArgumentException, IllegalAccessException { CompressionAdapter compressionTypes = CompressionAdapter.getJsonAdapter(); Field field = CompressionAdapter.class.getDeclaredField("compressionConstructors"); field.setAccessible(true); Object value = field.get(compressionTypes); MapUtils.verbosePrint(System.out, "", (Map)value); field = CompressionAdapter.class.getDeclaredField("compressionParameters"); field.setAccessible(true); value = field.get(compressionTypes); MapUtils.verbosePrint(System.out, "", (Map)value); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/demo/AttributePathDemo.java ================================================ package org.janelia.saalfeldlab.n5.demo; import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; import java.util.Arrays; import java.util.Collections; import java.util.Map; import org.janelia.saalfeldlab.n5.N5FSWriter; import org.janelia.saalfeldlab.n5.N5Reader; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonObject; public class AttributePathDemo { final Gson gson; public AttributePathDemo() { gson = new Gson(); } public static void main(String[] args) throws IOException { new AttributePathDemo().demo(); } public void demo() throws IOException { final String rootPath = "/home/john/projects/n5/demo.n5"; final N5FSWriter n5 = new N5FSWriter( rootPath ); final N5FSWriter n5WithNulls = new N5FSWriter( rootPath, new GsonBuilder().serializeNulls() ); final String group = ""; final String specialGroup = "specialCharsGroup"; final String rmAndNulls = "rmAndNulls"; n5.createGroup(group); n5.createGroup(specialGroup); n5.createGroup(rmAndNulls); // clear all attributes n5.setAttribute(group, "/", new JsonObject()); n5.setAttribute(specialGroup, "/", new JsonObject()); n5.setAttribute(rmAndNulls, "/", new JsonObject()); simple(n5, group); arrays(n5, group); objects(n5, group); specialChars(n5, specialGroup); removingAttributesAndNulls(n5, rmAndNulls); removingAttributesAndNulls(n5WithNulls, rmAndNulls); n5.close(); } public void simple(final N5FSWriter n5, final String group) throws IOException { n5.setAttribute(group, "six", 6); System.out.println(n5.getAttribute("/", "six", Integer.class)); // 6 System.out.println(n5.getAttribute("/", "twelve", Integer.class)); // null final String longKey = "The Answer to the Ultimate Question"; n5.setAttribute(group, longKey, 42); System.out.println(n5.getAttribute(group, longKey, Integer.class)); // 42 n5.setAttribute(group, "name", "Marie Daly"); System.out.println(n5.getAttribute(group, "name", String.class)); // returns "Marie Daly" System.out.println(n5.getAttribute(group, "name", int.class)); // returns null n5.setAttribute(group, "year", "1921"); System.out.println( "(String):" + n5.getAttribute(group, "year", String.class)); System.out.println( "(int) :" + n5.getAttribute(group, "year", int.class)); n5.setAttribute(group, "animal", "aardvark"); System.out.println( n5.getAttribute(group, "animal", String.class)); // "aardvark" n5.setAttribute(group, "animal", new String[]{"bat", "cat", "dog"}); // overwrites "animal" printAsJson( n5.getAttribute(group, "animal", String[].class)); // ["bat", "cat", "dog"] System.out.println(getRawJson(n5, group)); // {"six":6,"The Answer to the Ultimate Question":42,"name":"Marie Daly","year":"1921","animal":["bat","cat","dog"]} n5.setAttribute(group, "/", new JsonObject()); // overwrites "animal" System.out.println(getRawJson(n5, group)); // {} } public void arrays(final N5FSWriter n5, final String group) throws IOException { n5.setAttribute(group, "array", new double[] { 5, 6, 7, 8 }); System.out.println( Arrays.toString(n5.getAttribute(group, "array", double[].class))); // [5.0, 6.0, 7.0, 8.0] System.out.println( n5.getAttribute(group, "array[0]", double.class)); // 7.0 System.out.println( n5.getAttribute(group, "array[2]", double.class)); // 7.0 System.out.println( n5.getAttribute(group, "array[999]", double.class)); // null System.out.println( n5.getAttribute(group, "array[-1]", double.class)); // null n5.setAttribute(group, "array[1]", 0.6); System.out.println( Arrays.toString(n5.getAttribute(group, "array", double[].class))); // [5.0, 0.6, 7.0, 8.0] n5.setAttribute(group, "array[6]", 99.99 ); System.out.println( Arrays.toString(n5.getAttribute(group, "array", double[].class))); // [5.0, 0.6, 7.0, 8.0, 0.0, 0.0, 99.99] n5.setAttribute(group, "array[-5]", -5 ); System.out.println( Arrays.toString(n5.getAttribute(group, "array", double[].class))); // [5.0, 0.6, 7.0, 8.0, 0.0, 0.0, 99.99] System.out.println( n5.getAttribute(group, "array", int.class)); // [5.0, 0.6, 7.0, 8.0, 0.0, 0.0, 99.99] } @SuppressWarnings("rawtypes") public void objects(final N5FSWriter n5, final String group) throws IOException { Map a = Collections.singletonMap("a", "A"); Map b = Collections.singletonMap("b", "B"); Map c = Collections.singletonMap("c", "C"); n5.setAttribute(group, "obj", a ); printAsJson(n5.getAttribute(group, "obj", Map.class)); // {"a":"A"} System.out.println(""); n5.setAttribute(group, "obj/a", b); printAsJson(n5.getAttribute(group, "obj", Map.class)); // {"a": {"b": "B"}} printAsJson(n5.getAttribute(group, "obj/a", Map.class)); // {"b": "B"} System.out.println(""); n5.setAttribute(group, "obj/a/b", c); printAsJson(n5.getAttribute(group, "obj", Map.class)); // {"a": {"b": {"c": "C"}}} printAsJson(n5.getAttribute(group, "obj/a", Map.class)); // {"b": {"c": "C"}} printAsJson(n5.getAttribute(group, "obj/a/b", Map.class)); // {"c": "C"} printAsJson(n5.getAttribute(group, "/", Map.class)); // returns {"obj": {"a": {"b": {"c": "C"}}}} System.out.println(""); n5.setAttribute(group, "pet", new Pet("Pluto", 93)); System.out.println(n5.getAttribute(group, "pet", Pet.class)); // Pet("Pluto", 93) printAsJson(n5.getAttribute(group, "pet", Map.class)); // {"name": "Pluto", "age": 93} n5.setAttribute(group, "pet/likes", new String[]{"Micky"}); printAsJson(n5.getAttribute(group, "pet", Map.class)); // {"name": "Pluto", "age": 93, "likes": ["Micky"]} System.out.println(""); n5.removeAttribute(group, "/"); System.out.println(getRawJson(n5, group)); // null n5.setAttribute(group, "one/[2]/three/[4]", 5); System.out.println(getRawJson(n5, group)); // {"one":[null,null,{"three":[0,0,0,0,5]}]} } public void specialChars(final N5FSWriter n5, final String group) throws IOException { n5.setAttribute(group, "\\/", "fwdSlash"); printAsJson(n5.getAttribute(group, "\\/", String.class )); // "fwdSlash" n5.setAttribute(group, "\\\\", "bckSlash"); printAsJson(n5.getAttribute(group, "\\\\", String.class )); // "bckSlash" // print out the contents of attributes.json System.out.println("\n" + getRawJson(n5, group)); // {"/":"fwdSlash","\\\\":"bckSlash"} } public void removingAttributesAndNulls(final N5FSWriter n5, final String group) throws IOException { n5.setAttribute(group, "cow", "moo"); n5.setAttribute(group, "dog", "woof"); n5.setAttribute(group, "sheep", "baa"); System.out.println(getRawJson(n5, group)); // {"sheep":"baa","cow":"moo","dog":"woof"} n5.removeAttribute(group, "cow"); // void method System.out.println(getRawJson(n5, group)); // {"sheep":"baa","dog":"woof"} String theDogSays = n5.removeAttribute(group, "dog", String.class); // returns type System.out.println(theDogSays); // woof System.out.println(getRawJson(n5, group)); // {"sheep":"baa"} n5.removeAttribute(group, "sheep", int.class); // returns type System.out.println(getRawJson(n5, group)); // {"sheep":"baa"} System.out.println( n5.removeAttribute(group, "sheep", String.class)); // "baa" System.out.println(getRawJson(n5, group)); // {} n5.setAttribute(group, "attr", "value"); System.out.println(getRawJson(n5, group)); // {"attr":"value"} n5.setAttribute(group, "attr", null); System.out.println(getRawJson(n5, group)); // if serializeNulls {"attr":null} n5.setAttribute(group, "foo", 12); System.out.println(getRawJson(n5, group)); // {"foo":12} n5.removeAttribute(group, "foo"); System.out.println(getRawJson(n5, group)); // {} } public String getRawJson(final N5Reader n5, final String group) throws IOException { return new String( Files.readAllBytes( Paths.get(Paths.get(n5.getURI()).toAbsolutePath().toString(), group, "attributes.json")), Charset.defaultCharset()); } public void printAsJson(final Object obj) { System.out.println(gson.toJson(obj)); } class Pet { String name; int age; public Pet(String name, int age) { this.name = name; this.age = age; } public String toString() { return String.format("pet %s is %d", name, age); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/http/HttpKeyValueAccessTest.java ================================================ package org.janelia.saalfeldlab.n5.http; import org.apache.commons.io.IOUtils; import org.janelia.saalfeldlab.n5.HttpKeyValueAccess; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import org.junit.Test; import java.io.IOException; import java.net.URI; import java.nio.charset.Charset; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assume.assumeTrue; public class HttpKeyValueAccessTest { static final URI baseUrl = URI.create("https://raw.githubusercontent.com/saalfeldlab/n5/afb067678b4827777bb26b6412e7759fb7edee5a/src/test/resources/url/urlAttributes.n5"); static final String expectedAttributes = "{\"n5\":\"2.6.1\",\"foo\":\"bar\",\"f o o\":\"b a r\",\"list\":[0,1,2,3],\"nestedList\":[[[1,2,3,4]],[[10,20,30,40]],[[100,200,300,400]],[[1000,2000,3000,4000]]],\"object\":{\"a\":\"aa\",\"b\":\"bb\"}}"; @Test public void testExistsRead() { final HttpKeyValueAccess kva = new HttpKeyValueAccess(); final String key = "attributes.json"; final String absolutePath = kva.compose(baseUrl, key); assumeTrue(kva.exists(absolutePath)); try (VolatileReadData data = kva.createReadData(absolutePath)) { IOUtils.toString(data.inputStream(), Charset.defaultCharset()); } catch (IOException e) { // not correct to fail for an IO exception e.printStackTrace(); } } @Test public void testUnsupportedOperations() { final HttpKeyValueAccess kva = new HttpKeyValueAccess(); assertThrows(N5Exception.class, () -> kva.delete("foo")); assertThrows(N5Exception.class, () -> kva.write("bar", ReadData.from(os -> {}))); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/http/HttpReaderFsWriter.java ================================================ package org.janelia.saalfeldlab.n5.http; import com.google.gson.Gson; import com.google.gson.JsonElement; import org.janelia.saalfeldlab.n5.CachedGsonKeyValueN5Reader; import org.janelia.saalfeldlab.n5.CachedGsonKeyValueN5Writer; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.GsonKeyValueN5Reader; import org.janelia.saalfeldlab.n5.GsonKeyValueN5Writer; import org.janelia.saalfeldlab.n5.KeyValueAccess; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.N5KeyValueReader; import java.io.Serializable; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.net.URI; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.function.Predicate; public class HttpReaderFsWriter implements GsonKeyValueN5Writer { private final GsonKeyValueN5Writer writer; private final GsonKeyValueN5Reader reader; public HttpReaderFsWriter(final W writer, final R reader) { this.writer = writer; this.reader = reader; if (reader instanceof CachedGsonKeyValueN5Reader && writer instanceof CachedGsonKeyValueN5Writer) { final CachedGsonKeyValueN5Reader cachedReader = (CachedGsonKeyValueN5Reader)reader; final CachedGsonKeyValueN5Writer cachedWriter = (CachedGsonKeyValueN5Writer)writer; if (cachedReader.cacheMeta()) { /* Hack necessary to test HTTP reader caching without creating the data entirely first */ try { // Access the private 'cache' field in the reader (or the N5KeyValueReader as a fallback) Field cacheField; try { cacheField = reader.getClass().getDeclaredField("cache"); } catch (NoSuchFieldException e) { cacheField = N5KeyValueReader.class.getDeclaredField("cache"); } cacheField.setAccessible(true); // Set the value of 'cache' to the one from writer.getCache() cacheField.set(reader, cachedWriter.getCache()); } catch (NoSuchFieldException | IllegalAccessException e) { throw new RuntimeException("Failed to set reader cache reflectively", e); } } } } @Override public String getAttributesKey() { return writer.getAttributesKey(); } @Override public Version getVersion() throws N5Exception { return reader.getVersion(); } @Override public URI getURI() { return reader.getURI(); } @Override public T getAttribute(String pathName, String key, Class clazz) throws N5Exception { return reader.getAttribute(pathName, key, clazz); } @Override public T getAttribute(String pathName, String key, Type type) throws N5Exception { return reader.getAttribute(pathName, key, type); } @Override public DatasetAttributes getDatasetAttributes(String pathName) throws N5Exception { return reader.getDatasetAttributes(pathName); } @Override public DataBlock readChunk(String pathName, DatasetAttributes datasetAttributes, long... gridPosition) throws N5Exception { return reader.readChunk(pathName, getConvertedDatasetAttributes(datasetAttributes), gridPosition); } @Override public T readSerializedBlock(String dataset, DatasetAttributes attributes, long... gridPosition) throws N5Exception, ClassNotFoundException { return reader.readSerializedBlock(dataset, getConvertedDatasetAttributes(attributes), gridPosition); } @Override public KeyValueAccess getKeyValueAccess() { return reader.getKeyValueAccess(); } @Override public boolean exists(String pathName) { return reader.exists(pathName); } @Override public boolean datasetExists(String pathName) throws N5Exception { return reader.datasetExists(pathName); } @Override public String[] list(String pathName) throws N5Exception { return reader.list(pathName); } @Override public String[] deepList(String pathName, Predicate filter) throws N5Exception { return reader.deepList(pathName, filter); } @Override public String[] deepList(String pathName) throws N5Exception { return reader.deepList(pathName); } @Override public String[] deepListDatasets(String pathName, Predicate filter) throws N5Exception { return reader.deepListDatasets(pathName, filter); } @Override public String[] deepListDatasets(String pathName) throws N5Exception { return reader.deepListDatasets(pathName); } @Override public String[] deepList(String pathName, Predicate filter, ExecutorService executor) throws N5Exception, InterruptedException, ExecutionException { return reader.deepList(pathName, filter, executor); } @Override public String[] deepList(String pathName, ExecutorService executor) throws N5Exception, InterruptedException, ExecutionException { return reader.deepList(pathName, executor); } @Override public String[] deepListDatasets(String pathName, Predicate filter, ExecutorService executor) throws N5Exception, InterruptedException, ExecutionException { return reader.deepListDatasets(pathName, filter, executor); } @Override public String[] deepListDatasets(String pathName, ExecutorService executor) throws N5Exception, InterruptedException, ExecutionException { return reader.deepListDatasets(pathName, executor); } @Override public Gson getGson() { return reader.getGson(); } @Override public Map> listAttributes(String pathName) throws N5Exception { return reader.listAttributes(pathName); } @Override public String getGroupSeparator() { return reader.getGroupSeparator(); } @Override public String groupPath(String... nodes) { return reader.groupPath(nodes); } @Override public void close() { reader.close(); writer.close(); } @Override public void setAttribute(String groupPath, String attributePath, T attribute) throws N5Exception { writer.setAttribute(groupPath, attributePath, attribute); } @Override public void setAttributes(String groupPath, Map attributes) throws N5Exception { writer.setAttributes(groupPath, attributes); } @Override public boolean removeAttribute(String groupPath, String attributePath) throws N5Exception { return writer.removeAttribute(groupPath, attributePath); } @Override public T removeAttribute(String groupPath, String attributePath, Class clazz) throws N5Exception { return writer.removeAttribute(groupPath, attributePath, clazz); } @Override public boolean removeAttributes(String groupPath, List attributePaths) throws N5Exception { return writer.removeAttributes(groupPath, attributePaths); } @Override public void setDatasetAttributes(String datasetPath, DatasetAttributes datasetAttributes) throws N5Exception { writer.setDatasetAttributes(datasetPath, datasetAttributes); } @Override public DatasetAttributes getConvertedDatasetAttributes(DatasetAttributes datasetAttributes) { return writer.getConvertedDatasetAttributes(datasetAttributes); } @Override public void setVersion() throws N5Exception { writer.setVersion(); } @Override public void createGroup(String groupPath) throws N5Exception { writer.createGroup(groupPath); } @Override public boolean remove(String groupPath) throws N5Exception { return writer.remove(groupPath); } @Override public boolean remove() throws N5Exception { return writer.remove(); } @Override public DatasetAttributes createDataset(String datasetPath, DatasetAttributes datasetAttributes) throws N5Exception { DatasetAttributes convertedDatasetAttributes = getConvertedDatasetAttributes(datasetAttributes); writer.createDataset(datasetPath, convertedDatasetAttributes); return convertedDatasetAttributes; } @Override public void writeChunk(String datasetPath, DatasetAttributes datasetAttributes, DataBlock chunk) throws N5Exception { writer.writeChunk(datasetPath, getConvertedDatasetAttributes(datasetAttributes), chunk); } @Override public void writeBlock(String datasetPath, DatasetAttributes datasetAttributes, DataBlock dataBlock) throws N5Exception { writer.writeBlock(datasetPath, getConvertedDatasetAttributes(datasetAttributes), dataBlock); } @Override public boolean deleteChunk(String datasetPath, long... gridPosition) throws N5Exception { return writer.deleteChunk(datasetPath, gridPosition); } @Override public boolean deleteChunks(String datasetPath, DatasetAttributes datasetAttributes, List gridPositions) throws N5Exception { return writer.deleteChunks(datasetPath, datasetAttributes, gridPositions); } @Override public void writeSerializedBlock(Serializable object, String datasetPath, DatasetAttributes datasetAttributes, long... gridPosition) throws N5Exception { writer.writeSerializedBlock(object, datasetPath, getConvertedDatasetAttributes(datasetAttributes), gridPosition); } @Override public void setVersion(String path) { writer.setVersion(path); } @Override public void writeAttributes(String normalGroupPath, JsonElement attributes) throws N5Exception { writer.writeAttributes(normalGroupPath, attributes); } @Override public void setAttributes(String path, JsonElement attributes) throws N5Exception { writer.setAttributes(path, attributes); } @Override public void writeAttributes(String normalGroupPath, Map attributes) throws N5Exception { writer.writeAttributes(normalGroupPath, attributes); } @Override public void writeChunks(String datasetPath, DatasetAttributes datasetAttributes, DataBlock... chunks) throws N5Exception { writer.writeChunks(datasetPath, getConvertedDatasetAttributes(datasetAttributes), chunks); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/http/N5HttpTest.java ================================================ package org.janelia.saalfeldlab.n5.http; import com.google.gson.GsonBuilder; import org.janelia.saalfeldlab.n5.AbstractN5Test; import org.janelia.saalfeldlab.n5.HttpKeyValueAccess; import org.janelia.saalfeldlab.n5.N5FSWriter; import org.janelia.saalfeldlab.n5.N5KeyValueReader; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.N5Writer; import org.junit.After; import org.junit.AfterClass; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized.Parameter; import java.io.File; import java.io.IOException; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; @RunWith(RunnerWithHttpServer.class) public class N5HttpTest extends AbstractN5Test { @Parameter public static Path httpServerDirectory; @Parameter public URI httpServerURI; @Override protected String tempN5Location() { try { final File tmpFile = Files.createTempFile(httpServerDirectory, "n5-http-test-", ".n5").toFile(); assertTrue(tmpFile.delete()); return tmpFile.getName(); } catch (final IOException e) { throw new RuntimeException(e); } } private static final ArrayList tempClassWriters = new ArrayList<>(); @After @Override public void removeTempWriters() { //For HTTP, don't remove After, remove AfterClass, since we need the server to be shut down first // move the writer to a static list tempClassWriters.addAll(tempWriters); tempWriters.clear(); } @AfterClass public static void removeClassTempWriters() { for (final N5Writer writer : tempClassWriters) { try { writer.remove(); } catch (final Exception e) { } } tempClassWriters.clear(); } private static final boolean cacheMeta = true; @Override protected N5Writer createN5Writer( final String location, final GsonBuilder gson) throws IOException { final String writerFsPath = httpServerDirectory.resolve(location).toFile().getCanonicalPath(); final N5FSWriter writer = new N5FSWriter(writerFsPath, gson, cacheMeta); final N5KeyValueReader reader = (N5KeyValueReader)createN5Reader(location, gson); return new HttpReaderFsWriter(writer, reader); } @Override protected N5Reader createN5Reader( final String location, final GsonBuilder gson) { final String readerHttpPath = httpServerURI.resolve(location).toString(); return new N5KeyValueReader(new HttpKeyValueAccess(), readerHttpPath, gson, cacheMeta); } @Test @Override public void testVersion() throws NumberFormatException { try (final N5Writer writer = createTempN5Writer()) { final N5Reader.Version n5Version = writer.getVersion(); assertEquals(n5Version, N5Reader.VERSION); final N5Reader.Version incompatibleVersion = new N5Reader.Version(N5Reader.VERSION.getMajor() + 1, N5Reader.VERSION.getMinor(), N5Reader.VERSION.getPatch()); writer.setAttribute("/", N5Reader.VERSION_KEY, incompatibleVersion.toString()); final N5Reader.Version version = writer.getVersion(); assertFalse(N5Reader.VERSION.isCompatible(version)); final N5Reader.Version compatibleVersion = new N5Reader.Version(N5Reader.VERSION.getMajor(), N5Reader.VERSION.getMinor(), N5Reader.VERSION.getPatch()); writer.setAttribute("/", N5Reader.VERSION_KEY, compatibleVersion.toString()); } } @Ignore("N5Writer not supported for HTTP") @Override public void testRemoveGroup() { } @Ignore("N5Writer not supported for HTTP") @Override public void testRemoveAttributes() { } @Ignore("N5Writer not supported for HTTP") @Override public void testRemoveContainer() { } @Ignore("N5Writer not supported for HTTP") @Override public void testDelete() { } @Ignore("N5Writer not supported for HTTP") @Override public void testWriterSeparation() { } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/http/RunnerWithHttpServer.java ================================================ package org.janelia.saalfeldlab.n5.http; import org.junit.After; import org.junit.Before; import org.junit.internal.runners.statements.RunAfters; import org.junit.internal.runners.statements.RunBefores; import org.junit.runner.Description; import org.junit.runner.notification.RunNotifier; import org.junit.runners.BlockJUnit4ClassRunner; import org.junit.runners.Parameterized; import org.junit.runners.model.FrameworkField; import org.junit.runners.model.FrameworkMethod; import org.junit.runners.model.Statement; import org.junit.runners.model.TestClass; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.URI; import java.nio.file.Files; import java.nio.file.Path; import java.util.List; import java.util.concurrent.TimeUnit; public class RunnerWithHttpServer extends BlockJUnit4ClassRunner { private Process process; private final StringBuilder perTestHttpOut = new StringBuilder(); private Path httpServerDirectory; private final URI httpUri = URI.create("http://localhost:8000/"); public RunnerWithHttpServer(Class klass) throws Exception { super(klass); } private static Path createTmpServerDirectory() throws IOException { /* deleteOnExit doesn't work on temporary files, so delete it manually and recreate explicitly...*/ final Path tempDirectory = Files.createTempDirectory("n5-http-test-server-"); tempDirectory.toFile().delete(); tempDirectory.toFile().mkdirs(); tempDirectory.toFile().deleteOnExit(); return tempDirectory; } @Override protected Object createTest() throws Exception { final Object test = super.createTest(); for (FrameworkField field : getTestClass().getAnnotatedFields(Parameterized.Parameter.class)) { if (field.getType().isAssignableFrom(Path.class)) { field.getField().set(test, httpServerDirectory); } else if (field.getType().isAssignableFrom(URI.class)) { field.getField().set(test, httpUri); } } return test; } @Override protected void runChild(FrameworkMethod method, RunNotifier notifier) { if (!process.isAlive()) { logHttpOutput(); return; } Description description = describeChild(method); if (isIgnored(method)) { notifier.fireTestIgnored(description); } else { Statement statement = new Statement() { @Override public void evaluate() throws Throwable { try { methodBlock(method).evaluate(); } catch (Exception e) { if (!process.isAlive()) logHttpOutput(); throw e; } finally { perTestHttpOut.setLength(0); } } }; runLeaf(statement, description, notifier); } } private void logHttpOutput() { if (perTestHttpOut.length() > 0) { perTestHttpOut.insert(0, "Last HTTP Server Output.\n"); perTestHttpOut.insert(0, "Http Server is not alive.\n"); System.err.println(perTestHttpOut); } } @Before public void startHttpServer() throws Exception { httpServerDirectory = createTmpServerDirectory(); final String osName = System.getProperty("os.name"); final String pythonBinary = "Mac OS X".equals(osName) ? "python3" : "python"; ProcessBuilder processBuilder = new ProcessBuilder(pythonBinary, "-m", "http.server"); processBuilder.directory(httpServerDirectory.toFile()); processBuilder.redirectErrorStream(true); process = processBuilder.start(); waitForHttpReady(); /* give the server some time to finish startup */ final Thread clearStdout = new Thread(() -> { try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) { String line; while ((line = reader.readLine()) != null) { perTestHttpOut.append(line).append("\n"); } } catch (IOException e) { throw new RuntimeException(e); } }); clearStdout.setDaemon(true); clearStdout.start(); } private void waitForHttpReady() throws IOException, InterruptedException { final Thread waitForConnect = new Thread(() -> { while (true) { try { httpUri.toURL().openConnection().connect(); return; } catch (Exception e) { //ignore } } }); waitForConnect.start(); waitForConnect.join(10_000); httpUri.toURL().openConnection().connect(); } @After public void stopHttpServer() { process.destroy(); try { process.waitFor(1, TimeUnit.SECONDS); } catch (InterruptedException e) { process.destroyForcibly(); } } @Override protected Statement withBeforeClasses(Statement statement) { final Statement testClassBefore = super.withBeforeClasses(statement); final List beforeTestClass = new TestClass(RunnerWithHttpServer.class).getAnnotatedMethods(Before.class); return new RunBefores(testClassBefore, beforeTestClass, this); } @Override protected Statement withAfterClasses(Statement statement) { final List afterTestClass = new TestClass(RunnerWithHttpServer.class).getAnnotatedMethods(After.class); final RunAfters runnerAfterClass = new RunAfters(statement, afterTestClass, this); return super.withAfterClasses(runnerAfterClass); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/kva/AbstractKeyValueAccessTest.java ================================================ package org.janelia.saalfeldlab.n5.kva; import org.janelia.saalfeldlab.n5.KeyValueAccess; import org.janelia.saalfeldlab.n5.N5URI; import org.junit.Test; import java.net.URI; import java.util.LinkedHashSet; import java.util.Set; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; /** * @author Stephan Saalfeld <saalfelds@janelia.hhmi.org> */ public abstract class AbstractKeyValueAccessTest { protected abstract KeyValueAccess newKeyValueAccess(URI root); protected KeyValueAccess newKeyValueAccess() { return newKeyValueAccess(tempUri()); } protected abstract URI tempUri(); protected URI[] testURIs(final URI base) { final Set testUris = new LinkedHashSet<>(); /*add the base uri as a test case */ testUris.add(base); /* NOTE: Java 8 doesn't behave well with URIs with empty path when resolving against a path. * See KeyValueAccess#compose for more details. * In tests with that as a base URI, resolve against `/` first. * Should be unnecessary in Java 21*/ final URI testUri = base.getPath().isEmpty() ? base.resolve("/") : base; final URI[] pathParts = new URI[]{ N5URI.getAsUri("test/path/file"), // typical path, with no leading or trailing slashes N5URI.getAsUri("test/path/file/"), // typical path, with trailing slash N5URI.getAsUri("/test/path/file"), // typical path, with leading slash N5URI.getAsUri("/test/path/file/"), // typical path, with leading and trailing slash N5URI.getAsUri("file"), // single path N5URI.getAsUri("file/"), // single path N5URI.getAsUri("/file"), // single path N5URI.getAsUri("/file/"), // single path N5URI.getAsUri("path/w i t h/spaces"), N5URI.getAsUri("uri/illegal%character"), N5URI.getAsUri("/"), // root path N5URI.getAsUri("") // empty path }; for (final URI pathPart : pathParts) { testUris.add(testUri.resolve(pathPart)); } return testUris.toArray(new URI[0]); } protected String[][] testPathComponents(final URI base) { final URI[] testPaths = testURIs(base); final String[][] expectedComponents = new String[testPaths.length][]; for (int i = 0; i < testPaths.length; ++i) { final URI testUri = testPaths[i]; final String uriPath = testUri.getPath(); expectedComponents[i] = uriPath.split("/"); if (uriPath.startsWith("/")) { //We always expect the first path to be forward slash if it's absolute if (expectedComponents[i].length == 0) expectedComponents[i] = new String[1]; expectedComponents[i][0] = "/"; } if (uriPath.endsWith("/")) { final int lastCompIdx = expectedComponents[i].length - 1; final String lastComponent = expectedComponents[i][lastCompIdx]; if (!lastComponent.endsWith("/")) { expectedComponents[i][lastCompIdx] = lastComponent + "/"; } } } return expectedComponents; } protected void testComponentsAtLocation(URI testRoot) { final KeyValueAccess access = newKeyValueAccess(); final URI[] testPaths = testURIs(testRoot); final String[][] expectedComponents = testPathComponents(testRoot); for (int i = 0; i < testPaths.length; ++i) { final String[] components = access.components(testPaths[i].getPath()); assertArrayEquals("Failure at Index " + i, expectedComponents[i], components); } } protected void testComposeAtLocation(URI uri) { final KeyValueAccess access = newKeyValueAccess(); /* remove any path information to get the base URI without path. */ final URI[] testUris = testURIs(uri); final String[][] testPathComponents = testPathComponents(uri); for (int i = 0; i < testPathComponents.length; ++i) { testPathComponents[i] = testPathComponents[i].clone(); /* Don't add the "/" if the input uri path is empty. just use it. Otherwise, remove the parts and start with "/" */ final URI baseUri = uri.getPath().isEmpty() ? uri : testUris[i].resolve("/"); final String[] components = testPathComponents[i]; final String actualCompose = access.compose(baseUri, components); final String expectedCompose = access.compose(testUris[i]); assertEquals("Failure at Index " + i, expectedCompose, actualCompose); } } @Test public void testComponents() { testComponentsAtLocation(tempUri()); } @Test public void testComponentsWithPathSlash() { final URI uriWithPathSlash = setUriPath(tempUri(), "/"); testComponentsAtLocation(uriWithPathSlash); } @Test public void testComponentsWithPathEmpty() { final URI uriWithPathEmpty = setUriPath(tempUri(), ""); testComponentsAtLocation(uriWithPathEmpty); } @Test public void testCompose() { final URI uri = tempUri(); testComposeAtLocation(uri); final KeyValueAccess kva = newKeyValueAccess(); final URI uriWithPath = setUriPath(uri, "/foo"); assertEquals("Non-empty Path", "/foo", uriWithPath.getPath()); assertComposeEquals("Non-empty Path, no empty or slash in components", kva, uriWithPath, "/foo/bar/baz", "bar", "baz"); assertComposeEquals("Non-empty Path, first components leading slash", kva, uriWithPath, "/bar/baz", "/bar", "baz"); assertComposeEquals("Non-empty Path, first components slash only", kva, uriWithPath,"/bar/baz", "/", "bar", "baz"); assertComposeEquals("Non-empty Path, first components slash only", kva, uriWithPath,"/foo/bar/baz", "", "bar", "baz"); assertComposeEquals("Non-empty Path, first components slash only", kva, uriWithPath,"/bar/baz", "", "/bar", "baz"); assertComposeEquals("Non-empty Path, null and empty inner components", kva, uriWithPath,"/foo/bar/baz", "bar", null, "", "baz"); assertComposeEquals("Non-empty Path, null and empty inner components", kva, uriWithPath,"/bar/baz", "/bar", null, "", "baz"); } @Test public void testComposeWithPathSlash() { final URI uriWithSlashRoot = setUriPath(tempUri(), "/"); assertEquals("Root (/) Path", "/", uriWithSlashRoot.getPath()); testComposeAtLocation(uriWithSlashRoot); final KeyValueAccess kva = newKeyValueAccess(); assertComposeEquals("Root (/) Path, no empty or slash in components", kva, uriWithSlashRoot, "/bar/baz", "bar", "baz"); assertComposeEquals("Root (/) Path, first components leading slash", kva, uriWithSlashRoot, "/bar/baz", "/bar", "baz"); assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithSlashRoot,"/bar/baz", "/", "bar", "baz"); assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithSlashRoot,"/bar/baz", "", "bar", "baz"); assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithSlashRoot,"/bar/baz", "", "/bar", "baz"); assertComposeEquals("Root (/) Path, null and empty inner components", kva, uriWithSlashRoot,"/bar/baz", "bar", null, "", "baz"); assertComposeEquals("Root (/) Path, null and empty inner components", kva, uriWithSlashRoot,"/bar/baz", "/bar", null, "", "baz"); } @Test public void testComposeWithPathEmpty() { final URI uriWithEmptyRoot = setUriPath(tempUri(), ""); assertEquals("Empty Path", "", uriWithEmptyRoot.getPath()); testComposeAtLocation(uriWithEmptyRoot); final KeyValueAccess kva = newKeyValueAccess(); assertComposeEquals("Root (/) Path, no empty or slash in components", kva, uriWithEmptyRoot, "/bar/baz", "bar", "baz"); assertComposeEquals("Root (/) Path, first components leading slash", kva, uriWithEmptyRoot, "/bar/baz", "/bar", "baz"); assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithEmptyRoot,"/bar/baz", "/", "bar", "baz"); assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithEmptyRoot,"/bar/baz", "", "bar", "baz"); assertComposeEquals("Root (/) Path, first components slash only", kva, uriWithEmptyRoot,"/bar/baz", "", "/bar", "baz"); assertComposeEquals("Root (/) Path, null and empty inner components", kva, uriWithEmptyRoot,"/bar/baz", "bar", null, "", "baz"); assertComposeEquals("Root (/) Path, null and empty inner components", kva, uriWithEmptyRoot,"/bar/baz", "/bar", null, "", "baz"); } public void assertComposeEquals(String reason, KeyValueAccess kva, URI uri, String absoluteExpectedPath, String... components) { String actual = kva.compose(uri, components); String expected = kva.compose(uri.resolve(absoluteExpectedPath)); assertEquals(reason, expected, actual); } public URI setUriPath(final URI uri, final String path) { final URI tempUri = uri.resolve("/"); final String newUri = tempUri.toString().replaceAll(tempUri.getPath() + "$", path); final URI uriWithNewPath = N5URI.getAsUri(newUri); assertEquals("setUriPath failed", path, uriWithNewPath.getPath()); return uriWithNewPath; } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/kva/DelegateKeyValueAccess.java ================================================ package org.janelia.saalfeldlab.n5.kva; import org.janelia.saalfeldlab.n5.KeyValueAccess; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import java.net.URI; import java.net.URISyntaxException; public class DelegateKeyValueAccess implements KeyValueAccess { protected final KeyValueAccess kva; public DelegateKeyValueAccess(KeyValueAccess kva) { this.kva = kva; } @Override public String[] components(String path) { return kva.components(path); } @Override public String compose(URI uri, String... components) { return kva.compose(uri, components); } @Override public String compose(String... components) { return kva.compose(components); } @Override public String parent(String path) { return kva.parent(path); } @Override public String relativize(String path, String base) { return kva.relativize(path, base); } @Override public String normalize(String path) { return kva.normalize(path); } @Override public URI uri(String uriString) throws URISyntaxException { return kva.uri(uriString); } @Override public boolean exists(String normalPath) { return kva.exists(normalPath); } @Override public long size(String normalPath) throws N5Exception.N5NoSuchKeyException { return kva.size(normalPath); } @Override public boolean isDirectory(String normalPath) { return kva.isDirectory(normalPath); } @Override public boolean isFile(String normalPath) { return kva.isFile(normalPath); } @Override public VolatileReadData createReadData(String normalPath) throws N5Exception.N5IOException { return kva.createReadData(normalPath); } @Override public void write(String normalPath, ReadData data) throws N5Exception.N5IOException { kva.write( normalPath, data); } @Override public String[] listDirectories(String normalPath) throws N5Exception.N5IOException { return kva.listDirectories(normalPath); } @Override public String[] list(String normalPath) throws N5Exception.N5IOException { return kva.list(normalPath); } @Override public void createDirectories(String normalPath) throws N5Exception.N5IOException { kva.createDirectories(normalPath); } @Override public void delete(String normalPath) throws N5Exception.N5IOException { kva.delete(normalPath); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/kva/FileSystemKeyValueAccessTest.java ================================================ package org.janelia.saalfeldlab.n5.kva; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import java.io.File; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.janelia.saalfeldlab.n5.FileSystemKeyValueAccess; import org.janelia.saalfeldlab.n5.KeyValueAccess; import org.janelia.saalfeldlab.n5.N5URI; import org.junit.Ignore; import org.junit.Test; /** * @author Stephan Saalfeld <saalfelds@janelia.hhmi.org> * */ public class FileSystemKeyValueAccessTest extends AbstractKeyValueAccessTest { /* Weird, but consistent on linux and windows */ private static URI root = Paths.get(Paths.get("/").toUri()).toUri(); private static String separator = FileSystems.getDefault().getSeparator(); private static final FileSystemKeyValueAccess fileSystemKva = new FileSystemKeyValueAccess(); @Override protected KeyValueAccess newKeyValueAccess(URI root) { return fileSystemKva; } @Override protected KeyValueAccess newKeyValueAccess() { return fileSystemKva; } @Override protected URI[] testURIs(URI base) { final URI[] testUris = super.testURIs(base); final URI[] addRelativeUris = new URI[testUris.length * 3]; for (int i = 0; i < testUris.length; i++) { URI testUri = testUris[i]; addRelativeUris[i * 3] = testUri; Path asPath = Paths.get(testUri); final URI schemeLess = asPath.toUri(); addRelativeUris[i * 3 + 1] = schemeLess; URI relativeUri = N5URI.encodeAsUriPath(testUri.getPath().replaceAll("^"+base.getPath(), "")); addRelativeUris[i * 3 + 2] = relativeUri; } return addRelativeUris; } @Override protected void testComponentsAtLocation(URI testRoot) { final KeyValueAccess access = newKeyValueAccess(); final URI[] testPaths = testURIs(testRoot); final String[][] expectedComponents = testPathComponents(testRoot); for (int i = 0; i < testPaths.length; ++i) { String pathString; if (testPaths[i].isAbsolute()) pathString = Paths.get(testPaths[i]).toString(); else pathString = Paths.get(testPaths[i].getPath()).toString(); if (pathString.length() > 1 && testPaths[i].toString().endsWith("/")) pathString += "/"; final String[] components = access.components(pathString); assertArrayEquals("Failure at Index " + i ,expectedComponents[i], components); } } private Path getPathFromFileURI(URI fileUri) { try { return new File(fileUri).toPath(); } catch (Exception ignore) { } try { return new File(fileUri.getPath()).toPath(); } catch (Exception ignore) { } throw new IllegalArgumentException("Unable to get Path for URI: " + fileUri); } @Override protected String[][] testPathComponents(URI base) { final URI[] testPaths = testURIs(base); final String[][] expectedComponents = new String[testPaths.length][]; for (int i = 0; i < testPaths.length; ++i) { final URI testUri = testPaths[i]; final String testPathStr = testUri.getPath(); final Path testPath = getPathFromFileURI(testUri); final int numComponents = (testPath.getRoot() != null ? 1 : 0) + testPath.getNameCount(); final String[] components = new String[numComponents]; int cIdx = 0; if (testPath.getRoot() != null) components[cIdx++] = testPath.getRoot().toString(); for (int nameIdx = 0; nameIdx < testPath.getNameCount(); nameIdx++) { components[cIdx++] = testPath.getName(nameIdx).toString(); } if (components.length > 0 && (testPath.getRoot()==null || !components[components.length - 1].equals(testPath.getRoot().toString())) && testPathStr.endsWith("/")) { final int lastCompIdx = components.length - 1; final String lastComponent = components[lastCompIdx]; if (!lastComponent.endsWith("/")) { components[lastCompIdx] = lastComponent + "/"; } } expectedComponents[i] = components; } return expectedComponents; } @Override protected URI tempUri() { try { final Path tempDirectory = Files.createTempDirectory("n5-filesystem-kva-test-"); final File tmpDir = tempDirectory.toFile(); tmpDir.delete(); tmpDir.mkdir(); //DeleteOnExit doesn't work on temp directory... so we delete and make it explicitly. tmpDir.deleteOnExit(); return tempDirectory.toUri() ; } catch (IOException e) { throw new RuntimeException(e); } } protected void testComposeAtLocation(URI uri) { final KeyValueAccess access = newKeyValueAccess(); /* remove any path information to get the base URI without path. */ final URI[] testUris = testURIs(uri); final String[][] testPathComponents = testPathComponents(uri); for (int i = 0; i < testPathComponents.length; ++i) { final URI baseUri = testUris[i].resolve("/"); final String[] components = testPathComponents[i]; final String composedKey = access.compose(baseUri, components); final URI absoluteUri = testUris[i].isAbsolute() ? testUris[i] : uri.resolve("/").resolve(testUris[i]); final String testPath = FileSystems.getDefault().provider().getPath(absoluteUri).toAbsolutePath().toString(); assertEquals("Failure at Index " + i , testPath, composedKey); } } @Override @Test @Ignore("Empty path is invalid for file URIs.") public void testComponentsWithPathEmpty() { /* file URIs are purely paths (optional file: scheme) so empty path resolves to a relative path (not the root of the container). * Because of that, there is no valid file URI with an empty path (it's just an empty string, which is invalid, or `file://` which is invalid. */ super.testComponentsWithPathEmpty(); } @Override @Test @Ignore("Empty path is invalid for file URIs.") public void testComposeWithPathEmpty() { /* file URIs are purely paths (optional file: scheme) so empty path resolves to a relative path (not the root of the container). * Because of that, there is no valid file URI with an empty path (it's just an empty string, which is invalid, or `file://` which is invalid. */ super.testComposeWithPathEmpty(); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/kva/FsLockingValidation.java ================================================ package org.janelia.saalfeldlab.n5.kva; import java.util.Arrays; import java.util.Random; import java.util.concurrent.Callable; import org.janelia.saalfeldlab.n5.FileSystemKeyValueAccess; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import picocli.CommandLine; import picocli.CommandLine.Command; import picocli.CommandLine.Option; @Command( name = "FsLockingValidation", mixinStandardHelpOptions = true, description = "Two-process test for FileLock race conditions.") public class FsLockingValidation implements Callable { @Option(names = {"--file", "-f"}, required = true, description = "Path of the file used for the locking test.") String file; @Option(names = {"--data-size"}, description = "Bytes written per repeat (default: ${DEFAULT-VALUE}).") int dataSize = 65_536; @Option(names = {"--num-repeats"}, description = "Number of write/read cycles (default: ${DEFAULT-VALUE}).") int numRepeats = 1000; // Amount of sleep time after getting file size, but before attempting to actually read @Option(names = {"--sleep", "-s"}, description = "ms writer sleeps after TRUNCATE_EXISTING and before lock() (default: ${DEFAULT-VALUE}).") long sleepMs = 0; static Random random = new Random(); static final int id = random.nextInt(9999); public static void main(String[] args) { System.exit(new CommandLine(new FsLockingValidation()).execute(args)); } @Override public Integer call() throws Exception { final FileSystemKeyValueAccess kva = new FileSystemKeyValueAccess(); // Seed the file so it exists and has the expected size before any loop iteration. final byte[] seed = new byte[dataSize]; Arrays.fill(seed, (byte) 0x11); kva.write(file, ReadData.from(seed)); // Size of the "index" slice read from the end of the file, analogous to // RawShardCodec.decode() reading the shard index at indexOffset = requireLength() - indexBlockSizeInBytes. final long indexSliceSize = Math.max(1, dataSize / 8); int errors = 0; for (int i = 0; i < numRepeats; i++) { try { ReadData modifiedReadData = null; try (VolatileReadData vrd = kva.createReadData(file)) { // 1. slice the vrd for a subset near the end — mirrors the index read in // RawShardCodec.decode(). requireLength() is where Files.size() is called // and where a negative offset would be computed if the file was truncated // by a concurrent writer before its exclusive lock was acquired. final long totalLength = vrd.requireLength(); final long sliceOffset = totalLength - indexSliceSize; Thread.sleep(sleepMs); vrd.slice(sliceOffset, indexSliceSize).materialize(); // 2. create new ReadData of dataSize, as if we merged the existing // shard with new blocks and are ready to write the result. final byte[] newData = new byte[dataSize]; Arrays.fill(newData, (byte) (i & 0xFF)); // 3. set modifiedReadData modifiedReadData = ReadData.from(newData); modifiedReadData.materialize(); } kva.write(file, modifiedReadData); } catch (Exception e) { errors++; System.err.printf("[id %d] ERROR at repeat %d: %s%n", id, i, e.getMessage()); } } return errors > 0 ? 1 : 0; } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/kva/HttpKeyValueAccessTest.java ================================================ package org.janelia.saalfeldlab.n5.kva; import org.janelia.saalfeldlab.n5.AbstractN5Test; import org.janelia.saalfeldlab.n5.HttpKeyValueAccess; import org.janelia.saalfeldlab.n5.KeyValueAccess; import org.janelia.saalfeldlab.n5.http.RunnerWithHttpServer; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import java.net.URI; import java.nio.file.Path; /** * @author Stephan Saalfeld <saalfelds@janelia.hhmi.org> * */ @RunWith(RunnerWithHttpServer.class) public class HttpKeyValueAccessTest extends AbstractKeyValueAccessTest { @Parameterized.Parameter public static Path httpServerDirectory; @Parameterized.Parameter public URI httpServerURI; private static final HttpKeyValueAccess httpKva = new HttpKeyValueAccess(); @Override protected KeyValueAccess newKeyValueAccess(URI root) { return httpKva; } @Override protected KeyValueAccess newKeyValueAccess() { return httpKva; } @Override protected URI tempUri() { final URI tmpUri = AbstractN5Test.createTempUri("n5-http-kva-test-", null, httpServerURI); return tmpUri; } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/kva/TrackingKeyValueAccess.java ================================================ package org.janelia.saalfeldlab.n5.kva; import org.janelia.saalfeldlab.n5.KeyValueAccess; import org.janelia.saalfeldlab.n5.N5Exception; import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import org.janelia.saalfeldlab.n5.readdata.prefetch.AggregatingPrefetchLazyRead; public class TrackingKeyValueAccess extends DelegateKeyValueAccess { public int numMaterializeCalls = 0; public int numIsFileCalls = 0; public long totalBytesRead = 0; public boolean aggregate = false; public TrackingKeyValueAccess(final KeyValueAccess kva) { super(kva); } @Override public boolean isFile(String normalPath) { numIsFileCalls++; return kva.isFile(normalPath); } @Override public VolatileReadData createReadData(final String normalPath) { final VolatileReadData volatileReadData = kva.createReadData(normalPath); final TrackingLazyRead trackingLazyRead = new TrackingLazyRead(volatileReadData); LazyRead lazyRead = trackingLazyRead; if (aggregate) lazyRead = new AggregatingPrefetchLazyRead(trackingLazyRead); return VolatileReadData.from( lazyRead ); } private class TrackingLazyRead implements LazyRead { private final VolatileReadData readData; TrackingLazyRead(final VolatileReadData readData) { this.readData = readData; } @Override public long size() throws N5Exception.N5IOException { return readData.requireLength(); } @Override public ReadData materialize(final long offset, final long length) { numMaterializeCalls++; return readData.slice(offset, length).materialize(); } @Override public void close() { readData.close(); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/locking/JustFileChannels.java ================================================ package org.janelia.saalfeldlab.n5.locking; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.nio.channels.OverlappingFileLockException; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Random; import static java.nio.file.StandardOpenOption.CREATE; import static java.nio.file.StandardOpenOption.READ; import static java.nio.file.StandardOpenOption.WRITE; public class JustFileChannels { // Idea: // // Writers write random valid files. // // Readers verify that a file is valid. // Verifying that a file is valid should take multiple reads, to make it // similar to chunk reading. // // Valid file has an "index" at the end. // The index is int[2*N] where N is predefined. The index comprises // consecutive pairs (offset, value) where `offset` is a byte offset in the // file and `value` is the in value that should be there. // // Writer creates int[random_length + 2*N] and fills the first // random_length ints with random data. It then draws N random indices into // that data, looks up those values in the data, and creates an index in // the final 2*N ints. // // To verify, the reader // 1. gets the channel size // 2. reads the index // 3. reads N 4-byte slices to verify that the file has the expected values // at the expected indices. // static final int minDataSize = 1024; static final int maxDataSize = 1024 * 1024; static final int indexPairs = 100; static void write(String path, boolean doLock, final Random random) { // final long id = Thread.currentThread().getId(); // System.out.println("write ("+id+")"); try { // NB: not creating any parent directories for now final Path p = Paths.get(path); final FileChannel channel = FileChannel.open(p, READ, WRITE, CREATE); FileLock lock = null; if (doLock) lock = tryLockWait(channel, false); channel.truncate(0); final int n = minDataSize + random.nextInt(maxDataSize - minDataSize); final int[] content = new int[n + 2 * indexPairs]; for (int i = 0; i < n; i++) { content[i] = random.nextInt(); } for (int i = n; i < n + 2 * indexPairs; i+=2) { final int offset = random.nextInt(n); content[i] = offset; content[i+1] = content[offset]; } final int capacity = 4 * content.length; ByteBuffer buffer = ByteBuffer.allocate(capacity); buffer.asIntBuffer().put(content); if (channel.write(buffer) != capacity) throw new RuntimeException("write failed"); if (lock != null) lock.release(); channel.close(); } catch (IOException e) { throw new RuntimeException(e); } } static FileLock tryLockWait(FileChannel channel, boolean shared) throws IOException { int i = 0; while (i < 9999) { try { return channel.lock(0, Long.MAX_VALUE, shared); } catch (final OverlappingFileLockException e) { try { Thread.sleep(100); } catch (final InterruptedException ie) { Thread.currentThread().interrupt(); throw new IOException("Interrupted while waiting for file lock", ie); } } i++; } throw new IOException("Could not get a lock"); } // throws RuntimeException if file is not valid static void verify(String path, boolean doLock) { try { final Path p = Paths.get(path); final FileChannel channel = FileChannel.open(p, READ); FileLock lock = null; if (doLock) lock = tryLockWait(channel, true); final long size = channel.size(); final int[] index = new int[2 * indexPairs]; ByteBuffer buffer = ByteBuffer.allocate(index.length * 4); channel.read(buffer, size - 4 * index.length); buffer.position(0); buffer.asIntBuffer().get(index); for (int i = 0; i < indexPairs; i++) { final int offset = index[2 * i]; final int expected = index[2 * i + 1]; buffer = ByteBuffer.allocate(4); channel.read(buffer, offset * 4); buffer.position(0); final int actual = buffer.asIntBuffer().get(0); if (actual != expected) throw new RuntimeException("verify failed"); } if (lock != null) lock.release(); channel.close(); } catch (IOException e) { throw new RuntimeException(e); } } public static void main(String[] args) throws InterruptedException { // Repeatedly calls write, then verify using the file at path. // Sleeps for sleepTime(default=0) ms in between the write and verify calls. final Random random = new Random(); final String path = args[0]; final int N = Integer.parseInt(args[1]); long sleepTime = 0; if( args.length > 2) { if( args[2].startsWith("rand")) sleepTime = -1; else sleepTime = Long.parseLong(args[2]); } System.out.println("sleep Time: " + ((sleepTime < 0 ) ? "random" : sleepTime)); boolean doLock = true; if( args.length > 3) { doLock = args[3].equals("lock"); } System.out.println("do lock: " + doLock); for( int i = 0; i < N; i++ ) { write(path, doLock, random); if (sleepTime < 0) Thread.sleep(random.nextInt(200)); else Thread.sleep(sleepTime); verify(path, doLock); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/locking/JustFileChannelsThreaded.java ================================================ package org.janelia.saalfeldlab.n5.locking; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; public class JustFileChannelsThreaded { public static void main(String[] args) throws InterruptedException { final int nThreads = Integer.parseInt(args[0]); final String[] subArgs = new String[args.length - 1]; System.arraycopy(args, 1, subArgs, 0, args.length - 1); final ExecutorService exec = Executors.newFixedThreadPool(nThreads); for( int i = 0; i < nThreads; i++ ) { exec.submit( () -> { try { Thread.sleep(200); JustFileChannels.main(subArgs); } catch (Exception e) { e.printStackTrace(); } }); } exec.awaitTermination(5, TimeUnit.MINUTES); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/readdata/RangeTests.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertSame; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.Test; public class RangeTests { @Test public void testAggregate() { List nonOverlapping = Stream.of(Range.at(0, 5), Range.at(12, 2)).collect(Collectors.toList()); assertSame(nonOverlapping, Range.aggregate(nonOverlapping)); List nonOverlappingRev = Stream.of(Range.at(12, 2), Range.at(0, 5)).collect(Collectors.toList()); assertSame(nonOverlappingRev, Range.aggregate(nonOverlappingRev)); /** * 0 1 2 3 4 5 * x x x x x - * - x - - - - */ List containing = Stream.of(Range.at(0, 5), Range.at(1, 1)).collect(Collectors.toList()); assertEquals(Collections.singletonList(Range.at(0, 5)), Range.aggregate(containing)); /** * 0 1 2 3 4 5 6 * x x x x x - - * - - x x x x x */ List overlapping = Stream.of(Range.at(0, 5), Range.at(2, 5)).collect(Collectors.toList()); assertEquals(Collections.singletonList(Range.at(0, 7)), Range.aggregate(overlapping)); /** * 0 1 2 3 4 5 * x x x x x - * - - - - - x */ List adjacent = Stream.of(Range.at(0, 5), Range.at(5, 1)).collect(Collectors.toList()); assertEquals(Collections.singletonList(Range.at(0, 6)), Range.aggregate(adjacent)); /** * 0 1 2 3 4 5 * - - - x x - * x x - - - - * - - - - - x */ List three = Stream.of(Range.at(3, 2), Range.at(0, 2), Range.at(5, 1)).collect(Collectors.toList()); assertEquals(Stream.of(Range.at(0, 2), Range.at(3, 3)).collect(Collectors.toList()), Range.aggregate(three)); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/readdata/ReadDataTests.java ================================================ package org.janelia.saalfeldlab.n5.readdata; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Arrays; import java.util.function.IntUnaryOperator; import org.apache.commons.compress.utils.IOUtils; import org.janelia.saalfeldlab.n5.FileSystemKeyValueAccess; import org.janelia.saalfeldlab.n5.readdata.ReadData.OutputStreamOperator; import org.junit.Test; public class ReadDataTests { @Test public void testLazyReadData() throws IOException { final int N = 128; byte[] data = new byte[N]; for( int i = 0; i < N; i++ ) data[i] = (byte)i; final ReadData readData = ReadData.from(out -> { out.write(data); }); assertTrue(readData instanceof LazyGeneratedReadData); readDataTestHelper(readData, -1, N); sliceTestHelper(readData, N); } @Test public void testByteArrayReadData() throws IOException { final int N = 128; byte[] data = new byte[N]; for( int i = 0; i < N; i++ ) data[i] = (byte)i; ReadData readData = ReadData.from(data).materialize(); assertTrue(readData instanceof ByteArrayReadData); readDataTestHelper(readData, N, N); readDataTestEncodeHelper(readData, N); sliceTestHelper(readData, N); } @Test public void testInputStreamReadData() throws IOException { final int N = 128; final InputStream is = new InputStream() { int val = 0; @Override public int read() throws IOException { return val++; } }; final ReadData readData = ReadData.from(is, N); readDataTestHelper(readData, N, N); sliceTestHelper(readData, N); } @Test public void testFileKvaReadData() throws IOException { int N = 128; byte[] data = new byte[N]; for( int i = 0; i < N; i++ ) data[i] = (byte)i; final File tmpF = File.createTempFile("test-file-splittable-data", ".bin"); tmpF.deleteOnExit(); try (FileOutputStream os = new FileOutputStream(tmpF)) { os.write(data); } try( final VolatileReadData readData = new FileSystemKeyValueAccess() .createReadData(tmpF.getAbsolutePath())) { assertEquals("file read data length", -1, readData.length()); assertEquals("file read data length", 128, readData.requireLength()); sliceTestHelper(readData, N); } } private void readDataTestHelper( ReadData readData, int N, int materializedN ) throws IOException { assertEquals("full length", N, readData.length()); assertEquals("full length after materialize", materializedN, readData.materialize().length()); } private void readDataTestEncodeHelper( ReadData readData, int N ) throws IOException { final byte[] origCopy = new byte[N]; IOUtils.readFully(readData.inputStream(), origCopy); final byte[] expected = Arrays.copyOf(origCopy, N); for( int i = 0; i < expected.length; i++) expected[i]+=2; final ReadData encoded = readData.encode(new ByteFun(x -> x+2)); assertArrayEquals(expected, encoded.allBytes()); final ReadData encodedTwice = encoded.encode(new ByteFun(x -> x-2)); assertArrayEquals(origCopy, encodedTwice.allBytes()); } private void sliceTestHelper( ReadData readData, int N ) throws IOException { assertEquals("length one", 1, readData.slice(9, 1).length()); assertEquals("split length zero", 0, readData.slice(9, 0).length()); assertEquals("split length zero allBytes", 0, readData.slice(9, 0).allBytes().length); ReadData limited = readData.limit(2); assertEquals(2, limited.length()); ReadData unboundedLength = readData.slice(1, -1); assertEquals("unbounded length allBytes", N - 1, unboundedLength.allBytes().length); // slice may throw an exception if it knows its length and can detect out-of-bounds // otherwise the exception may be thrown on a read operation (e.g. allBytes) assertThrows("Out-of-range slice read", IndexOutOfBoundsException.class, () -> readData.slice(N-1, 3).allBytes()); assertThrows("slice throws if offset too large", IndexOutOfBoundsException.class, () -> readData.slice(N, 0).allBytes()); assertThrows("too large offset slice read", IndexOutOfBoundsException.class, () -> readData.slice(N-1, 3).allBytes()); assertThrows("negative offset", IndexOutOfBoundsException.class, () -> readData.slice(-1, 1)); } private static class ByteFun implements OutputStreamOperator { IntUnaryOperator fun; public ByteFun(IntUnaryOperator fun) { this.fun = fun; } @Override public OutputStream apply(OutputStream o) throws IOException { return new OutputStream() { @Override public void write(int b) throws IOException { o.write(fun.applyAsInt(b)); } }; } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/readdata/prefetch/SliceTrackingLazyReadTests.java ================================================ package org.janelia.saalfeldlab.n5.readdata.prefetch; import static org.junit.Assert.assertEquals; import java.io.IOException; import java.util.Arrays; import java.util.List; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.LazyRead; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.junit.Test; public class SliceTrackingLazyReadTests { @Test public void testDefaultSliceTracking() throws N5IOException { /** * 1. Create sample ReadData from byte[] * 2. Create a DummyLazy Read * 3. Make a DefaultSliceTrackingLazyRead * 4. Create a list of ranges * 5. Call prefetch * 6. Ensure the correct number of materialize calls were made (always 1 for DefaultSliceTrackingLazyRead) * 7. Verify the stored slices contain the correct range */ // 1-2. Create a DummyLazyRead with 64 bytes DummyLazyRead dummyLazyRead = createDummyLazyRead(64); // 3. Make a testable DefaultSliceTrackingLazyRead TestableDefaultSliceTracker sliceTracking = new TestableDefaultSliceTracker(dummyLazyRead); // 4. Create a list of ranges (two non-overlapping ranges with a gap) List ranges = Arrays.asList( Range.at(10, 5), // offset 10, length 5 (bytes 10-14) Range.at(50, 10) // offset 50, length 10 (bytes 50-59) ); // 5. Call prefetch sliceTracking.prefetch(ranges); // 6. Ensure exactly 1 materialize call was made // DefaultSliceTrackingLazyRead creates a single large slice covering all ranges assertEquals("DefaultSliceTrackingLazyRead should make exactly 1 materialize call", 1, dummyLazyRead.getNumMaterializeCalls()); // 7. Verify the stored slice covers the entire range from offset 10 with length 50 assertStoredSlices(sliceTracking, Arrays.asList( Range.at(10, 50) // Single slice covering offset 10-59 )); } @Test public void testAggregatingSliceTracking() throws N5IOException { /** * 1. Create sample ReadData from byte[] * 2. Create a DummyLazyRead * 3. Make an AggregatingSliceTrackingLazyRead * 4. Create a list of ranges * 5. Call prefetch * 6. Ensure the correct number of materialize calls were made (one per aggregated range) * 7. Verify the stored slices contain the correct ranges */ // 1-2. Create a DummyLazyRead with 64 bytes DummyLazyRead dummyLazyRead = createDummyLazyRead(64); // 3. Make a testable AggregatingSliceTrackingLazyRead TestableAggregatingSliceTracker sliceTracking = new TestableAggregatingSliceTracker(dummyLazyRead); /* * Non-adjacent ranges */ // 4. Create a list of ranges (two non-overlapping ranges with a gap) List ranges = Arrays.asList( Range.at(10, 5), // offset 10, length 5 (bytes 10-14) Range.at(50, 10) // offset 50, length 10 (bytes 50-59) ); // 5. Call prefetch sliceTracking.prefetch(ranges); // 6. Ensure exactly 2 materialize calls were made // AggregatingSliceTrackingLazyRead aggregates overlapping/adjacent ranges // Since these ranges are not adjacent or overlapping, it makes 2 separate calls assertEquals("AggregatingSliceTrackingLazyRead should make 2 materialize calls for non-adjacent ranges", 2, dummyLazyRead.getNumMaterializeCalls()); // 7. Verify the stored slices contain two separate ranges assertStoredSlices(sliceTracking, Arrays.asList( Range.at(10, 5), // First slice Range.at(50, 10) // Second slice )); /* * Adjacent ranges */ // new sliceTracking instance to clear slices sliceTracking = new TestableAggregatingSliceTracker(dummyLazyRead); dummyLazyRead.resetNumMaterializeCalls(); // 4. Create a list of three contiguous ranges List adjacentRanges = Arrays.asList( Range.at(10, 5), // offset 10, length 5 (bytes 10-14) Range.at(15, 10), // offset 15, length 10 (bytes 15-24) Range.at(25, 5) // offset 25, length 5 (bytes 25-29) ); // 5. Call prefetch sliceTracking.prefetch(adjacentRanges); // 6. Ensure exactly 1 materialize call was made // AggregatingSliceTrackingLazyRead should aggregate these three contiguous ranges // into a single range from offset 10 to 30, with length 20 assertEquals("AggregatingSliceTrackingLazyRead should make 1 materialize call for contiguous ranges", 1, dummyLazyRead.getNumMaterializeCalls()); // 7. Verify the stored slices now contain three ranges total: // the two from the first prefetch plus one aggregated range from the second prefetch assertStoredSlices(sliceTracking, Arrays.asList( Range.at(10, 20) // Aggregated range )); } private static DummyLazyRead createDummyLazyRead(int size) { byte[] data = new byte[size]; for (int i = 0; i < data.length; i++) { data[i] = (byte)i; } return new DummyLazyRead(ReadData.from(data)); } /** * Helper method to verify that stored slices match expected ranges. * * @param sliceTracking the SliceTrackingLazyRead instance * @param expectedRanges the expected ranges stored in slices */ private static void assertStoredSlices(TestableSliceTracker sliceTracking, List expectedRanges) { // Access protected slices field via a test helper List actualSlices = sliceTracking.getSlices(); assertEquals("Number of stored slices should match", expectedRanges.size(), actualSlices.size()); for (int i = 0; i < expectedRanges.size(); i++) { Range expected = expectedRanges.get(i); Range actual = actualSlices.get(i); assertEquals("Slice " + i + " offset should match", expected.offset(), actual.offset()); assertEquals("Slice " + i + " length should match", expected.length(), actual.length()); } } /** * Testable wrapper for DefaultSliceTrackingLazyRead that exposes slices. */ static class TestableDefaultSliceTracker extends EnclosingPrefetchLazyRead implements TestableSliceTracker { public TestableDefaultSliceTracker(LazyRead delegate) { super(delegate); } @Override public List getSlices() { return java.util.Collections.unmodifiableList(slices); } } /** * Testable wrapper for AggregatingSliceTrackingLazyRead that exposes slices. */ static class TestableAggregatingSliceTracker extends AggregatingPrefetchLazyRead implements TestableSliceTracker { public TestableAggregatingSliceTracker(LazyRead delegate) { super(delegate); } @Override public List getSlices() { return java.util.Collections.unmodifiableList(slices); } } /** * Interface for testable slice trackers. */ interface TestableSliceTracker { List getSlices(); } static class DummyLazyRead implements LazyRead { private ReadData data; private int numMaterializeCalls = 0; public DummyLazyRead( ReadData data ) { this.data = data; } @Override public void close() throws IOException { // no op } @Override public ReadData materialize(long offset, long length) throws N5IOException { numMaterializeCalls++; return data.slice(offset, length).materialize(); } @Override public long size() throws N5IOException { return data.length(); } public int getNumMaterializeCalls() { return numMaterializeCalls; } public void resetNumMaterializeCalls() { numMaterializeCalls = 0; } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/readdata/prefetch/SlicesTest.java ================================================ package org.janelia.saalfeldlab.n5.readdata.prefetch; import java.util.ArrayList; import java.util.List; import org.janelia.saalfeldlab.n5.readdata.Range; import org.junit.Test; import static org.junit.Assert.assertEquals; public class SlicesTest { private List createSlices(final long[] offsets, final long[] lengths) { final List slices = new ArrayList<>(); for (int i = 0; i < offsets.length; ++i) { slices.add(Range.at(offsets[i], lengths[i])); } return slices; } @Test public void testFindContaining() { // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (2,6) [-----------] // (6,4) [---------] // (8,6) [-----------] final List slices = createSlices( new long[] {2, 6, 8}, new long[] {6, 4, 6}); Range slice; // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (1,1) [-] slice = Slices.findContainingSlice(slices, 1, 1); assertEquals(null, slice); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (2,1) [-] slice = Slices.findContainingSlice(slices, 2, 1); assertEquals(2, slice.offset()); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (2,6) [-----------] slice = Slices.findContainingSlice(slices, 2, 6); assertEquals(2, slice.offset()); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (2,7) [-------------] slice = Slices.findContainingSlice(slices, 2, 7); assertEquals(null, slice); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (6,4) [-------] slice = Slices.findContainingSlice(slices, 6, 4); assertEquals(6, slice.offset()); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (8,2) [---] slice = Slices.findContainingSlice(slices, 8, 2); assertEquals(8, slice.offset()); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (12,2) [---] slice = Slices.findContainingSlice(slices, 12, 2); assertEquals(8, slice.offset()); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (12,3) [-----] slice = Slices.findContainingSlice(slices, 12, 3); assertEquals(null, slice); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (14,1) [-] slice = Slices.findContainingSlice(slices, 14, 1); assertEquals(null, slice); } @Test public void testAddSlice() { // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (2,6) [-----------] // (6,4) [---------] // (8,6) [-----------] final List initial = createSlices( new long[] {2, 6, 8}, new long[] {6, 4, 6}); List slices; slices = new ArrayList<>(initial); Slices.addSlice(slices, Range.at(0, 1)); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (0,1) [-] // (2,6) [-----------] // (6,4) [---------] // (8,6) [-----------] assertEquals(createSlices( new long[] {0, 2, 6, 8}, new long[] {1, 6, 4, 6}), slices); slices = new ArrayList<>(initial); Slices.addSlice(slices, Range.at(0, 16)); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (0,16)[-------------------------------] assertEquals(createSlices( new long[] {0}, new long[] {16}), slices); slices = new ArrayList<>(initial); Slices.addSlice(slices, Range.at(2, 8)); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (2,8) [-----------------] // (8,6) [-----------] assertEquals(createSlices( new long[] {2, 8}, new long[] {8, 6}), slices); slices = new ArrayList<>(initial); Slices.addSlice(slices, Range.at(1, 10)); // 0 1 2 3 4 5 6 7 8 9 A B C D E F // (1,10) [---------------------] // (8,6) [-----------] assertEquals(createSlices( new long[] {1, 8}, new long[] {10, 6}), slices); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/readdata/segment/ConcatenatedReadDataTest.java ================================================ package org.janelia.saalfeldlab.n5.readdata.segment; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.segment.SegmentedReadData.SegmentsAndData; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertEquals; public class ConcatenatedReadDataTest { private final byte[] data = new byte[100]; @Before public void fillData() { for (int i = 0; i < data.length; i++) { data[i] = (byte) i; } } /** * Create a SegmentedReadData with segments at (10,l=30), (0,l=10), and (40,l=20). * The returned ReadData knows its length. *

*

	 * [0.....10.....20.....30.....40.....50.....60.....70.....80.....90.....]
	 * [(-s1-)(---------s0--------)(-----s2-----)............................]
	 * 
*/ private SegmentsAndData createKnownLength() { return SegmentedReadData.wrap( ReadData.from(data), Range.at(10, 30), Range.at(0, 10), Range.at(40, 20)); } /** * Create a SegmentedReadData with one segment spanning it completely. * The returned ReadData doesn't know its length. *

*

	 * [0.....10.....20.....30.....40.....50.....60.....70.....80.....90.....]
	 * [(------------------------------s3-----------------------------------)]
	 * 
*/ private SegmentsAndData createUnknownLength() { final SegmentedReadData srd = SegmentedReadData.wrap( ReadData.from( new ByteArrayInputStream(data))); return new SegmentsAndData() { @Override public List segments() { return Collections.singletonList(srd.segments().get(0)); } @Override public SegmentedReadData data() { return srd; } }; } /** * Create slices of known length and unknown length SegmentedReadData, and concatenate. * Take slice (0,l=40) *

*

	 *
	 * KNOWN_LENGTH:
	 * [0.....10.....20.....30.....40.....50.....60.....70.....80.....90.....]
	 * [(-s1-)(---------s0--------)(-----s2-----)............................]
	 *
	 * SLICE_0
	 * [(-s1-)(---------s0--------)]
	 *
	 *                            SLICE_1
	 *                            [(-----s2-----)]
	 *
	 * UNKNOWN_LENGTH:
	 * [0.....10.....20.....30.....40.....50.....60.....70.....80.....90.....]
	 * [(------------------------------s3-----------------------------------)]
	 *
	 * CONCATENATED:
	 * [-------- SLICE_0 ----------][- UNKNOWN_LENGTH -][-- SLICE_1 ---]
	 * [(-s1-)(---------s0--------)][(--...--s3--...--)][(-----s2-----)]
	 *
	 * 
*/ private SegmentsAndData createConcatenate() { final SegmentsAndData segmentsAndData0 = createKnownLength(); final SegmentedReadData r0 = segmentsAndData0.data(); final SegmentsAndData segmentsAndData1 = createUnknownLength(); final SegmentedReadData r1 = segmentsAndData1.data(); final List segments = new ArrayList<>(); segments.addAll(segmentsAndData0.segments()); segments.addAll(segmentsAndData1.segments()); final List datas = new ArrayList<>(); datas.add(r0.slice(0,40)); datas.add(r1); datas.add(r0.slice(segments.get(2))); final SegmentedReadData concatenated = SegmentedReadData.concatenate(datas); return new SegmentsAndData() { @Override public List segments() { return segments; } @Override public SegmentedReadData data() { return concatenated; } }; } /** * Check that segments in the concatenated ReadData are at the expected locations. * The segments should be laid out like this: *

*

	 * CONCATENATED:
	 * [0.....10.....20.....30.....40...             ...140.....150.....]
	 * [-------- SLICE_0 ----------][- UNKNOWN_LENGTH -][--- SLICE_1 ---]
	 * [(-s1-)(---------s0--------)][(--...--s3--...--)][(------s2-----)]
	 * 
*

*/ private static void checkSegmentLocations(final SegmentsAndData segmentsAndData) { checkSegmentRange(segmentsAndData,1, Range.at(0, 10)); checkSegmentRange(segmentsAndData,0, Range.at(10, 30)); checkSegmentRange(segmentsAndData,3, Range.at(40, 100)); checkSegmentRange(segmentsAndData,2, Range.at(140, 20)); } private static void checkSegmentRange(final SegmentsAndData data, final int segmentIndex, final Range expectedLocation) { final Range location = data.data().location(data.segments().get(segmentIndex)); assertEquals(expectedLocation.offset(), location.offset()); assertEquals(expectedLocation.length(), location.length()); } // A concatenated ReadData containing unknown-length parts requires // either materialize() or writeTo(OutputStream) to make those lengths // known. Otherwise, trying to get segment locations from the // concatenated ReadData fails with an IllegalStateException. @Test(expected = IllegalStateException.class) public void testConcatenateUnmaterialized() { final SegmentsAndData segmentsAndData = createConcatenate(); final SegmentedReadData c = segmentsAndData.data(); final List s = segmentsAndData.segments(); System.out.println(c.location(s.get(0))); } // Calling materialize() on the concatenated ReadData makes sure that all // segment offsets are known. @Test public void testConcatenateMaterialize() { final SegmentsAndData segmentsAndData = createConcatenate(); segmentsAndData.data().materialize(); checkSegmentLocations(segmentsAndData); } // Calling writeTo(OutputStream) on the concatenated ReadData makes sure // that all segment offsets are known. @Test public void testConcatenateWriteTo() { final SegmentsAndData segmentsAndData = createConcatenate(); segmentsAndData.data().writeTo(new ByteArrayOutputStream()); checkSegmentLocations(segmentsAndData); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/readdata/segment/SegmentTest.java ================================================ package org.janelia.saalfeldlab.n5.readdata.segment; import java.io.ByteArrayInputStream; import java.util.List; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertEquals; public class SegmentTest { private ReadData readData; private ReadData readDataUnknownLength; @Before public void createReadData() { final byte[] data = new byte[100]; for (int i = 0; i < data.length; i++) { data[i] = (byte) i; } readData = ReadData.from(data); readDataUnknownLength = ReadData.from(new ByteArrayInputStream(data)); } @Test public void testWrap() { final Range[] locations = { Range.at(0, 10), Range.at(10, 10), Range.at(40, 20)}; final SegmentedReadData r = SegmentedReadData.wrap(readData, locations).data(); assertEquals(3, r.segments().size()); final Range l0 = r.location(r.segments().get(0)); assertEquals(0, l0.offset()); assertEquals(10, l0.length()); final Range l1 = r.location(r.segments().get(1)); assertEquals(10, l1.offset()); assertEquals(10, l1.length()); final Range l2 = r.location(r.segments().get(2)); assertEquals(40, l2.offset()); assertEquals(20, l2.length()); } @Test public void testWrapOrder() { final Range[] locations = { Range.at(10, 10), Range.at(0, 10), Range.at(40, 20)}; final SegmentedReadData.SegmentsAndData segmentsAndData = SegmentedReadData.wrap(readData, locations); final SegmentedReadData r = segmentsAndData.data(); final List segments = segmentsAndData.segments(); assertEquals(3, segments.size()); final Range l0 = r.location(segments.get(0)); assertEquals(10, l0.offset()); assertEquals(10, l0.length()); final Range l1 = r.location(segments.get(1)); assertEquals(0, l1.offset()); assertEquals(10, l1.length()); final Range l2 = r.location(segments.get(2)); assertEquals(40, l2.offset()); assertEquals(20, l2.length()); } @Test public void testSlice() { final Range[] locations = { Range.at(0, 10), Range.at(10, 10), Range.at(40, 20)}; final SegmentedReadData r = SegmentedReadData.wrap(readData, locations).data(); final SegmentedReadData s = r.slice(10, 60); assertEquals(60, s.length()); assertEquals(2, s.segments().size()); final Range l0 = s.location(s.segments().get(0)); assertEquals(0, l0.offset()); assertEquals(10, l0.length()); final Range l1 = s.location(s.segments().get(1)); assertEquals(30, l1.offset()); assertEquals(20, l1.length()); } @Test public void testSliceSegment() { final Range[] locations = { Range.at(0, 10), Range.at(10, 10), Range.at(40, 20)}; final SegmentedReadData r = SegmentedReadData.wrap(readData, locations).data(); final SegmentedReadData s = r.slice(r.segments().get(2)); assertEquals(20, s.length()); assertEquals(1, s.segments().size()); final Range l0 = s.location(s.segments().get(0)); assertEquals(0, l0.offset()); assertEquals(20, l0.length()); } @Test public void testPartialSliceSegment() { final Range[] locations = { Range.at(0, 10), Range.at(10, 10), Range.at(40, 20)}; final SegmentedReadData r = SegmentedReadData.wrap(readData, locations).data(); // slice covers all of second segment, part of first and third final SegmentedReadData s = r.slice(9, 15); assertEquals(15, s.length()); assertEquals(1, s.segments().size()); final Range l0 = s.location(s.segments().get(0)); assertEquals(1, l0.offset()); assertEquals(10, l0.length()); } @Test public void testWrapFully() { final SegmentedReadData r = SegmentedReadData.wrap(readDataUnknownLength); assertEquals(1, r.segments().size()); final Range l0 = r.location(r.segments().get(0)); assertEquals(0, l0.offset()); assertEquals(-1, l0.length()); final SegmentedReadData m = r.materialize(); final Range l0m = r.location(r.segments().get(0)); assertEquals(0, l0m.offset()); assertEquals(100, l0m.length()); final Range l0m2 = m.location(m.segments().get(0)); assertEquals(0, l0m2.offset()); assertEquals(100, l0m2.length()); } @Test public void testSliceFullyWrapped() { final SegmentedReadData r = SegmentedReadData.wrap(readDataUnknownLength); final SegmentedReadData s = r.slice(0, 100); assertEquals(100, s.length()); assertEquals(1, s.segments().size()); final Range l0 = s.location(s.segments().get(0)); assertEquals(0, l0.offset()); assertEquals(100, l0.length()); final SegmentedReadData s0 = r.slice(0, 99); assertEquals(99, s0.length()); assertEquals(0, s0.segments().size()); final SegmentedReadData s1 = r.slice(1, 99); assertEquals(99, s1.length()); assertEquals(0, s1.segments().size()); } @Test public void testSliceSegmentFullyWrapped() { final SegmentedReadData r = SegmentedReadData.wrap(readDataUnknownLength); final SegmentedReadData s = r.slice(r.segments().get(0)); assertEquals(-1, s.length()); assertEquals(1, s.segments().size()); final Range l0 = s.location(s.segments().get(0)); assertEquals(0, l0.offset()); assertEquals(-1, l0.length()); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/serialization/CodecSerializationTest.java ================================================ package org.janelia.saalfeldlab.n5.serialization; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.GzipCompression; import org.janelia.saalfeldlab.n5.NameConfigAdapter; import org.janelia.saalfeldlab.n5.codec.BytesCodecTests.BitShiftBytesCodec; import org.janelia.saalfeldlab.n5.codec.CodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.IdentityCodec; import org.junit.Before; import org.junit.Ignore; 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 CodecSerializationTest { private Gson gson; @Before public void before() { final GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(DataType.class, new DataType.JsonAdapter()); gsonBuilder.registerTypeHierarchyAdapter(DataCodecInfo.class, NameConfigAdapter.getJsonAdapter(DataCodecInfo.class)); gsonBuilder.registerTypeHierarchyAdapter(CodecInfo.class, NameConfigAdapter.getJsonAdapter(CodecInfo.class)); gsonBuilder.disableHtmlEscaping(); gson = gsonBuilder.create(); } @Test public void testCodecSerialization() { final IdentityCodec id = new IdentityCodec(); final JsonObject jsonId = gson.toJsonTree(id).getAsJsonObject(); final JsonElement expected = gson.fromJson("{\"name\":\"id\"}", JsonElement.class); assertEquals("identity json", expected, jsonId.getAsJsonObject()); final BitShiftBytesCodec codec = new BitShiftBytesCodec(3); final JsonObject bitShiftJson = gson.toJsonTree(codec).getAsJsonObject(); final JsonElement expectedBitShift = gson.fromJson( "{\"name\":\"bitshift\",\"configuration\":{\"shift\":3}}", JsonElement.class); assertEquals("bitshift json", expectedBitShift, bitShiftJson); final DataCodecInfo deserializedCodecInfo = gson.fromJson(bitShiftJson, DataCodecInfo.class); // Verify deserialized codec assertEquals("Deserialized codec should equal original", codec, deserializedCodecInfo); } @Test @Ignore public void testSerializeCodecArray() { CodecInfo[] codecs = new CodecInfo[]{ new IdentityCodec() }; JsonArray jsonCodecArray = gson.toJsonTree(codecs).getAsJsonArray(); JsonElement expected = gson.fromJson( "[{\"name\":\"id\"}]", JsonElement.class); assertEquals("codec array", expected, jsonCodecArray.getAsJsonArray()); CodecInfo[] codecsDeserialized = gson.fromJson(expected, CodecInfo[].class); assertEquals("codecs length not 1", 1, codecsDeserialized.length); assertTrue("first codec not identity", codecsDeserialized[0] instanceof IdentityCodec); codecs = new CodecInfo[]{ new GzipCompression() }; jsonCodecArray = gson.toJsonTree(codecs).getAsJsonArray(); expected = gson.fromJson( "[{\"name\":\"gzip\",\"configuration\":{\"level\":-1,\"useZlib\":false}}]", JsonElement.class); assertEquals("codec array", expected, jsonCodecArray.getAsJsonArray()); codecsDeserialized = gson.fromJson(expected, CodecInfo[].class); assertEquals("codecs length not 1", 1, codecsDeserialized.length); assertTrue("second codec not gzip", codecsDeserialized[0] instanceof GzipCompression); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/shard/DatasetAccessTest.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.ArrayList; import java.util.Arrays; import java.util.List; 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.N5Exception; import org.janelia.saalfeldlab.n5.RawCompression; import org.janelia.saalfeldlab.n5.codec.BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.N5BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.RawBlockCodecInfo; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; public class DatasetAccessTest { // DataBlocks are 3x3x3 // Level 1 shards are 6x6x6 (contain 2x2x2 DataBlocks) // Level 2 shards are 24x24x24 (contain 4x4x4 Level 1 shards) private final int[] dataBlockSize = {3, 3, 3}; private final int[] level1ShardSize = {6, 6, 6}; private final int[] level2ShardSize = {24, 24, 24}; private final long[] datasetDimensions = {240, 240, 240}; private DatasetAccess datasetAccess; @Before public void setup() { final BlockCodecInfo c0 = new N5BlockCodecInfo(); final ShardCodecInfo c1 = new DefaultShardCodecInfo( dataBlockSize, c0, new DataCodecInfo[] {new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[] {new RawCompression()}, IndexLocation.END ); final ShardCodecInfo c2 = new DefaultShardCodecInfo( level1ShardSize, c1, new DataCodecInfo[] {new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[] {new RawCompression()}, IndexLocation.START ); final TestDatasetAttributes attributes = new TestDatasetAttributes( datasetDimensions, level2ShardSize, DataType.INT8, c2, new RawCompression() ); datasetAccess = attributes.getDatasetAccess(); } @Test public void testWriteReadIndividual() { final PositionValueAccess store = new TestPositionValueAccess(); // write some blocks, filled with constant values datasetAccess.writeChunk(store, createDataBlock(dataBlockSize, new long[] {0, 0, 0}, 1)); datasetAccess.writeChunk(store, createDataBlock(dataBlockSize, new long[] {1, 0, 0}, 2)); datasetAccess.writeChunk(store, createDataBlock(dataBlockSize, new long[] {0, 1, 0}, 3)); datasetAccess.writeChunk(store, createDataBlock(dataBlockSize, new long[] {1, 1, 0}, 4)); datasetAccess.writeChunk(store, createDataBlock(dataBlockSize, new long[] {3, 2, 1}, 5)); datasetAccess.writeChunk(store, createDataBlock(dataBlockSize, new long[] {8, 4, 1}, 6)); // verify that the written blocks can be read back with the correct values checkBlock(datasetAccess.readChunk(store, new long[] {0, 0, 0}), true, 1); checkBlock(datasetAccess.readChunk(store, new long[] {1, 0, 0}), true, 2); checkBlock(datasetAccess.readChunk(store, new long[] {0, 1, 0}), true, 3); checkBlock(datasetAccess.readChunk(store, new long[] {1, 1, 0}), true, 4); checkBlock(datasetAccess.readChunk(store, new long[] {3, 2, 1}), true, 5); checkBlock(datasetAccess.readChunk(store, new long[] {8, 4, 1}), true, 6); } @Test public void testWriteReadBulk() { final PositionValueAccess store = new TestPositionValueAccess(); // write some blocks, filled with constant values final List writeGridPositions = Arrays.asList(new long[][] { {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {1, 1, 0}, {3, 2, 1}, {8, 4, 1} }); final List> writeBlocks = new ArrayList<>(); for (int i = 0; i < writeGridPositions.size(); i++) { writeBlocks.add(createDataBlock(dataBlockSize, writeGridPositions.get(i), 1 + i)); } datasetAccess.writeChunks(store, writeBlocks); // verify that the written blocks can be read back with the correct values final List readGridPositions = Arrays.asList(new long[][] { {1, 0, 0}, {0, 0, 0}, {0, 1, 0}, {2, 4, 2}, {3, 2, 1}, {8, 4, 1} }); final List> readBlocks = datasetAccess.readChunks(store, readGridPositions); checkBlock(readBlocks.get(0), true, 2); checkBlock(readBlocks.get(1), true, 1); checkBlock(readBlocks.get(2), true, 3); checkBlock(readBlocks.get(3), false, 4); checkBlock(readBlocks.get(4), true, 5); checkBlock(readBlocks.get(5), true, 6); } @Test public void testDeleteBlock() { final PositionValueAccess store = new TestPositionValueAccess(); // write some blocks, filled with constant values final List writeGridPositions = Arrays.asList(new long[][] { {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {1, 1, 0}, {3, 2, 1}, {8, 4, 1} }); final List> writeBlocks = new ArrayList<>(); for (int i = 0; i < writeGridPositions.size(); i++) { writeBlocks.add(createDataBlock(dataBlockSize, writeGridPositions.get(i), 1 + i)); } datasetAccess.writeChunks(store, writeBlocks); // verify that deleting a block removes it from the shard (while other blocks in the same shard are still present) datasetAccess.deleteChunk(store, new long[] {0, 0, 0}); checkBlock(datasetAccess.readChunk(store, new long[] {0, 0, 0}), false, 1); checkBlock(datasetAccess.readChunk(store, new long[] {1, 0, 0}), true, 2); // if a shard becomes empty the corresponding key should be deleted assertTrue(keyExists(store, new long[] {1, 0, 0})); datasetAccess.deleteChunk(store, new long[] {8, 4, 1}); assertFalse(keyExists(store, new long[] {1, 0, 0})); // deleting a non-existent block should not fail datasetAccess.deleteChunk(store, new long[] {0, 0, 8}); } private boolean keyExists(final PositionValueAccess store, final long[] key) { try (final VolatileReadData data = store.get(key)) { if (data != null) { data.requireLength(); return true; } } catch (N5Exception.N5IOException ignored) { } return false; } @Test public void testDeleteBlocks() { final PositionValueAccess store = new TestPositionValueAccess(); // write some blocks, filled with constant values final List writeGridPositions = Arrays.asList(new long[][] { {0, 0, 0}, {1, 0, 0}, {0, 1, 0}, {1, 1, 0}, {3, 2, 1}, {8, 4, 1} }); final List> writeBlocks = new ArrayList<>(); for (int i = 0; i < writeGridPositions.size(); i++) { writeBlocks.add(createDataBlock(dataBlockSize, writeGridPositions.get(i), 1 + i)); } datasetAccess.writeChunks(store, writeBlocks); // verify that deleting a block removes it from the shard (while other blocks in the same shard are still present) datasetAccess.deleteChunks(store, Arrays.asList(new long[][] {{0, 0, 0}, {4, 2, 2}, {3, 2, 1}})); checkBlock(datasetAccess.readChunk(store, new long[] {0, 0, 0}), false, 1); checkBlock(datasetAccess.readChunk(store, new long[] {1, 0, 0}), true, 2); // if a shard becomes empty the corresponding key should be deleted assertTrue(keyExists(store, new long[] {1, 0, 0})); datasetAccess.deleteChunks(store, Arrays.asList(new long[][] {{8, 4, 1}})); assertFalse(keyExists(store, new long[] {1, 0, 0})); // deleting a non-existent block should not fail datasetAccess.deleteChunks(store, Arrays.asList(new long[] {0, 0, 8})); } private static void checkBlock(final DataBlock dataBlock, final boolean expectedNonNull, final int expectedFillValue) { if (expectedNonNull) { assertNotNull("expected non-null dataBlock", dataBlock); for (byte b : dataBlock.getData()) { Assert.assertTrue("expected all values to be " + expectedFillValue, b == (byte) expectedFillValue); } } else { assertNull("expected null dataBlock", dataBlock); } } private static DataBlock createDataBlock(int[] size, long[] gridPosition, int fillValue) { final byte[] bytes = new byte[DataBlock.getNumElements(size)]; Arrays.fill(bytes, (byte) fillValue); return new ByteArrayDataBlock(size, gridPosition, bytes); } public static class TestDatasetAttributes extends DatasetAttributes { public TestDatasetAttributes(long[] dimensions, int[] outerBlockSize, DataType dataType, BlockCodecInfo blockCodecInfo, DataCodecInfo... dataCodecInfos) { super(dimensions, outerBlockSize, dataType, blockCodecInfo, dataCodecInfos); } @Override // to make this accessible for the test protected DatasetAccess getDatasetAccess() { return super.getDatasetAccess(); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/shard/NestedGridTest.java ================================================ package org.janelia.saalfeldlab.n5.shard; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertThrows; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import org.janelia.saalfeldlab.n5.shard.Nesting.NestedGrid; import org.junit.Assert; import org.junit.Test; public class NestedGridTest { private static long absPosition1D(final NestedGrid grid, final int sourcePos, final int targetLevel) { return grid.absolutePosition(new long[] {sourcePos}, targetLevel)[0]; } @Test public void testValidateInput() { int[][] blockSizes = {{3}, {7}, {11}}; assertThrows(IllegalArgumentException.class, () -> new NestedGrid(blockSizes)); } @Test public void testAbsolutePosition() { int[][] blockSizes = {{1}, {3}, {6}, {24}}; NestedGrid grid = new NestedGrid(blockSizes); Assert.assertEquals(38, absPosition1D(grid, 38, 0)); Assert.assertEquals(12, absPosition1D(grid, 36, 1)); Assert.assertEquals(12, absPosition1D(grid, 37, 1)); Assert.assertEquals(12, absPosition1D(grid, 38, 1)); Assert.assertEquals(6, absPosition1D(grid, 38, 2)); Assert.assertEquals(1, absPosition1D(grid, 38, 3)); } @Test public void testAbsolutePositionChunkSize() { int[][] blockSizes = {{10}, {30}, {60}, {240}}; NestedGrid grid = new NestedGrid(blockSizes); Assert.assertEquals(38, absPosition1D(grid, 38, 0)); Assert.assertEquals(12, absPosition1D(grid, 38, 1)); Assert.assertEquals(6, absPosition1D(grid, 38, 2)); Assert.assertEquals(1, absPosition1D(grid, 38, 3)); } private static long relPosition1D(final NestedGrid grid, final int sourcePos, final int targetLevel) { return grid.relativePosition(new long[] {sourcePos}, 0, targetLevel)[0]; } @Test public void testRelativePosition() { int[][] blockSizes = {{1}, {3}, {6}, {24}}; NestedGrid grid = new NestedGrid(blockSizes); Assert.assertEquals(2, relPosition1D(grid, 38, 0)); Assert.assertEquals(0, relPosition1D(grid, 38, 1)); Assert.assertEquals(2, relPosition1D(grid, 38, 2)); Assert.assertEquals(1, relPosition1D(grid, 38, 3)); } @Test public void testRelativePositionChunkSize() { int[][] blockSizes = {{10}, {30}, {60}, {240}}; NestedGrid grid = new NestedGrid(blockSizes); Assert.assertEquals(2, relPosition1D(grid, 38, 0)); Assert.assertEquals(0, relPosition1D(grid, 38, 1)); Assert.assertEquals(2, relPosition1D(grid, 38, 2)); Assert.assertEquals(1, relPosition1D(grid, 38, 3)); } @Test public void testNd() { int[][] blockSizes = {{5, 7}, {5*3, 7*2}}; NestedGrid grid = new NestedGrid(blockSizes); System.out.println(grid); assertArrayEquals(new long[]{1, 2}, grid.absolutePosition(new long[]{1, 2}, 0)); assertArrayEquals(new long[]{99, 99}, grid.absolutePosition(new long[]{99, 99}, 0)); assertArrayEquals(new long[]{0, 0}, grid.absolutePosition(new long[]{0, 1}, 1)); assertArrayEquals(new long[]{0, 1}, grid.absolutePosition(new long[]{0, 2}, 1)); assertArrayEquals(new long[]{1, 1}, grid.absolutePosition(new long[]{3, 2}, 1)); } @Test public void testNestedPositionOrder() { // NestedPosition should be ordered such that positions from a // (sub-)shard are grouped together: // For nested = {X,Y,Z} compare by Z, then Y, then X. // For X = [x,y,z] compare by z, then y, then x. (flattening order) final NestedGrid grid = new NestedGrid(new int[][]{{3,3},{6,6}}); final List positions = new ArrayList<>(); positions.add(grid.nestedPosition(new long[]{3, 1})); // {[1, 1] / [1, 0]} positions.add(grid.nestedPosition(new long[]{3, 5})); // {[1, 1] / [1, 2]} positions.add(grid.nestedPosition(new long[]{2, 2})); // {[0, 0] / [1, 1]} positions.add(grid.nestedPosition(new long[]{1, 0})); // {[1, 0] / [0, 0]} positions.add(grid.nestedPosition(new long[]{6, 7})); // {[0, 1] / [3, 3]} positions.add(grid.nestedPosition(new long[]{0, 1})); // {[0, 1] / [0, 0]} positions.add(grid.nestedPosition(new long[]{0, 2})); // {[0, 0] / [0, 1]} positions.add(grid.nestedPosition(new long[]{1, 1})); // {[1, 1] / [0, 0]} positions.add(grid.nestedPosition(new long[]{5, 0})); // {[1, 0] / [2, 0]} positions.add(grid.nestedPosition(new long[]{0, 0})); // {[0, 0] / [0, 0]} final List orderedPositions = new ArrayList<>(); orderedPositions.add(grid.nestedPosition(new long[]{0, 0})); // {[0, 0] / [0, 0]} orderedPositions.add(grid.nestedPosition(new long[]{1, 0})); // {[1, 0] / [0, 0]} orderedPositions.add(grid.nestedPosition(new long[]{0, 1})); // {[0, 1] / [0, 0]} orderedPositions.add(grid.nestedPosition(new long[]{1, 1})); // {[1, 1] / [0, 0]} orderedPositions.add(grid.nestedPosition(new long[]{3, 1})); // {[1, 1] / [1, 0]} orderedPositions.add(grid.nestedPosition(new long[]{5, 0})); // {[1, 0] / [2, 0]} orderedPositions.add(grid.nestedPosition(new long[]{0, 2})); // {[0, 0] / [0, 1]} orderedPositions.add(grid.nestedPosition(new long[]{2, 2})); // {[0, 0] / [1, 1]} orderedPositions.add(grid.nestedPosition(new long[]{3, 5})); // {[1, 1] / [1, 2]} orderedPositions.add(grid.nestedPosition(new long[]{6, 7})); // {[0, 1] / [3, 3]} Collections.sort(positions); Assert.assertEquals(orderedPositions,positions); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/shard/ShardTest.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.io.File; import java.net.URI; import java.net.URISyntaxException; import java.nio.ByteOrder; import java.nio.file.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.function.Predicate; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; import org.janelia.saalfeldlab.n5.*; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.N5BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.RawBlockCodecInfo; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; import org.junit.After; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; @RunWith(Parameterized.class) public class ShardTest { private static final boolean LOCAL_DEBUG = false; private static final N5FSTest tempN5Factory = new N5FSTest() { @Override public N5Writer createTempN5Writer() { if (LOCAL_DEBUG) { final N5Writer writer = new TrackingN5Writer("src/test/resources/test.n5", new FileSystemKeyValueAccess()); writer.remove(""); // Clear old when starting new test return writer; } final String basePath = new File(tempN5PathName()).toURI().normalize().getPath(); try { String uri = new URI("file", null, basePath, null).toString(); return new TrackingN5Writer(uri, new FileSystemKeyValueAccess()); } catch (URISyntaxException e) { e.printStackTrace(); } return null; } private String tempN5PathName() { try { final File tmpFile = Files.createTempDirectory("n5-shard-test-").toFile(); tmpFile.delete(); tmpFile.mkdir(); tmpFile.deleteOnExit(); return tmpFile.getCanonicalPath(); } catch (final Exception e) { throw new RuntimeException(e); } } }; @Parameterized.Parameters(name = "IndexLocation({0}), Index ByteOrder({1})") public static Collection data() { final ArrayList params = new ArrayList<>(); for (IndexLocation indexLoc : IndexLocation.values()) { for (ByteOrder indexByteOrder : new ByteOrder[]{ByteOrder.BIG_ENDIAN, ByteOrder.LITTLE_ENDIAN}) { params.add(new Object[]{indexLoc, indexByteOrder}); } } final int numParams = params.size(); final Object[][] paramArray = new Object[numParams][]; Arrays.setAll(paramArray, params::get); return Arrays.asList(paramArray); } @Parameterized.Parameter() public IndexLocation indexLocation; @Parameterized.Parameter(1) public ByteOrder indexByteOrder; @After public void removeTempWriters() { tempN5Factory.removeTempWriters(); } private DatasetAttributes getTestAttributes(long[] dimensions, int[] shardSize, int[] chunkSize) { return getTestAttributes(DataType.UINT8, dimensions, shardSize, chunkSize); } private DatasetAttributes getTestAttributes(DataType dataType, long[] dimensions, int[] shardSize, int[] chunkSize) { DefaultShardCodecInfo blockCodec = new DefaultShardCodecInfo( chunkSize, new N5BlockCodecInfo(), new DataCodecInfo[]{new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[]{new RawCompression()}, IndexLocation.END); return new DatasetAttributes( dimensions, shardSize, dataType, blockCodec); } protected DatasetAttributes getTestAttributes() { return getTestAttributes(new long[]{8, 8}, new int[]{4, 4}, new int[]{2, 2}); } @Test public void writeReadChunksTest() { final N5Writer writer = tempN5Factory.createTempN5Writer(); final DatasetAttributes datasetAttributes = getTestAttributes( new long[]{24, 24}, new int[]{8, 8}, new int[]{2, 2} ); final String dataset = "writeReadBlocks"; writer.remove(dataset); writer.createDataset(dataset, datasetAttributes); final int[] chunkSize = datasetAttributes.getBlockSize(); final int numElements = chunkSize[0] * chunkSize[1]; final byte[] data = new byte[numElements]; for (int i = 0; i < data.length; i++) { data[i] = (byte)((100) + (10) + i); } writer.writeChunks( dataset, datasetAttributes, /* shard (0, 0) */ new ByteArrayDataBlock(chunkSize, new long[]{0, 0}, data), new ByteArrayDataBlock(chunkSize, new long[]{0, 1}, data), new ByteArrayDataBlock(chunkSize, new long[]{1, 0}, data), new ByteArrayDataBlock(chunkSize, new long[]{1, 1}, data), /* shard (1, 0) */ new ByteArrayDataBlock(chunkSize, new long[]{4, 0}, data), new ByteArrayDataBlock(chunkSize, new long[]{5, 0}, data), /* shard (2, 2) */ new ByteArrayDataBlock(chunkSize, new long[]{11, 11}, data) ); final KeyValueAccess kva = ((N5KeyValueWriter)writer).getKeyValueAccess(); long[][] keys = new long[][]{ {0, 0}, {1, 0}, {2, 2} }; final long[][] someUnusedKeys = new long[][]{ {0, 1}, {1, 1}, {1, 2}, {2, 1} }; ensureKeysExist(kva, writer.getURI(), dataset, datasetAttributes, keys); ensureKeysDoNotExist(kva, writer.getURI(), dataset, datasetAttributes, someUnusedKeys); final long[][] chunkIndices = new long[][]{{0, 0}, {0, 1}, {1, 0}, {1, 1}, {4, 0}, {5, 0}, {11, 11}}; for (long[] chunkIndex : chunkIndices) { final DataBlock chunk = writer.readChunk(dataset, datasetAttributes, chunkIndex); Assert.assertArrayEquals("Read from shard doesn't match", data, (byte[])chunk.getData()); } final byte[] data2 = new byte[numElements]; for (int i = 0; i < data2.length; i++) { data2[i] = (byte)(10 + i); } writer.writeChunks( dataset, datasetAttributes, /* shard (0, 0) */ new ByteArrayDataBlock(chunkSize, new long[]{0, 0}, data2), new ByteArrayDataBlock(chunkSize, new long[]{1, 1}, data2), /* shard (0, 1) */ new ByteArrayDataBlock(chunkSize, new long[]{0, 4}, data2), new ByteArrayDataBlock(chunkSize, new long[]{0, 5}, data2), /* shard (2, 2) */ new ByteArrayDataBlock(chunkSize, new long[]{10, 10}, data2)); long[][] keys2 = new long[][]{ {0, 0}, {1, 0}, {0, 1}, {2, 2} }; long[][] someUnusedKeys2 = new long[][]{ {1, 1}, {1, 2}, {2, 1} }; ensureKeysExist(kva, writer.getURI(), dataset, datasetAttributes, keys2); ensureKeysDoNotExist(kva, writer.getURI(), dataset, datasetAttributes, someUnusedKeys2); final long[][] oldChunkIndices = new long[][]{{0, 1}, {1, 0}, {4, 0}, {5, 0}, {11, 11}}; for (long[] chunkIndex : oldChunkIndices) { final DataBlock chunk = writer.readChunk(dataset, datasetAttributes, chunkIndex); Assert.assertArrayEquals("Read from shard doesn't match", data, (byte[])chunk.getData()); } final long[][] newChunkIndices = new long[][]{{0, 0}, {1, 1}, {0, 4}, {0, 5}, {10, 10}}; final List newChunkIndexList = Arrays.asList(newChunkIndices); final List> readChunks = writer.readChunks(dataset, datasetAttributes, newChunkIndexList); for (int i = 0; i < newChunkIndices.length; i++) { final long[] chunkIndex = newChunkIndices[i]; final DataBlock chunk = writer.readChunk(dataset, datasetAttributes, chunkIndex); Assert.assertArrayEquals("Read from shard doesn't match", data2, (byte[])chunk.getData()); final DataBlock chunkFromReadChunks = readChunks.get(i); Assert.assertArrayEquals("Read from shard doesn't match", data2, (byte[])chunkFromReadChunks.getData()); } } private void ensureKeysExist(KeyValueAccess kva, URI uri, String dataset, DatasetAttributes datasetAttributes, long[][] keys) { for (long[] key : keys) { final String shard = kva.compose(uri, dataset, datasetAttributes.relativeBlockPath(key)); Assert.assertTrue("Shard at" + shard + "Does not exist", kva.exists(shard)); } } private void ensureKeysDoNotExist(KeyValueAccess kva, URI uri, String dataset, DatasetAttributes datasetAttributes, long[][] keys) { for (long[] key : keys) { final String shard = kva.compose(uri, dataset, datasetAttributes.relativeBlockPath(key)); Assert.assertFalse("Shard at" + shard + " exists but should not.", kva.exists(shard)); } } @Test public void writeShardDataSizeTest() { // note: this test depends on the use of raw compression final N5Writer writer = tempN5Factory.createTempN5Writer(); int numChunksPerShard = 16; final int n5HeaderSizeBytes = 12; // 2 + 2 + 4*2 final DatasetAttributes attrs = getTestAttributes( new long[]{24, 24}, new int[]{8, 8}, new int[]{2, 2} ); final String dataset = "writeBlocksShardSize"; writer.remove(dataset); final DatasetAttributes datasetAttributes = writer.createDataset(dataset, attrs); assertTrue(datasetAttributes.isSharded()); final KeyValueAccess kva = ((N5KeyValueWriter)writer).getKeyValueAccess(); final int[] chunkSize = datasetAttributes.getChunkSize(); final int numElements = chunkSize[0] * chunkSize[1]; final byte[] data = new byte[numElements]; for (int i = 0; i < data.length; i++) { data[i] = (byte)((100) + (10) + i); } /* * No chunks or shards exist. * Calling readChunks should return a list that is the same length as the requested grid positions, * and every entry should be null. */ final long[][] newChunkIndices = new long[][]{{0, 0}, {1, 1}, {0, 4}, {0, 5}, {10, 10}}; final List> readChunks = writer.readChunks(dataset, datasetAttributes, Arrays.asList(newChunkIndices)); assertEquals(newChunkIndices.length, readChunks.size()); assertTrue("readChunks for empty shard: all chunks null", readChunks.stream().allMatch(Objects::isNull)); /* * Now write chunks */ writer.writeChunks( dataset, datasetAttributes, /* shard (0, 0) */ new ByteArrayDataBlock(chunkSize, new long[]{0, 0}, data), new ByteArrayDataBlock(chunkSize, new long[]{0, 1}, data), new ByteArrayDataBlock(chunkSize, new long[]{1, 0}, data), new ByteArrayDataBlock(chunkSize, new long[]{1, 1}, data), /* shard (1, 0) */ new ByteArrayDataBlock(chunkSize, new long[]{4, 0}, data), new ByteArrayDataBlock(chunkSize, new long[]{5, 0}, data), /* shard (2, 2) */ new ByteArrayDataBlock(chunkSize, new long[]{11, 11}, data) ); final int indexSizeBytes = numChunksPerShard * 16; // 8 bytes per long * final int chunkDataSizeBytes = numElements + n5HeaderSizeBytes; // shard 0,0 has 4 chunks so should be this size: long shard00SizeBytes = indexSizeBytes + 4 * chunkDataSizeBytes; long shard10SizeBytes = indexSizeBytes + 2 * chunkDataSizeBytes; long shard22SizeBytes = indexSizeBytes + 1 * chunkDataSizeBytes; final String[][] keys = new String[][]{ {dataset, "0", "0"}, {dataset, "1", "0"}, {dataset, "2", "2"} }; long[] shardSizes = new long[]{ shard00SizeBytes, shard10SizeBytes, shard22SizeBytes }; int i = 0; for (String[] key : keys) { final String shardPath = kva.compose(writer.getURI(), key); Assert.assertEquals("shard at " + shardPath + " was the wrong size", shardSizes[i++], kva.size(shardPath)); } } @Test public void writeReadChunkTest() { final GsonKeyValueN5Writer writer = (GsonKeyValueN5Writer)tempN5Factory.createTempN5Writer(); final DatasetAttributes datasetAttributes = getTestAttributes(); final String dataset = "writeReadBlock"; writer.remove(dataset); writer.createDataset(dataset, datasetAttributes); final int[] chunkSize = datasetAttributes.getChunkSize(); final DataType dataType = datasetAttributes.getDataType(); final int numElements = 2 * 2; final HashMap writtenChunks = new HashMap<>(); for (int idx1 = 1; idx1 >= 0; idx1--) { for (int idx2 = 1; idx2 >= 0; idx2--) { final long[] gridPosition = {idx1, idx2}; final DataBlock chunk = (DataBlock)dataType.createDataBlock(chunkSize, gridPosition, numElements); byte[] data = chunk.getData(); for (int i = 0; i < data.length; i++) { data[i] = (byte)((idx1 * 100) + (idx2 * 10) + i); } writer.writeChunk(dataset, datasetAttributes, chunk); final DataBlock readChunk = writer.readChunk(dataset, datasetAttributes, chunk.getGridPosition().clone()); Assert.assertArrayEquals("Read from shard doesn't match", data, readChunk.getData()); for (Map.Entry entry : writtenChunks.entrySet()) { final long[] otherGridPosition = entry.getKey(); final byte[] otherData = entry.getValue(); final DataBlock otherChunk = writer.readChunk(dataset, datasetAttributes, otherGridPosition); Assert.assertArrayEquals("Read prior write from shard no loner matches", otherData, otherChunk.getData()); } writtenChunks.put(gridPosition, data); } } } @Test public void writeReadShardTest() { try ( final N5Writer n5 = tempN5Factory.createTempN5Writer() ) { final int[] shardSize = new int[] {4,4}; final int shardN = 16; final int[] chunkSize = new int[] {2,2}; final String dataset = "writeReadShard"; DatasetAttributes attrs = getTestAttributes(DataType.INT32, new long[]{8, 8}, shardSize, chunkSize); final int[] shardData = range(shardN); IntArrayDataBlock shard = new IntArrayDataBlock(shardSize, new long[]{0, 0}, shardData); n5.writeBlock(dataset, attrs, shard); DataBlock readBlock = n5.readBlock(dataset, attrs, 0, 0); assertArrayEquals(shardData, readBlock.getData()); /** * The 4x4 shard at (0,0) * and the 2x2 chunks it contains * * * 0 1 | 2 3 * 4 5 | 6 7 * ---------------- * 8 9 | 10 11 * 12 13 | 14 15 */ assertArrayEquals(new int[]{0, 1, 4, 5}, (int[])n5.readChunk(dataset, attrs, 0, 0).getData()); assertArrayEquals(new int[]{2, 3, 6, 7}, (int[])n5.readChunk(dataset, attrs, 1, 0).getData()); assertArrayEquals(new int[]{8, 9, 12, 13}, (int[])n5.readChunk(dataset, attrs, 0, 1).getData()); assertArrayEquals(new int[]{10, 11, 14, 15}, (int[])n5.readChunk(dataset, attrs, 1, 1).getData()); n5.deleteChunk(dataset, attrs, new long[]{1, 1}); /** * After deleting chunk (1,1) * * 0 1 | 2 3 * 4 5 | 6 7 * ---------------- * 8 9 | 0 0 * 12 13 | 0 0 */ final DataBlock partlyEmptyShard = n5.readBlock(dataset, attrs, 0, 0); assertArrayEquals(new int[]{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 12, 13, 0, 0}, partlyEmptyShard.getData()); // Delete the rest of the chunks n5.deleteChunks(dataset, attrs, Stream.of( new long[] {0,0}, new long[] {1,0}, new long[] {0,1}).collect(Collectors.toList())); assertNull(n5.readBlock(dataset, attrs, 0, 0)); // write the shard again n5.writeBlock(dataset, attrs, shard); // delete the chard // ensure it returns true because the shard exists assertTrue(n5.deleteBlock(dataset, attrs, shard.getGridPosition())); // ensure it returns false when the shard does not exist assertFalse(n5.deleteBlock(dataset, attrs, shard.getGridPosition())); // readBlock must return null for the deleted shard assertNull(n5.readBlock(dataset, attrs, shard.getGridPosition())); } } /** * Checks how many read calls to the backend are performed for a particular readChunks * call. At this time (Nov 4 2025), one read for the index, and one read per block are performed. */ public void numReadsTest() { final TrackingN5Writer writer = (TrackingN5Writer)tempN5Factory.createTempN5Writer(); final DatasetAttributes datasetAttributes = getTestAttributes( new long[]{24, 24}, new int[]{8, 8}, new int[]{2, 2} ); final String dataset = "writeReadBlocks"; writer.remove(dataset); writer.createDataset(dataset, datasetAttributes); final int[] chunkSize = datasetAttributes.getChunkSize(); final int numElements = chunkSize[0] * chunkSize[1]; final byte[] data = new byte[numElements]; for (int i = 0; i < data.length; i++) { data[i] = (byte)((100) + (10) + i); } writer.writeChunks( dataset, datasetAttributes, /* shard (0, 0) */ new ByteArrayDataBlock(chunkSize, new long[]{0, 0}, data), new ByteArrayDataBlock(chunkSize, new long[]{0, 1}, data), new ByteArrayDataBlock(chunkSize, new long[]{1, 0}, data), new ByteArrayDataBlock(chunkSize, new long[]{1, 1}, data), /* shard (1, 0) */ new ByteArrayDataBlock(chunkSize, new long[]{4, 0}, data), new ByteArrayDataBlock(chunkSize, new long[]{5, 0}, data), /* shard (2, 2) */ new ByteArrayDataBlock(chunkSize, new long[]{11, 11}, data) ); writer.resetNumMaterializeCalls(); writer.readChunks(dataset, datasetAttributes, Collections.singletonList(new long[] {0,0})); ArrayList ptList = new ArrayList<>(); ptList.add(new long[] {0, 0}); ptList.add(new long[] {0, 1}); ptList.add(new long[] {1, 0}); ptList.add(new long[] {1, 1}); writer.resetNumMaterializeCalls(); writer.readChunks(dataset, datasetAttributes, ptList); } @Test public void shardExistsTest() { final N5Writer writer = tempN5Factory.createTempN5Writer(); final DatasetAttributes datasetAttributes = getTestAttributes( new long[]{24, 24}, new int[]{8, 8}, new int[]{2, 2} ); final String dataset = "shardExists"; writer.remove(dataset); DatasetAttributes attrs = writer.createDataset(dataset, datasetAttributes); final int[] chunkSize = datasetAttributes.getChunkSize(); final int numElements = chunkSize[0] * chunkSize[1]; final byte[] data = new byte[numElements]; for (int i = 0; i < data.length; i++) { data[i] = (byte)(i); } /* write blocks to shards (0,0), (1,0), and (2,2) */ writer.writeChunks( dataset, attrs, new ByteArrayDataBlock(chunkSize, new long[]{0, 0}, data), /* shard (0, 0) */ new ByteArrayDataBlock(chunkSize, new long[]{4, 0}, data), /* shard (1, 0) */ new ByteArrayDataBlock(chunkSize, new long[]{11, 11}, data) /* shard (2, 2) */ ); TrackingN5Writer trackingWriter = ((TrackingN5Writer) writer); Predicate assertShardExistsTracking = (gridPosition) -> { trackingWriter.resetAllTracking(); final boolean exists = writer.blockExists(dataset, attrs, gridPosition); assertEquals("isFileCheck incremented", 1, trackingWriter.getNumIsFileCalls()); assertEquals("No Bytes Read", 0, trackingWriter.getTotalBytesRead()); return exists; }; trackingWriter.resetAllTracking(); /* shards that should exist should only check file */ Assert.assertTrue("Shard (0,0) should exist", assertShardExistsTracking.test(new long[]{0, 0})); Assert.assertTrue("Shard (1,0) should exist", assertShardExistsTracking.test(new long[]{1, 0})); Assert.assertTrue("Shard (2,2) should exist", assertShardExistsTracking.test(new long[]{2, 2})); /* shards that should NOT exist */ Assert.assertFalse("Shard (0,1) should not exist", assertShardExistsTracking.test(new long[]{0, 1})); Assert.assertFalse("Shard (1,1) should not exist", assertShardExistsTracking.test(new long[]{1, 1})); Assert.assertFalse("Shard (2,0) should not exist", assertShardExistsTracking.test(new long[]{2, 0})); Assert.assertFalse("Shard (0,2) should not exist", assertShardExistsTracking.test(new long[]{0, 2})); } /** * Checks how many read calls to the backend are performed for a particular readBlocks * call. At this time (Jan 4 2026), one read for the index, and one read per block are performed. */ @Test public void testPartialReadAggregationBehavior() { final DatasetAttributes datasetAttributes = getTestAttributes( new long[]{24, 24}, new int[]{8, 8}, new int[]{2, 2} ); try (TrackingN5Writer writer = (TrackingN5Writer)tempN5Factory.createTempN5Writer()) { final String dataset = "shardExists"; writer.remove(dataset); DatasetAttributes attrs = writer.createDataset(dataset, datasetAttributes); final int[] chunkSize = attrs.getChunkSize(); final int numElements = chunkSize[0] * chunkSize[1]; final byte[] data = new byte[numElements]; for (int i = 0; i < data.length; i++) { data[i] = (byte)(i); } // four blocks in shard (0,0) ArrayList ptList = new ArrayList<>(); ptList.add(new long[] {0,0}); ptList.add(new long[] {0,1}); ptList.add(new long[] {1,0}); ptList.add(new long[] {1,1}); /* write blocks to shard (0,0) */ writer.writeChunks( dataset, datasetAttributes, new ByteArrayDataBlock(chunkSize, ptList.get(0), data), new ByteArrayDataBlock(chunkSize, ptList.get(1), data), new ByteArrayDataBlock(chunkSize, ptList.get(2), data), new ByteArrayDataBlock(chunkSize, ptList.get(3), data) ); writer.resetNumMaterializeCalls(); writer.readChunks(dataset, datasetAttributes, ptList); // one for the index, one for the four blocks (aggregated) assertEquals(2, writer.getNumMaterializeCalls()); writer.resetNumMaterializeCalls(); writer.readBlock(dataset, datasetAttributes, new long[] {0,0}); // one for the index, one for the four blocks (aggregated) assertEquals(2, writer.getNumMaterializeCalls()); /** * Aggregate read calls */ writer.tkva.aggregate = true; writer.resetNumMaterializeCalls(); writer.readChunks(dataset, datasetAttributes, ptList); // one for the index, one that covers ALL the blocks) assertEquals(2, writer.getNumMaterializeCalls()); writer.resetNumMaterializeCalls(); writer.readBlock(dataset, datasetAttributes, new long[] {0,0}); // one for the index, one that covers ALL the blocks assertEquals(2, writer.getNumMaterializeCalls()); } } private int[] range(int N) { return IntStream.range(0, N).toArray(); } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/shard/TestPositionValueAccess.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import org.janelia.saalfeldlab.n5.N5Exception.N5IOException; import org.janelia.saalfeldlab.n5.readdata.Range; import org.janelia.saalfeldlab.n5.readdata.ReadData; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; /** * Implementation of {@link PositionValueAccess} for tests. *

* Instead of writing to a {@code KeyValueAccess} backend, here, data is just * stored in a {@code Map} as {@code byte[]} arrays. */ public class TestPositionValueAccess implements PositionValueAccess { private final Map map = new HashMap<>(); @Override public VolatileReadData get(final long[] key) { final byte[] bytes = map.get(new Key(key)); return bytes == null ? null : new ClosableWrapper(ReadData.from(bytes)); } public void set(final long[] key, final ReadData data) { if (data == null) { map.remove(new Key(key)); return; } final byte[] bytes = data.allBytes(); map.put(new Key(key), bytes); } @Override public boolean exists(long[] key) throws N5IOException { return map.containsKey(new Key(key)); } @Override public boolean remove(final long[] key) throws N5IOException { return map.remove(new Key(key)) != null; } private static class Key { private final long[] data; Key(long[] data) { this.data = data; } @Override public final boolean equals(final Object o) { if (!(o instanceof Key)) { return false; } final Key key = (Key)o; return Arrays.equals(data, key.data); } @Override public int hashCode() { return Arrays.hashCode(data); } } private static class ClosableWrapper implements VolatileReadData { private final ReadData delegate; ClosableWrapper(final ReadData delegate) { this.delegate = delegate; } @Override public long length() { return delegate.length(); } @Override public long requireLength() throws N5IOException { return delegate.requireLength(); } @Override public ReadData limit(final long length) throws N5IOException { return delegate.limit(length); } @Override public ReadData slice(final long offset, final long length) throws N5IOException { return delegate.slice(offset, length); } @Override public ReadData slice(final Range range) throws N5IOException { return delegate.slice(range); } @Override public InputStream inputStream() throws N5IOException, IllegalStateException { return delegate.inputStream(); } @Override public byte[] allBytes() throws N5IOException, IllegalStateException { return delegate.allBytes(); } @Override public ByteBuffer toByteBuffer() throws N5IOException, IllegalStateException { return delegate.toByteBuffer(); } @Override public ReadData materialize() throws N5IOException { delegate.materialize(); return this; } @Override public void writeTo(final OutputStream outputStream) throws N5IOException, IllegalStateException { delegate.writeTo(outputStream); } @Override public void prefetch(final Collection ranges) throws N5IOException { delegate.prefetch(ranges); } @Override public ReadData encode(final OutputStreamOperator encoder) { return delegate.encode(encoder); } @Override public void close() throws N5IOException { } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/shard/WriteRegionTest.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.Arrays; 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.RawCompression; import org.janelia.saalfeldlab.n5.codec.BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.N5BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.RawBlockCodecInfo; import org.janelia.saalfeldlab.n5.readdata.VolatileReadData; import org.janelia.saalfeldlab.n5.N5Writer.DataBlockSupplier; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; import org.junit.Test; public class WriteRegionTest { @Test public void testWriteRegion() { int[] chunkSize = {3}; final long[] datasetDimensions = {15}; final BlockCodecInfo c0 = new N5BlockCodecInfo(); TestDatasetAttributes attributes = new TestDatasetAttributes( datasetDimensions, chunkSize, DataType.INT8, c0, new RawCompression()); final DatasetAccess datasetAccess = attributes.getDatasetAccess(); final PositionValueAccess store = new TestPositionValueAccess(); DataBlockSupplier chunks = (gridPos, existing) -> { return createDataBlock(chunkSize, gridPos.clone(), (byte) gridPos[0]); }; DataBlockSupplier chunks255 = (gridPos, existing) -> { return createDataBlock(chunkSize, gridPos.clone(), (byte)255); }; DataBlockSupplier chunksDelete = (gridPos, existing) -> { return null; }; // write one chunk at grid Position 1 datasetAccess.writeRegion(store, new long[] {3}, new long[] {3}, chunks, false); // Chunks // |...|...|...|...|...| // Pixels indexes // 0 3 6 9 12 15- checkChunk(datasetAccess.readChunk(store, new long[] {0}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[] {1}), true, 1); checkChunk(datasetAccess.readChunk(store, new long[] {2}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[] {3}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[] {4}), false, 0); // write two chunks at grid positions 2 and 3 datasetAccess.writeRegion(store, new long[]{6}, new long[]{6}, chunks, false); checkChunk(datasetAccess.readChunk(store, new long[] {0}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[] {1}), true, 1); checkChunk(datasetAccess.readChunk(store, new long[] {2}), true, 2); checkChunk(datasetAccess.readChunk(store, new long[] {3}), true, 3); checkChunk(datasetAccess.readChunk(store, new long[] {4}), false, 0); // delete two chunks at grid positions 3 and 4 datasetAccess.writeRegion(store, new long[]{9}, new long[]{6}, chunksDelete, false); } @Test public void testWriteRegionSharded() { // Shards // #...............................#...............................#...............................#...............................# // Chunks // |...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...| // Pixels indexes // 0 3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 int[] chunkSize = {3}; int[] shardSize = {24}; final long[] datasetDimensions = {96}; int numChunks = (int)(datasetDimensions[0] / chunkSize[0]); // Chunks are size 3 // Shards are size 24 (contain 8 chunks) final BlockCodecInfo c0 = new N5BlockCodecInfo(); final ShardCodecInfo c1 = new DefaultShardCodecInfo( chunkSize, c0, new DataCodecInfo[] {new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[] {new RawCompression()}, IndexLocation.END ); TestDatasetAttributes attributes = new TestDatasetAttributes( datasetDimensions, shardSize, DataType.INT8, c1, new RawCompression()); final DatasetAccess datasetAccess = attributes.getDatasetAccess(); final PositionValueAccess store = new TestPositionValueAccess(); DataBlockSupplier chunks = (gridPos, existing) -> { return createDataBlock(chunkSize, gridPos.clone(), (byte) gridPos[0]); }; DataBlockSupplier chunks255 = (gridPos, existing) -> { return createDataBlock(chunkSize, gridPos.clone(), (byte)255); }; DataBlockSupplier chunksDelete = (gridPos, existing) -> { return null; }; // write one chunk at grid Position 1 datasetAccess.writeRegion(store, new long[] {3}, new long[] {3}, chunks, false); checkChunk(datasetAccess.readChunk(store, new long[] {0}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[] {1}), true, 1); checkChunk(datasetAccess.readChunk(store, new long[] {2}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[] {3}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[] {4}), false, 0); // only the first shard should exist checkKey(store, new long[]{0}, true); checkKey(store, new long[]{1}, false); checkKey(store, new long[]{2}, false); checkKey(store, new long[]{3}, false); // write two chunks at grid positions 2 and 3 datasetAccess.writeRegion(store, new long[]{6}, new long[]{6}, chunks, false); checkChunk(datasetAccess.readChunk(store, new long[]{0}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[]{1}), true, 1); checkChunk(datasetAccess.readChunk(store, new long[]{2}), true, 2); checkChunk(datasetAccess.readChunk(store, new long[]{3}), true, 3); checkChunk(datasetAccess.readChunk(store, new long[]{4}), false, 0); // delete two chunks at grid positions 3 and 4 datasetAccess.writeRegion(store, new long[]{9}, new long[]{6}, chunksDelete, false); checkChunk(datasetAccess.readChunk(store, new long[]{0}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[]{1}), true, 1); checkChunk(datasetAccess.readChunk(store, new long[]{2}), true, 2); checkChunk(datasetAccess.readChunk(store, new long[]{3}), false, 0); checkChunk(datasetAccess.readChunk(store, new long[]{4}), false, 0); // overwrite all chunks to hold 255 datasetAccess.writeRegion(store, new long[]{0}, new long[]{96}, chunks255, false); for (int i = 0; i < numChunks; i++) { checkChunk(datasetAccess.readChunk(store, new long[]{i}), true, 255); } // all shards should exist checkKey(store, new long[]{0}, true); checkKey(store, new long[]{1}, true); checkKey(store, new long[]{2}, true); checkKey(store, new long[]{3}, true); // delete some chunks datasetAccess.writeRegion(store, new long[]{18}, new long[]{18}, chunksDelete, false); checkChunk(datasetAccess.readChunk(store, new long[]{5}), true, 255); checkChunk(datasetAccess.readChunk(store, new long[]{6}), false, 0); // all shards should exist checkKey(store, new long[]{0}, true); checkKey(store, new long[]{1}, true); checkKey(store, new long[]{2}, true); checkKey(store, new long[]{3}, true); // delete more chunks so that shard 1 is empty datasetAccess.writeRegion(store, new long[]{36}, new long[]{15}, chunksDelete, false); // shard 1 should be gone checkKey(store, new long[]{0}, true); checkKey(store, new long[]{1}, false); checkKey(store, new long[]{2}, true); checkKey(store, new long[]{3}, true); } private static void checkChunk(final DataBlock chunk, final boolean expectedNonNull, final int expectedFillValue) { if (chunk == null) { if (expectedNonNull) { throw new IllegalStateException("expected non-null dataBlock"); } } else { if (!expectedNonNull) { throw new IllegalStateException("expected null dataBlock"); } final byte[] bytes = chunk.getData(); for (byte b : bytes) { if (b != (byte) expectedFillValue) { throw new IllegalStateException("expected all values to be " + expectedFillValue); } } } } private static void checkKey(PositionValueAccess pva, long[] position, final boolean expectedNonNull) { try (VolatileReadData val = pva.get(position)) { if (expectedNonNull && val == null) throw new IllegalStateException("expected non-null value"); else if (!expectedNonNull && val != null) throw new IllegalStateException("expected null value"); } } private static DataBlock createDataBlock(int[] size, long[] gridPosition, int fillValue) { final byte[] bytes = new byte[DataBlock.getNumElements(size)]; Arrays.fill(bytes, (byte) fillValue); return new ByteArrayDataBlock(size, gridPosition, bytes); } public static class TestDatasetAttributes extends DatasetAttributes { public TestDatasetAttributes(long[] dimensions, int[] outerBlockSize, DataType dataType, BlockCodecInfo blockCodecInfo, DataCodecInfo... dataCodecInfos) { super(dimensions, outerBlockSize, dataType, blockCodecInfo, dataCodecInfos); } @Override // to make this accessible for the test protected DatasetAccess getDatasetAccess() { return super.getDatasetAccess(); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/shard/WriteShardTest.java ================================================ package org.janelia.saalfeldlab.n5.shard; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import java.util.Arrays; import java.util.stream.Collectors; import org.apache.commons.lang3.stream.Streams; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.IntArrayDataBlock; import org.janelia.saalfeldlab.n5.RawCompression; import org.janelia.saalfeldlab.n5.codec.BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.N5BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.RawBlockCodecInfo; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; import org.junit.Test; public class WriteShardTest { // #...............................#...............................#...............................#...............................# // $.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$ // |...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...| // 0 3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 public static void main(String[] args) { final int[] chunkSize = {3}; final int[] level1ShardSize = {6}; final long[] datasetDimensions = {36}; // Chunks are 3 // Level 1 shards are 6 final BlockCodecInfo c0 = new N5BlockCodecInfo(); final ShardCodecInfo c1 = new DefaultShardCodecInfo( chunkSize, c0, new DataCodecInfo[] {new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[] {new RawCompression()}, IndexLocation.END ); TestDatasetAttributes attributes = new TestDatasetAttributes( datasetDimensions, level1ShardSize, DataType.INT32, c1, new RawCompression()); final DatasetAccess datasetAccess = attributes.getDatasetAccess(); final PositionValueAccess store = new TestPositionValueAccess(); // 0 1 2 3 4 5 6 // $.......$.......$.......$.......$.......$.......$ // 0 1 2 3 4 5 6 7 8 9 10 11 12 // |...|...|...|...|...|...|...|...|...|...|...|...| // 0 3 6 9 12 15 18 21 24 27 30 33 36 // ................................................. final int[] dataBlockSize = c1.getInnerBlockSize(); // create a shard DataBlock { final DataBlock shard = createDataBlock(level1ShardSize, new long[] {2}, 1); System.out.println("shard.getGridPosition() = " + Arrays.toString(shard.getGridPosition())); System.out.println("shard.getSize() = " + Arrays.toString(shard.getSize())); System.out.println("shard.getData() = " + Arrays.toString(shard.getData())); System.out.println(); datasetAccess.writeBlock(store, shard, 1); System.out.println(); System.out.println(); } { final DataBlock shard = createDataBlock(level1ShardSize, new long[] {5}, 1); System.out.println("shard.getGridPosition() = " + Arrays.toString(shard.getGridPosition())); System.out.println("shard.getSize() = " + Arrays.toString(shard.getSize())); System.out.println("shard.getData() = " + Arrays.toString(shard.getData())); System.out.println(); datasetAccess.writeBlock(store, shard, 1); System.out.println(); System.out.println(); } System.out.println("all good"); } @Test public void testShardDatasetAccess() { final int[] chunkSize = {3}; final int[] level1ShardSize = {6}; final long[] datasetDimensions = {36}; // Chunks are 3 // Level 1 shards are 6 final BlockCodecInfo c0 = new N5BlockCodecInfo(); final ShardCodecInfo c1 = new DefaultShardCodecInfo( chunkSize, c0, new DataCodecInfo[] {new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[] {new RawCompression()}, IndexLocation.END ); TestDatasetAttributes attributes = new TestDatasetAttributes( datasetDimensions, level1ShardSize, DataType.INT32, c1, new RawCompression()); final DatasetAccess datasetAccess = attributes.getDatasetAccess(); final PositionValueAccess store = new TestPositionValueAccess(); // 0 1 2 3 4 5 6 // $.......$.......$.......$.......$.......$.......$ // 0 1 2 3 4 5 6 7 8 9 10 11 12 // |...|...|...|...|...|...|...|...|...|...|...|...| // 0 3 6 9 12 15 18 21 24 27 30 33 36 // ................................................. long[] p = {0}; assertFalse(store.exists(p)); } @Test public void testWriteNullBlockRemovesShard() throws Exception { final int[] chunkSize = {3}; final int[] level1ShardSize = {6}; final long[] datasetDimensions = {36}; // Level 1 shards have size 6 (each containing two datablocks of size 3) final BlockCodecInfo c0 = new N5BlockCodecInfo(); final ShardCodecInfo c1 = new DefaultShardCodecInfo( chunkSize, c0, new DataCodecInfo[]{new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[]{new RawCompression()}, IndexLocation.END); TestDatasetAttributes attributes = new TestDatasetAttributes( datasetDimensions, level1ShardSize, DataType.INT32, c1, new RawCompression()); final DatasetAccess datasetAccess = attributes.getDatasetAccess(); final PositionValueAccess store = new TestPositionValueAccess(); final long[] shardKey = {1}; /** * ONE BLOCK, ONE SHARD */ assertFalse("Shard should not exist at the start of the test", store.exists(shardKey)); // Write a single chunk at grid position [3] // This chunk is in shard [1] // ( shard 0 contains chunks 0-1, // shard 1 contains chunks 2-3 ) final long[] chunkGridPosition = {3}; final DataBlock chunk = createDataBlock(chunkSize, chunkGridPosition, 100); datasetAccess.writeChunk(store, chunk); // Verify the shard exists assertTrue("Shard should exist after writing chunk", store.exists(shardKey)); // Write a null chunk at the same location using writeRegion // This should remove the chunk and delete the now-empty shard final long[] regionMin = {9}; // pixel position of chunk [3] final long[] regionSize = {3}; // size of one block datasetAccess.writeRegion( store, regionMin, regionSize, (gridPosition, // block supplier returns null to indicate removal existingBlock) -> null, false); // Verify the shard has been removed assertFalse("Shard should be removed after writing null chunk", store.exists(shardKey)); /** * THREE CHUNKS, TWO SHARDS */ // Write two chunks into the same shard, and one chunk into a second shard // Shard [1] contains chunks [2] and [3] // Shard [2] contains chunk [4] final long[] shardKey1 = {1}; final long[] shardKey2 = {2}; final DataBlock chunk1 = createDataBlock(chunkSize, new long[]{2}, 100); final DataBlock chunk2 = createDataBlock(chunkSize, new long[]{3}, 200); final DataBlock chunk3 = createDataBlock(chunkSize, new long[]{4}, 300); assertFalse("Shard should not exist at the start of the test", store.exists(shardKey1)); assertFalse("Shard should not exist at the start of the test", store.exists(shardKey2)); // write blocks datasetAccess.writeChunks(store, Streams.of(chunk1, chunk2, chunk3).collect(Collectors.toList())); // Verify the shard exists assertTrue("Shard should exist after writing chunks", store.exists(shardKey1)); assertTrue("Shard should exist after writing chunks", store.exists(shardKey2)); // Write a null block at block [3]'s location datasetAccess.writeRegion( store, regionMin, regionSize, (gridPosition, existingBlock) -> null, false); // Verify the shard still exists (because chunk [2] is still there) assertTrue("Shard should still exist because it contains another chunk", store.exists(shardKey1)); assertTrue("Shard should still exist because was not affected", store.exists(shardKey2)); // Verify we can still read chunk [2] final DataBlock readChunk = datasetAccess.readChunk(store, new long[]{2}); assertTrue("Block [2] should still be readable", readChunk != null); assertTrue("Block [2] data should match", Arrays.equals(chunk1.getData(), readChunk.getData())); // Verify chunk [3] is gone final DataBlock readChunk2 = datasetAccess.readChunk(store, new long[]{3}); assertNull("Block [3] should be null after removal", readChunk2); // Verify chunk [4] exists final DataBlock readChunk3 = datasetAccess.readChunk(store, new long[]{4}); assertTrue("Block [4] should still be readable", readChunk3 != null); assertTrue("Block [4] data should match", Arrays.equals(chunk3.getData(), readChunk3.getData())); // Write a null chunk at chunk [2]'s location final long[] regionMin2 = {6}; // pixel position of chunk [3] final long[] regionSize2 = {3}; datasetAccess.writeRegion( store, regionMin2, regionSize2, (gridPosition, existingBlock) -> null, false); assertFalse("Shard should not exist after deleting all chunks", store.exists(shardKey1)); assertTrue("Shard should still exist because was not affected", store.exists(shardKey2)); } private static DataBlock createDataBlock(int[] size, long[] gridPosition, int startValue) { final int[] ints = new int[DataBlock.getNumElements(size)]; Arrays.setAll(ints, i -> i + startValue); return new IntArrayDataBlock(size, gridPosition, ints); } public static class TestDatasetAttributes extends DatasetAttributes { public TestDatasetAttributes(long[] dimensions, int[] outerBlockSize, DataType dataType, BlockCodecInfo blockCodecInfo, DataCodecInfo... dataCodecInfos) { super(dimensions, outerBlockSize, dataType, blockCodecInfo, dataCodecInfos); } @Override // to make this accessible for the test protected DatasetAccess getDatasetAccess() { return super.getDatasetAccess(); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/shard/WriteShardTest2.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.Arrays; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.IntArrayDataBlock; import org.janelia.saalfeldlab.n5.RawCompression; import org.janelia.saalfeldlab.n5.codec.BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.N5BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.RawBlockCodecInfo; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; public class WriteShardTest2 { // #...............................#...............................#...............................#...............................# // $.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$ // |...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...| // 0 3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 public static void main(String[] args) { final int[] chunkSize = {3,3}; final int[] level1ShardSize = {6,6}; final long[] datasetDimensions = {36, 18}; // Chunks are 3x3 // Level 1 shards are 6x6 final BlockCodecInfo c0 = new N5BlockCodecInfo(); final ShardCodecInfo c1 = new DefaultShardCodecInfo( chunkSize, c0, new DataCodecInfo[] {new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[] {new RawCompression()}, IndexLocation.END ); TestDatasetAttributes attributes = new TestDatasetAttributes( datasetDimensions, level1ShardSize, DataType.INT32, c1, new RawCompression()); final DatasetAccess datasetAccess = attributes.getDatasetAccess(); final PositionValueAccess store = new TestPositionValueAccess(); // 0 1 2 3 4 5 6 // $.......$.......$.......$.......$.......$.......$ // 0 1 2 3 4 5 6 7 8 9 10 11 12 // |...|...|...|...|...|...|...|...|...|...|...|...| // 0 3 6 9 12 15 18 21 24 27 30 33 36 // ................................................. final int[] dataBlockSize = c1.getInnerBlockSize(); // create and write a shard DataBlock final DataBlock shard = createDataBlock(level1ShardSize, new long[] {2,1}, 1); System.out.println("shard.getGridPosition() = " + Arrays.toString(shard.getGridPosition())); System.out.println("shard.getSize() = " + Arrays.toString(shard.getSize())); System.out.println("shard.getData() = " + Arrays.toString(shard.getData())); datasetAccess.writeBlock(store, shard, 1); // we should get these Chunk values // 1, 2, 3, | 4, 5, 6, // 7, 8, 9, | 10, 11, 12, // 13, 14, 15, | 16, 17, 18, // ------------+------------ // 19, 20, 21, | 22, 23, 24, // 25, 26, 27, | 28, 29, 30, // 31, 32, 33, | 34, 35, 36 System.out.println("{4, 2}.data = " + Arrays.toString( datasetAccess.readChunk(store, new long[] {4, 2}).getData())); System.out.println("{5, 2}.data = " + Arrays.toString( datasetAccess.readChunk(store, new long[] {5, 2}).getData())); System.out.println("{4, 3}.data = " + Arrays.toString( datasetAccess.readChunk(store, new long[] {4, 3}).getData())); System.out.println("{5, 3}.data = " + Arrays.toString( datasetAccess.readChunk(store, new long[] {5, 3}).getData())); final DataBlock readShard = datasetAccess.readBlock(store, new long[] {2, 1}, 1); System.out.println("readShard.getData() = " + Arrays.toString(readShard.getData())); } private static DataBlock createDataBlock(int[] size, long[] gridPosition, int startValue) { final int[] ints = new int[DataBlock.getNumElements(size)]; Arrays.setAll(ints, i -> i + startValue); return new IntArrayDataBlock(size, gridPosition, ints); } public static class TestDatasetAttributes extends DatasetAttributes { public TestDatasetAttributes(long[] dimensions, int[] outerBlockSize, DataType dataType, BlockCodecInfo blockCodecInfo, DataCodecInfo... dataCodecInfos) { super(dimensions, outerBlockSize, dataType, blockCodecInfo, dataCodecInfos); } @Override // to make this accessible for the test protected DatasetAccess getDatasetAccess() { return super.getDatasetAccess(); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/shard/WriteShardTestTruncate.java ================================================ package org.janelia.saalfeldlab.n5.shard; import java.util.Arrays; import org.janelia.saalfeldlab.n5.DataBlock; import org.janelia.saalfeldlab.n5.DataType; import org.janelia.saalfeldlab.n5.DatasetAttributes; import org.janelia.saalfeldlab.n5.IntArrayDataBlock; import org.janelia.saalfeldlab.n5.RawCompression; import org.janelia.saalfeldlab.n5.codec.BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.DataCodecInfo; import org.janelia.saalfeldlab.n5.codec.N5BlockCodecInfo; import org.janelia.saalfeldlab.n5.codec.RawBlockCodecInfo; import org.janelia.saalfeldlab.n5.shard.ShardIndex.IndexLocation; public class WriteShardTestTruncate { // #...............................#...............................#...............................#...............................# // $.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$.......$ // |...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...|...| // 0 3 6 9 12 15 18 21 24 27 30 33 36 39 42 45 48 51 54 57 60 63 66 69 72 75 78 81 84 87 90 93 96 public static void main(String[] args) { final int[] chunkSize = {3,3}; final int[] level1ShardSize = {6,6}; final long[] datasetDimensions = {34, 17}; // 6 x 3 shards, border (2, 1) // Chunks are 3x3 // Level 1 shards are 6x6 final BlockCodecInfo c0 = new N5BlockCodecInfo(); final ShardCodecInfo c1 = new DefaultShardCodecInfo( chunkSize, c0, new DataCodecInfo[] {new RawCompression()}, new RawBlockCodecInfo(), new DataCodecInfo[] {new RawCompression()}, IndexLocation.END ); TestDatasetAttributes attributes = new TestDatasetAttributes( datasetDimensions, level1ShardSize, DataType.INT32, c1, new RawCompression()); final DatasetAccess datasetAccess = attributes.getDatasetAccess(); final PositionValueAccess store = new TestPositionValueAccess(); // 0 1 2 3 4 5 6 // $.......$.......$.......$.......$.......$.......$ // 0 1 2 3 4 5 6 7 8 9 10 11 12 // |...|...|...|...|...|...|...|...|...|...|...|...| // 0 3 6 9 12 15 18 21 24 27 30 33 36 // ................................................. final int[] dataBlockSize = c1.getInnerBlockSize(); // create and write a shard DataBlock final DataBlock shard = createDataBlock(level1ShardSize, new long[] {5,2}, 1); System.out.println("shard.getGridPosition() = " + Arrays.toString(shard.getGridPosition())); System.out.println("shard.getSize() = " + Arrays.toString(shard.getSize())); System.out.println("shard.getData() = " + Arrays.toString(shard.getData())); datasetAccess.writeBlock(store, shard, 1); // we should get these chunk values // 1, 2, 3, | 4, 5, 6, // 7, 8, 9, | 10, 11, 12, // 13, 14, 15, | 16, 17, 18, // ------------+------------ // 19, 20, 21, | 22, 23, 24, // 25, 26, 27, | 28, 29, 30, // 31, 32, 33, | 34, 35, 36 System.out.println("{10, 4}.data = " + Arrays.toString( datasetAccess.readChunk(store, new long[] {10, 4}).getData())); System.out.println("{11, 4}.data = " + Arrays.toString( datasetAccess.readChunk(store, new long[] {11, 4}).getData())); System.out.println("{10, 5}.data = " + Arrays.toString( datasetAccess.readChunk(store, new long[] {10, 5}).getData())); System.out.println("{11, 5}.data = " + Arrays.toString( datasetAccess.readChunk(store, new long[] {11, 5}).getData())); // 1, 2, 3, | 4 // 7, 8, 9, | 10 // 13, 14, 15, | 16 // ------------+--- // 19, 20, 21, | 22 // 25, 26, 27, | 28 final DataBlock readShard = datasetAccess.readBlock(store, new long[] {5, 2}, 1); System.out.println("readShard.getData() = " + Arrays.toString(readShard.getData())); } private static DataBlock createDataBlock(int[] size, long[] gridPosition, int startValue) { final int[] ints = new int[DataBlock.getNumElements(size)]; Arrays.setAll(ints, i -> i + startValue); return new IntArrayDataBlock(size, gridPosition, ints); } public static class TestDatasetAttributes extends DatasetAttributes { public TestDatasetAttributes(long[] dimensions, int[] outerBlockSize, DataType dataType, BlockCodecInfo blockCodecInfo, DataCodecInfo... dataCodecInfos) { super(dimensions, outerBlockSize, dataType, blockCodecInfo, dataCodecInfos); } @Override // to make this accessible for the test protected DatasetAccess getDatasetAccess() { return super.getDatasetAccess(); } } } ================================================ FILE: src/test/java/org/janelia/saalfeldlab/n5/url/UriAttributeTest.java ================================================ package org.janelia.saalfeldlab.n5.url; import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import java.io.IOException; import java.net.URISyntaxException; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.janelia.saalfeldlab.n5.N5FSWriter; import org.janelia.saalfeldlab.n5.N5Reader; import org.janelia.saalfeldlab.n5.N5URI; import org.junit.Assert; import org.junit.Before; import org.junit.Test; public class UriAttributeTest { N5Reader n5; String rootContext = ""; int[] list; HashMap obj; Set rootKeys; TestInts testObjInts; TestDoubles testObjDoubles; @Before public void before() { n5 = new N5FSWriter("src/test/resources/url/urlAttributes.n5"); rootContext = ""; list = new int[]{0, 1, 2, 3}; obj = new HashMap<>(); obj.put("a", "aa"); obj.put("b", "bb"); rootKeys = new HashSet<>(); rootKeys.addAll(Stream.of("n5", "foo", "list", "object").collect(Collectors.toList())); testObjInts = new TestInts("ints", "intsName", new int[]{5, 4, 3}); testObjDoubles = new TestDoubles("doubles", "doublesName", new double[]{5.5, 4.4, 3.3}); } @SuppressWarnings("unchecked") @Test public void testRootAttributes() throws URISyntaxException, IOException { // get final Map everything = getAttribute(n5, new N5URI(""), Map.class); final String version = "2.6.1"; // the n5 version at the time the test // data were created assertEquals("empty url", version, everything.get("n5")); final Map everything2 = getAttribute(n5, new N5URI("#/"), Map.class); assertEquals("root attribute", version, everything2.get("n5")); assertEquals("url to attribute", "bar", getAttribute(n5, new N5URI("#foo"), String.class)); assertEquals("url to attribute absolute", "bar", getAttribute(n5, new N5URI("#/foo"), String.class)); assertEquals("#foo", "bar", getAttribute(n5, new N5URI("#foo"), String.class)); assertEquals("#/foo", "bar", getAttribute(n5, new N5URI("#/foo"), String.class)); assertEquals("?#foo", "bar", getAttribute(n5, new N5URI("?#foo"), String.class)); assertEquals("?#/foo", "bar", getAttribute(n5, new N5URI("?#/foo"), String.class)); assertEquals("?/#/foo", "bar", getAttribute(n5, new N5URI("?/#/foo"), String.class)); assertEquals("?/.#/foo", "bar", getAttribute(n5, new N5URI("?/.#/foo"), String.class)); assertEquals("?./#/foo", "bar", getAttribute(n5, new N5URI("?./#/foo"), String.class)); assertEquals("?.#foo", "bar", getAttribute(n5, new N5URI("?.#foo"), String.class)); assertEquals("?/a/..#foo", "bar", getAttribute(n5, new N5URI("?/a/..#foo"), String.class)); assertEquals("?/a/../.#foo", "bar", getAttribute(n5, new N5URI("?/a/../.#foo"), String.class)); /* whitespace-encoding-necesary, fragment-only test */ assertEquals("#f o o", "b a r", getAttribute(n5, new N5URI("#f o o"), String.class)); Assert.assertArrayEquals("url list", list, getAttribute(n5, new N5URI("#list"), int[].class)); // list assertEquals("url list[0]", list[0], (int)getAttribute(n5, new N5URI("#list[0]"), Integer.class)); assertEquals("url list[1]", list[1], (int)getAttribute(n5, new N5URI("#list[1]"), Integer.class)); assertEquals("url list[2]", list[2], (int)getAttribute(n5, new N5URI("#list[2]"), Integer.class)); assertEquals("url list[3]", list[3], (int)getAttribute(n5, new N5URI("#list[3]"), Integer.class)); assertEquals("url list/[3]", list[3], (int)getAttribute(n5, new N5URI("#list/[3]"), Integer.class)); assertEquals("url list//[3]", list[3], (int)getAttribute(n5, new N5URI("#list//[3]"), Integer.class)); assertEquals("url //list//[3]", list[3], (int)getAttribute(n5, new N5URI("#//list//[3]"), Integer.class)); assertEquals("url //list//[3]//", list[3], (int)getAttribute(n5, new N5URI("#//list////[3]//"), Integer.class)); // object assertTrue("url object", mapsEqual(obj, getAttribute(n5, new N5URI("#object"), Map.class))); assertEquals("url object/a", "aa", getAttribute(n5, new N5URI("#object/a"), String.class)); assertEquals("url object/b", "bb", getAttribute(n5, new N5URI("#object/b"), String.class)); } @Test public void testPathAttributes() throws URISyntaxException, IOException { final String a = "a"; final String aa = "aa"; final String aaa = "aaa"; final N5URI aUrl = new N5URI("?/a"); final N5URI aaUrl = new N5URI("?/a/aa"); final N5URI aaaUrl = new N5URI("?/a/aa/aaa"); // name of a assertEquals("name of a from root", a, getAttribute(n5, "?/a#name", String.class)); assertEquals("name of a from root", a, getAttribute(n5, new N5URI("?a#name"), String.class)); assertEquals("name of a from a", a, getAttribute(n5, aUrl.resolve(new N5URI("?/a#name")), String.class)); assertEquals("name of a from aa", a, getAttribute(n5, aaUrl.resolve("?..#name"), String.class)); assertEquals("name of a from aaa", a, getAttribute(n5, aaaUrl.resolve(new N5URI("?../..#name")), String.class)); // name of aa assertEquals("name of aa from root", aa, getAttribute(n5, new N5URI("?/a/aa#name"), String.class)); assertEquals("name of aa from root", aa, getAttribute(n5, new N5URI("?a/aa#name"), String.class)); assertEquals("name of aa from a", aa, getAttribute(n5, aUrl.resolve("?aa#name"), String.class)); assertEquals("name of aa from aa", aa, getAttribute(n5, aaUrl.resolve("?./#name"), String.class)); assertEquals("name of aa from aa", aa, getAttribute(n5, aaUrl.resolve("#name"), String.class)); assertEquals("name of aa from aaa", aa, getAttribute(n5, aaaUrl.resolve("?..#name"), String.class)); // name of aaa assertEquals("name of aaa from root", aaa, getAttribute(n5, new N5URI("?/a/aa/aaa#name"), String.class)); assertEquals("name of aaa from root", aaa, getAttribute(n5, new N5URI("?a/aa/aaa#name"), String.class)); assertEquals("name of aaa from a", aaa, getAttribute(n5, aUrl.resolve("?aa/aaa#name"), String.class)); assertEquals("name of aaa from aa", aaa, getAttribute(n5, aaUrl.resolve("?aaa#name"), String.class)); assertEquals("name of aaa from aaa", aaa, getAttribute(n5, aaaUrl.resolve("#name"), String.class)); assertEquals("name of aaa from aaa", aaa, getAttribute(n5, aaaUrl.resolve("?./#name"), String.class)); assertEquals("nested list 1", (Integer)1, getAttribute(n5, new N5URI("#nestedList[0][0][0]"), Integer.class)); assertEquals("nested list 1", (Integer)1, getAttribute(n5, new N5URI("#/nestedList/[0][0][0]"), Integer.class)); assertEquals( "nested list 1", (Integer)1, getAttribute(n5, new N5URI("#nestedList//[0]/[0]///[0]"), Integer.class)); assertEquals( "nested list 1", (Integer)1, getAttribute(n5, new N5URI("#/nestedList[0]//[0][0]"), Integer.class)); } private T getAttribute( final N5Reader n5, final String url1, final Class clazz) throws URISyntaxException, IOException { return getAttribute(n5, url1, null, clazz); } private T getAttribute( final N5Reader n5, final String url1, final String url2, final Class clazz) throws URISyntaxException, IOException { final N5URI n5URL = url2 == null ? new N5URI(url1) : new N5URI(url1).resolve(url2); return getAttribute(n5, n5URL, clazz); } private T getAttribute( final N5Reader n5, final N5URI url1, final Class clazz) throws URISyntaxException, IOException { return getAttribute(n5, url1, null, clazz); } private T getAttribute( final N5Reader n5, final N5URI url1, final String url2, final Class clazz) throws URISyntaxException, IOException { final N5URI n5URL = url2 == null ? url1 : url1.resolve(url2); return n5.getAttribute(n5URL.getGroupPath(), n5URL.getAttributePath(), clazz); } @Test public void testPathObject() throws IOException, URISyntaxException { final TestInts ints = getAttribute(n5, new N5URI("?objs#intsKey"), TestInts.class); assertEquals(testObjInts.name, ints.name); assertEquals(testObjInts.type, ints.type); assertArrayEquals(testObjInts.t(), ints.t()); final TestDoubles doubles = getAttribute(n5, new N5URI("?objs#doublesKey"), TestDoubles.class); assertEquals(testObjDoubles.name, doubles.name); assertEquals(testObjDoubles.type, doubles.type); assertArrayEquals(testObjDoubles.t(), doubles.t(), 1e-9); final TestDoubles[] doubleArray = getAttribute(n5, new N5URI("?objs#array"), TestDoubles[].class); final TestDoubles doubles1 = new TestDoubles("doubles", "doubles1", new double[]{5.7, 4.5, 3.4}); final TestDoubles doubles2 = new TestDoubles("doubles", "doubles2", new double[]{5.8, 4.6, 3.5}); final TestDoubles doubles3 = new TestDoubles("doubles", "doubles3", new double[]{5.9, 4.7, 3.6}); final TestDoubles doubles4 = new TestDoubles("doubles", "doubles4", new double[]{5.10, 4.8, 3.7}); final TestDoubles[] expectedDoubles = new TestDoubles[]{doubles1, doubles2, doubles3, doubles4}; assertArrayEquals(expectedDoubles, doubleArray); final String[] stringArray = getAttribute(n5, new N5URI("?objs#String"), String[].class); final String[] expectedString = new String[]{"This", "is", "a", "test"}; final Integer[] integerArray = getAttribute(n5, new N5URI("?objs#Integer"), Integer[].class); final Integer[] expectedInteger = new Integer[]{1, 2, 3, 4}; final int[] intArray = getAttribute(n5, new N5URI("?objs#int"), int[].class); final int[] expectedInt = new int[]{1, 2, 3, 4}; assertArrayEquals(expectedInt, intArray); } private boolean mapsEqual(final Map a, final Map b) { if (!a.keySet().equals(b.keySet())) return false; for (final K k : a.keySet()) { if (!a.get(k).equals(b.get(k))) return false; } return true; } private static class TestObject { String type; String name; T t; public TestObject(final String type, final String name, final T t) { this.name = name; this.type = type; this.t = t; } public T t() { return t; } @Override public boolean equals(final Object obj) { if (obj.getClass() == this.getClass()) { final TestObject otherTestObject = (TestObject)obj; return Objects.equals(name, otherTestObject.name) && Objects.equals(type, otherTestObject.type) && Objects.equals(t, otherTestObject.t); } return false; } } public static class TestDoubles extends TestObject { public TestDoubles(final String type, final String name, final double[] t) { super(type, name, t); } @Override public boolean equals(final Object obj) { if (obj.getClass() == this.getClass()) { final TestDoubles otherTestObject = (TestDoubles)obj; return Objects.equals(name, otherTestObject.name) && Objects.equals(type, otherTestObject.type) && Arrays.equals(t, otherTestObject.t); } return false; } } private static class TestInts extends TestObject { public TestInts(final String type, final String name, final int[] t) { super(type, name, t); } @Override public boolean equals(final Object obj) { if (obj.getClass() == this.getClass()) { final TestInts otherTestObject = (TestInts)obj; return Objects.equals(name, otherTestObject.name) && Objects.equals(type, otherTestObject.type) && Arrays.equals(t, otherTestObject.t); } return false; } } } ================================================ FILE: src/test/resources/backward/data-1.5.0.n5/attributes.json ================================================ {"n5":"1.5.0"} ================================================ FILE: src/test/resources/backward/data-1.5.0.n5/raw/attributes.json ================================================ {"dataType":"uint8","compressionType":"raw","blockSize":[5,4],"dimensions":[7,5]} ================================================ FILE: src/test/resources/backward/data-2.5.1.n5/attributes.json ================================================ {"n5":"2.5.1"} ================================================ FILE: src/test/resources/backward/data-2.5.1.n5/raw/attributes.json ================================================ {"dataType":"uint8","compression":{"type":"raw"},"blockSize":[5,4],"dimensions":[7,5]} ================================================ FILE: src/test/resources/backward/data-3.1.3.n5/attributes.json ================================================ {"n5":"4.0.0"} ================================================ FILE: src/test/resources/backward/data-3.1.3.n5/raw/attributes.json ================================================ {"dataType":"uint8","compression":{"type":"raw"},"blockSize":[5,4],"dimensions":[7,5]} ================================================ FILE: src/test/resources/url/urlAttributes.n5/a/aa/aaa/attributes.json ================================================ { "name" : "aaa" } ================================================ FILE: src/test/resources/url/urlAttributes.n5/a/aa/attributes.json ================================================ { "name" : "aa" } ================================================ FILE: src/test/resources/url/urlAttributes.n5/a/attributes.json ================================================ { "name" : "a" } ================================================ FILE: src/test/resources/url/urlAttributes.n5/attributes.json ================================================ {"n5":"2.6.1","foo":"bar","f o o":"b a r","list":[0,1,2,3],"nestedList":[[[1,2,3,4]],[[10,20,30,40]],[[100,200,300,400]],[[1000,2000,3000,4000]]],"object":{"a":"aa","b":"bb"}} ================================================ FILE: src/test/resources/url/urlAttributes.n5/objs/attributes.json ================================================ { "intsKey": { "type": "ints", "name": "intsName", "t": [ 5, 4, 3 ] }, "doublesKey": { "type": "doubles", "name": "doublesName", "t": [ 5.5, 4.4, 3.3 ] }, "int": [1, 2,3,4], "Integer": [1, 2,3,4], "String": ["This", "is", "a", "test"], "array": [ { "type": "doubles", "name": "doubles1", "t": [ 5.7, 4.5, 3.4 ] }, { "type": "doubles", "name": "doubles2", "t": [ 5.8, 4.6, 3.5 ] }, { "type": "doubles", "name": "doubles3", "t": [ 5.9, 4.7, 3.6 ] }, { "type": "doubles", "name": "doubles4", "t": [ 5.10, 4.8, 3.7 ] } ] }