]> git.basschouten.com Git - openhab-addons.git/blob
6ed517a6070c3e5cd1c1c71ca42381550c6e1393
[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.enturno.internal.connection;
14
15 import static java.util.stream.Collectors.groupingBy;
16 import static org.eclipse.jetty.http.HttpMethod.POST;
17 import static org.eclipse.jetty.http.HttpStatus.*;
18 import static org.openhab.binding.enturno.internal.EnturNoBindingConstants.TIME_ZONE;
19
20 import java.io.BufferedReader;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.time.LocalDateTime;
25 import java.time.ZoneId;
26 import java.util.ArrayList;
27 import java.util.HashMap;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.concurrent.ExecutionException;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.TimeoutException;
33 import java.util.stream.Collectors;
34
35 import org.eclipse.jdt.annotation.NonNullByDefault;
36 import org.eclipse.jdt.annotation.Nullable;
37 import org.eclipse.jetty.client.HttpClient;
38 import org.eclipse.jetty.client.api.ContentResponse;
39 import org.eclipse.jetty.client.api.Request;
40 import org.eclipse.jetty.client.util.StringContentProvider;
41 import org.eclipse.jetty.http.HttpHeader;
42 import org.openhab.binding.enturno.internal.EnturNoConfiguration;
43 import org.openhab.binding.enturno.internal.EnturNoHandler;
44 import org.openhab.binding.enturno.internal.dto.EnturJsonData;
45 import org.openhab.binding.enturno.internal.dto.estimated.EstimatedCalls;
46 import org.openhab.binding.enturno.internal.dto.simplified.DisplayData;
47 import org.openhab.binding.enturno.internal.dto.stopplace.StopPlace;
48 import org.openhab.binding.enturno.internal.util.DateUtil;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
51
52 import com.google.gson.Gson;
53 import com.google.gson.JsonObject;
54 import com.google.gson.JsonParser;
55 import com.google.gson.JsonSyntaxException;
56
57 /**
58  * The {@link EnturNoConnection} is responsible for handling connection to Entur.no API
59  *
60  * @author Michal Kloc - Initial contribution
61  */
62 @NonNullByDefault
63 public class EnturNoConnection {
64
65     private final Logger logger = LoggerFactory.getLogger(EnturNoConnection.class);
66     private static final String REQUEST_BODY = "realtime_request.graphql";
67     private static final String PROPERTY_MESSAGE = "message";
68     private static final String CONTENT_TYPE = "application/graphql";
69     private static final String REQUIRED_CLIENT_NAME_HEADER = "ET-Client-Name";
70     private static final String REQUIRED_CLIENT_NAME = "openHAB-enturnobinding";
71
72     private static final String PARAM_STOPID = "stopid";
73     private static final String PARAM_START_DATE_TIME = "startDateTime";
74
75     private static final String REALTIME_URL = "https://api.entur.io/journey-planner/v2/graphql";
76
77     private final EnturNoHandler handler;
78     private final HttpClient httpClient;
79
80     private final Gson gson = new Gson();
81
82     public EnturNoConnection(EnturNoHandler handler, HttpClient httpClient) {
83         this.handler = handler;
84         this.httpClient = httpClient;
85     }
86
87     /**
88      * Requests the real-time timetable for specified line and stop place
89      *
90      * @param stopPlaceId stop place id see https://en-tur.no
91      * @return the real-time timetable
92      * @throws JsonSyntaxException
93      * @throws EnturCommunicationException
94      * @throws EnturConfigurationException
95      */
96     public synchronized List<DisplayData> getEnturTimeTable(@Nullable String stopPlaceId, @Nullable String lineCode)
97             throws JsonSyntaxException, EnturConfigurationException, EnturCommunicationException {
98         if (stopPlaceId == null || stopPlaceId.isBlank()) {
99             throw new EnturConfigurationException("Stop place id cannot be empty or null");
100         } else if (lineCode == null || lineCode.isBlank()) {
101             throw new EnturConfigurationException("Line code cannot be empty or null");
102         }
103
104         Map<String, String> params = getRequestParams(handler.getEnturNoConfiguration());
105
106         EnturJsonData enturJsonData = gson.fromJson(getResponse(REALTIME_URL, params), EnturJsonData.class);
107
108         if (enturJsonData == null) {
109             throw new EnturCommunicationException("Error when deserializing response to EnturJsonData.class");
110         }
111
112         return processData(enturJsonData.data.stopPlace, lineCode);
113     }
114
115     private Map<String, String> getRequestParams(EnturNoConfiguration config) {
116         Map<String, String> params = new HashMap<>();
117         String stopPlaceId = config.getStopPlaceId();
118         params.put(PARAM_STOPID, stopPlaceId == null ? "" : stopPlaceId.trim());
119         params.put(PARAM_START_DATE_TIME, LocalDateTime.now(ZoneId.of(TIME_ZONE)).toString());
120
121         return params;
122     }
123
124     private String getResponse(String url, Map<String, String> params) {
125         try {
126             if (logger.isTraceEnabled()) {
127                 logger.trace("Entur request: URL = '{}', graphQL parameters -> startTime = '{}', stopId = '{}'",
128                         REALTIME_URL, params.get(PARAM_START_DATE_TIME), params.get(PARAM_STOPID));
129             }
130
131             Request request = httpClient.newRequest(url);
132             request.method(POST);
133             request.timeout(10, TimeUnit.SECONDS);
134             request.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE);
135             request.header(REQUIRED_CLIENT_NAME_HEADER, REQUIRED_CLIENT_NAME);
136             request.content(new StringContentProvider(getRequestBody(params)));
137
138             logger.trace("Request body: {}", getRequestBody(params));
139
140             ContentResponse contentResponse = request.send();
141
142             int httpStatus = contentResponse.getStatus();
143             String content = contentResponse.getContentAsString();
144             String errorMessage = "";
145             logger.trace("Entur response: status = {}, content = '{}'", httpStatus, content);
146             switch (httpStatus) {
147                 case OK_200:
148                     return content;
149                 case BAD_REQUEST_400:
150                 case NOT_FOUND_404:
151                     errorMessage = getErrorMessage(content);
152                     logger.debug("Entur server responded with status code {}: {}", httpStatus, errorMessage);
153                     throw new EnturConfigurationException(errorMessage);
154                 default:
155                     errorMessage = getErrorMessage(content);
156                     logger.debug("Entur server responded with status code {}: {}", httpStatus, errorMessage);
157                     throw new EnturCommunicationException(errorMessage);
158             }
159         } catch (ExecutionException e) {
160             String errorMessage = e.getLocalizedMessage();
161             logger.debug("Exception occurred during execution: {}", errorMessage, e);
162             throw new EnturCommunicationException(errorMessage, e);
163         } catch (TimeoutException | IOException e) {
164             logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
165             throw new EnturCommunicationException(e.getLocalizedMessage(), e);
166         } catch (InterruptedException e) {
167             logger.debug("Execution interrupted: {}", e.getLocalizedMessage(), e);
168             Thread.currentThread().interrupt();
169             throw new EnturCommunicationException(e.getLocalizedMessage(), e);
170         }
171     }
172
173     private String getErrorMessage(String response) {
174         JsonObject jsonResponse = JsonParser.parseString(response).getAsJsonObject();
175         if (jsonResponse.has(PROPERTY_MESSAGE)) {
176             return jsonResponse.get(PROPERTY_MESSAGE).getAsString();
177         }
178         return response;
179     }
180
181     private String getRequestBody(Map<String, String> params) throws IOException {
182         try (InputStream inputStream = EnturNoConnection.class.getClassLoader().getResourceAsStream(REQUEST_BODY);
183                 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
184             String json = bufferedReader.lines().collect(Collectors.joining("\n"));
185
186             return json.replaceAll("\\{stopPlaceId}", "" + params.get(PARAM_STOPID)).replaceAll("\\{startDateTime}",
187                     "" + params.get(PARAM_START_DATE_TIME));
188         }
189     }
190
191     private List<DisplayData> processData(StopPlace stopPlace, String lineCode) {
192         Map<String, List<EstimatedCalls>> departures = stopPlace.estimatedCalls.stream().filter(
193                 call -> call.serviceJourney.journeyPattern.line.publicCode.strip().equalsIgnoreCase(lineCode.strip()))
194                 .collect(groupingBy(call -> call.quay.id));
195
196         List<DisplayData> processedData = new ArrayList<>();
197         if (!departures.keySet().isEmpty()) {
198             DisplayData processedData01 = getDisplayData(stopPlace, departures, 0);
199             processedData.add(processedData01);
200         }
201
202         if (departures.keySet().size() > 1) {
203             DisplayData processedData02 = getDisplayData(stopPlace, departures, 1);
204             processedData.add(processedData02);
205         }
206
207         return processedData;
208     }
209
210     private DisplayData getDisplayData(StopPlace stopPlace, Map<String, List<EstimatedCalls>> departures,
211             int quayIndex) {
212         List<String> keys = new ArrayList<>(departures.keySet());
213         DisplayData processedData = new DisplayData();
214         List<EstimatedCalls> quayCalls = departures.get(keys.get(quayIndex));
215         List<String> departureTimes = quayCalls.stream().map(eq -> eq.expectedDepartureTime)
216                 .map(DateUtil::getIsoDateTime).collect(Collectors.toList());
217
218         List<String> estimatedFlags = quayCalls.stream().map(es -> es.realtime).collect(Collectors.toList());
219
220         if (quayCalls.size() > quayIndex) {
221             String lineCode = quayCalls.get(0).serviceJourney.journeyPattern.line.publicCode;
222             String frontText = quayCalls.get(0).destinationDisplay.frontText;
223             processedData.lineCode = lineCode;
224             processedData.frontText = frontText;
225             processedData.departures = departureTimes;
226             processedData.estimatedFlags = estimatedFlags;
227         }
228
229         processedData.stopPlaceId = stopPlace.id;
230         processedData.stopName = stopPlace.name;
231         processedData.transportMode = stopPlace.transportMode;
232         return processedData;
233     }
234 }