2 * Copyright (c) 2010-2020 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.unifi.internal.api.model;
15 import java.net.ConnectException;
16 import java.net.UnknownHostException;
17 import java.nio.charset.StandardCharsets;
18 import java.util.HashMap;
20 import java.util.Map.Entry;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
25 import javax.net.ssl.SSLException;
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;
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;
59 * The {@link UniFiControllerRequest} encapsulates a request sent by the {@link UniFiController}.
61 * @author Matthew Bowman - Initial contribution
63 * @param <T> The response type expected as a result of the request's execution
66 public class UniFiControllerRequest<T> {
68 private static final String CONTENT_TYPE_APPLICATION_JSON = MimeTypes.Type.APPLICATION_JSON.asString();
70 private static final long TIMEOUT_SECONDS = 5;
72 private static final String PROPERTY_DATA = "data";
74 private final Logger logger = LoggerFactory.getLogger(UniFiControllerRequest.class);
76 private final Gson gson;
78 private final HttpClient httpClient;
80 private final String host;
82 private final int port;
84 private String path = "/";
86 private Map<String, String> queryParameters = new HashMap<>();
88 private Map<String, String> bodyParameters = new HashMap<>();
90 private final Class<T> resultType;
94 public UniFiControllerRequest(Class<T> resultType, Gson gson, HttpClient httpClient, String host, int port) {
95 this.resultType = resultType;
97 this.httpClient = httpClient;
102 public void setPath(String path) {
106 public void setBodyParameter(String key, Object value) {
107 this.bodyParameters.put(key, String.valueOf(value));
110 public void setQueryParameter(String key, Object value) {
111 this.queryParameters.put(key, String.valueOf(value));
114 public @Nullable T execute() throws UniFiException {
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);
129 private String getContent() throws UniFiException {
131 ContentResponse response = getContentResponse();
132 int status = response.getStatus();
134 case HttpStatus.OK_200:
135 content = response.getContentAsString();
136 if (logger.isTraceEnabled()) {
137 logger.trace("<< {} {} \n{}", status, HttpStatus.getMessage(status), prettyPrintJson(content));
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");
147 throw new UniFiException("Unknown HTTP status code " + status + " returned by the controller");
152 private ContentResponse getContentResponse() throws UniFiException {
153 Request request = newRequest();
154 logger.trace(">> {} {}", request.getMethod(), request.getURI());
155 ContentResponse response;
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) {
165 throw new UniFiInvalidHostException(cause);
166 } else if (cause instanceof ConnectException) {
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();
181 throw new UniFiException(cause);
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)
192 for (Entry<String, String> entry : queryParameters.entrySet()) {
193 request.param(entry.getKey(), entry.getValue());
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);
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()) {
210 jsonElement = jsonParser.parse(entry.getValue());
211 } catch (JsonSyntaxException e) {
212 jsonElement = new JsonPrimitive(entry.getValue());
214 jsonObject.add(entry.getKey(), jsonElement);
216 return jsonObject.toString();
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);