2 * Copyright (c) 2010-2023 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.easee.internal.command;
15 import static org.openhab.binding.easee.internal.EaseeBindingConstants.WEB_REQUEST_BEARER_TOKEN_PREFIX;
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;
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;
44 import com.google.gson.Gson;
45 import com.google.gson.GsonBuilder;
46 import com.google.gson.JsonObject;
47 import com.google.gson.ToNumberPolicy;
50 * base class for all commands. common logic should be implemented here
52 * @author Alexander Friese - initial contribution
55 public abstract class AbstractCommand extends BufferingResponseListener implements EaseeCommand {
57 public static enum RetryOnFailure {
62 public static enum ProcessFailureResponse {
70 private final Logger logger = LoggerFactory.getLogger(AbstractCommand.class);
75 protected final EaseeThingHandler handler;
80 protected final Gson gson;
83 * status code of fulfilled request
85 private final CommunicationStatus communicationStatus;
88 * generic transformer which just transfers all values in a plain map.
90 private final GenericResponseTransformer transformer;
95 private int retries = 0;
100 private final RetryOnFailure retryOnFailure;
103 * process error response, e.g. set handler offline on error
105 private final ProcessFailureResponse processFailureResponse;
108 * allows further processing of the json result data, if set.
110 private List<JsonResultProcessor> resultProcessors;
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;
129 public AbstractCommand(EaseeThingHandler handler, RetryOnFailure retryOnFailure,
130 ProcessFailureResponse processFailureResponse, JsonResultProcessor resultProcessor) {
131 this(handler, retryOnFailure, processFailureResponse);
132 this.resultProcessors.add(resultProcessor);
136 * Log request success
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());
148 * Log request failure
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);
161 communicationStatus.setHttpCode(Code.INTERNAL_SERVER_ERROR);
164 logger.info("Request failed");
166 if (response != null && response.getStatus() > 0) {
167 communicationStatus.setHttpCode(HttpStatus.getCode(response.getStatus()));
172 * just for logging of content
175 public void onContent(@Nullable Response response, @Nullable ByteBuffer content) {
176 super.onContent(response, content);
177 logger.debug("received content, length: {}", getContentAsString().length());
181 * default handling of successful requests.
184 public void onComplete(@Nullable Result result) {
185 String json = getContentAsString(StandardCharsets.UTF_8);
187 logger.debug("JSON String: {}", json);
188 switch (getCommunicationStatus().getHttpCode()) {
191 onCompleteCodeOk(json);
194 onCompleteCodeDefault(json);
199 * handling of result in case of HTTP response OK.
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);
213 * handling of result in default case, this means error handling of http codes where no specific handling applies.
217 protected void onCompleteCodeDefault(@Nullable String json) {
218 JsonObject jsonObject = transform(json);
219 if (jsonObject == null) {
220 jsonObject = new JsonObject();
222 if (processFailureResponse == ProcessFailureResponse.YES) {
223 processResult(jsonObject);
225 logger.info("command failed, url: {} - code: {} - result: {}", getURL(),
226 getCommunicationStatus().getHttpCode(), jsonObject.get(EaseeBindingConstants.JSON_KEY_ERROR_TITLE));
229 if (retryOnFailure == RetryOnFailure.YES && retries++ < MAX_RETRIES) {
230 handler.enqueueCommand(this);
235 * error safe json transformer.
240 private @Nullable JsonObject transform(@Nullable String json) {
243 return gson.fromJson(json, JsonObject.class);
244 } catch (Exception ex) {
245 logger.debug("JSON could not be parsed: {}\nError: {}", json, ex.getMessage());
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.
255 * @throws ValidationException
258 public void performAction(HttpClient asyncclient, String accessToken) throws ValidationException {
259 Request request = asyncclient.newRequest(getURL()).timeout(handler.getBridgeConfiguration().getAsyncTimeout(),
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");
266 // this should be the default for Easee Cloud API
267 request.followRedirects(false);
269 // add authentication data for every request. Handling this here makes it obsolete to implement for each and
271 if (!accessToken.isBlank()) {
272 request.header(HttpHeader.AUTHORIZATION, WEB_REQUEST_BEARER_TOKEN_PREFIX + accessToken);
275 prepareRequest(request).send(this);
279 * @return returns Http Status Code
281 public CommunicationStatus getCommunicationStatus() {
282 return communicationStatus;
286 * calls the registered resultPRocessors.
290 protected final void processResult(JsonObject jsonObject) {
291 for (JsonResultProcessor processor : resultProcessors) {
293 processor.processResult(getCommunicationStatus(), jsonObject);
294 } catch (Exception ex) {
295 // this should not happen
296 logger.warn("Exception caught: {}", ex.getMessage(), ex);
302 * concrete implementation has to prepare the requests with additional parameters, etc
304 * @param requestToPrepare the request to prepare
305 * @return prepared Request object
306 * @throws ValidationException
308 protected abstract Request prepareRequest(Request requestToPrepare) throws ValidationException;
311 * concrete implementation has to provide the channel group.
315 protected abstract String getChannelGroup();
318 * concrete implementation has to provide the URL
322 protected abstract String getURL();
325 public void registerResultProcessor(JsonResultProcessor resultProcessor) {
326 this.resultProcessors.add(resultProcessor);