public static final String DEFAULT_SOUND = "default";
public static final String DEFAULT_TITLE = "openHAB";
+
+ public static final String TEXT_OFFLINE_COMMUNICATION_ERROR = "@text/offline.communication-error";
+ public static final String TEXT_OFFLINE_CONF_ERROR_MISSING_APIKEY = "@text/offline.conf-error-missing-apikey";
+ public static final String TEXT_OFFLINE_CONF_ERROR_MISSING_USER = "@text/offline.conf-error-missing-user";
+ public static final String TEXT_OFFLINE_CONF_ERROR_UNKNOWN = "@text/offline.conf-error-unknown";
+ public static final String TEXT_ERROR_SKIP_SENDING_MESSAGE = "@text/error.skip-sending-message";
}
*/
package org.openhab.binding.pushover.internal.connection;
+import static org.openhab.binding.pushover.internal.PushoverBindingConstants.*;
+
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import org.eclipse.jetty.http.HttpStatus;
import org.openhab.binding.pushover.internal.config.PushoverAccountConfiguration;
import org.openhab.binding.pushover.internal.dto.Sound;
-import org.openhab.core.cache.ExpiringCacheMap;
+import org.openhab.core.cache.ExpiringCache;
+import org.openhab.core.i18n.CommunicationException;
+import org.openhab.core.i18n.ConfigurationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
+import com.google.gson.JsonSyntaxException;
/**
* The {@link PushoverAPIConnection} is responsible for handling the connections to Pushover Messages API.
@NonNullByDefault
public class PushoverAPIConnection {
+ private static final String JSON_VALUE_ERRORS = "errors";
+ private static final String JSON_VALUE_RECEIPT = "receipt";
+ private static final String JSON_VALUE_SOUNDS = "sounds";
+ private static final String JSON_VALUE_STATUS = "status";
+
private final Logger logger = LoggerFactory.getLogger(PushoverAPIConnection.class);
private static final String VALIDATE_URL = "https://api.pushover.net/1/users/validate.json";
private static final String MESSAGE_URL = "https://api.pushover.net/1/messages.json";
- private static final String CANCEL_MESSAGE_URL = "https://api.pushover.net/1/receipts/{receipt}/cancel.json";
+ private static final String CANCEL_MESSAGE_URL = "https://api.pushover.net/1/receipts/%s/cancel.json";
private static final String SOUNDS_URL = "https://api.pushover.net/1/sounds.json";
private final HttpClient httpClient;
private final PushoverAccountConfiguration config;
- private final ExpiringCacheMap<String, String> cache = new ExpiringCacheMap<>(TimeUnit.DAYS.toMillis(1));
+ private final ExpiringCache<List<Sound>> cache = new ExpiringCache<>(TimeUnit.DAYS.toMillis(1),
+ this::getSoundsFromSource);
public PushoverAPIConnection(HttpClient httpClient, PushoverAccountConfiguration config) {
this.httpClient = httpClient;
this.config = config;
}
- public boolean validateUser() throws PushoverCommunicationException, PushoverConfigurationException {
+ public boolean validateUser() throws CommunicationException, ConfigurationException {
return getMessageStatus(
post(VALIDATE_URL, PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
}
- public boolean sendMessage(PushoverMessageBuilder message)
- throws PushoverCommunicationException, PushoverConfigurationException {
+ public boolean sendMessage(PushoverMessageBuilder message) throws CommunicationException, ConfigurationException {
return getMessageStatus(post(MESSAGE_URL, message.build()));
}
public String sendPriorityMessage(PushoverMessageBuilder message)
- throws PushoverCommunicationException, PushoverConfigurationException {
+ throws CommunicationException, ConfigurationException {
final JsonObject json = JsonParser.parseString(post(MESSAGE_URL, message.build())).getAsJsonObject();
- return getMessageStatus(json) && json.has("receipt") ? json.get("receipt").getAsString() : "";
+ return getMessageStatus(json) && json.has(JSON_VALUE_RECEIPT) ? json.get(JSON_VALUE_RECEIPT).getAsString() : "";
}
- public boolean cancelPriorityMessage(String receipt)
- throws PushoverCommunicationException, PushoverConfigurationException {
- return getMessageStatus(post(CANCEL_MESSAGE_URL.replace("{receipt}", receipt),
+ public boolean cancelPriorityMessage(String receipt) throws CommunicationException, ConfigurationException {
+ return getMessageStatus(post(String.format(CANCEL_MESSAGE_URL, receipt),
PushoverMessageBuilder.getInstance(config.apikey, config.user).build()));
}
- public List<Sound> getSounds() throws PushoverCommunicationException, PushoverConfigurationException {
+ public @Nullable List<Sound> getSounds() {
+ return cache.getValue();
+ }
+
+ private List<Sound> getSoundsFromSource() throws CommunicationException, ConfigurationException {
final String localApikey = config.apikey;
- if (localApikey == null || localApikey.isEmpty()) {
- throw new PushoverConfigurationException("@text/offline.conf-error-missing-apikey");
+ if (localApikey == null || localApikey.isBlank()) {
+ throw new ConfigurationException(TEXT_OFFLINE_CONF_ERROR_MISSING_APIKEY);
}
- final Map<String, String> params = new HashMap<>(1);
- params.put(PushoverMessageBuilder.MESSAGE_KEY_TOKEN, localApikey);
-
- // TODO do not cache the response, cache the parsed list of sounds
- final String content = getFromCache(buildURL(SOUNDS_URL, params));
- final JsonObject json = content == null ? null : JsonParser.parseString(content).getAsJsonObject();
- final JsonObject sounds = json == null || !json.has("sounds") ? null : json.get("sounds").getAsJsonObject();
-
- return sounds == null ? List.of()
- : sounds.entrySet().stream().map(entry -> new Sound(entry.getKey(), entry.getValue().getAsString()))
+ try {
+ final String content = get(
+ buildURL(SOUNDS_URL, Map.of(PushoverMessageBuilder.MESSAGE_KEY_TOKEN, localApikey)));
+ final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
+ final JsonObject sounds = json.has(JSON_VALUE_SOUNDS) ? json.get(JSON_VALUE_SOUNDS).getAsJsonObject()
+ : null;
+ if (sounds != null) {
+ return sounds.entrySet().stream()
+ .map(entry -> new Sound(entry.getKey(), entry.getValue().getAsString()))
.collect(Collectors.toUnmodifiableList());
+ }
+ } catch (JsonSyntaxException e) {
+ // do nothing
+ }
+ return List.of();
}
private String buildURL(String url, Map<String, String> requestParams) {
return value == null ? "" : URLEncoder.encode(value, StandardCharsets.UTF_8);
}
- private @Nullable String getFromCache(String url) {
- return cache.putIfAbsentAndGet(url, () -> get(url));
- }
-
- private String get(String url) throws PushoverCommunicationException, PushoverConfigurationException {
+ private String get(String url) throws CommunicationException, ConfigurationException {
return executeRequest(HttpMethod.GET, url, null);
}
- private String post(String url, ContentProvider body)
- throws PushoverCommunicationException, PushoverConfigurationException {
+ private String post(String url, ContentProvider body) throws CommunicationException, ConfigurationException {
return executeRequest(HttpMethod.POST, url, body);
}
private synchronized String executeRequest(HttpMethod httpMethod, String url, @Nullable ContentProvider body)
- throws PushoverCommunicationException, PushoverConfigurationException {
+ throws CommunicationException, ConfigurationException {
logger.trace("Pushover request: {} - URL = '{}'", httpMethod, url);
try {
final Request request = httpClient.newRequest(url).method(httpMethod).timeout(config.timeout,
return content;
case HttpStatus.BAD_REQUEST_400:
logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
- throw new PushoverConfigurationException(getMessageError(content));
+ throw new ConfigurationException(getMessageError(content));
default:
logger.debug("Pushover server responded with status code {}: {}", httpStatus, content);
- throw new PushoverCommunicationException(content);
+ throw new CommunicationException(content);
}
} catch (ExecutionException e) {
- logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
- throw new PushoverCommunicationException(e.getLocalizedMessage(), e.getCause());
- } catch (InterruptedException | TimeoutException e) {
- logger.debug("Exception occurred during execution: {}", e.getLocalizedMessage(), e);
- throw new PushoverCommunicationException(e.getLocalizedMessage());
+ String message = e.getMessage();
+ logger.debug("ExecutionException occurred during execution: {}", message, e);
+ throw new CommunicationException(message == null ? TEXT_OFFLINE_COMMUNICATION_ERROR : message,
+ e.getCause());
+ } catch (TimeoutException e) {
+ String message = e.getMessage();
+ logger.debug("TimeoutException occurred during execution: {}", message, e);
+ throw new CommunicationException(message == null ? TEXT_OFFLINE_COMMUNICATION_ERROR : message);
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ String message = e.getMessage();
+ logger.debug("InterruptedException occurred during execution: {}", message, e);
+ throw new CommunicationException(message == null ? TEXT_OFFLINE_COMMUNICATION_ERROR : message);
}
}
private String getMessageError(String content) {
final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
- final JsonElement errorsElement = json.get("errors");
+ final JsonElement errorsElement = json.get(JSON_VALUE_ERRORS);
if (errorsElement != null && errorsElement.isJsonArray()) {
return errorsElement.getAsJsonArray().toString();
}
- return "@text/offline.conf-error-unknown";
+ return TEXT_OFFLINE_CONF_ERROR_UNKNOWN;
}
private boolean getMessageStatus(String content) {
final JsonObject json = JsonParser.parseString(content).getAsJsonObject();
- return json.has("status") ? json.get("status").getAsInt() == 1 : false;
+ return json.has(JSON_VALUE_STATUS) ? json.get(JSON_VALUE_STATUS).getAsInt() == 1 : false;
}
private boolean getMessageStatus(JsonObject json) {
- return json.has("status") ? json.get("status").getAsInt() == 1 : false;
+ return json.has(JSON_VALUE_STATUS) ? json.get(JSON_VALUE_STATUS).getAsInt() == 1 : false;
}
}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.pushover.internal.connection;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-import org.eclipse.jdt.annotation.Nullable;
-
-/**
- * The {@link PushoverCommunicationException} is a configuration exception for the connections to Pushover Messages API.
- *
- * @author Christoph Weitkamp - Initial contribution
- */
-@NonNullByDefault
-public class PushoverCommunicationException extends RuntimeException {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a new exception with null as its detail message.
- */
- public PushoverCommunicationException() {
- super();
- }
-
- /**
- * Constructs a new exception with the specified detail message.
- *
- * @param message Detail message
- */
- public PushoverCommunicationException(@Nullable String message) {
- super(message);
- }
-
- /**
- * Constructs a new exception with the specified cause.
- *
- * @param cause The cause
- */
- public PushoverCommunicationException(@Nullable Throwable cause) {
- super(cause);
- }
-
- /**
- * Constructs a new exception with the specified detail message and cause.
- *
- * @param message Detail message
- * @param cause The cause
- */
- public PushoverCommunicationException(@Nullable String message, @Nullable Throwable cause) {
- super(message, cause);
- }
-}
+++ /dev/null
-/**
- * Copyright (c) 2010-2022 Contributors to the openHAB project
- *
- * See the NOTICE file(s) distributed with this work for additional
- * information.
- *
- * This program and the accompanying materials are made available under the
- * terms of the Eclipse Public License 2.0 which is available at
- * http://www.eclipse.org/legal/epl-2.0
- *
- * SPDX-License-Identifier: EPL-2.0
- */
-package org.openhab.binding.pushover.internal.connection;
-
-import org.eclipse.jdt.annotation.NonNullByDefault;
-
-/**
- * The {@link PushoverConfigurationException} is a configuration exception for the connections to Pushover Messages API.
- *
- * @author Christoph Weitkamp - Initial contribution
- */
-@NonNullByDefault
-public class PushoverConfigurationException extends IllegalArgumentException {
-
- private static final long serialVersionUID = 1L;
-
- /**
- * Constructs a new exception with null as its detail message.
- */
- public PushoverConfigurationException() {
- super();
- }
-
- /**
- * Constructs a new exception with the specified detail message.
- *
- * @param message Detail message
- */
- public PushoverConfigurationException(String message) {
- super(message);
- }
-
- /**
- * Constructs a new exception with the specified cause.
- *
- * @param cause The cause
- */
- public PushoverConfigurationException(Throwable cause) {
- super(cause);
- }
-
- /**
- * Constructs a new exception with the specified detail message and cause.
- *
- * @param message Detail message
- * @param cause The cause
- */
- public PushoverConfigurationException(String message, Throwable cause) {
- super(message, cause);
- }
-}
*/
package org.openhab.binding.pushover.internal.connection;
+import static org.openhab.binding.pushover.internal.PushoverBindingConstants.*;
+
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import org.eclipse.jetty.client.util.MultiPartContentProvider;
import org.eclipse.jetty.client.util.PathContentProvider;
import org.eclipse.jetty.client.util.StringContentProvider;
+import org.openhab.core.i18n.CommunicationException;
+import org.openhab.core.i18n.ConfigurationException;
import org.openhab.core.io.net.http.HttpUtil;
import org.openhab.core.library.types.RawType;
import org.slf4j.Logger;
private boolean html = false;
private boolean monospace = false;
- private PushoverMessageBuilder(String apikey, String user) throws PushoverConfigurationException {
+ private PushoverMessageBuilder(String apikey, String user) throws ConfigurationException {
body.addFieldPart(MESSAGE_KEY_TOKEN, new StringContentProvider(apikey), null);
body.addFieldPart(MESSAGE_KEY_USER, new StringContentProvider(user), null);
}
public static PushoverMessageBuilder getInstance(@Nullable String apikey, @Nullable String user)
- throws PushoverConfigurationException {
- if (apikey == null || apikey.isEmpty()) {
- throw new PushoverConfigurationException("@text/offline.conf-error-missing-apikey");
+ throws ConfigurationException {
+ if (apikey == null || apikey.isBlank()) {
+ throw new ConfigurationException(TEXT_OFFLINE_CONF_ERROR_MISSING_APIKEY);
}
- if (user == null || user.isEmpty()) {
- throw new PushoverConfigurationException("@text/offline.conf-error-missing-user");
+ if (user == null || user.isBlank()) {
+ throw new ConfigurationException(TEXT_OFFLINE_CONF_ERROR_MISSING_USER);
}
return new PushoverMessageBuilder(apikey, user);
return this;
}
- public ContentProvider build() throws PushoverCommunicationException {
+ public ContentProvider build() throws CommunicationException {
if (message != null) {
if (message.length() > MAX_MESSAGE_LENGTH) {
throw new IllegalArgumentException(String.format(
return body;
}
- private Path createTempFile(byte[] data) throws PushoverCommunicationException {
+ private Path createTempFile(byte[] data) throws CommunicationException {
try {
Path tmpFile = Files.createTempFile("pushover-", ".tmp");
return Files.write(tmpFile, data);
} catch (IOException e) {
- logger.debug("IOException occurred while creating temp file - skip sending message: {}",
- e.getLocalizedMessage(), e);
- throw new PushoverCommunicationException(
- String.format("Skip sending the message: %s", e.getLocalizedMessage()), e);
+ logger.debug("IOException occurred while creating temp file - skip sending the message: {}", e.getMessage(),
+ e);
+ throw new CommunicationException(TEXT_ERROR_SKIP_SENDING_MESSAGE, e.getCause(), e.getLocalizedMessage());
}
}
- private void addFilePart(Path path, @Nullable String contentType) throws PushoverCommunicationException {
+ private void addFilePart(Path path, @Nullable String contentType) throws CommunicationException {
try {
body.addFilePart(MESSAGE_KEY_ATTACHMENT, path.toFile().getName(),
new PathContentProvider(contentType == null ? DEFAULT_CONTENT_TYPE : contentType, path), null);
} catch (IOException e) {
- logger.debug("IOException occurred while adding content - skip sending message: {}",
- e.getLocalizedMessage(), e);
- throw new PushoverCommunicationException(
- String.format("Skip sending the message: %s", e.getLocalizedMessage()), e);
+ logger.debug("IOException occurred while adding content - skip sending the message: {}", e.getMessage(), e);
+ throw new CommunicationException(TEXT_ERROR_SKIP_SENDING_MESSAGE, e.getCause(), e.getLocalizedMessage());
}
}
}
*/
package org.openhab.binding.pushover.internal.handler;
-import static org.openhab.binding.pushover.internal.PushoverBindingConstants.DEFAULT_SOUND;
+import static org.openhab.binding.pushover.internal.PushoverBindingConstants.*;
import java.util.Collection;
import java.util.List;
import org.openhab.binding.pushover.internal.config.PushoverAccountConfiguration;
import org.openhab.binding.pushover.internal.config.PushoverConfigOptionProvider;
import org.openhab.binding.pushover.internal.connection.PushoverAPIConnection;
-import org.openhab.binding.pushover.internal.connection.PushoverCommunicationException;
-import org.openhab.binding.pushover.internal.connection.PushoverConfigurationException;
import org.openhab.binding.pushover.internal.connection.PushoverMessageBuilder;
import org.openhab.binding.pushover.internal.dto.Sound;
+import org.openhab.core.i18n.CommunicationException;
+import org.openhab.core.i18n.ConfigurationException;
import org.openhab.core.thing.ChannelUID;
import org.openhab.core.thing.Thing;
import org.openhab.core.thing.ThingStatus;
boolean configValid = true;
final String apikey = config.apikey;
- if (apikey == null || apikey.isEmpty()) {
+ if (apikey == null || apikey.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
- "@text/offline.conf-error-missing-apikey");
+ TEXT_OFFLINE_CONF_ERROR_MISSING_APIKEY);
configValid = false;
}
final String user = config.user;
- if (user == null || user.isEmpty()) {
+ if (user == null || user.isBlank()) {
updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
- "@text/offline.conf-error-missing-user");
+ TEXT_OFFLINE_CONF_ERROR_MISSING_USER);
configValid = false;
}
*/
public List<Sound> getSounds() {
try {
- return connection != null ? connection.getSounds() : PushoverAccountConfiguration.DEFAULT_SOUNDS;
- } catch (PushoverCommunicationException e) {
+ if (connection != null) {
+ List<Sound> sounds = connection.getSounds();
+ if (sounds != null) {
+ return sounds;
+ }
+ }
+ } catch (CommunicationException e) {
// do nothing, causing exception is already logged
- } catch (PushoverConfigurationException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ } catch (ConfigurationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getRawMessage());
}
return PushoverAccountConfiguration.DEFAULT_SOUNDS;
}
if (connection != null) {
try {
return connection.sendMessage(messageBuilder);
- } catch (PushoverCommunicationException e) {
+ } catch (CommunicationException e) {
// do nothing, causing exception is already logged
- } catch (PushoverConfigurationException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ } catch (ConfigurationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getRawMessage());
}
return false;
} else {
if (connection != null) {
try {
return connection.sendPriorityMessage(messageBuilder);
- } catch (PushoverCommunicationException e) {
+ } catch (CommunicationException e) {
// do nothing, causing exception is already logged
- } catch (PushoverConfigurationException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ } catch (ConfigurationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getRawMessage());
}
return "";
} else {
if (connection != null) {
try {
return connection.cancelPriorityMessage(receipt);
- } catch (PushoverCommunicationException e) {
+ } catch (CommunicationException e) {
// do nothing, causing exception is already logged
- } catch (PushoverConfigurationException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ } catch (ConfigurationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getRawMessage());
}
return false;
} else {
try {
connection.validateUser();
updateStatus(ThingStatus.ONLINE);
- } catch (PushoverCommunicationException | PushoverConfigurationException e) {
- updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage());
+ } catch (CommunicationException | ConfigurationException e) {
+ updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getRawMessage());
}
}
}
# user defined messages
+offline.communication-error = An unexpected exception occurred during execution.
offline.conf-error-missing-apikey = The 'apikey' parameter must be configured.
offline.conf-error-missing-user = The 'user' parameter must be configured.
offline.conf-error-unknown = An unknown error occurred.
+error.skip-sending-message = Skip sending the message: {0}.
# actions
*
* @author Christoph Weitkamp - Initial contribution
*/
+@NonNullByDefault
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.WARN)
public class PushoverActionsTest {
private static final String URL_TITLE = "Some Link";
private static final String RECEIPT = "12345";
- @NonNullByDefault
private final ThingActions thingActionsStub = new ThingActions() {
@Override
public void setThingHandler(ThingHandler handler) {
}
};
- private @Mock PushoverAccountHandler mockPushoverAccountHandler;
-
- private PushoverActions pushoverThingActions;
+ private @NonNullByDefault({}) @Mock PushoverAccountHandler mockPushoverAccountHandler;
+ private @NonNullByDefault({}) PushoverActions pushoverThingActions;
@BeforeEach
public void setUp() {