]> git.basschouten.com Git - openhab-addons.git/blob
e4eccc5370b674dcc7b9841730739cdb4e7d9966
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.deutschebahn.internal.timetable;
14
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;
23
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;
29
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;
37
38 /**
39  * Default Implementation of {@link TimetablesV1Api}.
40  *
41  * @author Sönke Küper - Initial contribution
42  */
43 @NonNullByDefault
44 public final class TimetablesV1Impl implements TimetablesV1Api {
45
46     /**
47      * Interface for stubbing HTTP-Calls in jUnit tests.
48      */
49     public interface HttpCallable {
50
51         /**
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}.
55          *
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
60          *            be sent.
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
65          */
66         public abstract String executeUrl(String httpMethod, String url, Properties httpHeaders,
67                 @Nullable InputStream content, @Nullable String contentType, int timeout) throws IOException;
68     }
69
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%";
73
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");
77
78     private final String authToken;
79     private final HttpCallable httpCallable;
80
81     private final Logger logger = LoggerFactory.getLogger(TimetablesV1Impl.class);
82     private JAXBContext jaxbContext;
83     // private Schema schema;
84
85     /**
86      * Creates an new {@link TimetablesV1Impl}.
87      * 
88      * @param authToken The authentication token for timetable api on developer.deutschebahn.com.
89      */
90     public TimetablesV1Impl(final String authToken, final HttpCallable httpCallable)
91             throws JAXBException, SAXException, URISyntaxException {
92         this.authToken = authToken;
93         this.httpCallable = httpCallable;
94
95         // The results from webservice does not conform to the schema provided. The triplabel-Element (tl) is expected
96         // to occour as
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.
100
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());
106     }
107
108     @Override
109     public Timetable getPlan(final String evaNo, final Date time) throws IOException {
110         return this.performHttpApiRequest(buildPlanRequestURL(evaNo, time));
111     }
112
113     @Override
114     public Timetable getFullChanges(final String evaNo) throws IOException {
115         return this.performHttpApiRequest(buildFchgRequestURL(evaNo));
116     }
117
118     @Override
119     public Timetable getRecentChanges(final String evaNo) throws IOException {
120         return this.performHttpApiRequest(buildRchgRequestURL(evaNo));
121     }
122
123     private Timetable performHttpApiRequest(final String url) throws IOException {
124         this.logger.debug("Performing http request to timetable api with url {}", url);
125
126         String response;
127         try {
128             response = this.httpCallable.executeUrl( //
129                     "GET", //
130                     url, //
131                     this.createHeaders(), //
132                     null, //
133                     null, //
134                     REQUEST_TIMEOUT_MS);
135             return this.mapResponseToTimetable(response);
136         } catch (IOException e) {
137             logger.debug("Error getting data from webservice.", e);
138             throw e;
139         }
140     }
141
142     /**
143      * Parses and creates the {@link Timetable} from the response or
144      * returns an empty {@link Timetable} if response was empty.
145      */
146     private Timetable mapResponseToTimetable(final String response) throws IOException {
147         if (response.isEmpty()) {
148             return new Timetable();
149         }
150
151         try {
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);
156         }
157     }
158
159     /**
160      * Creates the HTTP-Headers required for http requests.
161      */
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);
166         return headers;
167     }
168
169     private <T> T unmarshal(final String xmlContent, final Class<T> clazz) throws JAXBException, SAXException {
170         return unmarshal( //
171                 jaxbContext, //
172                 null, // Provide no schema, due webservice results are not schema-valid.
173                 xmlContent, //
174                 clazz //
175         );
176     }
177
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();
185     }
186
187     /**
188      * Build rest endpoint URL for request the planned timetable.
189      */
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);
194
195             return PLAN_URL //
196                     .replace("%evaNo%", evaNr) //
197                     .replace("%date%", dateParam) //
198                     .replace("%hour%", hourParam);
199         }
200     }
201
202     /**
203      * Build rest endpoint URL for request all known changes in the timetable.
204      */
205     private static String buildFchgRequestURL(final String evaNr) {
206         return FCHG_URL.replace("%evaNo%", evaNr);
207     }
208
209     /**
210      * Build rest endpoint URL for request all known changes in the timetable.
211      */
212     private static String buildRchgRequestURL(final String evaNr) {
213         return RCHG_URL.replace("%evaNo%", evaNr);
214     }
215 }