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.freeboxos.internal.api;
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;
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.api.deserialization.ForegroundAppDeserializer;
36 import org.openhab.binding.freeboxos.internal.api.deserialization.ListDeserializer;
37 import org.openhab.binding.freeboxos.internal.api.deserialization.StrictEnumTypeAdapterFactory;
38 import org.openhab.binding.freeboxos.internal.api.rest.PlayerManager.ForegroundApp;
39 import org.openhab.core.i18n.TimeZoneProvider;
40 import org.slf4j.Logger;
41 import org.slf4j.LoggerFactory;
43 import com.google.gson.FieldNamingPolicy;
44 import com.google.gson.Gson;
45 import com.google.gson.GsonBuilder;
46 import com.google.gson.JsonDeserializer;
48 import inet.ipaddr.IPAddress;
49 import inet.ipaddr.IPAddressString;
50 import inet.ipaddr.MACAddressString;
51 import inet.ipaddr.mac.MACAddress;
54 * The {@link ApiHandler} is responsible for sending requests toward a given url and transform the answer in appropriate
57 * @author Gaƫl L'hopital - Initial contribution
60 public class ApiHandler {
61 public static final String AUTH_HEADER = "X-Fbx-App-Auth";
62 private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
63 private static final String CONTENT_TYPE = "application/json; charset=" + DEFAULT_CHARSET.name();
65 private final Logger logger = LoggerFactory.getLogger(ApiHandler.class);
66 private final HttpClient httpClient;
67 private final Gson gson;
69 private long timeoutInMs = TimeUnit.SECONDS.toMillis(8);
71 public ApiHandler(HttpClient httpClient, TimeZoneProvider timeZoneProvider) {
72 this.httpClient = httpClient;
73 this.gson = new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
74 .registerTypeAdapter(ZonedDateTime.class,
75 (JsonDeserializer<ZonedDateTime>) (json, type, jsonDeserializationContext) -> {
76 long timestamp = json.getAsJsonPrimitive().getAsLong();
77 Instant i = Instant.ofEpochSecond(timestamp);
78 return ZonedDateTime.ofInstant(i, timeZoneProvider.getTimeZone());
80 .registerTypeAdapter(MACAddress.class,
81 (JsonDeserializer<MACAddress>) (json, type,
82 jsonDeserializationContext) -> new MACAddressString(json.getAsString()).getAddress())
83 .registerTypeAdapter(IPAddress.class,
84 (JsonDeserializer<IPAddress>) (json, type,
85 jsonDeserializationContext) -> new IPAddressString(json.getAsString()).getAddress())
86 .registerTypeAdapter(ForegroundApp.class, new ForegroundAppDeserializer())
87 .registerTypeAdapter(List.class, new ListDeserializer()).serializeNulls()
88 .registerTypeAdapterFactory(new StrictEnumTypeAdapterFactory()).create();
91 public synchronized <T> T executeUri(URI uri, HttpMethod method, Class<T> clazz, @Nullable String sessionToken,
92 @Nullable Object payload) throws FreeboxException, InterruptedException {
93 logger.debug("executeUrl {}: {} ", method, uri);
95 Request request = httpClient.newRequest(uri).method(method).timeout(timeoutInMs, TimeUnit.MILLISECONDS)
96 .header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE);
98 if (sessionToken != null) {
99 request.header(AUTH_HEADER, sessionToken);
102 if (payload != null) {
103 request.content(new StringContentProvider(serialize(payload), DEFAULT_CHARSET), null);
107 ContentResponse response = request.send();
109 Code statusCode = HttpStatus.getCode(response.getStatus());
111 if (statusCode != Code.OK && statusCode != Code.FORBIDDEN) {
112 throw new FreeboxException(statusCode.getMessage());
115 String content = new String(response.getContent(), DEFAULT_CHARSET);
116 T result = deserialize(clazz, content);
117 logger.trace("executeUrl {} - {} returned {}", method, uri, content);
119 if (statusCode == Code.OK) {
121 } else if (statusCode == Code.FORBIDDEN) {
122 logger.debug("Fobidden, serviceReponse was {}, ", content);
123 if (result instanceof Response<?> errorResponse) {
124 if (errorResponse.getErrorCode() == Response.ErrorCode.INSUFFICIENT_RIGHTS) {
125 throw new PermissionException(errorResponse.getMissingRight(), errorResponse.getMsg());
127 throw new FreeboxException(errorResponse.getErrorCode(), errorResponse.getMsg());
132 throw new FreeboxException("Error '%s' requesting: %s", statusCode.getMessage(), uri.toString());
133 } catch (TimeoutException | ExecutionException e) {
134 throw new FreeboxException(e, "Exception while calling %s", request.getURI());
138 public <T> T deserialize(Class<T> clazz, String json) {
140 T result = gson.fromJson(json, clazz);
141 if (result != null) {
144 throw new IllegalArgumentException("Null result deserializing '%s', please file a bug report.".formatted(json));
147 public String serialize(Object payload) {
148 return gson.toJson(payload);
151 public HttpClient getHttpClient() {
155 public void setTimeout(long millis) {
156 timeoutInMs = millis;
157 logger.debug("Timeout set to {} ms", timeoutInMs);