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