2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.persistence.dynamodb.internal;
15 import java.util.concurrent.CompletableFuture;
16 import java.util.concurrent.ExecutorService;
17 import java.util.function.Consumer;
19 import org.eclipse.jdt.annotation.NonNullByDefault;
20 import org.eclipse.jdt.annotation.Nullable;
21 import org.slf4j.Logger;
22 import org.slf4j.LoggerFactory;
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;
30 * The DynamoDBTableNameResolver resolves DynamoDB table name for a given item.
32 * @author Sami Salonen - Initial contribution
36 public class DynamoDBTableNameResolver {
37 private final Logger logger = LoggerFactory.getLogger(DynamoDBTableNameResolver.class);
39 private final String tablePrefix;
40 private ExpectedTableSchema tableRevision;
43 public DynamoDBTableNameResolver(ExpectedTableSchema tableRevision, String table, String tablePrefix) {
44 this.tableRevision = tableRevision;
46 this.tablePrefix = tablePrefix;
47 switch (tableRevision) {
49 if (table.isBlank()) {
50 throw new IllegalArgumentException("table should be specified with NEW schema");
54 if (table.isBlank()) {
55 throw new IllegalArgumentException("table should be specified with MAYBE_LEGACY schema");
59 if (tablePrefix.isBlank()) {
60 throw new IllegalArgumentException("tablePrefix should be specified with LEGACY schema");
64 throw new IllegalArgumentException("Bug");
69 * Create instance of DynamoDBTableNameResolver using given DynamoDBItem. Item's class is used to determine the
73 * @param item dto to use to determine table name
75 * @throws IllegalStateException when table schmea is not determined
77 public String fromItem(DynamoDBItem<?> item) {
78 if (!isFullyResolved()) {
79 throw new IllegalStateException();
81 switch (tableRevision) {
83 return getTableNameAccordingToNewSchema();
85 return getTableNameAccordingToLegacySchema(item);
87 throw new IllegalArgumentException("Bug");
92 * Get table name according to new schema. This instance does not have to have fully determined schema
96 private String getTableNameAccordingToNewSchema() {
101 * Get table name according to legacy schema. This instance does not have to have fully determined schema
103 * @param item dto to use to determine table name
106 private String getTableNameAccordingToLegacySchema(DynamoDBItem<?> item) {
107 // Use the visitor pattern to deduce the table name
108 return item.accept(new DynamoDBItemVisitor<String>() {
111 public String visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
112 return tablePrefix + "bigdecimal";
116 public String visit(DynamoDBStringItem dynamoStringItem) {
117 return tablePrefix + "string";
123 * Construct DynamoDBTableNameResolver corresponding to DynamoDBItem class
128 public String fromClass(Class<? extends DynamoDBItem<?>> clazz) {
129 DynamoDBItem<?> dummy;
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));
137 return this.fromItem(dummy);
141 * Whether we have determined the schema and table names to use
143 * @return true when schema revision is clearly specified
145 public boolean isFullyResolved() {
146 return tableRevision.isFullyResolved();
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);
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);
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
167 // If old tables do not exist, we default to new table layout/schema
168 tableRevision = (!table1Present && !table2Present) ? ExpectedTableSchema.NEW
169 : ExpectedTableSchema.LEGACY;
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);
184 * @return whether table exists, or null when state is unknown
186 private CompletableFuture<@Nullable Boolean> tableIsPresent(DynamoDbAsyncClient lowLevelClient,
187 Consumer<DescribeTableRequest.Builder> describeTableRequestMutator, ExecutorService executor,
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);
199 "Could not verify whether table {} is present: {} {}. Cannot determine table schema.",
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);
211 private boolean tableIsBeingRemoved(TableStatus tableStatus) {
212 return (tableStatus == TableStatus.ARCHIVING || tableStatus == TableStatus.DELETING
213 || tableStatus == TableStatus.ARCHIVED);
216 public ExpectedTableSchema getTableSchema() {
217 return tableRevision;