github.com/muhammedhassanm/blockchain@v0.0.0-20200120143007-697261defd4d/sawtooth-core-master/sdk/examples/xo_java/XoHandler.java (about)

     1  /* Copyright 2017 Intel Corporation
     2   Licensed under the Apache License, Version 2.0 (the "License");
     3   you may not use this file except in compliance with the License.
     4   You may obtain a copy of the License at
     5  
     6       http://www.apache.org/licenses/LICENSE-2.0
     7  
     8   Unless required by applicable law or agreed to in writing, software
     9   distributed under the License is distributed on an "AS IS" BASIS,
    10   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11   See the License for the specific language governing permissions and
    12   limitations under the License.
    13  ------------------------------------------------------------------------------*/
    14  
    15  package sawtooth.examples.xo;
    16  
    17  
    18  import com.google.protobuf.ByteString;
    19  import com.google.protobuf.InvalidProtocolBufferException;
    20  
    21  import org.apache.commons.lang3.StringUtils;
    22  
    23  import sawtooth.sdk.processor.State;
    24  import sawtooth.sdk.processor.TransactionHandler;
    25  import sawtooth.sdk.processor.Utils;
    26  import sawtooth.sdk.processor.exceptions.InternalError;
    27  import sawtooth.sdk.processor.exceptions.InvalidTransactionException;
    28  import sawtooth.sdk.protobuf.TpProcessRequest;
    29  import sawtooth.sdk.protobuf.TransactionHeader;
    30  
    31  import java.io.UnsupportedEncodingException;
    32  import java.util.AbstractMap;
    33  import java.util.ArrayList;
    34  import java.util.Arrays;
    35  import java.util.Collection;
    36  import java.util.Collections;
    37  import java.util.Map;
    38  import java.util.logging.Logger;
    39  
    40  public class XoHandler implements TransactionHandler {
    41  
    42    private final Logger logger = Logger.getLogger(XoHandler.class.getName());
    43    private String xoNameSpace;
    44  
    45    /**
    46     * constructor.
    47     */
    48    public XoHandler() {
    49      try {
    50        this.xoNameSpace = Utils.hash512(
    51          this.transactionFamilyName().getBytes("UTF-8")).substring(0, 6);
    52      } catch (UnsupportedEncodingException usee) {
    53        usee.printStackTrace();
    54        this.xoNameSpace = "";
    55      }
    56    }
    57  
    58    @Override
    59    public String transactionFamilyName() {
    60      return "xo";
    61    }
    62  
    63    @Override
    64    public String getVersion() {
    65      return "1.0";
    66    }
    67  
    68    @Override
    69    public Collection<String> getNameSpaces() {
    70      ArrayList<String> namespaces = new ArrayList<>();
    71      namespaces.add(this.xoNameSpace);
    72      return namespaces;
    73    }
    74  
    75    class TransactionData {
    76      final String gameName;
    77      final String action;
    78      final String space;
    79  
    80      TransactionData(String gameName, String action, String space) {
    81        this.gameName = gameName;
    82        this.action = action;
    83        this.space = space;
    84      }
    85    }
    86  
    87    class GameData {
    88      final String gameName;
    89      final String board;
    90      final String state;
    91      final String playerOne;
    92      final String playerTwo;
    93  
    94      GameData(String gameName, String board, String state, String playerOne, String playerTwo) {
    95        this.gameName = gameName;
    96        this.board = board;
    97        this.state = state;
    98        this.playerOne = playerOne;
    99        this.playerTwo = playerTwo;
   100      }
   101    }
   102  
   103    @Override
   104    public void apply(TpProcessRequest transactionRequest, State stateStore)
   105        throws InvalidTransactionException, InternalError {
   106      TransactionData transactionData = getUnpackedTransaction(transactionRequest);
   107  
   108      // The transaction signer is the player
   109      String player;
   110      TransactionHeader header = transactionRequest.getHeader();
   111      player = header.getSignerPublicKey();
   112  
   113      if (transactionData.gameName.equals("")) {
   114        throw new InvalidTransactionException("Name is required");
   115      }
   116      if (transactionData.gameName.contains("|")) {
   117        throw new InvalidTransactionException("Game name cannot contain '|'");
   118      }
   119      if (transactionData.action.equals("")) {
   120        throw new InvalidTransactionException("Action is required");
   121      }
   122      if (transactionData.action.equals("take")) {
   123        try {
   124          int space = Integer.parseInt(transactionData.space);
   125  
   126          if (space < 1 || space > 9) {
   127            throw new InvalidTransactionException(
   128                String.format("Invalid space: %s", transactionData.space));
   129          }
   130        } catch (NumberFormatException e) {
   131          throw new InvalidTransactionException("Space could not be converted to an integer.");
   132        }
   133      }
   134      if (!transactionData.action.equals("take") && !transactionData.action.equals("create")) {
   135        throw new InvalidTransactionException(
   136            String.format("Invalid action: %s", transactionData.action));
   137      }
   138  
   139      String address = makeGameAddress(transactionData.gameName);
   140      // stateStore.get() returns a list.
   141      // If no data has been stored yet at the given address, it will be empty.
   142      String stateEntry = stateStore.getState(
   143          Collections.singletonList(address)
   144      ).get(address).toStringUtf8();
   145      GameData stateData = getStateData(stateEntry, transactionData.gameName);
   146      GameData updatedGameData = playXo(transactionData, stateData, player);
   147      storeGameData(address, updatedGameData, stateEntry, stateStore);
   148    }
   149  
   150    /**
   151     * Helper function to retrieve game gameName, action, and space from transaction request.
   152     */
   153    private TransactionData getUnpackedTransaction(TpProcessRequest transactionRequest)
   154        throws InvalidTransactionException {
   155      String payload =  transactionRequest.getPayload().toStringUtf8();
   156      ArrayList<String> payloadList = new ArrayList<>(Arrays.asList(payload.split(",")));
   157      if (payloadList.size() > 3) {
   158        throw new InvalidTransactionException("Invalid payload serialization");
   159      }
   160      while (payloadList.size() < 3) {
   161        payloadList.add("");
   162      }
   163      return new TransactionData(payloadList.get(0), payloadList.get(1), payloadList.get(2));
   164    }
   165  
   166    /**
   167     * Helper function to retrieve the board, state, playerOne, and playerTwo from state store.
   168     */
   169    private GameData getStateData(String stateEntry, String gameName)
   170        throws InternalError, InvalidTransactionException {
   171      if (stateEntry.length() == 0) {
   172        return new GameData("", "", "", "", "");
   173      } else {
   174        try {
   175          String gameCsv = getGameCsv(stateEntry, gameName);
   176          ArrayList<String> gameList = new ArrayList<>(Arrays.asList(gameCsv.split(",")));
   177          while (gameList.size() < 5) {
   178            gameList.add("");
   179          }
   180          return new GameData(gameList.get(0), gameList.get(1),
   181              gameList.get(2), gameList.get(3), gameList.get(4));
   182        } catch (Error e) {
   183          throw new InternalError("Failed to deserialize game data");
   184        }
   185      }
   186    }
   187  
   188    /**
   189     * Helper function to generate game address.
   190     */
   191    private String makeGameAddress(String gameName) throws InternalError {
   192      try {
   193        String hashedName = Utils.hash512(gameName.getBytes("UTF-8"));
   194        return xoNameSpace + hashedName.substring(0, 64);
   195      } catch (UnsupportedEncodingException e) {
   196        throw new InternalError("Internal Error: " + e.toString());
   197      }
   198    }
   199  
   200    /**
   201     * Helper function to retrieve the correct game info from the list of game data CSV.
   202     */
   203    private String getGameCsv(String stateEntry, String gameName) {
   204      ArrayList<String> gameCsvList = new ArrayList<>(Arrays.asList(stateEntry.split("\\|")));
   205      for (String gameCsv : gameCsvList) {
   206        if (gameCsv.regionMatches(0, gameName, 0, gameName.length())) {
   207          return gameCsv;
   208        }
   209      }
   210      return "";
   211    }
   212  
   213    /**
   214     * Helper function to store state data.
   215     */
   216    private void storeGameData(String address, GameData gameData, String stateEntry, State stateStore)
   217        throws InternalError, InvalidTransactionException {
   218      String gameDataCsv = String.format("%s,%s,%s,%s,%s",
   219          gameData.gameName, gameData.board, gameData.state, gameData.playerOne, gameData.playerTwo);
   220      if (stateEntry.length() == 0) {
   221        stateEntry = gameDataCsv;
   222      } else {
   223        ArrayList<String> dataList = new ArrayList<>(Arrays.asList(stateEntry.split("\\|")));
   224        for (int i = 0; i <= dataList.size(); i++) {
   225          if (i == dataList.size()
   226              || dataList.get(i).regionMatches(0, gameData.gameName, 0, gameData.gameName.length())) {
   227            dataList.set(i, gameDataCsv);
   228            break;
   229          }
   230        }
   231        stateEntry = StringUtils.join(dataList, "|");
   232      }
   233  
   234      ByteString csvByteString = ByteString.copyFromUtf8(stateEntry);
   235      Map.Entry<String, ByteString> entry = new AbstractMap.SimpleEntry<>(address, csvByteString);
   236      Collection<Map.Entry<String, ByteString>> addressValues = Collections.singletonList(entry);
   237      Collection<String> addresses = stateStore.setState(addressValues);
   238      if (addresses.size() < 1) {
   239        throw new InternalError("State Error");
   240      }
   241    }
   242  
   243    /**
   244     * Function that handles game logic.
   245     */
   246    private GameData playXo(TransactionData transactionData, GameData gameData, String player)
   247        throws InvalidTransactionException, InternalError {
   248      switch (transactionData.action) {
   249        case "create":
   250          return applyCreate(transactionData, gameData, player);
   251        case "take":
   252          return applyTake(transactionData, gameData, player);
   253        default:
   254          throw new InvalidTransactionException(String.format(
   255              "Invalid action: %s", transactionData.action));
   256      }
   257    }
   258  
   259    /**
   260     * Function that handles game logic for 'create' action.
   261     */
   262    private GameData applyCreate(TransactionData transactionData, GameData gameData, String player)
   263        throws InvalidTransactionException {
   264      if (!gameData.board.equals("")) {
   265        throw new InvalidTransactionException("Invalid Action: Game already exists");
   266      }
   267      display(String.format("Player %s created a game", abbreviate(player)));
   268      return new GameData(transactionData.gameName, "---------", "P1-NEXT", "", "");
   269    }
   270  
   271    /**
   272     * Function that handles game logic for 'take' action.
   273     */
   274    private GameData applyTake(TransactionData transactionData, GameData gameData, String player)
   275        throws InvalidTransactionException, InternalError {
   276      if (Arrays.asList("P1-WIN", "P2-WIN", "TIE").contains(gameData.state)) {
   277        throw new InvalidTransactionException("Invalid action: Game has ended");
   278      }
   279      if (gameData.board.equals("")) {
   280        throw new InvalidTransactionException("Invalid action: 'take' requires an existing game");
   281      }
   282      if (!Arrays.asList("P1-NEXT", "P2-NEXT").contains(gameData.state)) {
   283        throw new InternalError(String.format(
   284            "Internal Error: Game has reached an invalid state: %s", gameData.state));
   285      }
   286  
   287      // Assign players if new game
   288      String updatedPlayerOne = gameData.playerOne;
   289      String updatedPlayerTwo = gameData.playerTwo;
   290      if (gameData.playerOne.equals("")) {
   291        updatedPlayerOne = player;
   292      } else if (gameData.playerTwo.equals("")) {
   293        updatedPlayerTwo = player;
   294      }
   295  
   296      // Verify player identity and take space
   297      int space = Integer.parseInt(transactionData.space);
   298      char[] boardList = gameData.board.toCharArray();
   299      String updatedState;
   300      if (boardList[space - 1] != '-') {
   301        throw new InvalidTransactionException("Space already taken");
   302      }
   303  
   304      if (gameData.state.equals("P1-NEXT") && player.equals(updatedPlayerOne)) {
   305        boardList[space - 1] = 'X';
   306        updatedState = "P2-NEXT";
   307      } else if (gameData.state.equals("P2-NEXT") && player.equals(updatedPlayerTwo)) {
   308        boardList[space - 1] = 'O';
   309        updatedState = "P1-NEXT";
   310      } else {
   311        throw new InvalidTransactionException(String.format(
   312            "Not this player's turn: %s", abbreviate(player)));
   313      }
   314  
   315      String updatedBoard = String.valueOf(boardList);
   316      updatedState = determineState(boardList, updatedState);
   317      GameData updatedGameData = new GameData(
   318          gameData.gameName, updatedBoard, updatedState, updatedPlayerOne, updatedPlayerTwo);
   319  
   320      display(
   321          String.format("Player %s takes space %d \n", abbreviate(player), space)
   322              + gameDataToString(updatedGameData));
   323      return updatedGameData;
   324    }
   325  
   326    /**
   327     * Helper function that updates game state based on the current board position.
   328     */
   329    private String determineState(char[] boardList, String state) {
   330      if (isWin(boardList, 'X')) {
   331        state = "P1-WIN";
   332      } else if (isWin(boardList, 'O')) {
   333        state = "P2-WIN";
   334      } else if (!(String.valueOf(boardList).contains("-"))) {
   335        state = "TIE";
   336      }
   337      return state;
   338    }
   339  
   340    /**
   341     * Helper function that analyzes board position to determine if it is in a winning state.
   342     */
   343    private boolean isWin(char[] board, char letter) {
   344      int[][] wins = new int[][]{
   345          {1, 2, 3}, {4, 5, 6}, {7, 8, 9},
   346          {1, 4, 7}, {2, 5, 8}, {3, 6, 9},
   347          {1, 5, 9}, {3, 5, 7},
   348      };
   349  
   350      for (int[] win : wins) {
   351        if (board[win[0] - 1] == letter
   352            && board[win[1] - 1] == letter
   353            && board[win[2] - 1] == letter) {
   354          return true;
   355        }
   356      }
   357      return false;
   358    }
   359  
   360    /**
   361     * Helper function to create an ASCII representation of the board.
   362     */
   363    private String gameDataToString(GameData gameData) {
   364      String out = "";
   365      out += String.format("GAME: %s\n", gameData.gameName);
   366      out += String.format("PLAYER 1: %s\n", abbreviate(gameData.playerOne));
   367      out += String.format("PLAYER 2: %s\n", abbreviate(gameData.playerTwo));
   368      out += String.format("STATE: %s\n", gameData.state);
   369      out += "\n";
   370  
   371      char[] board = gameData.board.replace('-',' ').toCharArray();
   372      out += String.format("%c | %c |  %c\n", board[0], board[1], board[2]);
   373      out += "---|---|---\n";
   374      out += String.format("%c | %c |  %c\n", board[3], board[4], board[5]);
   375      out += "---|---|---\n";
   376      out += String.format("%c | %c |  %c\n", board[6], board[7], board[8]);
   377      return out;
   378    }
   379  
   380    /**
   381     * Helper function to print game data to the logger.
   382     */
   383    private void display(String msg) {
   384      String displayMsg = "";
   385      int length = 0;
   386      String[] msgLines = msg.split("\n");
   387      if (msg.contains("\n")) {
   388        for (String line : msgLines) {
   389          if (line.length() > length) {
   390            length = line.length();
   391          }
   392        }
   393      } else {
   394        length = msg.length();
   395      }
   396  
   397      displayMsg = displayMsg.concat("\n+" + printDashes(length + 2) + "+\n");
   398      for (String line : msgLines) {
   399        displayMsg = displayMsg.concat("+" + StringUtils.center(line, length + 2) + "+\n");
   400      }
   401      displayMsg = displayMsg.concat("+" + printDashes(length + 2) + "+");
   402      logger.info(displayMsg);
   403    }
   404  
   405    /**
   406     * Helper function to create a string with a specified number of dashes (for logging purposes).
   407     */
   408    private String printDashes(int length) {
   409      String dashes = "";
   410      for (int i = 0; i < length; i++) {
   411        dashes = dashes.concat("-");
   412      }
   413      return dashes;
   414    }
   415  
   416    /**
   417     * Helper function to shorten a string to a max of 6 characters for logging purposes.
   418     */
   419    private Object abbreviate(String player) {
   420      return player.substring(0, Math.min(player.length(), 6));
   421    }
   422  }