diff --git a/at_client/README.md b/at_client/README.md
index 639a2f6a..44b277e7 100644
--- a/at_client/README.md
+++ b/at_client/README.md
@@ -7,13 +7,14 @@ This module contains the AtSign Java SDK.
If you are using maven, add the following to your pom.xml
```xml
-
-
- org.atsign
- at_client
- 1.0.0
-
-
+
+
+
+ org.atsign
+ at_client
+ 1.0.0
+
+
```
If you are using gradle, add the following to your build.gradle
@@ -29,33 +30,29 @@ dependencies {
The latest snapshot version can be added as a maven dependency like this...
```xml
-
-
- Central Portal Snapshots
- central-portal-snapshots
- https://central.sonatype.com/repository/maven-snapshots
-
- false
-
-
- true
-
-
-
-
-
-
- org.atsign
- at_client
- 1.0.1-SNAPSHOT
-
-
-```
-
-## Logging
-The SDK uses the slf4j-api. It is up to you to decide which slf4j binding to use.
-See [slf4j providers](https://slf4j.org/manual.html#swapping).
+
+
+ Central Portal Snapshots
+ central-portal-snapshots
+ https://central.sonatype.com/repository/maven-snapshots
+
+ false
+
+
+ true
+
+
+
+
+
+
+ org.atsign
+ at_client
+ 1.0.1-SNAPSHOT
+
+
+```
## Dependencies
@@ -67,19 +64,167 @@ The SDK currently depends on the following
* Slf4j api (Simple logging facade that can be bound a variety of logging
frameworks)
-## Example Usage
+## Logging
-The command line classes under
-[src/main/java/org/atsign/client/cli](src/main/java/org/atsign/client/impl/cli)
-serve as simple examples of how to instantiate an AtClient instance and invoke its
-interface.
+The SDK uses the slf4j-api. It is up to you to decide which slf4j binding to use.
+See [slf4j providers](https://slf4j.org/manual.html#swapping).
+
+If you are simply executing the example code below then you can just add slf4j-simple
+as a dependency in your pom.xml.
+
+```xml
+
+ org.slf4j
+ slf4j-simple
+ 2.0.13
+
+```
+
+## Getting Started
+
+The classes **org.atsign.client.impl.AtCommandExecutors** and
+**org.atsign.client.impl.AtClients**
+provide default implementations of the AtSign SDK interfaces (
+**org.atsign.client.api.AtCommandExecutor** and
+**org.atsign.client.api.AtClient** respectively).
+
+An **org.atsign.client.api.AtCommandExecutor** represents something
+that can send At Protocol commands to an At Server (including the root
+/ directory sever).
+
+The package [commands](src/org/atsign/client/impl/commands) contains utility code
+which implements the
+[At Protocol Specification ](https://github.com/atsign-foundation/at_protocol/blob/trunk/specification/at_protocol_specification.md)
+, for instance...
+
+* Authentication
+* Notifications
+* Verifying At Server responses
+* Signing public key values
+* End to end encryption / decryption for self keys and shared keys
+
+An **org.atsign.client.api.AtClient** provides a "map like" interface to
+an At Server. Enabling you to put, get and delete the different types of
+keys. By combining a default **org.atsign.client.api.AtCommandExecutor** and
+the utility code in the **commands** package it provides the core capabilities
+of the Java SDK.
+
+The following code illustrates putting some key values.
+
+```java
+package org.example;
+
+import org.atsign.client.api.AtClient;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.api.Keys;
+import org.atsign.client.impl.AtClients;
+
+public class Main {
+
+ public static void main(String[] args) throws Exception {
+
+ AtSign atSign = AtSign.createAtSign("18prettyopera");
-See also the [examples](../examples) module.
+ try (AtClient client = AtClients.builder().atSign(atSign).build()) {
+
+ Keys.PublicKey key = Keys.publicKeyBuilder()
+ .sharedBy(atSign)
+ .name("greeting")
+ .build();
+
+ client.put(key, "hello").get();
+
+ AtSign anotherAtSign = AtSign.createAtSign("41malakoff");
+
+ Keys.SharedKey anotherKey = Keys.sharedKeyBuilder()
+ .sharedBy(atSign)
+ .sharedWith(anotherAtSign)
+ .name("greeting")
+ .build();
+
+ client.put(anotherKey, "hola").get();
+
+ }
+
+ }
+}
+```
+And this code illustrates creating getting the key values that were put in the
+previous example code.
+
+```java
+package org.example;
+
+import org.atsign.client.api.AtClient;
+import org.atsign.client.api.AtSign;
+import org.atsign.client.api.Keys;
+import org.atsign.client.impl.AtClients;
+
+public class Main {
+
+ public static void main(String[] args) throws Exception {
+
+ AtSign atSign = AtSign.createAtSign("41malakoff");
+
+ try (AtClient client = AtClients.builder().atSign(atSign).build()) {
+
+ AtSign anotherAtSign = AtSign.createAtSign("18prettyopera");
+
+ Keys.PublicKey key = Keys.publicKeyBuilder()
+ .sharedBy(anotherAtSign)
+ .name("greeting")
+ .build();
+
+ System.out.println(client.get(key).get());
+
+ Keys.SharedKey anotherKey = Keys.sharedKeyBuilder()
+ .sharedBy(anotherAtSign)
+ .sharedWith(atSign)
+ .name("greeting")
+ .build();
+
+ System.out.println(client.get(anotherKey).get());
+
+ }
+
+ }
+}
+```
+The **AtClients.builder()** will create an **AtCommandExecutor** which will resolve the
+endpoint for the atsign's at server from **root.atsign.org**. The **AtCommandExector**
+will automatically retry connections and reconnect if it becomes disconnected. The
+commands that are sent to the asign atserver will automatically timeout. The builder
+will automatically load **AtKeys** which correspond to the atsign name and will assume
+that these reside under ~/.atsign/keys. All of this behavior can be overridden with the
+following builder fields.
+
+* **url** is used to set other root / directory endpoints, a proxy endpoint
+ or the explicit endpoint for the at server.
+* **keys** is used to explicitly provide the **AtKeys** which will be used for
+ authentication, signing and encryption.
+* **reconnect** is used to supply a **ReconnectStrategy** implementation that
+ controls if reconnect is supported at all, how many retries and reconnections
+ are supported, the pauses to apply between retries and when / if the at server
+ endpoint should be re-resolved.
+* **timeoutMillis** is used to specify the command timeout
+* **queueLimit** is used to specify how many commands can be queued. By default
+ the queue limit is zero.
+* **awaitReadyMillis** is used to specify how long the builder blocks for the
+ **AtCommandExecutor** to become ready (to send commands). NOTE: This will NOT
+ trigger a build exception if the executor fails to become ready in that time, but
+ the first command that is sent might be queued.
+
+See the [examples](../examples) module.
---
## Command Line Utilities
+The command line classes under
+[src/main/java/org/atsign/client/cli](src/main/java/org/atsign/client/impl/cli)
+serve as simple examples of how to instantiate an AtClient instance and invoke its
+interface.
+
The following utilities provide a simple way to test behavior as-well as illustrating
the fundamentals of using the SDK.
@@ -144,6 +289,7 @@ mvn exec:java -Dexec.mainClass=org.atsign.client.cli.register.Register -Dexec.ar
When using the SUPER_API Key to register an atsign, the following sequence of
calls take place:
+
1. User provides at_java/Register with the SUPER_API Key passed as an argument
2. at_java calls the AtSign Registrar API* Endpoint(get-atsign) with the
SUPER_API Key provided
diff --git a/at_client/pom.xml b/at_client/pom.xml
index 897b3e3a..a9e4c642 100644
--- a/at_client/pom.xml
+++ b/at_client/pom.xml
@@ -219,6 +219,13 @@
test
+
+ com.github.stefanbirkner
+ system-lambda
+ 1.2.1
+ test
+
+
diff --git a/at_client/src/main/java/org/atsign/client/impl/AtClients.java b/at_client/src/main/java/org/atsign/client/impl/AtClients.java
index a7df018d..df86a059 100644
--- a/at_client/src/main/java/org/atsign/client/impl/AtClients.java
+++ b/at_client/src/main/java/org/atsign/client/impl/AtClients.java
@@ -37,6 +37,7 @@ public static AtClient createAtClient(String url,
Long timeoutMillis,
Long awaitReadyMillis,
ReconnectStrategy reconnect,
+ Integer queueLimit,
Boolean isVerbose)
throws AtException {
@@ -50,6 +51,7 @@ public static AtClient createAtClient(String url,
.timeoutMillis(timeoutMillis)
.awaitReadyMillis(awaitReadyMillis)
.reconnect(reconnect)
+ .queueLimit(queueLimit)
.isVerbose(isVerbose)
.build();
@@ -76,6 +78,7 @@ public static AtClient createAtClient(String url,
* .timeoutMillis() // timeout after which commands will complete exceptionally (optional)
* .awaitReadyMillis() // how long to wait for executor to become ready during build() (optional)
* .reconnect() // a ReconnectStrategy (optional)
+ * .queueLimit() // number of queued commands that are permitted (optional)
* .isVerbose(...) // true or false (optional)
* .build();
* }
@@ -92,6 +95,7 @@ public static AtClient createAtClient(String url,
* {@link AtCommandExecutors#DEFAULT_TIMEOUT_MILLIS}.
* If reconnect is not set then the builder will default to a {@link SimpleReconnectStrategy}
* with no limit to the retry attempts.
+ * If queueLimit is not set then the builder will default zero queued commands
*/
public static class AtClientBuilder {
// required for javadoc
diff --git a/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java b/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java
index d560f253..625feab6 100644
--- a/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java
+++ b/at_client/src/main/java/org/atsign/client/impl/AtCommandExecutors.java
@@ -51,6 +51,7 @@ public static AtCommandExecutor createCommandExecutor(String url,
Long timeoutMillis,
Long awaitReadyMillis,
ReconnectStrategy reconnect,
+ Integer queueLimit,
Boolean isVerbose)
throws AtException {
@@ -64,6 +65,7 @@ public static AtCommandExecutor createCommandExecutor(String url,
.timeoutMillis(defaultIfNotSet(timeoutMillis, DEFAULT_TIMEOUT_MILLIS))
.awaitReadyMillis(defaultIfNotSet(awaitReadyMillis, DEFAULT_TIMEOUT_MILLIS))
.reconnect(defaultIfNotSet(reconnect))
+ .queueLimit(queueLimit)
.onReady(createOnReady(atSign, keys))
.build();
}
diff --git a/at_client/src/main/java/org/atsign/client/impl/util/KeysUtils.java b/at_client/src/main/java/org/atsign/client/impl/util/KeysUtils.java
index 3bd620db..a9b6c040 100644
--- a/at_client/src/main/java/org/atsign/client/impl/util/KeysUtils.java
+++ b/at_client/src/main/java/org/atsign/client/impl/util/KeysUtils.java
@@ -122,13 +122,12 @@ private static File getKeysFileFallbackToLegacyLocation(AtSign atSign) throws At
if (!file.exists()) {
// if keys do not exist in root, check in keys sub-directory under current working directory
- file = getKeysFile(atSign, legacyKeysFilesLocation);
+ File legacyFile = getKeysFile(atSign, legacyKeysFilesLocation);
// if file does not exist under current working directory, we're done - can't find the keys file
- if (!file.exists()) {
- throw new AtClientConfigException("loadKeys: No file called " + atSign + keysFileSuffix
- + " at " + expectedKeysFilesLocation + " or " + legacyKeysFilesLocation +
- "\t Keys files are expected to be in ~/.atsign/keys/ (canonical location) or ./keys/ (legacy location)");
+ if (!legacyFile.exists()) {
+ throw new AtClientConfigException("loadKeys: No file found at " + file + " or " + legacyFile);
}
+ file = legacyFile;
}
return file;
}
diff --git a/at_client/src/test/java/org/atsign/client/impl/cli/CliIT.java b/at_client/src/test/java/org/atsign/client/impl/cli/CliIT.java
new file mode 100644
index 00000000..b82cc981
--- /dev/null
+++ b/at_client/src/test/java/org/atsign/client/impl/cli/CliIT.java
@@ -0,0 +1,164 @@
+package org.atsign.client.impl.cli;
+
+import static java.util.concurrent.TimeUnit.SECONDS;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.*;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.security.SecureRandom;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import org.atsign.client.impl.util.KeysUtils;
+import org.atsign.cucumber.helpers.Helpers;
+import org.atsign.virtualenv.VirtualEnv;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import com.github.stefanbirkner.systemlambda.Statement;
+import com.github.stefanbirkner.systemlambda.SystemLambda;
+
+class CliIT {
+
+ public static final String VIRTUAL_ENV_ROOT = "vip.ve.atsign.zone:64";
+ public static String AT_SIGN_KEYS_DIR;
+ public static String ATSIGN_KEYS_SUFFIX;
+
+ @BeforeAll
+ public static void classSetup() {
+ if (!Helpers.isHostPortReachable(VIRTUAL_ENV_ROOT, SECONDS.toMillis(2))) {
+ VirtualEnv.setUp();
+ }
+ AT_SIGN_KEYS_DIR = KeysUtils.expectedKeysFilesLocation;
+ ATSIGN_KEYS_SUFFIX = KeysUtils.keysFileSuffix;
+ KeysUtils.expectedKeysFilesLocation = "target/at_demo_data/lib/assets/atkeys";
+ KeysUtils.keysFileSuffix = ".atKeys";
+ }
+
+ @AfterAll
+ public static void classTeardown() {
+ KeysUtils.expectedKeysFilesLocation = AT_SIGN_KEYS_DIR;
+ KeysUtils.keysFileSuffix = ATSIGN_KEYS_SUFFIX;
+ }
+
+ @Test
+ public void testShareUsage() throws Exception {
+ int shareCode = runMainExpectSystemExit(() -> Share.main(new String[] {VIRTUAL_ENV_ROOT}));
+ assertThat(shareCode, not(equalTo(0)));
+
+ int scanCode = runMainExpectSystemExit(() -> Scan.main(new String[] {VIRTUAL_ENV_ROOT}));
+ assertThat(scanCode, not(equalTo(0)));
+
+ int getCode = runMainExpectSystemExit(() -> Get.main(new String[] {VIRTUAL_ENV_ROOT}));
+ assertThat(getCode, not(equalTo(0)));
+
+ int deleteCode = runMainExpectSystemExit(() -> Delete.main(new String[] {VIRTUAL_ENV_ROOT}));
+ assertThat(deleteCode, not(equalTo(0)));
+ }
+
+ @Test
+ public void testDumpKeys() throws Exception {
+ List stdout = runMain(() -> DumpKeys.main(new String[] {"colin"}));
+ findLines(stdout,
+ "\\s+key: aesPkamPublicKey", "\\s+value:\\s+\\S+",
+ "\\s+key: aesPkamPrivateKey", "\\s+value:\\s+\\S+");
+
+ }
+
+ @Test
+ public void testShareScanGetDelete() throws Exception {
+
+ String keyname = randomId(6);
+
+ // create a new key value
+
+ String[] shareArgs = {VIRTUAL_ENV_ROOT, "colin", "jeremy", keyname, "mary had a little lamb"};
+ List sharedStdout = runMain(() -> Share.main(shareArgs));
+
+ assertThat(sharedStdout, is(empty()));
+
+ // scan for new key value
+
+ String[] scanArgs = {VIRTUAL_ENV_ROOT, "colin", keyname};
+ List scanStdOut = runMain(() -> Scan.main(scanArgs), "l", "0", "q");
+
+ scanStdOut = findLines(scanStdOut, "Enter index you want to llookup.+");
+ scanStdOut = findLines(scanStdOut, "\\s+0:\\s+.*" + keyname + ".*");
+ findLines(scanStdOut,
+ "Full KeyName: @jeremy:" + keyname + "@colin",
+ "KeyName: " + keyname,
+ "SharedBy: @colin",
+ "SharedWith: @jeremy",
+ "KeyType: SharedKey");
+
+ // get the key value as the at sign which the key is shared with
+
+ String[] getArgs = {VIRTUAL_ENV_ROOT, "jeremy", "colin", keyname};
+ List getStdout = runMain(() -> Get.main(getArgs));
+ findLines(getStdout, "get response : mary had a little lamb");
+
+ // delete the key value
+
+ String[] deleteArgs = {VIRTUAL_ENV_ROOT, "colin", "jeremy", keyname};
+ List deleteStdout = runMain(() -> Delete.main(deleteArgs));
+ assertThat(deleteStdout, is(empty()));
+
+ // verify it's gone
+
+ List secondScanStdout = runMain(() -> Scan.main(scanArgs), "l", "q");
+ assertThrows(AssertionError.class, () -> findLines(secondScanStdout, "\\s+0:\\s+.*" + keyname + ".*"));
+ }
+
+ public static int runMainExpectSystemExit(Statement statement, String... stdin) throws Exception {
+ AtomicReference stdout = new AtomicReference<>();
+ return SystemLambda.catchSystemExit(() -> stdout.set(SystemLambda.tapSystemErr(() -> {
+ SystemLambda.SystemInStub stub = SystemLambda.withTextFromSystemIn(String.join("\n", List.of(stdin)));
+ stub.execute(statement);
+ })));
+ }
+
+ public static List runMain(Statement statement, String... stdin) throws Exception {
+ AtomicReference stdout = new AtomicReference<>();
+ stdout.set(SystemLambda.tapSystemOut(() -> {
+ SystemLambda.SystemInStub stub = SystemLambda.withTextFromSystemIn(String.join("\n", List.of(stdin)));
+ stub.execute(statement);
+ }));
+ return stdout.get().lines().collect(Collectors.toList());
+ }
+
+ public static List findLines(List lines, String... regexes) {
+ List result = lines;
+ for (String regex : regexes) {
+ result = findLine(result, Pattern.compile(regex).matcher(""));
+ }
+ return result;
+ }
+
+ public static List findLine(List lines, Matcher matcher) {
+ for (int i = 0; i < lines.size(); i++) {
+ if (matcher.reset(lines.get(i)).matches()) {
+ return lines.subList(i, lines.size() - 1);
+ }
+ }
+ throw new AssertionError("unable to find line that matches : " + matcher.pattern().pattern() + " in\n" +
+ String.join("\n", lines));
+ }
+
+ private static final char[] ALPHABET = "0123456789abcdefghijklmnopqrstuvwxyz".toCharArray();
+
+ private static final SecureRandom RANDOM = new SecureRandom();
+
+ public static String randomId(int length) {
+ char[] result = new char[length];
+
+ for (int i = 0; i < length; i++) {
+ result[i] = ALPHABET[RANDOM.nextInt(ALPHABET.length)];
+ }
+
+ return new String(result);
+ }
+}
diff --git a/examples/src/main/resources/simpleLogger.properties b/examples/src/main/resources/simpleLogger.properties
new file mode 100644
index 00000000..70f7fb47
--- /dev/null
+++ b/examples/src/main/resources/simpleLogger.properties
@@ -0,0 +1,5 @@
+org.slf4j.simpleLogger.defaultLogLevel=info
+org.slf4j.simpleLogger.showDateTime=true
+org.slf4j.simpleLogger.dateTimeFormat=HH:mm:ss.SSS
+
+#org.slf4j.simpleLogger.log.org.atsign.client=debug