-
Notifications
You must be signed in to change notification settings - Fork 22
URLs
This describes a URL scheme to reference hierarchical / chunked storage containers (hdf5, n5, and zarr), their groups / datasets, and attributes.
The URL consists of three parts, the container
, the group
, and the attribute
:
container?group#attribute
Where the container
specifies a path to the root of the container (usually file system or cloud storage), the group
specifies a path to the group, relative to the container root, and the attribute
specifies the path to an attribute relative to the group.
The URL syntax is based on w3c's syntax for URI's, outlined by the diagram below.
A URL is formatted as:
container?group#attribute
Which was chosen to align closely to general URIs, which are formatted as:
-
scheme://userinfo@host:port/path?query#fragment
- where the
userinfo@host:port
is called theauthority
- Having an authority is supported, but all current use cases have no authority
- where the
Our URL is such that
- the
container
is thescheme
,authority
, andpath
of the URI - the
group
is thequery
of the URI - the
attribute
is thefragment
of the URI
For example:
URL: container group attribute
____________________________|_____________________________ _______|______ ____|____
/ \ / \ / \
s3://janelia-cosem-datasets/jrc_mus-kidney/jrc_mus-kidney.n5?/em/fibsem-uint8#multiscales
\_/ \_____________________________________________________/ \______________/ \________/
| | | |
URI: scheme path query fragment
When using the N5-API, metadata attributes are generally refered to by a string valued name or "key".
For example, the version of an n5 container is stored at the key "n5"
:
{ "n5" : "2.6.1" }
Note We will visualize attributes using JSON here, but the same principles will work for backends that do not store attributes with JSON (e.g. HDF5).
Attributes can be written to a container using N5Writer
's setAttribute( String group, String key, Object value)
method and
read using N5Reader
's getAttribute( String group, String key, Class class)
method.
n5.setAttribute("group", "six", 6);
n5.getAttribute("group", "six", int.class); // returns 6
the result
{ "six" : 6 }
where the examples here assume a variable n5
exists of type N5Writer
. Keys may be long and contain whitespace:
n5.setAttribute("group", "The Answer to the Ultimate Question of Life, The Universe, and Everything", 42);
n5.getAttribute("group", "The Answer to the Ultimate Question of Life, The Universe, and Everything", int.class); // returns 42
n5.getAttribute("group", "The Answer to the Ultimate Question of Life, The Universe, and Everything", String.class); // returns "42"
Note that the requested output type need not be the same as the input type, it is only required that the requested type be
We see above that int
types may be interpreted as String
s, but the reverse is not possible, in general.
n5.setAttribute("group", "name", "Marie Daly");
n5.getAttribute("group", "name", String.class); // returns "Marie Daly"
n5.getAttribute("group", "name", int.class); // returns null
n5.setAttribute("group", "year", "1921"); // write the year as a string
n5.getAttribute("group", "name", String.class); // returns "1921"
n5.getAttribute("group", "name", int.class); // returns 1921
The value of a given attribute can be a more complex type, such an array.
n5.setAttribute(group, "array", new double[]{ 5, 6, 7, 8 });
n5.getAttribute(group, "array", double[].class))); // returns [5.0, 6.0, 7.0, 8.0]
Individual elements of the array can be retrieved by adding [i]
after the key, where i
is an integer (zero-based indexing).
N5 will return null
for indexes outside the bounds of the array, including for negative values.
n5.getAttribute(group, "array[0]", double.class); // returns 5.0
n5.getAttribute(group, "array[2]", double.class); // returns 7.0
n5.getAttribute(group, "array[9]", double.class); // returns null
n5.getAttribute(group, "array[-1]", double.class); // returns null
This notation may be used to set array values as well. Arrays will grow in size if they are too small to fit the requested (positive) index. Numeric arrays will be filled with zero. Non-numeric arrays will be filled with null. Setting the value at a negative array index does nothing.
n5.setAttribute(group, "array[1]", 0.6); // array is now [ 5.0, 0.6, 7.0, 8.0 ]
n5.setAttribute(group, "array[6]", 99.99); // array is now [ 5.0, 0.6, 7.0, 8.0, 0.0, 0.0, 99.99 ]
n5.setAttribute(group, "array[-5]", -5); // array is now [ 5.0, 0.6, 7.0, 8.0, 0.0, 0.0, 99.99 ]
N5's setAttribute
will always do what is requested when possible, even if it will overwrite data.
If safety is necessary, developers should manually check if an attribute key is present. Use of the type JsonElement
type is
the most safe, because a non-null JsonElement
will be returned if data of any type is present at the requested key.
n5.setAttribute(group, "array", new String[]{"destroy"}); // array is now [ "destroy" ]
if( n5.getAttribute( group, "array", JsonElement.class ) == null )
n5.setAttribute(group, "array", new String[]{}); // array is still [ "destroy" ]
if( n5.getAttribute( group, "array", double[].class ) == null )
n5.setAttribute(group, "array", new String[]{}); // array is now []
Objects are structures with "fields" that can be referenced by their String name. One way to set objects is by using a Map
.
Map a = Collections.singletonMap("a", "A");
Map b = Collections.singletonMap("b", "B");
Map c = Collections.singletonMap("c", "C");
n5.setAttribute(group, "obj", a );
n5.getAttribute(group, "obj", Map.class); // returns {"a": "A"}
The value for an object's field can be any type, even another object. Individual fields for an object can be accessed
by appending /<field-name>
to the attribute name. For example:
n5.setAttribute(group, "obj/a", b);
n5.getAttribute(group, "obj", Map.class); // returns {"a": {"b": "B"}}
n5.getAttribute(group, "obj/a", Map.class); // returns {"b": "B"}
n5.setAttribute(group, "obj/a", b);
n5.getAttribute(group, "obj", Map.class); // returns {"a": {"b": "B"}}
n5.getAttribute(group, "obj/a", Map.class); // returns {"b": "B"}
n5.setAttribute(group, "obj/a/b", c);
n5.getAttribute(group, "obj", Map.class); // returns {"a": {"b": {"c": "C"}}}
n5.getAttribute(group, "obj/a", Map.class); // returns {"b": {"c": "C"}}
n5.getAttribute(group, "obj/a/b", Map.class); // returns {"c": "C"}
Notice that it is possible to repeatedly access subfields of nested objects. In fact, the set of all attributes in
an N5 group is usually itself an object! We call it the "root object" and access it with the the path "/"
n5.getAttribute(group, "/", Map.class); // returns {"obj": {"a": {"b": {"c": "C"}}}}
Besides Map
s, one can set at attribute's value using general structured data, as an object. For example consider the Pet
type with String name
and int age
.
Definition of `Pet`
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);
}
}
n5.setAttribute(group, "pet", new Pet("Pluto", 93));
n5.getAttribute(group, "pet", Pet.class); // returns Pet("Pluto", 93)
n5.getAttribute(group, "pet", Map.class); // {"name": "Pluto", "age": 93}
One can add fields to an attribute by setting the desired field.
n5.setAttribute(group, "pet/likes", new String[]{"Micky"});
n5.getAttribute(group, "pet", Map.class); // {"name": "Pluto", "age": 93, "likes": ["Micky"]}