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.lang.reflect.InvocationTargetException;
16 import java.time.ZonedDateTime;
18 import org.eclipse.jdt.annotation.NonNullByDefault;
19 import org.eclipse.jdt.annotation.Nullable;
20 import org.openhab.core.items.GenericItem;
21 import org.openhab.core.items.Item;
22 import org.openhab.core.persistence.FilterCriteria;
23 import org.openhab.core.persistence.FilterCriteria.Operator;
24 import org.openhab.core.persistence.FilterCriteria.Ordering;
26 import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
27 import software.amazon.awssdk.enhanced.dynamodb.Expression;
28 import software.amazon.awssdk.enhanced.dynamodb.Expression.Builder;
29 import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
30 import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest;
31 import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
36 * @author Sami Salonen - Initial contribution
39 public class DynamoDBQueryUtils {
41 * Construct dynamodb query from filter
43 * @param dtoClass dto class
44 * @param expectedTableSchema table schema to query against
45 * @param item item corresponding to filter
46 * @param filter filter for the query
47 * @return DynamoDBQueryExpression corresponding to the given FilterCriteria
48 * @throws IllegalArgumentException when schema is not fully resolved
50 public static QueryEnhancedRequest createQueryExpression(Class<? extends DynamoDBItem<?>> dtoClass,
51 ExpectedTableSchema expectedTableSchema, Item item, FilterCriteria filter) {
52 if (!expectedTableSchema.isFullyResolved()) {
53 throw new IllegalArgumentException("Schema not resolved");
55 QueryEnhancedRequest.Builder queryBuilder = QueryEnhancedRequest.builder()
56 .scanIndexForward(filter.getOrdering() == Ordering.ASCENDING);
57 addFilterbyItemAndTimeFilter(queryBuilder, expectedTableSchema, filter.getItemName(), filter);
58 addStateFilter(queryBuilder, expectedTableSchema, item, dtoClass, filter);
59 addProjection(dtoClass, expectedTableSchema, queryBuilder);
60 return queryBuilder.build();
64 * Add projection for key parameters only, not expire date
66 private static void addProjection(Class<? extends DynamoDBItem<?>> dtoClass,
67 ExpectedTableSchema expectedTableSchema, QueryEnhancedRequest.Builder queryBuilder) {
68 boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
70 queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME_LEGACY,
71 DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC_LEGACY, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY);
73 acceptAsEmptyDTO(dtoClass, new DynamoDBItemVisitor<@Nullable Void>() {
75 public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
76 queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
77 DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
82 public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
83 queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
84 DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
91 private static void addStateFilter(QueryEnhancedRequest.Builder queryBuilder,
92 ExpectedTableSchema expectedTableSchema, Item item, Class<? extends DynamoDBItem<?>> dtoClass,
93 FilterCriteria filter) {
94 final Expression expression;
95 Builder itemStateTypeExpressionBuilder = Expression.builder()
96 .expression(String.format("attribute_exists(#attr)"));
97 boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
98 acceptAsEmptyDTO(dtoClass, new DynamoDBItemVisitor<@Nullable Void>() {
100 public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
101 itemStateTypeExpressionBuilder.putExpressionName("#attr",
102 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
103 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
108 public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
109 itemStateTypeExpressionBuilder.putExpressionName("#attr",
110 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
111 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
115 if (filter.getOperator() != null && filter.getState() != null) {
116 // Convert filter's state to DynamoDBItem in order get suitable string representation for the state
117 Expression.Builder stateFilterExpressionBuilder = Expression.builder()
118 .expression(String.format("#attr %s :value", operatorAsString(filter.getOperator())));
119 // Following will throw IllegalArgumentException when filter state is not compatible with
120 // item. This is acceptable.
121 GenericItem stateToFind = DynamoDBPersistenceService.copyItem(item, item, filter.getItemName(),
123 acceptAsDTO(stateToFind, legacy, new DynamoDBItemVisitor<@Nullable Void>() {
125 public @Nullable Void visit(DynamoDBStringItem serialized) {
126 stateFilterExpressionBuilder.putExpressionName("#attr",
127 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
128 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
129 stateFilterExpressionBuilder.putExpressionValue(":value",
130 AttributeValue.builder().s(serialized.getState()).build());
134 @SuppressWarnings("null")
136 public @Nullable Void visit(DynamoDBBigDecimalItem serialized) {
137 stateFilterExpressionBuilder.putExpressionName("#attr",
138 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
139 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
140 stateFilterExpressionBuilder.putExpressionValue(":value",
141 AttributeValue.builder().n(serialized.getState().toPlainString()).build());
145 expression = Expression.join(stateFilterExpressionBuilder.build(), itemStateTypeExpressionBuilder.build(),
148 queryBuilder.filterExpression(expression);
150 expression = itemStateTypeExpressionBuilder.build();
152 queryBuilder.filterExpression(expression);
155 private static void addFilterbyItemAndTimeFilter(QueryEnhancedRequest.Builder queryBuilder,
156 ExpectedTableSchema expectedTableSchema, String partition, final FilterCriteria filter) {
157 boolean hasBegin = filter.getBeginDate() != null;
158 boolean hasEnd = filter.getEndDate() != null;
159 boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
161 AttributeConverter<ZonedDateTime> timeConverter = AbstractDynamoDBItem.getTimestampConverter(legacy);
163 if (!hasBegin && !hasEnd) {
164 // No need to place time filter filter but we do filter by partition
165 queryBuilder.queryConditional(QueryConditional.keyEqualTo(k -> k.partitionValue(partition)));
166 } else if (hasBegin && !hasEnd) {
167 queryBuilder.queryConditional(QueryConditional.sortGreaterThan(
168 k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(filter.getBeginDate()))));
169 } else if (!hasBegin && hasEnd) {
170 queryBuilder.queryConditional(QueryConditional.sortLessThan(
171 k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(filter.getEndDate()))));
173 assert hasBegin && hasEnd; // invariant
174 queryBuilder.queryConditional(QueryConditional.sortBetween(
175 k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(filter.getBeginDate())),
176 k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(filter.getEndDate()))));
181 * Convert op to string suitable for dynamodb filter expression
184 * @return string representation corresponding to the given the Operator
186 private static String operatorAsString(Operator op) {
202 throw new IllegalStateException("Unknown operator " + op);
206 private static <T> void acceptAsDTO(Item item, boolean legacy, DynamoDBItemVisitor<T> visitor) {
207 ZonedDateTime dummyTimestamp = ZonedDateTime.now();
209 AbstractDynamoDBItem.fromStateLegacy(item, dummyTimestamp).accept(visitor);
211 AbstractDynamoDBItem.fromStateNew(item, dummyTimestamp, null).accept(visitor);
215 private static <T> void acceptAsEmptyDTO(Class<? extends DynamoDBItem<?>> dtoClass,
216 DynamoDBItemVisitor<T> visitor) {
218 dtoClass.getDeclaredConstructor().newInstance().accept(visitor);
219 } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
220 | NoSuchMethodException | SecurityException e) {
221 throw new IllegalStateException(e);