2 * Copyright (c) 2010-2021 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.plugwiseha.internal.api.model;
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;
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;
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;
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;
56 import com.thoughtworks.xstream.XStream;
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}.
64 * @author B. van Wetten - Initial contribution
67 public class PlugwiseHAControllerRequest<T> {
69 private static final String CONTENT_TYPE_TEXT_XML = MimeTypes.Type.TEXT_XML_8859_1.toString();
70 private static final long TIMEOUT_SECONDS = 5;
72 private final Logger logger = LoggerFactory.getLogger(PlugwiseHAControllerRequest.class);
73 private final XStream xStream;
74 private final HttpClient httpClient;
75 private final String host;
76 private final int port;
77 private final Class<T> resultType;
78 private final @Nullable Transformer transformer;
80 private Map<String, String> headers = new HashMap<>();
81 private Map<String, String> queryParameters = new HashMap<>();
82 private @Nullable Object bodyParameter;
83 private String serverDateTime = "";
84 private String path = "/";
88 <X extends XStream> PlugwiseHAControllerRequest(Class<T> resultType, X xStream, @Nullable Transformer transformer,
89 HttpClient httpClient, String host, int port, String username, String password) {
90 this.resultType = resultType;
91 this.xStream = xStream;
92 this.transformer = transformer;
93 this.httpClient = httpClient;
97 setHeader(HttpHeader.ACCEPT.toString(), CONTENT_TYPE_TEXT_XML);
99 // Create Basic Auth header if username and password are supplied
100 if (!username.isBlank() && !password.isBlank()) {
101 setHeader(HttpHeader.AUTHORIZATION.toString(), "Basic " + Base64.getEncoder()
102 .encodeToString(String.format("%s:%s", username, password).getBytes(StandardCharsets.UTF_8)));
108 public void setPath(String path) {
109 this.setPath(path, (HashMap<String, String>) null);
112 public void setPath(String path, @Nullable HashMap<String, String> pathParameters) {
115 if (pathParameters != null) {
116 this.path += pathParameters.entrySet().stream().map(Object::toString).collect(Collectors.joining(";"));
120 public void setHeader(String key, Object value) {
121 this.headers.put(key, String.valueOf(value));
124 public void addPathParameter(String key) {
125 this.path += String.format(";%s", key);
128 public void addPathParameter(String key, Object value) {
129 this.path += String.format(";%s=%s", key, value);
132 public void addPathFilter(String key, String operator, Object value) {
133 this.path += String.format(";%s:%s:%s", key, operator, value);
136 public void setQueryParameter(String key, Object value) {
137 this.queryParameters.put(key, String.valueOf(value));
140 public void setBodyParameter(Object body) {
141 this.bodyParameter = body;
144 public String getServerDateTime() {
145 return this.serverDateTime;
148 @SuppressWarnings("unchecked")
149 public @Nullable T execute() throws PlugwiseHAException {
151 String xml = getContent();
153 if (String.class.equals(resultType)) {
154 if (this.transformer != null) {
155 result = (T) this.transformXML(xml);
159 } else if (!Void.class.equals(resultType)) {
160 if (this.transformer != null) {
161 result = (T) this.xStream.fromXML(this.transformXML(xml));
163 result = (T) this.xStream.fromXML(xml);
172 // Protected and private methods
174 private String transformXML(String xml) throws PlugwiseHAException {
175 StringReader input = new StringReader(xml);
176 StringWriter output = new StringWriter();
177 Transformer localTransformer = this.transformer;
178 if (localTransformer != null) {
180 localTransformer.transform(new StreamSource(input), new StreamResult(output));
181 } catch (TransformerException e) {
182 logger.debug("Could not apply XML stylesheet", e);
183 throw new PlugwiseHAException("Could not apply XML stylesheet", e);
186 throw new PlugwiseHAException("Could not transform XML stylesheet, the transformer is null");
189 return output.toString();
192 private String getContent() throws PlugwiseHAException {
194 ContentResponse response;
197 response = getContentResponse();
198 } catch (PlugwiseHATimeoutException e) {
200 response = getContentResponse();
203 int status = response.getStatus();
205 case HttpStatus.OK_200:
206 case HttpStatus.ACCEPTED_202:
207 content = response.getContentAsString();
208 if (logger.isTraceEnabled()) {
209 logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), content);
212 case HttpStatus.BAD_REQUEST_400:
213 throw new PlugwiseHABadRequestException("Bad request");
214 case HttpStatus.UNAUTHORIZED_401:
215 throw new PlugwiseHAUnauthorizedException("Unauthorized");
216 case HttpStatus.FORBIDDEN_403:
217 throw new PlugwiseHAForbiddenException("Forbidden");
219 throw new PlugwiseHAException("Unknown HTTP status code " + status + " returned by the controller");
222 this.serverDateTime = response.getHeaders().get("Date");
227 private ContentResponse getContentResponse() throws PlugwiseHAException {
228 Request request = newRequest();
229 ContentResponse response;
231 if (logger.isTraceEnabled()) {
232 logger.trace(">> {} {}", request.getMethod(), request.getURI());
236 response = request.send();
237 } catch (TimeoutException | InterruptedException e) {
238 throw new PlugwiseHATimeoutException(e);
239 } catch (ExecutionException e) {
240 // Unwrap the cause and try to cleanly handle it
241 Throwable cause = e.getCause();
242 if (cause instanceof UnknownHostException) {
244 throw new PlugwiseHAException(cause);
245 } else if (cause instanceof ConnectException) {
247 throw new PlugwiseHAException(cause);
248 } else if (cause instanceof SocketTimeoutException) {
249 throw new PlugwiseHATimeoutException(cause);
250 } else if (cause == null) {
252 throw new PlugwiseHAException(e);
255 throw new PlugwiseHAException(cause);
261 private Request newRequest() {
262 HttpMethod method = bodyParameter == null ? HttpMethod.GET : HttpMethod.PUT;
263 HttpURI uri = new HttpURI(HttpScheme.HTTP.asString(), this.host, this.port, this.path);
264 Request request = httpClient.newRequest(uri.toString()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
267 for (Entry<String, String> entry : this.headers.entrySet()) {
268 request.header(entry.getKey(), entry.getValue());
271 for (Entry<String, String> entry : this.queryParameters.entrySet()) {
272 request.param(entry.getKey(), entry.getValue());
275 if (this.bodyParameter != null) {
276 String xmlBody = getRequestBodyAsXml();
277 ContentProvider content = new StringContentProvider(CONTENT_TYPE_TEXT_XML, xmlBody, StandardCharsets.UTF_8);
278 request = request.content(content);
283 private String getRequestBodyAsXml() {
284 return this.xStream.toXML(this.bodyParameter);