]> git.basschouten.com Git - openhab-addons.git/blob
2045f34cdfd6af3d2366147b76f4b7d87fcceb49
[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.http.internal.http;
14
15 import java.net.MalformedURLException;
16 import java.net.URI;
17 import java.net.URISyntaxException;
18 import java.util.Date;
19 import java.util.Map;
20 import java.util.Optional;
21 import java.util.Set;
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;
29
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;
38
39 /**
40  * The {@link RefreshingUrlCache} is responsible for requesting from a single URL and passing the content to the
41  * channels
42  *
43  * @author Jan N. Klug - Initial contribution
44  */
45 @NonNullByDefault
46 public class RefreshingUrlCache {
47     private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
48
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;
61
62     private @Nullable ScheduledFuture<?> future;
63     private @Nullable ChannelHandlerContent lastContent;
64
65     public RefreshingUrlCache(RateLimitedHttpClient httpClient, String url, HttpThingConfig thingConfig,
66             String httpContent, @Nullable String httpContentType, HttpStatusListener httpStatusListener) {
67         this.httpClient = httpClient;
68         this.url = url;
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;
78     }
79
80     public void start(ScheduledExecutorService executor, int refreshTime) {
81         if (future != null) {
82             logger.warn("Starting refresh task requested but it is already started. This is bug.");
83             return;
84         }
85         future = executor.scheduleWithFixedDelay(this::refresh, 1, refreshTime, TimeUnit.SECONDS);
86         logger.trace("Started refresh task for URL '{}' with interval {}s", url, refreshTime);
87     }
88
89     public void stop() {
90         // clearing all listeners to prevent further updates
91         consumers.clear();
92         ScheduledFuture<?> future = this.future;
93         if (future != null) {
94             future.cancel(true);
95             logger.trace("Stopped refresh task for URL '{}'", url);
96         }
97     }
98
99     private void refresh() {
100         refresh(false);
101     }
102
103     private void refresh(boolean isRetry) {
104         if (consumers.isEmpty()) {
105             // do not refresh if we don't have listeners
106             return;
107         }
108
109         // format URL
110         try {
111             URI uri = Util.uriFromString(String.format(this.url, new Date()));
112             logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout);
113
114             httpClient.newRequest(uri, httpMethod, httpContent, httpContentType).thenAccept(request -> {
115                 request.timeout(timeout, TimeUnit.MILLISECONDS);
116                 headers.forEach(request::header);
117
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");
124                         } else {
125                             refresh(true);
126                         }
127                     }
128                     return null;
129                 }).thenAccept(this::processResult);
130
131                 if (logger.isTraceEnabled()) {
132                     logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
133                 }
134
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);
140                 } else {
141                     logger.warn("Request to URL {} failed: {}", uri, e.getMessage());
142                 }
143                 return null;
144             });
145         } catch (IllegalArgumentException | URISyntaxException | MalformedURLException e) {
146             logger.warn("Creating request for '{}' failed: {}", url, e.getMessage());
147         }
148     }
149
150     public void addConsumer(Consumer<@Nullable ChannelHandlerContent> consumer) {
151         consumers.add(consumer);
152     }
153
154     public Optional<ChannelHandlerContent> get() {
155         return Optional.ofNullable(lastContent);
156     }
157
158     private void processResult(@Nullable ChannelHandlerContent content) {
159         if (content != null || strictErrorHandling) {
160             for (Consumer<@Nullable ChannelHandlerContent> consumer : consumers) {
161                 try {
162                     consumer.accept(content);
163                 } catch (IllegalArgumentException | IllegalStateException e) {
164                     logger.warn("Failed processing result for URL {}: {}", url, e.getMessage());
165                 }
166             }
167         }
168         lastContent = content;
169     }
170 }