]> git.basschouten.com Git - openhab-addons.git/blob
01da6f07671c539c249656b2f76ce74818436031
[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.plugwiseha.internal.api.model;
14
15 import java.io.StringReader;
16 import java.io.StringWriter;
17 import java.net.ConnectException;
18 import java.net.SocketTimeoutException;
19 import java.net.UnknownHostException;
20 import java.nio.ByteBuffer;
21 import java.nio.charset.StandardCharsets;
22 import java.util.Base64;
23 import java.util.HashMap;
24 import java.util.Iterator;
25 import java.util.Map;
26 import java.util.Map.Entry;
27 import java.util.concurrent.ExecutionException;
28 import java.util.concurrent.TimeUnit;
29 import java.util.concurrent.TimeoutException;
30 import java.util.stream.Collectors;
31
32 import javax.xml.transform.Transformer;
33 import javax.xml.transform.TransformerException;
34 import javax.xml.transform.stream.StreamResult;
35 import javax.xml.transform.stream.StreamSource;
36
37 import org.eclipse.jdt.annotation.NonNullByDefault;
38 import org.eclipse.jdt.annotation.Nullable;
39 import org.eclipse.jetty.client.HttpClient;
40 import org.eclipse.jetty.client.api.ContentProvider;
41 import org.eclipse.jetty.client.api.ContentResponse;
42 import org.eclipse.jetty.client.api.Request;
43 import org.eclipse.jetty.client.util.StringContentProvider;
44 import org.eclipse.jetty.http.HttpHeader;
45 import org.eclipse.jetty.http.HttpMethod;
46 import org.eclipse.jetty.http.HttpScheme;
47 import org.eclipse.jetty.http.HttpStatus;
48 import org.eclipse.jetty.http.HttpURI;
49 import org.eclipse.jetty.http.MimeTypes;
50 import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHABadRequestException;
51 import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAException;
52 import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAForbiddenException;
53 import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHATimeoutException;
54 import org.openhab.binding.plugwiseha.internal.api.exception.PlugwiseHAUnauthorizedException;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 import com.thoughtworks.xstream.XStream;
59
60 /**
61  * The {@link PlugwiseHAControllerRequest} class is a utility class to create
62  * API requests to the Plugwise Home Automation controller and to deserialize
63  * incoming XML into the appropriate model objects to be used by the {@link
64  * PlugwiseHAController}.
65  * 
66  * @author B. van Wetten - Initial contribution
67  * @author Leo Siepel - Adjustments to timeout logic
68  */
69 @NonNullByDefault
70 public class PlugwiseHAControllerRequest<T> {
71
72     private static final String CONTENT_TYPE_TEXT_XML = MimeTypes.Type.TEXT_XML_8859_1.toString();
73     private static final long TIMEOUT_SECONDS = 5;
74     private static final int REQUEST_MAX_RETRY_COUNT = 3;
75
76     private final Logger logger = LoggerFactory.getLogger(PlugwiseHAControllerRequest.class);
77     private final XStream xStream;
78     private final HttpClient httpClient;
79     private final String host;
80     private final int port;
81     private final Class<T> resultType;
82     private final @Nullable Transformer transformer;
83
84     private Map<String, String> headers = new HashMap<>();
85     private Map<String, String> queryParameters = new HashMap<>();
86     private @Nullable Object bodyParameter;
87     private String serverDateTime = "";
88     private String path = "/";
89
90     // Constructor
91
92     <X extends XStream> PlugwiseHAControllerRequest(Class<T> resultType, X xStream, @Nullable Transformer transformer,
93             HttpClient httpClient, String host, int port, String username, String password) {
94         this.resultType = resultType;
95         this.xStream = xStream;
96         this.transformer = transformer;
97         this.httpClient = httpClient;
98         this.host = host;
99         this.port = port;
100
101         setHeader(HttpHeader.ACCEPT.toString(), CONTENT_TYPE_TEXT_XML);
102
103         // Create Basic Auth header if username and password are supplied
104         if (!username.isBlank() && !password.isBlank()) {
105             setHeader(HttpHeader.AUTHORIZATION.toString(), "Basic " + Base64.getEncoder()
106                     .encodeToString(String.format("%s:%s", username, password).getBytes(StandardCharsets.UTF_8)));
107         }
108     }
109
110     // Public methods
111
112     public void setPath(String path) {
113         this.setPath(path, (HashMap<String, String>) null);
114     }
115
116     public void setPath(String path, @Nullable HashMap<String, String> pathParameters) {
117         this.path = path;
118
119         if (pathParameters != null) {
120             this.path += pathParameters.entrySet().stream().map(Object::toString).collect(Collectors.joining(";"));
121         }
122     }
123
124     public void setHeader(String key, Object value) {
125         this.headers.put(key, String.valueOf(value));
126     }
127
128     public void addPathParameter(String key) {
129         this.path += String.format(";%s", key);
130     }
131
132     public void addPathParameter(String key, Object value) {
133         this.path += String.format(";%s=%s", key, value);
134     }
135
136     public void addPathFilter(String key, String operator, Object value) {
137         this.path += String.format(";%s:%s:%s", key, operator, value);
138     }
139
140     public void setQueryParameter(String key, Object value) {
141         this.queryParameters.put(key, String.valueOf(value));
142     }
143
144     public void setBodyParameter(Object body) {
145         this.bodyParameter = body;
146     }
147
148     public String getServerDateTime() {
149         return this.serverDateTime;
150     }
151
152     @SuppressWarnings("unchecked")
153     public @Nullable T execute() throws PlugwiseHAException {
154         T result;
155         String xml = getContent();
156
157         if (String.class.equals(resultType)) {
158             if (this.transformer != null) {
159                 result = (T) this.transformXML(xml);
160             } else {
161                 result = (T) xml;
162             }
163         } else if (!Void.class.equals(resultType)) {
164             if (this.transformer != null) {
165                 result = (T) this.xStream.fromXML(this.transformXML(xml));
166             } else {
167                 result = (T) this.xStream.fromXML(xml);
168             }
169         } else {
170             return null;
171         }
172
173         return result;
174     }
175
176     // Protected and private methods
177
178     private String transformXML(String xml) throws PlugwiseHAException {
179         StringReader input = new StringReader(xml);
180         StringWriter output = new StringWriter();
181         Transformer localTransformer = this.transformer;
182         if (localTransformer != null) {
183             try {
184                 localTransformer.transform(new StreamSource(input), new StreamResult(output));
185             } catch (TransformerException e) {
186                 logger.debug("Could not apply XML stylesheet", e);
187                 throw new PlugwiseHAException("Could not apply XML stylesheet", e);
188             }
189         } else {
190             throw new PlugwiseHAException("Could not transform XML stylesheet, the transformer is null");
191         }
192
193         return output.toString();
194     }
195
196     private String getContent() throws PlugwiseHAException {
197         String content;
198         ContentResponse response = getContentResponse(REQUEST_MAX_RETRY_COUNT);
199
200         int status = response.getStatus();
201         switch (status) {
202             case HttpStatus.OK_200:
203             case HttpStatus.ACCEPTED_202:
204                 content = response.getContentAsString();
205                 if (logger.isTraceEnabled()) {
206                     logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), content);
207                 }
208                 break;
209             case HttpStatus.BAD_REQUEST_400:
210                 throw new PlugwiseHABadRequestException("Bad request");
211             case HttpStatus.UNAUTHORIZED_401:
212                 throw new PlugwiseHAUnauthorizedException("Unauthorized");
213             case HttpStatus.FORBIDDEN_403:
214                 throw new PlugwiseHAForbiddenException("Forbidden");
215             default:
216                 throw new PlugwiseHAException("Unknown HTTP status code " + status + " returned by the controller");
217         }
218
219         this.serverDateTime = response.getHeaders().get("Date");
220
221         return content;
222     }
223
224     private ContentResponse getContentResponse(int retries) throws PlugwiseHAException {
225         Request request = newRequest();
226         ContentResponse response;
227
228         this.logger.debug("Performing API request: {} {}", request.getMethod(), request.getURI());
229
230         if (logger.isTraceEnabled()) {
231             String content = "";
232             final ContentProvider provider = request.getContent();
233             if (provider != null) {
234                 final Iterator<ByteBuffer> it = provider.iterator();
235                 while (it.hasNext()) {
236                     final ByteBuffer next = it.next();
237                     final byte[] bytes = new byte[next.capacity()];
238                     next.get(bytes);
239                     content += String.format("{}\n", new String(bytes, StandardCharsets.UTF_8));
240                 }
241             }
242
243             logger.trace(">> \n{}", content);
244         }
245
246         try {
247             response = request.send();
248         } catch (InterruptedException e) {
249             this.logger.trace("InterruptedException occured {} {}", e.getMessage(), e.getStackTrace());
250             Thread.currentThread().interrupt();
251             throw new PlugwiseHATimeoutException(e);
252         } catch (TimeoutException e) {
253             if (retries > 0) {
254                 this.logger.debug("TimeoutException occured, remaining retries {}", retries - 1);
255                 return getContentResponse(retries - 1);
256             } else {
257                 throw new PlugwiseHATimeoutException(e);
258             }
259         } catch (ExecutionException e) {
260             // Unwrap the cause and try to cleanly handle it
261             Throwable cause = e.getCause();
262             if (cause instanceof UnknownHostException) {
263                 // Invalid hostname
264                 throw new PlugwiseHAException(cause);
265             } else if (cause instanceof ConnectException) {
266                 // Cannot connect
267                 throw new PlugwiseHAException(cause);
268             } else if (cause instanceof SocketTimeoutException) {
269                 throw new PlugwiseHATimeoutException(cause);
270             } else if (cause == null) {
271                 // Unable to unwrap
272                 throw new PlugwiseHAException(e);
273             } else {
274                 // Catch all
275                 throw new PlugwiseHAException(cause);
276             }
277         }
278         return response;
279     }
280
281     private Request newRequest() {
282         HttpMethod method = bodyParameter == null ? HttpMethod.GET : HttpMethod.PUT;
283         HttpURI uri = new HttpURI(HttpScheme.HTTP.asString(), this.host, this.port, this.path);
284         Request request = httpClient.newRequest(uri.toString()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
285                 .method(method);
286
287         for (Entry<String, String> entry : this.headers.entrySet()) {
288             request.header(entry.getKey(), entry.getValue());
289         }
290
291         for (Entry<String, String> entry : this.queryParameters.entrySet()) {
292             request.param(entry.getKey(), entry.getValue());
293         }
294
295         if (this.bodyParameter != null) {
296             String xmlBody = getRequestBodyAsXml();
297             ContentProvider content = new StringContentProvider(CONTENT_TYPE_TEXT_XML, xmlBody, StandardCharsets.UTF_8);
298             request = request.content(content);
299         }
300         return request;
301     }
302
303     private String getRequestBodyAsXml() {
304         return this.xStream.toXML(this.bodyParameter);
305     }
306 }