Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion exonum-light-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@
<junit.jupiter.version>5.4.0</junit.jupiter.version>
<mockito.version>2.24.0</mockito.version>
<hamcrest.version>2.0.0.0</hamcrest.version>

<jsonpath.version>2.4.0</jsonpath.version>
<!--Plugins-->
<!-- Checkstyle -->
<checkstyle.severity>warning</checkstyle.severity>
Expand Down Expand Up @@ -121,6 +121,12 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
Expand All @@ -142,6 +148,13 @@
<scope>test</scope>
</dependency>

<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path-assert</artifactId>
<version>${jsonpath.version}</version>
<scope>test</scope>
</dependency>

</dependencies>

<dependencyManagement>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,17 @@

import com.exonum.binding.common.hash.HashCode;
import com.exonum.binding.common.message.TransactionMessage;
import com.exonum.client.response.HealthCheckInfo;
import java.net.MalformedURLException;
import java.net.URL;
import okhttp3.OkHttpClient;

/**
* Main interface for Exonum Light client.
* Provides a convenient way for interaction with Exonum framework APIs.
* All the methods of the interface work in a blocking way
* i.e. invoke underlying request immediately, and block until the response can be processed
* or an error occurs.
* <p/><i>Implementations of that interface are required to be thread-safe</i>.
**/
public interface ExonumClient {
Expand All @@ -40,6 +44,28 @@ public interface ExonumClient {
*/
HashCode submitTransaction(TransactionMessage tx);

/**
* Returns a number of unconfirmed transactions which are currently located in
* the unconfirmed transactions pool and are waiting for acceptance to a block.
* @throws RuntimeException if the client is unable to complete a request
* (e.g., in case of connectivity problems)
*/
int getUnconfirmedTransactionsCount();

/**
* Returns the node health check information.
* @throws RuntimeException if the client is unable to complete a request
* (e.g., in case of connectivity problems)
*/
HealthCheckInfo healthCheck();

/**
* Returns string containing information about Exonum, Rust and OS version.
* @throws RuntimeException if the client is unable to complete a request
* (e.g., in case of connectivity problems)
*/
String getUserAgentInfo();

/**
* Returns Exonum client builder.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,16 +17,20 @@

package com.exonum.client;

import static com.exonum.binding.common.serialization.json.JsonSerializer.json;
import static com.exonum.client.ExonumUrls.HEALTH_CHECK;
import static com.exonum.client.ExonumUrls.MEMORY_POOL;
import static com.exonum.client.ExonumUrls.SUBMIT_TRANSACTION;
import static com.exonum.client.ExonumUrls.USER_AGENT;

import com.exonum.binding.common.hash.HashCode;
import com.exonum.binding.common.message.TransactionMessage;
import com.exonum.client.response.HealthCheckInfo;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.io.BaseEncoding;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.function.Function;
import okhttp3.MediaType;
import okhttp3.OkHttpClient;
import okhttp3.Request;
Expand All @@ -53,20 +57,45 @@ class ExonumHttpClient implements ExonumClient {
@Override
public HashCode submitTransaction(TransactionMessage transactionMessage) {
String msg = toHex(transactionMessage.toBytes());
Request request = postRequest(toFullUrl(SUBMIT_TRANSACTION),
ExplorerApiHelper.createSubmitTxBody(msg));

Request request = createRequest(toFullUrl(SUBMIT_TRANSACTION), new SubmitTxRequest(msg));
SubmitTxResponse result = blockingExecuteWithResponse(request, SubmitTxResponse.class);
return blockingExecuteAndParse(request, ExplorerApiHelper::parseSubmitTxResponse);
}

@Override
public int getUnconfirmedTransactionsCount() {
Request request = getRequest(toFullUrl(MEMORY_POOL));

return blockingExecuteAndParse(request, SystemApiHelper::parseMemoryPoolJson);
}

@Override
public HealthCheckInfo healthCheck() {
Request request = getRequest(toFullUrl(HEALTH_CHECK));

return blockingExecuteAndParse(request, SystemApiHelper::parseHealthCheckJson);
}

return result.getHash();
@Override
public String getUserAgentInfo() {
Request request = getRequest(toFullUrl(USER_AGENT));

return blockingExecutePlainText(request);
}

private static String toHex(byte[] array) {
return HEX_ENCODER.encode(array);
}

private static Request createRequest(URL url, Object requestBody) {
String jsonBody = json().toJson(requestBody);
private static Request getRequest(URL url) {
return new Request.Builder()
.url(url)
.get()
.build();
}

private static Request postRequest(URL url, String jsonBody) {
return new Request.Builder()
.url(url)
.post(RequestBody.create(MEDIA_TYPE_JSON, jsonBody))
Expand All @@ -81,17 +110,20 @@ private URL toFullUrl(String relativeUrl) {
}
}

private <T> T blockingExecuteWithResponse(Request request, Class<T> responseObject) {
private String blockingExecutePlainText(Request request) {
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new RuntimeException("Execution wasn't success: " + response.toString());
}
String responseJson = response.body().string();

return json().fromJson(responseJson, responseObject);
return response.body().string();
} catch (IOException e) {
throw new RuntimeException(e);
}
}

private <T> T blockingExecuteAndParse(Request request, Function<String, T> parser) {
String response = blockingExecutePlainText(request);
return parser.apply(response);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,11 @@
* Contains Exonum API URLs.
*/
final class ExonumUrls {
static final String SUBMIT_TRANSACTION = "/api/explorer/v1/transactions";
private static final String EXPLORER_PATHS_PREFIX = "/api/explorer/v1";
private static final String SYS_PATHS_PREFIX = "/api/system/v1";
static final String SUBMIT_TRANSACTION = EXPLORER_PATHS_PREFIX + "/transactions";
static final String MEMORY_POOL = SYS_PATHS_PREFIX + "/mempool";
static final String HEALTH_CHECK = SYS_PATHS_PREFIX + "/healthcheck";
static final String USER_AGENT = SYS_PATHS_PREFIX + "/user_agent";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/*
* Copyright 2019 The Exonum Team
*
* 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 com.exonum.client;

import static com.exonum.binding.common.serialization.json.JsonSerializer.json;

import com.exonum.binding.common.hash.HashCode;
import com.google.common.annotations.VisibleForTesting;
import com.google.gson.annotations.SerializedName;
import lombok.Value;

/**
* Utility class for Exonum Explorer API.
*/
final class ExplorerApiHelper {

static String createSubmitTxBody(String message) {
SubmitTxRequest request = new SubmitTxRequest(message);
return json().toJson(request);
}

static HashCode parseSubmitTxResponse(String json) {
SubmitTxResponse response = json().fromJson(json, SubmitTxResponse.class);
return response.getHash();
}

/**
* Json object wrapper for submit transaction request.
*/
@Value
@VisibleForTesting
static class SubmitTxRequest {
@SerializedName("tx_body")
String body;
}

/**
* Json object wrapper for submit transaction response.
*/
@Value
private static class SubmitTxResponse {
@SerializedName("tx_hash")
HashCode hash;
}

private ExplorerApiHelper() {
throw new UnsupportedOperationException("Not instantiable");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright 2019 The Exonum Team
*
* 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 com.exonum.client;

import static com.exonum.binding.common.serialization.json.JsonSerializer.json;

import com.exonum.client.response.ConsensusStatus;
import com.exonum.client.response.HealthCheckInfo;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.annotations.SerializedName;
import lombok.Value;

/**
* Utility class for Exonum System API.
*/
final class SystemApiHelper {

static HealthCheckInfo parseHealthCheckJson(String json) {
// TODO: ECR-2925 (core dependency) change after the response format update
HealthCheckResponse response = json().fromJson(json, HealthCheckResponse.class);
String consensusStatus = response.getConsensusStatus().toUpperCase();
JsonElement connectivity = response.getConnectivity();
if (connectivity.isJsonObject()) {
JsonObject connectivityObject = connectivity.getAsJsonObject();
int connectionsNumber = connectivityObject
.get("Connected").getAsJsonObject()
.get("amount").getAsInt();
return new HealthCheckInfo(ConsensusStatus.valueOf(consensusStatus), connectionsNumber);
} else {
return new HealthCheckInfo(ConsensusStatus.valueOf(consensusStatus), 0);
}
}

static int parseMemoryPoolJson(String json) {
MemoryPoolResponse response = json().fromJson(json, MemoryPoolResponse.class);
return response.getSize();
}

/**
* Json object wrapper for memory pool response.
*/
@Value
private static class MemoryPoolResponse {
int size;
}

/**
* Json object wrapper for health check response.
*/
@Value
private class HealthCheckResponse {
@SerializedName("consensus_status")
String consensusStatus;
JsonElement connectivity;
}

private SystemApiHelper() {
throw new UnsupportedOperationException("Not instantiable");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,23 @@
*
*/

package com.exonum.client;

import com.google.gson.annotations.SerializedName;
import lombok.Value;
package com.exonum.client.response;

/**
* Json object wrapper for submit transaction request.
* Consensus status of a particular node.
*/
@Value
class SubmitTxRequest {
@SerializedName("tx_body")
String body;
public enum ConsensusStatus {
/**
* Shows that consensus is active
* i.e. it is enabled and the node has enough connected peers.
*/
ACTIVE,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you happen to know the difference between these two? As it is public, I'd document it.

/**
* Shows that consensus is enabled on the node.
*/
ENABLED,
/**
* Shows that consensus is disabled on the node.
*/
DISABLED
}
Loading