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