From 1ce220842b96db2ea57e456c70977620943cb5de Mon Sep 17 00:00:00 2001 From: =?utf8?q?David=20=E2=80=98Bombe=E2=80=99=20Roden?= Date: Wed, 8 Jul 2015 20:53:00 +0200 Subject: [PATCH] Add ClientPut command implementation --- src/main/java/net/pterodactylus/fcp/Key.java | 20 +++ .../fcp/quelaton/ClientPutCommand.java | 21 +++ .../fcp/quelaton/ClientPutCommandImpl.java | 160 +++++++++++++++++++++ .../fcp/quelaton/DefaultFcpClient.java | 5 + .../net/pterodactylus/fcp/quelaton/FcpClient.java | 1 + .../java/net/pterodactylus/fcp/quelaton/Keyed.java | 18 +++ .../net/pterodactylus/fcp/quelaton/Lengthed.java | 14 ++ .../fcp/quelaton/DefaultFcpClientTest.java | 97 ++++++++++++- 8 files changed, 335 insertions(+), 1 deletion(-) create mode 100644 src/main/java/net/pterodactylus/fcp/Key.java create mode 100644 src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommand.java create mode 100644 src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommandImpl.java create mode 100644 src/main/java/net/pterodactylus/fcp/quelaton/Keyed.java create mode 100644 src/main/java/net/pterodactylus/fcp/quelaton/Lengthed.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 index 0000000..f16b752 --- /dev/null +++ b/src/main/java/net/pterodactylus/fcp/Key.java @@ -0,0 +1,20 @@ +package net.pterodactylus.fcp; + +/** + * Non-validating wrapper around a Freenet key. + * + * @author David ‘Bombe’ Roden + */ +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 index 0000000..bc8046f --- /dev/null +++ b/src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommand.java @@ -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 David ‘Bombe’ Roden + */ +public interface ClientPutCommand { + + ClientPutCommand named(String targetFilename); + Keyed> redirectTo(Key key); + Keyed> from(File file); + Lengthed>> 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 index 0000000..43d6c99 --- /dev/null +++ b/src/main/java/net/pterodactylus/fcp/quelaton/ClientPutCommandImpl.java @@ -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 David ‘Bombe’ Roden + */ +class ClientPutCommandImpl implements ClientPutCommand { + + private final ListeningExecutorService threadPool; + private final ConnectionSupplier connectionSupplier; + private final AtomicReference redirectUri = new AtomicReference<>(); + private final AtomicReference file = new AtomicReference<>(); + private final AtomicReference payload = new AtomicReference<>(); + private final AtomicLong length = new AtomicLong(); + private final AtomicReference 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> redirectTo(Key key) { + this.redirectUri.set(Objects.requireNonNull(key, "key must not be null").getKey()); + return this::key; + } + + @Override + public Keyed> from(File file) { + this.file.set(Objects.requireNonNull(file, "file must not be null")); + return this::key; + } + + @Override + public Lengthed>> from(InputStream inputStream) { + payload.set(Objects.requireNonNull(inputStream, "inputStream must not be null")); + return this::length; + } + + private Keyed> length(long length) { + this.length.set(length); + return this::key; + } + + private ListenableFuture> 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> { + + private final AtomicReference identifier = new AtomicReference<>(); + private final AtomicReference 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 getResult() { + return Optional.ofNullable(finalKey.get()); + } + + @Override + public ListenableFuture> 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); + } + } + +} diff --git a/src/main/java/net/pterodactylus/fcp/quelaton/DefaultFcpClient.java b/src/main/java/net/pterodactylus/fcp/quelaton/DefaultFcpClient.java index 6d46ef7..42522e0 100644 --- a/src/main/java/net/pterodactylus/fcp/quelaton/DefaultFcpClient.java +++ b/src/main/java/net/pterodactylus/fcp/quelaton/DefaultFcpClient.java @@ -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 { private final AtomicReference receivedNodeHello; diff --git a/src/main/java/net/pterodactylus/fcp/quelaton/FcpClient.java b/src/main/java/net/pterodactylus/fcp/quelaton/FcpClient.java index 905dd4f..6e5ab0a 100644 --- a/src/main/java/net/pterodactylus/fcp/quelaton/FcpClient.java +++ b/src/main/java/net/pterodactylus/fcp/quelaton/FcpClient.java @@ -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 index 0000000..ea10ab9 --- /dev/null +++ b/src/main/java/net/pterodactylus/fcp/quelaton/Keyed.java @@ -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 + * The type of the command result + * @author David ‘Bombe’ Roden + */ +public interface Keyed { + + ListenableFuture 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 index 0000000..e55b197 --- /dev/null +++ b/src/main/java/net/pterodactylus/fcp/quelaton/Lengthed.java @@ -0,0 +1,14 @@ +package net.pterodactylus.fcp.quelaton; + +/** + * An intermediary interface for FCP commands that require a length parameter. + * + * @param + * The type of the next command part + * @author David ‘Bombe’ Roden + */ +public interface Lengthed { + + R length(long length); + +} diff --git a/src/test/java/net/pterodactylus/fcp/quelaton/DefaultFcpClientTest.java b/src/test/java/net/pterodactylus/fcp/quelaton/DefaultFcpClientTest.java index 5a3c6a9..73774be 100644 --- a/src/test/java/net/pterodactylus/fcp/quelaton/DefaultFcpClientTest.java +++ b/src/test/java/net/pterodactylus/fcp/quelaton/DefaultFcpClientTest.java @@ -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 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> key = fcpClient.clientPut() + .from(new ByteArrayInputStream("Hello\n".getBytes())) + .length(6) + .key(new Key("KSK@foo.txt")); + connectNode(); + List 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> key = fcpClient.clientPut() + .from(new ByteArrayInputStream("Hello\n".getBytes())) + .length(6) + .key(new Key("KSK@foo.txt")); + connectNode(); + List 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 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 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 lines = fcpServer.collectUntil(is("EndMessage")); + assertThat(lines, + matchesFcpMessage("ClientPut", "UploadFrom=disk", "URI=KSK@foo.txt", "Filename=/tmp/data.txt")); + } + } -- 2.7.4