2 * Copyright (c) 2010-2024 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.binding.salus.internal.rest;
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;
22 import java.util.ArrayList;
23 import java.util.Collections;
24 import java.util.LinkedHashMap;
25 import java.util.List;
27 import java.util.Optional;
28 import java.util.SortedMap;
29 import java.util.TreeMap;
30 import java.util.stream.Collectors;
32 import javax.validation.constraints.NotNull;
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;
40 import com.google.gson.Gson;
41 import com.google.gson.JsonSyntaxException;
42 import com.google.gson.reflect.TypeToken;
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,
49 * @author Martin GrzeĊlowski - Initial contribution
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<>() {
57 private static final TypeToken<List<Object>> LIST_TYPE_REFERENCE = new TypeToken<>() {
59 private final Gson gson = new Gson();
61 public String loginParam(String username, char[] password) {
62 return gson.toJson(Map.of("user", Map.of("email", username, "password", new String(password))));
65 public AuthToken authToken(String json) {
66 return requireNonNull(gson.fromJson(json, AuthToken.class));
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();
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);
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);
89 var objLevel2 = firstLevelMap.get("device");
91 if (!(objLevel2 instanceof Map<?, ?> map)) {
92 logger.debug("Cannot parse device, because object is not type of map!\n{}", obj);
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);
105 var dsn = requireNonNull((String) map.get("dsn"));
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);
116 var name = requireNonNull((String) map.get("product_name"));
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()))
129 Map<@NotNull String, @Nullable Object> properties = new LinkedHashMap<>();
130 for (var entry : list) {
131 properties.put(entry.key, entry.value);
133 properties = Collections.unmodifiableMap(properties);
135 return Optional.of(new Device(dsn.trim(), name.trim(), properties));
138 @SuppressWarnings("SameParameterValue")
139 private <T> T tryParseBody(@Nullable String body, TypeToken<T> typeToken, T defaultValue) {
144 return gson.fromJson(body, typeToken);
145 } catch (JsonSyntaxException e) {
146 if (logger.isTraceEnabled()) {
147 logger.trace("Error when parsing body!\n{}", body, e);
149 logger.debug("Error when parsing body! Turn on TRACE for more details", e);
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);
161 return Collections.unmodifiableList(deviceProperties);
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);
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{}",
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);
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);
196 var name = requireNonNull((String) map.get("name"));
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");
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()))
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);
225 properties = unmodifiableSortedMap(properties);
227 return Optional.of(buildDeviceProperty(name, baseType, value, readOnly, direction, dataUpdatedAt, productName,
228 displayName, properties));
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)) {
239 } else if (value instanceof Boolean typedValue) {
241 } else if (value instanceof Number typedValue) {
242 bool = typedValue.longValue() != 0;
243 } else if (value instanceof String typedValue) {
244 bool = parseBoolean(typedValue);
246 logger.debug("Cannot parse boolean from [{}]", value);
249 return new DeviceProperty.BooleanDeviceProperty(name, readOnly, direction, dataUpdatedAt, productName,
250 displayName, bool, properties);
252 if ("integer".equalsIgnoreCase(baseType)) {
256 } else if (value instanceof Number typedValue) {
257 longValue = typedValue.longValue();
258 } else if (value instanceof String string) {
260 longValue = parseLong(string);
261 } catch (NumberFormatException ex) {
262 logger.debug("Cannot parse long from [{}]", value, ex);
266 logger.debug("Cannot parse long from [{}]", value);
269 return new DeviceProperty.LongDeviceProperty(name, readOnly, direction, dataUpdatedAt, productName,
270 displayName, longValue, properties);
272 var string = value != null ? value.toString() : null;
273 return new DeviceProperty.StringDeviceProperty(name, readOnly, direction, dataUpdatedAt, productName,
274 displayName, string, properties);
278 private String findOrNull(Map<?, ?> map, String name) {
279 if (!map.containsKey(name)) {
282 return (String) map.get(name);
285 @SuppressWarnings("SameParameterValue")
287 private Boolean findBoolOrNull(Map<?, ?> map, String name) {
288 if (!map.containsKey(name)) {
291 var value = map.get(name);
295 if (value instanceof Boolean bool) {
298 if (value instanceof String string) {
299 return parseBoolean(string);
304 @SuppressWarnings("SameParameterValue")
306 private Object findObjectOrNull(Map<?, ?> map, String name) {
307 if (!map.containsKey(name)) {
310 return map.get(name);
313 public String datapointParam(Object value) {
314 return gson.toJson(Map.of("datapoint", Map.of("value", value)));
317 public Optional<Object> datapointValue(@Nullable String json) {
321 var map = tryParseBody(json, MAP_TYPE_REFERENCE, Map.of());
322 if (!map.containsKey("datapoint")) {
325 var datapoint = (Map<?, ?>) map.get("datapoint");
326 if (datapoint == null || !datapoint.containsKey("value")) {
329 return Optional.ofNullable(datapoint.get("value"));
332 private static record Pair<K, @Nullable V> (K key, @Nullable V value) {