70: Refactor the notification framework into transport-independent notification services

This commit is contained in:
2026-08-11 20:03:06 +02:00
parent b33db6da49
commit df504e29bf
25 changed files with 522 additions and 168 deletions
@@ -1,14 +0,0 @@
package com.r35157.libs.notification;
import java.io.IOException;
public interface AddressedNotifier<
D extends NotificationDestination,
M extends NotificationMessage>
{
void push(D destination, M message) throws IOException;
default BoundNotifier<M> bind(D destination) {
return message -> push(destination, message);
}
}
@@ -1,7 +0,0 @@
package com.r35157.libs.notification;
import java.io.IOException;
public interface BoundNotifier<M extends NotificationMessage> {
void push(M message) throws IOException;
}
@@ -1,3 +0,0 @@
package com.r35157.libs.notification;
public interface NotificationDestination {}
@@ -1,4 +0,0 @@
package com.r35157.libs.notification;
public interface NotificationMessage {
}
@@ -1,11 +0,0 @@
package com.r35157.libs.notification.impl.discord;
import com.r35157.libs.notification.NotificationMessage;
public record DiscordMessage(String text) implements NotificationMessage {
public DiscordMessage {
if (text == null || text.isBlank()) {
throw new IllegalArgumentException("Discord message cannot be blank");
}
}
}
@@ -1,32 +0,0 @@
package com.r35157.libs.notification.impl.discord;
import com.r35157.libs.notification.BoundNotifier;
import java.io.IOException;
import java.net.URL;
import java.net.http.HttpClient;
public class DiscordNotifier implements BoundNotifier<DiscordMessage> {
public DiscordNotifier(URL discordWebhookUrl) {
this.httpClient = HttpClient.newHttpClient();
this.discordWebhookUrl = discordWebhookUrl;
}
@Override
public void push(DiscordMessage message) throws IOException {
throw new UnsupportedOperationException("Not implemented yet!");
}
private void test() throws IOException {
URL discordWebhookUrl = null;
BoundNotifier<DiscordMessage> boundNotifier = new DiscordNotifier(discordWebhookUrl);
DiscordMessage message = new DiscordMessage("Hello World!");
boundNotifier.push(message);
}
private final HttpClient httpClient;
private final URL discordWebhookUrl;
}
@@ -1,11 +0,0 @@
package com.r35157.libs.notification.impl.pushover;
import com.r35157.libs.notification.NotificationMessage;
public record PushMessage(String text) implements NotificationMessage {
public PushMessage {
if (text == null || text.isBlank()) {
throw new IllegalArgumentException("Push message cannot be blank");
}
}
}
@@ -1,9 +0,0 @@
package com.r35157.libs.notification.impl.smtp;
public record EmailBody(String text) {
public EmailBody {
if (text == null || text.isBlank()) {
throw new IllegalArgumentException("Email body cannot be blank!");
}
}
}
@@ -1,9 +0,0 @@
package com.r35157.libs.notification.impl.smtp;
import com.r35157.libs.notification.NotificationMessage;
public record EmailMessage(
EmailSubject subject,
EmailBody body
) implements NotificationMessage {
}
@@ -1,9 +0,0 @@
package com.r35157.libs.notification.impl.smtp;
public record EmailSubject(String text) {
public EmailSubject {
if (text == null || text.isBlank()) {
throw new IllegalArgumentException("Email subject cannot be blank!");
}
}
}
@@ -1,5 +0,0 @@
package com.r35157.libs.notification.impl.smtp;
import com.r35157.libs.notification.NotificationDestination;
public record SMTPDestination(ΩEmailAddressΩ emailAddress) implements NotificationDestination { }
@@ -1,45 +0,0 @@
package com.r35157.libs.notification.impl.smtp;
import com.r35157.libs.notification.AddressedNotifier;
import com.r35157.libs.notification.BoundNotifier;
import com.r35157.libs.valuetypes.basic.Credentials;
import com.r35157.libs.valuetypes.basic.NetworkEndPoint;
import com.r35157.libs.valuetypes.basic.SmtpConfiguration;
import java.io.IOException;
public class SMTPNotifier implements AddressedNotifier<SMTPDestination, EmailMessage> {
public SMTPNotifier(SmtpConfiguration smtpConfiguration) {
this.smtpConfiguration = smtpConfiguration;
}
@Override
public void push(SMTPDestination destination, EmailMessage message) throws IOException {
throw new UnsupportedOperationException("Not implemented yet!");
}
private void test() throws IOException {
ΩHostnameΩ hostName = new ΩHostnameΩ("SomeSMTPHost");
ΩportNumberΩ portNumber = 2525;
NetworkEndPoint networkEndPoint = new NetworkEndPoint(hostName, portNumber);
ΩUserNameΩ userName = "SomeUser";
ΩPasswordΩ password = "SomePassword";
Credentials credentials = new Credentials(userName, password);
SmtpConfiguration smtpConfiguration = new SmtpConfiguration(networkEndPoint, credentials);
AddressedNotifier<SMTPDestination, EmailMessage> notifier = new SMTPNotifier(smtpConfiguration);
ΩEmailAddressΩ emailAddress = "SomeReceiver";
SMTPDestination destination = new SMTPDestination(emailAddress);
BoundNotifier<EmailMessage> boundNotifier = notifier.bind(destination);
EmailSubject subject = new EmailSubject("SomeSubject");
EmailBody body = new EmailBody("SomeBody");
EmailMessage message = new EmailMessage(subject, body);
boundNotifier.push(message);
}
private final SmtpConfiguration smtpConfiguration;
}
@@ -0,0 +1,41 @@
package com.r35157.service.notification;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.Objects;
/**
* Sends notification messages to explicitly supplied destinations.
*
* @param <D> transport-specific destination configuration
*/
public interface AddressedNotificationService<
D extends NotificationDestination>
{
/**
* Sends a notification message to a destination.
*
* @param destination the destination configuration
* @param message the non-blank notification text
* @throws IllegalArgumentException if {@code message} is {@code null},
* empty, or blank
* @throws IOException if synchronous notification delivery fails
*/
void push(
@NotNull D destination,
@NotNull ΩNotificationMessageΩ message
) throws IOException;
/**
* Binds a destination into a transport-independent notification service.
*
* @param destination the destination configuration to capture
* @return a service that requires only a notification message
* @throws NullPointerException if {@code destination} is {@code null}
*/
default @NotNull BoundNotificationService bind(@NotNull D destination) {
Objects.requireNonNull(destination, "destination");
return message -> push(destination, message);
}
}
@@ -0,0 +1,25 @@
package com.r35157.service.notification;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
/**
* Sends notification messages through a fully configured transport and
* destination.
*
* <p>Consumers of this interface do not need to know which transport is used
* or how its destination is configured.</p>
*/
@FunctionalInterface
public interface BoundNotificationService {
/**
* Sends a notification message.
*
* @param message the non-blank notification text
* @throws IllegalArgumentException if {@code message} is {@code null},
* empty, or blank
* @throws IOException if synchronous notification delivery fails
*/
void push(@NotNull ΩNotificationMessageΩ message) throws IOException;
}
@@ -0,0 +1,8 @@
package com.r35157.service.notification;
/**
* Marks transport-specific destination configuration that can be bound into a
* {@link BoundNotificationService}.
*/
public interface NotificationDestination {
}
@@ -0,0 +1,49 @@
package com.r35157.service.notification.impl.discord;
import com.r35157.service.notification.BoundNotificationService;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.net.URL;
import java.net.http.HttpClient;
/**
* Placeholder for bound Discord webhook notification delivery.
*/
public class DiscordNotificationService implements BoundNotificationService {
/**
* Creates a service bound to a Discord webhook URL.
*
* @param discordWebhookUrl Discord webhook URL
*/
public DiscordNotificationService(URL discordWebhookUrl) {
this.httpClient = HttpClient.newHttpClient();
this.discordWebhookUrl = discordWebhookUrl;
}
@Override
public void push(@NotNull ΩNotificationMessageΩ message) throws IOException {
validateDiscordMessage(message);
throw new UnsupportedOperationException("Not implemented yet!");
}
private static void validateDiscordMessage(ΩNotificationMessageΩ message) {
if (message == null || message.isBlank()) {
throw new IllegalArgumentException("Notification message cannot be blank");
}
}
private void test() throws IOException {
URL discordWebhookUrl = null;
BoundNotificationService notificationService =
new DiscordNotificationService(discordWebhookUrl);
ΩNotificationMessageΩ message = "Hello World!";
notificationService.push(message);
}
private final HttpClient httpClient;
private final URL discordWebhookUrl;
}
@@ -1,6 +1,7 @@
package com.r35157.libs.notification.impl.pushover;
package com.r35157.service.notification.impl.pushover;
import com.r35157.libs.notification.BoundNotifier;
import com.r35157.service.notification.BoundNotificationService;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.net.URI;
@@ -10,20 +11,32 @@ import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
public class PushOverNotifier implements BoundNotifier<PushMessage> {
/**
* Sends bound notification messages through the Pushover Messages API.
*/
public class PushoverNotificationService implements BoundNotificationService {
public PushOverNotifier(ΩAPIKeyΩ token, ΩUserNameΩ userName) {
/**
* Creates a Pushover service bound to an application token and user or
* group key.
*
* @param token Pushover application token
* @param userName Pushover user or group key
*/
public PushoverNotificationService(ΩAPIKeyΩ token, ΩUserNameΩ userName) {
this.httpClient = HttpClient.newHttpClient();
this.token = token;
this.userName = userName;
}
@Override
public void push(PushMessage message) throws IOException {
public void push(@NotNull ΩNotificationMessageΩ message) throws IOException {
validatePushoverMessage(message);
String body = formEncode(
"token", token,
"user", userName,
"message", message.text()
"message", message
);
HttpRequest request = HttpRequest.newBuilder()
@@ -46,6 +59,12 @@ public class PushOverNotifier implements BoundNotifier<PushMessage> {
}
}
private static void validatePushoverMessage(ΩNotificationMessageΩ message) {
if (message == null || message.isBlank()) {
throw new IllegalArgumentException("Notification message cannot be blank");
}
}
private static String formEncode(String... keyValues) {
if (keyValues.length % 2 != 0) {
throw new IllegalArgumentException("keyValues must contain key/value pairs");
@@ -74,10 +93,11 @@ public class PushOverNotifier implements BoundNotifier<PushMessage> {
ΩAPIKeyΩ apiKey = "SomeKey";
ΩUserNameΩ userName = "userNmae";
BoundNotifier<PushMessage> boundNotifier = new PushOverNotifier(apiKey, userName);
BoundNotificationService notificationService =
new PushoverNotificationService(apiKey, userName);
PushMessage message = new PushMessage("Hello World!");
boundNotifier.push(message);
ΩNotificationMessageΩ message = "Hello World!";
notificationService.push(message);
}
private static final URI PUSHOVER_MESSAGES_URI =
@@ -0,0 +1,31 @@
package com.r35157.service.notification.impl.smtp;
import com.r35157.service.notification.NotificationDestination;
import org.jetbrains.annotations.NotNull;
/**
* Complete SMTP destination configuration for a bound plain-text
* notification service.
*
* @param emailAddress destination email address
* @param subject fixed subject for every notification sent to the destination
*/
public record SMTPDestination(
@NotNull ΩEmailAddressΩ emailAddress,
@NotNull ΩEmailSubjectΩ subject
) implements NotificationDestination {
/**
* Validates the complete SMTP destination binding.
*
* @throws IllegalArgumentException if the email address or subject is
* {@code null}, empty, or blank
*/
public SMTPDestination {
if (emailAddress == null || emailAddress.isBlank()) {
throw new IllegalArgumentException("Email address cannot be blank");
}
if (subject == null || subject.isBlank()) {
throw new IllegalArgumentException("Email subject cannot be blank");
}
}
}
@@ -0,0 +1,83 @@
package com.r35157.service.notification.impl.smtp;
import com.r35157.libs.valuetypes.basic.Credentials;
import com.r35157.libs.valuetypes.basic.NetworkEndPoint;
import com.r35157.libs.valuetypes.basic.SmtpConfiguration;
import com.r35157.service.notification.AddressedNotificationService;
import com.r35157.service.notification.BoundNotificationService;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
/**
* Placeholder for SMTP notification delivery with destination and subject
* binding.
*/
public class SMTPNotificationService
implements AddressedNotificationService<SMTPDestination>
{
/**
* Creates an SMTP notification service.
*
* @param smtpConfiguration SMTP transport configuration
*/
public SMTPNotificationService(SmtpConfiguration smtpConfiguration) {
this.smtpConfiguration = smtpConfiguration;
}
@Override
public void push(
@NotNull SMTPDestination destination,
@NotNull ΩNotificationMessageΩ message
) throws IOException {
validateSmtpMessage(message);
throw new UnsupportedOperationException("Not implemented yet!");
}
/**
* Binds an email address and fixed subject into a transport-independent
* notification service.
*
* @param emailAddress destination email address
* @param subject fixed subject for every notification
* @return a service requiring only a notification message
* @throws IllegalArgumentException if the address or subject is
* {@code null}, empty, or blank
*/
public @NotNull BoundNotificationService bind(
@NotNull ΩEmailAddressΩ emailAddress,
@NotNull ΩEmailSubjectΩ subject
) {
return bind(new SMTPDestination(emailAddress, subject));
}
private static void validateSmtpMessage(ΩNotificationMessageΩ message) {
if (message == null || message.isBlank()) {
throw new IllegalArgumentException("Notification message cannot be blank");
}
}
private void test() throws IOException {
ΩHostnameΩ hostName = new ΩHostnameΩ("SomeSMTPHost");
ΩportNumberΩ portNumber = 2525;
NetworkEndPoint networkEndPoint = new NetworkEndPoint(hostName, portNumber);
ΩUserNameΩ userName = "SomeUser";
ΩPasswordΩ password = "SomePassword";
Credentials credentials = new Credentials(userName, password);
SmtpConfiguration smtpConfiguration = new SmtpConfiguration(networkEndPoint, credentials);
SMTPNotificationService smtpNotificationService =
new SMTPNotificationService(smtpConfiguration);
ΩEmailAddressΩ emailAddress = "SomeReceiver";
ΩEmailSubjectΩ subject = "SomeSubject";
BoundNotificationService notificationService =
smtpNotificationService.bind(emailAddress, subject);
ΩNotificationMessageΩ message = "SomeBody";
notificationService.push(message);
}
private final SmtpConfiguration smtpConfiguration;
}