diff --git a/exonum-java-binding/CHANGELOG.md b/exonum-java-binding/CHANGELOG.md index 02cbc7a54f..6328d8db1c 100644 --- a/exonum-java-binding/CHANGELOG.md +++ b/exonum-java-binding/CHANGELOG.md @@ -33,6 +33,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. `ListProof`; - [`Blockchain`][blockchain-proofs]. - `ProofEntryIndexProxy` collection. +- Transaction precondition utility methods, + see `com.exonum.binding.core.transaction.ExecutionPreconditions`.(#1351) - `supervisor-mode` CLI parameter added for `generate-template` command. It allows to configure the mode of the Supervisor service. Possible values are "simple" and "decentralized". (#1361) diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/transaction/ExecutionException.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/transaction/ExecutionException.java index fb8c072388..a9f8de199c 100644 --- a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/transaction/ExecutionException.java +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/transaction/ExecutionException.java @@ -53,6 +53,7 @@ * @see Blockchain#getTxResult(HashCode) * @see Blockchain#getCallErrors(long) * @see ExecutionStatus + * @see ExecutionPreconditions */ public class ExecutionException extends RuntimeException { diff --git a/exonum-java-binding/core/src/main/java/com/exonum/binding/core/transaction/ExecutionPreconditions.java b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/transaction/ExecutionPreconditions.java new file mode 100644 index 0000000000..ea284cbec3 --- /dev/null +++ b/exonum-java-binding/core/src/main/java/com/exonum/binding/core/transaction/ExecutionPreconditions.java @@ -0,0 +1,271 @@ +/* + * Copyright 2020 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.transaction; + +import static com.google.common.base.Strings.lenientFormat; + +import org.checkerframework.checker.nullness.qual.Nullable; + +/** + * Utility methods that helps verifying conditions conducted in expression + * while transaction execution. + * If the condition is not met, the {@code ExecutionPreconditions} method + * throws {@link ExecutionException}. + * + *

Consider the following example: + *

{@code
+ *   void checkEnoughMoney(long balance, long amount) {
+ *     if(balance < amount) {
+ *       throw new ExecutionException((byte)3, "Not enough money. Operation amount is " + amount
+ *       + ", but actual balance was " + balance);
+ *     }
+ *   }
+ * }
+ * + *

which can be replaced using ExecutionPreconditions: + *

{@code
+ *   checkExecution(amount <= balance, (byte)3,
+ *       "Not enough money. Operation amount is %s, but actual balance was %s",
+ *       amount, balance);
+ * }
+ * + * @see ExecutionException + */ +public final class ExecutionPreconditions { + + /** + * Verifies the truth of the given expression. + * + * @param expression a boolean expression + * @param errorCode execution error code + * @throws ExecutionException if {@code expression} is false + */ + public static void checkExecution(boolean expression, byte errorCode) { + if (!expression) { + throw new ExecutionException(errorCode); + } + } + + /** + * Verifies the truth of the given expression. + * + * @param expression a boolean expression + * @param errorCode execution error code + * @param errorMessage execution error description to use if the check fails + * @throws ExecutionException if {@code expression} is false + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable Object errorMessage) { + if (!expression) { + throw new ExecutionException(errorCode, String.valueOf(errorMessage)); + } + } + + /** + * Verifies the truth of the given expression. + * + * @param expression a boolean expression + * @param errorCode execution error code + * @param errorMessageTemplate execution error description template to use if the check fails. + * The template could have placeholders {@code %s} which will be replaced by arguments + * resolved by position + * @param errorMessageArgs arguments to be used in the template. Each argument will be converted + * to string using {@link String#valueOf(Object)} + * @throws ExecutionException if {@code expression} is false + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + @Nullable Object... errorMessageArgs) { + if (!expression) { + throw new ExecutionException(errorCode, + lenientFormat(errorMessageTemplate, errorMessageArgs)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + int arg1) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + int arg1, int arg2) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1, arg2)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + int arg1, long arg2) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1, arg2)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + int arg1, @Nullable Object arg2) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1, arg2)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + long arg1) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + long arg1, int arg2) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1, arg2)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + long arg1, long arg2) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1, arg2)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + long arg1, @Nullable Object arg2) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1, arg2)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + @Nullable Object arg1) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + @Nullable Object arg1, int arg2) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1, arg2)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + @Nullable Object arg1, long arg2) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1, arg2)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + @Nullable Object arg1, @Nullable Object arg2) { + if (!expression) { + throw new ExecutionException(errorCode, lenientFormat(errorMessageTemplate, arg1, arg2)); + } + } + + /** + * Verifies the truth of the given expression. + * + *

See {@link #checkExecution(boolean, byte, String, Object...)} for details. + */ + public static void checkExecution(boolean expression, byte errorCode, + @Nullable String errorMessageTemplate, + @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) { + if (!expression) { + throw new ExecutionException(errorCode, + lenientFormat(errorMessageTemplate, arg1, arg2, arg3)); + } + } + + private ExecutionPreconditions() { + } +} diff --git a/exonum-java-binding/core/src/test/java/com/exonum/binding/core/transaction/ExecutionPreconditionsTest.java b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/transaction/ExecutionPreconditionsTest.java new file mode 100644 index 0000000000..df51e9660e --- /dev/null +++ b/exonum-java-binding/core/src/test/java/com/exonum/binding/core/transaction/ExecutionPreconditionsTest.java @@ -0,0 +1,68 @@ +/* + * Copyright 2020 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.transaction; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +class ExecutionPreconditionsTest { + private static final byte TEST_ERROR_CODE = 1; + + @Test + void trueConditionDoesNothing() { + ExecutionPreconditions.checkExecution(true, TEST_ERROR_CODE); + } + + @Test + void errorCodeIsPresent() { + ExecutionException e = assertThrows(ExecutionException.class, + () -> ExecutionPreconditions.checkExecution(false, TEST_ERROR_CODE)); + + assertThat(e.getErrorCode()).isEqualTo(TEST_ERROR_CODE); + } + + @Test + void errorDescriptionIsPresent() { + String description = "evil error"; + ExecutionException e = assertThrows(ExecutionException.class, + () -> ExecutionPreconditions.checkExecution(false, TEST_ERROR_CODE, description)); + + assertThat(e.getErrorCode()).isEqualTo(TEST_ERROR_CODE); + assertThat(e).hasMessage(description); + } + + @Test + void nullableDescription() { + ExecutionException e = assertThrows(ExecutionException.class, + () -> ExecutionPreconditions.checkExecution(false, TEST_ERROR_CODE, null)); + + assertThat(e).hasMessage("null"); + } + + @Test + void errorDescriptionFormat() { + int p1 = 10; + int p2 = 20; + + ExecutionException e = assertThrows(ExecutionException.class, + () -> ExecutionPreconditions.checkExecution(p1 == p2, TEST_ERROR_CODE, "%s != %s", p1, p2)); + + assertThat(e).hasMessage("10 != 20"); + } +} diff --git a/exonum-java-binding/cryptocurrency-demo/src/main/java/com/exonum/binding/cryptocurrency/CryptocurrencyServiceImpl.java b/exonum-java-binding/cryptocurrency-demo/src/main/java/com/exonum/binding/cryptocurrency/CryptocurrencyServiceImpl.java index 113c110bcb..2bc96c3a7c 100644 --- a/exonum-java-binding/cryptocurrency-demo/src/main/java/com/exonum/binding/cryptocurrency/CryptocurrencyServiceImpl.java +++ b/exonum-java-binding/cryptocurrency-demo/src/main/java/com/exonum/binding/cryptocurrency/CryptocurrencyServiceImpl.java @@ -16,6 +16,7 @@ package com.exonum.binding.cryptocurrency; +import static com.exonum.binding.core.transaction.ExecutionPreconditions.checkExecution; import static com.exonum.binding.cryptocurrency.TransactionError.INSUFFICIENT_FUNDS; import static com.exonum.binding.cryptocurrency.TransactionError.NON_POSITIVE_TRANSFER_AMOUNT; import static com.exonum.binding.cryptocurrency.TransactionError.SAME_SENDER_AND_RECEIVER; @@ -37,7 +38,6 @@ import com.exonum.binding.core.storage.indices.ListIndex; import com.exonum.binding.core.storage.indices.MapIndex; import com.exonum.binding.core.storage.indices.ProofMapIndexProxy; -import com.exonum.binding.core.transaction.ExecutionException; import com.exonum.binding.core.transaction.Transaction; import com.exonum.binding.core.transaction.TransactionContext; import com.exonum.binding.cryptocurrency.transactions.TxMessageProtos; @@ -56,7 +56,8 @@ public final class CryptocurrencyServiceImpl extends AbstractService public static final int CREATE_WALLET_TX_ID = 1; public static final int TRANSFER_TX_ID = 2; - @Nullable private Node node; + @Nullable + private Node node; @Inject public CryptocurrencyServiceImpl(ServiceInstanceSpec instanceSpec) { @@ -161,20 +162,6 @@ private static PublicKey toPublicKey(ByteString s) { return PublicKey.fromBytes(s.toByteArray()); } - // todo: consider extracting in a TransactionPreconditions or - // ExecutionException, with proper lazy formatting: ECR-2746. - /** Checks a transaction execution precondition, throwing if it is false. */ - private static void checkExecution(boolean precondition, byte errorCode) { - checkExecution(precondition, errorCode, null); - } - - private static void checkExecution(boolean precondition, byte errorCode, - @Nullable String message) { - if (!precondition) { - throw new ExecutionException(errorCode, message); - } - } - private HistoryEntity createTransferHistoryEntry(TransactionMessage txMessage) { try { TxMessageProtos.TransferTx txBody = TxMessageProtos.TransferTx diff --git a/exonum-java-binding/qa-service/src/main/java/com/exonum/binding/qaservice/QaServiceImpl.java b/exonum-java-binding/qa-service/src/main/java/com/exonum/binding/qaservice/QaServiceImpl.java index f65d819a2d..3acb1bb3ae 100644 --- a/exonum-java-binding/qa-service/src/main/java/com/exonum/binding/qaservice/QaServiceImpl.java +++ b/exonum-java-binding/qa-service/src/main/java/com/exonum/binding/qaservice/QaServiceImpl.java @@ -16,6 +16,7 @@ package com.exonum.binding.qaservice; +import static com.exonum.binding.core.transaction.ExecutionPreconditions.checkExecution; import static com.exonum.binding.qaservice.QaExecutionError.COUNTER_ALREADY_EXISTS; import static com.exonum.binding.qaservice.QaExecutionError.EMPTY_TIME_ORACLE_NAME; import static com.exonum.binding.qaservice.QaExecutionError.UNKNOWN_COUNTER; @@ -286,10 +287,8 @@ private void createCounter(String counterName, Fork fork) { HashCode counterId = Hashing.defaultHashFunction() .hashString(counterName, UTF_8); - if (counters.containsKey(counterId)) { - throw new ExecutionException(COUNTER_ALREADY_EXISTS.code, - format("Counter %s already exists", counterName)); - } + checkExecution(!counters.containsKey(counterId), + COUNTER_ALREADY_EXISTS.code, "Counter %s already exists", counterName); assert !names.containsKey(counterId) : "counterNames must not contain the id of " + counterName; counters.put(counterId, 0L); @@ -307,9 +306,8 @@ public void incrementCounter(TxMessageProtos.IncrementCounterTxBody arguments, ProofMapIndexProxy counters = schema.counters(); // Increment the counter if there is such. - if (!counters.containsKey(counterId)) { - throw new ExecutionException(UNKNOWN_COUNTER.code); - } + checkExecution(counters.containsKey(counterId), UNKNOWN_COUNTER.code); + long newValue = counters.get(counterId) + 1; counters.put(counterId, newValue); } @@ -348,10 +346,8 @@ private void checkConfiguration(QaConfiguration config) { // We do *not* check if the time oracle is active to (a) allow running this service with // reduced read functionality without time oracle; (b) testing time schema when it is not // active. - if (Strings.isNullOrEmpty(timeOracleName)) { - throw new ExecutionException(EMPTY_TIME_ORACLE_NAME.code, - format("Empty time oracle name: %s", timeOracleName)); - } + checkExecution(!Strings.isNullOrEmpty(timeOracleName), EMPTY_TIME_ORACLE_NAME.code, + "Empty time oracle name: %s", timeOracleName); } private void updateTimeOracle(Fork fork, Configuration configuration) {