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.http.internal.http;
15 import java.net.MalformedURLException;
17 import java.net.URISyntaxException;
18 import java.util.Date;
19 import java.util.List;
20 import java.util.Optional;
22 import java.util.concurrent.CancellationException;
23 import java.util.concurrent.CompletableFuture;
24 import java.util.concurrent.ConcurrentHashMap;
25 import java.util.concurrent.ScheduledExecutorService;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
28 import java.util.function.Consumer;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.api.Authentication;
33 import org.eclipse.jetty.client.api.AuthenticationStore;
34 import org.eclipse.jetty.http.HttpMethod;
35 import org.openhab.binding.http.internal.Util;
36 import org.openhab.binding.http.internal.config.HttpThingConfig;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
41 * The {@link RefreshingUrlCache} is responsible for requesting from a single URL and passing the content to the
44 * @author Jan N. Klug - Initial contribution
47 public class RefreshingUrlCache {
48 private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
50 private final String url;
51 private final boolean escapedUrl;
52 private final RateLimitedHttpClient httpClient;
53 private final int timeout;
54 private final int bufferSize;
55 private final @Nullable String fallbackEncoding;
56 private final Set<Consumer<Content>> consumers = ConcurrentHashMap.newKeySet();
57 private final List<String> headers;
58 private final HttpMethod httpMethod;
59 private final String httpContent;
61 private final ScheduledFuture<?> future;
62 private @Nullable Content lastContent;
64 public RefreshingUrlCache(ScheduledExecutorService executor, RateLimitedHttpClient httpClient, String url,
65 boolean escapedUrl, HttpThingConfig thingConfig, String httpContent) {
66 this.httpClient = httpClient;
68 this.escapedUrl = escapedUrl;
69 this.timeout = thingConfig.timeout;
70 this.bufferSize = thingConfig.bufferSize;
71 this.headers = thingConfig.headers;
72 this.httpMethod = thingConfig.stateMethod;
73 this.httpContent = httpContent;
74 fallbackEncoding = thingConfig.encoding;
76 future = executor.scheduleWithFixedDelay(this::refresh, 1, thingConfig.refresh, TimeUnit.SECONDS);
77 logger.trace("Started refresh task for URL '{}' with interval {}s", url, thingConfig.refresh);
80 private void refresh() {
84 private void refresh(boolean isRetry) {
85 if (consumers.isEmpty()) {
86 // do not refresh if we don't have listeners
92 String url = String.format(this.url, new Date());
93 URI uri = escapedUrl ? new URI(url) : Util.uriFromString(url);
94 logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout);
96 httpClient.newRequest(uri, httpMethod, httpContent).thenAccept(request -> {
97 request.timeout(timeout, TimeUnit.MILLISECONDS);
99 headers.forEach(header -> {
100 String[] keyValuePair = header.split("=", 2);
101 if (keyValuePair.length == 2) {
102 request.header(keyValuePair[0].trim(), keyValuePair[1].trim());
104 logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
108 CompletableFuture<@Nullable Content> response = new CompletableFuture<>();
109 response.exceptionally(e -> {
110 if (e instanceof HttpAuthException) {
112 logger.warn("Retry after authentication failure failed again for '{}', failing here", uri);
114 AuthenticationStore authStore = httpClient.getAuthenticationStore();
115 Authentication.Result authResult = authStore.findAuthenticationResult(uri);
116 if (authResult != null) {
117 authStore.removeAuthenticationResult(authResult);
118 logger.debug("Cleared authentication result for '{}', retrying immediately", uri);
121 logger.warn("Could not find authentication result for '{}', failing here", uri);
126 }).thenAccept(this::processResult);
128 if (logger.isTraceEnabled()) {
129 logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
132 request.send(new HttpResponseListener(response, fallbackEncoding, bufferSize));
133 }).exceptionally(e -> {
134 if (e instanceof CancellationException) {
135 logger.debug("Request to URL {} was cancelled by thing handler.", uri);
137 logger.warn("Request to URL {} failed: {}", uri, e.getMessage());
141 } catch (IllegalArgumentException | URISyntaxException | MalformedURLException e) {
142 logger.warn("Creating request for '{}' failed: {}", url, e.getMessage());
147 // clearing all listeners to prevent further updates
149 future.cancel(false);
150 logger.trace("Stopped refresh task for URL '{}'", url);
153 public void addConsumer(Consumer<Content> consumer) {
154 consumers.add(consumer);
157 public Optional<Content> get() {
158 final Content content = lastContent;
159 if (content == null) {
160 return Optional.empty();
162 return Optional.of(content);
166 private void processResult(@Nullable Content content) {
167 if (content != null) {
168 for (Consumer<Content> consumer : consumers) {
170 consumer.accept(content);
171 } catch (IllegalArgumentException | IllegalStateException e) {
172 logger.warn("Failed processing result for URL {}: {}", url, e.getMessage());
176 lastContent = content;