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;
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.http.HttpMethod;
33 import org.openhab.binding.http.internal.Util;
34 import org.openhab.binding.http.internal.config.HttpThingConfig;
35 import org.openhab.core.thing.binding.generic.ChannelHandlerContent;
36 import org.slf4j.Logger;
37 import org.slf4j.LoggerFactory;
40 * The {@link RefreshingUrlCache} is responsible for requesting from a single URL and passing the content to the
43 * @author Jan N. Klug - Initial contribution
46 public class RefreshingUrlCache {
47 private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
49 private final String url;
50 private final RateLimitedHttpClient httpClient;
51 private final boolean strictErrorHandling;
52 private final int timeout;
53 private final int bufferSize;
54 private final @Nullable String fallbackEncoding;
55 private final Set<Consumer<@Nullable ChannelHandlerContent>> consumers = ConcurrentHashMap.newKeySet();
56 private final Map<String, String> headers;
57 private final HttpMethod httpMethod;
58 private final String httpContent;
59 private final @Nullable String httpContentType;
60 private final HttpStatusListener httpStatusListener;
62 private @Nullable ScheduledFuture<?> future;
63 private @Nullable ChannelHandlerContent lastContent;
65 public RefreshingUrlCache(RateLimitedHttpClient httpClient, String url, HttpThingConfig thingConfig,
66 String httpContent, @Nullable String httpContentType, HttpStatusListener httpStatusListener) {
67 this.httpClient = httpClient;
69 this.strictErrorHandling = thingConfig.strictErrorHandling;
70 this.timeout = thingConfig.timeout;
71 this.bufferSize = thingConfig.bufferSize;
72 this.httpMethod = thingConfig.stateMethod;
73 this.headers = thingConfig.getHeaders();
74 this.httpContent = httpContent;
75 this.httpContentType = httpContentType;
76 this.httpStatusListener = httpStatusListener;
77 fallbackEncoding = thingConfig.encoding;
80 public void start(ScheduledExecutorService executor, int refreshTime) {
82 logger.warn("Starting refresh task requested but it is already started. This is bug.");
85 future = executor.scheduleWithFixedDelay(this::refresh, 1, refreshTime, TimeUnit.SECONDS);
86 logger.trace("Started refresh task for URL '{}' with interval {}s", url, refreshTime);
90 // clearing all listeners to prevent further updates
92 ScheduledFuture<?> future = this.future;
95 logger.trace("Stopped refresh task for URL '{}'", url);
99 private void refresh() {
103 private void refresh(boolean isRetry) {
104 if (consumers.isEmpty()) {
105 // do not refresh if we don't have listeners
111 URI uri = Util.uriFromString(String.format(this.url, new Date()));
112 logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout);
114 httpClient.newRequest(uri, httpMethod, httpContent, httpContentType).thenAccept(request -> {
115 request.timeout(timeout, TimeUnit.MILLISECONDS);
116 headers.forEach(request::header);
118 CompletableFuture<@Nullable ChannelHandlerContent> responseContentFuture = new CompletableFuture<>();
119 responseContentFuture.exceptionally(t -> {
120 if (t instanceof HttpAuthException) {
121 if (isRetry || !httpClient.reAuth(uri)) {
122 logger.debug("Authentication failed for '{}', retry={}", uri, isRetry);
123 httpStatusListener.onHttpError("Authentication failed");
129 }).thenAccept(this::processResult);
131 if (logger.isTraceEnabled()) {
132 logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
135 request.send(new HttpResponseListener(responseContentFuture, fallbackEncoding, bufferSize,
136 httpStatusListener));
137 }).exceptionally(e -> {
138 if (e instanceof CancellationException) {
139 logger.debug("Request to URL {} was cancelled by thing handler.", uri);
141 logger.warn("Request to URL {} failed: {}", uri, e.getMessage());
145 } catch (IllegalArgumentException | URISyntaxException | MalformedURLException e) {
146 logger.warn("Creating request for '{}' failed: {}", url, e.getMessage());
150 public void addConsumer(Consumer<@Nullable ChannelHandlerContent> consumer) {
151 consumers.add(consumer);
154 public Optional<ChannelHandlerContent> get() {
155 return Optional.ofNullable(lastContent);
158 private void processResult(@Nullable ChannelHandlerContent content) {
159 if (content != null || strictErrorHandling) {
160 for (Consumer<@Nullable ChannelHandlerContent> consumer : consumers) {
162 consumer.accept(content);
163 } catch (IllegalArgumentException | IllegalStateException e) {
164 logger.warn("Failed processing result for URL {}: {}", url, e.getMessage());
168 lastContent = content;