]> git.basschouten.com Git - openhab-addons.git/blob
7ee5fcdce1a0c50332d5da64d25575113575f2d6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.*;
16 import java.util.Date;
17 import java.util.List;
18 import java.util.Optional;
19 import java.util.Set;
20 import java.util.concurrent.*;
21 import java.util.function.Consumer;
22
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.eclipse.jetty.client.api.Authentication;
26 import org.eclipse.jetty.client.api.AuthenticationStore;
27 import org.openhab.binding.http.internal.Util;
28 import org.openhab.binding.http.internal.config.HttpThingConfig;
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31
32 /**
33  * The {@link RefreshingUrlCache} is responsible for requesting from a single URL and passing the content to the
34  * channels
35  *
36  * @author Jan N. Klug - Initial contribution
37  */
38 @NonNullByDefault
39 public class RefreshingUrlCache {
40     private final Logger logger = LoggerFactory.getLogger(RefreshingUrlCache.class);
41
42     private final String url;
43     private final RateLimitedHttpClient httpClient;
44     private final int timeout;
45     private final int bufferSize;
46     private final @Nullable String fallbackEncoding;
47     private final Set<Consumer<Content>> consumers = ConcurrentHashMap.newKeySet();
48     private final List<String> headers;
49
50     private final ScheduledFuture<?> future;
51     private @Nullable Content lastContent;
52
53     public RefreshingUrlCache(ScheduledExecutorService executor, RateLimitedHttpClient httpClient, String url,
54             HttpThingConfig thingConfig) {
55         this.httpClient = httpClient;
56         this.url = url;
57         this.timeout = thingConfig.timeout;
58         this.bufferSize = thingConfig.bufferSize;
59         this.headers = thingConfig.headers;
60         fallbackEncoding = thingConfig.encoding;
61
62         future = executor.scheduleWithFixedDelay(this::refresh, 0, thingConfig.refresh, TimeUnit.SECONDS);
63         logger.trace("Started refresh task for URL '{}' with interval {}s", url, thingConfig.refresh);
64     }
65
66     private void refresh() {
67         refresh(false);
68     }
69
70     private void refresh(boolean isRetry) {
71         if (consumers.isEmpty()) {
72             // do not refresh if we don't have listeners
73             return;
74         }
75
76         // format URL
77         try {
78             URI uri = Util.uriFromString(String.format(this.url, new Date()));
79             logger.trace("Requesting refresh (retry={}) from '{}' with timeout {}ms", isRetry, uri, timeout);
80
81             httpClient.newRequest(uri).thenAccept(request -> {
82                 request.timeout(timeout, TimeUnit.MILLISECONDS);
83
84                 headers.forEach(header -> {
85                     String[] keyValuePair = header.split("=", 2);
86                     if (keyValuePair.length == 2) {
87                         request.header(keyValuePair[0].trim(), keyValuePair[1].trim());
88                     } else {
89                         logger.warn("Splitting header '{}' failed. No '=' was found. Ignoring", header);
90                     }
91                 });
92
93                 CompletableFuture<@Nullable Content> response = new CompletableFuture<>();
94                 response.exceptionally(e -> {
95                     if (e instanceof HttpAuthException) {
96                         if (isRetry) {
97                             logger.warn("Retry after authentication  failure failed again for '{}', failing here", uri);
98                         } else {
99                             AuthenticationStore authStore = httpClient.getAuthenticationStore();
100                             Authentication.Result authResult = authStore.findAuthenticationResult(uri);
101                             if (authResult != null) {
102                                 authStore.removeAuthenticationResult(authResult);
103                                 logger.debug("Cleared authentication result for '{}', retrying immediately", uri);
104                                 refresh(true);
105                             } else {
106                                 logger.warn("Could not find authentication result for '{}', failing here", uri);
107                             }
108                         }
109                     }
110                     return null;
111                 }).thenAccept(this::processResult);
112
113                 if (logger.isTraceEnabled()) {
114                     logger.trace("Sending to '{}': {}", uri, Util.requestToLogString(request));
115                 }
116
117                 request.send(new HttpResponseListener(response, fallbackEncoding, bufferSize));
118             }).exceptionally(e -> {
119                 if (e instanceof CancellationException) {
120                     logger.debug("Request to URL {} was cancelled by thing handler.", uri);
121                 } else {
122                     logger.warn("Request to URL {} failed: {}", uri, e.getMessage());
123                 }
124                 return null;
125             });
126         } catch (IllegalArgumentException | URISyntaxException | MalformedURLException e) {
127             logger.warn("Creating request for '{}' failed: {}", url, e.getMessage());
128         }
129     }
130
131     public void stop() {
132         // clearing all listeners to prevent further updates
133         consumers.clear();
134         future.cancel(false);
135         logger.trace("Stopped refresh task for URL '{}'", url);
136     }
137
138     public void addConsumer(Consumer<Content> consumer) {
139         consumers.add(consumer);
140     }
141
142     public Optional<Content> get() {
143         final Content content = lastContent;
144         if (content == null) {
145             return Optional.empty();
146         } else {
147             return Optional.of(content);
148         }
149     }
150
151     private void processResult(@Nullable Content content) {
152         if (content != null) {
153             for (Consumer<Content> consumer : consumers) {
154                 try {
155                     consumer.accept(content);
156                 } catch (IllegalArgumentException | IllegalStateException e) {
157                     logger.warn("Failed processing result for URL {}: {}", url, e.getMessage());
158                 }
159             }
160         }
161         lastContent = content;
162     }
163 }