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 String itemName = filter.getItemName();
58 if (itemName == null) {
59 throw new IllegalArgumentException("Item name not set");
61 addFilterbyItemAndTimeFilter(queryBuilder, expectedTableSchema, itemName, filter);
62 addStateFilter(queryBuilder, expectedTableSchema, item, dtoClass, filter);
63 addProjection(dtoClass, expectedTableSchema, queryBuilder);
64 return queryBuilder.build();
68 * Add projection for key parameters only, not expire date
70 private static void addProjection(Class<? extends DynamoDBItem<?>> dtoClass,
71 ExpectedTableSchema expectedTableSchema, QueryEnhancedRequest.Builder queryBuilder) {
72 boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
74 queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME_LEGACY,
75 DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC_LEGACY, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY);
77 acceptAsEmptyDTO(dtoClass, new DynamoDBItemVisitor<@Nullable Void>() {
79 public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
80 queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
81 DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
86 public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
87 queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
88 DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
95 private static void addStateFilter(QueryEnhancedRequest.Builder queryBuilder,
96 ExpectedTableSchema expectedTableSchema, Item item, Class<? extends DynamoDBItem<?>> dtoClass,
97 FilterCriteria filter) {
98 final Expression expression;
99 Builder itemStateTypeExpressionBuilder = Expression.builder()
100 .expression(String.format("attribute_exists(#attr)"));
101 boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
102 acceptAsEmptyDTO(dtoClass, new DynamoDBItemVisitor<@Nullable Void>() {
104 public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
105 itemStateTypeExpressionBuilder.putExpressionName("#attr",
106 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
107 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
112 public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
113 itemStateTypeExpressionBuilder.putExpressionName("#attr",
114 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
115 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
119 if (filter.getOperator() != null && filter.getState() != null) {
120 // Convert filter's state to DynamoDBItem in order get suitable string representation for the state
121 Expression.Builder stateFilterExpressionBuilder = Expression.builder()
122 .expression(String.format("#attr %s :value", operatorAsString(filter.getOperator())));
123 // Following will throw IllegalArgumentException when filter state is not compatible with
124 // item. This is acceptable.
125 GenericItem stateToFind = DynamoDBPersistenceService.copyItem(item, item, filter.getItemName(),
127 acceptAsDTO(stateToFind, legacy, new DynamoDBItemVisitor<@Nullable Void>() {
129 public @Nullable Void visit(DynamoDBStringItem serialized) {
130 stateFilterExpressionBuilder.putExpressionName("#attr",
131 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
132 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
133 stateFilterExpressionBuilder.putExpressionValue(":value",
134 AttributeValue.builder().s(serialized.getState()).build());
138 @SuppressWarnings("null")
140 public @Nullable Void visit(DynamoDBBigDecimalItem serialized) {
141 stateFilterExpressionBuilder.putExpressionName("#attr",
142 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
143 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
144 stateFilterExpressionBuilder.putExpressionValue(":value",
145 AttributeValue.builder().n(serialized.getState().toPlainString()).build());
149 expression = Expression.join(stateFilterExpressionBuilder.build(), itemStateTypeExpressionBuilder.build(),
152 queryBuilder.filterExpression(expression);
154 expression = itemStateTypeExpressionBuilder.build();
156 queryBuilder.filterExpression(expression);
159 private static void addFilterbyItemAndTimeFilter(QueryEnhancedRequest.Builder queryBuilder,
160 ExpectedTableSchema expectedTableSchema, String partition, final FilterCriteria filter) {
161 ZonedDateTime begin = filter.getBeginDate();
162 ZonedDateTime end = filter.getEndDate();
163 boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
165 AttributeConverter<ZonedDateTime> timeConverter = AbstractDynamoDBItem.getTimestampConverter(legacy);
167 if (begin == null && end == null) {
168 // No need to place time filter, but we do filter by partition
169 queryBuilder.queryConditional(QueryConditional.keyEqualTo(k -> k.partitionValue(partition)));
170 } else if (begin != null && end == null) {
171 queryBuilder.queryConditional(QueryConditional
172 .sortGreaterThan(k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(begin))));
173 } else if (begin == null && end != null) {
174 queryBuilder.queryConditional(QueryConditional
175 .sortLessThan(k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(end))));
176 } else if (begin != null && end != null) {
177 queryBuilder.queryConditional(QueryConditional.sortBetween(
178 k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(begin)),
179 k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(end))));
184 * Convert op to string suitable for dynamodb filter expression
187 * @return string representation corresponding to the given the Operator
189 private static String operatorAsString(Operator op) {
205 throw new IllegalStateException("Unknown operator " + op);
209 private static <T> void acceptAsDTO(Item item, boolean legacy, DynamoDBItemVisitor<T> visitor) {
210 ZonedDateTime dummyTimestamp = ZonedDateTime.now();
212 AbstractDynamoDBItem.fromStateLegacy(item, dummyTimestamp).accept(visitor);
214 AbstractDynamoDBItem.fromStateNew(item, dummyTimestamp, null).accept(visitor);
218 private static <T> void acceptAsEmptyDTO(Class<? extends DynamoDBItem<?>> dtoClass,
219 DynamoDBItemVisitor<T> visitor) {
221 dtoClass.getDeclaredConstructor().newInstance().accept(visitor);
222 } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
223 | NoSuchMethodException | SecurityException e) {
224 throw new IllegalStateException(e);