]> git.basschouten.com Git - openhab-addons.git/blob
d1bc6686140813fbfca8c760aa5e1d6a829de235
[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.easee.internal.command;
14
15 import static org.openhab.binding.easee.internal.EaseeBindingConstants.WEB_REQUEST_BEARER_TOKEN_PREFIX;
16
17 import java.net.SocketTimeoutException;
18 import java.net.UnknownHostException;
19 import java.nio.ByteBuffer;
20 import java.nio.charset.StandardCharsets;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.concurrent.TimeUnit;
24 import java.util.concurrent.TimeoutException;
25
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.Request;
30 import org.eclipse.jetty.client.api.Response;
31 import org.eclipse.jetty.client.api.Result;
32 import org.eclipse.jetty.client.util.BufferingResponseListener;
33 import org.eclipse.jetty.http.HttpHeader;
34 import org.eclipse.jetty.http.HttpStatus;
35 import org.eclipse.jetty.http.HttpStatus.Code;
36 import org.openhab.binding.easee.internal.EaseeBindingConstants;
37 import org.openhab.binding.easee.internal.connector.CommunicationStatus;
38 import org.openhab.binding.easee.internal.handler.EaseeThingHandler;
39 import org.openhab.binding.easee.internal.model.GenericResponseTransformer;
40 import org.openhab.binding.easee.internal.model.ValidationException;
41 import org.slf4j.Logger;
42 import org.slf4j.LoggerFactory;
43
44 import com.google.gson.Gson;
45 import com.google.gson.GsonBuilder;
46 import com.google.gson.JsonObject;
47 import com.google.gson.ToNumberPolicy;
48
49 /**
50  * base class for all commands. common logic should be implemented here
51  *
52  * @author Alexander Friese - initial contribution
53  */
54 @NonNullByDefault
55 public abstract class AbstractCommand extends BufferingResponseListener implements EaseeCommand {
56
57     public static enum RetryOnFailure {
58         YES,
59         NO;
60     }
61
62     public static enum ProcessFailureResponse {
63         YES,
64         NO;
65     }
66
67     /**
68      * logger
69      */
70     private final Logger logger = LoggerFactory.getLogger(AbstractCommand.class);
71
72     /**
73      * the configuration
74      */
75     protected final EaseeThingHandler handler;
76
77     /**
78      * JSON deserializer
79      */
80     protected final Gson gson;
81
82     /**
83      * status code of fulfilled request
84      */
85     private final CommunicationStatus communicationStatus;
86
87     /**
88      * generic transformer which just transfers all values in a plain map.
89      */
90     private final GenericResponseTransformer transformer;
91
92     /**
93      * retry counter.
94      */
95     private int retries = 0;
96
97     /**
98      * retry active
99      */
100     private final RetryOnFailure retryOnFailure;
101
102     /**
103      * process error response, e.g. set handler offline on error
104      */
105     private final ProcessFailureResponse processFailureResponse;
106
107     /**
108      * allows further processing of the json result data, if set.
109      */
110     private List<JsonResultProcessor> resultProcessors;
111
112     /**
113      * the constructor
114      */
115     public AbstractCommand(EaseeThingHandler handler, RetryOnFailure retryOnFailure,
116             ProcessFailureResponse processFailureResponse) {
117         this.gson = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create();
118         this.communicationStatus = new CommunicationStatus();
119         this.resultProcessors = new ArrayList<>();
120         this.transformer = new GenericResponseTransformer(handler);
121         this.handler = handler;
122         this.processFailureResponse = processFailureResponse;
123         this.retryOnFailure = retryOnFailure;
124     }
125
126     /**
127      * the constructor
128      */
129     public AbstractCommand(EaseeThingHandler handler, RetryOnFailure retryOnFailure,
130             ProcessFailureResponse processFailureResponse, JsonResultProcessor resultProcessor) {
131         this(handler, retryOnFailure, processFailureResponse);
132         this.resultProcessors.add(resultProcessor);
133     }
134
135     /**
136      * Log request success
137      */
138     @Override
139     public final void onSuccess(@Nullable Response response) {
140         super.onSuccess(response);
141         if (response != null) {
142             communicationStatus.setHttpCode(HttpStatus.getCode(response.getStatus()));
143             logger.debug("HTTP response {}", response.getStatus());
144         }
145     }
146
147     /**
148      * Log request failure
149      */
150     @Override
151     public final void onFailure(@Nullable Response response, @Nullable Throwable failure) {
152         super.onFailure(response, failure);
153         if (failure != null) {
154             logger.info("Request failed: {}", failure.toString());
155             communicationStatus.setError((Exception) failure);
156             if (failure instanceof SocketTimeoutException || failure instanceof TimeoutException) {
157                 communicationStatus.setHttpCode(Code.REQUEST_TIMEOUT);
158             } else if (failure instanceof UnknownHostException) {
159                 communicationStatus.setHttpCode(Code.BAD_GATEWAY);
160             } else {
161                 communicationStatus.setHttpCode(Code.INTERNAL_SERVER_ERROR);
162             }
163         } else {
164             logger.info("Request failed");
165         }
166         if (response != null && response.getStatus() > 0) {
167             communicationStatus.setHttpCode(HttpStatus.getCode(response.getStatus()));
168         }
169     }
170
171     /**
172      * just for logging of content
173      */
174     @Override
175     public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
176         super.onContent(response, content);
177         logger.debug("received content, length: {}", getContentAsString().length());
178     }
179
180     /**
181      * default handling of successful requests.
182      */
183     @Override
184     public void onComplete(@Nullable Result result) {
185         String json = getContentAsString(StandardCharsets.UTF_8);
186
187         logger.debug("JSON String: {}", json);
188         switch (getCommunicationStatus().getHttpCode()) {
189             case OK:
190             case ACCEPTED:
191                 onCompleteCodeOk(json);
192                 break;
193             default:
194                 onCompleteCodeDefault(json);
195         }
196     }
197
198     /**
199      * handling of result in case of HTTP response OK.
200      *
201      * @param json
202      */
203     protected void onCompleteCodeOk(@Nullable String json) {
204         JsonObject jsonObject = transform(json);
205         if (jsonObject != null) {
206             logger.debug("success");
207             handler.updateChannelStatus(transformer.transform(jsonObject, getChannelGroup()));
208             processResult(jsonObject);
209         }
210     }
211
212     /**
213      * handling of result in default case, this means error handling of http codes where no specific handling applies.
214      *
215      * @param json
216      */
217     protected void onCompleteCodeDefault(@Nullable String json) {
218         JsonObject jsonObject = transform(json);
219         if (jsonObject == null) {
220             jsonObject = new JsonObject();
221         }
222         if (processFailureResponse == ProcessFailureResponse.YES) {
223             processResult(jsonObject);
224         } else {
225             logger.info("command failed, url: {} - code: {} - result: {}", getURL(),
226                     getCommunicationStatus().getHttpCode(), jsonObject.get(EaseeBindingConstants.JSON_KEY_ERROR_TITLE));
227         }
228
229         if (retryOnFailure == RetryOnFailure.YES && retries++ < MAX_RETRIES) {
230             handler.enqueueCommand(this);
231         }
232     }
233
234     /**
235      * error safe json transformer.
236      *
237      * @param json
238      * @return
239      */
240     private @Nullable JsonObject transform(@Nullable String json) {
241         if (json != null) {
242             try {
243                 return gson.fromJson(json, JsonObject.class);
244             } catch (Exception ex) {
245                 logger.debug("JSON could not be parsed: {}\nError: {}", json, ex.getMessage());
246             }
247         }
248         return null;
249     }
250
251     /**
252      * preparation of the request. will call a hook (prepareRequest) that has to be implemented in the subclass to add
253      * content to the request.
254      *
255      * @throws ValidationException
256      */
257     @Override
258     public void performAction(HttpClient asyncclient, String accessToken) throws ValidationException {
259         Request request = asyncclient.newRequest(getURL()).timeout(handler.getBridgeConfiguration().getAsyncTimeout(),
260                 TimeUnit.SECONDS);
261
262         // we want to send and receive json only, so explicitely set this!
263         request.header(HttpHeader.ACCEPT, "application/json");
264         request.header(HttpHeader.CONTENT_TYPE, "application/json");
265
266         // this should be the default for Easee Cloud API
267         request.followRedirects(false);
268
269         // add authentication data for every request. Handling this here makes it obsolete to implement for each and
270         // every command
271         if (!accessToken.isBlank()) {
272             request.header(HttpHeader.AUTHORIZATION, WEB_REQUEST_BEARER_TOKEN_PREFIX + accessToken);
273         }
274
275         prepareRequest(request).send(this);
276     }
277
278     /**
279      * @return returns Http Status Code
280      */
281     public CommunicationStatus getCommunicationStatus() {
282         return communicationStatus;
283     }
284
285     /**
286      * calls the registered resultPRocessors.
287      *
288      * @param jsonObject
289      */
290     protected final void processResult(JsonObject jsonObject) {
291         for (JsonResultProcessor processor : resultProcessors) {
292             try {
293                 processor.processResult(getCommunicationStatus(), jsonObject);
294             } catch (Exception ex) {
295                 // this should not happen
296                 logger.warn("Exception caught: {}", ex.getMessage(), ex);
297             }
298         }
299     }
300
301     /**
302      * concrete implementation has to prepare the requests with additional parameters, etc
303      *
304      * @param requestToPrepare the request to prepare
305      * @return prepared Request object
306      * @throws ValidationException
307      */
308     protected abstract Request prepareRequest(Request requestToPrepare) throws ValidationException;
309
310     /**
311      * concrete implementation has to provide the channel group.
312      *
313      * @return
314      */
315     protected abstract String getChannelGroup();
316
317     /**
318      * concrete implementation has to provide the URL
319      *
320      * @return Url
321      */
322     protected abstract String getURL();
323
324     @Override
325     public void registerResultProcessor(JsonResultProcessor resultProcessor) {
326         this.resultProcessors.add(resultProcessor);
327     }
328 }