]> git.basschouten.com Git - openhab-addons.git/blob
9fcb2ad5c88eca87c494228a06bac327b596463f
[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.freeboxos.internal.api;
14
15 import java.net.URI;
16 import java.nio.charset.Charset;
17 import java.nio.charset.StandardCharsets;
18 import java.time.Instant;
19 import java.time.ZonedDateTime;
20 import java.util.List;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.eclipse.jetty.client.api.ContentResponse;
29 import org.eclipse.jetty.client.api.Request;
30 import org.eclipse.jetty.client.util.StringContentProvider;
31 import org.eclipse.jetty.http.HttpHeader;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.eclipse.jetty.http.HttpStatus.Code;
35 import org.openhab.binding.freeboxos.internal.FreeboxOsBindingConstants;
36 import org.openhab.binding.freeboxos.internal.api.deserialization.ForegroundAppDeserializer;
37 import org.openhab.binding.freeboxos.internal.api.deserialization.ListDeserializer;
38 import org.openhab.binding.freeboxos.internal.api.deserialization.StrictEnumTypeAdapterFactory;
39 import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.ForegroundApp;
40 import org.openhab.core.i18n.TimeZoneProvider;
41 import org.openhab.core.io.net.http.HttpClientFactory;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 import com.google.gson.FieldNamingPolicy;
46 import com.google.gson.Gson;
47 import com.google.gson.GsonBuilder;
48 import com.google.gson.JsonDeserializer;
49
50 import inet.ipaddr.IPAddress;
51 import inet.ipaddr.IPAddressString;
52 import inet.ipaddr.MACAddressString;
53 import inet.ipaddr.mac.MACAddress;
54
55 /**
56  * The {@link ApiHandler} is responsible for sending requests toward a given url and transform the answer in appropriate
57  * DTO.
58  *
59  * @author GaĆ«l L'hopital - Initial contribution
60  */
61 @NonNullByDefault
62 public class ApiHandler {
63     private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
64     private static final String CONTENT_TYPE = "application/json; charset=" + DEFAULT_CHARSET.name();
65     private static final int RESPONSE_BUFFER_SIZE = 65536;
66     public static final String AUTH_HEADER = "X-Fbx-App-Auth";
67
68     private final Logger logger = LoggerFactory.getLogger(ApiHandler.class);
69     private final HttpClient httpClient;
70     private final Gson gson;
71
72     private long timeoutInMs = TimeUnit.SECONDS.toMillis(8);
73
74     public ApiHandler(HttpClientFactory httpClientFactory, TimeZoneProvider timeZoneProvider) {
75         this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
76                 .registerTypeAdapter(ZonedDateTime.class,
77                         (JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> {
78                             long timestamp = json.getAsJsonPrimitive().getAsLong();
79                             Instant i = Instant.ofEpochSecond(timestamp);
80                             return ZonedDateTime.ofInstant(i, timeZoneProvider.getTimeZone());
81                         })
82                 .registerTypeAdapter(MACAddress.class,
83                         (JsonDeserializer<MACAddress>) (json, type,
84                                 jsonDeserializationContext) -> new MACAddressString(json.getAsString()).getAddress())
85                 .registerTypeAdapter(IPAddress.class,
86                         (JsonDeserializer<IPAddress>) (json, type,
87                                 jsonDeserializationContext) -> new IPAddressString(json.getAsString()).getAddress())
88                 .registerTypeAdapter(ForegroundApp.class, new ForegroundAppDeserializer())
89                 .registerTypeAdapter(List.class, new ListDeserializer()).serializeNulls()
90                 .registerTypeAdapterFactory(new StrictEnumTypeAdapterFactory()).create();
91         httpClient = httpClientFactory.createHttpClient(FreeboxOsBindingConstants.BINDING_ID);
92         httpClient.setResponseBufferSize(RESPONSE_BUFFER_SIZE);
93         try {
94             httpClient.start();
95         } catch (Exception e) {
96             logger.warn("Unable to start httpClient: {}", e.getMessage());
97         }
98     }
99
100     public void dispose() {
101         try {
102             httpClient.stop();
103         } catch (Exception e) {
104             logger.warn("Unable to stop httpClient: {}", e.getMessage());
105         }
106     }
107
108     public synchronized <T> T executeUri(URI uri, HttpMethod method, Class<T> clazz, @Nullable String sessionToken,
109             @Nullable Object payload) throws FreeboxException, InterruptedException {
110         logger.debug("executeUrl {}: {} ", method, uri);
111
112         Request request = httpClient.newRequest(uri).method(method).timeout(timeoutInMs, TimeUnit.MILLISECONDS)
113                 .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE);
114
115         if (sessionToken != null) {
116             request.header(AUTH_HEADER, sessionToken);
117         }
118
119         if (payload != null) {
120             request.content(new StringContentProvider(serialize(payload), DEFAULT_CHARSET), null);
121         }
122
123         try {
124             ContentResponse response = request.send();
125
126             Code statusCode = HttpStatus.getCode(response.getStatus());
127
128             if (statusCode != Code.OK && statusCode != Code.FORBIDDEN) {
129                 throw new FreeboxException(statusCode.getMessage());
130             }
131
132             String content = new String(response.getContent(), DEFAULT_CHARSET);
133             T result = deserialize(clazz, content);
134             logger.trace("executeUrl {} - {} returned {}", method, uri, content);
135
136             if (statusCode == Code.OK) {
137                 return result;
138             } else if (statusCode == Code.FORBIDDEN) {
139                 logger.debug("Fobidden, serviceReponse was {}, ", content);
140                 if (result instanceof Response<?> errorResponse) {
141                     if (errorResponse.getErrorCode() == Response.ErrorCode.INSUFFICIENT_RIGHTS) {
142                         throw new PermissionException(errorResponse.getMissingRight(), errorResponse.getMsg());
143                     } else {
144                         throw new FreeboxException(errorResponse.getErrorCode(), errorResponse.getMsg());
145                     }
146                 }
147             }
148
149             throw new FreeboxException("Error '%s' requesting: %s", statusCode.getMessage(), uri.toString());
150         } catch (TimeoutException | ExecutionException e) {
151             throw new FreeboxException(e, "Exception while calling %s", request.getURI());
152         }
153     }
154
155     public <T> T deserialize(Class<T> clazz, String json) {
156         @Nullable
157         T result = gson.fromJson(json, clazz);
158         if (result != null) {
159             return result;
160         }
161         throw new IllegalArgumentException("Null result deserializing '%s', please file a bug report.".formatted(json));
162     }
163
164     public String serialize(Object payload) {
165         return gson.toJson(payload);
166     }
167
168     public HttpClient getHttpClient() {
169         return httpClient;
170     }
171
172     public void setTimeout(long millis) {
173         timeoutInMs = millis;
174         logger.debug("Timeout set to {} ms", timeoutInMs);
175     }
176 }