1 /* 2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"). 5 * You may not use this file except in compliance with the License. 6 * A copy of the License is located at 7 * 8 * http://aws.amazon.com/apache2.0 9 * 10 * or in the "license" file accompanying this file. This file is distributed 11 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 12 * express or implied. See the License for the specific language governing 13 * permissions and limitations under the License. 14 */ 15 16 package utils.test.resources; 17 18 import java.util.List; 19 20 import software.amazon.awssdk.awscore.exception.AwsServiceException; 21 import software.amazon.awssdk.core.exception.SdkServiceException; 22 import software.amazon.awssdk.services.dynamodb.DynamoDbClient; 23 import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; 24 import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; 25 import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest; 26 import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; 27 import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndexDescription; 28 import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndex; 29 import software.amazon.awssdk.services.dynamodb.model.LocalSecondaryIndexDescription; 30 import software.amazon.awssdk.services.dynamodb.model.Projection; 31 import software.amazon.awssdk.services.dynamodb.model.TableDescription; 32 import software.amazon.awssdk.services.dynamodb.model.TableStatus; 33 import software.amazon.awssdk.testutils.UnorderedCollectionComparator; 34 import software.amazon.awssdk.utils.Logger; 35 import utils.resources.TestResource; 36 import utils.test.util.DynamoDBTestBase; 37 import utils.test.util.TableUtils; 38 39 public abstract class DynamoDBTableResource implements TestResource { 40 private static final Logger log = Logger.loggerFor(DynamoDBTableResource.class); 41 42 /** 43 * Returns true if the two lists of GlobalSecondaryIndex and 44 * GlobalSecondaryIndexDescription share the same set of: 45 * 1) indexName 46 * 2) projection 47 * 3) keySchema (compared as unordered lists) 48 */ equalUnorderedGsiLists(List<GlobalSecondaryIndex> listA, List<GlobalSecondaryIndexDescription> listB)49 static boolean equalUnorderedGsiLists(List<GlobalSecondaryIndex> listA, List<GlobalSecondaryIndexDescription> listB) { 50 return UnorderedCollectionComparator.equalUnorderedCollections( 51 listA, listB, 52 new UnorderedCollectionComparator.CrossTypeComparator<GlobalSecondaryIndex, GlobalSecondaryIndexDescription>() { 53 @Override 54 public boolean equals(GlobalSecondaryIndex a, GlobalSecondaryIndexDescription b) { 55 return a.indexName().equals(b.indexName()) 56 && equalProjections(a.projection(), b.projection()) 57 && UnorderedCollectionComparator.equalUnorderedCollections(a.keySchema(), b.keySchema()); 58 } 59 }); 60 } 61 62 /** 63 * Returns true if the two lists of LocalSecondaryIndex and 64 * LocalSecondaryIndexDescription share the same set of: 65 * 1) indexName 66 * 2) projection 67 * 3) keySchema (compared as unordered lists) 68 */ 69 static boolean equalUnorderedLsiLists(List<LocalSecondaryIndex> listA, List<LocalSecondaryIndexDescription> listB) { 70 return UnorderedCollectionComparator.equalUnorderedCollections( 71 listA, listB, 72 new UnorderedCollectionComparator.CrossTypeComparator<LocalSecondaryIndex, LocalSecondaryIndexDescription>() { 73 @Override 74 public boolean equals(LocalSecondaryIndex a, LocalSecondaryIndexDescription b) { 75 // Project parameter might not be specified in the 76 // CreateTableRequest. But it should be treated as equal 77 // to the default projection type - KEYS_ONLY. 78 return a.indexName().equals(b.indexName()) 79 && equalProjections(a.projection(), b.projection()) 80 && UnorderedCollectionComparator.equalUnorderedCollections(a.keySchema(), b.keySchema()); 81 } 82 }); 83 } 84 85 /** 86 * Compares the Projection parameter included in the CreateTableRequest, 87 * with the one returned from DescribeTableResponse. 88 */ 89 static boolean equalProjections(Projection fromCreateTableRequest, Projection fromDescribeTableResponse) { 90 if (fromCreateTableRequest == null || fromDescribeTableResponse == null) { 91 throw new IllegalStateException("The projection parameter should never be null."); 92 } 93 94 return fromCreateTableRequest.projectionType().equals( 95 fromDescribeTableResponse.projectionType()) 96 && UnorderedCollectionComparator.equalUnorderedCollections( 97 fromCreateTableRequest.nonKeyAttributes(), 98 fromDescribeTableResponse.nonKeyAttributes()); 99 } 100 101 protected abstract DynamoDbClient getClient(); 102 103 protected abstract CreateTableRequest getCreateTableRequest(); 104 105 /** 106 * Implementation of TestResource interfaces 107 */ 108 109 @Override 110 public void create(boolean waitTillFinished) { 111 log.info(() -> "Creating " + this + "..."); 112 getClient().createTable(getCreateTableRequest()); 113 114 if (waitTillFinished) { 115 log.info(() -> "Waiting for " + this + " to become active..."); 116 try { 117 TableUtils.waitUntilActive(getClient(), getCreateTableRequest().tableName()); 118 } catch (InterruptedException e) { 119 Thread.currentThread().interrupt(); 120 } 121 } 122 } 123 124 @Override 125 public void delete(boolean waitTillFinished) { 126 log.info(() -> "Deleting " + this + "..."); 127 getClient().deleteTable(DeleteTableRequest.builder().tableName(getCreateTableRequest().tableName()).build()); 128 129 if (waitTillFinished) { 130 log.info(() -> "Waiting for " + this + " to become deleted..."); 131 DynamoDBTestBase.waitForTableToBecomeDeleted(getClient(), getCreateTableRequest().tableName()); 132 } 133 } 134 135 @Override 136 public ResourceStatus getResourceStatus() { 137 CreateTableRequest createRequest = getCreateTableRequest(); 138 TableDescription table; 139 try { 140 table = getClient().describeTable(DescribeTableRequest.builder().tableName( 141 createRequest.tableName()).build()).table(); 142 } catch (AwsServiceException exception) { 143 if (exception.awsErrorDetails().errorCode().equalsIgnoreCase("ResourceNotFoundException")) { 144 return ResourceStatus.NOT_EXIST; 145 } 146 throw exception; 147 } 148 149 if (table.tableStatus() == TableStatus.ACTIVE) { 150 // returns AVAILABLE only if table KeySchema + LSIs + GSIs all match. 151 if (UnorderedCollectionComparator.equalUnorderedCollections(createRequest.keySchema(), table.keySchema()) 152 && equalUnorderedGsiLists(createRequest.globalSecondaryIndexes(), table.globalSecondaryIndexes()) 153 && equalUnorderedLsiLists(createRequest.localSecondaryIndexes(), table.localSecondaryIndexes())) { 154 return ResourceStatus.AVAILABLE; 155 } else { 156 return ResourceStatus.EXIST_INCOMPATIBLE_RESOURCE; 157 } 158 } else if (table.tableStatus() == TableStatus.CREATING 159 || table.tableStatus() == TableStatus.UPDATING 160 || table.tableStatus() == TableStatus.DELETING) { 161 return ResourceStatus.TRANSIENT; 162 } else { 163 return ResourceStatus.NOT_EXIST; 164 } 165 } 166 167 /** 168 * Object interfaces 169 */ 170 @Override 171 public String toString() { 172 return "DynamoDB Table [" + getCreateTableRequest().tableName() + "]"; 173 } 174 175 @Override 176 public int hashCode() { 177 return getCreateTableRequest().hashCode(); 178 } 179 180 @Override 181 public boolean equals(Object other) { 182 if (!(other instanceof DynamoDBTableResource)) { 183 return false; 184 } 185 return getCreateTableRequest().equals( 186 ((DynamoDBTableResource) other).getCreateTableRequest()); 187 } 188 } 189