From af6b45c3d5ea2e8b47e384c8c215cb73f6a4d30c Mon Sep 17 00:00:00 2001 From: shemnon Date: Fri, 6 Sep 2019 07:05:19 -0600 Subject: [PATCH 1/2] Initial PoC for RPC end points via the plugin mechanism. --- .../acceptance/dsl/node/PantheonNode.java | 2 +- .../dsl/node/ThreadPantheonNodeRunner.java | 5 + .../configuration/PantheonNodeFactory.java | 17 ++- .../plugins/RpcEndpointPluginTest.java | 106 ++++++++++++++++++ .../jsonrpc/JsonRpcConfiguration.java | 11 ++ .../internal/methods/PluginJsonRpcMethod.java | 54 +++++++++ .../tech/pegasys/pantheon/RunnerBuilder.java | 21 ++-- .../pegasys/pantheon/cli/PantheonCommand.java | 6 + .../services/RPCEndpointServiceImpl.java | 42 +++++++ .../plugins/TestRpcEndpointPlugin.java | 97 ++++++++++++++++ .../services/RpcEndpointImplTest.java | 61 ++++++++++ plugin-api/build.gradle | 2 +- .../plugin/services/RpcEndpointService.java | 52 +++++++++ .../plugin/services/rpc/RPCMethod.java | 15 +++ 14 files changed, 478 insertions(+), 13 deletions(-) create mode 100644 acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/plugins/RpcEndpointPluginTest.java create mode 100644 ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/PluginJsonRpcMethod.java create mode 100644 pantheon/src/main/java/tech/pegasys/pantheon/services/RPCEndpointServiceImpl.java create mode 100644 pantheon/src/test/java/tech/pegasys/pantheon/plugins/TestRpcEndpointPlugin.java create mode 100644 pantheon/src/test/java/tech/pegasys/pantheon/services/RpcEndpointImplTest.java create mode 100644 plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/RpcEndpointService.java create mode 100644 plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/rpc/RPCMethod.java diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java index d13d486db4..5ac50cd234 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/PantheonNode.java @@ -247,7 +247,7 @@ public Optional getJsonRpcWebSocketPort() { } public Optional getJsonRpcSocketPort() { - if (isWebSocketsRpcEnabled()) { + if (isJsonRpcEnabled()) { return Optional.of(Integer.valueOf(portsProperties.getProperty("json-rpc"))); } else { return Optional.empty(); diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java index 19f530ee09..8ac767a995 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/ThreadPantheonNodeRunner.java @@ -30,9 +30,11 @@ import tech.pegasys.pantheon.metrics.noop.NoOpMetricsSystem; import tech.pegasys.pantheon.plugin.services.PantheonEvents; import tech.pegasys.pantheon.plugin.services.PicoCLIOptions; +import tech.pegasys.pantheon.plugin.services.RpcEndpointService; import tech.pegasys.pantheon.services.PantheonEventsImpl; import tech.pegasys.pantheon.services.PantheonPluginContextImpl; import tech.pegasys.pantheon.services.PicoCLIOptionsImpl; +import tech.pegasys.pantheon.services.RPCEndpointServiceImpl; import tech.pegasys.pantheon.services.kvstore.RocksDbConfiguration; import java.io.File; @@ -72,6 +74,9 @@ private PantheonPluginContextImpl buildPluginContext(final PantheonNode node) { pluginsDirFile.deleteOnExit(); } System.setProperty("pantheon.plugins.dir", pluginsPath.toString()); + final RPCEndpointServiceImpl rpcEndpointService = new RPCEndpointServiceImpl(); + node.jsonRpcConfiguration().setPluginEndpoints(rpcEndpointService.getRpcMethods()); + pantheonPluginContext.addService(RpcEndpointService.class, rpcEndpointService); pantheonPluginContext.registerPlugins(pluginsPath); return pantheonPluginContext; } diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/configuration/PantheonNodeFactory.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/configuration/PantheonNodeFactory.java index 511a173440..77c60a2959 100644 --- a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/configuration/PantheonNodeFactory.java +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/dsl/node/configuration/PantheonNodeFactory.java @@ -165,14 +165,23 @@ public PantheonNode createArchiveNodeWithRpcDisabled(final String name) throws I } public PantheonNode createPluginsNode( - final String name, final List plugins, final List extraCLIOptions) + final String name, + final List plugins, + final List extraCLIOptions, + final RpcApi... enabledRpcApis) throws IOException { - return create( + final PantheonNodeConfigurationBuilder pantheonNodeConfigurationBuilder = new PantheonNodeConfigurationBuilder() .name(name) .plugins(plugins) - .extraCLIOptions(extraCLIOptions) - .build()); + .extraCLIOptions(extraCLIOptions); + if (enabledRpcApis.length > 0) { + final JsonRpcConfiguration jsonRpcConfig = node.createJsonRpcEnabledConfig(); + jsonRpcConfig.setRpcApis(asList(enabledRpcApis)); + pantheonNodeConfigurationBuilder.jsonRpcConfiguration(jsonRpcConfig); + } + + return create(pantheonNodeConfigurationBuilder.build()); } public PantheonNode createArchiveNodeWithRpcApis( diff --git a/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/plugins/RpcEndpointPluginTest.java b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/plugins/RpcEndpointPluginTest.java new file mode 100644 index 0000000000..b4b3e5833a --- /dev/null +++ b/acceptance-tests/src/test/java/tech/pegasys/pantheon/tests/acceptance/plugins/RpcEndpointPluginTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.tests.acceptance.plugins; + +import static org.assertj.core.api.Assertions.assertThat; + +import tech.pegasys.pantheon.config.JsonUtil; +import tech.pegasys.pantheon.ethereum.jsonrpc.RpcApis; +import tech.pegasys.pantheon.tests.acceptance.dsl.AcceptanceTestBase; +import tech.pegasys.pantheon.tests.acceptance.dsl.node.PantheonNode; + +import java.io.IOException; +import java.util.Collections; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import org.junit.Before; +import org.junit.Test; + +public class RpcEndpointPluginTest extends AcceptanceTestBase { + + private PantheonNode node; + private OkHttpClient client; + protected static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); + + @Before + public void setUp() throws Exception { + node = + pantheon.createPluginsNode( + "node1", + Collections.singletonList("testPlugins"), + Collections.emptyList(), + RpcApis.NET); + cluster.start(node); + client = new OkHttpClient(); + } + + @Test + public void rpcWorking() throws IOException { + final String firstCall = "FirstCall"; + final String secondCall = "SecondCall"; + final String thirdCall = "ThirdCall"; + final String fourthCall = "FourthCall"; + + ObjectNode resultJson = callTestMethod("unitTests_replaceValue", firstCall); + assertThat(resultJson.get("result").asText()).isEqualTo("InitialValue"); + + resultJson = callTestMethod("unitTests_replaceValueArray", secondCall); + assertThat(resultJson.get("result").get(0).asText()).isEqualTo(firstCall); + + resultJson = callTestMethod("unitTests_replaceValueBean", thirdCall); + assertThat(resultJson.get("result").get("value").asText()).isEqualTo(secondCall); + + resultJson = callTestMethod("unitTests_replaceValueLength", fourthCall); + assertThat(resultJson.get("result").asInt()).isEqualTo(thirdCall.length()); + } + + @Test + public void throwsError() throws IOException { + ObjectNode resultJson = callTestMethod("unitTests_replaceValue", null); + assertThat(resultJson.get("result").asText()).isEqualTo("InitialValue"); + + resultJson = callTestMethod("unitTests_replaceValueLength", "InitialValue"); + assertThat(resultJson.get("error").get("message").asText()).isEqualTo("Internal error"); + } + + private ObjectNode callTestMethod(final String method, final String value) throws IOException { + final String resultString = + client + .newCall( + new Request.Builder() + .post( + RequestBody.create( + JSON, + "{\"jsonrpc\":\"2.0\",\"method\":\"" + + method + + "\",\"params\":[" + + (value == null ? value : "\"" + value + "\"") + + "],\"id\":33}")) + .url( + "http://" + + node.getHostName() + + ":" + + node.getJsonRpcSocketPort().get() + + "/") + .build()) + .execute() + .body() + .string(); + System.out.println(resultString); + return JsonUtil.objectNodeFromString(resultString); + } +} diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcConfiguration.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcConfiguration.java index f5297ec56a..733e47dd47 100644 --- a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcConfiguration.java +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/JsonRpcConfiguration.java @@ -17,7 +17,9 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Map; import java.util.Objects; +import java.util.function.Function; import com.google.common.base.MoreObjects; @@ -33,6 +35,7 @@ public class JsonRpcConfiguration { private List hostsWhitelist = Arrays.asList("localhost", "127.0.0.1"); private boolean authenticationEnabled = false; private String authenticationCredentialsFile; + private Map, ?>> pluginEndpoints = Collections.emptyMap(); public static JsonRpcConfiguration createDefault() { final JsonRpcConfiguration config = new JsonRpcConfiguration(); @@ -100,6 +103,14 @@ public void setHostsWhitelist(final List hostsWhitelist) { this.hostsWhitelist = hostsWhitelist; } + public Map, ?>> getPluginEndpoints() { + return pluginEndpoints; + } + + public void setPluginEndpoints(final Map, ?>> pluginEndpoints) { + this.pluginEndpoints = pluginEndpoints; + } + @Override public String toString() { return MoreObjects.toStringHelper(this) diff --git a/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/PluginJsonRpcMethod.java b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/PluginJsonRpcMethod.java new file mode 100644 index 0000000000..599a96332a --- /dev/null +++ b/ethereum/jsonrpc/src/main/java/tech/pegasys/pantheon/ethereum/jsonrpc/internal/methods/PluginJsonRpcMethod.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods; + +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.JsonRpcRequest; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcError; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcErrorResponse; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcResponse; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.response.JsonRpcSuccessResponse; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class PluginJsonRpcMethod implements JsonRpcMethod { + + private final String name; + private final Function, ?> function; + + public PluginJsonRpcMethod(final String name, final Function, ?> function) { + this.name = name; + this.function = function; + } + + @Override + public String getName() { + return name; + } + + @Override + public JsonRpcResponse response(final JsonRpcRequest request) { + try { + return new JsonRpcSuccessResponse( + request.getId(), + function.apply( + Arrays.stream(request.getParams()) + .map(o -> o == null ? null : o.toString()) + .collect(Collectors.toList()))); + } catch (final RuntimeException re) { + return new JsonRpcErrorResponse(request.getId(), JsonRpcError.INTERNAL_ERROR); + } + } +} diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java index 2cf3cf9bb3..8388b7b1e5 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/RunnerBuilder.java @@ -37,6 +37,7 @@ import tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter.FilterManager; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.filter.FilterRepository; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.JsonRpcMethod; +import tech.pegasys.pantheon.ethereum.jsonrpc.internal.methods.PluginJsonRpcMethod; import tech.pegasys.pantheon.ethereum.jsonrpc.internal.queries.BlockchainQueries; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketConfiguration; import tech.pegasys.pantheon.ethereum.jsonrpc.websocket.WebSocketRequestHandler; @@ -92,6 +93,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; @@ -105,7 +107,7 @@ public class RunnerBuilder { private PantheonController pantheonController; private NetworkingConfiguration networkingConfiguration = NetworkingConfiguration.create(); - private Collection bannedNodeIds = new ArrayList<>(); + private final Collection bannedNodeIds = new ArrayList<>(); private boolean p2pEnabled = true; private boolean discovery; private String p2pAdvertisedHost; @@ -304,8 +306,8 @@ public Runner build() { final Optional natManager = buildNatManager(natMethod); - NetworkBuilder inactiveNetwork = (caps) -> new NoopP2PNetwork(); - NetworkBuilder activeNetwork = + final NetworkBuilder inactiveNetwork = (caps) -> new NoopP2PNetwork(); + final NetworkBuilder activeNetwork = (caps) -> DefaultP2PNetwork.builder() .vertx(vertx) @@ -375,7 +377,8 @@ public Runner build() { privacyParameters, jsonRpcConfiguration, webSocketConfiguration, - metricsConfiguration); + metricsConfiguration, + jsonRpcConfiguration.getPluginEndpoints()); jsonRpcHttpService = Optional.of( new JsonRpcHttpService( @@ -433,7 +436,8 @@ public Runner build() { privacyParameters, jsonRpcConfiguration, webSocketConfiguration, - metricsConfiguration); + metricsConfiguration, + Collections.emptyMap()); final SubscriptionManager subscriptionManager = createSubscriptionManager(vertx, transactionPool); @@ -510,7 +514,7 @@ private Optional buildAccountPermissioningContro final TransactionSimulator transactionSimulator) { if (permissioningConfiguration.isPresent()) { - Optional accountPermissioningController = + final Optional accountPermissioningController = AccountPermissioningControllerFactory.create( permissioningConfiguration.get(), transactionSimulator, metricsSystem); @@ -563,7 +567,8 @@ private Map jsonRpcMethods( final PrivacyParameters privacyParameters, final JsonRpcConfiguration jsonRpcConfiguration, final WebSocketConfiguration webSocketConfiguration, - final MetricsConfiguration metricsConfiguration) { + final MetricsConfiguration metricsConfiguration, + final Map, ?>> pluginMethods) { final Map methods = new JsonRpcMethodsFactory() .methods( @@ -588,6 +593,8 @@ private Map jsonRpcMethods( webSocketConfiguration, metricsConfiguration); methods.putAll(pantheonController.getAdditionalJsonRpcMethods(jsonRpcApis)); + pluginMethods.forEach( + (name, function) -> methods.put(name, new PluginJsonRpcMethod(name, function))); return methods; } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java index 12f107054d..b688654c7f 100644 --- a/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java +++ b/pantheon/src/main/java/tech/pegasys/pantheon/cli/PantheonCommand.java @@ -92,10 +92,12 @@ import tech.pegasys.pantheon.plugin.services.MetricsSystem; import tech.pegasys.pantheon.plugin.services.PantheonEvents; import tech.pegasys.pantheon.plugin.services.PicoCLIOptions; +import tech.pegasys.pantheon.plugin.services.RpcEndpointService; import tech.pegasys.pantheon.plugin.services.metrics.MetricCategory; import tech.pegasys.pantheon.services.PantheonEventsImpl; import tech.pegasys.pantheon.services.PantheonPluginContextImpl; import tech.pegasys.pantheon.services.PicoCLIOptionsImpl; +import tech.pegasys.pantheon.services.RPCEndpointServiceImpl; import tech.pegasys.pantheon.services.kvstore.RocksDbConfiguration; import tech.pegasys.pantheon.util.PermissioningConfigurationValidator; import tech.pegasys.pantheon.util.bytes.BytesValue; @@ -171,6 +173,7 @@ public class PantheonCommand implements DefaultCommandValues, Runnable { private final RunnerBuilder runnerBuilder; private final PantheonController.Builder controllerBuilderFactory; private final PantheonPluginContextImpl pantheonPluginContext; + private final RPCEndpointServiceImpl rpcEndpointService; private final Map environment; protected KeyLoader getKeyLoader() { @@ -679,6 +682,7 @@ public PantheonCommand( this.controllerBuilderFactory = controllerBuilderFactory; this.pantheonPluginContext = pantheonPluginContext; this.environment = environment; + this.rpcEndpointService = new RPCEndpointServiceImpl(); } private StandaloneCommand standaloneCommands; @@ -774,6 +778,7 @@ private PantheonCommand handleUnstableOptions() { private PantheonCommand preparePlugins() { pantheonPluginContext.addService(PicoCLIOptions.class, new PicoCLIOptionsImpl(commandLine)); pantheonPluginContext.addService(MetricsSystem.class, getMetricsSystem()); + pantheonPluginContext.addService(RpcEndpointService.class, rpcEndpointService); pantheonPluginContext.registerPlugins(pluginsDir()); return this; } @@ -1002,6 +1007,7 @@ private JsonRpcConfiguration jsonRpcConfiguration() { jsonRpcConfiguration.setHostsWhitelist(hostsWhitelist); jsonRpcConfiguration.setAuthenticationEnabled(isRpcHttpAuthenticationEnabled); jsonRpcConfiguration.setAuthenticationCredentialsFile(rpcHttpAuthenticationCredentialsFile()); + jsonRpcConfiguration.setPluginEndpoints(rpcEndpointService.getRpcMethods()); return jsonRpcConfiguration; } diff --git a/pantheon/src/main/java/tech/pegasys/pantheon/services/RPCEndpointServiceImpl.java b/pantheon/src/main/java/tech/pegasys/pantheon/services/RPCEndpointServiceImpl.java new file mode 100644 index 0000000000..f7574de81f --- /dev/null +++ b/pantheon/src/main/java/tech/pegasys/pantheon/services/RPCEndpointServiceImpl.java @@ -0,0 +1,42 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.services; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; + +import tech.pegasys.pantheon.plugin.services.RpcEndpointService; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +public class RPCEndpointServiceImpl implements RpcEndpointService { + + private final Map, ?>> rpcMethods = new HashMap<>(); + + @Override + public void registerRPCEndpoint( + final String namespace, final String functionName, final Function, T> function) { + checkArgument(namespace.matches("\\p{Alnum}+"), "Namespace must be only alpha numeric"); + checkArgument(functionName.matches("\\p{Alnum}+"), "Function Name must be only alpha numeric"); + checkNotNull(function); + + rpcMethods.put(namespace + "_" + functionName, function); + } + + public Map, ?>> getRpcMethods() { + return rpcMethods; + } +} diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/plugins/TestRpcEndpointPlugin.java b/pantheon/src/test/java/tech/pegasys/pantheon/plugins/TestRpcEndpointPlugin.java new file mode 100644 index 0000000000..e2994dfe58 --- /dev/null +++ b/pantheon/src/test/java/tech/pegasys/pantheon/plugins/TestRpcEndpointPlugin.java @@ -0,0 +1,97 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.plugins; + +import static com.google.common.base.Preconditions.checkArgument; + +import tech.pegasys.pantheon.plugin.PantheonContext; +import tech.pegasys.pantheon.plugin.PantheonPlugin; +import tech.pegasys.pantheon.plugin.services.RpcEndpointService; + +import java.util.List; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.auto.service.AutoService; + +@AutoService(PantheonPlugin.class) +public class TestRpcEndpointPlugin implements PantheonPlugin { + + static class Bean { + final String foo = "bar"; + final String baz = "qux"; + final String value; + + Bean(final String value) { + this.value = value; + } + + public String getFoo() { + return foo; + } + + public String getBaz() { + return baz; + } + + public String getValue() { + return value; + } + } + + private final AtomicReference storage = new AtomicReference<>("InitialValue"); + + private String replaceValue(final List strings) { + checkArgument(strings.size() == 1, "Only one parameter accepted"); + return storage.getAndSet(strings.get(0)); + } + + private String[] replaceValueArray(final List strings) { + return new String[] {replaceValue(strings)}; + } + + private Bean replaceValueBean(final List strings) { + return new Bean(replaceValue(strings)); + } + + private long replaceValueLength(final List strings) { + // The NullPointerException risk here is deliberate and used in acceptance tests. + return replaceValue(strings).length(); + } + + @Override + public void register(final PantheonContext context) { + context + .getService(RpcEndpointService.class) + .ifPresent( + rpcEndpointService -> { + rpcEndpointService.registerRPCEndpoint( + "unitTests", "replaceValue", this::replaceValue); + rpcEndpointService.registerRPCEndpoint( + "unitTests", "replaceValueArray", this::replaceValueArray); + rpcEndpointService.registerRPCEndpoint( + "unitTests", "replaceValueBean", this::replaceValueBean); + rpcEndpointService.registerRPCEndpoint( + "unitTests", "replaceValueLength", this::replaceValueLength); + }); + } + + @Override + public void start() { + // nothing to do + } + + @Override + public void stop() { + // nothing to do + } +} diff --git a/pantheon/src/test/java/tech/pegasys/pantheon/services/RpcEndpointImplTest.java b/pantheon/src/test/java/tech/pegasys/pantheon/services/RpcEndpointImplTest.java new file mode 100644 index 0000000000..ce50698273 --- /dev/null +++ b/pantheon/src/test/java/tech/pegasys/pantheon/services/RpcEndpointImplTest.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.services; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.util.List; + +import org.junit.Before; +import org.junit.Test; + +public class RpcEndpointImplTest { + + private RPCEndpointServiceImpl serviceImpl; + + @Before + public void setUp() { + serviceImpl = new RPCEndpointServiceImpl(); + } + + @Test + public void testAddsRPC() { + serviceImpl.registerRPCEndpoint("unit", "test", Object::toString); + + assertThat(serviceImpl.getRpcMethods().size()).isEqualTo(1); + assertThat(serviceImpl.getRpcMethods()).containsOnlyKeys("unit_test"); + assertThat(serviceImpl.getRpcMethods().get("unit_test").apply(List.of("item"))) + .isEqualTo("[item]"); + } + + @Test + public void testBadNamespace() { + assertThatThrownBy( + () -> serviceImpl.registerRPCEndpoint("this won't work", "test", Object::toString)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testBadFunctionName() { + assertThatThrownBy( + () -> serviceImpl.registerRPCEndpoint("unit", "this won't work", Object::toString)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void testNullMethod() { + assertThatThrownBy(() -> serviceImpl.registerRPCEndpoint("unit", "test", null)) + .isInstanceOf(NullPointerException.class); + } +} diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index 6c2bad3b13..5d708626eb 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -56,7 +56,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'PBo0D4R6/1EYXEn+k0nmWHW4TkklUWQbQGNqgWzslfw=' + knownHash = 'cnmr8O+TmDi1NB3M1wNkMS9e8S0tdROuVytDCoX9ViA=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/RpcEndpointService.java b/plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/RpcEndpointService.java new file mode 100644 index 0000000000..7878fdd38f --- /dev/null +++ b/plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/RpcEndpointService.java @@ -0,0 +1,52 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.plugin.services; + +import java.util.List; +import java.util.function.Function; + +/** + * This service allows you to add method references and functions exposed via RPC endpoints. + * + *

This service will be available during the registration callback and must be used during the + * registration callback. RPC endpoints are configured prior to the start callback and all endpoints + * connected. No endpoint will actually be called prior to the start callback so initialization + * unrelated to the callback registration can also be done at that time. + */ +public interface RpcEndpointService { + + /** + * Register a function as an RPC endpoint exposed via JSON-RPC. + * + *

The mechanism is a Java function that takes a list of Strings and returns any Java object, + * registered in a specific namespace with a function name. + * + *

The resulting endpoint is the namespace and the function name concatinated with an + * underscore. In the future we may prohibit registration in certain well-known namespaces. + * + *

The method takes a list of the inputs expressed entirely as strings. Javascript numbers are + * converted to strings via their toString method, and complex input objects are not supported. + * + *

The output is a Java object, primitive, or array that will be inspected via Jackson databind. In general if + * JavaBeans naming patterns are followed those names will be reflected in the returned JSON + * object. If the method throws an exception the return is an error with an INTERNAL_ERROR + * treatment. + * + * @param namespace The namespace of the method, must be alphanumeric. + * @param functionName The name of the function, must be alphanumeric. + * @param function The function itself. + */ + void registerRPCEndpoint( + String namespace, String functionName, Function, T> function); +} diff --git a/plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/rpc/RPCMethod.java b/plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/rpc/RPCMethod.java new file mode 100644 index 0000000000..22ab4a1b95 --- /dev/null +++ b/plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/rpc/RPCMethod.java @@ -0,0 +1,15 @@ +/* + * Copyright 2019 ConsenSys AG. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package tech.pegasys.pantheon.plugin.services.rpc; + +public interface RPCMethod {} From bdcaea3b153882243bd8183a55d75ee77799fad8 Mon Sep 17 00:00:00 2001 From: shemnon Date: Fri, 6 Sep 2019 07:11:39 -0600 Subject: [PATCH 2/2] remove RPCMethod API, it's not needed --- plugin-api/build.gradle | 2 +- .../pantheon/plugin/services/rpc/RPCMethod.java | 15 --------------- 2 files changed, 1 insertion(+), 16 deletions(-) delete mode 100644 plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/rpc/RPCMethod.java diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index 5d708626eb..c6d6e44f98 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -56,7 +56,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'cnmr8O+TmDi1NB3M1wNkMS9e8S0tdROuVytDCoX9ViA=' + knownHash = 'jjDW1hosyQQ4gYAf8SfnZ0JEwU5KugoAlM1ckJmOoh0=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/rpc/RPCMethod.java b/plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/rpc/RPCMethod.java deleted file mode 100644 index 22ab4a1b95..0000000000 --- a/plugin-api/src/main/java/tech/pegasys/pantheon/plugin/services/rpc/RPCMethod.java +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2019 ConsenSys AG. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on - * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the - * specific language governing permissions and limitations under the License. - */ -package tech.pegasys.pantheon.plugin.services.rpc; - -public interface RPCMethod {}