diff --git a/README.md b/README.md index ac1d7d9..d35a955 100644 --- a/README.md +++ b/README.md @@ -3,47 +3,58 @@ 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 ------------------ 1) Add the jpushover dependency to your pom.xml: -``` - - de.svenkubiak - jpushover - x.x.x - -``` + + + de.svenkubiak + jpushover + x.x.x + + + 2) Use the JPushover object with the required informations were you want -``` -JPushover.create() - .withToken("MyToken") - .withUser("MyUser") - .withMessage("MyMessage") - .push(); -``` -You can additionally add all available options from the official [Pushover documentation][2] + + JPushover.newMessage() + .withToken("MyToken") + .withUser("MyUser") + .withMessage("MyMessage") + .push(); + + 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 - boolean valid = JPushover.create() + boolean valid = JPushover.newMessage() .withToken("MyToken") .withUser("MyUser") .validate(); 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") .withUser("MyUser") .withMessage("MyMessage") .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 -[2]: https://pushover.net/api \ No newline at end of file +[2]: https://pushover.net/api +[3]: https://pushover.net/api +[4]: https://pushover.net/api/glances \ No newline at end of file diff --git a/pom.xml b/pom.xml index 71d9207..4c01d89 100644 --- a/pom.xml +++ b/pom.xml @@ -1,4 +1,6 @@ - + 4.0.0 de.svenkubiak jpushover @@ -20,6 +22,8 @@ 11 UTF-8 + 1.3.1 + 5.3.1 scm:git:git@github.com:svenkubiak/JPushover.git @@ -59,7 +63,7 @@ org.apache.maven.plugins maven-enforcer-plugin - 3.0.0-M2 + 3.0.0-M2 @@ -127,22 +131,22 @@ sonar-maven-plugin 3.5.0.1254 - - org.owasp - dependency-check-maven - 3.3.2 - - 12 - 4 - - - - - check - - - - + + org.owasp + dependency-check-maven + 4.0.0 + + 12 + 4 + + + + + check + + + + org.apache.maven.plugins maven-release-plugin @@ -170,7 +174,7 @@ org.apache.maven.plugins maven-surefire-plugin - 2.22.0 + 3.0.0-M1 org.apache.maven.plugins @@ -194,6 +198,120 @@ + + + com.github.tomakehurst + wiremock + 2.19.0 + test + + + org.slf4j + slf4j-api + + + org.apache.commons + commons-lang3 + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + com.fasterxml.jackson.core + jackson-core + + + junit + junit + + + + + org.apache.commons + commons-lang3 + 3.7 + test + + + com.fasterxml.jackson.core + jackson-databind + 2.8.11 + test + + + com.fasterxml.jackson.core + jackson-core + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + + + com.fasterxml.jackson.core + jackson-annotations + 2.8.11 + test + + + com.fasterxml.jackson.core + jackson-core + 2.8.11 + test + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + org.junit.platform + junit-platform-launcher + ${junit.platform.version} + test + + + org.junit.platform + junit-platform-runner + ${junit.platform.version} + test + + + org.apache.logging.log4j + log4j-slf4j18-impl + 2.11.1 + test + + + org.slf4j + slf4j-api + + + + + org.slf4j + slf4j-api + 1.8.0-beta2 + test + + ossrh diff --git a/src/main/java/de/svenkubiak/jpushover/JPushover.java b/src/main/java/de/svenkubiak/jpushover/JPushover.java index a15db6b..57af088 100644 --- a/src/main/java/de/svenkubiak/jpushover/JPushover.java +++ b/src/main/java/de/svenkubiak/jpushover/JPushover.java @@ -1,352 +1,32 @@ package de.svenkubiak.jpushover; -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; -import java.util.TreeMap; - -import de.svenkubiak.jpushover.enums.Constants; -import de.svenkubiak.jpushover.enums.Priority; -import de.svenkubiak.jpushover.enums.Sound; +import de.svenkubiak.jpushover.apis.Glance; +import de.svenkubiak.jpushover.apis.Message; /** * - * 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 * */ public class JPushover { - private static final int HTTP_OK = 200; - private Priority pushoverPriority; - private Sound pushoverSound; - private String pushoverToken; - private String pushoverUser; - private String pushoverMessage; - private String pushoverDevice; - 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; + /** + * Creates a new Glance instance for the Glances API + * + * @return Glance instance + */ + public static Glance newGlance() { + return new Glance(); + } - public JPushover() { - this.withSound(Sound.PUSHOVER); - this.withPriority(Priority.NORMAL); - } - /** - * Creates a new JPushover instance - * @return JPushover instance + * Creates a new Message instance for the Messages API + * + * @return Message instance */ - public static JPushover create() { - return new JPushover(); - } - - /** - * 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 pushover dashboard - * (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 documentation - * (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 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 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 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 body) { - StringBuilder buffer = new StringBuilder(); - buffer.append("{"); - for (Map.Entry 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(",}", "}"); + public static Message newMessage() { + return new Message(); } } \ No newline at end of file diff --git a/src/main/java/de/svenkubiak/jpushover/apis/Glance.java b/src/main/java/de/svenkubiak/jpushover/apis/Glance.java new file mode 100644 index 0000000..33c5d07 --- /dev/null +++ b/src/main/java/de/svenkubiak/jpushover/apis/Glance.java @@ -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 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); + } +} \ No newline at end of file diff --git a/src/main/java/de/svenkubiak/jpushover/apis/Message.java b/src/main/java/de/svenkubiak/jpushover/apis/Message.java new file mode 100644 index 0000000..6014806 --- /dev/null +++ b/src/main/java/de/svenkubiak/jpushover/apis/Message.java @@ -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 pushover dashboard + * (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 documentation + * (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 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 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); + } +} \ No newline at end of file diff --git a/src/main/java/de/svenkubiak/jpushover/enums/Constants.java b/src/main/java/de/svenkubiak/jpushover/enums/Param.java similarity index 74% rename from src/main/java/de/svenkubiak/jpushover/enums/Constants.java rename to src/main/java/de/svenkubiak/jpushover/enums/Param.java index 1fa18af..427e2ea 100644 --- a/src/main/java/de/svenkubiak/jpushover/enums/Constants.java +++ b/src/main/java/de/svenkubiak/jpushover/enums/Param.java @@ -5,14 +5,13 @@ package de.svenkubiak.jpushover.enums; * @author svenkubiak * */ -public enum Constants { +public enum Param { ATTACHMENT("attachment"), CALLBACK("callback"), DEVICE("device"), EXPIRE("expire"), HTML("html"), MESSAGE("message"), - MESSAGES_URL("https://api.pushover.net/1/messages.json"), PRIORITY("priority"), RETRY("retry"), SOUND("sound"), @@ -22,11 +21,14 @@ public enum Constants { URL("url"), URLTITLE("urltitle"), USER("user"), - VALIDATION_URL("https://api.pushover.net/1/users/validate.json"); + TEXT("text"), + SUBTEXT("subtext"), + COUNT("count"), + PERCENT("percent"); private final String value; - Constants (String value) { + Param (String value) { this.value = value; } diff --git a/src/main/java/de/svenkubiak/jpushover/http/PushoverRequest.java b/src/main/java/de/svenkubiak/jpushover/http/PushoverRequest.java new file mode 100644 index 0000000..ec63cf8 --- /dev/null +++ b/src/main/java/de/svenkubiak/jpushover/http/PushoverRequest.java @@ -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 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 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 body) { + StringBuilder buffer = new StringBuilder(); + buffer.append("{"); + for (Map.Entry 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(",}", "}"); + } +} \ No newline at end of file diff --git a/src/main/java/de/svenkubiak/jpushover/JPushoverResponse.java b/src/main/java/de/svenkubiak/jpushover/http/PushoverResponse.java similarity index 70% rename from src/main/java/de/svenkubiak/jpushover/JPushoverResponse.java rename to src/main/java/de/svenkubiak/jpushover/http/PushoverResponse.java index a4fc7dd..3bbb49f 100644 --- a/src/main/java/de/svenkubiak/jpushover/JPushoverResponse.java +++ b/src/main/java/de/svenkubiak/jpushover/http/PushoverResponse.java @@ -1,26 +1,26 @@ -package de.svenkubiak.jpushover; +package de.svenkubiak.jpushover.http; /** * * @author svenkubiak * */ -public class JPushoverResponse { +public class PushoverResponse { private String pushoverResponse; private int pushoverHttpStatus; private boolean pushoverSuccessful; - public JPushoverResponse response(String response) { + public PushoverResponse response(String response) { this.pushoverResponse = response; return this; } - public JPushoverResponse httpStatus(int httpStatus) { + public PushoverResponse httpStatus(int httpStatus) { this.pushoverHttpStatus = httpStatus; return this; } - public JPushoverResponse isSuccessful(boolean successful) { + public PushoverResponse isSuccessful(boolean successful) { this.pushoverSuccessful = successful; 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() { return pushoverSuccessful; diff --git a/src/main/java/de/svenkubiak/jpushover/utils/Urls.java b/src/main/java/de/svenkubiak/jpushover/utils/Urls.java new file mode 100644 index 0000000..85445e4 --- /dev/null +++ b/src/main/java/de/svenkubiak/jpushover/utils/Urls.java @@ -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"; + } +} \ No newline at end of file diff --git a/src/main/java/de/svenkubiak/jpushover/utils/Validate.java b/src/main/java/de/svenkubiak/jpushover/utils/Validate.java new file mode 100644 index 0000000..01c1e75 --- /dev/null +++ b/src/main/java/de/svenkubiak/jpushover/utils/Validate.java @@ -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)); + } + } +} \ No newline at end of file diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java deleted file mode 100644 index 5c20eb0..0000000 --- a/src/main/java/module-info.java +++ /dev/null @@ -1,5 +0,0 @@ -module jpushover { - requires java.net.http; - exports de.svenkubiak.jpushover; - exports de.svenkubiak.jpushover.enums; -} \ No newline at end of file diff --git a/src/test/java/jpushover/JPushoverTest.java b/src/test/java/jpushover/JPushoverTest.java new file mode 100644 index 0000000..8011634 --- /dev/null +++ b/src/test/java/jpushover/JPushoverTest.java @@ -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); + } +} diff --git a/src/test/java/jpushover/MockServer.java b/src/test/java/jpushover/MockServer.java new file mode 100644 index 0000000..c1cd26a --- /dev/null +++ b/src/test/java/jpushover/MockServer.java @@ -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; + } + } +} \ No newline at end of file diff --git a/src/test/java/jpushover/apis/GlanceTest.java b/src/test/java/jpushover/apis/GlanceTest.java new file mode 100644 index 0000000..5440a4f --- /dev/null +++ b/src/test/java/jpushover/apis/GlanceTest.java @@ -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()); + } +} \ No newline at end of file diff --git a/src/test/java/jpushover/apis/MessageTest.java b/src/test/java/jpushover/apis/MessageTest.java new file mode 100644 index 0000000..b23f870 --- /dev/null +++ b/src/test/java/jpushover/apis/MessageTest.java @@ -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()); + } +} diff --git a/src/test/java/jpushover/utils/ValidateTest.java b/src/test/java/jpushover/utils/ValidateTest.java new file mode 100644 index 0000000..519908b --- /dev/null +++ b/src/test/java/jpushover/utils/ValidateTest.java @@ -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"); + }); + } +} \ No newline at end of file diff --git a/src/test/resources/log4j2.xml b/src/test/resources/log4j2.xml new file mode 100644 index 0000000..77b7820 --- /dev/null +++ b/src/test/resources/log4j2.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file