]> git.basschouten.com Git - openhab-addons.git/blob
db9083826cf88b3ea7e07543e9be6ee73fe968cf
[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.binding.salus.internal.rest;
14
15 import static java.lang.Boolean.parseBoolean;
16 import static java.lang.Long.parseLong;
17 import static java.lang.String.format;
18 import static java.util.Collections.unmodifiableSortedMap;
19 import static java.util.Objects.requireNonNull;
20 import static java.util.Optional.empty;
21
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.LinkedHashMap;
25 import java.util.List;
26 import java.util.Map;
27 import java.util.Optional;
28 import java.util.SortedMap;
29 import java.util.TreeMap;
30 import java.util.stream.Collectors;
31
32 import javax.validation.constraints.NotNull;
33
34 import org.checkerframework.checker.units.qual.K;
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
39
40 import com.google.gson.Gson;
41 import com.google.gson.JsonSyntaxException;
42 import com.google.gson.reflect.TypeToken;
43
44 /**
45  * The GsonMapper class is responsible for mapping JSON data to Java objects using the Gson library. It provides methods
46  * for converting JSON strings to various types of objects, such as authentication tokens, devices, device properties,
47  * and error messages.
48  * 
49  * @author Martin GrzeĊ›lowski - Initial contribution
50  */
51 @NonNullByDefault
52 public class GsonMapper {
53     public static final GsonMapper INSTANCE = new GsonMapper();
54     private final Logger logger = LoggerFactory.getLogger(GsonMapper.class);
55     private static final TypeToken<Map<String, Object>> MAP_TYPE_REFERENCE = new TypeToken<>() {
56     };
57     private static final TypeToken<List<Object>> LIST_TYPE_REFERENCE = new TypeToken<>() {
58     };
59     private final Gson gson = new Gson();
60
61     public String loginParam(String username, char[] password) {
62         return gson.toJson(Map.of("user", Map.of("email", username, "password", new String(password))));
63     }
64
65     public AuthToken authToken(String json) {
66         return requireNonNull(gson.fromJson(json, AuthToken.class));
67     }
68
69     public List<Device> parseDevices(String json) {
70         return tryParseBody(json, LIST_TYPE_REFERENCE, List.of()).stream().map(this::parseDevice)
71                 .filter(Optional::isPresent).map(Optional::get).toList();
72     }
73
74     private Optional<Device> parseDevice(Object obj) {
75         if (!(obj instanceof Map<?, ?> firstLevelMap)) {
76             logger.debug("Cannot parse device, because object is not type of map!\n{}", obj);
77             return empty();
78         }
79
80         if (!firstLevelMap.containsKey("device")) {
81             if (logger.isWarnEnabled()) {
82                 var str = firstLevelMap.entrySet().stream()
83                         .map(entry -> format("%s=%s", entry.getKey(), entry.getValue()))
84                         .collect(Collectors.joining("\n"));
85                 logger.debug("Cannot parse device, because firstLevelMap does not have [device] key!\n{}", str);
86             }
87             return empty();
88         }
89         var objLevel2 = firstLevelMap.get("device");
90
91         if (!(objLevel2 instanceof Map<?, ?> map)) {
92             logger.debug("Cannot parse device, because object is not type of map!\n{}", obj);
93             return empty();
94         }
95
96         // parse `dns`
97         if (!map.containsKey("dsn")) {
98             if (logger.isWarnEnabled()) {
99                 var str = map.entrySet().stream().map(entry -> format("%s=%s", entry.getKey(), entry.getValue()))
100                         .collect(Collectors.joining("\n"));
101                 logger.debug("Cannot parse device, because map does not have [dsn] key!\n{}", str);
102             }
103             return empty();
104         }
105         var dsn = requireNonNull((String) map.get("dsn"));
106
107         // parse `name`
108         if (!map.containsKey("product_name")) {
109             if (logger.isWarnEnabled()) {
110                 var str = map.entrySet().stream().map(entry -> format("%s=%s", entry.getKey(), entry.getValue()))
111                         .collect(Collectors.joining("\n"));
112                 logger.debug("Cannot parse device, because map does not have [product_name] key!\n{}", str);
113             }
114             return empty();
115         }
116         var name = requireNonNull((String) map.get("product_name"));
117
118         // parse `properties`
119         var list = map.entrySet().stream().filter(entry -> entry.getKey() != null)
120                 .filter(entry -> !"name".equals(entry.getKey())).filter(entry -> !"base_type".equals(entry.getKey()))
121                 .filter(entry -> !"read_only".equals(entry.getKey()))
122                 .filter(entry -> !"direction".equals(entry.getKey()))
123                 .filter(entry -> !"data_updated_at".equals(entry.getKey()))
124                 .filter(entry -> !"product_name".equals(entry.getKey()))
125                 .filter(entry -> !"display_name".equals(entry.getKey()))
126                 .filter(entry -> !"value".equals(entry.getKey()))
127                 .map(entry -> new Pair<>(requireNonNull(entry.getKey()).toString(), (Object) entry.getValue()))
128                 .toList();
129         Map<@NotNull String, @Nullable Object> properties = new LinkedHashMap<>();
130         for (var entry : list) {
131             properties.put(entry.key, entry.value);
132         }
133         properties = Collections.unmodifiableMap(properties);
134
135         return Optional.of(new Device(dsn.trim(), name.trim(), properties));
136     }
137
138     @SuppressWarnings("SameParameterValue")
139     private <T> T tryParseBody(@Nullable String body, TypeToken<T> typeToken, T defaultValue) {
140         if (body == null) {
141             return defaultValue;
142         }
143         try {
144             return gson.fromJson(body, typeToken);
145         } catch (JsonSyntaxException e) {
146             if (logger.isTraceEnabled()) {
147                 logger.trace("Error when parsing body!\n{}", body, e);
148             } else {
149                 logger.debug("Error when parsing body! Turn on TRACE for more details", e);
150             }
151             return defaultValue;
152         }
153     }
154
155     public List<DeviceProperty<?>> parseDeviceProperties(String json) {
156         var deviceProperties = new ArrayList<DeviceProperty<?>>();
157         var objects = tryParseBody(json, LIST_TYPE_REFERENCE, List.of());
158         for (var obj : objects) {
159             parseDeviceProperty(obj).ifPresent(deviceProperties::add);
160         }
161         return Collections.unmodifiableList(deviceProperties);
162     }
163
164     private Optional<DeviceProperty<?>> parseDeviceProperty(@Nullable Object obj) {
165         if (!(obj instanceof Map<?, ?> firstLevelMap)) {
166             logger.debug("Cannot parse device property, because object is not type of map!\n{}", obj);
167             return empty();
168         }
169
170         if (!firstLevelMap.containsKey("property")) {
171             if (logger.isWarnEnabled()) {
172                 var str = firstLevelMap.entrySet().stream()
173                         .map(entry -> format("%s=%s", entry.getKey(), entry.getValue()))
174                         .collect(Collectors.joining("\n"));
175                 logger.debug("Cannot parse device property, because firstLevelMap does not have [property] key!\n{}",
176                         str);
177             }
178             return empty();
179         }
180
181         var objLevel2 = firstLevelMap.get("property");
182         if (!(objLevel2 instanceof Map<?, ?> map)) {
183             logger.debug("Cannot parse device property, because object is not type of map!\n{}", obj);
184             return empty();
185         }
186
187         // name
188         if (!map.containsKey("name")) {
189             if (logger.isWarnEnabled()) {
190                 var str = map.entrySet().stream().map(entry -> format("%s=%s", entry.getKey(), entry.getValue()))
191                         .collect(Collectors.joining("\n"));
192                 logger.debug("Cannot parse device property, because map does not have [name] key!\n{}", str);
193             }
194             return empty();
195         }
196         var name = requireNonNull((String) map.get("name"));
197
198         // other meaningful properties
199         var baseType = findOrNull(map, "base_type");
200         var readOnly = findBoolOrNull(map, "read_only");
201         var direction = findOrNull(map, "direction");
202         var dataUpdatedAt = findOrNull(map, "data_updated_at");
203         var productName = findOrNull(map, "product_name");
204         var displayName = findOrNull(map, "display_name");
205         var value = findObjectOrNull(map, "value");
206
207         // parse `properties`
208         var list = map.entrySet().stream().filter(entry -> entry.getKey() != null)
209                 .filter(entry -> !"name".equals(entry.getKey())).filter(entry -> !"base_type".equals(entry.getKey()))
210                 .filter(entry -> !"read_only".equals(entry.getKey()))
211                 .filter(entry -> !"direction".equals(entry.getKey()))
212                 .filter(entry -> !"data_updated_at".equals(entry.getKey()))
213                 .filter(entry -> !"product_name".equals(entry.getKey()))
214                 .filter(entry -> !"display_name".equals(entry.getKey()))
215                 .filter(entry -> !"value".equals(entry.getKey()))
216                 .map(entry -> new Pair<>(requireNonNull(entry.getKey()).toString(), (Object) entry.getValue()))
217                 .toList();
218         // this weird thing need to be done,
219         // because `Collectors.toMap` does not support value=null
220         // and in our case, sometimes the values are null
221         SortedMap<@NotNull String, @Nullable Object> properties = new TreeMap<>();
222         for (var entry : list) {
223             properties.put(entry.key, entry.value);
224         }
225         properties = unmodifiableSortedMap(properties);
226
227         return Optional.of(buildDeviceProperty(name, baseType, value, readOnly, direction, dataUpdatedAt, productName,
228                 displayName, properties));
229     }
230
231     private DeviceProperty<?> buildDeviceProperty(String name, @Nullable String baseType, @Nullable Object value,
232             @Nullable Boolean readOnly, @Nullable String direction, @Nullable String dataUpdatedAt,
233             @Nullable String productName, @Nullable String displayName,
234             SortedMap<String, @Nullable Object> properties) {
235         if ("boolean".equalsIgnoreCase(baseType)) {
236             Boolean bool;
237             if (value == null) {
238                 bool = null;
239             } else if (value instanceof Boolean typedValue) {
240                 bool = typedValue;
241             } else if (value instanceof Number typedValue) {
242                 bool = typedValue.longValue() != 0;
243             } else if (value instanceof String typedValue) {
244                 bool = parseBoolean(typedValue);
245             } else {
246                 logger.debug("Cannot parse boolean from [{}]", value);
247                 bool = null;
248             }
249             return new DeviceProperty.BooleanDeviceProperty(name, readOnly, direction, dataUpdatedAt, productName,
250                     displayName, bool, properties);
251         }
252         if ("integer".equalsIgnoreCase(baseType)) {
253             Long longValue;
254             if (value == null) {
255                 longValue = null;
256             } else if (value instanceof Number typedValue) {
257                 longValue = typedValue.longValue();
258             } else if (value instanceof String string) {
259                 try {
260                     longValue = parseLong(string);
261                 } catch (NumberFormatException ex) {
262                     logger.debug("Cannot parse long from [{}]", value, ex);
263                     longValue = null;
264                 }
265             } else {
266                 logger.debug("Cannot parse long from [{}]", value);
267                 longValue = null;
268             }
269             return new DeviceProperty.LongDeviceProperty(name, readOnly, direction, dataUpdatedAt, productName,
270                     displayName, longValue, properties);
271         }
272         var string = value != null ? value.toString() : null;
273         return new DeviceProperty.StringDeviceProperty(name, readOnly, direction, dataUpdatedAt, productName,
274                 displayName, string, properties);
275     }
276
277     @Nullable
278     private String findOrNull(Map<?, ?> map, String name) {
279         if (!map.containsKey(name)) {
280             return null;
281         }
282         return (String) map.get(name);
283     }
284
285     @SuppressWarnings("SameParameterValue")
286     @Nullable
287     private Boolean findBoolOrNull(Map<?, ?> map, String name) {
288         if (!map.containsKey(name)) {
289             return null;
290         }
291         var value = map.get(name);
292         if (value == null) {
293             return null;
294         }
295         if (value instanceof Boolean bool) {
296             return bool;
297         }
298         if (value instanceof String string) {
299             return parseBoolean(string);
300         }
301         return null;
302     }
303
304     @SuppressWarnings("SameParameterValue")
305     @Nullable
306     private Object findObjectOrNull(Map<?, ?> map, String name) {
307         if (!map.containsKey(name)) {
308             return null;
309         }
310         return map.get(name);
311     }
312
313     public String datapointParam(Object value) {
314         return gson.toJson(Map.of("datapoint", Map.of("value", value)));
315     }
316
317     public Optional<Object> datapointValue(@Nullable String json) {
318         if (json == null) {
319             return empty();
320         }
321         var map = tryParseBody(json, MAP_TYPE_REFERENCE, Map.of());
322         if (!map.containsKey("datapoint")) {
323             return empty();
324         }
325         var datapoint = (Map<?, ?>) map.get("datapoint");
326         if (datapoint == null || !datapoint.containsKey("value")) {
327             return empty();
328         }
329         return Optional.ofNullable(datapoint.get("value"));
330     }
331
332     private static record Pair<K, @Nullable V> (K key, @Nullable V value) {
333     }
334 }