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.deutschebahn.internal.timetable;
15 import java.io.IOException;
16 import java.io.InputStream;
17 import java.io.StringReader;
18 import java.text.SimpleDateFormat;
19 import java.util.Date;
20 import java.util.Properties;
21 import java.util.concurrent.TimeUnit;
23 import javax.xml.bind.JAXBContext;
24 import javax.xml.bind.JAXBElement;
25 import javax.xml.bind.JAXBException;
26 import javax.xml.bind.Unmarshaller;
27 import javax.xml.validation.Schema;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.http.HttpHeader;
32 import org.openhab.binding.deutschebahn.internal.timetable.dto.Timetable;
33 import org.slf4j.Logger;
34 import org.slf4j.LoggerFactory;
35 import org.xml.sax.SAXException;
38 * Default Implementation of {@link TimetablesV1Api}.
40 * @author Sönke Küper - Initial contribution
43 public final class TimetablesV1Impl implements TimetablesV1Api {
46 * Interface for stubbing HTTP-Calls in jUnit tests.
48 public interface HttpCallable {
51 * Executes the given <code>url</code> with the given <code>httpMethod</code>.
52 * Furthermore, the <code>http.proxyXXX</code> System variables are read and
53 * set into the {@link org.eclipse.jetty.client.HttpClient}.
55 * @param httpMethod the HTTP method to use
56 * @param url the url to execute
57 * @param httpHeaders optional http request headers which has to be sent within request
58 * @param content the content to be sent to the given <code>url</code> or <code>null</code> if no content should
60 * @param contentType the content type of the given <code>content</code>
61 * @param timeout the socket timeout in milliseconds to wait for data
62 * @return the response body or <code>NULL</code> when the request went wrong
63 * @throws IOException when the request execution failed, timed out, or it was interrupted
65 public abstract String executeUrl(String httpMethod, String url, Properties httpHeaders,
66 @Nullable InputStream content, @Nullable String contentType, int timeout) throws IOException;
69 private static final String BASE_URL = "https://apis.deutschebahn.com/db-api-marketplace/apis/timetables/v1";
70 private static final String PLAN_URL = BASE_URL + "/plan/%evaNo%/%date%/%hour%";
71 private static final String FCHG_URL = BASE_URL + "/fchg/%evaNo%";
72 private static final String RCHG_URL = BASE_URL + "/rchg/%evaNo%";
74 private static final String DB_CLIENT_ID_HEADER_NAME = "DB-Client-Id";
75 private static final String DB_CLIENT_SECRET_HEADER_NAME = "DB-Api-Key";
77 private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
78 private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMdd");
79 private static final SimpleDateFormat HOUR_FORMAT = new SimpleDateFormat("HH");
81 private final String clientId;
82 private final String clientSecret;
83 private final HttpCallable httpCallable;
85 private final Logger logger = LoggerFactory.getLogger(TimetablesV1Impl.class);
86 private final JAXBContext jaxbContext;
87 // private Schema schema;
90 * Creates a new {@link TimetablesV1Impl}.
92 * @param clientSecret The client secret for application with linked timetable api on developers.deutschebahn.com.
94 public TimetablesV1Impl( //
95 final String clientId, //
96 final String clientSecret, //
97 final HttpCallable httpCallable) throws JAXBException {
98 this.clientId = clientId;
99 this.clientSecret = clientSecret;
100 this.httpCallable = httpCallable;
102 // The results from webservice does not conform to the schema provided. The triplabel-Element (tl) is expected
104 // last Element within a timetableStop (s) element. But it is the first element when requesting the plan.
105 // When requesting the changes it is the last element, so the schema can't just be corrected.
106 // If written to developer support, but got no response yet, so schema validation is disabled at the moment.
108 // final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
109 // final URL schemaURL = getClass().getResource("/xsd/Timetables_REST.xsd");
110 // assert schemaURL != null;
111 // this.schema = schemaFactory.newSchema(schemaURL);
112 this.jaxbContext = JAXBContext.newInstance(Timetable.class.getPackageName(), Timetable.class.getClassLoader());
116 public Timetable getPlan(final String evaNo, final Date time) throws IOException {
117 return this.performHttpApiRequest(buildPlanRequestURL(evaNo, time));
121 public Timetable getFullChanges(final String evaNo) throws IOException {
122 return this.performHttpApiRequest(buildFchgRequestURL(evaNo));
126 public Timetable getRecentChanges(final String evaNo) throws IOException {
127 return this.performHttpApiRequest(buildRchgRequestURL(evaNo));
130 private Timetable performHttpApiRequest(final String url) throws IOException {
131 this.logger.debug("Performing http request to timetable api with url {}", url);
135 response = this.httpCallable.executeUrl( //
138 this.createHeaders(), //
142 return this.mapResponseToTimetable(response);
143 } catch (IOException e) {
144 logger.debug("Error getting data from webservice.", e);
150 * Parses and creates the {@link Timetable} from the response or
151 * returns an empty {@link Timetable} if response was empty.
153 private Timetable mapResponseToTimetable(final String response) throws IOException {
154 if (response.isEmpty()) {
155 return new Timetable();
159 return unmarshal(response);
160 } catch (JAXBException | SAXException e) {
161 this.logger.error("Error parsing response from timetable api.", e);
162 throw new IOException(e);
167 * Creates the HTTP-Headers required for http requests.
169 private Properties createHeaders() {
170 final Properties headers = new Properties();
171 headers.put(HttpHeader.ACCEPT.asString(), "application/xml");
172 headers.put(DB_CLIENT_ID_HEADER_NAME, this.clientId);
173 headers.put(DB_CLIENT_SECRET_HEADER_NAME, this.clientSecret);
177 private <T> T unmarshal(final String xmlContent) throws JAXBException, SAXException {
180 null, // Provide no schema, due webservice results are not schema-valid.
184 @SuppressWarnings("unchecked")
185 private static <T> T unmarshal(final JAXBContext jaxbContext, @Nullable final Schema schema,
186 final String xmlContent) throws JAXBException {
187 final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
188 unmarshaller.setSchema(schema);
189 final JAXBElement<T> resultObject = (JAXBElement<T>) unmarshaller.unmarshal(new StringReader(xmlContent));
190 return resultObject.getValue();
194 * Build rest endpoint URL for request the planned timetable.
196 private String buildPlanRequestURL(final String evaNr, final Date date) {
197 synchronized (this) {
198 final String dateParam = DATE_FORMAT.format(date);
199 final String hourParam = HOUR_FORMAT.format(date);
202 .replace("%evaNo%", evaNr) //
203 .replace("%date%", dateParam) //
204 .replace("%hour%", hourParam);
209 * Build rest endpoint URL for request all known changes in the timetable.
211 private static String buildFchgRequestURL(final String evaNr) {
212 return FCHG_URL.replace("%evaNo%", evaNr);
216 * Build rest endpoint URL for request all known changes in the timetable.
218 private static String buildRchgRequestURL(final String evaNr) {
219 return RCHG_URL.replace("%evaNo%", evaNr);