2 * Copyright (c) 2010-2024 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.icalendar.internal.handler;
15 import static org.openhab.binding.icalendar.internal.ICalendarBindingConstants.HTTP_TIMEOUT_SECS;
18 import java.io.FileInputStream;
19 import java.io.FileOutputStream;
20 import java.io.IOException;
21 import java.io.InputStream;
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;
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;
46 * The Job for pulling an update of a calendar. Fires
47 * {@link CalendarUpdateListener#onCalendarUpdated()} after successful update.
49 * @author Michael Wodniok - Initial contribution
50 * @author Michael Wodniok - Added better descriptions for some errors while
51 * downloading calendar
54 class PullJob implements Runnable {
55 private static final String TMP_FILE_PREFIX = "icalendardld";
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;
66 * Constructor of PullJob for creating a single pull of a calendar.
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.
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);
83 authentication = null;
85 this.destination = destination;
86 this.listener = listener;
87 this.maxSize = maxSize;
92 final Request request = httpClient.newRequest(sourceURI).followRedirects(true).method(HttpMethod.GET)
93 .timeout(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS);
94 final Authentication.Result currentAuthentication = authentication;
95 if (currentAuthentication != null) {
96 currentAuthentication.apply(request);
99 final InputStreamResponseListener asyncListener = new InputStreamResponseListener();
100 request.send(asyncListener);
104 response = asyncListener.get(HTTP_TIMEOUT_SECS, TimeUnit.SECONDS);
105 } catch (InterruptedException e1) {
106 logger.warn("Download of calendar was interrupted: {}", e1.getMessage());
107 request.abort(e1.getCause() != null ? e1.getCause() : e1);
109 } catch (TimeoutException e1) {
110 logger.warn("Download of calendar timed out (waited too long for headers): {}", e1.getMessage());
111 request.abort(e1.getCause() != null ? e1.getCause() : e1);
113 } catch (ExecutionException e1) {
114 String msg = e1.getCause() != null ? e1.getCause().getMessage() : "";
115 logger.warn("Download of calendar failed with ExecutionException: {}", msg);
116 request.abort(e1.getCause() != null ? e1.getCause() : e1);
120 if (response.getStatus() != HttpStatus.OK_200) {
121 logger.warn("Response status for getting \"{}\" was {} instead of 200. Ignoring it.", sourceURI,
122 response.getStatus());
123 request.abort(new IllegalStateException(
124 "Got response status " + response.getStatus() + " while requesting " + sourceURI));
128 final String responseLength = response.getHeaders().get(HttpHeader.CONTENT_LENGTH);
129 if (responseLength != null) {
131 if (Integer.parseInt(responseLength) > maxSize) {
133 "Calendar is too big ({} bytes > {} bytes), aborting request. You may change the maximum calendar size in configuration, if appropriate.",
134 responseLength, maxSize);
135 response.abort(new ResponseTooBigException());
138 } catch (NumberFormatException e) {
140 "While requesting calendar Content-Length was set, but is malformed. Falling back to read-loop.",
147 tmpTargetFile = Files.createTempFile(TMP_FILE_PREFIX, null).toFile();
148 } catch (IOException e) {
149 logger.warn("Not able to create temporary file for downloading iCal. Error message is: {}", e.getMessage());
153 try (final FileOutputStream tmpOutStream = new FileOutputStream(tmpTargetFile);
154 final InputStream httpInputStream = asyncListener.getInputStream()) {
155 final byte[] buffer = new byte[1024];
156 int readBytesTotal = 0;
157 int currentReadBytes = -1;
158 while ((currentReadBytes = httpInputStream.read(buffer)) > -1) {
159 readBytesTotal += currentReadBytes;
160 if (readBytesTotal > maxSize) {
162 "Calendar is too big (> {} bytes). Stopping receiving calendar. You may change the maximum calendar size in configuration, if appropriate.",
164 response.abort(new ResponseTooBigException());
167 tmpOutStream.write(buffer, 0, currentReadBytes);
169 } catch (IOException e) {
170 logger.warn("Not able to write temporary file with downloaded iCal. Error Message is: {}", e.getMessage());
174 try (final FileInputStream tmpInput = new FileInputStream(tmpTargetFile)) {
175 AbstractPresentableCalendar.create(tmpInput);
176 } catch (IOException | CalendarException e) {
178 "Not able to read downloaded iCal. Validation failed or file not readable. Error message is: {}",
184 Files.move(tmpTargetFile.toPath(), destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
185 } catch (IOException e) {
186 logger.warn("Failed to replace iCal file. Error message is: {}", e.getMessage());
191 listener.onCalendarUpdated();
192 } catch (Exception e) {
193 logger.debug("An Exception was thrown while calling back", e);
198 * Interface for calling back when the update succeed.
200 public static interface CalendarUpdateListener {
202 * Callback when update was successful and result was placed onto target file.
204 public void onCalendarUpdated();
208 * Exception for failure if size of the response is greater than allowed.
210 private static class ResponseTooBigException extends Exception {
213 * The only local definition. Rest of implementation is taken from Exception or is default.
215 private static final long serialVersionUID = 7033851403473533793L;