Add ClientPut command implementation
authorDavid ‘Bombe’ Roden <bombe@freenetproject.org>
Wed, 8 Jul 2015 18:53:00 +0000 (20:53 +0200)
committerDavid ‘Bombe’ Roden <bombe@freenetproject.org>
Wed, 8 Jul 2015 18:53:00 +0000 (20:53 +0200)
src/main/java/net/pterodactylus/fcp/Key.java [new file with mode: 0644]
src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommand.java [new file with mode: 0644]
src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommandImpl.java [new file with mode: 0644]
src/main/java/net/pterodactylus/fcp/quelaton/DefaultFcpClient.java
src/main/java/net/pterodactylus/fcp/quelaton/FcpClient.java
src/main/java/net/pterodactylus/fcp/quelaton/Keyed.java [new file with mode: 0644]
src/main/java/net/pterodactylus/fcp/quelaton/Lengthed.java [new file with mode: 0644]
src/test/java/net/pterodactylus/fcp/quelaton/DefaultFcpClientTest.java

diff --git a/src/main/java/net/pterodactylus/fcp/Key.java b/src/main/java/net/pterodactylus/fcp/Key.java
new file mode 100644 (file)
index 0000000..f16b752
--- /dev/null
@@ -0,0 +1,20 @@
+package net.pterodactylus.fcp;
+
+/**
+ * Non-validating wrapper around a Freenet key.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Key {
+
+       private final String key;
+
+       public Key(String key) {
+               this.key = key;
+       }
+
+       public String getKey() {
+               return key;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommand.java b/src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommand.java
new file mode 100644 (file)
index 0000000..bc8046f
--- /dev/null
@@ -0,0 +1,21 @@
+package net.pterodactylus.fcp.quelaton;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.Optional;
+
+import net.pterodactylus.fcp.Key;
+
+/**
+ * FCP command that inserts data into Freenet.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ClientPutCommand {
+
+       ClientPutCommand named(String targetFilename);
+       Keyed<Optional<Key>> redirectTo(Key key);
+       Keyed<Optional<Key>> from(File file);
+       Lengthed<Keyed<Optional<Key>>> from(InputStream inputStream);
+
+}
diff --git a/src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommandImpl.java b/src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommandImpl.java
new file mode 100644 (file)
index 0000000..43d6c99
--- /dev/null
@@ -0,0 +1,160 @@
+package net.pterodactylus.fcp.quelaton;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+
+import net.pterodactylus.fcp.ClientPut;
+import net.pterodactylus.fcp.FcpMessage;
+import net.pterodactylus.fcp.Key;
+import net.pterodactylus.fcp.PutFailed;
+import net.pterodactylus.fcp.PutSuccessful;
+import net.pterodactylus.fcp.UploadFrom;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+/**
+ * Default {@link ClientPutCommand} implemented based on {@link FcpReplySequence}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+class ClientPutCommandImpl implements ClientPutCommand {
+
+       private final ListeningExecutorService threadPool;
+       private final ConnectionSupplier connectionSupplier;
+       private final AtomicReference<String> redirectUri = new AtomicReference<>();
+       private final AtomicReference<File> file = new AtomicReference<>();
+       private final AtomicReference<InputStream> payload = new AtomicReference<>();
+       private final AtomicLong length = new AtomicLong();
+       private final AtomicReference<String> targetFilename = new AtomicReference<>();
+
+       public ClientPutCommandImpl(ExecutorService threadPool, ConnectionSupplier connectionSupplier) {
+               this.threadPool = MoreExecutors.listeningDecorator(threadPool);
+               this.connectionSupplier = connectionSupplier;
+       }
+
+       @Override
+       public ClientPutCommand named(String targetFilename) {
+               this.targetFilename.set(targetFilename);
+               return this;
+       }
+
+       @Override
+       public Keyed<Optional<Key>> redirectTo(Key key) {
+               this.redirectUri.set(Objects.requireNonNull(key, "key must not be null").getKey());
+               return this::key;
+       }
+
+       @Override
+       public Keyed<Optional<Key>> from(File file) {
+               this.file.set(Objects.requireNonNull(file, "file must not be null"));
+               return this::key;
+       }
+
+       @Override
+       public Lengthed<Keyed<Optional<Key>>> from(InputStream inputStream) {
+               payload.set(Objects.requireNonNull(inputStream, "inputStream must not be null"));
+               return this::length;
+       }
+
+       private Keyed<Optional<Key>> length(long length) {
+               this.length.set(length);
+               return this::key;
+       }
+
+       private ListenableFuture<Optional<Key>> key(Key key) {
+               String identifier = new RandomIdentifierGenerator().generate();
+               ClientPut clientPut = createClientPutCommand(key.getKey(), identifier);
+               return threadPool.submit(() -> new ClientPutReplySequence().send(clientPut).get());
+       }
+
+       private ClientPut createClientPutCommand(String uri, String identifier) {
+               ClientPut clientPut;
+               if (file.get() != null) {
+                       clientPut = createClientPutFromDisk(uri, identifier, file.get());
+               } else if (redirectUri.get() != null) {
+                       clientPut = createClientPutRedirect(uri, identifier, redirectUri.get());
+               } else {
+                       clientPut = createClientPutDirect(uri, identifier, length.get(), payload.get());
+               }
+               if (targetFilename.get() != null) {
+                       clientPut.setTargetFilename(targetFilename.get());
+               }
+               return clientPut;
+       }
+
+       private ClientPut createClientPutFromDisk(String uri, String identifier, File file) {
+               ClientPut clientPut = new ClientPut(uri, identifier, UploadFrom.disk);
+               clientPut.setFilename(file.getAbsolutePath());
+               return clientPut;
+       }
+
+       private ClientPut createClientPutRedirect(String uri, String identifier, String redirectUri) {
+               ClientPut clientPut = new ClientPut(uri, identifier, UploadFrom.redirect);
+               clientPut.setTargetURI(redirectUri);
+               return clientPut;
+       }
+
+       private ClientPut createClientPutDirect(String uri, String identifier, long length, InputStream payload) {
+               ClientPut clientPut = new ClientPut(uri, identifier, UploadFrom.direct);
+               clientPut.setDataLength(length);
+               clientPut.setPayloadInputStream(payload);
+               return clientPut;
+       }
+
+       private class ClientPutReplySequence extends FcpReplySequence<Optional<Key>> {
+
+               private final AtomicReference<String> identifier = new AtomicReference<>();
+               private final AtomicReference<Key> finalKey = new AtomicReference<>();
+               private final AtomicBoolean putFinished = new AtomicBoolean();
+
+               public ClientPutReplySequence() throws IOException {
+                       super(ClientPutCommandImpl.this.threadPool, ClientPutCommandImpl.this.connectionSupplier.get());
+               }
+
+               @Override
+               protected boolean isFinished() {
+                       return putFinished.get();
+               }
+
+               @Override
+               protected Optional<Key> getResult() {
+                       return Optional.ofNullable(finalKey.get());
+               }
+
+               @Override
+               public ListenableFuture<Optional<Key>> send(FcpMessage fcpMessage) throws IOException {
+                       identifier.set(fcpMessage.getField("Identifier"));
+                       return super.send(fcpMessage);
+               }
+
+               @Override
+               protected void consumePutSuccessful(PutSuccessful putSuccessful) {
+                       if (putSuccessful.getIdentifier().equals(identifier.get())) {
+                               finalKey.set(new Key(putSuccessful.getURI()));
+                               putFinished.set(true);
+                       }
+               }
+
+               @Override
+               protected void consumePutFailed(PutFailed putFailed) {
+                       if (putFailed.getIdentifier().equals(identifier.get())) {
+                               putFinished.set(true);
+                       }
+               }
+
+               @Override
+               protected void consumeConnectionClosed(Throwable throwable) {
+                       putFinished.set(true);
+               }
+       }
+
+}
index 6d46ef7..42522e0 100644 (file)
@@ -72,6 +72,11 @@ public class DefaultFcpClient implements FcpClient {
                return new ClientGetCommandImpl(threadPool, this::connect);
        }
 
+       @Override
+       public ClientPutCommand clientPut() {
+               return new ClientPutCommandImpl(threadPool, this::connect);
+       }
+
        private class ClientHelloReplySequence extends FcpReplySequence<Void> {
 
                private final AtomicReference<NodeHello> receivedNodeHello;
index 905dd4f..6e5ab0a 100644 (file)
@@ -9,5 +9,6 @@ public interface FcpClient {
 
        GenerateKeypairCommand generateKeypair();
        ClientGetCommand clientGet();
+       ClientPutCommand clientPut();
 
 }
diff --git a/src/main/java/net/pterodactylus/fcp/quelaton/Keyed.java b/src/main/java/net/pterodactylus/fcp/quelaton/Keyed.java
new file mode 100644 (file)
index 0000000..ea10ab9
--- /dev/null
@@ -0,0 +1,18 @@
+package net.pterodactylus.fcp.quelaton;
+
+import net.pterodactylus.fcp.Key;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+/**
+ * The terminal operation of an FCP command, requiring a {@link Key}.
+ *
+ * @param <R>
+ *     The type of the command result
+ * @author <a href="mailto:bombe@freenetproject.org">David ‘Bombe’ Roden</a>
+ */
+public interface Keyed<R> {
+
+       ListenableFuture<R> key(Key key);
+
+}
diff --git a/src/main/java/net/pterodactylus/fcp/quelaton/Lengthed.java b/src/main/java/net/pterodactylus/fcp/quelaton/Lengthed.java
new file mode 100644 (file)
index 0000000..e55b197
--- /dev/null
@@ -0,0 +1,14 @@
+package net.pterodactylus.fcp.quelaton;
+
+/**
+ * An intermediary interface for FCP commands that require a length parameter.
+ *
+ * @param <R>
+ *     The type of the next command part
+ * @author <a href="mailto:bombe@freenetproject.org">David ‘Bombe’ Roden</a>
+ */
+public interface Lengthed<R> {
+
+       R length(long length);
+
+}
index 5a3c6a9..73774be 100644 (file)
@@ -1,9 +1,10 @@
 package net.pterodactylus.fcp.quelaton;
 
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.containsInAnyOrder;
 import static org.hamcrest.Matchers.is;
 
+import java.io.ByteArrayInputStream;
+import java.io.File;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.List;
@@ -14,6 +15,7 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.Future;
 
 import net.pterodactylus.fcp.FcpKeyPair;
+import net.pterodactylus.fcp.Key;
 import net.pterodactylus.fcp.Priority;
 import net.pterodactylus.fcp.fake.FakeTcpServer;
 import net.pterodactylus.fcp.quelaton.ClientGetCommand.Data;
@@ -268,4 +270,97 @@ public class DefaultFcpClientTest {
                };
        }
 
+       @Test
+       public void clientPutWithDirectDataSendsCorrectCommand()
+       throws IOException, ExecutionException, InterruptedException {
+               fcpClient.clientPut()
+                       .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                       .length(6)
+                       .key(new Key("KSK@foo.txt"));
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("Hello"));
+               assertThat(lines, matchesFcpMessage("ClientPut", "UploadFrom=direct", "DataLength=6", "URI=KSK@foo.txt"));
+       }
+
+       @Test
+       public void clientPutWithDirectDataSucceedsOnCorrectIdentifier()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Key>> key = fcpClient.clientPut()
+                       .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                       .length(6)
+                       .key(new Key("KSK@foo.txt"));
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("Hello"));
+               String identifier = extractIdentifier(lines);
+               fcpServer.writeLine(
+                       "PutFailed",
+                       "Identifier=not-the-right-one",
+                       "EndMessage"
+               );
+               fcpServer.writeLine(
+                       "PutSuccessful",
+                       "URI=KSK@foo.txt",
+                       "Identifier=" + identifier,
+                       "EndMessage"
+               );
+               assertThat(key.get().get().getKey(), is("KSK@foo.txt"));
+       }
+
+       @Test
+       public void clientPutWithDirectDataFailsOnCorrectIdentifier()
+       throws InterruptedException, ExecutionException, IOException {
+               Future<Optional<Key>> key = fcpClient.clientPut()
+                       .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                       .length(6)
+                       .key(new Key("KSK@foo.txt"));
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("Hello"));
+               String identifier = extractIdentifier(lines);
+               fcpServer.writeLine(
+                       "PutSuccessful",
+                       "Identifier=not-the-right-one",
+                       "URI=KSK@foo.txt",
+                       "EndMessage"
+               );
+               fcpServer.writeLine(
+                       "PutFailed",
+                       "Identifier=" + identifier,
+                       "EndMessage"
+               );
+               assertThat(key.get().isPresent(), is(false));
+       }
+
+       @Test
+       public void clientPutWithRenamedDirectDataSendsCorrectCommand()
+       throws InterruptedException, ExecutionException, IOException {
+               fcpClient.clientPut()
+                       .named("otherName.txt")
+                       .from(new ByteArrayInputStream("Hello\n".getBytes()))
+                       .length(6)
+                       .key(new Key("KSK@foo.txt"));
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("Hello"));
+               assertThat(lines, matchesFcpMessage("ClientPut", "TargetFilename=otherName.txt", "UploadFrom=direct",
+                       "DataLength=6", "URI=KSK@foo.txt"));
+       }
+
+       @Test
+       public void clientPutWithRedirectSendsCorrectCommand()
+       throws IOException, ExecutionException, InterruptedException {
+               fcpClient.clientPut().redirectTo(new Key("KSK@bar.txt")).key(new Key("KSK@foo.txt"));
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               assertThat(lines,
+                       matchesFcpMessage("ClientPut", "UploadFrom=redirect", "URI=KSK@foo.txt", "TargetURI=KSK@bar.txt"));
+       }
+
+       @Test
+       public void clientPutWithFileSendsCorrectCommand() throws InterruptedException, ExecutionException, IOException {
+               fcpClient.clientPut().from(new File("/tmp/data.txt")).key(new Key("KSK@foo.txt"));
+               connectNode();
+               List<String> lines = fcpServer.collectUntil(is("EndMessage"));
+               assertThat(lines,
+                       matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt", "Filename=/tmp/data.txt"));
+       }
+
 }