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