Skip to content

Commit c5c89be

Browse files
authored
Fix tests (#68)
* bump versions * Create Makefile * ignore intellij files * ignore blank workflows * createNamespaceAtUri * CHANGELOG * debug testS3WorkflowPush failure * passed metadata schema * updated hash for new config file * publish 0.1.6 * Trim logging * Manifest.BuildFromPaths * intellij linting * Manifest.BuildFromDir * update hash - always create well-formed system metadata - use filename for logical key (to test BuildFromDir) --------- Co-authored-by: Dr. Ernie Prabhakar <19791+drernie@users.noreply.github.com>
1 parent 19df56a commit c5c89be

12 files changed

Lines changed: 239 additions & 123 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,4 @@ lib/bin
1010
.vscode
1111
.DS_Store
1212
gradle.properties
13+
/.idea

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# CHANGELOG.md
22

3+
## [0.1.6] - UNRELEASED
4+
5+
- Fix failing CI tests
6+
- Add convenience methods:
7+
- Registry.CreateNamespaceAtUri
8+
- Manifest.BuildFromPaths
9+
- Manifest.BuildFromDir
10+
311
## [0.1.5] - 2024-08-27
412

513
- Remove debugging

Makefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
.PHONY: all verify clean compile
2+
3+
verify:
4+
./gradlew check || open lib/build/reports/tests/test/index.html
5+
6+
clean:
7+
./gradlew clean
8+
9+
compile: clean
10+
./gradlew compileJava

lib/build.gradle

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,11 @@ dependencies {
2626
implementation 'software.amazon.awssdk.crt:aws-crt:0.30.9'
2727

2828
// JSON and YAML parsing.
29-
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
30-
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.17.2'
29+
implementation 'com.fasterxml.jackson.core:jackson-databind:2.18.2'
30+
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.18.2'
3131

3232
// JSON Schema validator.
33-
implementation 'io.vertx:vertx-json-schema:4.5.9'
33+
implementation 'io.vertx:vertx-json-schema:4.5.11'
3434

3535
// Workarounds for Java's checked exceptions.
3636
implementation 'com.pivovarit:throwing-function:1.6.1'
@@ -74,7 +74,7 @@ mavenPublishing {
7474

7575
signAllPublications()
7676

77-
coordinates('com.quiltdata', 'quiltcore', '0.1.5')
77+
coordinates('com.quiltdata', 'quiltcore', '0.1.6')
7878

7979
pom {
8080
name.set('QuiltCore')
@@ -83,7 +83,7 @@ mavenPublishing {
8383
licenses {
8484
license {
8585
name.set('Apache License, Version 2.0')
86-
url.set('http://www.apache.org/licenses/LICENSE-2.0.txt')
86+
url.set('https://www.apache.org/licenses/LICENSE-2.0.txt')
8787
}
8888
}
8989
developers {

lib/src/main/java/com/quiltdata/quiltcore/Entry.java

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,6 @@
66
import java.security.NoSuchAlgorithmException;
77

88
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
9-
/**
10-
* A Jackson class that represents a JSON object node.
11-
*
12-
* <p>
13-
* An `ObjectNode` is a container for key-value pairs, where the keys are strings and the values can be any valid JSON node.
14-
* It provides methods to manipulate and access the key-value pairs within the object.
15-
* </p>
16-
*
17-
* <p>
18-
* This class is part of the Jackson JSON library, which provides a fast and flexible way to process JSON data in Java.
19-
* </p>
20-
*/
219
import com.fasterxml.jackson.databind.node.ObjectNode;
2210
import com.quiltdata.quiltcore.key.PhysicalKey;
2311

@@ -26,19 +14,25 @@
2614
import org.slf4j.LoggerFactory;
2715

2816
/**
29-
* Represents an entry in the Quilt data repository.
30-
* An entry contains information about a file or object stored in the repository.
31-
*/
32-
/**
33-
* Represents an entry in the Quilt dataset.
17+
* Represents an individual entry in the Quilt data repository.
18+
*
19+
* <p>
20+
* The {@code Entry} class represents a row in a Quilt package.
21+
* </p>
22+
*
23+
* <h2>Usage Example:</h2>
24+
* <pre>{@code
25+
* Entry entry = new Entry(new LocalPhysicalKey("foo), 123, hash, metadata)
26+
* }</pre>
27+
*
3428
*/
3529
public class Entry {
3630
private static final Logger logger = LoggerFactory.getLogger(Entry.class);
3731

3832
/**
3933
* Enumerates the types of hash algorithms supported by Quilt.
4034
*/
41-
public static enum HashType {
35+
public enum HashType {
4236
/**
4337
* The SHA-256 hash algorithm.
4438
*/
@@ -108,6 +102,21 @@ public Entry(PhysicalKey physicalKey, long size, Hash hash, ObjectNode metadata)
108102
this.metadata = metadata == null ? JsonNodeFactory.instance.objectNode() : metadata.deepCopy();
109103
}
110104

105+
/**
106+
* String representation
107+
*
108+
* @return the Entry details
109+
*/
110+
@Override
111+
public String toString() {
112+
return "Entry{" +
113+
"physicalKey=" + physicalKey +
114+
", size=" + size +
115+
", hash=" + hash +
116+
", meta=" + metadata +
117+
'}';
118+
}
119+
111120
/**
112121
* Returns the physical key of the entry.
113122
*

lib/src/main/java/com/quiltdata/quiltcore/Manifest.java

Lines changed: 121 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ public class Manifest {
6060
* The version of the manifest.
6161
*/
6262
public static final String VERSION = "v0";
63+
public static final String USER_META = "user_meta";
6364

6465
private static final ObjectMapper TOP_HASH_MAPPER;
6566

@@ -77,13 +78,13 @@ public class Manifest {
7778
* Returns a map for a URI of the form
7879
* "quilt+s3://bucket#package=package{@literal @}hash{@literal &}path=path"
7980
*
80-
* @param uri
81+
* @param uri: Quilt+ URI
8182
* @return Map{@literal <}String, String{@literal >}
82-
* @throws IllegalArgumentException
83+
* @throws IllegalArgumentException if scheme != quilt+s3
8384
*/
8485

8586
public static Map<String, String> ParseQuiltURI(URI uri) throws IllegalArgumentException {
86-
Map<String, String> result = new TreeMap<java.lang.String, java.lang.String>();
87+
Map<String, String> result = new TreeMap<>();
8788
String scheme = uri.getScheme();
8889
if (!scheme.equals("quilt+s3")) {
8990
throw new IllegalArgumentException("Invalid scheme: " + scheme);
@@ -151,15 +152,117 @@ public static Manifest FromQuiltURI(String quiltURI) throws URISyntaxException,
151152
String revision = parts.get("revision");
152153
hash = n.getHash(revision);
153154
}
154-
Manifest m = n.getManifest(hash);
155-
return m;
155+
return n.getManifest(hash);
156156
}
157-
157+
158+
/**
159+
* Formats the provided user metadata into a JSON {@link ObjectNode}.
160+
*
161+
* <p>
162+
* This method takes a user-provided metadata object, processes it, and converts it
163+
* into a structured JSON object. The resulting {@code ObjectNode} can be integrated
164+
* into manifests or other contexts where JSON metadata is required.
165+
* </p>
166+
*
167+
* @param user_meta The user metadata to be formatted. This can be a Map, a String, or null.
168+
*
169+
* @return A JSON {@link ObjectNode} representing the formatted user metadata.
170+
* Returns an empty {@link ObjectNode} if {@code user_meta} is {@code null}.
171+
*
172+
* @throws IllegalArgumentException if the provided {@code user_meta} is invalid
173+
* and cannot be processed.
174+
*
175+
* <h2>Usage Example:</h2>
176+
* <pre>{@code
177+
* Object userMeta = new HashMap<>();
178+
* ((Map) userMeta).put("key", "value");
179+
*
180+
* ObjectNode formattedMeta = FormatUserMeta(userMeta);
181+
* System.out.println(formattedMeta.toString()); // Outputs JSON representation
182+
* }</pre>
183+
*/
184+
public static ObjectNode FormatUserMeta(Object user_meta) {
185+
ObjectNode base = JsonNodeFactory.instance.objectNode().put("version", VERSION);
186+
187+
if (user_meta == null) {
188+
return base.set(USER_META, null);
189+
} else if (user_meta instanceof Map) {
190+
ObjectMapper mapper = new ObjectMapper();
191+
return base.set(USER_META, mapper.valueToTree(user_meta));
192+
} else if (user_meta instanceof String) {
193+
return base.put(USER_META, (String)user_meta);
194+
}
195+
throw new IllegalArgumentException("Invalid user_meta[" + user_meta.getClass().getName() + "] " + user_meta);
196+
}
197+
198+
/**
199+
* Builds a {@link Manifest} from the provided paths, user metadata, and object metadata.
200+
*
201+
* <p>
202+
* This static method constructs a {@code Manifest} object by taking a mapping of
203+
* logical names to physical paths, along with optional user-provided metadata and
204+
* object-specific metadata. It validates and processes the input to generate
205+
* a structured representation of the manifest.
206+
* </p>
207+
*
208+
* @param paths A map where the keys are logical names, and the values are
209+
* corresponding file paths on the system. Cannot be {@code null}.
210+
* @param user_meta Optional user metadata associated with the manifest. Can be {@code null}.
211+
* @param object_meta A map where the keys are object names,
212+
* and the values are JSON object nodes containing metadata for each object. Can be {@code null}.
213+
*
214+
* @return A constructed {@link Manifest} instance encapsulating the provided data.
215+
*
216+
* @throws IllegalArgumentException if the {@code paths} map is {@code null} or contains invalid entries.
217+
*
218+
* <h2>Usage Example:</h2>
219+
* <pre>{@code
220+
* Map<String, Path> paths = new HashMap<>();
221+
* paths.put("exampleKey", Paths.get("/example/path"));
222+
*
223+
* Manifest manifest = Manifest.BuildFromPaths(paths, null, null);
224+
* }</pre>
225+
*/
226+
public static Manifest BuildFromPaths(Map<String, Path> paths, Object user_meta, Map<String, ObjectNode> object_meta) {
227+
Manifest.Builder b = Manifest.builder();
228+
ObjectNode packageMeta = FormatUserMeta(user_meta);
229+
b.setMetadata(packageMeta);
230+
for (Map.Entry<String, Path> e : paths.entrySet()) {
231+
String key = e.getKey();
232+
Path p = e.getValue();
233+
ObjectNode obj_meta = (object_meta != null) ? object_meta.get(key) : null;
234+
try {
235+
long size = Files.size(p);
236+
b.addEntry(key, new Entry(new LocalPhysicalKey(p), size, null, obj_meta));
237+
} catch (IOException ex) {
238+
logger.error("Skipping entry[{}]: failed to get size for path {}", key, p, ex);
239+
}
240+
}
241+
return b.build();
242+
}
243+
244+
public static Manifest BuildFromDir(Path dir, Object user_meta, String regex) {
245+
Map<String, Path> map = new TreeMap<>();
246+
try {
247+
Files.walk(dir)
248+
.filter(Files::isRegularFile) // Filter regular files
249+
.forEach(f -> {
250+
String logicalKey = dir.relativize(f).toString();
251+
if (regex == null || logicalKey.matches(regex)) {
252+
map.put(logicalKey, f); // Add the entry to the map
253+
}
254+
});
255+
} catch (IOException e) {
256+
throw new RuntimeException(e);
257+
}
258+
return BuildFromPaths(map, user_meta, null);
259+
}
260+
158261
/**
159262
* Represents a builder for creating a {@link Manifest} object.
160263
*/
161264
public static class Builder {
162-
private SortedMap<String, Entry> entries;
265+
private final SortedMap<String, Entry> entries;
163266
private ObjectNode metadata;
164267

165268
/**
@@ -265,12 +368,12 @@ public static Manifest createFromFile(PhysicalKey path) throws IOException, Ille
265368
Entry.HashType hashType = Entry.HashType.enumFor(hashNode.get("type").asText());
266369
String hashValue = hashNode.get("value").asText();
267370
JsonNode meta = row.get("meta");
268-
if (meta == null) {
269-
// leave it as is
270-
} else if (meta.isNull()) {
271-
meta = null;
272-
} else if (!meta.isObject()) {
273-
throw new IOException("Invalid entry metadata: " + node);
371+
if (meta != null) {
372+
if (meta.isNull()) {
373+
meta = null;
374+
} else if (!meta.isObject()) {
375+
throw new IOException("Invalid entry metadata: " + node);
376+
}
274377
}
275378

276379
Entry entry = new Entry(physicalKey, size, new Entry.Hash(hashType, hashValue), (ObjectNode)meta);
@@ -442,7 +545,7 @@ public void install(Path dest) throws IOException {
442545
S3TransferManager transferManager =
443546
S3TransferManager.builder()
444547
.s3Client(s3)
445-
.build();
548+
.build()
446549
) {
447550
List<CompletableFuture<CompletedFileDownload>> futures = new ArrayList<>(bucketEntries.size());
448551

@@ -475,9 +578,10 @@ public void install(Path dest) throws IOException {
475578
}
476579

477580
private JsonNode validate(Namespace namespace, String message, String workflow) throws ConfigurationException, WorkflowException {
581+
logger.info("Validating manifest with {} entries for namespace: {} workflow: {}", entries.size(), namespace.getName(), workflow);
478582
WorkflowConfig config = namespace.getRegistry().getWorkflowConfig();
479583
if (config == null) {
480-
if (workflow == null) {
584+
if (workflow == null || workflow.isBlank()) {
481585
return null;
482586
}
483587
throw new WorkflowException("Workflow is specified, but no workflows config exists");
@@ -534,12 +638,12 @@ public Manifest push(Namespace namespace, String message, String workflow) throw
534638
}
535639
builder.setMetadata(newMetadata);
536640

537-
logger.debug("Building transfer manager for bucket: {}", destBucket);
641+
logger.debug("push: building transfer manager for bucket: {}", destBucket);
538642
try(
539643
S3TransferManager transferManager =
540644
S3TransferManager.builder()
541645
.s3Client(s3)
542-
.build();
646+
.build()
543647
) {
544648
List<Map.Entry<String, CompletableFuture<CompletedFileUpload>>> futures =
545649
new ArrayList<>(entriesWithHashes.size());

lib/src/main/java/com/quiltdata/quiltcore/Registry.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
import org.slf4j.Logger;
77
import org.slf4j.LoggerFactory;
88

9+
import java.net.URI;
10+
import java.net.URISyntaxException;
11+
912
/**
1013
* The Registry class represents a registry of packages and namespaces in the Quilt Core library.
1114
* It provides methods to access and manipulate the registry.
@@ -17,6 +20,24 @@ public class Registry {
1720
private final PhysicalKey versions;
1821
private final PhysicalKey workflowConfigPath;
1922

23+
/**
24+
* Constructs a new Namespace object for a registry as that @uriString
25+
*
26+
* @param pkgName: namespace of the package
27+
* @param uriString: uri of the physical key hosting the registry
28+
* @return Namespace
29+
* @throws URISyntaxException: if uriString is invalid
30+
*/
31+
public static Namespace CreateNamespaceAtUri(String pkgName, String uriString) throws URISyntaxException {
32+
if (!uriString.endsWith("/")) {
33+
uriString += '/';
34+
}
35+
URI uri = new URI(uriString);
36+
PhysicalKey pk = PhysicalKey.fromUri(uri);
37+
Registry r = new Registry(pk);
38+
return r.getNamespace(pkgName);
39+
}
40+
2041
/**
2142
* Constructs a new Registry object with the specified root physical key.
2243
*
@@ -27,6 +48,7 @@ public Registry(PhysicalKey root) {
2748
names = root.resolve(".quilt/named_packages");
2849
versions = root.resolve(".quilt/packages");
2950
workflowConfigPath = root.resolve(".quilt/workflows/config.yml");
51+
// TODO: Handle config.yaml as well
3052
}
3153

3254
/**

lib/src/main/java/com/quiltdata/quiltcore/S3ClientStore.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,9 @@ public class S3ClientStore {
2020
private static final Logger logger = LoggerFactory.getLogger(S3ClientStore.class);
2121
private static final S3Client LOCATION_CLIENT = createClient(Region.US_EAST_1);
2222

23-
private static Map<String, Region> regionMap = Collections.synchronizedMap(new HashMap<>());
24-
private static Map<Region, S3AsyncClient> asyncClientMap = Collections.synchronizedMap(new HashMap<>());
25-
private static Map<Region, S3Client> clientMap = Collections.synchronizedMap(new HashMap<>());
23+
private static final Map<String, Region> regionMap = Collections.synchronizedMap(new HashMap<>());
24+
private static final Map<Region, S3AsyncClient> asyncClientMap = Collections.synchronizedMap(new HashMap<>());
25+
private static final Map<Region, S3Client> clientMap = Collections.synchronizedMap(new HashMap<>());
2626

2727
/**
2828
* Retrieves an asynchronous S3 client for the specified bucket.

0 commit comments

Comments
 (0)