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 }