@@ -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 ());
0 commit comments