Added support for Glances API and completly reworked API

This commit is contained in:
Sven Kubiak 2018-11-22 14:45:16 +01:00
parent e70aa6ccb6
commit 69632b331f
17 changed files with 1023 additions and 392 deletions

View File

@ -3,47 +3,58 @@
JPushover JPushover
================ ================
Zero-dependency convenient class for sending messages to [Pushover][1] in Java. A zero-dependency convenient class for sending messages to [Pushover][1] in Java.
Starting with version 3.x JPushover is build for and requires Java 11. Requires Java 11.
Support [Messages API][3] and [Glances API][4].
Usage Usage
------------------ ------------------
1) Add the jpushover dependency to your pom.xml: 1) Add the jpushover dependency to your pom.xml:
```
<dependency> <dependency>
<groupId>de.svenkubiak</groupId> <groupId>de.svenkubiak</groupId>
<artifactId>jpushover</artifactId> <artifactId>jpushover</artifactId>
<version>x.x.x</version> <version>x.x.x</version>
</dependency> </dependency>
```
2) Use the JPushover object with the required informations were you want 2) Use the JPushover object with the required informations were you want
```
JPushover.create() JPushover.newMessage()
.withToken("MyToken") .withToken("MyToken")
.withUser("MyUser") .withUser("MyUser")
.withMessage("MyMessage") .withMessage("MyMessage")
.push(); .push();
```
You can additionally add all available options from the official [Pushover documentation][2] JPushover.newGlance()
.withToken("MyToken")
.withUser("MyUser")
.withText("MyText")
.push();
When using the Message API you can additionally add available options from the official [Pushover documentation][2]
You can also validate a user and token using the following method You can also validate a user and token using the following method
boolean valid = JPushover.create() boolean valid = JPushover.newMessage()
.withToken("MyToken") .withToken("MyToken")
.withUser("MyUser") .withUser("MyUser")
.validate(); .validate();
If you want more information and/or the response from the Pushover API, use the JPushoverResponse object. If you want more information and/or the response from the Pushover API, use the JPushoverResponse object.
JPushoverResponse jPushoverResponse = JPushover.create() JPushoverResponse jPushoverResponse = JPushover.newMessage()
.withToken("MyToken") .withToken("MyToken")
.withUser("MyUser") .withUser("MyUser")
.withMessage("MyMessage") .withMessage("MyMessage")
.push(); .push();
The JPushoverResponse will return the raw HTTP status code, along with the raw JSON response and a convenient boolean if the request was successful or not. The JPushoverResponse will return the raw HTTP status code, along with the raw JSON response and a convenient boolean if the request was successful or not.
[1]: https://pushover.net [1]: https://pushover.net
[2]: https://pushover.net/api [2]: https://pushover.net/api
[3]: https://pushover.net/api
[4]: https://pushover.net/api/glances

156
pom.xml
View File

@ -1,4 +1,6 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>de.svenkubiak</groupId> <groupId>de.svenkubiak</groupId>
<artifactId>jpushover</artifactId> <artifactId>jpushover</artifactId>
@ -20,6 +22,8 @@
<properties> <properties>
<java.version>11</java.version> <java.version>11</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<junit.platform.version>1.3.1</junit.platform.version>
<junit.version>5.3.1</junit.version>
</properties> </properties>
<scm> <scm>
<connection>scm:git:git@github.com:svenkubiak/JPushover.git</connection> <connection>scm:git:git@github.com:svenkubiak/JPushover.git</connection>
@ -59,7 +63,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId> <artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M2</version> <version>3.0.0-M2</version>
<configuration> <configuration>
<rules> <rules>
<DependencyConvergence> <DependencyConvergence>
@ -127,22 +131,22 @@
<artifactId>sonar-maven-plugin</artifactId> <artifactId>sonar-maven-plugin</artifactId>
<version>3.5.0.1254</version> <version>3.5.0.1254</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.owasp</groupId> <groupId>org.owasp</groupId>
<artifactId>dependency-check-maven</artifactId> <artifactId>dependency-check-maven</artifactId>
<version>3.3.2</version> <version>4.0.0</version>
<configuration> <configuration>
<cveValidForHours>12</cveValidForHours> <cveValidForHours>12</cveValidForHours>
<failBuildOnCVSS>4</failBuildOnCVSS> <failBuildOnCVSS>4</failBuildOnCVSS>
</configuration> </configuration>
<executions> <executions>
<execution> <execution>
<goals> <goals>
<goal>check</goal> <goal>check</goal>
</goals> </goals>
</execution> </execution>
</executions> </executions>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-release-plugin</artifactId> <artifactId>maven-release-plugin</artifactId>
@ -170,7 +174,7 @@
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-surefire-plugin</artifactId>
<version>2.22.0</version> <version>3.0.0-M1</version>
</plugin> </plugin>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
@ -194,6 +198,120 @@
</plugin> </plugin>
</plugins> </plugins>
</build> </build>
<dependencies>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock</artifactId>
<version>2.19.0</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.7</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.11</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</exclusion>
<exclusion>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-runner</artifactId>
<version>${junit.platform.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j18-impl</artifactId>
<version>2.11.1</version>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.8.0-beta2</version>
<scope>test</scope>
</dependency>
</dependencies>
<distributionManagement> <distributionManagement>
<snapshotRepository> <snapshotRepository>
<id>ossrh</id> <id>ossrh</id>

View File

@ -1,352 +1,32 @@
package de.svenkubiak.jpushover; package de.svenkubiak.jpushover;
import java.io.IOException; import de.svenkubiak.jpushover.apis.Glance;
import java.net.InetSocketAddress; import de.svenkubiak.jpushover.apis.Message;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
import de.svenkubiak.jpushover.enums.Constants;
import de.svenkubiak.jpushover.enums.Priority;
import de.svenkubiak.jpushover.enums.Sound;
/** /**
* *
* Zero-dependency convenient class for sending messages to Pushover * Zero-dependency convenient class for working with the Pushover API
* See https://pushover.net/api for API details
* *
* @author svenkubiak * @author svenkubiak
* *
*/ */
public class JPushover { public class JPushover {
private static final int HTTP_OK = 200; /**
private Priority pushoverPriority; * Creates a new Glance instance for the Glances API
private Sound pushoverSound; *
private String pushoverToken; * @return Glance instance
private String pushoverUser; */
private String pushoverMessage; public static Glance newGlance() {
private String pushoverDevice; return new Glance();
private String pushoverTitle; }
private String pushoverUrl;
private String pushoverUrlTitle;
private String pushoverTimestamp;
private String pushoverRetry;
private String pushoverExpire;
private String pushoverCallback;
private String proxyHost;
private int proxyPort;
private boolean pushoverHtml;
public JPushover() {
this.withSound(Sound.PUSHOVER);
this.withPriority(Priority.NORMAL);
}
/** /**
* Creates a new JPushover instance * Creates a new Message instance for the Messages API
* @return JPushover instance *
* @return Message instance
*/ */
public static JPushover create() { public static Message newMessage() {
return new JPushover(); return new Message();
}
/**
* Your application's API token
* (required)
*
* @param token The pushover API token
* @return JPushover instance
*/
public final JPushover withToken(final String token) {
this.pushoverToken = token;
return this;
}
/**
* The user/group key (not e-mail address) of your user (or you),
* viewable when logged into the @see <a href="https://pushover.net/login">pushover dashboard</a>
* (required)
*
* @param user The username
* @return JPushover instance
*/
public final JPushover withUser(final String user) {
this.pushoverUser = user;
return this;
}
/**
* Specifies how often (in seconds) the Pushover servers will send the same notification to the user.
* Only required if priority is set to emergency.
*
* @param retry Number of seconds
* @return JPushover instance
*/
public final JPushover withRetry(final String retry) {
this.pushoverRetry = retry;
return this;
}
/**
* Specifies how many seconds your notification will continue to be retried for (every retry seconds).
* Only required if priority is set to emergency.
*
* @param expire Number of seconds
* @return JPushover instance
*/
public final JPushover withExpire(final String expire) {
this.pushoverExpire = expire;
return this;
}
/**
* Your message
* (required)
*
* @param message The message to sent
* @return JPushover instance
*/
public final JPushover withMessage(final String message) {
this.pushoverMessage = message;
return this;
}
/**
* Your user's device name to send the message directly to that device,
* rather than all of the user's devices
* (optional)
*
* @param device The device name
* @return JPushover instance
*/
public final JPushover withDevice(final String device) {
this.pushoverDevice = device;
return this;
}
/**
* Your message's title, otherwise your app's name is used
* (optional)
*
* @param title The title
* @return JPushover instance
*/
public final JPushover withTitle(final String title) {
this.pushoverTitle = title;
return this;
}
/**
* A supplementary URL to show with your message
* (optional)
*
* @param url The url
* @return JPushover instance
*/
public final JPushover withUrl(final String url) {
this.pushoverUrl = url;
return this;
}
/**
* Enables HTML in the pushover message
* (optional)
*
* @return JPushover instance
*/
public final JPushover enableHtml() {
this.pushoverHtml = true;
return this;
}
/**
* A title for your supplementary URL, otherwise just the URL is shown
*
* @param urlTitle The url title
* @return JPushover instance
*/
public final JPushover withUrlTitle(final String urlTitle) {
this.pushoverUrlTitle = urlTitle;
return this;
}
/**
* A Unix timestamp of your message's date and time to display to the user,
* rather than the time your message is received by our API
*
* @param timestamp The Unix timestamp
* @return JPushover instance
*/
public final JPushover withTimestamp(final String timestamp) {
this.pushoverTimestamp = timestamp;
return this;
}
/**
* Priority of the message based on the @see <a href="https://pushover.net/api#priority">documentation</a>
* (optional)
*
* @param priority The priority enum
* @return JPushover instance
*/
public final JPushover withPriority(final Priority priority) {
this.pushoverPriority = priority;
return this;
}
/**
* The name of one of the sounds supported by device clients to override
* the user's default sound choice
* (optional)
*
* @param sound THe sound enum
* @return JPushover instance
*/
public final JPushover withSound(final Sound sound) {
this.pushoverSound = sound;
return this;
}
/**
* Callback parameter may be supplied with a publicly-accessible URL that the
* pushover servers will send a request to when the user has acknowledged your
* notification.
* Only required if priority is set to emergency.
*
* @param callback The callback URL
* @return JPushover instance
*/
public final JPushover withCallback(final String callback) {
this.pushoverCallback = callback;
return this;
}
/**
* Uses the given proxy for HTTP requests
*
* @param proxyHost The host that should be used for the Proxy
* @param proxyPort The port that should be used for the Proxy
* @return JPushover instance
*/
public final JPushover withProxy(final String proxyHost, final int proxyPort) {
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
return this;
}
/**
* Sends a validation request to pushover ensuring that the token and user
* is correct, that there is at least one active device on the account.
*
* Requires token parameter
* Requires user parameter
* Optional device parameter to check specific device
*
* @return true if token and user are valid and at least on device is on the account, false otherwise
*
* @throws IOException if validation fails
* @throws InterruptedException if validation fails
*/
public boolean validate() throws IOException, InterruptedException {
Objects.requireNonNull(this.pushoverToken, "Token is required for validation");
Objects.requireNonNull(this.pushoverUser, "User is required for validation");
NavigableMap<String, String> body = new TreeMap<>();
body.put(Constants.TOKEN.toString(), this.pushoverToken);
body.put(Constants.USER.toString(), this.pushoverUser);
var httpResponse = getResponse(toJson(body), Constants.VALIDATION_URL.toString());
var valid = false;
if (httpResponse.statusCode() == HTTP_OK) {
var response = httpResponse.body();
if (response != null && response.contains("\"status\":1")) {
valid = true;
}
}
return valid;
}
/**
* Sends a message to pushover
*
* @return JPushoverResponse instance
*
* @throws IOException if sending the message fails
* @throws InterruptedException if sending the message fails
*/
public final JPushoverResponse push() throws IOException, InterruptedException {
Objects.requireNonNull(this.pushoverToken, "Token is required for a message");
Objects.requireNonNull(this.pushoverUser, "User is required for a message");
Objects.requireNonNull(this.pushoverMessage, "Message is required for a message");
if (Priority.EMERGENCY.equals(this.pushoverPriority)) {
Objects.requireNonNull(this.pushoverRetry, "Retry is required on priority emergency");
Objects.requireNonNull(this.pushoverExpire, "Expire is required on priority emergency");
}
NavigableMap<String, String> body = new TreeMap<>();
body.put(Constants.TOKEN.toString(), this.pushoverToken);
body.put(Constants.USER.toString(), this.pushoverUser);
body.put(Constants.MESSAGE.toString(), this.pushoverMessage);
body.put(Constants.DEVICE.toString(), this.pushoverDevice);
body.put(Constants.TITLE.toString(), this.pushoverTitle);
body.put(Constants.URL.toString(), this.pushoverUrl);
body.put(Constants.RETRY.toString(), this.pushoverRetry);
body.put(Constants.EXPIRE.toString(), this.pushoverExpire);
body.put(Constants.CALLBACK.toString(), this.pushoverCallback);
body.put(Constants.URLTITLE.toString(), this.pushoverUrlTitle);
body.put(Constants.PRIORITY.toString(), this.pushoverPriority.toString());
body.put(Constants.TIMESTAMP.toString(), this.pushoverTimestamp);
body.put(Constants.SOUND.toString(), this.pushoverSound.toString());
body.put(Constants.HTML.toString(), this.pushoverHtml ? "1" : "0");
var httpResponse = getResponse(toJson(body), Constants.MESSAGES_URL.toString());
var jPushoverResponse = new JPushoverResponse().isSuccessful(false);
jPushoverResponse
.httpStatus(httpResponse.statusCode())
.response(httpResponse.body())
.isSuccessful((httpResponse.statusCode() == HTTP_OK) ? true : false);
return jPushoverResponse;
}
private HttpResponse<String> getResponse(String body, String url) throws IOException, InterruptedException {
var httpRequest = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
var httpClientBuilder = HttpClient.newBuilder();
if (this.proxyHost != null && this.proxyPort > 0) {
httpClientBuilder.proxy(ProxySelector.of(new InetSocketAddress(this.proxyHost, this.proxyPort)));
}
return httpClientBuilder.build().send(httpRequest, HttpResponse.BodyHandlers.ofString());
}
private String toJson(NavigableMap<String, String> body) {
StringBuilder buffer = new StringBuilder();
buffer.append("{");
for (Map.Entry<String, String> entry : body.entrySet()) {
buffer.append("\"").append(entry.getKey()).append("\"");
buffer.append(":");
buffer.append("\"").append(entry.getValue()).append("\"");
buffer.append(",");
}
buffer.append("}");
return buffer.toString().replace(",}", "}");
} }
} }

View File

@ -0,0 +1,142 @@
package de.svenkubiak.jpushover.apis;
import java.io.IOException;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
import de.svenkubiak.jpushover.enums.Param;
import de.svenkubiak.jpushover.http.PushoverRequest;
import de.svenkubiak.jpushover.http.PushoverResponse;
import de.svenkubiak.jpushover.utils.Urls;
import de.svenkubiak.jpushover.utils.Validate;
/**
*
* @author svenkubiak
*
*/
public class Glance {
private static final String GLANCE_URL = Urls.getGlanceUrl();
private String token;
private String user;
private String device;
private String title;
private String text;
private String subtext;
private int count;
private int percent;
private String proxyHost;
private int proxyPort;
public Glance withToken(String token) {
Objects.requireNonNull(token, "token can not be null");
this.token = token;
return this;
}
public Glance withUser(String user) {
Objects.requireNonNull(user, "user can not be null");
this.user = user;
return this;
}
public Glance withDevice(String device) {
Objects.requireNonNull(device, "device can not be null");
this.device = device;
return this;
}
/**
* A description of the data being shown, such as "Widgets Sold"
*
* @param title the title to use
* @return Glance instance
*/
public Glance withTitle(String title) {
Objects.requireNonNull(title, "title can not be null");
Validate.checkArgument(title.length() <= 100, "Title must not exceed a length of 100 characters");
this.title = title;
return this;
}
/**
* The main line of data, used on most screens
*
* @param text the text to use
* @return Glance instance
*/
public Glance withText(String text) {
Objects.requireNonNull(text, "text can not be null");
Validate.checkArgument(text.length() <= 100, "Text must not exceed a length of 100 characters");
this.text = text;
return this;
}
/**
* A second line of data
*
* @param subtext the subtext to use
* @return Glance instance
*/
public Glance withSubtext(String subtext) {
Objects.requireNonNull(subtext, "subtext can not be null");
Validate.checkArgument(subtext.length() <= 100, "subtext must not exceed a length of 100 characters");
this.subtext = subtext;
return this;
}
/**
* Shown on smaller screens; useful for simple counts
*
* @param count the count to use
* @return Glance instance
*/
public Glance withCount(int count) {
this.count = count;
return this;
}
/**
* Shown on some screens as a progress bar/circle
*
* @param percent the percent to use
* @return GLance instance
*/
public Glance withPercent(int percent) {
this.percent = percent;
return this;
}
/**
* Sends a glance to pushover
*
* @return PushoverResponse instance
*
* @throws IOException if sending the message fails
* @throws InterruptedException if sending the message fails
*/
public PushoverResponse push() throws IOException, InterruptedException {
Objects.requireNonNull(this.token, "Token is required for a glance");
Objects.requireNonNull(this.user, "User is required for a glance");
NavigableMap<String, String> body = new TreeMap<>();
body.put(Param.TOKEN.toString(), this.token);
body.put(Param.USER.toString(), this.user);
body.put(Param.DEVICE.toString(), this.device);
body.put(Param.TITLE.toString(), this.title);
body.put(Param.TEXT.toString(), this.text);
body.put(Param.SUBTEXT.toString(), this.subtext);
body.put(Param.COUNT.toString(), String.valueOf(this.count));
body.put(Param.PERCENT.toString(), String.valueOf(this.percent));
return new PushoverRequest().push(GLANCE_URL, body, this.proxyHost, this.proxyPort);
}
}

View File

@ -0,0 +1,301 @@
package de.svenkubiak.jpushover.apis;
import java.io.IOException;
import java.util.NavigableMap;
import java.util.Objects;
import java.util.TreeMap;
import de.svenkubiak.jpushover.enums.Param;
import de.svenkubiak.jpushover.enums.Priority;
import de.svenkubiak.jpushover.enums.Sound;
import de.svenkubiak.jpushover.http.PushoverRequest;
import de.svenkubiak.jpushover.http.PushoverResponse;
import de.svenkubiak.jpushover.utils.Urls;
/**
*
* @author svenkubiak
*
*/
public class Message {
private static final String MESSAGE_URL = Urls.getMessageUrl();
private static final String VALIDATION_URL = Urls.getValidationUrl();
private Priority priority;
private Sound sound;
private String token;
private String user;
private String message;
private String device;
private String title;
private String url;
private String urlTitle;
private String timestamp;
private String retry;
private String expire;
private String callback;
private String proxyHost;
private int proxyPort;
private boolean html;
public Message() {
this.withSound(Sound.PUSHOVER);
this.withPriority(Priority.NORMAL);
}
/**
* Your application's API token
* (required)
*
* @param token The pushover API token
* @return Message instance
*/
public final Message withToken(final String token) {
Objects.requireNonNull(token, "Token can not be null");
this.token = token;
return this;
}
/**
* The user/group key (not e-mail address) of your user (or you),
* viewable when logged into the @see <a href="https://pushover.net/login">pushover dashboard</a>
* (required)
*
* @param user The username
* @return Message instance
*/
public final Message withUser(final String user) {
this.user = user;
return this;
}
/**
* Specifies how often (in seconds) the Pushover servers will send the same notification to the user.
* Only required if priority is set to emergency.
*
* @param retry Number of seconds
* @return Message instance
*/
public final Message withRetry(final String retry) {
this.retry = retry;
return this;
}
/**
* Specifies how many seconds your notification will continue to be retried for (every retry seconds).
* Only required if priority is set to emergency.
*
* @param expire Number of seconds
* @return Message instance
*/
public final Message withExpire(final String expire) {
this.expire = expire;
return this;
}
/**
* Your message
* (required)
*
* @param message The message to sent
* @return Message instance
*/
public final Message withMessage(final String message) {
this.message = message;
return this;
}
/**
* Your user's device name to send the message directly to that device,
* rather than all of the user's devices
* (optional)
*
* @param device The device name
* @return Message instance
*/
public final Message withDevice(final String device) {
this.device = device;
return this;
}
/**
* Your message's title, otherwise your app's name is used
* (optional)
*
* @param title The title
* @return Message instance
*/
public final Message withTitle(final String title) {
this.title = title;
return this;
}
/**
* A supplementary URL to show with your message
* (optional)
*
* @param url The url
* @return Message instance
*/
public final Message withUrl(final String url) {
this.url = url;
return this;
}
/**
* Enables HTML in the pushover message
* (optional)
*
* @return Message instance
*/
public final Message enableHtml() {
this.html = true;
return this;
}
/**
* A title for your supplementary URL, otherwise just the URL is shown
*
* @param urlTitle The url title
* @return Message instance
*/
public final Message withUrlTitle(final String urlTitle) {
this.urlTitle = urlTitle;
return this;
}
/**
* A Unix timestamp of your message's date and time to display to the user,
* rather than the time your message is received by our API
*
* @param timestamp The Unix timestamp
* @return Message instance
*/
public final Message withTimestamp(final String timestamp) {
this.timestamp = timestamp;
return this;
}
/**
* Priority of the message based on the @see <a href="https://pushover.net/api#priority">documentation</a>
* (optional)
*
* @param priority The priority enum
* @return Message instance
*/
public final Message withPriority(final Priority priority) {
this.priority = priority;
return this;
}
/**
* The name of one of the sounds supported by device clients to override
* the user's default sound choice
* (optional)
*
* @param sound THe sound enum
* @return Message instance
*/
public final Message withSound(final Sound sound) {
this.sound = sound;
return this;
}
/**
* Callback parameter may be supplied with a publicly-accessible URL that the
* pushover servers will send a request to when the user has acknowledged your
* notification.
* Only required if priority is set to emergency.
*
* @param callback The callback URL
* @return Message instance
*/
public final Message withCallback(final String callback) {
this.callback = callback;
return this;
}
/**
* Uses the given proxy for HTTP requests
*
* @param proxyHost The host that should be used for the Proxy
* @param proxyPort The port that should be used for the Proxy
* @return Message instance
*/
public final Message withProxy(final String proxyHost, final int proxyPort) {
this.proxyHost = proxyHost;
this.proxyPort = proxyPort;
return this;
}
/**
* Sends a validation request to pushover ensuring that the token and user
* is correct, that there is at least one active device on the account.
*
* Requires token parameter
* Requires user parameter
* Optional device parameter to check specific device
*
* @return true if token and user are valid and at least on device is on the account, false otherwise
*
* @throws IOException if validation fails
* @throws InterruptedException if validation fails
*/
public boolean validate() throws IOException, InterruptedException {
Objects.requireNonNull(this.token, "Token is required for validation");
Objects.requireNonNull(this.user, "User is required for validation");
NavigableMap<String, String> body = new TreeMap<>();
body.put(Param.TOKEN.toString(), this.token);
body.put(Param.USER.toString(), this.user);
var pushoverResponse = new PushoverRequest().push(VALIDATION_URL, body, this.proxyHost, this.proxyPort);
var valid = false;
if (pushoverResponse.getHttpStatus() == 200) {
var response = pushoverResponse.getResponse();
if (response != null && response.contains("\"status\":1")) {
valid = true;
}
}
return valid;
}
/**
* Sends a message to pushover
*
* @return PushoverResponse instance
*
* @throws IOException if sending the message fails
* @throws InterruptedException if sending the message fails
*/
public final PushoverResponse push() throws IOException, InterruptedException {
Objects.requireNonNull(this.token, "Token is required for a message");
Objects.requireNonNull(this.user, "User is required for a message");
Objects.requireNonNull(this.message, "Message is required for a message");
if (Priority.EMERGENCY.equals(this.priority)) {
Objects.requireNonNull(this.retry, "Retry is required on priority emergency");
Objects.requireNonNull(this.expire, "Expire is required on priority emergency");
}
NavigableMap<String, String> body = new TreeMap<>();
body.put(Param.TOKEN.toString(), this.token);
body.put(Param.USER.toString(), this.user);
body.put(Param.MESSAGE.toString(), this.message);
body.put(Param.DEVICE.toString(), this.device);
body.put(Param.TITLE.toString(), this.title);
body.put(Param.URL.toString(), this.url);
body.put(Param.RETRY.toString(), this.retry);
body.put(Param.EXPIRE.toString(), this.expire);
body.put(Param.CALLBACK.toString(), this.callback);
body.put(Param.URLTITLE.toString(), this.urlTitle);
body.put(Param.PRIORITY.toString(), this.priority.toString());
body.put(Param.TIMESTAMP.toString(), this.timestamp);
body.put(Param.SOUND.toString(), this.sound.toString());
body.put(Param.HTML.toString(), this.html ? "1" : "0");
return new PushoverRequest().push(MESSAGE_URL, body, this.proxyHost, this.proxyPort);
}
}

View File

@ -5,14 +5,13 @@ package de.svenkubiak.jpushover.enums;
* @author svenkubiak * @author svenkubiak
* *
*/ */
public enum Constants { public enum Param {
ATTACHMENT("attachment"), ATTACHMENT("attachment"),
CALLBACK("callback"), CALLBACK("callback"),
DEVICE("device"), DEVICE("device"),
EXPIRE("expire"), EXPIRE("expire"),
HTML("html"), HTML("html"),
MESSAGE("message"), MESSAGE("message"),
MESSAGES_URL("https://api.pushover.net/1/messages.json"),
PRIORITY("priority"), PRIORITY("priority"),
RETRY("retry"), RETRY("retry"),
SOUND("sound"), SOUND("sound"),
@ -22,11 +21,14 @@ public enum Constants {
URL("url"), URL("url"),
URLTITLE("urltitle"), URLTITLE("urltitle"),
USER("user"), USER("user"),
VALIDATION_URL("https://api.pushover.net/1/users/validate.json"); TEXT("text"),
SUBTEXT("subtext"),
COUNT("count"),
PERCENT("percent");
private final String value; private final String value;
Constants (String value) { Param (String value) {
this.value = value; this.value = value;
} }

View File

@ -0,0 +1,68 @@
package de.svenkubiak.jpushover.http;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProxySelector;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Objects;
/**
*
* @author svenkubiak
*
*/
public class PushoverRequest {
public PushoverResponse push(String url, NavigableMap<String, String> body, String proxyHost, int proxyPort) throws IOException, InterruptedException {
Objects.requireNonNull(url, "API URL can not be null");
Objects.requireNonNull(body, "body can not be null");
var httpResponse = getResponse(toJson(body), url, proxyHost, proxyPort);
var jPushoverResponse = new PushoverResponse().isSuccessful(false);
jPushoverResponse
.httpStatus(httpResponse.statusCode())
.response(httpResponse.body())
.isSuccessful((httpResponse.statusCode() == 200) ? true : false);
return jPushoverResponse;
}
private HttpResponse<String> getResponse(String body, String url, String proxyHost, int proxyPort) throws IOException, InterruptedException {
var httpRequest = HttpRequest.newBuilder()
.uri(URI.create(url))
.timeout(Duration.ofSeconds(5))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
var httpClientBuilder = HttpClient.newBuilder();
if (proxyHost != null && proxyPort > 0) {
httpClientBuilder.proxy(ProxySelector.of(new InetSocketAddress(proxyHost, proxyPort)));
}
return httpClientBuilder.build().send(httpRequest, HttpResponse.BodyHandlers.ofString());
}
private String toJson(NavigableMap<String, String> body) {
StringBuilder buffer = new StringBuilder();
buffer.append("{");
for (Map.Entry<String, String> entry : body.entrySet()) {
buffer.append("\"").append(entry.getKey()).append("\"");
buffer.append(":");
buffer.append("\"").append(entry.getValue()).append("\"");
buffer.append(",");
}
buffer.append("}");
return buffer.toString().replace(",}", "}");
}
}

View File

@ -1,26 +1,26 @@
package de.svenkubiak.jpushover; package de.svenkubiak.jpushover.http;
/** /**
* *
* @author svenkubiak * @author svenkubiak
* *
*/ */
public class JPushoverResponse { public class PushoverResponse {
private String pushoverResponse; private String pushoverResponse;
private int pushoverHttpStatus; private int pushoverHttpStatus;
private boolean pushoverSuccessful; private boolean pushoverSuccessful;
public JPushoverResponse response(String response) { public PushoverResponse response(String response) {
this.pushoverResponse = response; this.pushoverResponse = response;
return this; return this;
} }
public JPushoverResponse httpStatus(int httpStatus) { public PushoverResponse httpStatus(int httpStatus) {
this.pushoverHttpStatus = httpStatus; this.pushoverHttpStatus = httpStatus;
return this; return this;
} }
public JPushoverResponse isSuccessful(boolean successful) { public PushoverResponse isSuccessful(boolean successful) {
this.pushoverSuccessful = successful; this.pushoverSuccessful = successful;
return this; return this;
} }
@ -40,7 +40,7 @@ public class JPushoverResponse {
} }
/** /**
* @return true if the api returned a HTTP status code 200, false otherwise * @return true if the API returned a HTTP status code 200, false otherwise
*/ */
public boolean isSuccessful() { public boolean isSuccessful() {
return pushoverSuccessful; return pushoverSuccessful;

View File

@ -0,0 +1,35 @@
package de.svenkubiak.jpushover.utils;
/**
*
* @author svenkubiak
*
*/
public class Urls {
public static String getGlanceUrl() {
String mode = System.getProperty("mode");
if (("test").equals(mode)) {
return "http://127.0.0.1:8080/1/glances.json";
}
return "https://api.pushover.net/1/glances.json";
}
public static String getMessageUrl() {
String mode = System.getProperty("mode");
if (("test").equals(mode)) {
return "http://127.0.0.1:8080/1/messages.json";
}
return "https://api.pushover.net/1/messages.json";
}
public static String getValidationUrl() {
String mode = System.getProperty("mode");
if (("test").equals(mode)) {
return "http://127.0.0.1:8080/1/users/validate.json";
}
return "https://api.pushover.net/1/users/validate.json";
}
}

View File

@ -0,0 +1,22 @@
package de.svenkubiak.jpushover.utils;
/**
*
* @author svenkubiak
*
*/
public final class Validate {
/**
* Ensures the truth of an expression involving one or more parameters to the calling method.
*
* @param expression a boolean expression
* @param errorMessage the exception message to use if the check fails; will be converted to a string using {@link String#valueOf(Object)}
*
* @throws IllegalArgumentException if {@code expression} is false
*/
public static void checkArgument(boolean expression, Object errorMessage) {
if (!expression) {
throw new IllegalArgumentException(String.valueOf(errorMessage));
}
}
}

View File

@ -1,5 +0,0 @@
module jpushover {
requires java.net.http;
exports de.svenkubiak.jpushover;
exports de.svenkubiak.jpushover.enums;
}

View File

@ -0,0 +1,23 @@
package jpushover;
import static org.junit.Assert.assertTrue;
import de.svenkubiak.jpushover.JPushover;
import de.svenkubiak.jpushover.apis.Glance;
import de.svenkubiak.jpushover.apis.Message;
/**
*
* @author svenkubiak
*
*/
public class JPushoverTest {
public void testNewGlance() {
assertTrue(JPushover.newGlance() instanceof Glance);
}
public void testNewMessage() {
assertTrue(JPushover.newMessage() instanceof Message);
}
}

View File

@ -0,0 +1,29 @@
package jpushover;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
import com.github.tomakehurst.wiremock.WireMockServer;
/**
*
* @author svenkubiak
*
*/
public final class MockServer {
private static WireMockServer wireMockServer;
private static boolean started;
public MockServer() {
}
public static void start() {
if (!started) {
System.setProperty("mode", "test");
wireMockServer = new WireMockServer(options()
.bindAddress("127.0.0.1")
);
wireMockServer.start();
started = true;
}
}
}

View File

@ -0,0 +1,77 @@
package jpushover.apis;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import de.svenkubiak.jpushover.JPushover;
import de.svenkubiak.jpushover.http.PushoverResponse;
import jpushover.MockServer;
/**
*
* @author svenkubiak
*
*/
public class GlanceTest {
private static final String APPLICATION_JSON = "application/json; charset=utf-8";
private static final String CONTENT_TYPE = "Content-Type";
private GlanceTest () {
MockServer.start();
}
@Test()
public void testTokenRequired() throws IOException, InterruptedException {
stubFor(post(urlEqualTo("/1/glances.json"))
.willReturn(aResponse()
.withStatus(400)
.withHeader(CONTENT_TYPE, APPLICATION_JSON)));
Assertions.assertThrows(NullPointerException.class, () -> {
JPushover.newGlance().push();
});
}
@Test()
public void testUserRequired() throws IOException, InterruptedException {
stubFor(post(urlEqualTo("/1/glances.json"))
.willReturn(aResponse()
.withStatus(400)
.withHeader(CONTENT_TYPE, APPLICATION_JSON)));
Assertions.assertThrows(NullPointerException.class, () -> {
JPushover.newGlance().withToken("token").push();
});
}
@Test()
public void testPushWithoutContent() throws IOException, InterruptedException {
stubFor(post(urlEqualTo("/1/glances.json"))
.willReturn(aResponse()
.withStatus(400)
.withHeader(CONTENT_TYPE, APPLICATION_JSON)));
PushoverResponse response = JPushover.newGlance().withToken("token").withUser("user").push();
assertFalse(response.isSuccessful());
}
@Test
public void testPush() throws IOException, InterruptedException {
stubFor(post(urlEqualTo("/1/glances.json"))
.willReturn(aResponse()
.withStatus(200)
.withHeader(CONTENT_TYPE, APPLICATION_JSON)));
PushoverResponse response = JPushover.newGlance().withToken("foo").withUser("bla").withText("foobar").push();
assertTrue(response.isSuccessful());
}
}

View File

@ -0,0 +1,89 @@
package jpushover.apis;
import static com.github.tomakehurst.wiremock.client.WireMock.aResponse;
import static com.github.tomakehurst.wiremock.client.WireMock.post;
import static com.github.tomakehurst.wiremock.client.WireMock.stubFor;
import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import de.svenkubiak.jpushover.JPushover;
import de.svenkubiak.jpushover.http.PushoverResponse;
import jpushover.MockServer;
/**
*
* @author svenkubiak
*
*/
public class MessageTest {
private static final String APPLICATION_JSON = "application/json; charset=utf-8";
private static final String CONTENT_TYPE = "Content-Type";
private MessageTest () {
MockServer.start();
}
@Test()
public void testTokenRequired() throws IOException, InterruptedException {
stubFor(post(urlEqualTo("/1/messages.json"))
.willReturn(aResponse()
.withStatus(400)
.withHeader(CONTENT_TYPE, APPLICATION_JSON)));
Assertions.assertThrows(NullPointerException.class, () -> {
JPushover.newMessage().push();
});
}
@Test()
public void testUserRequired() throws IOException, InterruptedException {
stubFor(post(urlEqualTo("/1/messages.json"))
.willReturn(aResponse()
.withStatus(400)
.withHeader(CONTENT_TYPE, APPLICATION_JSON)));
Assertions.assertThrows(NullPointerException.class, () -> {
JPushover.newMessage().withToken("token").push();
});
}
@Test()
public void testMessageRequired() throws IOException, InterruptedException {
stubFor(post(urlEqualTo("/1/messages.json"))
.willReturn(aResponse()
.withStatus(400)
.withHeader(CONTENT_TYPE, APPLICATION_JSON)));
Assertions.assertThrows(NullPointerException.class, () -> {
JPushover.newMessage().withToken("token").withUser("user").push();
});
}
@Test()
public void testPushWithoutContent() throws IOException, InterruptedException {
stubFor(post(urlEqualTo("/1/messages.json"))
.willReturn(aResponse()
.withStatus(400)
.withHeader(CONTENT_TYPE, APPLICATION_JSON)));
PushoverResponse response = JPushover.newMessage().withToken("token").withUser("user").withMessage("").push();
assertFalse(response.isSuccessful());
}
@Test
public void testPush() throws IOException, InterruptedException {
stubFor(post(urlEqualTo("/1/messages.json"))
.willReturn(aResponse()
.withStatus(200)
.withHeader(CONTENT_TYPE, APPLICATION_JSON)));
PushoverResponse response = JPushover.newMessage().withToken("foo").withUser("bla").withMessage("foobar").push();
assertTrue(response.isSuccessful());
}
}

View File

@ -0,0 +1,26 @@
package jpushover.utils;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import de.svenkubiak.jpushover.utils.Validate;
/**
*
* @author svenkubiak
*
*/
public class ValidateTest {
@Test
public void testTrue() {
Validate.checkArgument(true, "foo");
}
@Test
public void testFalse() {
Assertions.assertThrows(IllegalArgumentException.class, () -> {
Validate.checkArgument(false, "bar");
});
}
}

View File

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<Configuration>
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n" />
</Console>
</Appenders>
<Loggers>
<Root level="info">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>