Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 95 additions & 1 deletion src/main/java/dev/zarr/zarrjava/core/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
import dev.zarr.zarrjava.ZarrException;
import dev.zarr.zarrjava.store.FilesystemStore;
import dev.zarr.zarrjava.store.StoreHandle;
import dev.zarr.zarrjava.utils.Utils;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.AbstractMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public abstract class Group extends AbstractNode {
Expand Down Expand Up @@ -70,13 +77,100 @@ public Node get(String key) throws ZarrException, IOException {
return get(new String[]{key});
}

public abstract Stream<Node> list();
/**
* Keys that hold metadata of the group itself and never point at a child node.
*/
protected abstract Set<String> metadataKeys();

/**
* Lists the immediate children (arrays and subgroups) of this group.
* <p>
* This costs a single listing request on the underlying store, plus one metadata read per
* child. Keys that do not hold a Zarr node are skipped.
*
* @return a stream of the direct children of this group
* @throws UnsupportedOperationException if the underlying store does not support listing
*/
public Stream<Node> members() {
return descendantKeys(new String[0]).parallelStream()
.map(this::openDescendant)
.filter(Objects::nonNull)
.collect(Collectors.toList())
.stream();
}

public Node[] membersAsArray() {
try (Stream<Node> nodeStream = members()) {
return nodeStream.toArray(Node[]::new);
}
}

/**
* Recursively lists all descendants (arrays and groups) of this group, at any depth.
* <p>
* The group hierarchy is walked one level at a time, so only group keys are listed and chunk
* keys are never enumerated. Descending into an array is not necessary and does not happen.
*
* @return a stream of all descendants of this group, excluding the group itself
* @throws UnsupportedOperationException if the underlying store does not support listing
*/
public Stream<Node> list() {
return listDescendants(new String[0]);
}

public Node[] listAsArray() {
try (Stream<Node> nodeStream = list()) {
return nodeStream.toArray(Node[]::new);
}
}

private Stream<Node> listDescendants(String[] prefix) {
List<Map.Entry<String[], Node>> children = descendantKeys(prefix).parallelStream()
.map(key -> new AbstractMap.SimpleEntry<String[], Node>(key, openDescendant(key)))
.collect(Collectors.toList());

return children.stream().flatMap(child -> {
Node node = child.getValue();
if (node == null) {
// Not a node itself, but it may still contain nodes further down.
return listDescendants(child.getKey());
}
if (node instanceof Group) {
return Stream.concat(Stream.of(node), listDescendants(child.getKey()));
}
return Stream.of(node);
});
}

/**
* Lists the keys directly below {@code prefix} that may hold a child node, relative to this
* group.
*/
private List<String[]> descendantKeys(String[] prefix) {
Set<String> metadataKeys = metadataKeys();
try (Stream<String> children = storeHandle.resolve(prefix).listChildren()) {
return children
.filter(name -> !metadataKeys.contains(name))
.map(name -> Utils.concatArrays(prefix, new String[]{name}))
.collect(Collectors.toList());
}
}

/**
* Opens the node at {@code key}, or returns null if there is no node there.
*/
@Nullable
private Node openDescendant(String[] key) {
try {
return get(key);
} catch (IOException e) {
throw new RuntimeException(
"Failed to read node metadata for key '" + String.join("/", key) + "': " + e.getMessage(), e);
} catch (ZarrException e) {
throw new RuntimeException(
"Failed to parse node metadata for key '" + String.join("/", key) + "': " + e.getMessage(), e);
}
}

public abstract GroupMetadata metadata();
}
19 changes: 19 additions & 0 deletions src/main/java/dev/zarr/zarrjava/store/FilesystemStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,16 @@ public boolean exists(String[] keys) {
return Files.isRegularFile(resolveKeys(keys));
}

/**
* Whether there is no file at the given keys. Reading such a key does not always fail with a
* {@link NoSuchFileException}: if one of the path components is a file rather than a
* directory, the filesystem reports "Not a directory" instead. Either way the key holds no
* data, so {@link #get} has to return null for it.
*/
private boolean isMissing(String[] keys) {
return !Files.isRegularFile(resolveKeys(keys));
}

@Nullable
@Override
public ByteBuffer get(String[] keys) {
Expand All @@ -52,6 +62,9 @@ public ByteBuffer get(String[] keys) {
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
if (isMissing(keys)) {
return null;
}
throw StoreException.readFailed(this.toString(), keys, e);
}
}
Expand All @@ -75,6 +88,9 @@ public ByteBuffer get(String[] keys, long start) {
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
if (isMissing(keys)) {
return null;
}
throw StoreException.readFailed(this.toString(), keys, e);
}
}
Expand All @@ -97,6 +113,9 @@ public ByteBuffer get(String[] keys, long start, long end) {
} catch (NoSuchFileException e) {
return null;
} catch (IOException e) {
if (isMissing(keys)) {
return null;
}
throw StoreException.readFailed(this.toString(), keys, e);
}
}
Expand Down
22 changes: 14 additions & 8 deletions src/main/java/dev/zarr/zarrjava/store/S3Store.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;

import javax.annotation.Nonnull;
Expand All @@ -12,7 +14,6 @@
import software.amazon.awssdk.core.ResponseInputStream;
import software.amazon.awssdk.core.sync.RequestBody;
import software.amazon.awssdk.services.s3.S3Client;
import software.amazon.awssdk.services.s3.model.CommonPrefix;
import software.amazon.awssdk.services.s3.model.DeleteObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
import software.amazon.awssdk.services.s3.model.GetObjectResponse;
Expand Down Expand Up @@ -184,15 +185,20 @@ public Stream<String> listChildren(String[] keys) {
.delimiter("/")
.build();

ListObjectsV2Response res = s3client.listObjectsV2(req);

// Combine CommonPrefixes (folders) and Contents (files)
Stream<String> folders = res.commonPrefixes().stream().map(CommonPrefix::prefix);
final String finalFullPrefix = fullPrefix;
Stream<String> files = res.contents().stream().map(S3Object::key)
.filter(key -> !key.equals(finalFullPrefix));
// Combine CommonPrefixes (folders) and Contents (files) across all pages. A single
// listObjectsV2 call returns at most 1000 entries, which would silently truncate the
// children of a large group.
List<String> children = new ArrayList<>();
for (ListObjectsV2Response res : s3client.listObjectsV2Paginator(req)) {
res.commonPrefixes().forEach(commonPrefix -> children.add(commonPrefix.prefix()));
res.contents().stream()
.map(S3Object::key)
.filter(key -> !key.equals(finalFullPrefix))
.forEach(children::add);
}

return Stream.concat(folders, files)
return children.stream()
.map(k -> keyToRelativeArray(k, finalFullPrefix)[0]);
}

Expand Down
37 changes: 15 additions & 22 deletions src/main/java/dev/zarr/zarrjava/v2/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,22 @@
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Objects;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;

import static dev.zarr.zarrjava.v2.Node.makeObjectMapper;
import static dev.zarr.zarrjava.v2.Node.makeObjectWriter;

public class Group extends dev.zarr.zarrjava.core.Group implements Node {

/**
* Keys that hold metadata of a v2 group itself and never point at a child node.
*/
private static final Set<String> METADATA_KEYS = Collections.unmodifiableSet(
new HashSet<>(Arrays.asList(ZGROUP, ZATTRS, ZARRAY)));

public GroupMetadata metadata;

protected Group(@Nonnull StoreHandle storeHandle, @Nonnull GroupMetadata groupMetadata) {
Expand Down Expand Up @@ -182,26 +190,6 @@ public Node get(String[] key) throws ZarrException, IOException {
}
}

@Override
public Stream<dev.zarr.zarrjava.core.Node> list() {
return storeHandle.list().map(key -> {
if (key.length <= 1) return null; // exclude root from list
String fileName = key[key.length - 1];
StoreHandle parent = storeHandle.resolve(Arrays.copyOf(key, key.length - 1));
try {
if (fileName.equals(ZARRAY)) {
return Array.open(parent);
}
if (fileName.equals(ZGROUP)) {
return (dev.zarr.zarrjava.core.Node) Group.open(parent);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
return null;
}).filter(Objects::nonNull);
}

/**
* Creates a new subgroup with default metadata at the specified key.
*
Expand Down Expand Up @@ -293,6 +281,11 @@ public String toString() {
return String.format("<v2.Group {%s}>", storeHandle);
}

@Override
protected Set<String> metadataKeys() {
return METADATA_KEYS;
}

@Override
public GroupMetadata metadata() {
return metadata;
Expand Down
33 changes: 12 additions & 21 deletions src/main/java/dev/zarr/zarrjava/v3/Group.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,21 @@
import java.nio.file.NoSuchFileException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collections;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Stream;

import static dev.zarr.zarrjava.v3.Node.makeObjectMapper;
import static dev.zarr.zarrjava.v3.Node.makeObjectWriter;


public class Group extends dev.zarr.zarrjava.core.Group implements Node {

/**
* Keys that hold metadata of a v3 group itself and never point at a child node.
*/
private static final Set<String> METADATA_KEYS = Collections.singleton(ZARR_JSON);

public GroupMetadata metadata;

protected Group(@Nonnull StoreHandle storeHandle, @Nonnull GroupMetadata groupMetadata) throws IOException {
Expand Down Expand Up @@ -192,25 +197,6 @@ public Node get(String[] key) throws ZarrException, IOException {
}
}

@Override
public Stream<dev.zarr.zarrjava.core.Node> list() {
Stream<String[]> metadataKeys = storeHandle.list()
.filter(key -> key[key.length - 1].equals(ZARR_JSON))
.filter(key -> key.length > 1); // exclude root from list
return metadataKeys.map(key -> {
try {
return get(Arrays.copyOf(key, key.length - 1));
} catch (IOException e) {
throw new RuntimeException(
"Failed to read node metadata for key '" + String.join("/", key) + "': " + e.getMessage(), e);
} catch (ZarrException e) {
throw new RuntimeException(
"Failed to parse node metadata for key '" + String.join("/", key) + "': " + e.getMessage(), e);
}
});
}


/**
* Creates a new subgroup with the provided metadata at the specified key.
*
Expand Down Expand Up @@ -311,6 +297,11 @@ public String toString() {
return String.format("<v3.Group {%s}>", storeHandle);
}

@Override
protected Set<String> metadataKeys() {
return METADATA_KEYS;
}

@Override
public GroupMetadata metadata() {
return metadata;
Expand Down
Loading
Loading