]> git.basschouten.com Git - openhab-addons.git/blob
a3bd6a76a6d98eb8e70805c8a872279d1f61773f
[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.unifi.internal.api.model;
14
15 import java.net.ConnectException;
16 import java.net.UnknownHostException;
17 import java.nio.charset.StandardCharsets;
18 import java.util.HashMap;
19 import java.util.Map;
20 import java.util.Map.Entry;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24
25 import javax.net.ssl.SSLException;
26
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.HttpResponseException;
31 import org.eclipse.jetty.client.api.ContentProvider;
32 import org.eclipse.jetty.client.api.ContentResponse;
33 import org.eclipse.jetty.client.api.Request;
34 import org.eclipse.jetty.client.util.StringContentProvider;
35 import org.eclipse.jetty.http.HttpMethod;
36 import org.eclipse.jetty.http.HttpScheme;
37 import org.eclipse.jetty.http.HttpStatus;
38 import org.eclipse.jetty.http.HttpURI;
39 import org.eclipse.jetty.http.MimeTypes;
40 import org.openhab.binding.unifi.internal.api.UniFiCommunicationException;
41 import org.openhab.binding.unifi.internal.api.UniFiException;
42 import org.openhab.binding.unifi.internal.api.UniFiExpiredSessionException;
43 import org.openhab.binding.unifi.internal.api.UniFiInvalidCredentialsException;
44 import org.openhab.binding.unifi.internal.api.UniFiInvalidHostException;
45 import org.openhab.binding.unifi.internal.api.UniFiNotAuthorizedException;
46 import org.openhab.binding.unifi.internal.api.UniFiSSLException;
47 import org.slf4j.Logger;
48 import org.slf4j.LoggerFactory;
49
50 import com.google.gson.Gson;
51 import com.google.gson.GsonBuilder;
52 import com.google.gson.JsonElement;
53 import com.google.gson.JsonObject;
54 import com.google.gson.JsonParser;
55 import com.google.gson.JsonPrimitive;
56 import com.google.gson.JsonSyntaxException;
57
58 /**
59  * The {@link UniFiControllerRequest} encapsulates a request sent by the {@link UniFiController}.
60  *
61  * @author Matthew Bowman - Initial contribution
62  *
63  * @param <T> The response type expected as a result of the request's execution
64  */
65 @NonNullByDefault
66 public class UniFiControllerRequest<T> {
67
68     private static final String CONTENT_TYPE_APPLICATION_JSON = MimeTypes.Type.APPLICATION_JSON.asString();
69
70     private static final long TIMEOUT_SECONDS = 5;
71
72     private static final String PROPERTY_DATA = "data";
73
74     private final Logger logger = LoggerFactory.getLogger(UniFiControllerRequest.class);
75
76     private final Gson gson;
77
78     private final HttpClient httpClient;
79
80     private final String host;
81
82     private final int port;
83
84     private String path = "/";
85
86     private Map<String, String> queryParameters = new HashMap<>();
87
88     private Map<String, String> bodyParameters = new HashMap<>();
89
90     private final Class<T> resultType;
91
92     // Public API
93
94     public UniFiControllerRequest(Class<T> resultType, Gson gson, HttpClient httpClient, String host, int port) {
95         this.resultType = resultType;
96         this.gson = gson;
97         this.httpClient = httpClient;
98         this.host = host;
99         this.port = port;
100     }
101
102     public void setPath(String path) {
103         this.path = path;
104     }
105
106     public void setBodyParameter(String key, Object value) {
107         this.bodyParameters.put(key, String.valueOf(value));
108     }
109
110     public void setQueryParameter(String key, Object value) {
111         this.queryParameters.put(key, String.valueOf(value));
112     }
113
114     public @Nullable T execute() throws UniFiException {
115         T result = null;
116         String json = getContent();
117         // mgb: only try and unmarshall non-void result types
118         if (!Void.class.equals(resultType)) {
119             JsonObject jsonObject = new JsonParser().parse(json).getAsJsonObject();
120             if (jsonObject.has(PROPERTY_DATA) && jsonObject.get(PROPERTY_DATA).isJsonArray()) {
121                 result = gson.fromJson(jsonObject.getAsJsonArray(PROPERTY_DATA), resultType);
122             }
123         }
124         return result;
125     }
126
127     // Private API
128
129     private String getContent() throws UniFiException {
130         String content;
131         ContentResponse response = getContentResponse();
132         int status = response.getStatus();
133         switch (status) {
134             case HttpStatus.OK_200:
135                 content = response.getContentAsString();
136                 if (logger.isTraceEnabled()) {
137                     logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), prettyPrintJson(content));
138                 }
139                 break;
140             case HttpStatus.BAD_REQUEST_400:
141                 throw new UniFiInvalidCredentialsException("Invalid Credentials");
142             case HttpStatus.UNAUTHORIZED_401:
143                 throw new UniFiExpiredSessionException("Expired Credentials");
144             case HttpStatus.FORBIDDEN_403:
145                 throw new UniFiNotAuthorizedException("Unauthorized Access");
146             default:
147                 throw new UniFiException("Unknown HTTP status code " + status + " returned by the controller");
148         }
149         return content;
150     }
151
152     private ContentResponse getContentResponse() throws UniFiException {
153         Request request = newRequest();
154         logger.trace(">> {} {}", request.getMethod(), request.getURI());
155         ContentResponse response;
156         try {
157             response = request.send();
158         } catch (TimeoutException | InterruptedException e) {
159             throw new UniFiCommunicationException(e);
160         } catch (ExecutionException e) {
161             // mgb: unwrap the cause and try to cleanly handle it
162             Throwable cause = e.getCause();
163             if (cause instanceof UnknownHostException) {
164                 // invalid hostname
165                 throw new UniFiInvalidHostException(cause);
166             } else if (cause instanceof ConnectException) {
167                 // cannot connect
168                 throw new UniFiCommunicationException(cause);
169             } else if (cause instanceof SSLException) {
170                 // cannot establish ssl connection
171                 throw new UniFiSSLException(cause);
172             } else if (cause instanceof HttpResponseException
173                     && ((HttpResponseException) cause).getResponse() instanceof ContentResponse) {
174                 // the UniFi controller violates the HTTP protocol
175                 // - it returns 401 UNAUTHORIZED without the WWW-Authenticate response header
176                 // - this causes an ExceptionException to be thrown
177                 // - we unwrap the response from the exception for proper handling of the 401 status code
178                 response = (ContentResponse) ((HttpResponseException) cause).getResponse();
179             } else {
180                 // catch all
181                 throw new UniFiException(cause);
182             }
183         }
184         return response;
185     }
186
187     private Request newRequest() {
188         HttpMethod method = bodyParameters.isEmpty() ? HttpMethod.GET : HttpMethod.POST;
189         HttpURI uri = new HttpURI(HttpScheme.HTTPS.asString(), host, port, path);
190         Request request = httpClient.newRequest(uri.toString()).timeout(TIMEOUT_SECONDS, TimeUnit.SECONDS)
191                 .method(method);
192         for (Entry<String, String> entry : queryParameters.entrySet()) {
193             request.param(entry.getKey(), entry.getValue());
194         }
195         if (!bodyParameters.isEmpty()) {
196             String jsonBody = getRequestBodyAsJson();
197             ContentProvider content = new StringContentProvider(CONTENT_TYPE_APPLICATION_JSON, jsonBody,
198                     StandardCharsets.UTF_8);
199             request = request.content(content);
200         }
201         return request;
202     }
203
204     private String getRequestBodyAsJson() {
205         JsonParser jsonParser = new JsonParser();
206         JsonObject jsonObject = new JsonObject();
207         JsonElement jsonElement = null;
208         for (Entry<String, String> entry : bodyParameters.entrySet()) {
209             try {
210                 jsonElement = jsonParser.parse(entry.getValue());
211             } catch (JsonSyntaxException e) {
212                 jsonElement = new JsonPrimitive(entry.getValue());
213             }
214             jsonObject.add(entry.getKey(), jsonElement);
215         }
216         return jsonObject.toString();
217     }
218
219     private static String prettyPrintJson(String content) {
220         JsonParser parser = new JsonParser();
221         JsonObject json = parser.parse(content).getAsJsonObject();
222         Gson prettyGson = new GsonBuilder().setPrettyPrinting().create();
223         return prettyGson.toJson(json);
224     }
225 }