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