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.i18n.UnitProvider;
21 import org.openhab.core.items.GenericItem;
22 import org.openhab.core.items.Item;
23 import org.openhab.core.persistence.FilterCriteria;
24 import org.openhab.core.persistence.FilterCriteria.Operator;
25 import org.openhab.core.persistence.FilterCriteria.Ordering;
27 import software.amazon.awssdk.enhanced.dynamodb.AttributeConverter;
28 import software.amazon.awssdk.enhanced.dynamodb.Expression;
29 import software.amazon.awssdk.enhanced.dynamodb.Expression.Builder;
30 import software.amazon.awssdk.enhanced.dynamodb.model.QueryConditional;
31 import software.amazon.awssdk.enhanced.dynamodb.model.QueryEnhancedRequest;
32 import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
37 * @author Sami Salonen - Initial contribution
40 public class DynamoDBQueryUtils {
42 * Construct dynamodb query from filter
44 * @param dtoClass dto class
45 * @param expectedTableSchema table schema to query against
46 * @param item item corresponding to filter
47 * @param filter filter for the query
48 * @return DynamoDBQueryExpression corresponding to the given FilterCriteria
49 * @param unitProvider the unit provider for number with dimension
50 * @throws IllegalArgumentException when schema is not fully resolved
52 public static QueryEnhancedRequest createQueryExpression(Class<? extends DynamoDBItem<?>> dtoClass,
53 ExpectedTableSchema expectedTableSchema, Item item, FilterCriteria filter, UnitProvider unitProvider) {
54 if (!expectedTableSchema.isFullyResolved()) {
55 throw new IllegalArgumentException("Schema not resolved");
57 QueryEnhancedRequest.Builder queryBuilder = QueryEnhancedRequest.builder()
58 .scanIndexForward(filter.getOrdering() == Ordering.ASCENDING);
59 String itemName = filter.getItemName();
60 if (itemName == null) {
61 throw new IllegalArgumentException("Item name not set");
63 addFilterbyItemAndTimeFilter(queryBuilder, expectedTableSchema, itemName, filter);
64 addStateFilter(queryBuilder, expectedTableSchema, item, dtoClass, filter, unitProvider);
65 addProjection(dtoClass, expectedTableSchema, queryBuilder);
66 return queryBuilder.build();
70 * Add projection for key parameters only, not expire date
72 private static void addProjection(Class<? extends DynamoDBItem<?>> dtoClass,
73 ExpectedTableSchema expectedTableSchema, QueryEnhancedRequest.Builder queryBuilder) {
74 boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
76 queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME_LEGACY,
77 DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC_LEGACY, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY);
79 acceptAsEmptyDTO(dtoClass, new DynamoDBItemVisitor<@Nullable Void>() {
81 public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
82 queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
83 DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
88 public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
89 queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
90 DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
97 private static void addStateFilter(QueryEnhancedRequest.Builder queryBuilder,
98 ExpectedTableSchema expectedTableSchema, Item item, Class<? extends DynamoDBItem<?>> dtoClass,
99 FilterCriteria filter, UnitProvider unitProvider) {
100 final Expression expression;
101 Builder itemStateTypeExpressionBuilder = Expression.builder()
102 .expression(String.format("attribute_exists(#attr)"));
103 boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
104 acceptAsEmptyDTO(dtoClass, new DynamoDBItemVisitor<@Nullable Void>() {
106 public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
107 itemStateTypeExpressionBuilder.putExpressionName("#attr",
108 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
109 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
114 public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
115 itemStateTypeExpressionBuilder.putExpressionName("#attr",
116 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
117 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
121 if (filter.getOperator() != null && filter.getState() != null) {
122 // Convert filter's state to DynamoDBItem in order get suitable string representation for the state
123 Expression.Builder stateFilterExpressionBuilder = Expression.builder()
124 .expression(String.format("#attr %s :value", operatorAsString(filter.getOperator())));
125 // Following will throw IllegalArgumentException when filter state is not compatible with
126 // item. This is acceptable.
127 GenericItem stateToFind = DynamoDBPersistenceService.copyItem(item, item, filter.getItemName(),
128 filter.getState(), unitProvider);
129 acceptAsDTO(stateToFind, legacy, new DynamoDBItemVisitor<@Nullable Void>() {
131 public @Nullable Void visit(DynamoDBStringItem serialized) {
132 stateFilterExpressionBuilder.putExpressionName("#attr",
133 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
134 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
135 stateFilterExpressionBuilder.putExpressionValue(":value",
136 AttributeValue.builder().s(serialized.getState()).build());
140 @SuppressWarnings("null")
142 public @Nullable Void visit(DynamoDBBigDecimalItem serialized) {
143 stateFilterExpressionBuilder.putExpressionName("#attr",
144 legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
145 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
146 stateFilterExpressionBuilder.putExpressionValue(":value",
147 AttributeValue.builder().n(serialized.getState().toPlainString()).build());
151 expression = Expression.join(stateFilterExpressionBuilder.build(), itemStateTypeExpressionBuilder.build(),
154 queryBuilder.filterExpression(expression);
156 expression = itemStateTypeExpressionBuilder.build();
158 queryBuilder.filterExpression(expression);
161 private static void addFilterbyItemAndTimeFilter(QueryEnhancedRequest.Builder queryBuilder,
162 ExpectedTableSchema expectedTableSchema, String partition, final FilterCriteria filter) {
163 ZonedDateTime begin = filter.getBeginDate();
164 ZonedDateTime end = filter.getEndDate();
165 boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
167 AttributeConverter<ZonedDateTime> timeConverter = AbstractDynamoDBItem.getTimestampConverter(legacy);
169 if (begin == null && end == null) {
170 // No need to place time filter, but we do filter by partition
171 queryBuilder.queryConditional(QueryConditional.keyEqualTo(k -> k.partitionValue(partition)));
172 } else if (begin != null && end == null) {
173 queryBuilder.queryConditional(QueryConditional
174 .sortGreaterThan(k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(begin))));
175 } else if (begin == null && end != null) {
176 queryBuilder.queryConditional(QueryConditional
177 .sortLessThan(k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(end))));
178 } else if (begin != null && end != null) {
179 queryBuilder.queryConditional(QueryConditional.sortBetween(
180 k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(begin)),
181 k -> k.partitionValue(partition).sortValue(timeConverter.transformFrom(end))));
186 * Convert op to string suitable for dynamodb filter expression
189 * @return string representation corresponding to the given the Operator
191 private static String operatorAsString(Operator op) {
207 throw new IllegalStateException("Unknown operator " + op);
211 private static <T> void acceptAsDTO(Item item, boolean legacy, DynamoDBItemVisitor<T> visitor) {
212 ZonedDateTime dummyTimestamp = ZonedDateTime.now();
214 AbstractDynamoDBItem.fromStateLegacy(item, dummyTimestamp).accept(visitor);
216 AbstractDynamoDBItem.fromStateNew(item, dummyTimestamp, null).accept(visitor);
220 private static <T> void acceptAsEmptyDTO(Class<? extends DynamoDBItem<?>> dtoClass,
221 DynamoDBItemVisitor<T> visitor) {
223 dtoClass.getDeclaredConstructor().newInstance().accept(visitor);
224 } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
225 | NoSuchMethodException | SecurityException e) {
226 throw new IllegalStateException(e);