]> git.basschouten.com Git - openhab-addons.git/blob
f1a99ad363012d557771f7b7e20c6f6e6b026110
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 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.List;
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.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;
39
40 /**
41  * The {@link RefreshingUrlCache} is responsible for requesting from a single URL and passing the content to the
42  * channels
43  *
44  * @author Jan N. Klug - Initial contribution
45  */
46 @NonNullByDefault
47 public class RefreshingUrlCache {
48     private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
49
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;
60
61     private final ScheduledFuture<?> future;
62     private @Nullable Content lastContent;
63
64     public RefreshingUrlCache(ScheduledExecutorService executor, RateLimitedHttpClient httpClient, String url,
65             boolean escapedUrl, HttpThingConfig thingConfig, String httpContent) {
66         this.httpClient = httpClient;
67         this.url = url;
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;
75
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);
78     }
79
80     private void refresh() {
81         refresh(false);
82     }
83
84     private void refresh(boolean isRetry) {
85         if (consumers.isEmpty()) {
86             // do not refresh if we don't have listeners
87             return;
88         }
89
90         // format URL
91         try {
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);
95
96             httpClient.newRequest(uri, httpMethod, httpContent).thenAccept(request -> {
97                 request.timeout(timeout, TimeUnit.MILLISECONDS);
98
99                 headers.forEach(header -> {
100                     String[] keyValuePair = header.split("=", 2);
101                     if (keyValuePair.length == 2) {
102                         request.header(keyValuePair[0].trim(), keyValuePair[1].trim());
103                     } else {
104                         logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
105                     }
106                 });
107
108                 CompletableFuture<@Nullable Content> response = new CompletableFuture<>();
109                 response.exceptionally(e -> {
110                     if (e instanceof HttpAuthException) {
111                         if (isRetry) {
112                             logger.warn("Retry after authentication failure failed again for '{}', failing here", uri);
113                         } else {
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);
119                                 refresh(true);
120                             } else {
121                                 logger.warn("Could not find authentication result for '{}', failing here", uri);
122                             }
123                         }
124                     }
125                     return null;
126                 }).thenAccept(this::processResult);
127
128                 if (logger.isTraceEnabled()) {
129                     logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
130                 }
131
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);
136                 } else {
137                     logger.warn("Request to URL {} failed: {}", uri, e.getMessage());
138                 }
139                 return null;
140             });
141         } catch (IllegalArgumentException | URISyntaxException | MalformedURLException e) {
142             logger.warn("Creating request for '{}' failed: {}", url, e.getMessage());
143         }
144     }
145
146     public void stop() {
147         // clearing all listeners to prevent further updates
148         consumers.clear();
149         future.cancel(false);
150         logger.trace("Stopped refresh task for URL '{}'", url);
151     }
152
153     public void addConsumer(Consumer<Content> consumer) {
154         consumers.add(consumer);
155     }
156
157     public Optional<Content> get() {
158         final Content content = lastContent;
159         if (content == null) {
160             return Optional.empty();
161         } else {
162             return Optional.of(content);
163         }
164     }
165
166     private void processResult(@Nullable Content content) {
167         if (content != null) {
168             for (Consumer<Content> consumer : consumers) {
169                 try {
170                     consumer.accept(content);
171                 } catch (IllegalArgumentException | IllegalStateException e) {
172                     logger.warn("Failed processing result for URL {}: {}", url, e.getMessage());
173                 }
174             }
175         }
176         lastContent = content;
177     }
178 }