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
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
*/
final class TransactionInvoker {
private final Service service;
private final Map<Integer, MethodHandle> transactionMethods;
private final Map<Integer, TransactionMethodObject> transactionMethods;

TransactionInvoker(Service service) {
this.service = service;
Expand All @@ -54,9 +54,11 @@ void invokeTransaction(int transactionId, byte[] arguments, TransactionContext c
throws TransactionExecutionException {
checkArgument(transactionMethods.containsKey(transactionId),
"No method with transaction id (%s)", transactionId);
TransactionMethodObject transactionMethodObject = transactionMethods.get(transactionId);
Object argumentsObject = transactionMethodObject.serializeArguments(arguments);
MethodHandle methodHandle = transactionMethodObject.getMethodHandle();
try {
MethodHandle methodHandle = transactionMethods.get(transactionId);
methodHandle.invoke(service, arguments, context);
methodHandle.invoke(service, argumentsObject, context);
} catch (Throwable throwable) {
if (throwable instanceof TransactionExecutionException) {
throw (TransactionExecutionException) throwable;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@
import static com.google.common.base.Preconditions.checkArgument;
import static java.util.stream.Collectors.toMap;

import com.exonum.binding.common.serialization.Serializer;
import com.exonum.binding.common.serialization.StandardSerializers;
import com.exonum.binding.core.transaction.TransactionContext;
import com.exonum.binding.core.transaction.TransactionMethod;
import com.google.common.annotations.VisibleForTesting;

import com.google.protobuf.MessageLite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
Expand All @@ -40,14 +42,14 @@ final class TransactionMethodExtractor {
*
* @see TransactionMethod
*/
static Map<Integer, MethodHandle> extractTransactionMethods(Class<?> serviceClass) {
static Map<Integer, TransactionMethodObject> extractTransactionMethods(Class<?> serviceClass) {
Map<Integer, Method> transactionMethods = findTransactionMethods(serviceClass);
Lookup lookup = MethodHandles.publicLookup()
.in(serviceClass);
return transactionMethods.entrySet().stream()
.peek(tx -> validateTransactionMethod(tx.getValue(), serviceClass))
.collect(toMap(Map.Entry::getKey,
(e) -> toMethodHandle(e.getValue(), lookup)));
(e) -> toTransactionMethodObject(e.getValue(), lookup)));
}

@VisibleForTesting
Expand Down Expand Up @@ -84,27 +86,42 @@ private static void checkDuplicates(Map<Integer, Method> transactionMethods, int
*/
private static void validateTransactionMethod(Method transaction, Class<?> serviceClass) {
String errorMessage = String.format("Method %s in a service class %s annotated with"
+ " @TransactionMethod should have precisely two parameters of the following types:"
+ " 'byte[]' and 'com.exonum.binding.core.transaction.TransactionContext'",
+ " @TransactionMethod should have precisely two parameters: transaction arguments of"
+ " 'byte[]' type or a protobuf type and transaction context of"
+ " 'com.exonum.binding.core.transaction.TransactionContext' type",
transaction.getName(), serviceClass.getName());
checkArgument(transaction.getParameterCount() == 2, errorMessage);
Class<?> firstParameter = transaction.getParameterTypes()[0];
Class<?> secondParameter = transaction.getParameterTypes()[1];
checkArgument(firstParameter == byte[].class,
checkArgument(firstParameter == byte[].class || isProtobufArgument(firstParameter),
String.format(errorMessage
+ ". But first parameter type was: %s", firstParameter.getName()));
checkArgument(TransactionContext.class.isAssignableFrom(secondParameter),
String.format(errorMessage
+ ". But second parameter type was: %s", secondParameter.getName()));
}

private static MethodHandle toMethodHandle(Method method, Lookup lookup) {
private static TransactionMethodObject toTransactionMethodObject(Method method, Lookup lookup) {
Serializer<?> argumentsSerializer = StandardSerializers.bytes();
Class parameterType = method.getParameterTypes()[0];
if (isProtobufArgument(parameterType)) {
argumentsSerializer = StandardSerializers.protobuf(parameterType);
}
MethodHandle methodHandle;
try {
return lookup.unreflect(method);
methodHandle = lookup.unreflect(method);
} catch (IllegalAccessException e) {
throw new IllegalArgumentException(
String.format("Couldn't access method %s", method.getName()), e);
}
return new TransactionMethodObject(methodHandle, argumentsSerializer);
}

/**
* Returns true if given class is a protobuf type; false otherwise.
*/
private static boolean isProtobufArgument(Class type) {
return MessageLite.class.isAssignableFrom(type);
}

private TransactionMethodExtractor() {}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* 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.binding.core.runtime;

import com.exonum.binding.common.serialization.Serializer;
import java.lang.invoke.MethodHandle;

/**
* Stores a method handle of a transaction and a protobuf serializer in case of a protobuf type
* transaction arguments.
*/
// TODO: rename to TransactionMethod after migration? Another name?
class TransactionMethodObject {
private final MethodHandle methodHandle;
private final Serializer<?> argumentsSerializer;

TransactionMethodObject(MethodHandle methodHandle, Serializer<?> argumentsSerializer) {
this.methodHandle = methodHandle;
this.argumentsSerializer = argumentsSerializer;
}

MethodHandle getMethodHandle() {
return methodHandle;
}

Object serializeArguments(byte[] arguments) {
return argumentsSerializer.fromBytes(arguments);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.exonum.binding.core.transaction;

import com.exonum.binding.common.message.TransactionMessage;
import com.exonum.core.messages.Runtime.ErrorKind;
import com.exonum.core.messages.Runtime.ExecutionError;
import java.lang.annotation.ElementType;
Expand All @@ -26,14 +25,16 @@

/**
* Indicates that a method is a transaction method. The annotated method should execute the
* transaction, possibly modifying the blockchain state. The method should:
* transaction, possibly modifying the blockchain state.
*
* <p>The method should be {@code public} and have the following parameters
* (in this particular order):
* <ul>
* <li>be public
* <li>have exactly two parameters - the
* {@linkplain TransactionMessage#getPayload() serialized transaction arguments} of type
* 'byte[]' and a transaction execution context, which allows to access the information about
* this transaction and modify the blockchain state through the included database fork of
* type '{@link TransactionContext}' in this particular order
* <li>transaction arguments either as 'byte[]' or as a protobuf message. Protobuf messages will
* be deserialized using a {@code #parseFrom(byte[])} method
* <li>transaction execution context, which allows to access the information about this
* transaction and modify the blockchain state through the included database fork of type
* '{@link TransactionContext}'
* </ul>
*
* <p>The annotated method might throw {@linkplain TransactionExecutionException} if the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import com.exonum.binding.core.service.Node;
import com.exonum.binding.core.service.Service;
import com.exonum.binding.core.storage.database.Snapshot;
import com.exonum.binding.core.storage.indices.TestProtoMessages;
import com.exonum.binding.core.transaction.TransactionContext;
import com.exonum.binding.core.transaction.TransactionExecutionException;
import com.exonum.binding.core.transaction.TransactionMethod;
Expand Down Expand Up @@ -77,6 +78,21 @@ void invokeThrowingServiceException() {
assertThat(e.getCause().getClass()).isEqualTo(IllegalArgumentException.class);
}

@Test
void invokeProtobufArgumentsService() throws Exception {
ProtobufArgumentsService service = spy(new ProtobufArgumentsService());
TransactionInvoker invoker = new TransactionInvoker(service);
TestProtoMessages.Point point = TestProtoMessages.Point.newBuilder()
.setX(1)
.setY(1)
.build();

invoker.invokeTransaction(ProtobufArgumentsService.TRANSACTION_ID, point.toByteArray(),
context);

verify(service).transactionMethod(point, context);
}

static class BasicService implements Service {

static final int TRANSACTION_ID = 1;
Expand All @@ -97,13 +113,11 @@ public static class ValidService extends BasicService {

@TransactionMethod(TRANSACTION_ID)
@SuppressWarnings("WeakerAccess") // Should be accessible
public void transactionMethod(byte[] arguments, TransactionContext context) {
}
public void transactionMethod(byte[] arguments, TransactionContext context) {}

@TransactionMethod(TRANSACTION_ID_2)
@SuppressWarnings("WeakerAccess") // Should be accessible
public void transactionMethod2(byte[] arguments, TransactionContext context) {
}
public void transactionMethod2(byte[] arguments, TransactionContext context) {}
}

public static class ThrowingService extends BasicService {
Expand All @@ -123,4 +137,11 @@ public void transactionMethod2(byte[] arguments, TransactionContext context)
throw new IllegalArgumentException(ERROR_MESSAGE);
}
}

public static class ProtobufArgumentsService extends BasicService {

@TransactionMethod(TRANSACTION_ID)
@SuppressWarnings("WeakerAccess") // Should be accessible
public void transactionMethod(TestProtoMessages.Point arguments, TransactionContext context) {}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.exonum.binding.core.service.Node;
import com.exonum.binding.core.service.Service;
import com.exonum.binding.core.storage.database.Snapshot;
import com.exonum.binding.core.storage.indices.TestProtoMessages;
import com.exonum.binding.core.transaction.TransactionContext;
import com.exonum.binding.core.transaction.TransactionMethod;
import io.vertx.ext.web.Router;
Expand Down Expand Up @@ -67,8 +68,9 @@ void missingTransactionMethodArgumentsServiceMethodExtraction() {
.extractTransactionMethods(MissingTransactionMethodArgumentsService.class));
String methodName = "transactionMethod";
String errorMessage = String.format("Method %s in a service class %s annotated with"
+ " @TransactionMethod should have precisely two parameters of the following types:"
+ " 'byte[]' and 'com.exonum.binding.core.transaction.TransactionContext'",
+ " @TransactionMethod should have precisely two parameters: transaction arguments of"
+ " 'byte[]' type or a protobuf type and transaction context of"
+ " 'com.exonum.binding.core.transaction.TransactionContext' type",
methodName, MissingTransactionMethodArgumentsService.class.getName());
assertThat(e.getMessage()).contains(errorMessage);
}
Expand All @@ -80,8 +82,9 @@ void invalidTransactionMethodArgumentServiceMethodExtraction() {
.extractTransactionMethods(InvalidTransactionMethodArgumentsService.class));
String methodName = "transactionMethod";
String errorMessage = String.format("Method %s in a service class %s annotated with"
+ " @TransactionMethod should have precisely two parameters of the following types:"
+ " 'byte[]' and 'com.exonum.binding.core.transaction.TransactionContext'"
+ " @TransactionMethod should have precisely two parameters: transaction arguments of"
+ " 'byte[]' type or a protobuf type and transaction context of"
+ " 'com.exonum.binding.core.transaction.TransactionContext' type"
+ ". But second parameter type was: " + String.class.getName(),
methodName, InvalidTransactionMethodArgumentsService.class.getName());
assertThat(e.getMessage()).contains(errorMessage);
Expand All @@ -94,8 +97,9 @@ void duplicateTransactionMethodArgumentServiceMethodExtraction() {
.extractTransactionMethods(DuplicateTransactionMethodArgumentsService.class));
String methodName = "transactionMethod";
String errorMessage = String.format("Method %s in a service class %s annotated with"
+ " @TransactionMethod should have precisely two parameters of the following types:"
+ " 'byte[]' and 'com.exonum.binding.core.transaction.TransactionContext'"
+ " @TransactionMethod should have precisely two parameters: transaction arguments of"
+ " 'byte[]' type or a protobuf type and transaction context of"
+ " 'com.exonum.binding.core.transaction.TransactionContext' type"
+ ". But second parameter type was: " + byte[].class.getName(),
methodName, DuplicateTransactionMethodArgumentsService.class.getName());
assertThat(e.getMessage()).contains(errorMessage);
Expand All @@ -115,6 +119,18 @@ void findMethodsValidServiceInterfaceImplementation() throws Exception {
assertThat(actualMethods).containsExactlyInAnyOrderElementsOf(transactions.values());
}

@Test
void findTransactionMethodsValidServiceProtobufArguments() throws Exception {
Map<Integer, Method> transactions =
TransactionMethodExtractor.findTransactionMethods(ValidServiceProtobufArgument.class);
assertThat(transactions).hasSize(1);
Method transactionMethod =
ValidServiceProtobufArgument.class.getMethod("transactionMethod",
TestProtoMessages.Point.class, TransactionContext.class);
assertThat(transactions.values())
.containsExactlyElementsOf(singletonList(transactionMethod));
}

static class BasicService implements Service {

static final int TRANSACTION_ID = 1;
Expand Down Expand Up @@ -182,4 +198,10 @@ public void transactionMethod(byte[] arguments, TransactionContext context) {}
@SuppressWarnings("WeakerAccess") // Should be accessible
public void transactionMethod2(byte[] arguments, TransactionContext context) {}
}

static class ValidServiceProtobufArgument extends BasicService {

@TransactionMethod(TRANSACTION_ID)
public void transactionMethod(TestProtoMessages.Point arguments, TransactionContext context) {}
}
}