diff --git a/.gitignore b/.gitignore index 6ab3f7a5..7f4b61fc 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ vendor/ .idea # Dependency directories node_modules/ +# Ignore Gradle build output directory +build diff --git a/asset-transfer-basic/chaincode-java/.gitattributes b/asset-transfer-basic/chaincode-java/.gitattributes new file mode 100644 index 00000000..00a51aff --- /dev/null +++ b/asset-transfer-basic/chaincode-java/.gitattributes @@ -0,0 +1,6 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# These are explicitly windows files and should use crlf +*.bat text eol=crlf + diff --git a/asset-transfer-basic/chaincode-java/build.gradle b/asset-transfer-basic/chaincode-java/build.gradle new file mode 100644 index 00000000..5f90c5ac --- /dev/null +++ b/asset-transfer-basic/chaincode-java/build.gradle @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +plugins { + id 'application' + id 'checkstyle' + id 'jacoco' +} + +group 'org.hyperledger.fabric.samples' +version '1.0-SNAPSHOT' + +dependencies { + + compileOnly 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + implementation 'com.owlike:genson:1.5' + testImplementation 'org.hyperledger.fabric-chaincode-java:fabric-chaincode-shim:2.+' + testImplementation 'org.junit.jupiter:junit-jupiter:5.4.2' + testImplementation 'org.assertj:assertj-core:3.11.1' + testImplementation 'org.mockito:mockito-core:2.+' +} + +repositories { + maven { + url "https://hyperledger.jfrog.io/hyperledger/fabric-maven" + } + jcenter() + maven { + url 'https://jitpack.io' + } +} + +application { + mainClass = 'org.hyperledger.fabric.contract.ContractRouter' +} + +checkstyle { + toolVersion '8.21' + configFile file("config/checkstyle/checkstyle.xml") +} + +checkstyleMain { + source ='src/main/java' +} + +checkstyleTest { + source ='src/test/java' +} + +jacocoTestReport { + dependsOn test +} + +jacocoTestCoverageVerification { + violationRules { + rule { + limit { + minimum = 0.9 + } + } + } + + finalizedBy jacocoTestReport +} + +test { + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + } +} + +check.dependsOn jacocoTestCoverageVerification +installDist.dependsOn check diff --git a/asset-transfer-basic/chaincode-java/config/checkstyle/checkstyle.xml b/asset-transfer-basic/chaincode-java/config/checkstyle/checkstyle.xml new file mode 100644 index 00000000..acd5df44 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/config/checkstyle/checkstyle.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/asset-transfer-basic/chaincode-java/config/checkstyle/suppressions.xml b/asset-transfer-basic/chaincode-java/config/checkstyle/suppressions.xml new file mode 100644 index 00000000..8c44b0a0 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/config/checkstyle/suppressions.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.jar b/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..5c2d1cf0 Binary files /dev/null and b/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.properties b/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..bb8b2fc2 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/asset-transfer-basic/chaincode-java/gradlew b/asset-transfer-basic/chaincode-java/gradlew new file mode 100755 index 00000000..83f2acfd --- /dev/null +++ b/asset-transfer-basic/chaincode-java/gradlew @@ -0,0 +1,188 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/asset-transfer-basic/chaincode-java/gradlew.bat b/asset-transfer-basic/chaincode-java/gradlew.bat new file mode 100644 index 00000000..9618d8d9 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/gradlew.bat @@ -0,0 +1,100 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/asset-transfer-basic/chaincode-java/settings.gradle b/asset-transfer-basic/chaincode-java/settings.gradle new file mode 100644 index 00000000..2633c4b9 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/settings.gradle @@ -0,0 +1,5 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +rootProject.name = 'basic' diff --git a/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/Asset.java b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/Asset.java new file mode 100644 index 00000000..803f22fb --- /dev/null +++ b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/Asset.java @@ -0,0 +1,93 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.assettransfer; + +import java.util.Objects; + +import org.hyperledger.fabric.contract.annotation.DataType; +import org.hyperledger.fabric.contract.annotation.Property; + +import com.owlike.genson.annotation.JsonProperty; + +@DataType() +public final class Asset { + + @Property() + private final String assetID; + + @Property() + private final String color; + + @Property() + private final int size; + + @Property() + private final String owner; + + @Property() + private final int appraisedValue; + + public String getAssetID() { + return assetID; + } + + public String getColor() { + return color; + } + + public int getSize() { + return size; + } + + public String getOwner() { + return owner; + } + + public int getAppraisedValue() { + return appraisedValue; + } + + public Asset(@JsonProperty("assetID") final String assetID, @JsonProperty("color") final String color, + @JsonProperty("size") final int size, @JsonProperty("owner") final String owner, + @JsonProperty("appraisedValue") final int appraisedValue) { + this.assetID = assetID; + this.color = color; + this.size = size; + this.owner = owner; + this.appraisedValue = appraisedValue; + } + + @Override + public boolean equals(final Object obj) { + if (this == obj) { + return true; + } + + if ((obj == null) || (getClass() != obj.getClass())) { + return false; + } + + Asset other = (Asset) obj; + + return Objects.deepEquals( + new String[] {getAssetID(), getColor(), getOwner()}, + new String[] {other.getAssetID(), other.getColor(), other.getOwner()}) + && + Objects.deepEquals( + new int[] {getSize(), getAppraisedValue()}, + new int[] {other.getSize(), other.getAppraisedValue()}); + } + + @Override + public int hashCode() { + return Objects.hash(getAssetID(), getColor(), getSize(), getOwner(), getAppraisedValue()); + } + + @Override + public String toString() { + return this.getClass().getSimpleName() + "@" + Integer.toHexString(hashCode()) + " [assetID=" + assetID + ", color=" + + color + ", size=" + size + ", owner=" + owner + ", appraisedValue=" + appraisedValue + "]"; + } +} diff --git a/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/AssetTransfer.java b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/AssetTransfer.java new file mode 100644 index 00000000..465d4694 --- /dev/null +++ b/asset-transfer-basic/chaincode-java/src/main/java/org/hyperledger/fabric/samples/assettransfer/AssetTransfer.java @@ -0,0 +1,236 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.assettransfer; + +import java.util.ArrayList; +import java.util.List; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.contract.ContractInterface; +import org.hyperledger.fabric.contract.annotation.Contact; +import org.hyperledger.fabric.contract.annotation.Contract; +import org.hyperledger.fabric.contract.annotation.Default; +import org.hyperledger.fabric.contract.annotation.Info; +import org.hyperledger.fabric.contract.annotation.License; +import org.hyperledger.fabric.contract.annotation.Transaction; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.hyperledger.fabric.shim.ledger.QueryResultsIterator; + +import com.owlike.genson.Genson; + +@Contract( + name = "basic", + info = @Info( + title = "Asset Transfer", + description = "The hyperlegendary asset transfer", + version = "0.0.1-SNAPSHOT", + license = @License( + name = "Apache 2.0 License", + url = "http://www.apache.org/licenses/LICENSE-2.0.html"), + contact = @Contact( + email = "a.transfer@example.com", + name = "Adrian Transfer", + url = "https://hyperledger.example.com"))) +@Default +public final class AssetTransfer implements ContractInterface { + + private final Genson genson = new Genson(); + + private enum AssetTransferErrors { + ASSET_NOT_FOUND, + ASSET_ALREADY_EXISTS + } + + /** + * Creates some initial assets on the ledger. + * + * @param ctx the transaction context + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void InitLedger(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + + CreateAsset(ctx, "asset1", "blue", 5, "Tomoko", 300); + CreateAsset(ctx, "asset2", "red", 5, "Brad", 400); + CreateAsset(ctx, "asset3", "green", 10, "Jin Soo", 500); + CreateAsset(ctx, "asset4", "yellow", 10, "Max", 600); + CreateAsset(ctx, "asset5", "black", 15, "Adrian", 700); + CreateAsset(ctx, "asset6", "white", 15, "Michel", 700); + + } + + /** + * Creates a new asset on the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the new asset + * @param color the color of the new asset + * @param size the size for the new asset + * @param owner the owner of the new asset + * @param appraisedValue the appraisedValue of the new asset + * @return the created asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset CreateAsset(final Context ctx, final String assetID, final String color, final int size, + final String owner, final int appraisedValue) { + ChaincodeStub stub = ctx.getStub(); + + if (AssetExists(ctx, assetID)) { + String errorMessage = String.format("Asset %s already exists", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_ALREADY_EXISTS.toString()); + } + + Asset asset = new Asset(assetID, color, size, owner, appraisedValue); + String assetJSON = genson.serialize(asset); + stub.putStringState(assetID, assetJSON); + + return asset; + } + + /** + * Retrieves an asset with the specified ID from the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the asset + * @return the asset found on the ledger if there was one + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public Asset ReadAsset(final Context ctx, final String assetID) { + ChaincodeStub stub = ctx.getStub(); + String assetJSON = stub.getStringState(assetID); + + if (assetJSON == null || assetJSON.isEmpty()) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + Asset asset = genson.deserialize(assetJSON, Asset.class); + return asset; + } + + /** + * Updates the properties of an asset on the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the asset being updated + * @param color the color of the asset being updated + * @param size the size of the asset being updated + * @param owner the owner of the asset being updated + * @param appraisedValue the appraisedValue of the asset being updated + * @return the transferred asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset UpdateAsset(final Context ctx, final String assetID, final String color, final int size, + final String owner, final int appraisedValue) { + ChaincodeStub stub = ctx.getStub(); + + if (!AssetExists(ctx, assetID)) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + Asset newAsset = new Asset(assetID, color, size, owner, appraisedValue); + String newAssetJSON = genson.serialize(newAsset); + stub.putStringState(assetID, newAssetJSON); + + return newAsset; + } + + /** + * Deletes asset on the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the asset being deleted + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public void DeleteAsset(final Context ctx, final String assetID) { + ChaincodeStub stub = ctx.getStub(); + + if (!AssetExists(ctx, assetID)) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + stub.delState(assetID); + } + + /** + * Checks the existence of the asset on the ledger + * + * @param ctx the transaction context + * @param assetID the ID of the asset + * @return boolean indicating the existence of the asset + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public boolean AssetExists(final Context ctx, final String assetID) { + ChaincodeStub stub = ctx.getStub(); + String assetJSON = stub.getStringState(assetID); + + return (assetJSON != null && !assetJSON.isEmpty()); + } + + /** + * Changes the owner of a asset on the ledger. + * + * @param ctx the transaction context + * @param assetID the ID of the asset being transferred + * @param newOwner the new owner + * @return the updated asset + */ + @Transaction(intent = Transaction.TYPE.SUBMIT) + public Asset TransferAsset(final Context ctx, final String assetID, final String newOwner) { + ChaincodeStub stub = ctx.getStub(); + String assetJSON = stub.getStringState(assetID); + + if (assetJSON == null || assetJSON.isEmpty()) { + String errorMessage = String.format("Asset %s does not exist", assetID); + System.out.println(errorMessage); + throw new ChaincodeException(errorMessage, AssetTransferErrors.ASSET_NOT_FOUND.toString()); + } + + Asset asset = genson.deserialize(assetJSON, Asset.class); + + Asset newAsset = new Asset(asset.getAssetID(), asset.getColor(), asset.getSize(), newOwner, asset.getAppraisedValue()); + String newAssetJSON = genson.serialize(newAsset); + stub.putStringState(assetID, newAssetJSON); + + return newAsset; + } + + /** + * Retrieves all assets from the ledger. + * + * @param ctx the transaction context + * @return array of assets found on the ledger + */ + @Transaction(intent = Transaction.TYPE.EVALUATE) + public String GetAllAssets(final Context ctx) { + ChaincodeStub stub = ctx.getStub(); + + List queryResults = new ArrayList(); + + // To retrieve all assets from the ledger use getStateByRange with empty startKey & endKey. + // Giving empty startKey & endKey is interpreted as all the keys from beginning to end. + // As another example, if you use startKey = 'asset0', endKey = 'asset9' , + // then getStateByRange will retrieve asset with keys between asset0 (inclusive) and asset9 (exclusive) in lexical order. + QueryResultsIterator results = stub.getStateByRange("", ""); + + for (KeyValue result: results) { + Asset asset = genson.deserialize(result.getStringValue(), Asset.class); + queryResults.add(asset); + System.out.println(asset.toString()); + } + + final String response = genson.serialize(queryResults); + + return response; + } +} diff --git a/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTest.java b/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTest.java new file mode 100644 index 00000000..7da94caa --- /dev/null +++ b/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTest.java @@ -0,0 +1,74 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.assettransfer; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +public final class AssetTest { + + @Nested + class Equality { + + @Test + public void isReflexive() { + Asset asset = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(asset).isEqualTo(asset); + } + + @Test + public void isSymmetric() { + Asset assetA = new Asset("asset1", "Blue", 20, "Guy", 100); + Asset assetB = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(assetA).isEqualTo(assetB); + assertThat(assetB).isEqualTo(assetA); + } + + @Test + public void isTransitive() { + Asset assetA = new Asset("asset1", "Blue", 20, "Guy", 100); + Asset assetB = new Asset("asset1", "Blue", 20, "Guy", 100); + Asset assetC = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(assetA).isEqualTo(assetB); + assertThat(assetB).isEqualTo(assetC); + assertThat(assetA).isEqualTo(assetC); + } + + @Test + public void handlesInequality() { + Asset assetA = new Asset("asset1", "Blue", 20, "Guy", 100); + Asset assetB = new Asset("asset2", "Red", 40, "Lady", 200); + + assertThat(assetA).isNotEqualTo(assetB); + } + + @Test + public void handlesOtherObjects() { + Asset assetA = new Asset("asset1", "Blue", 20, "Guy", 100); + String assetB = "not a asset"; + + assertThat(assetA).isNotEqualTo(assetB); + } + + @Test + public void handlesNull() { + Asset asset = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(asset).isNotEqualTo(null); + } + } + + @Test + public void toStringIdentifiesAsset() { + Asset asset = new Asset("asset1", "Blue", 20, "Guy", 100); + + assertThat(asset.toString()).isEqualTo("Asset@e04f6c53 [assetID=asset1, color=Blue, size=20, owner=Guy, appraisedValue=100]"); + } +} diff --git a/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTransferTest.java b/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTransferTest.java new file mode 100644 index 00000000..cbbb172f --- /dev/null +++ b/asset-transfer-basic/chaincode-java/src/test/java/org/hyperledger/fabric/samples/assettransfer/AssetTransferTest.java @@ -0,0 +1,305 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.hyperledger.fabric.samples.assettransfer; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.ThrowableAssert.catchThrowable; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.hyperledger.fabric.contract.Context; +import org.hyperledger.fabric.shim.ChaincodeException; +import org.hyperledger.fabric.shim.ChaincodeStub; +import org.hyperledger.fabric.shim.ledger.KeyValue; +import org.hyperledger.fabric.shim.ledger.QueryResultsIterator; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.mockito.InOrder; + +public final class AssetTransferTest { + + private final class MockKeyValue implements KeyValue { + + private final String key; + private final String value; + + MockKeyValue(final String key, final String value) { + super(); + this.key = key; + this.value = value; + } + + @Override + public String getKey() { + return this.key; + } + + @Override + public String getStringValue() { + return this.value; + } + + @Override + public byte[] getValue() { + return this.value.getBytes(); + } + + } + + private final class MockAssetResultsIterator implements QueryResultsIterator { + + private final List assetList; + + MockAssetResultsIterator() { + super(); + + assetList = new ArrayList(); + + assetList.add(new MockKeyValue("asset1", + "{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"Tomoko\", \"appraisedValue\": 300 }")); + assetList.add(new MockKeyValue("asset2", + "{ \"assetID\": \"asset2\", \"color\": \"red\", \"size\": 5,\"owner\": \"Brad\", \"appraisedValue\": 400 }")); + assetList.add(new MockKeyValue("asset3", + "{ \"assetID\": \"asset3\", \"color\": \"green\", \"size\": 10,\"owner\": \"Jin Soo\", \"appraisedValue\": 500 }")); + assetList.add(new MockKeyValue("asset4", + "{ \"assetID\": \"asset4\", \"color\": \"yellow\", \"size\": 10,\"owner\": \"Max\", \"appraisedValue\": 600 }")); + assetList.add(new MockKeyValue("asset5", + "{ \"assetID\": \"asset5\", \"color\": \"black\", \"size\": 15,\"owner\": \"Adrian\", \"appraisedValue\": 700 }")); + assetList.add(new MockKeyValue("asset6", + "{ \"assetID\": \"asset6\", \"color\": \"white\", \"size\": 15,\"owner\": \"Michel\", \"appraisedValue\": 800 }")); + } + + @Override + public Iterator iterator() { + return assetList.iterator(); + } + + @Override + public void close() throws Exception { + // do nothing + } + + } + + @Test + public void invokeUnknownTransaction() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + + Throwable thrown = catchThrowable(() -> { + contract.unknownTransaction(ctx); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Undefined contract method called"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo(null); + + verifyZeroInteractions(ctx); + } + + @Nested + class InvokeReadAssetTransaction { + + @Test + public void whenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")) + .thenReturn("{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"Tomoko\", \"appraisedValue\": 300 }"); + + Asset asset = contract.ReadAsset(ctx, "asset1"); + + assertThat(asset).isEqualTo(new Asset("asset1", "blue", 5, "Tomoko", 300)); + } + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.ReadAsset(ctx, "asset1"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_NOT_FOUND".getBytes()); + } + } + + @Test + void invokeInitLedgerTransaction() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + + contract.InitLedger(ctx); + + InOrder inOrder = inOrder(stub); + inOrder.verify(stub).putStringState("asset1", "{\"appraisedValue\":300,\"assetID\":\"asset1\",\"color\":\"blue\",\"owner\":\"Tomoko\",\"size\":5}"); + inOrder.verify(stub).putStringState("asset2", "{\"appraisedValue\":400,\"assetID\":\"asset2\",\"color\":\"red\",\"owner\":\"Brad\",\"size\":5}"); + inOrder.verify(stub).putStringState("asset3", "{\"appraisedValue\":500,\"assetID\":\"asset3\",\"color\":\"green\",\"owner\":\"Jin Soo\",\"size\":10}"); + inOrder.verify(stub).putStringState("asset4", "{\"appraisedValue\":600,\"assetID\":\"asset4\",\"color\":\"yellow\",\"owner\":\"Max\",\"size\":10}"); + inOrder.verify(stub).putStringState("asset5", "{\"appraisedValue\":700,\"assetID\":\"asset5\",\"color\":\"black\",\"owner\":\"Adrian\",\"size\":15}"); + + } + + @Nested + class InvokeCreateAssetTransaction { + + @Test + public void whenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")) + .thenReturn("{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"Tomoko\", \"appraisedValue\": 300 }"); + + Throwable thrown = catchThrowable(() -> { + contract.CreateAsset(ctx, "asset1", "blue", 45, "Siobhán", 60); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 already exists"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_ALREADY_EXISTS".getBytes()); + } + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Asset asset = contract.CreateAsset(ctx, "asset1", "blue", 45, "Siobhán", 60); + + assertThat(asset).isEqualTo(new Asset("asset1", "blue", 45, "Siobhán", 60)); + } + } + + @Test + void invokeGetAllAssetsTransaction() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStateByRange("", "")).thenReturn(new MockAssetResultsIterator()); + + String assets = contract.GetAllAssets(ctx); + + assertThat(assets).isEqualTo("[{\"appraisedValue\":300,\"assetID\":\"asset1\",\"color\":\"blue\",\"owner\":\"Tomoko\",\"size\":5}," + + "{\"appraisedValue\":400,\"assetID\":\"asset2\",\"color\":\"red\",\"owner\":\"Brad\",\"size\":5}," + + "{\"appraisedValue\":500,\"assetID\":\"asset3\",\"color\":\"green\",\"owner\":\"Jin Soo\",\"size\":10}," + + "{\"appraisedValue\":600,\"assetID\":\"asset4\",\"color\":\"yellow\",\"owner\":\"Max\",\"size\":10}," + + "{\"appraisedValue\":700,\"assetID\":\"asset5\",\"color\":\"black\",\"owner\":\"Adrian\",\"size\":15}," + + "{\"appraisedValue\":800,\"assetID\":\"asset6\",\"color\":\"white\",\"owner\":\"Michel\",\"size\":15}]"); + + } + + @Nested + class TransferAssetTransaction { + + @Test + public void whenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")) + .thenReturn("{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 5, \"owner\": \"Tomoko\", \"appraisedValue\": 300 }"); + + Asset asset = contract.TransferAsset(ctx, "asset1", "Dr Evil"); + + assertThat(asset).isEqualTo(new Asset("asset1", "blue", 5, "Dr Evil", 300)); + } + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.TransferAsset(ctx, "asset1", "Dr Evil"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_NOT_FOUND".getBytes()); + } + } + + @Nested + class UpdateAssetTransaction { + + @Test + public void whenAssetExists() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")) + .thenReturn("{ \"assetID\": \"asset1\", \"color\": \"blue\", \"size\": 45, \"owner\": \"Arturo\", \"appraisedValue\": 60 }"); + + Asset asset = contract.UpdateAsset(ctx, "asset1", "pink", 45, "Arturo", 600); + + assertThat(asset).isEqualTo(new Asset("asset1", "pink", 45, "Arturo", 600)); + } + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.TransferAsset(ctx, "asset1", "Alex"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_NOT_FOUND".getBytes()); + } + } + + @Nested + class DeleteAssetTransaction { + + @Test + public void whenAssetDoesNotExist() { + AssetTransfer contract = new AssetTransfer(); + Context ctx = mock(Context.class); + ChaincodeStub stub = mock(ChaincodeStub.class); + when(ctx.getStub()).thenReturn(stub); + when(stub.getStringState("asset1")).thenReturn(""); + + Throwable thrown = catchThrowable(() -> { + contract.DeleteAsset(ctx, "asset1"); + }); + + assertThat(thrown).isInstanceOf(ChaincodeException.class).hasNoCause() + .hasMessage("Asset asset1 does not exist"); + assertThat(((ChaincodeException) thrown).getPayload()).isEqualTo("ASSET_NOT_FOUND".getBytes()); + } + } +} diff --git a/ci/azure-pipelines.yml b/ci/azure-pipelines.yml index 9875d0da..759d36a3 100644 --- a/ci/azure-pipelines.yml +++ b/ci/azure-pipelines.yml @@ -84,6 +84,10 @@ jobs: DIRECTORY: asset-transfer-basic LANGUAGE: go TYPE: chaincode + Basic-Chaincode-Java: + DIRECTORY: asset-transfer-basic + LANGUAGE: java + TYPE: chaincode Basic-Chaincode-Javascript: DIRECTORY: asset-transfer-basic LANGUAGE: javascript @@ -126,6 +130,9 @@ jobs: Basic-Go: CHAINCODE_NAME: basic CHAINCODE_LANGUAGE: go + Basic-Java: + CHAINCODE_NAME: basic + CHAINCODE_LANGUAGE: java Basic-Javascript: CHAINCODE_NAME: basic CHAINCODE_LANGUAGE: javascript diff --git a/ci/scripts/lint.sh b/ci/scripts/lint.sh index 9e8fd4e3..275b4d62 100755 --- a/ci/scripts/lint.sh +++ b/ci/scripts/lint.sh @@ -27,6 +27,10 @@ if [[ "${LANGUAGE}" == "go" ]]; then print "The following files contain import errors, please run 'goimports -l -w ' to fix these issues:" echo "${output}" fi +elif [[ "${LANGUAGE}" == "java" ]]; then + cd "${DIRECTORY}/${TYPE}-${LANGUAGE}" + print "Running Gradle Build" + ./gradlew build elif [[ "${LANGUAGE}" == "javascript" ]]; then npm install -g eslint cd "${DIRECTORY}/${TYPE}-${LANGUAGE}"