2 * Copyright (c) 2010-2023 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.mqtt.homeassistant.internal.config;
15 import java.io.IOException;
16 import java.lang.reflect.Field;
17 import java.util.Arrays;
18 import java.util.Objects;
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;
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;
35 * This a Gson type adapter factory.
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.
43 * In elements, whose JSON name end in'_topic' '~' replacement is performed.
46 * The adapters also handle {@link Device}
48 * @author Jochen Klein - Initial contribution
51 public class ChannelConfigurationTypeAdapterFactory implements TypeAdapterFactory {
52 private static final String MQTT_TOPIC_FIELD_SUFFIX = "_topic";
56 public <T> TypeAdapter<T> create(@Nullable Gson gson, @Nullable TypeToken<T> type) {
57 if (gson == null || type == null) {
60 if (AbstractChannelConfiguration.class.isAssignableFrom(type.getRawType())) {
61 return createHAConfig(gson, type);
63 if (Device.class.isAssignableFrom(type.getRawType())) {
64 return createHADevice(gson, type);
71 * AbstractChannelConfiguration}
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);
81 return new TypeAdapter<T>() {
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));
92 public void write(JsonWriter out, @Nullable T value) throws IOException {
93 delegate.write(out, value);
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);
102 return new TypeAdapter<T>() {
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 T result = delegate.read(MappingJsonReader.getDeviceMapper(in));
111 public void write(JsonWriter out, @Nullable T value) throws IOException {
112 delegate.write(out, value);
117 private void expandTidleInTopics(AbstractChannelConfiguration config) {
118 Class<?> type = config.getClass();
120 String parentTopic = config.getParentTopic();
122 while (type != Object.class) {
123 Objects.requireNonNull(type, "Bug: type is null"); // Should not happen? Making compiler happy
124 Arrays.stream(type.getDeclaredFields()).filter(this::isMqttTopicField)
125 .forEach(field -> replacePlaceholderByParentTopic(config, field, parentTopic));
126 type = type.getSuperclass();
130 private boolean isMqttTopicField(Field field) {
131 if (String.class.isAssignableFrom(field.getType())) {
132 final var serializedNameAnnotation = field.getAnnotation(SerializedName.class);
133 if (serializedNameAnnotation != null && serializedNameAnnotation.value() != null
134 && serializedNameAnnotation.value().endsWith(MQTT_TOPIC_FIELD_SUFFIX)) {
141 private void replacePlaceholderByParentTopic(AbstractChannelConfiguration config, Field field, String parentTopic) {
142 field.setAccessible(true);
145 final String oldValue = (String) field.get(config);
147 String newValue = oldValue;
148 if (oldValue != null && !oldValue.isBlank()) {
149 if (oldValue.charAt(0) == AbstractChannelConfiguration.PARENT_TOPIC_PLACEHOLDER) {
150 newValue = parentTopic + oldValue.substring(1);
152 .charAt(oldValue.length() - 1) == AbstractChannelConfiguration.PARENT_TOPIC_PLACEHOLDER) {
153 newValue = oldValue.substring(0, oldValue.length() - 1) + parentTopic;
157 field.set(config, newValue);
158 } catch (IllegalArgumentException | IllegalAccessException e) {
159 throw new IllegalStateException(e);