2 * Copyright (c) 2010-2020 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 private final JsonParser parser = new JsonParser();
66 public PushoverAPIConnection(HttpClient httpClient, PushoverAccountConfiguration config) {
67 this.httpClient = httpClient;
71 public boolean validateUser() throws PushoverCommunicationException, PushoverConfigurationException {
72 return getMessageStatus(
73 post(VALIDATE_URL, PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
76 public boolean sendMessage(PushoverMessageBuilder message)
77 throws PushoverCommunicationException, PushoverConfigurationException {
78 return getMessageStatus(post(MESSAGE_URL, message.build()));
81 public String sendPriorityMessage(PushoverMessageBuilder message)
82 throws PushoverCommunicationException, PushoverConfigurationException {
83 final JsonObject json = parser.parse(post(MESSAGE_URL, message.build())).getAsJsonObject();
84 return getMessageStatus(json) && json.has("receipt") ? json.get("receipt").getAsString() : "";
87 public boolean cancelPriorityMessage(String receipt)
88 throws PushoverCommunicationException, PushoverConfigurationException {
89 return getMessageStatus(post(CANCEL_MESSAGE_URL.replace("{receipt}", receipt),
90 PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
93 public List<Sound> getSounds() throws PushoverCommunicationException, PushoverConfigurationException {
94 final String localApikey = config.apikey;
95 if (localApikey == null || localApikey.isEmpty()) {
96 throw new PushoverConfigurationException("@text/offline.conf-error-missing-apikey");
99 final Map<String, String> params = new HashMap<>(1);
100 params.put(PushoverMessageBuilder.MESSAGE_KEY_TOKEN, localApikey);
102 // TODO do not cache the response, cache the parsed list of sounds
103 final JsonObject json = parser.parse(getFromCache(buildURL(SOUNDS_URL, params))).getAsJsonObject();
104 if (json.has("sounds")) {
105 final JsonObject sounds = json.get("sounds").getAsJsonObject();
106 if (sounds != null) {
107 return Collections.unmodifiableList(sounds.entrySet().stream()
108 .map(entry -> new Sound(entry.getKey(), entry.getValue().getAsString()))
109 .collect(Collectors.toList()));
112 return Collections.emptyList();
115 private String buildURL(String url, Map<String, String> requestParams) {
116 return requestParams.keySet().stream().map(key -> key + "=" + encodeParam(requestParams.get(key)))
117 .collect(Collectors.joining("&", url + "?", ""));
120 private String encodeParam(@Nullable String value) {
121 return value == null ? "" : URLEncoder.encode(value, StandardCharsets.UTF_8);
124 private @Nullable String getFromCache(String url) {
125 return cache.putIfAbsentAndGet(url, () -> get(url));
128 private String get(String url) throws PushoverCommunicationException, PushoverConfigurationException {
129 return executeRequest(HttpMethod.GET, url, null);
132 private String post(String url, ContentProvider body)
133 throws PushoverCommunicationException, PushoverConfigurationException {
134 return executeRequest(HttpMethod.POST, url, body);
137 private String executeRequest(HttpMethod httpMethod, String url, @Nullable ContentProvider body)
138 throws PushoverCommunicationException, PushoverConfigurationException {
139 logger.trace("Pushover request: {} - URL = '{}'", httpMethod, url);
141 final Request request = httpClient.newRequest(url).method(httpMethod).timeout(10, TimeUnit.SECONDS);
144 if (logger.isTraceEnabled()) {
145 logger.trace("Pushover request body: '{}'", body);
147 request.content(body);
150 final ContentResponse contentResponse = request.send();
152 final int httpStatus = contentResponse.getStatus();
153 final String content = contentResponse.getContentAsString();
154 logger.trace("Pushover response: status = {}, content = '{}'", httpStatus, content);
155 switch (httpStatus) {
156 case HttpStatus.OK_200:
158 case HttpStatus.BAD_REQUEST_400:
159 logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
160 throw new PushoverConfigurationException(getMessageError(content));
162 logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
163 throw new PushoverCommunicationException(content);
165 } catch (ExecutionException e) {
166 logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
167 throw new PushoverCommunicationException(e.getLocalizedMessage(), e.getCause());
168 } catch (InterruptedException | TimeoutException e) {
169 logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
170 throw new PushoverCommunicationException(e.getLocalizedMessage());
174 private String getMessageError(String content) {
175 final JsonObject json = parser.parse(content).getAsJsonObject();
176 if (json.has("errors")) {
177 final JsonArray errors = json.get("errors").getAsJsonArray();
178 if (errors != null) {
179 return errors.toString();
182 return "Unknown error occured.";
185 private boolean getMessageStatus(String content) {
186 final JsonObject json = parser.parse(content).getAsJsonObject();
187 return json.has("status") ? json.get("status").getAsInt() == 1 : false;
190 private boolean getMessageStatus(JsonObject json) {
191 return json.has("status") ? json.get("status").getAsInt() == 1 : false;