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