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