]> git.basschouten.com Git - openhab-addons.git/blob
7a95b36cf460d9a58a5cfba3326606548b492f8a
[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         addFilterbyItemAndTimeFilter(queryBuilder, expectedTableSchema, filter.getItemName(), filter);
58         addStateFilter(queryBuilder, expectedTableSchema, item, dtoClass, filter);
59         addProjection(dtoClass, expectedTableSchema, queryBuilder);
60         return queryBuilder.build();
61     }
62
63     /**
64      * Add projection for key parameters only, not expire date
65      */
66     private static void addProjection(Class<? extends DynamoDBItem<?>> dtoClass,
67             ExpectedTableSchema expectedTableSchema, QueryEnhancedRequest.Builder queryBuilder) {
68         boolean legacy = expectedTableSchema == ExpectedTableSchema.LEGACY;
69         if (legacy) {
70             queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME_LEGACY,
71                     DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC_LEGACY, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY);
72         } else {
73             acceptAsEmptyDTO(dtoClass, new DynamoDBItemVisitor<@Nullable Void>() {
74                 @Override
75                 public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
76                     queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
77                             DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
78                     return null;
79                 }
80
81                 @Override
82                 public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
83                     queryBuilder.attributesToProject(DynamoDBItem.ATTRIBUTE_NAME_ITEMNAME,
84                             DynamoDBItem.ATTRIBUTE_NAME_TIMEUTC, DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
85                     return null;
86                 }
87             });
88         }
89     }
90
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>() {
99             @Override
100             public @Nullable Void visit(DynamoDBStringItem dynamoStringItem) {
101                 itemStateTypeExpressionBuilder.putExpressionName("#attr",
102                         legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
103                                 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_STRING);
104                 return null;
105             }
106
107             @Override
108             public @Nullable Void visit(DynamoDBBigDecimalItem dynamoBigDecimalItem) {
109                 itemStateTypeExpressionBuilder.putExpressionName("#attr",
110                         legacy ? DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_LEGACY
111                                 : DynamoDBItem.ATTRIBUTE_NAME_ITEMSTATE_NUMBER);
112                 return null;
113             }
114         });
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(),
122                     filter.getState());
123             acceptAsDTO(stateToFind, legacy, new DynamoDBItemVisitor<@Nullable Void>() {
124                 @Override
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());
131                     return null;
132                 }
133
134                 @SuppressWarnings("null")
135                 @Override
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());
142                     return null;
143                 }
144             });
145             expression = Expression.join(stateFilterExpressionBuilder.build(), itemStateTypeExpressionBuilder.build(),
146                     "AND");
147
148             queryBuilder.filterExpression(expression);
149         } else {
150             expression = itemStateTypeExpressionBuilder.build();
151         }
152         queryBuilder.filterExpression(expression);
153     }
154
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;
160
161         AttributeConverter<ZonedDateTime> timeConverter = AbstractDynamoDBItem.getTimestampConverter(legacy);
162
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()))));
172         } else {
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()))));
177         }
178     }
179
180     /**
181      * Convert op to string suitable for dynamodb filter expression
182      *
183      * @param op
184      * @return string representation corresponding to the given the Operator
185      */
186     private static String operatorAsString(Operator op) {
187         switch (op) {
188             case EQ:
189                 return "=";
190             case NEQ:
191                 return "<>";
192             case LT:
193                 return "<";
194             case LTE:
195                 return "<=";
196             case GT:
197                 return ">";
198             case GTE:
199                 return ">=";
200
201             default:
202                 throw new IllegalStateException("Unknown operator " + op);
203         }
204     }
205
206     private static <T> void acceptAsDTO(Item item, boolean legacy, DynamoDBItemVisitor<T> visitor) {
207         ZonedDateTime dummyTimestamp = ZonedDateTime.now();
208         if (legacy) {
209             AbstractDynamoDBItem.fromStateLegacy(item, dummyTimestamp).accept(visitor);
210         } else {
211             AbstractDynamoDBItem.fromStateNew(item, dummyTimestamp, null).accept(visitor);
212         }
213     }
214
215     private static <T> void acceptAsEmptyDTO(Class<? extends DynamoDBItem<?>> dtoClass,
216             DynamoDBItemVisitor<T> visitor) {
217         try {
218             dtoClass.getDeclaredConstructor().newInstance().accept(visitor);
219         } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException
220                 | NoSuchMethodException | SecurityException e) {
221             throw new IllegalStateException(e);
222         }
223     }
224 }