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.remoteopenhab.internal.rest;
15 import java.io.ByteArrayOutputStream;
16 import java.io.EOFException;
17 import java.io.IOException;
18 import java.io.InputStream;
19 import java.nio.charset.StandardCharsets;
20 import java.util.Arrays;
21 import java.util.Base64;
22 import java.util.HashMap;
23 import java.util.List;
25 import java.util.Objects;
26 import java.util.concurrent.CopyOnWriteArrayList;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
31 import javax.net.ssl.HostnameVerifier;
32 import javax.net.ssl.SSLSession;
33 import javax.ws.rs.client.Client;
34 import javax.ws.rs.client.ClientBuilder;
35 import javax.ws.rs.core.HttpHeaders;
36 import javax.ws.rs.sse.InboundSseEvent;
37 import javax.ws.rs.sse.SseEventSource;
39 import org.eclipse.jdt.annotation.NonNullByDefault;
40 import org.eclipse.jdt.annotation.Nullable;
41 import org.eclipse.jetty.client.HttpClient;
42 import org.eclipse.jetty.client.api.ContentResponse;
43 import org.eclipse.jetty.client.api.Request;
44 import org.eclipse.jetty.client.api.Response;
45 import org.eclipse.jetty.client.util.InputStreamResponseListener;
46 import org.eclipse.jetty.client.util.StringContentProvider;
47 import org.eclipse.jetty.http.HttpMethod;
48 import org.eclipse.jetty.http.HttpStatus;
49 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabChannelDescriptionChangedEvent;
50 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabChannelTriggerEvent;
51 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabCommandDescription;
52 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabCommandOptions;
53 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabEvent;
54 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabEventPayload;
55 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabItem;
56 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabRestApi;
57 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabStateDescription;
58 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabStateOptions;
59 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabStatusInfo;
60 import org.openhab.binding.remoteopenhab.internal.data.RemoteopenhabThing;
61 import org.openhab.binding.remoteopenhab.internal.exceptions.RemoteopenhabException;
62 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabItemsDataListener;
63 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabStreamingDataListener;
64 import org.openhab.binding.remoteopenhab.internal.listener.RemoteopenhabThingsDataListener;
65 import org.openhab.core.i18n.TranslationProvider;
66 import org.openhab.core.types.Command;
67 import org.osgi.framework.Bundle;
68 import org.osgi.framework.FrameworkUtil;
69 import org.osgi.service.jaxrs.client.SseEventSourceFactory;
70 import org.slf4j.Logger;
71 import org.slf4j.LoggerFactory;
73 import com.google.gson.Gson;
74 import com.google.gson.JsonSyntaxException;
77 * A client to use the openHAB REST API and to receive/parse events received from the openHAB REST API Server-Sent
80 * @author Laurent Garnier - Initial contribution
83 public class RemoteopenhabRestClient {
85 private static final int REQUEST_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(30);
87 private final Logger logger = LoggerFactory.getLogger(RemoteopenhabRestClient.class);
89 private final ClientBuilder clientBuilder;
90 private final SseEventSourceFactory eventSourceFactory;
91 private final Gson jsonParser;
92 private final TranslationProvider i18nProvider;
93 private final Bundle bundle;
95 private final Object startStopLock = new Object();
96 private final List<RemoteopenhabStreamingDataListener> listeners = new CopyOnWriteArrayList<>();
97 private final List<RemoteopenhabItemsDataListener> itemsListeners = new CopyOnWriteArrayList<>();
98 private final List<RemoteopenhabThingsDataListener> thingsListeners = new CopyOnWriteArrayList<>();
100 private HttpClient httpClient;
101 private @Nullable String restUrl;
102 private @Nullable String restApiVersion;
103 private Map<String, @Nullable String> apiEndPointsUrls = new HashMap<>();
104 private @Nullable String topicNamespace;
105 private boolean authenticateAnyway;
106 private String accessToken;
107 private String credentialToken;
108 private boolean trustedCertificate;
109 private boolean connected;
110 private boolean completed;
112 private @Nullable SseEventSource eventSource;
113 private long lastEventTimestamp;
115 public RemoteopenhabRestClient(final HttpClient httpClient, final ClientBuilder clientBuilder,
116 final SseEventSourceFactory eventSourceFactory, final Gson jsonParser,
117 final TranslationProvider i18nProvider) {
118 this.httpClient = httpClient;
119 this.clientBuilder = clientBuilder;
120 this.eventSourceFactory = eventSourceFactory;
121 this.jsonParser = jsonParser;
122 this.i18nProvider = i18nProvider;
123 this.bundle = FrameworkUtil.getBundle(this.getClass());
124 this.accessToken = "";
125 this.credentialToken = "";
128 public void setHttpClient(HttpClient httpClient) {
129 this.httpClient = httpClient;
132 public String getRestUrl() throws RemoteopenhabException {
133 String url = restUrl;
135 throw new RemoteopenhabException("@text/exception.rest-client-not-setup");
140 public void setRestUrl(String restUrl) {
141 this.restUrl = restUrl;
144 public void setAuthenticationData(boolean authenticateAnyway, String accessToken, String username,
146 this.authenticateAnyway = authenticateAnyway;
147 this.accessToken = accessToken;
148 if (username.isBlank() || password.isBlank()) {
149 this.credentialToken = "";
151 String token = username + ":" + password;
152 this.credentialToken = Base64.getEncoder().encodeToString(token.getBytes(StandardCharsets.UTF_8));
156 public void setTrustedCertificate(boolean trustedCertificate) {
157 this.trustedCertificate = trustedCertificate;
160 public void tryApi() throws RemoteopenhabException {
162 String jsonResponse = executeGetUrl(getRestUrl(), "application/json", false, false);
163 if (jsonResponse.isEmpty()) {
164 throw new RemoteopenhabException("@text/exception.json-response-empty");
166 RemoteopenhabRestApi restApi = jsonParser.fromJson(jsonResponse, RemoteopenhabRestApi.class);
167 restApiVersion = restApi.version;
168 logger.debug("REST API version = {}", restApiVersion);
169 apiEndPointsUrls.clear();
170 for (int i = 0; i < restApi.links.length; i++) {
171 apiEndPointsUrls.put(restApi.links[i].type, restApi.links[i].url);
173 logger.debug("REST API items = {}", apiEndPointsUrls.get("items"));
174 logger.debug("REST API things = {}", apiEndPointsUrls.get("things"));
175 logger.debug("REST API events = {}", apiEndPointsUrls.get("events"));
176 topicNamespace = restApi.runtimeInfo != null ? "openhab" : "smarthome";
177 logger.debug("topic namespace = {}", topicNamespace);
178 } catch (RemoteopenhabException | JsonSyntaxException e) {
179 throw new RemoteopenhabException("@text/exception.root-rest-api-failed", e);
183 public List<RemoteopenhabItem> getRemoteItems(@Nullable String fields) throws RemoteopenhabException {
185 String url = String.format("%s?recursive=false", getRestApiUrl("items"));
186 if (fields != null) {
187 url += "&fields=" + fields;
189 boolean asyncReading = fields == null || Arrays.asList(fields.split(",")).contains("state");
190 String jsonResponse = executeGetUrl(url, "application/json", false, asyncReading);
191 if (jsonResponse.isEmpty()) {
192 throw new RemoteopenhabException("@text/exception.json-response-empty");
194 return Arrays.asList(jsonParser.fromJson(jsonResponse, RemoteopenhabItem[].class));
195 } catch (RemoteopenhabException | JsonSyntaxException e) {
196 throw new RemoteopenhabException("@text/exception.get-list-items-api-failed", e);
200 public String getRemoteItemState(String itemName) throws RemoteopenhabException {
202 String url = String.format("%s/%s/state", getRestApiUrl("items"), itemName);
203 return executeGetUrl(url, "text/plain", false, true);
204 } catch (RemoteopenhabException e) {
205 throw new RemoteopenhabException("@text/get-item-state-api-failed", e, itemName);
209 public void sendCommandToRemoteItem(String itemName, Command command) throws RemoteopenhabException {
211 String url = String.format("%s/%s", getRestApiUrl("items"), itemName);
212 executeUrl(HttpMethod.POST, url, "application/json", command.toFullString(), "text/plain", false, false,
214 } catch (RemoteopenhabException e) {
215 throw new RemoteopenhabException("@text/exception.send-item-command-api-failed", e, itemName);
219 public List<RemoteopenhabThing> getRemoteThings() throws RemoteopenhabException {
221 String jsonResponse = executeGetUrl(getRestApiUrl("things"), "application/json", true, false);
222 if (jsonResponse.isEmpty()) {
223 throw new RemoteopenhabException("@text/exception.json-response-empty");
225 return Arrays.asList(jsonParser.fromJson(jsonResponse, RemoteopenhabThing[].class));
226 } catch (RemoteopenhabException | JsonSyntaxException e) {
227 throw new RemoteopenhabException("@text/exception.get-list-things-api-failed", e);
231 public RemoteopenhabThing getRemoteThing(String uid) throws RemoteopenhabException {
233 String url = String.format("%s/%s", getRestApiUrl("things"), uid);
234 String jsonResponse = executeGetUrl(url, "application/json", true, false);
235 if (jsonResponse.isEmpty()) {
236 throw new RemoteopenhabException("@text/exception.json-response-empty");
238 return Objects.requireNonNull(jsonParser.fromJson(jsonResponse, RemoteopenhabThing.class));
239 } catch (RemoteopenhabException | JsonSyntaxException e) {
240 throw new RemoteopenhabException("@text/exception.get-thing-api-failed", e, uid);
244 public @Nullable String getRestApiVersion() {
245 return restApiVersion;
248 private String getRestApiUrl(String endPoint) throws RemoteopenhabException {
249 String url = apiEndPointsUrls.get(endPoint);
252 if (!url.endsWith("/")) {
260 public String getTopicNamespace() {
261 String namespace = topicNamespace;
262 return namespace != null ? namespace : "openhab";
265 public void start() {
266 synchronized (startStopLock) {
267 logger.debug("Opening EventSource");
269 logger.debug("EventSource started");
273 public void stop(boolean waitingForCompletion) {
274 synchronized (startStopLock) {
275 logger.debug("Closing EventSource");
276 closeEventSource(waitingForCompletion);
277 logger.debug("EventSource stopped");
278 lastEventTimestamp = 0;
282 private SseEventSource createEventSource(String restSseUrl) {
283 String credentialToken = restSseUrl.startsWith("https:") || authenticateAnyway ? this.credentialToken : "";
285 RemoteopenhabStreamingRequestFilter filter;
286 boolean filterRegistered = clientBuilder.getConfiguration()
287 .isRegistered(RemoteopenhabStreamingRequestFilter.class);
288 if (filterRegistered) {
289 filter = clientBuilder.getConfiguration().getInstances().stream()
290 .filter(instance -> instance instanceof RemoteopenhabStreamingRequestFilter)
291 .map(instance -> (RemoteopenhabStreamingRequestFilter) instance).findAny().orElseThrow();
293 filter = new RemoteopenhabStreamingRequestFilter();
295 filter.setCredentialToken(restSseUrl, credentialToken);
298 // Avoid a timeout exception after 1 minute by setting the read timeout to 0 (infinite)
299 if (trustedCertificate) {
300 HostnameVerifier alwaysValidHostname = new HostnameVerifier() {
302 public boolean verify(@Nullable String hostname, @Nullable SSLSession session) {
306 if (filterRegistered) {
307 client = clientBuilder.sslContext(httpClient.getSslContextFactory().getSslContext())
308 .hostnameVerifier(alwaysValidHostname).readTimeout(0, TimeUnit.SECONDS).build();
310 client = clientBuilder.sslContext(httpClient.getSslContextFactory().getSslContext())
311 .hostnameVerifier(alwaysValidHostname).readTimeout(0, TimeUnit.SECONDS).register(filter)
315 if (filterRegistered) {
316 client = clientBuilder.readTimeout(0, TimeUnit.SECONDS).build();
318 client = clientBuilder.readTimeout(0, TimeUnit.SECONDS).register(filter).build();
322 SseEventSource eventSource = eventSourceFactory.newSource(client.target(restSseUrl));
323 eventSource.register(this::onEvent, this::onError, this::onComplete);
327 private void reopenEventSource() {
328 logger.debug("Reopening EventSource");
333 "%s?topics=%s/items/*/*,%s/things/*/*,%s/channels/*/triggered,openhab/channels/*/descriptionchanged",
334 getRestApiUrl("events"), getTopicNamespace(), getTopicNamespace(), getTopicNamespace());
335 } catch (RemoteopenhabException e) {
336 logger.debug("reopenEventSource failed: {}", e.getMessage(bundle, i18nProvider));
340 closeEventSource(true);
342 logger.debug("Opening new EventSource {}", url);
343 SseEventSource localEventSource = createEventSource(url);
344 localEventSource.open();
346 eventSource = localEventSource;
349 private void closeEventSource(boolean waitingForCompletion) {
350 SseEventSource localEventSource = eventSource;
351 if (localEventSource != null) {
352 if (!localEventSource.isOpen() || completed) {
353 logger.debug("Existing EventSource is already closed");
354 } else if (localEventSource.close(waitingForCompletion ? 10 : 0, TimeUnit.SECONDS)) {
355 logger.debug("Succesfully closed existing EventSource");
357 logger.debug("Failed to close existing EventSource");
364 public boolean addStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
365 return listeners.add(listener);
368 public boolean removeStreamingDataListener(RemoteopenhabStreamingDataListener listener) {
369 return listeners.remove(listener);
372 public boolean addItemsDataListener(RemoteopenhabItemsDataListener listener) {
373 return itemsListeners.add(listener);
376 public boolean removeItemsDataListener(RemoteopenhabItemsDataListener listener) {
377 return itemsListeners.remove(listener);
380 public boolean addThingsDataListener(RemoteopenhabThingsDataListener listener) {
381 return thingsListeners.add(listener);
384 public boolean removeThingsDataListener(RemoteopenhabThingsDataListener listener) {
385 return thingsListeners.remove(listener);
388 public long getLastEventTimestamp() {
389 return lastEventTimestamp;
392 private void onEvent(InboundSseEvent inboundEvent) {
393 String name = inboundEvent.getName();
394 String data = inboundEvent.readData();
395 logger.trace("Received event name {} data {}", name, data);
397 lastEventTimestamp = System.currentTimeMillis();
399 logger.debug("Connected to streaming events");
401 listeners.forEach(listener -> listener.onConnected());
403 if (!"message".equals(name)) {
404 // Ignore silently all events which are not "message" events. This includes the "alive" events.
409 RemoteopenhabEvent event = jsonParser.fromJson(data, RemoteopenhabEvent.class);
412 RemoteopenhabEventPayload payload;
413 RemoteopenhabItem item;
414 RemoteopenhabThing thing;
415 switch (event.type) {
416 case "ItemStateEvent":
417 itemName = extractItemNameFromTopic(event.topic, event.type, "state");
418 payload = jsonParser.fromJson(event.payload, RemoteopenhabEventPayload.class);
419 itemsListeners.forEach(
420 listener -> listener.onItemStateEvent(itemName, payload.type, payload.value, false));
422 case "ItemStateChangedEvent":
423 itemName = extractItemNameFromTopic(event.topic, event.type, "statechanged");
424 payload = jsonParser.fromJson(event.payload, RemoteopenhabEventPayload.class);
425 itemsListeners.forEach(
426 listener -> listener.onItemStateEvent(itemName, payload.type, payload.value, true));
428 case "GroupItemStateChangedEvent":
429 itemName = extractItemNameFromTopic(event.topic, event.type, "statechanged");
430 payload = jsonParser.fromJson(event.payload, RemoteopenhabEventPayload.class);
431 itemsListeners.forEach(
432 listener -> listener.onItemStateEvent(itemName, payload.type, payload.value, false));
434 case "ItemAddedEvent":
435 itemName = extractItemNameFromTopic(event.topic, event.type, "added");
436 item = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabItem.class));
437 itemsListeners.forEach(listener -> listener.onItemAdded(item));
439 case "ItemRemovedEvent":
440 itemName = extractItemNameFromTopic(event.topic, event.type, "removed");
441 item = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabItem.class));
442 itemsListeners.forEach(listener -> listener.onItemRemoved(item));
444 case "ItemUpdatedEvent":
445 itemName = extractItemNameFromTopic(event.topic, event.type, "updated");
446 RemoteopenhabItem[] updItem = jsonParser.fromJson(event.payload, RemoteopenhabItem[].class);
447 if (updItem.length == 2) {
448 itemsListeners.forEach(listener -> listener.onItemUpdated(updItem[0], updItem[1]));
450 logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
453 case "ThingStatusInfoChangedEvent":
454 thingUID = extractThingUIDFromTopic(event.topic, event.type, "statuschanged");
455 RemoteopenhabStatusInfo[] updStatus = jsonParser.fromJson(event.payload,
456 RemoteopenhabStatusInfo[].class);
457 if (updStatus.length == 2) {
458 thingsListeners.forEach(listener -> listener.onThingStatusUpdated(thingUID, updStatus[0]));
460 logger.debug("Invalid payload for event type {} for topic {}", event.type, event.topic);
463 case "ThingAddedEvent":
464 thingUID = extractThingUIDFromTopic(event.topic, event.type, "added");
465 thing = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabThing.class));
466 thingsListeners.forEach(listener -> listener.onThingAdded(thing));
468 case "ThingRemovedEvent":
469 thingUID = extractThingUIDFromTopic(event.topic, event.type, "removed");
470 thing = Objects.requireNonNull(jsonParser.fromJson(event.payload, RemoteopenhabThing.class));
471 thingsListeners.forEach(listener -> listener.onThingRemoved(thing));
473 case "ChannelTriggeredEvent":
474 RemoteopenhabChannelTriggerEvent triggerEvent = jsonParser.fromJson(event.payload,
475 RemoteopenhabChannelTriggerEvent.class);
477 .forEach(listener -> listener.onChannelTriggered(triggerEvent.channel, triggerEvent.event));
479 case "ChannelDescriptionChangedEvent":
480 RemoteopenhabStateDescription stateDescription = new RemoteopenhabStateDescription();
481 RemoteopenhabCommandDescription commandDescription = new RemoteopenhabCommandDescription();
482 RemoteopenhabChannelDescriptionChangedEvent descriptionChanged = Objects.requireNonNull(
483 jsonParser.fromJson(event.payload, RemoteopenhabChannelDescriptionChangedEvent.class));
484 switch (descriptionChanged.field) {
485 case "STATE_OPTIONS":
486 RemoteopenhabStateOptions stateOptions = Objects.requireNonNull(
487 jsonParser.fromJson(descriptionChanged.value, RemoteopenhabStateOptions.class));
488 stateDescription.options = stateOptions.options;
490 case "COMMAND_OPTIONS":
491 RemoteopenhabCommandOptions commandOptions = Objects.requireNonNull(
492 jsonParser.fromJson(descriptionChanged.value, RemoteopenhabCommandOptions.class));
493 commandDescription.commandOptions = commandOptions.options;
498 if (stateDescription.options != null || commandDescription.commandOptions != null) {
499 descriptionChanged.linkedItemNames.forEach(linkedItemName -> {
500 RemoteopenhabItem item1 = new RemoteopenhabItem();
501 item1.name = linkedItemName;
502 item1.stateDescription = stateDescription;
503 item1.commandDescription = commandDescription;
504 itemsListeners.forEach(listener -> listener.onItemOptionsUpdatedd(item1));
508 case "ItemStatePredictedEvent":
509 case "ItemCommandEvent":
510 case "ThingStatusInfoEvent":
511 case "ThingUpdatedEvent":
512 logger.trace("Ignored event type {} for topic {}", event.type, event.topic);
515 logger.debug("Unexpected event type {} for topic {}", event.type, event.topic);
518 } catch (RemoteopenhabException | JsonSyntaxException e) {
519 logger.debug("An exception occurred while processing the inbound '{}' event containg data: {}", name, data,
524 private void onComplete() {
525 logger.debug("Disconnected from streaming events");
527 listeners.forEach(listener -> listener.onDisconnected());
530 private void onError(Throwable error) {
531 logger.debug("Error occurred while receiving events", error);
532 listeners.forEach(listener -> listener.onError("Error occurred while receiving events"));
535 private String extractItemNameFromTopic(String topic, String eventType, String finalPart)
536 throws RemoteopenhabException {
537 String[] parts = topic.split("/");
538 int expectedNbParts = "GroupItemStateChangedEvent".equals(eventType) ? 5 : 4;
539 if (parts.length != expectedNbParts || !getTopicNamespace().equals(parts[0]) || !"items".equals(parts[1])
540 || !finalPart.equals(parts[parts.length - 1])) {
541 throw new RemoteopenhabException("@text/exception.invalid-event-topic", topic, eventType);
546 private String extractThingUIDFromTopic(String topic, String eventType, String finalPart)
547 throws RemoteopenhabException {
548 String[] parts = topic.split("/");
549 int expectedNbParts = 4;
550 if (parts.length != expectedNbParts || !getTopicNamespace().equals(parts[0]) || !"things".equals(parts[1])
551 || !finalPart.equals(parts[parts.length - 1])) {
552 throw new RemoteopenhabException("@text/exception.invalid-event-topic", topic, eventType);
557 public String executeGetUrl(String url, String acceptHeader, boolean provideAccessToken, boolean asyncReading)
558 throws RemoteopenhabException {
559 return executeUrl(HttpMethod.GET, url, acceptHeader, null, null, provideAccessToken, asyncReading, true);
562 public String executeUrl(HttpMethod httpMethod, String url, String acceptHeader, @Nullable String content,
563 @Nullable String contentType, boolean provideAccessToken, boolean asyncReading, boolean retryIfEOF)
564 throws RemoteopenhabException {
565 final Request request = httpClient.newRequest(url).method(httpMethod)
566 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).followRedirects(false)
567 .header(HttpHeaders.ACCEPT, acceptHeader);
569 if (url.startsWith("https:") || authenticateAnyway) {
570 boolean useAlternativeHeader = false;
571 if (!credentialToken.isEmpty()) {
572 request.header(HttpHeaders.AUTHORIZATION, "Basic " + credentialToken);
573 useAlternativeHeader = true;
575 if (provideAccessToken && !accessToken.isEmpty()) {
576 if (useAlternativeHeader) {
577 request.header("X-OPENHAB-TOKEN", accessToken);
579 request.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
584 if (content != null && (HttpMethod.POST.equals(httpMethod) || HttpMethod.PUT.equals(httpMethod))
585 && contentType != null) {
586 request.content(new StringContentProvider(content), contentType);
589 logger.debug("Request {} {}", request.getMethod(), request.getURI());
593 InputStreamResponseListener listener = new InputStreamResponseListener();
594 request.send(listener);
595 Response response = listener.get(5, TimeUnit.SECONDS);
596 int statusCode = response.getStatus();
597 if (statusCode != HttpStatus.OK_200) {
598 response.abort(new Exception(response.getReason()));
599 String statusLine = statusCode + " " + response.getReason();
600 throw new RemoteopenhabException("@text/exception.http-call-failed", statusLine);
602 ByteArrayOutputStream responseContent = new ByteArrayOutputStream();
603 try (InputStream input = listener.getInputStream()) {
604 input.transferTo(responseContent);
606 return new String(responseContent.toByteArray(), StandardCharsets.UTF_8.name());
608 ContentResponse response = request.send();
609 int statusCode = response.getStatus();
610 if (statusCode == HttpStatus.MOVED_PERMANENTLY_301 || statusCode == HttpStatus.FOUND_302) {
611 String locationHeader = response.getHeaders().get(HttpHeaders.LOCATION);
612 if (locationHeader != null && !locationHeader.isBlank()) {
613 logger.debug("The remopte server redirected the request to this URL: {}", locationHeader);
614 return executeUrl(httpMethod, locationHeader, acceptHeader, content, contentType,
615 provideAccessToken, asyncReading, retryIfEOF);
617 String statusLine = statusCode + " " + response.getReason();
618 throw new RemoteopenhabException("@text/exception.http-call-failed", statusLine);
620 } else if (statusCode >= HttpStatus.BAD_REQUEST_400) {
621 String statusLine = statusCode + " " + response.getReason();
622 throw new RemoteopenhabException("@text/exception.http-call-failed", statusLine);
624 String encoding = response.getEncoding() != null ? response.getEncoding().replace("\"", "").trim()
625 : StandardCharsets.UTF_8.name();
626 return new String(response.getContent(), encoding);
628 } catch (ExecutionException e) {
629 // After a long network outage, the first HTTP request will fail with an EOFException exception.
630 // We retry the request a second time in this case.
631 Throwable cause = e.getCause();
632 if (retryIfEOF && cause instanceof EOFException) {
633 logger.debug("EOFException - retry the request");
634 return executeUrl(httpMethod, url, acceptHeader, content, contentType, provideAccessToken, asyncReading,
637 throw new RemoteopenhabException(e);
639 } catch (IOException | TimeoutException e) {
640 throw new RemoteopenhabException(e);
641 } catch (InterruptedException e) {
642 Thread.currentThread().interrupt();
643 throw new RemoteopenhabException(e);