]> git.basschouten.com Git - openhab-addons.git/blob
fa86f9a098fb36bf28f659ee37fccfb8f8fbe8fe
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.lang.reflect.InvocationTargetException;
16 import java.time.ZonedDateTime;
17
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;
25
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;
32
33 /**
34  * Utility class
35  *
36  * @author Sami Salonen - Initial contribution
37  */
38 @NonNullByDefault
39 public class DynamoDBQueryUtils {
40     /**
41      * Construct dynamodb query from filter
42      *
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
49      */
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");
54         }
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");
60         }
61         addFilterbyItemAndTimeFilter(queryBuilder, expectedTableSchema, itemName, filter);
62         addStateFilter(queryBuilder, expectedTableSchema, item, dtoClass, filter);
63         addProjection(dtoClass, expectedTableSchema, queryBuilder);
64         return queryBuilder.build();
65     }
66
67     /**
68      * Add projection for key parameters only, not expire date
69      */
70     private static void addProjection(Class<? extends DynamoDBItem<?>> dtoClass,
71             ExpectedTableSchema expectedTableSchema, QueryEnhancedRequest.Builder queryBuilder) {
72         boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
73         if (legacy) {
74             queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME_LEGACY,
75                     DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC_LEGACY, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY);
76         } else {
77             acceptAsEmptyDTO(dtoClass, new DynamoDBItemVisitor<@Nullable Void>() {
78                 @Override
79                 public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
80                     queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
81                             DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
82                     return null;
83                 }
84
85                 @Override
86                 public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
87                     queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
88                             DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
89                     return null;
90                 }
91             });
92         }
93     }
94
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>() {
103             @Override
104             public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
105                 itemStateTypeExpressionBuilder.putExpressionName("#attr",
106                         legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
107                                 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
108                 return null;
109             }
110
111             @Override
112             public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
113                 itemStateTypeExpressionBuilder.putExpressionName("#attr",
114                         legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
115                                 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
116                 return null;
117             }
118         });
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(),
126                     filter.getState());
127             acceptAsDTO(stateToFind, legacy, new DynamoDBItemVisitor<@Nullable Void>() {
128                 @Override
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());
135                     return null;
136                 }
137
138                 @SuppressWarnings("null")
139                 @Override
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());
146                     return null;
147                 }
148             });
149             expression = Expression.join(stateFilterExpressionBuilder.build(), itemStateTypeExpressionBuilder.build(),
150                     "AND");
151
152             queryBuilder.filterExpression(expression);
153         } else {
154             expression = itemStateTypeExpressionBuilder.build();
155         }
156         queryBuilder.filterExpression(expression);
157     }
158
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;
164
165         AttributeConverter<ZonedDateTime> timeConverter = AbstractDynamoDBItem.getTimestampConverter(legacy);
166
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))));
180         }
181     }
182
183     /**
184      * Convert op to string suitable for dynamodb filter expression
185      *
186      * @param op
187      * @return string representation corresponding to the given the Operator
188      */
189     private static String operatorAsString(Operator op) {
190         switch (op) {
191             case EQ:
192                 return "=";
193             case NEQ:
194                 return "<>";
195             case LT:
196                 return "<";
197             case LTE:
198                 return "<=";
199             case GT:
200                 return ">";
201             case GTE:
202                 return ">=";
203
204             default:
205                 throw new IllegalStateException("Unknown operator " + op);
206         }
207     }
208
209     private static <T> void acceptAsDTO(Item item, boolean legacy, DynamoDBItemVisitor<T> visitor) {
210         ZonedDateTime dummyTimestamp = ZonedDateTime.now();
211         if (legacy) {
212             AbstractDynamoDBItem.fromStateLegacy(item, dummyTimestamp).accept(visitor);
213         } else {
214             AbstractDynamoDBItem.fromStateNew(item, dummyTimestamp, null).accept(visitor);
215         }
216     }
217
218     private static <T> void acceptAsEmptyDTO(Class<? extends DynamoDBItem<?>> dtoClass,
219             DynamoDBItemVisitor<T> visitor) {
220         try {
221             dtoClass.getDeclaredConstructor().newInstance().accept(visitor);
222         } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
223                 | NoSuchMethodException | SecurityException e) {
224             throw new IllegalStateException(e);
225         }
226     }
227 }