]> git.basschouten.com Git - openhab-addons.git/blob
bc06ea544ddae984f7d92dce91a5b741c3229ab9
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
7  * This program and the accompanying materials are made available under the
8  * terms of the Eclipse Public License 2.0 which is available at
9  * http://www.eclipse.org/legal/epl-2.0
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.persistence.dynamodb.internal;
14
15 import java.util.concurrent.CompletableFuture;
16 import java.util.concurrent.ExecutorService;
17 import java.util.function.Consumer;
18
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.slf4j.Logger;
22 import org.slf4j.LoggerFactory;
23
24 import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient;
25 import software.amazon.awssdk.services.dynamodb.model.DescribeTableRequest;
26 import software.amazon.awssdk.services.dynamodb.model.ResourceNotFoundException;
27 import software.amazon.awssdk.services.dynamodb.model.TableStatus;
28
29 /**
30  * The DynamoDBTableNameResolver resolves DynamoDB table name for a given item.
31  *
32  * @author Sami Salonen - Initial contribution
33  *
34  */
35 @NonNullByDefault
36 public class DynamoDBTableNameResolver {
37     private final Logger logger = LoggerFactory.getLogger(DynamoDBTableNameResolver.class);
38
39     private final String tablePrefix;
40     private ExpectedTableSchema tableRevision;
41     private String table;
42
43     public DynamoDBTableNameResolver(ExpectedTableSchema tableRevision, String table, String tablePrefix) {
44         this.tableRevision = tableRevision;
45         this.table = table;
46         this.tablePrefix = tablePrefix;
47         switch (tableRevision) {
48             case NEW:
49                 if (table.isBlank()) {
50                     throw new IllegalArgumentException("table should be specified with NEW schema");
51                 }
52                 break;
53             case MAYBE_LEGACY:
54                 if (table.isBlank()) {
55                     throw new IllegalArgumentException("table should be specified with MAYBE_LEGACY schema");
56                 }
57                 // fall-through
58             case LEGACY:
59                 if (tablePrefix.isBlank()) {
60                     throw new IllegalArgumentException("tablePrefix should be specified with LEGACY schema");
61                 }
62                 break;
63             default:
64                 throw new IllegalArgumentException("Bug");
65         }
66     }
67
68     /**
69      * Create instance of DynamoDBTableNameResolver using given DynamoDBItem. Item's class is used to determine the
70      * table name.
71      *
72      *
73      * @param item dto to use to determine table name
74      * @return table name
75      * @throws IllegalStateException when table schmea is not determined
76      */
77     public String fromItem(DynamoDBItem<?> item) {
78         if (!isFullyResolved()) {
79             throw new IllegalStateException();
80         }
81         switch (tableRevision) {
82             case NEW:
83                 return getTableNameAccordingToNewSchema();
84             case LEGACY:
85                 return getTableNameAccordingToLegacySchema(item);
86             default:
87                 throw new IllegalArgumentException("Bug");
88         }
89     }
90
91     /**
92      * Get table name according to new schema. This instance does not have to have fully determined schema
93      *
94      * @return table name
95      */
96     private String getTableNameAccordingToNewSchema() {
97         return table;
98     }
99
100     /**
101      * Get table name according to legacy schema. This instance does not have to have fully determined schema
102      *
103      * @param item dto to use to determine table name
104      * @return table name
105      */
106     private String getTableNameAccordingToLegacySchema(DynamoDBItem<?> item) {
107         // Use the visitor pattern to deduce the table name
108         return item.accept(new DynamoDBItemVisitor<String>() {
109
110             @Override
111             public String visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
112                 return tablePrefix + "bigdecimal";
113             }
114
115             @Override
116             public String visit(DynamoDBStringItem dynamoStringItem) {
117                 return tablePrefix + "string";
118             }
119         });
120     }
121
122     /**
123      * Construct DynamoDBTableNameResolver corresponding to DynamoDBItem class
124      *
125      * @param clazz
126      * @return
127      */
128     public String fromClass(Class<? extends DynamoDBItem<?>> clazz) {
129         DynamoDBItem<?> dummy;
130         try {
131             // Construct new instance of this class (assuming presense no-argument constructor)
132             // in order to re-use fromItem(DynamoDBItem) constructor
133             dummy = clazz.getConstructor().newInstance();
134         } catch (Exception e) {
135             throw new IllegalStateException(String.format("Could not find suitable constructor for class %s", clazz));
136         }
137         return this.fromItem(dummy);
138     }
139
140     /**
141      * Whether we have determined the schema and table names to use
142      *
143      * @return true when schema revision is clearly specified
144      */
145     public boolean isFullyResolved() {
146         return tableRevision.isFullyResolved();
147     }
148
149     public CompletableFuture<Boolean> resolveSchema(DynamoDbAsyncClient lowLevelClient,
150             Consumer<DescribeTableRequest.Builder> describeTableRequestMutator, ExecutorService executor) {
151         CompletableFuture<Boolean> resolved = new CompletableFuture<>();
152         if (isFullyResolved()) {
153             resolved.complete(true);
154         }
155
156         String numberTableLegacy = getTableNameAccordingToLegacySchema(new DynamoDBBigDecimalItem());
157         String stringTableLegacy = getTableNameAccordingToLegacySchema(new DynamoDBStringItem());
158         CompletableFuture<@Nullable Boolean> tableSchemaNumbers = tableIsPresent(lowLevelClient,
159                 describeTableRequestMutator, executor, numberTableLegacy);
160         CompletableFuture<@Nullable Boolean> tableSchemaStrings = tableIsPresent(lowLevelClient,
161                 describeTableRequestMutator, executor, stringTableLegacy);
162
163         tableSchemaNumbers.thenAcceptBothAsync(tableSchemaStrings, (table1Present, table2Present) -> {
164             if (table1Present != null && table2Present != null) {
165                 // Since the Booleans are not null, we know for sure whether table is present or not
166
167                 // If old tables do not exist, we default to new table layout/schema
168                 tableRevision = (!table1Present && !table2Present) ? ExpectedTableSchema.NEW
169                         : ExpectedTableSchema.LEGACY;
170             }
171             resolved.complete(table1Present != null && table2Present != null);
172         }, executor).exceptionally(e -> {
173             // should not happen as individual futures have exceptions handled
174             logger.error("Unexpected error. BUG", e);
175             resolved.complete(false);
176             return null;
177         });
178
179         return resolved;
180     }
181
182     /**
183      *
184      * @return whether table exists, or null when state is unknown
185      */
186     private CompletableFuture<@Nullable Boolean> tableIsPresent(DynamoDbAsyncClient lowLevelClient,
187             Consumer<DescribeTableRequest.Builder> describeTableRequestMutator, ExecutorService executor,
188             String tableName) {
189         CompletableFuture<@Nullable Boolean> tableSchema = new CompletableFuture<>();
190         lowLevelClient.describeTable(b -> b.tableName(tableName).applyMutation(describeTableRequestMutator))
191                 .thenApplyAsync(r -> r.table().tableStatus(), executor)
192                 .thenApplyAsync(tableStatus -> tableIsBeingRemoved(tableStatus) ? false : true)
193                 .thenAccept(r -> tableSchema.complete(r)).exceptionally(exception -> {
194                     Throwable cause = exception.getCause();
195                     if (cause instanceof ResourceNotFoundException) {
196                         tableSchema.complete(false);
197                     } else {
198                         logger.warn(
199                                 "Could not verify whether table {} is present: {} {}. Cannot determine table schema.",
200                                 tableName,
201                                 cause == null ? exception.getClass().getSimpleName() : cause.getClass().getSimpleName(),
202                                 cause == null ? exception.getMessage() : cause.getMessage());
203                         // Other error, we could not resolve schema...
204                         tableSchema.complete(null);
205                     }
206                     return null;
207                 });
208         return tableSchema;
209     }
210
211     private boolean tableIsBeingRemoved(TableStatus tableStatus) {
212         return (tableStatus == TableStatus.ARCHIVING || tableStatus == TableStatus.DELETING
213                 || tableStatus == TableStatus.ARCHIVED);
214     }
215
216     public ExpectedTableSchema getTableSchema() {
217         return tableRevision;
218     }
219 }