]> git.basschouten.com Git - openhab-addons.git/blob
84275f8218b8657b9897c5638b6652e8e2452c76
[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.icalendar.internal.handler;
14
15 import static org.openhab.binding.icalendar.internal.ICalendarBindingConstants.HTTP_TIMEOUT_SECS;
16
17 import java.io.File;
18 import java.io.FileInputStream;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.net.URI;
23 import java.nio.file.Files;
24 import java.nio.file.StandardCopyOption;
25 import java.util.concurrent.ExecutionException;
26 import java.util.concurrent.TimeUnit;
27 import java.util.concurrent.TimeoutException;
28
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.eclipse.jetty.client.api.Authentication;
33 import org.eclipse.jetty.client.api.Request;
34 import org.eclipse.jetty.client.api.Response;
35 import org.eclipse.jetty.client.util.BasicAuthentication;
36 import org.eclipse.jetty.client.util.InputStreamResponseListener;
37 import org.eclipse.jetty.http.HttpHeader;
38 import org.eclipse.jetty.http.HttpMethod;
39 import org.eclipse.jetty.http.HttpStatus;
40 import org.openhab.binding.icalendar.internal.logic.AbstractPresentableCalendar;
41 import org.openhab.binding.icalendar.internal.logic.CalendarException;
42 import org.slf4j.Logger;
43 import org.slf4j.LoggerFactory;
44
45 /**
46  * The Job for pulling an update of a calendar. Fires
47  * {@link CalendarUpdateListener#onCalendarUpdated()} after successful update.
48  *
49  * @author Michael Wodniok - Initial contribution
50  * @author Michael Wodniok - Added better descriptions for some errors while
51  *         downloading calendar
52  */
53 @NonNullByDefault
54 class PullJob implements Runnable {
55     private static final String TMP_FILE_PREFIX = "icalendardld";
56
57     private final Authentication.@Nullable Result authentication;
58     private final File destination;
59     private final HttpClient httpClient;
60     private final CalendarUpdateListener listener;
61     private final Logger logger = LoggerFactory.getLogger(PullJob.class);
62     private final int maxSize;
63     private final URI sourceURI;
64
65     /**
66      * Constructor of PullJob for creating a single pull of a calendar.
67      *
68      * @param httpClient A HttpClient for getting the source
69      * @param sourceURI The source as URI
70      * @param username Optional username for basic auth. Must be set together with a password.
71      * @param password Optional password for basic auth. Must be set together with an username.
72      * @param destination The destination the downloaded calendar should be saved to.
73      * @param maxSize The maximum size of the downloaded calendar in bytes.
74      * @param listener The listener that should be fired when update succeed.
75      */
76     public PullJob(HttpClient httpClient, URI sourceURI, @Nullable String username, @Nullable String password,
77             File destination, int maxSize, CalendarUpdateListener listener) {
78         this.httpClient = httpClient;
79         this.sourceURI = sourceURI;
80         if (username != null && password != null) {
81             authentication = new BasicAuthentication.BasicResult(this.sourceURI, username, password);
82         } else {
83             authentication = null;
84         }
85         this.destination = destination;
86         this.listener = listener;
87         this.maxSize = maxSize;
88     }
89
90     @Override
91     public void run() {
92         final Request request = httpClient.newRequest(sourceURI).followRedirects(true).method(HttpMethod.GET);
93         final Authentication.Result currentAuthentication = authentication;
94         if (currentAuthentication != null) {
95             currentAuthentication.apply(request);
96         }
97
98         final InputStreamResponseListener asyncListener = new InputStreamResponseListener();
99         request.send(asyncListener);
100
101         Response response;
102         try {
103             response = asyncListener.get(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS);
104         } catch (InterruptedException e1) {
105             logger.warn("Download of calendar was interrupted: {}", e1.getMessage());
106             request.abort(e1.getCause() != null ? e1.getCause() : e1);
107             return;
108         } catch (TimeoutException e1) {
109             logger.warn("Download of calendar timed out (waited too long for headers): {}", e1.getMessage());
110             request.abort(e1.getCause() != null ? e1.getCause() : e1);
111             return;
112         } catch (ExecutionException e1) {
113             String msg = e1.getCause() != null ? e1.getCause().getMessage() : "";
114             logger.warn("Download of calendar failed with ExecutionException: {}", msg);
115             request.abort(e1.getCause() != null ? e1.getCause() : e1);
116             return;
117         }
118
119         if (response.getStatus() != HttpStatus.OK_200) {
120             logger.warn("Response status for getting \"{}\" was {} instead of 200. Ignoring it.", sourceURI,
121                     response.getStatus());
122             request.abort(new IllegalStateException(
123                     "Got response status " + response.getStatus() + " while requesting " + sourceURI));
124             return;
125         }
126
127         final String responseLength = response.getHeaders().get(HttpHeader.CONTENT_LENGTH);
128         if (responseLength != null) {
129             try {
130                 if (Integer.parseInt(responseLength) > maxSize) {
131                     logger.warn(
132                             "Calendar is too big ({} bytes > {} bytes), aborting request. You may change the maximum calendar size in configuration, if appropriate.",
133                             responseLength, maxSize);
134                     response.abort(new ResponseTooBigException());
135                     return;
136                 }
137             } catch (NumberFormatException e) {
138                 logger.debug(
139                         "While requesting calendar Content-Length was set, but is malformed. Falling back to read-loop.",
140                         e);
141             }
142         }
143
144         File tmpTargetFile;
145         try {
146             tmpTargetFile = Files.createTempFile(TMP_FILE_PREFIX, null).toFile();
147         } catch (IOException e) {
148             logger.warn("Not able to create temporary file for downloading iCal. Error message is: {}", e.getMessage());
149             return;
150         }
151
152         try (final FileOutputStream tmpOutStream = new FileOutputStream(tmpTargetFile);
153                 final InputStream httpInputStream = asyncListener.getInputStream()) {
154             final byte[] buffer = new byte[1024];
155             int readBytesTotal = 0;
156             int currentReadBytes = -1;
157             while ((currentReadBytes = httpInputStream.read(buffer)) > -1) {
158                 readBytesTotal += currentReadBytes;
159                 if (readBytesTotal > maxSize) {
160                     logger.warn(
161                             "Calendar is too big (> {} bytes). Stopping receiving calendar. You may change the maximum calendar size in configuration, if appropriate.",
162                             maxSize);
163                     response.abort(new ResponseTooBigException());
164                     return;
165                 }
166                 tmpOutStream.write(buffer, 0, currentReadBytes);
167             }
168         } catch (IOException e) {
169             logger.warn("Not able to write temporary file with downloaded iCal. Error Message is: {}", e.getMessage());
170             return;
171         }
172
173         try (final FileInputStream tmpInput = new FileInputStream(tmpTargetFile)) {
174             AbstractPresentableCalendar.create(tmpInput);
175         } catch (IOException | CalendarException e) {
176             logger.warn(
177                     "Not able to read downloaded iCal. Validation failed or file not readable. Error message is: {}",
178                     e.getMessage());
179             return;
180         }
181
182         try {
183             Files.move(tmpTargetFile.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
184         } catch (IOException e) {
185             logger.warn("Failed to replace iCal file. Error message is: {}", e.getMessage());
186             return;
187         }
188
189         try {
190             listener.onCalendarUpdated();
191         } catch (Exception e) {
192             logger.debug("An Exception was thrown while calling back", e);
193         }
194     }
195
196     /**
197      * Interface for calling back when the update succeed.
198      */
199     public static interface CalendarUpdateListener {
200         /**
201          * Callback when update was successful and result was placed onto target file.
202          */
203         public void onCalendarUpdated();
204     }
205
206     /**
207      * Exception for failure if size of the response is greater than allowed.
208      */
209     private static class ResponseTooBigException extends Exception {
210
211         /**
212          * The only local definition. Rest of implementation is taken from Exception or is default.
213          */
214         private static final long serialVersionUID = 7033851403473533793L;
215     }
216 }