2 * Copyright (c) 2010-2022 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.net.URISyntaxException;
19 import java.text.SimpleDateFormat;
20 import java.util.Date;
21 import java.util.Properties;
22 import java.util.concurrent.TimeUnit;
24 import javax.xml.bind.JAXBContext;
25 import javax.xml.bind.JAXBElement;
26 import javax.xml.bind.JAXBException;
27 import javax.xml.bind.Unmarshaller;
28 import javax.xml.validation.Schema;
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.http.HttpHeader;
33 import org.openhab.binding.deutschebahn.internal.timetable.dto.Timetable;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
36 import org.xml.sax.SAXException;
39 * Default Implementation of {@link TimetablesV1Api}.
41 * @author Sönke Küper - Initial contribution
44 public final class TimetablesV1Impl implements TimetablesV1Api {
47 * Interface for stubbing HTTP-Calls in jUnit tests.
49 public interface HttpCallable {
52 * Executes the given <code>url</code> with the given <code>httpMethod</code>.
53 * Furthermore the <code>http.proxyXXX</code> System variables are read and
54 * set into the {@link org.eclipse.jetty.client.HttpClient}.
56 * @param httpMethod the HTTP method to use
57 * @param url the url to execute
58 * @param httpHeaders optional http request headers which has to be sent within request
59 * @param content the content to be sent to the given <code>url</code> or <code>null</code> if no content should
61 * @param contentType the content type of the given <code>content</code>
62 * @param timeout the socket timeout in milliseconds to wait for data
63 * @return the response body or <code>NULL</code> when the request went wrong
64 * @throws IOException when the request execution failed, timed out or it was interrupted
66 public abstract String executeUrl(String httpMethod, String url, Properties httpHeaders,
67 @Nullable InputStream content, @Nullable String contentType, int timeout) throws IOException;
70 private static final String PLAN_URL = "https://api.deutschebahn.com/timetables/v1/plan/%evaNo%/%date%/%hour%";
71 private static final String FCHG_URL = "https://api.deutschebahn.com/timetables/v1/fchg/%evaNo%";
72 private static final String RCHG_URL = "https://api.deutschebahn.com/timetables/v1/rchg/%evaNo%";
74 private static final int REQUEST_TIMEOUT_MS = (int) TimeUnit.SECONDS.toMillis(30);
75 private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyMMdd");
76 private static final SimpleDateFormat HOUR_FORMAT = new SimpleDateFormat("HH");
78 private final String authToken;
79 private final HttpCallable httpCallable;
81 private final Logger logger = LoggerFactory.getLogger(TimetablesV1Impl.class);
82 private JAXBContext jaxbContext;
83 // private Schema schema;
86 * Creates an new {@link TimetablesV1Impl}.
88 * @param authToken The authentication token for timetable api on developer.deutschebahn.com.
90 public TimetablesV1Impl(final String authToken, final HttpCallable httpCallable)
91 throws JAXBException, SAXException, URISyntaxException {
92 this.authToken = authToken;
93 this.httpCallable = httpCallable;
95 // The results from webservice does not conform to the schema provided. The triplabel-Element (tl) is expected
97 // last Element within an timetableStop (s) element. But it is the first element when requesting the plan.
98 // When requesting the changes it is the last element, so the schema can't just be corrected.
99 // If written to developer support, but got no response yet, so schema validation is disabled at the moment.
101 // final SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
102 // final URL schemaURL = getClass().getResource("/xsd/Timetables_REST.xsd");
103 // assert schemaURL != null;
104 // this.schema = schemaFactory.newSchema(schemaURL);
105 this.jaxbContext = JAXBContext.newInstance(Timetable.class.getPackageName(), Timetable.class.getClassLoader());
109 public Timetable getPlan(final String evaNo, final Date time) throws IOException {
110 return this.performHttpApiRequest(buildPlanRequestURL(evaNo, time));
114 public Timetable getFullChanges(final String evaNo) throws IOException {
115 return this.performHttpApiRequest(buildFchgRequestURL(evaNo));
119 public Timetable getRecentChanges(final String evaNo) throws IOException {
120 return this.performHttpApiRequest(buildRchgRequestURL(evaNo));
123 private Timetable performHttpApiRequest(final String url) throws IOException {
124 this.logger.debug("Performing http request to timetable api with url {}", url);
128 response = this.httpCallable.executeUrl( //
131 this.createHeaders(), //
135 return this.mapResponseToTimetable(response);
136 } catch (IOException e) {
137 logger.debug("Error getting data from webservice.", e);
143 * Parses and creates the {@link Timetable} from the response or
144 * returns an empty {@link Timetable} if response was empty.
146 private Timetable mapResponseToTimetable(final String response) throws IOException {
147 if (response.isEmpty()) {
148 return new Timetable();
152 return unmarshal(response, Timetable.class);
153 } catch (JAXBException | SAXException e) {
154 this.logger.error("Error parsing response from timetable api.", e);
155 throw new IOException(e);
160 * Creates the HTTP-Headers required for http requests.
162 private Properties createHeaders() {
163 final Properties headers = new Properties();
164 headers.put(HttpHeader.ACCEPT.asString(), "application/xml");
165 headers.put(HttpHeader.AUTHORIZATION.asString(), "Bearer " + this.authToken);
169 private <T> T unmarshal(final String xmlContent, final Class<T> clazz) throws JAXBException, SAXException {
172 null, // Provide no schema, due webservice results are not schema-valid.
178 @SuppressWarnings("unchecked")
179 private static <T> T unmarshal(final JAXBContext jaxbContext, @Nullable final Schema schema,
180 final String xmlContent, final Class<T> clss) throws JAXBException {
181 final Unmarshaller unmarshaller = jaxbContext.createUnmarshaller();
182 unmarshaller.setSchema(schema);
183 final JAXBElement<T> resultObject = (JAXBElement<T>) unmarshaller.unmarshal(new StringReader(xmlContent));
184 return resultObject.getValue();
188 * Build rest endpoint URL for request the planned timetable.
190 private String buildPlanRequestURL(final String evaNr, final Date date) {
191 synchronized (this) {
192 final String dateParam = DATE_FORMAT.format(date);
193 final String hourParam = HOUR_FORMAT.format(date);
196 .replace("%evaNo%", evaNr) //
197 .replace("%date%", dateParam) //
198 .replace("%hour%", hourParam);
203 * Build rest endpoint URL for request all known changes in the timetable.
205 private static String buildFchgRequestURL(final String evaNr) {
206 return FCHG_URL.replace("%evaNo%", evaNr);
210 * Build rest endpoint URL for request all known changes in the timetable.
212 private static String buildRchgRequestURL(final String evaNr) {
213 return RCHG_URL.replace("%evaNo%", evaNr);