]> git.basschouten.com Git - openhab-addons.git/blob
c28e5a58a483977a73844dce74850c6cc83fd60d
[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.binding.mqtt.homeassistant.internal.config;
14
15 import java.io.IOException;
16 import java.lang.reflect.Field;
17 import java.util.Arrays;
18 import java.util.Objects;
19
20 import org.eclipse.jdt.annotation.NonNullByDefault;
21 import org.eclipse.jdt.annotation.Nullable;
22 import org.openhab.binding.mqtt.homeassistant.internal.MappingJsonReader;
23 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.AbstractChannelConfiguration;
24 import org.openhab.binding.mqtt.homeassistant.internal.config.dto.Device;
25
26 import com.google.gson.Gson;
27 import com.google.gson.TypeAdapter;
28 import com.google.gson.TypeAdapterFactory;
29 import com.google.gson.annotations.SerializedName;
30 import com.google.gson.reflect.TypeToken;
31 import com.google.gson.stream.JsonReader;
32 import com.google.gson.stream.JsonWriter;
33
34 /**
35  * This a Gson type adapter factory.
36  *
37  * <p>
38  * It will create a type adapter for every class derived from {@link
39  * AbstractChannelConfiguration} and ensures,
40  * that abbreviated names are replaces with their long versions during the read.
41  *
42  * <p>
43  * In elements, whose JSON name end in'_topic' '~' replacement is performed.
44  *
45  * <p>
46  * The adapters also handle {@link Device}
47  *
48  * @author Jochen Klein - Initial contribution
49  */
50 @NonNullByDefault
51 public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactory {
52     private static final String MQTT_TOPIC_FIELD_SUFFIX = "_topic";
53
54     @Override
55     @Nullable
56     public <T> TypeAdapter<T> create(@Nullable Gson gson, @Nullable TypeToken<T> type) {
57         if (gson == null || type == null) {
58             return null;
59         }
60         if (AbstractChannelConfiguration.class.isAssignableFrom(type.getRawType())) {
61             return createHAConfig(gson, type);
62         }
63         if (Device.class.isAssignableFrom(type.getRawType())) {
64             return createHADevice(gson, type);
65         }
66         return null;
67     }
68
69     /**
70      * Handle {@link
71      * AbstractChannelConfiguration}
72      *
73      * @param gson parser
74      * @param type type
75      * @return adapter
76      */
77     private <T> TypeAdapter<T> createHAConfig(Gson gson, TypeToken<T> type) {
78         /* The delegate is the 'default' adapter */
79         final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
80
81         return new TypeAdapter<T>() {
82             @Override
83             public @Nullable T read(JsonReader in) throws IOException {
84                 /* read the object using the default adapter, but translate the names in the reader */
85                 T result = delegate.read(MappingJsonReader.getConfigMapper(in));
86                 /* do the '~' expansion afterwards */
87                 expandTidleInTopics(AbstractChannelConfiguration.class.cast(result));
88                 return result;
89             }
90
91             @Override
92             public void write(JsonWriter out, @Nullable T value) throws IOException {
93                 delegate.write(out, value);
94             }
95         };
96     }
97
98     private <T> TypeAdapter<T> createHADevice(Gson gson, TypeToken<T> type) {
99         /* The delegate is the 'default' adapter */
100         final TypeAdapter<T> delegate = gson.getDelegateAdapter(this, type);
101
102         return new TypeAdapter<T>() {
103             @Override
104             public @Nullable T read(JsonReader in) throws IOException {
105                 /* read the object using the default adapter, but translate the names in the reader */
106                 return delegate.read(MappingJsonReader.getDeviceMapper(in));
107             }
108
109             @Override
110             public void write(JsonWriter out, @Nullable T value) throws IOException {
111                 delegate.write(out, value);
112             }
113         };
114     }
115
116     private void expandTidleInTopics(AbstractChannelConfiguration config) {
117         Class<?> type = config.getClass();
118
119         String parentTopic = config.getParentTopic();
120
121         while (type != Object.class) {
122             Objects.requireNonNull(type, "Bug: type is null"); // Should not happen? Making compiler happy
123             Arrays.stream(type.getDeclaredFields()).filter(this::isMqttTopicField)
124                     .forEach(field -> replacePlaceholderByParentTopic(config, field, parentTopic));
125             type = type.getSuperclass();
126         }
127     }
128
129     private boolean isMqttTopicField(Field field) {
130         if (String.class.isAssignableFrom(field.getType())) {
131             final var serializedNameAnnotation = field.getAnnotation(SerializedName.class);
132             if (serializedNameAnnotation != null && serializedNameAnnotation.value() != null
133                     && serializedNameAnnotation.value().endsWith(MQTT_TOPIC_FIELD_SUFFIX)) {
134                 return true;
135             }
136         }
137         return false;
138     }
139
140     private void replacePlaceholderByParentTopic(AbstractChannelConfiguration config, Field field, String parentTopic) {
141         field.setAccessible(true);
142
143         try {
144             final String oldValue = (String) field.get(config);
145
146             String newValue = oldValue;
147             if (oldValue != null && !oldValue.isBlank()) {
148                 if (oldValue.charAt(0) == AbstractChannelConfiguration.PARENT_TOPIC_PLACEHOLDER) {
149                     newValue = parentTopic + oldValue.substring(1);
150                 } else if (oldValue
151                         .charAt(oldValue.length() - 1) == AbstractChannelConfiguration.PARENT_TOPIC_PLACEHOLDER) {
152                     newValue = oldValue.substring(0, oldValue.length() - 1) + parentTopic;
153                 }
154             }
155
156             field.set(config, newValue);
157         } catch (IllegalArgumentException | IllegalAccessException e) {
158             throw new IllegalStateException(e);
159         }
160     }
161 }