2 * Copyright (c) 2010-2021 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.enturno.internal.connection;
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;
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;
30 import java.util.concurrent.ExecutionException;
31 import java.util.concurrent.TimeUnit;
32 import java.util.concurrent.TimeoutException;
33 import java.util.stream.Collectors;
35 import org.apache.commons.lang3.StringUtils;
36 import org.eclipse.jdt.annotation.NonNullByDefault;
37 import org.eclipse.jdt.annotation.Nullable;
38 import org.eclipse.jetty.client.HttpClient;
39 import org.eclipse.jetty.client.api.ContentResponse;
40 import org.eclipse.jetty.client.api.Request;
41 import org.eclipse.jetty.client.util.StringContentProvider;
42 import org.eclipse.jetty.http.HttpHeader;
43 import org.openhab.binding.enturno.internal.EnturNoConfiguration;
44 import org.openhab.binding.enturno.internal.EnturNoHandler;
45 import org.openhab.binding.enturno.internal.model.EnturJsonData;
46 import org.openhab.binding.enturno.internal.model.estimated.EstimatedCalls;
47 import org.openhab.binding.enturno.internal.model.simplified.DisplayData;
48 import org.openhab.binding.enturno.internal.model.stopplace.StopPlace;
49 import org.slf4j.Logger;
50 import org.slf4j.LoggerFactory;
52 import com.google.gson.Gson;
53 import com.google.gson.JsonObject;
54 import com.google.gson.JsonParser;
55 import com.google.gson.JsonSyntaxException;
58 * The {@link EnturNoConnection} is responsible for handling connection to Entur.no API
60 * @author Michal Kloc - Initial contribution
63 public class EnturNoConnection {
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";
72 private static final String PARAM_STOPID = "stopid";
73 private static final String PARAM_START_DATE_TIME = "startDateTime";
75 private static final String REALTIME_URL = "https://api.entur.io/journey-planner/v2/graphql";
77 private final EnturNoHandler handler;
78 private final HttpClient httpClient;
80 private final JsonParser parser = new JsonParser();
81 private final Gson gson = new Gson();
83 public EnturNoConnection(EnturNoHandler handler, HttpClient httpClient) {
84 this.handler = handler;
85 this.httpClient = httpClient;
89 * Requests the real-time timetable for specified line and stop place
91 * @param stopPlaceId stop place id see https://en-tur.no
92 * @return the real-time timetable
93 * @throws JsonSyntaxException
94 * @throws EnturCommunicationException
95 * @throws EnturConfigurationException
97 public synchronized List<DisplayData> getEnturTimeTable(@Nullable String stopPlaceId, @Nullable String lineCode)
98 throws JsonSyntaxException, EnturConfigurationException, EnturCommunicationException {
99 if (stopPlaceId == null || stopPlaceId.isBlank()) {
100 throw new EnturConfigurationException("Stop place id cannot be empty or null");
101 } else if (lineCode == null || lineCode.isBlank()) {
102 throw new EnturConfigurationException("Line code cannot be empty or null");
105 Map<String, String> params = getRequestParams(handler.getEnturNoConfiguration());
107 EnturJsonData enturJsonData = gson.fromJson(getResponse(REALTIME_URL, params), EnturJsonData.class);
109 if (enturJsonData == null) {
110 throw new EnturCommunicationException("Error when deserializing response to EnturJsonData.class");
113 return processData(enturJsonData.data.stopPlace, lineCode);
116 private Map<String, String> getRequestParams(EnturNoConfiguration config) {
117 Map<String, String> params = new HashMap<>();
118 String stopPlaceId = config.getStopPlaceId();
119 params.put(PARAM_STOPID, stopPlaceId == null ? "" : stopPlaceId.trim());
120 params.put(PARAM_START_DATE_TIME, LocalDateTime.now(ZoneId.of(TIME_ZONE)).toString());
125 private String getResponse(String url, Map<String, String> params) {
127 if (logger.isTraceEnabled()) {
128 logger.trace("Entur request: URL = '{}', graphQL parameters -> startTime = '{}', stopId = '{}'",
129 REALTIME_URL, params.get(PARAM_START_DATE_TIME), params.get(PARAM_STOPID));
132 Request request = httpClient.newRequest(url);
133 request.method(POST);
134 request.timeout(10, TimeUnit.SECONDS);
135 request.header(HttpHeader.CONTENT_TYPE, CONTENT_TYPE);
136 request.header(REQUIRED_CLIENT_NAME_HEADER, REQUIRED_CLIENT_NAME);
137 request.content(new StringContentProvider(getRequestBody(params)));
139 logger.trace("Request body: {}", getRequestBody(params));
141 ContentResponse contentResponse = request.send();
143 int httpStatus = contentResponse.getStatus();
144 String content = contentResponse.getContentAsString();
145 String errorMessage = "";
146 logger.trace("Entur response: status = {}, content = '{}'", httpStatus, content);
147 switch (httpStatus) {
150 case BAD_REQUEST_400:
152 errorMessage = getErrorMessage(content);
153 logger.debug("Entur server responded with status code {}: {}", httpStatus, errorMessage);
154 throw new EnturConfigurationException(errorMessage);
156 errorMessage = getErrorMessage(content);
157 logger.debug("Entur server responded with status code {}: {}", httpStatus, errorMessage);
158 throw new EnturCommunicationException(errorMessage);
160 } catch (ExecutionException e) {
161 String errorMessage = e.getLocalizedMessage();
162 logger.debug("Exception occurred during execution: {}", errorMessage, e);
163 throw new EnturCommunicationException(errorMessage, e);
164 } catch (InterruptedException | TimeoutException | IOException e) {
165 logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
166 throw new EnturCommunicationException(e.getLocalizedMessage(), e);
170 private String getErrorMessage(String response) {
171 JsonObject jsonResponse = parser.parse(response).getAsJsonObject();
172 if (jsonResponse.has(PROPERTY_MESSAGE)) {
173 return jsonResponse.get(PROPERTY_MESSAGE).getAsString();
178 private String getRequestBody(Map<String, String> params) throws IOException {
179 try (InputStream inputStream = EnturNoConnection.class.getClassLoader().getResourceAsStream(REQUEST_BODY);
180 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
181 String json = bufferedReader.lines().collect(Collectors.joining("\n"));
183 return json.replaceAll("\\{stopPlaceId}", "" + params.get(PARAM_STOPID)).replaceAll("\\{startDateTime}",
184 "" + params.get(PARAM_START_DATE_TIME));
188 private List<DisplayData> processData(StopPlace stopPlace, String lineCode) {
189 Map<String, List<EstimatedCalls>> departures = stopPlace.estimatedCalls.stream()
190 .filter(call -> StringUtils.equalsIgnoreCase(
191 StringUtils.trimToEmpty(call.serviceJourney.journeyPattern.line.publicCode),
192 StringUtils.trimToEmpty(lineCode)))
193 .collect(groupingBy(call -> call.quay.id));
195 List<DisplayData> processedData = new ArrayList<>();
196 if (departures.keySet().size() > 0) {
197 DisplayData processedData01 = getDisplayData(stopPlace, departures, 0);
198 processedData.add(processedData01);
201 if (departures.keySet().size() > 1) {
202 DisplayData processedData02 = getDisplayData(stopPlace, departures, 1);
203 processedData.add(processedData02);
206 return processedData;
209 private DisplayData getDisplayData(StopPlace stopPlace, Map<String, List<EstimatedCalls>> departures,
211 List<String> keys = new ArrayList<>(departures.keySet());
212 DisplayData processedData = new DisplayData();
213 List<EstimatedCalls> quayCalls = departures.get(keys.get(quayIndex));
214 List<String> departureTimes = quayCalls.stream().map(eq -> eq.expectedDepartureTime).map(this::getIsoDateTime)
215 .collect(Collectors.toList());
217 List<String> estimatedFlags = quayCalls.stream().map(es -> es.realtime).collect(Collectors.toList());
219 if (quayCalls.size() > quayIndex) {
220 String lineCode = quayCalls.get(0).serviceJourney.journeyPattern.line.publicCode;
221 String frontText = quayCalls.get(0).destinationDisplay.frontText;
222 processedData.lineCode = lineCode;
223 processedData.frontText = frontText;
224 processedData.departures = departureTimes;
225 processedData.estimatedFlags = estimatedFlags;
228 processedData.stopPlaceId = stopPlace.id;
229 processedData.stopName = stopPlace.name;
230 processedData.transportMode = stopPlace.transportMode;
231 return processedData;
234 private String getIsoDateTime(String dateTimeWithoutColonInZone) {
235 String dateTime = StringUtils.substringBeforeLast(dateTimeWithoutColonInZone, "+");
236 String offset = StringUtils.substringAfterLast(dateTimeWithoutColonInZone, "+");
238 StringBuilder builder = new StringBuilder();
239 return builder.append(dateTime).append("+").append(StringUtils.substring(offset, 0, 2)).append(":00")