2 * Copyright (c) 2010-2022 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.pushover.internal.connection;
15 import static org.openhab.binding.pushover.internal.PushoverBindingConstants.*;
18 import java.io.IOException;
19 import java.nio.file.Files;
20 import java.nio.file.Path;
21 import java.util.List;
22 import java.util.stream.Collectors;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.eclipse.jetty.client.api.ContentProvider;
27 import org.eclipse.jetty.client.util.MultiPartContentProvider;
28 import org.eclipse.jetty.client.util.PathContentProvider;
29 import org.eclipse.jetty.client.util.StringContentProvider;
30 import org.openhab.core.i18n.CommunicationException;
31 import org.openhab.core.i18n.ConfigurationException;
32 import org.openhab.core.io.net.http.HttpUtil;
33 import org.openhab.core.library.types.RawType;
34 import org.slf4j.Logger;
35 import org.slf4j.LoggerFactory;
38 * The {@link PushoverMessageBuilder} builds the body for Pushover Messages API requests.
40 * @author Christoph Weitkamp - Initial contribution
43 public class PushoverMessageBuilder {
45 private final Logger logger = LoggerFactory.getLogger(PushoverMessageBuilder.class);
47 public static final String MESSAGE_KEY_TOKEN = "token";
48 private static final String MESSAGE_KEY_USER = "user";
49 private static final String MESSAGE_KEY_MESSAGE = "message";
50 private static final String MESSAGE_KEY_TITLE = "title";
51 private static final String MESSAGE_KEY_DEVICE = "device";
52 private static final String MESSAGE_KEY_PRIORITY = "priority";
53 private static final String MESSAGE_KEY_RETRY = "retry";
54 private static final String MESSAGE_KEY_EXPIRE = "expire";
55 private static final String MESSAGE_KEY_URL = "url";
56 private static final String MESSAGE_KEY_URL_TITLE = "url_title";
57 private static final String MESSAGE_KEY_SOUND = "sound";
58 private static final String MESSAGE_KEY_ATTACHMENT = "attachment";
59 public static final String MESSAGE_KEY_HTML = "html";
60 public static final String MESSAGE_KEY_MONOSPACE = "monospace";
62 private static final int MAX_MESSAGE_LENGTH = 1024;
63 private static final int MAX_TITLE_LENGTH = 250;
64 private static final int MAX_DEVICE_LENGTH = 25;
65 private static final List<Integer> VALID_PRIORITY_LIST = List.of(-2, -1, 0, 1, 2);
66 private static final int DEFAULT_PRIORITY = 0;
67 public static final int EMERGENCY_PRIORITY = 2;
68 private static final int MIN_RETRY_SECONDS = 30;
69 private static final int MAX_EXPIRE_SECONDS = 10800;
70 private static final int MAX_URL_LENGTH = 512;
71 private static final int MAX_URL_TITLE_LENGTH = 100;
72 public static final String DEFAULT_CONTENT_TYPE = "image/jpeg";
74 private final MultiPartContentProvider body = new MultiPartContentProvider();
76 private @Nullable String message;
77 private @Nullable String title;
78 private @Nullable String device;
79 private int priority = DEFAULT_PRIORITY;
80 private int retry = 300;
81 private int expire = 3600;
82 private @Nullable String url;
83 private @Nullable String urlTitle;
84 private @Nullable String sound;
85 private @Nullable String attachment;
86 private @Nullable String contentType;
87 private boolean html = false;
88 private boolean monospace = false;
90 private PushoverMessageBuilder(String apikey, String user) throws ConfigurationException {
91 body.addFieldPart(MESSAGE_KEY_TOKEN, new StringContentProvider(apikey), null);
92 body.addFieldPart(MESSAGE_KEY_USER, new StringContentProvider(user), null);
95 public static PushoverMessageBuilder getInstance(@Nullable String apikey, @Nullable String user)
96 throws ConfigurationException {
97 if (apikey == null || apikey.isBlank()) {
98 throw new ConfigurationException(TEXT_OFFLINE_CONF_ERROR_MISSING_APIKEY);
101 if (user == null || user.isBlank()) {
102 throw new ConfigurationException(TEXT_OFFLINE_CONF_ERROR_MISSING_USER);
105 return new PushoverMessageBuilder(apikey, user);
108 public PushoverMessageBuilder withMessage(String message) {
109 this.message = message;
113 public PushoverMessageBuilder withTitle(String title) {
118 public PushoverMessageBuilder withDevice(String device) {
119 this.device = device;
123 public PushoverMessageBuilder withPriority(int priority) {
124 this.priority = priority;
128 public PushoverMessageBuilder withRetry(int retry) {
133 public PushoverMessageBuilder withExpire(int expire) {
134 this.expire = expire;
138 public PushoverMessageBuilder withUrl(String url) {
143 public PushoverMessageBuilder withUrlTitle(String urlTitle) {
144 this.urlTitle = urlTitle;
148 public PushoverMessageBuilder withSound(String sound) {
153 public PushoverMessageBuilder withAttachment(String attachment) {
154 this.attachment = attachment;
158 public PushoverMessageBuilder withContentType(String contentType) {
159 this.contentType = contentType;
163 public PushoverMessageBuilder withHtmlFormatting() {
168 public PushoverMessageBuilder withMonospaceFormatting() {
169 this.monospace = true;
173 public ContentProvider build() throws CommunicationException {
174 if (message != null) {
175 if (message.length() > MAX_MESSAGE_LENGTH) {
176 throw new IllegalArgumentException(String.format(
177 "Skip sending the message as 'message' is longer than %d characters.", MAX_MESSAGE_LENGTH));
179 body.addFieldPart(MESSAGE_KEY_MESSAGE, new StringContentProvider(message), null);
183 if (title.length() > MAX_TITLE_LENGTH) {
184 throw new IllegalArgumentException(String
185 .format("Skip sending the message as 'title' is longer than %d characters.", MAX_TITLE_LENGTH));
187 body.addFieldPart(MESSAGE_KEY_TITLE, new StringContentProvider(title), null);
190 if (device != null) {
191 if (device.length() > MAX_DEVICE_LENGTH) {
192 logger.warn("Skip 'device' as it is longer than {} characters. Got: {}.", MAX_DEVICE_LENGTH, device);
194 body.addFieldPart(MESSAGE_KEY_DEVICE, new StringContentProvider(device), null);
198 if (priority != DEFAULT_PRIORITY) {
199 if (VALID_PRIORITY_LIST.contains(priority)) {
200 body.addFieldPart(MESSAGE_KEY_PRIORITY, new StringContentProvider(String.valueOf(priority)), null);
202 if (priority == EMERGENCY_PRIORITY) {
203 if (retry < MIN_RETRY_SECONDS) {
204 logger.warn("Retry value of {} is too small. Using default value of {}.", retry,
206 body.addFieldPart(MESSAGE_KEY_RETRY,
207 new StringContentProvider(String.valueOf(MIN_RETRY_SECONDS)), null);
209 body.addFieldPart(MESSAGE_KEY_RETRY, new StringContentProvider(String.valueOf(retry)), null);
212 if (0 < expire && expire <= MAX_EXPIRE_SECONDS) {
213 body.addFieldPart(MESSAGE_KEY_EXPIRE, new StringContentProvider(String.valueOf(expire)), null);
215 logger.warn("Expire value of {} is invalid. Using default value of {}.", expire,
217 body.addFieldPart(MESSAGE_KEY_EXPIRE,
218 new StringContentProvider(String.valueOf(MAX_EXPIRE_SECONDS)), null);
222 logger.warn("Invalid 'priority', skipping. Expected: {}. Got: {}.",
223 VALID_PRIORITY_LIST.stream().map(i -> i.toString()).collect(Collectors.joining(",")), priority);
228 if (url.length() > MAX_URL_LENGTH) {
229 throw new IllegalArgumentException(String
230 .format("Skip sending the message as 'url' is longer than %d characters.", MAX_URL_LENGTH));
232 body.addFieldPart(MESSAGE_KEY_URL, new StringContentProvider(url), null);
234 if (urlTitle != null) {
235 if (urlTitle.length() > MAX_URL_TITLE_LENGTH) {
236 throw new IllegalArgumentException(
237 String.format("Skip sending the message as 'urlTitle' is longer than %d characters.",
238 MAX_URL_TITLE_LENGTH));
240 body.addFieldPart(MESSAGE_KEY_URL_TITLE, new StringContentProvider(urlTitle), null);
245 body.addFieldPart(MESSAGE_KEY_SOUND, new StringContentProvider(sound), null);
248 if (attachment != null) {
249 String localAttachment = attachment;
250 if (localAttachment.startsWith("http")) { // support data HTTP(S) scheme
251 RawType rawImage = HttpUtil.downloadImage(attachment, 10000);
252 if (rawImage == null) {
253 throw new IllegalArgumentException(
254 String.format("Skip sending the message as content '%s' does not exist.", attachment));
256 addFilePart(createTempFile(rawImage.getBytes()),
257 contentType == null ? rawImage.getMimeType() : contentType);
258 } else if (localAttachment.startsWith("data:")) { // support data URI scheme
260 RawType rawImage = RawType.valueOf(localAttachment);
261 addFilePart(createTempFile(rawImage.getBytes()),
262 contentType == null ? rawImage.getMimeType() : contentType);
263 } catch (IllegalArgumentException e) {
264 throw new IllegalArgumentException(String
265 .format("Skip sending the message because data URI scheme is invalid: %s", e.getMessage()));
268 File file = new File(attachment);
269 if (!file.exists()) {
270 throw new IllegalArgumentException(
271 String.format("Skip sending the message as file '%s' does not exist.", attachment));
273 addFilePart(file.toPath(), contentType);
278 body.addFieldPart(MESSAGE_KEY_HTML, new StringContentProvider("1"), null);
279 } else if (monospace) {
280 body.addFieldPart(MESSAGE_KEY_MONOSPACE, new StringContentProvider("1"), null);
286 private Path createTempFile(byte[] data) throws CommunicationException {
288 Path tmpFile = Files.createTempFile("pushover-", ".tmp");
289 return Files.write(tmpFile, data);
290 } catch (IOException e) {
291 logger.debug("IOException occurred while creating temp file - skip sending the message: {}", e.getMessage(),
293 throw new CommunicationException(TEXT_ERROR_SKIP_SENDING_MESSAGE, e.getCause(), e.getLocalizedMessage());
297 private void addFilePart(Path path, @Nullable String contentType) throws CommunicationException {
299 body.addFilePart(MESSAGE_KEY_ATTACHMENT, path.toFile().getName(),
300 new PathContentProvider(contentType == null ? DEFAULT_CONTENT_TYPE : contentType, path), null);
301 } catch (IOException e) {
302 logger.debug("IOException occurred while adding content - skip sending the message: {}", e.getMessage(), e);
303 throw new CommunicationException(TEXT_ERROR_SKIP_SENDING_MESSAGE, e.getCause(), e.getLocalizedMessage());