]> git.basschouten.com Git - openhab-addons.git/blob
475bf0168b59a4bc983ae2ced8002a3b54f5b2b3
[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.deutschebahn.internal.timetable;
14
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;
22
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;
28
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;
36
37 /**
38  * Default Implementation of {@link TimetablesV1Api}.
39  *
40  * @author Sönke Küper - Initial contribution
41  */
42 @NonNullByDefault
43 public final class TimetablesV1Impl implements TimetablesV1Api {
44
45     /**
46      * Interface for stubbing HTTP-Calls in jUnit tests.
47      */
48     public interface HttpCallable {
49
50         /**
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}.
54          *
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
59          *            be sent.
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
64          */
65         public abstract String executeUrl(String httpMethod, String url, Properties httpHeaders,
66                 @Nullable InputStream content, @Nullable String contentType, int timeout) throws IOException;
67     }
68
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%";
73
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";
76
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");
80
81     private final String clientId;
82     private final String clientSecret;
83     private final HttpCallable httpCallable;
84
85     private final Logger logger = LoggerFactory.getLogger(TimetablesV1Impl.class);
86     private final JAXBContext jaxbContext;
87     // private Schema schema;
88
89     /**
90      * Creates a new {@link TimetablesV1Impl}.
91      * 
92      * @param clientSecret The client secret for application with linked timetable api on developers.deutschebahn.com.
93      */
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;
101
102         // The results from webservice does not conform to the schema provided. The triplabel-Element (tl) is expected
103         // to occour as
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.
107
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());
113     }
114
115     @Override
116     public Timetable getPlan(final String evaNo, final Date time) throws IOException {
117         return this.performHttpApiRequest(buildPlanRequestURL(evaNo, time));
118     }
119
120     @Override
121     public Timetable getFullChanges(final String evaNo) throws IOException {
122         return this.performHttpApiRequest(buildFchgRequestURL(evaNo));
123     }
124
125     @Override
126     public Timetable getRecentChanges(final String evaNo) throws IOException {
127         return this.performHttpApiRequest(buildRchgRequestURL(evaNo));
128     }
129
130     private Timetable performHttpApiRequest(final String url) throws IOException {
131         this.logger.debug("Performing http request to timetable api with url {}", url);
132
133         String response;
134         try {
135             response = this.httpCallable.executeUrl( //
136                     "GET", //
137                     url, //
138                     this.createHeaders(), //
139                     null, //
140                     null, //
141                     REQUEST_TIMEOUT_MS);
142             return this.mapResponseToTimetable(response);
143         } catch (IOException e) {
144             logger.debug("Error getting data from webservice.", e);
145             throw e;
146         }
147     }
148
149     /**
150      * Parses and creates the {@link Timetable} from the response or
151      * returns an empty {@link Timetable} if response was empty.
152      */
153     private Timetable mapResponseToTimetable(final String response) throws IOException {
154         if (response.isEmpty()) {
155             return new Timetable();
156         }
157
158         try {
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);
163         }
164     }
165
166     /**
167      * Creates the HTTP-Headers required for http requests.
168      */
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);
174         return headers;
175     }
176
177     private <T> T unmarshal(final String xmlContent) throws JAXBException, SAXException {
178         return unmarshal( //
179                 jaxbContext, //
180                 null, // Provide no schema, due webservice results are not schema-valid.
181                 xmlContent);
182     }
183
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();
191     }
192
193     /**
194      * Build rest endpoint URL for request the planned timetable.
195      */
196     @SuppressWarnings("PMD.UnsynchronizedStaticFormatter")
197     private String buildPlanRequestURL(final String evaNr, final Date date) {
198         synchronized (this) {
199             final String dateParam = DATE_FORMAT.format(date);
200             final String hourParam = HOUR_FORMAT.format(date);
201
202             return PLAN_URL //
203                     .replace("%evaNo%", evaNr) //
204                     .replace("%date%", dateParam) //
205                     .replace("%hour%", hourParam);
206         }
207     }
208
209     /**
210      * Build rest endpoint URL for request all known changes in the timetable.
211      */
212     private static String buildFchgRequestURL(final String evaNr) {
213         return FCHG_URL.replace("%evaNo%", evaNr);
214     }
215
216     /**
217      * Build rest endpoint URL for request all known changes in the timetable.
218      */
219     private static String buildRchgRequestURL(final String evaNr) {
220         return RCHG_URL.replace("%evaNo%", evaNr);
221     }
222 }