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