Skip to content

Commit d76426e

Browse files
authored
Add hash store CLI (#256)
1 parent 56e56fa commit d76426e

24 files changed

+2988
-0
lines changed

hash-store/src/main/java/com/scalar/dl/hashstore/client/service/HashStoreClientService.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,32 @@ public ExecutionResult putObject(
345345
CONTRACT_PUT, contractArguments, FUNCTION_PUT, functionArguments));
346346
}
347347

348+
/**
349+
* Stores a new version of an object in the hash store using JsonNode arguments. Expected format:
350+
* {"objectId": "...", "hash": "...", "metadata": {...}}
351+
*
352+
* @param arguments JsonNode containing objectId, hash, and optional metadata
353+
* @return {@link ExecutionResult}
354+
* @throws ClientException if a request fails for some reason
355+
*/
356+
public ExecutionResult putObject(JsonNode arguments) {
357+
return new ExecutionResult(clientService.executeContract(CONTRACT_PUT, arguments));
358+
}
359+
360+
/**
361+
* Stores a new version of an object in the hash store and also performs a put operation to a
362+
* mutable database using JsonNode arguments.
363+
*
364+
* @param arguments JsonNode containing objectId, hash, and optional metadata
365+
* @param putToMutable JsonNode containing the Put operation data for the mutable database
366+
* @return {@link ExecutionResult}
367+
* @throws ClientException if a request fails for some reason
368+
*/
369+
public ExecutionResult putObject(JsonNode arguments, JsonNode putToMutable) {
370+
return new ExecutionResult(
371+
clientService.executeContract(CONTRACT_PUT, arguments, FUNCTION_PUT, putToMutable));
372+
}
373+
348374
private JsonNode buildPutObjectArguments(String objectId, String hash, JsonObject metadata) {
349375
return HashStoreClientUtils.createObjectNode()
350376
.put(OBJECT_ID, objectId)
@@ -467,6 +493,27 @@ public ExecutionResult compareObjectVersions(String objectId, List<Version> vers
467493
return new ExecutionResult(clientService.executeContract(CONTRACT_VALIDATE, arguments));
468494
}
469495

496+
/**
497+
* Compares object versions to detect tampering using JsonNode arguments.
498+
*
499+
* <p>Options can include:
500+
*
501+
* <ul>
502+
* <li>"verbose": true - shows detailed validation information
503+
* <li>"all": true - compares all versions including stored versions in the ledger
504+
* </ul>
505+
*
506+
* <p>Example: {"objectId": "obj1", "versions": [{"versionId": "v1", "hash": "hash1"}], "options":
507+
* {"all": true, "verbose": true}}
508+
*
509+
* @param arguments JsonNode containing objectId, versions array, and options
510+
* @return {@link ExecutionResult} containing validation results
511+
* @throws ClientException if a request fails for some reason
512+
*/
513+
public ExecutionResult compareObjectVersions(JsonNode arguments) {
514+
return new ExecutionResult(clientService.executeContract(CONTRACT_VALIDATE, arguments));
515+
}
516+
470517
/**
471518
* Compares specific versions of an object to detect tampering with verbose output option.
472519
*
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.scalar.dl.hashstore.client.tool;
2+
3+
import com.google.common.annotations.VisibleForTesting;
4+
import com.scalar.dl.client.config.ClientConfig;
5+
import com.scalar.dl.client.config.GatewayClientConfig;
6+
import com.scalar.dl.client.exception.ClientException;
7+
import com.scalar.dl.client.tool.Common;
8+
import com.scalar.dl.client.tool.CommonOptions;
9+
import com.scalar.dl.hashstore.client.service.HashStoreClientService;
10+
import com.scalar.dl.hashstore.client.service.HashStoreClientServiceFactory;
11+
import java.io.File;
12+
import java.util.concurrent.Callable;
13+
14+
public abstract class AbstractHashStoreCommand extends CommonOptions implements Callable<Integer> {
15+
16+
@Override
17+
public Integer call() throws Exception {
18+
return call(new HashStoreClientServiceFactory());
19+
}
20+
21+
@VisibleForTesting
22+
public final Integer call(HashStoreClientServiceFactory factory) throws Exception {
23+
try {
24+
HashStoreClientService service =
25+
useGateway
26+
? factory.create(new GatewayClientConfig(new File(properties)), false)
27+
: factory.create(new ClientConfig(new File(properties)), false);
28+
return execute(service);
29+
} catch (ClientException e) {
30+
Common.printError(e);
31+
printStackTrace(e);
32+
return 1;
33+
} finally {
34+
factory.close();
35+
}
36+
}
37+
38+
/**
39+
* Executes the specific command logic.
40+
*
41+
* @param service the client service to use for execution.
42+
* @return the exit code.
43+
* @throws ClientException if the execution fails.
44+
*/
45+
protected abstract Integer execute(HashStoreClientService service) throws ClientException;
46+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.scalar.dl.hashstore.client.tool;
2+
3+
import com.scalar.dl.client.exception.ClientException;
4+
import com.scalar.dl.hashstore.client.service.HashStoreClientService;
5+
import picocli.CommandLine;
6+
import picocli.CommandLine.Command;
7+
8+
@Command(
9+
name = "bootstrap",
10+
description = "Bootstrap the hash store by registering identity and contracts.")
11+
public class Bootstrap extends AbstractHashStoreCommand {
12+
13+
public static void main(String[] args) {
14+
int exitCode = new CommandLine(new Bootstrap()).execute(args);
15+
System.exit(exitCode);
16+
}
17+
18+
@Override
19+
protected Integer execute(HashStoreClientService service) throws ClientException {
20+
service.bootstrap();
21+
return 0;
22+
}
23+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.scalar.dl.hashstore.client.tool;
2+
3+
import com.scalar.dl.hashstore.client.service.HashStoreClientService;
4+
import java.util.ArrayList;
5+
import java.util.List;
6+
import picocli.CommandLine;
7+
import picocli.CommandLine.Command;
8+
9+
@Command(name = "create-collection", description = "Create a new collection.")
10+
public class CollectionCreation extends AbstractHashStoreCommand {
11+
12+
@CommandLine.Option(
13+
names = {"--collection-id"},
14+
required = true,
15+
paramLabel = "COLLECTION_ID",
16+
description = "The ID of the collection to create.")
17+
private String collectionId;
18+
19+
@CommandLine.Option(
20+
names = {"--object-ids"},
21+
paramLabel = "OBJECT_ID",
22+
description = "Object IDs to include in the collection.")
23+
private List<String> objectIds = new ArrayList<>();
24+
25+
public static void main(String[] args) {
26+
int exitCode = new CommandLine(new CollectionCreation()).execute(args);
27+
System.exit(exitCode);
28+
}
29+
30+
@Override
31+
protected Integer execute(HashStoreClientService service) {
32+
service.createCollection(collectionId, objectIds);
33+
return 0;
34+
}
35+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.scalar.dl.hashstore.client.tool;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.scalar.dl.client.exception.ClientException;
5+
import com.scalar.dl.client.tool.Common;
6+
import com.scalar.dl.hashstore.client.service.HashStoreClientService;
7+
import com.scalar.dl.ledger.model.ExecutionResult;
8+
import com.scalar.dl.ledger.util.JacksonSerDe;
9+
import picocli.CommandLine;
10+
import picocli.CommandLine.Command;
11+
12+
@Command(name = "get-collection", description = "Get a collection from the hash store.")
13+
public class CollectionGet extends AbstractHashStoreCommand {
14+
15+
@CommandLine.Option(
16+
names = {"--collection-id"},
17+
required = true,
18+
paramLabel = "COLLECTION_ID",
19+
description = "The ID of the collection to retrieve.")
20+
private String collectionId;
21+
22+
public static void main(String[] args) {
23+
int exitCode = new CommandLine(new CollectionGet()).execute(args);
24+
System.exit(exitCode);
25+
}
26+
27+
@Override
28+
protected Integer execute(HashStoreClientService service) throws ClientException {
29+
JacksonSerDe serde = new JacksonSerDe(new ObjectMapper());
30+
ExecutionResult result = service.getCollection(collectionId);
31+
if (result.getResult().isPresent()) {
32+
System.out.println("Result:");
33+
Common.printJson(serde.deserialize(result.getResult().get()));
34+
} else {
35+
System.out.println("Collection not found.");
36+
}
37+
return 0;
38+
}
39+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package com.scalar.dl.hashstore.client.tool;
2+
3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.scalar.dl.client.exception.ClientException;
5+
import com.scalar.dl.client.tool.Common;
6+
import com.scalar.dl.hashstore.client.service.HashStoreClientService;
7+
import com.scalar.dl.ledger.model.ExecutionResult;
8+
import com.scalar.dl.ledger.util.JacksonSerDe;
9+
import picocli.CommandLine;
10+
import picocli.CommandLine.Command;
11+
12+
@Command(name = "get-collection-history", description = "Get the history of a collection.")
13+
public class CollectionHistoryGet extends AbstractHashStoreCommand {
14+
15+
@CommandLine.Option(
16+
names = {"--collection-id"},
17+
required = true,
18+
paramLabel = "COLLECTION_ID",
19+
description = "The ID of the collection.")
20+
private String collectionId;
21+
22+
@CommandLine.Option(
23+
names = {"--limit"},
24+
paramLabel = "LIMIT",
25+
description = "Maximum number of recent history entries to return.")
26+
private Integer limit;
27+
28+
public static void main(String[] args) {
29+
int exitCode = new CommandLine(new CollectionHistoryGet()).execute(args);
30+
System.exit(exitCode);
31+
}
32+
33+
@Override
34+
protected Integer execute(HashStoreClientService service) throws ClientException {
35+
JacksonSerDe serde = new JacksonSerDe(new ObjectMapper());
36+
ExecutionResult result;
37+
if (limit != null) {
38+
result = service.getCollectionHistory(collectionId, limit);
39+
} else {
40+
result = service.getCollectionHistory(collectionId);
41+
}
42+
43+
if (result.getResult().isPresent()) {
44+
System.out.println("Result:");
45+
Common.printJson(serde.deserialize(result.getResult().get()));
46+
}
47+
return 0;
48+
}
49+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.scalar.dl.hashstore.client.tool;
2+
3+
import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST;
4+
import static picocli.CommandLine.Model.UsageMessageSpec.SECTION_KEY_COMMAND_LIST_HEADING;
5+
6+
import com.google.common.annotations.VisibleForTesting;
7+
import com.google.common.collect.ImmutableMap;
8+
import com.scalar.dl.client.tool.CommandGroupRenderer;
9+
import java.util.Arrays;
10+
import java.util.Collections;
11+
import java.util.List;
12+
import picocli.CommandLine;
13+
import picocli.CommandLine.Command;
14+
import picocli.CommandLine.HelpCommand;
15+
16+
@Command(
17+
name = "scalardl-hash-store",
18+
subcommands = {
19+
HelpCommand.class,
20+
Bootstrap.class,
21+
ObjectGet.class,
22+
ObjectPut.class,
23+
ObjectVersionsComparison.class,
24+
CollectionCreation.class,
25+
CollectionGet.class,
26+
ObjectAdditionToCollection.class,
27+
ObjectRemovalFromCollection.class,
28+
CollectionHistoryGet.class,
29+
LedgerValidation.class,
30+
},
31+
description = {"These are ScalarDL Hash Store commands used in various situations:"})
32+
public class HashStoreCommandLine {
33+
34+
public static void main(String[] args) {
35+
CommandLine commandLine = new CommandLine(new HashStoreCommandLine());
36+
setupSections(commandLine);
37+
38+
int exitCode = commandLine.execute(args);
39+
System.exit(exitCode);
40+
}
41+
42+
/**
43+
* Changes the usage message (a.k.a. help information) of the {@link CommandLine} into the grouped
44+
* sections.
45+
*
46+
* <p>[Note]
47+
*
48+
* <p>Please set up all the sections in this method.
49+
*
50+
* <p>Please refer to the [Usage] comment described in {@link CommandGroupRenderer}.
51+
*
52+
* @param cmd command line of {@link HashStoreCommandLine}
53+
*/
54+
@VisibleForTesting
55+
static void setupSections(CommandLine cmd) {
56+
// ref. Code to group subcommands from the official documentation.
57+
// https://github.com/remkop/picocli/issues/978#issuecomment-604174211
58+
59+
ImmutableMap.Builder<String, List<Class<?>>> sections = ImmutableMap.builder();
60+
// Section: register identity information.
61+
sections.put("%nbootstrap the hash store%n", Collections.singletonList(Bootstrap.class));
62+
// Section: manage objects.
63+
sections.put(
64+
"%nmanage objects%n",
65+
Arrays.asList(ObjectGet.class, ObjectPut.class, ObjectVersionsComparison.class));
66+
// Section: manage collections.
67+
sections.put(
68+
"%nmanage collections%n",
69+
Arrays.asList(
70+
CollectionCreation.class,
71+
CollectionGet.class,
72+
ObjectAdditionToCollection.class,
73+
ObjectRemovalFromCollection.class,
74+
CollectionHistoryGet.class));
75+
// Section: validate ledger.
76+
sections.put("%nvalidate ledger%n", Collections.singletonList(LedgerValidation.class));
77+
CommandGroupRenderer renderer = new CommandGroupRenderer(sections.build());
78+
79+
cmd.getHelpSectionMap().remove(SECTION_KEY_COMMAND_LIST_HEADING);
80+
cmd.getHelpSectionMap().put(SECTION_KEY_COMMAND_LIST, renderer);
81+
}
82+
}

0 commit comments

Comments
 (0)