2 * Copyright (c) 2010-2021 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 java.net.URLEncoder;
16 import java.nio.charset.StandardCharsets;
17 import java.util.Collections;
18 import java.util.HashMap;
19 import java.util.List;
21 import java.util.concurrent.ExecutionException;
22 import java.util.concurrent.TimeUnit;
23 import java.util.concurrent.TimeoutException;
24 import java.util.stream.Collectors;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.eclipse.jetty.client.HttpClient;
29 import org.eclipse.jetty.client.api.ContentProvider;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.api.Request;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.openhab.binding.pushover.internal.config.PushoverAccountConfiguration;
35 import org.openhab.binding.pushover.internal.dto.Sound;
36 import org.openhab.core.cache.ExpiringCacheMap;
37 import org.slf4j.Logger;
38 import org.slf4j.LoggerFactory;
40 import com.google.gson.JsonArray;
41 import com.google.gson.JsonObject;
42 import com.google.gson.JsonParser;
45 * The {@link PushoverAPIConnection} is responsible for handling the connections to Pushover Messages API.
47 * @author Christoph Weitkamp - Initial contribution
50 public class PushoverAPIConnection {
52 private final Logger logger = LoggerFactory.getLogger(PushoverAPIConnection.class);
54 private static final String VALIDATE_URL = "https://api.pushover.net/1/users/validate.json";
55 private static final String MESSAGE_URL = "https://api.pushover.net/1/messages.json";
56 private static final String CANCEL_MESSAGE_URL = "https://api.pushover.net/1/receipts/{receipt}/cancel.json";
57 private static final String SOUNDS_URL = "https://api.pushover.net/1/sounds.json";
59 private final HttpClient httpClient;
60 private final PushoverAccountConfiguration config;
62 private final ExpiringCacheMap<String, String> cache = new ExpiringCacheMap<>(TimeUnit.DAYS.toMillis(1));
64 public PushoverAPIConnection(HttpClient httpClient, PushoverAccountConfiguration config) {
65 this.httpClient = httpClient;
69 public boolean validateUser() throws PushoverCommunicationException, PushoverConfigurationException {
70 return getMessageStatus(
71 post(VALIDATE_URL, PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
74 public boolean sendMessage(PushoverMessageBuilder message)
75 throws PushoverCommunicationException, PushoverConfigurationException {
76 return getMessageStatus(post(MESSAGE_URL, message.build()));
79 public String sendPriorityMessage(PushoverMessageBuilder message)
80 throws PushoverCommunicationException, PushoverConfigurationException {
81 final JsonObject json = JsonParser.parseString(post(MESSAGE_URL, message.build())).getAsJsonObject();
82 return getMessageStatus(json) && json.has("receipt") ? json.get("receipt").getAsString() : "";
85 public boolean cancelPriorityMessage(String receipt)
86 throws PushoverCommunicationException, PushoverConfigurationException {
87 return getMessageStatus(post(CANCEL_MESSAGE_URL.replace("{receipt}", receipt),
88 PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
91 public List<Sound> getSounds() throws PushoverCommunicationException, PushoverConfigurationException {
92 final String localApikey = config.apikey;
93 if (localApikey == null || localApikey.isEmpty()) {
94 throw new PushoverConfigurationException("@text/offline.conf-error-missing-apikey");
97 final Map<String, String> params = new HashMap<>(1);
98 params.put(PushoverMessageBuilder.MESSAGE_KEY_TOKEN, localApikey);
100 // TODO do not cache the response, cache the parsed list of sounds
101 final String content = getFromCache(buildURL(SOUNDS_URL, params));
102 final JsonObject json = content == null ? null : JsonParser.parseString(content).getAsJsonObject();
103 final JsonObject sounds = json == null || !json.has("sounds") ? null : json.get("sounds").getAsJsonObject();
105 return sounds == null ? List.of()
106 : Collections.unmodifiableList(sounds.entrySet().stream()
107 .map(entry -> new Sound(entry.getKey(), entry.getValue().getAsString()))
108 .collect(Collectors.toList()));
111 private String buildURL(String url, Map<String, String> requestParams) {
112 return requestParams.keySet().stream().map(key -> key + "=" + encodeParam(requestParams.get(key)))
113 .collect(Collectors.joining("&", url + "?", ""));
116 private String encodeParam(@Nullable String value) {
117 return value == null ? "" : URLEncoder.encode(value, StandardCharsets.UTF_8);
120 private @Nullable String getFromCache(String url) {
121 return cache.putIfAbsentAndGet(url, () -> get(url));
124 private String get(String url) throws PushoverCommunicationException, PushoverConfigurationException {
125 return executeRequest(HttpMethod.GET, url, null);
128 private String post(String url, ContentProvider body)
129 throws PushoverCommunicationException, PushoverConfigurationException {
130 return executeRequest(HttpMethod.POST, url, body);
133 private String executeRequest(HttpMethod httpMethod, String url, @Nullable ContentProvider body)
134 throws PushoverCommunicationException, PushoverConfigurationException {
135 logger.trace("Pushover request: {} - URL = '{}'", httpMethod, url);
137 final Request request = httpClient.newRequest(url).method(httpMethod).timeout(10, TimeUnit.SECONDS);
140 if (logger.isTraceEnabled()) {
141 logger.trace("Pushover request body: '{}'", body);
143 request.content(body);
146 final ContentResponse contentResponse = request.send();
148 final int httpStatus = contentResponse.getStatus();
149 final String content = contentResponse.getContentAsString();
150 logger.trace("Pushover response: status = {}, content = '{}'", httpStatus, content);
151 switch (httpStatus) {
152 case HttpStatus.OK_200:
154 case HttpStatus.BAD_REQUEST_400:
155 logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
156 throw new PushoverConfigurationException(getMessageError(content));
158 logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
159 throw new PushoverCommunicationException(content);
161 } catch (ExecutionException e) {
162 logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
163 throw new PushoverCommunicationException(e.getLocalizedMessage(), e.getCause());
164 } catch (InterruptedException | TimeoutException e) {
165 logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
166 throw new PushoverCommunicationException(e.getLocalizedMessage());
170 private String getMessageError(String content) {
171 final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
172 if (json.has("errors")) {
173 final JsonArray errors = json.get("errors").getAsJsonArray();
174 if (errors != null) {
175 return errors.toString();
178 return "Unknown error occured.";
181 private boolean getMessageStatus(String content) {
182 final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
183 return json.has("status") ? json.get("status").getAsInt() == 1 : false;
186 private boolean getMessageStatus(JsonObject json) {
187 return json.has("status") ? json.get("status").getAsInt() == 1 : false;