70: Refactor the notification framework into transport-independent notification services
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
schema: spec-driven
|
||||||
|
created: 2026-08-11
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
## Context
|
||||||
|
|
||||||
|
See `proposal.md` for motivation. The current notification framework combines an API package and three implementation packages under `com.r35157.libs.notification`; its generic message wrappers leak transport choices into consumers. The code currently has no external production consumer, but each implementation contains an inline compile-time usage example. The separate alarm-specific Pushover action is intentionally outside this module and must remain unchanged.
|
||||||
|
|
||||||
|
The repository temporarily hosts immature API and implementation artifacts together. Package ownership must nevertheless express a notification-service module that can later be extracted, with API types separated from transport implementations.
|
||||||
|
|
||||||
|
## Goals / Non-Goals
|
||||||
|
|
||||||
|
**Goals:**
|
||||||
|
|
||||||
|
- Establish a small stable API package containing only the bound service, addressed service, and destination abstraction.
|
||||||
|
- Make transport implementations consume the three existing String-backed ValueTags directly.
|
||||||
|
- Preserve the existing Pushover HTTP request and synchronous error behavior while enforcing message validation before transport work.
|
||||||
|
- Make SMTP's complete destination configuration immutable and bindable before injection into a consumer.
|
||||||
|
|
||||||
|
**Non-Goals:**
|
||||||
|
|
||||||
|
- Implementing Discord or SMTP delivery.
|
||||||
|
- Adding notification configuration, asynchronous delivery, retries, queues, persistence, or timeout policies.
|
||||||
|
- Refactoring the alarm-specific Pushover implementation or changing `NenjimHubImpl.startAutoRunProcesses()`.
|
||||||
|
- Adding or modifying automated tests.
|
||||||
|
|
||||||
|
## Decisions
|
||||||
|
|
||||||
|
### Separate API and transport implementation packages under the service owner
|
||||||
|
|
||||||
|
The API will live directly in `com.r35157.service.notification`, while Discord, Pushover, and SMTP remain in `impl.discord`, `impl.pushover`, and `impl.smtp` below it. This follows the repository's future module boundaries while keeping the current single-project layout. Retaining aliases in the old package was rejected because it would prolong the incompatible abstraction and violates the issue's clean migration requirement.
|
||||||
|
|
||||||
|
### Represent messages directly with the semantic ValueTag
|
||||||
|
|
||||||
|
All push contracts use `ΩNotificationMessageΩ` directly. Validation is performed at each concrete public push entry point before any delivery work. A replacement record, marker hierarchy, or transport generic was rejected because it would recreate the coupling the change removes.
|
||||||
|
|
||||||
|
### Keep addressed binding minimal and destination-only
|
||||||
|
|
||||||
|
`AddressedNotificationService<D>` captures a destination in its default `bind(D)` result. `SMTPDestination` captures both email address and subject and validates both in its compact constructor. `SMTPNotificationService` adds a convenience overload accepting these two values and delegates to the standard destination binding. Keeping the subject on each message was rejected because it would leave the service only partially configured when injected.
|
||||||
|
|
||||||
|
### Preserve transport maturity independently
|
||||||
|
|
||||||
|
Pushover keeps its current form encoding, endpoint, synchronous HTTP call, status handling, and `IOException` contract. Discord and SMTP validate message input first and then retain their `UnsupportedOperationException` behavior for otherwise valid messages. This prevents a refactor from silently expanding either transport's behavior.
|
||||||
|
|
||||||
|
## Risks / Trade-offs
|
||||||
|
|
||||||
|
- **[Breaking source migration]** All old names and packages disappear at once. → Migrate every production reference and inline example in the same change, then structurally search for old names.
|
||||||
|
- **[Validation duplicated across implementations]** Each implementation owns a small null/blank guard. → Prefer explicit local validation over adding an unnecessary public utility or runtime message wrapper; keep the exception type and condition identical.
|
||||||
|
- **[No automated regression coverage]** The issue explicitly excludes test changes. → Compile both source sets, run the normal assembly, and perform strict structural checks of packages, types, call sites, and generated-source boundaries.
|
||||||
|
|
||||||
|
## Migration Plan
|
||||||
|
|
||||||
|
1. Introduce the renamed API and transport implementations in the new package using the existing ValueTags.
|
||||||
|
2. Update all production imports and inline examples to the new contracts.
|
||||||
|
3. Remove the complete old package and every obsolete notifier/message type without aliases.
|
||||||
|
4. Compile and assemble from `.tjava`, validate OpenSpec strictly, and verify the source tree contains no stale API references.
|
||||||
|
|
||||||
|
Rollback consists of reverting the complete source and OpenSpec change together; the old and new APIs are intentionally not supported in parallel.
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
## Why
|
||||||
|
|
||||||
|
The existing notification API exposes transport-specific message types to consumers, so a fully configured notification sender cannot be substituted without changing consumer code. Before adding further transports, the framework needs a stable service API in which configuration is bound at composition time and consumers provide only a semantically tagged text message.
|
||||||
|
|
||||||
|
## What Changes
|
||||||
|
|
||||||
|
- **BREAKING** Relocate the notification API and implementations from `com.r35157.libs.notification` to `com.r35157.service.notification`, without compatibility aliases.
|
||||||
|
- **BREAKING** Replace `BoundNotifier` and `AddressedNotifier` with transport-independent `BoundNotificationService` and destination-only generic `AddressedNotificationService` contracts.
|
||||||
|
- **BREAKING** Remove the marker and transport-specific message wrappers in favor of `ΩNotificationMessageΩ` directly.
|
||||||
|
- Rename the Pushover, Discord, and SMTP implementations to notification-service terminology while preserving their current delivery status.
|
||||||
|
- Validate every notification message before delivery, and make SMTP binding capture both a validated email address and fixed subject.
|
||||||
|
|
||||||
|
## Capabilities
|
||||||
|
|
||||||
|
### New Capabilities
|
||||||
|
|
||||||
|
- `notification-services`: Defines the transport-independent notification API, binding and validation behavior, and the required behavior of the existing Pushover, Discord, and SMTP implementations.
|
||||||
|
|
||||||
|
### Modified Capabilities
|
||||||
|
|
||||||
|
None.
|
||||||
|
|
||||||
|
## Impact
|
||||||
|
|
||||||
|
The change replaces the public notification package and its API types, updates all production imports and inline examples, and removes obsolete message classes. Pushover's synchronous HTTP delivery remains functional; Discord webhook and SMTP delivery remain unimplemented. Alarm-specific Pushover code and Nenjim startup composition are unaffected.
|
||||||
+78
@@ -0,0 +1,78 @@
|
|||||||
|
## Purpose
|
||||||
|
|
||||||
|
Defines transport-independent notification services whose delivery configuration is bound before consumers submit semantically tagged text messages.
|
||||||
|
|
||||||
|
## ADDED Requirements
|
||||||
|
|
||||||
|
### Requirement: Transport-independent bound notification contract
|
||||||
|
The notification API SHALL provide a non-generic bound service under `com.r35157.service.notification` whose only delivery input is an `ΩNotificationMessageΩ` and whose synchronous failures use `IOException`. A consumer using an already bound service SHALL NOT need a transport-specific message type or an implementation-package dependency.
|
||||||
|
|
||||||
|
#### Scenario: Bound consumer sends a message
|
||||||
|
- **WHEN** a consumer receives a configured `BoundNotificationService`
|
||||||
|
- **THEN** it can call `push(message)` using only an `ΩNotificationMessageΩ`
|
||||||
|
|
||||||
|
#### Scenario: Delivery failure is reported synchronously
|
||||||
|
- **WHEN** a bound transport encounters a delivery failure
|
||||||
|
- **THEN** `push(message)` reports the failure through its `IOException` contract
|
||||||
|
|
||||||
|
### Requirement: Addressed services bind destination configuration
|
||||||
|
The notification API SHALL provide an `AddressedNotificationService` generic only over a `NotificationDestination` type. Binding a destination SHALL produce a `BoundNotificationService` that captures the destination so later calls require only an `ΩNotificationMessageΩ`.
|
||||||
|
|
||||||
|
#### Scenario: Destination is bound once
|
||||||
|
- **WHEN** a destination is passed to an addressed notification service's binding operation
|
||||||
|
- **THEN** the returned bound service sends subsequent messages to that captured destination without requiring it again
|
||||||
|
|
||||||
|
### Requirement: Notification message validation
|
||||||
|
Every public notification-service `push(...)` entry point SHALL reject a null, empty, or blank `ΩNotificationMessageΩ` with `IllegalArgumentException` before attempting any network or transport operation.
|
||||||
|
|
||||||
|
#### Scenario: Null message is rejected
|
||||||
|
- **WHEN** a caller pushes a null notification message
|
||||||
|
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
|
||||||
|
|
||||||
|
#### Scenario: Empty or blank message is rejected
|
||||||
|
- **WHEN** a caller pushes an empty or whitespace-only notification message
|
||||||
|
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
|
||||||
|
|
||||||
|
### Requirement: Pushover delivery remains functional
|
||||||
|
The Pushover implementation SHALL accept `ΩNotificationMessageΩ` directly, preserve its existing synchronous HTTP delivery behavior, and expose it as `PushoverNotificationService` implementing `BoundNotificationService`.
|
||||||
|
|
||||||
|
#### Scenario: Valid Pushover message is submitted
|
||||||
|
- **WHEN** a caller pushes a valid message through a configured Pushover notification service
|
||||||
|
- **THEN** the service submits the same message through the existing Pushover API request flow
|
||||||
|
|
||||||
|
#### Scenario: Pushover rejects delivery
|
||||||
|
- **WHEN** Pushover returns a non-successful HTTP response or communication fails
|
||||||
|
- **THEN** the service reports the delivery failure as an `IOException`
|
||||||
|
|
||||||
|
### Requirement: SMTP binding captures address and subject
|
||||||
|
An SMTP destination SHALL contain both an `ΩEmailAddressΩ` and a fixed `ΩEmailSubjectΩ`. SMTP SHALL support binding these two values into a `BoundNotificationService`, and SHALL reject a null, empty, or blank address or subject with `IllegalArgumentException` when the destination is created.
|
||||||
|
|
||||||
|
#### Scenario: SMTP service is fully bound
|
||||||
|
- **WHEN** a caller binds a valid email address and subject
|
||||||
|
- **THEN** the returned bound service requires only an `ΩNotificationMessageΩ` for each push
|
||||||
|
|
||||||
|
#### Scenario: Invalid SMTP binding is rejected
|
||||||
|
- **WHEN** a caller creates or requests an SMTP binding with a null, empty, or blank email address or subject
|
||||||
|
- **THEN** the operation throws `IllegalArgumentException`
|
||||||
|
|
||||||
|
#### Scenario: Valid SMTP delivery remains unimplemented
|
||||||
|
- **WHEN** a valid message is pushed through a valid SMTP binding
|
||||||
|
- **THEN** the service reports that SMTP delivery is not implemented
|
||||||
|
|
||||||
|
### Requirement: Discord delivery remains unimplemented
|
||||||
|
The Discord stub SHALL implement `BoundNotificationService`, accept `ΩNotificationMessageΩ` directly, and SHALL NOT implement webhook delivery as part of this capability.
|
||||||
|
|
||||||
|
#### Scenario: Valid Discord delivery remains unimplemented
|
||||||
|
- **WHEN** a valid message is pushed through the configured Discord service
|
||||||
|
- **THEN** the service reports that Discord delivery is not implemented without performing webhook delivery
|
||||||
|
|
||||||
|
### Requirement: Obsolete notification API is removed
|
||||||
|
The notification framework SHALL reside exclusively under `com.r35157.service.notification`. It SHALL NOT retain compatibility aliases under `com.r35157.libs.notification`, notifier-named public interfaces or implementations, the `NotificationMessage` marker, or transport-specific message wrapper types.
|
||||||
|
|
||||||
|
#### Scenario: Production code uses the new service API
|
||||||
|
- **WHEN** the production source tree is inspected after migration
|
||||||
|
- **THEN** notification framework code and consumers reference only the new package and service-oriented types
|
||||||
|
|
||||||
|
#### Scenario: Removed types are unavailable
|
||||||
|
- **WHEN** downstream code attempts to import an old notifier or message-wrapper type
|
||||||
|
- **THEN** that type is no longer part of the source or compiled API
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
## 1. Notification API Migration
|
||||||
|
|
||||||
|
- [x] 1.1 Add the documented `BoundNotificationService`, `AddressedNotificationService`, and `NotificationDestination` API under `com.r35157.service.notification`.
|
||||||
|
- [x] 1.2 Remove the old API package, marker interface, notifier contracts, and all compatibility aliases.
|
||||||
|
|
||||||
|
## 2. Transport Implementations
|
||||||
|
|
||||||
|
- [x] 2.1 Migrate and rename Pushover to `PushoverNotificationService`, accept and validate `ΩNotificationMessageΩ`, and preserve the existing delivery flow.
|
||||||
|
- [x] 2.2 Migrate and rename Discord to `DiscordNotificationService`, validate direct messages, and preserve its unimplemented delivery status.
|
||||||
|
- [x] 2.3 Migrate SMTP to `SMTPNotificationService` with a validated address-and-subject `SMTPDestination`, direct binding convenience, direct message validation, and unimplemented delivery status.
|
||||||
|
- [x] 2.4 Update every production call site and inline example, then confirm the old package and removed types have no remaining production references.
|
||||||
|
|
||||||
|
## 3. Verification
|
||||||
|
|
||||||
|
- [x] 3.1 Compile main and test source sets and complete the repository's normal assembly/build without changing tests.
|
||||||
|
- [x] 3.2 Strictly validate the OpenSpec change and run whitespace/diff checks.
|
||||||
|
- [x] 3.3 Review the complete diff for scope, generated-source changes, test changes, transport-independent consumer dependencies, and preservation of excluded services.
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# notification-services Specification
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Defines transport-independent notification services whose delivery configuration is bound before consumers submit semantically tagged text messages.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: Transport-independent bound notification contract
|
||||||
|
The notification API SHALL provide a non-generic bound service under `com.r35157.service.notification` whose only delivery input is an `ΩNotificationMessageΩ` and whose synchronous failures use `IOException`. A consumer using an already bound service SHALL NOT need a transport-specific message type or an implementation-package dependency.
|
||||||
|
|
||||||
|
#### Scenario: Bound consumer sends a message
|
||||||
|
- **WHEN** a consumer receives a configured `BoundNotificationService`
|
||||||
|
- **THEN** it can call `push(message)` using only an `ΩNotificationMessageΩ`
|
||||||
|
|
||||||
|
#### Scenario: Delivery failure is reported synchronously
|
||||||
|
- **WHEN** a bound transport encounters a delivery failure
|
||||||
|
- **THEN** `push(message)` reports the failure through its `IOException` contract
|
||||||
|
|
||||||
|
### Requirement: Addressed services bind destination configuration
|
||||||
|
The notification API SHALL provide an `AddressedNotificationService` generic only over a `NotificationDestination` type. Binding a destination SHALL produce a `BoundNotificationService` that captures the destination so later calls require only an `ΩNotificationMessageΩ`.
|
||||||
|
|
||||||
|
#### Scenario: Destination is bound once
|
||||||
|
- **WHEN** a destination is passed to an addressed notification service's binding operation
|
||||||
|
- **THEN** the returned bound service sends subsequent messages to that captured destination without requiring it again
|
||||||
|
|
||||||
|
### Requirement: Notification message validation
|
||||||
|
Every public notification-service `push(...)` entry point SHALL reject a null, empty, or blank `ΩNotificationMessageΩ` with `IllegalArgumentException` before attempting any network or transport operation.
|
||||||
|
|
||||||
|
#### Scenario: Null message is rejected
|
||||||
|
- **WHEN** a caller pushes a null notification message
|
||||||
|
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
|
||||||
|
|
||||||
|
#### Scenario: Empty or blank message is rejected
|
||||||
|
- **WHEN** a caller pushes an empty or whitespace-only notification message
|
||||||
|
- **THEN** the service throws `IllegalArgumentException` before delivery is attempted
|
||||||
|
|
||||||
|
### Requirement: Pushover delivery remains functional
|
||||||
|
The Pushover implementation SHALL accept `ΩNotificationMessageΩ` directly, preserve its existing synchronous HTTP delivery behavior, and expose it as `PushoverNotificationService` implementing `BoundNotificationService`.
|
||||||
|
|
||||||
|
#### Scenario: Valid Pushover message is submitted
|
||||||
|
- **WHEN** a caller pushes a valid message through a configured Pushover notification service
|
||||||
|
- **THEN** the service submits the same message through the existing Pushover API request flow
|
||||||
|
|
||||||
|
#### Scenario: Pushover rejects delivery
|
||||||
|
- **WHEN** Pushover returns a non-successful HTTP response or communication fails
|
||||||
|
- **THEN** the service reports the delivery failure as an `IOException`
|
||||||
|
|
||||||
|
### Requirement: SMTP binding captures address and subject
|
||||||
|
An SMTP destination SHALL contain both an `ΩEmailAddressΩ` and a fixed `ΩEmailSubjectΩ`. SMTP SHALL support binding these two values into a `BoundNotificationService`, and SHALL reject a null, empty, or blank address or subject with `IllegalArgumentException` when the destination is created.
|
||||||
|
|
||||||
|
#### Scenario: SMTP service is fully bound
|
||||||
|
- **WHEN** a caller binds a valid email address and subject
|
||||||
|
- **THEN** the returned bound service requires only an `ΩNotificationMessageΩ` for each push
|
||||||
|
|
||||||
|
#### Scenario: Invalid SMTP binding is rejected
|
||||||
|
- **WHEN** a caller creates or requests an SMTP binding with a null, empty, or blank email address or subject
|
||||||
|
- **THEN** the operation throws `IllegalArgumentException`
|
||||||
|
|
||||||
|
#### Scenario: Valid SMTP delivery remains unimplemented
|
||||||
|
- **WHEN** a valid message is pushed through a valid SMTP binding
|
||||||
|
- **THEN** the service reports that SMTP delivery is not implemented
|
||||||
|
|
||||||
|
### Requirement: Discord delivery remains unimplemented
|
||||||
|
The Discord stub SHALL implement `BoundNotificationService`, accept `ΩNotificationMessageΩ` directly, and SHALL NOT implement webhook delivery as part of this capability.
|
||||||
|
|
||||||
|
#### Scenario: Valid Discord delivery remains unimplemented
|
||||||
|
- **WHEN** a valid message is pushed through the configured Discord service
|
||||||
|
- **THEN** the service reports that Discord delivery is not implemented without performing webhook delivery
|
||||||
|
|
||||||
|
### Requirement: Obsolete notification API is removed
|
||||||
|
The notification framework SHALL reside exclusively under `com.r35157.service.notification`. It SHALL NOT retain compatibility aliases under `com.r35157.libs.notification`, notifier-named public interfaces or implementations, the `NotificationMessage` marker, or transport-specific message wrapper types.
|
||||||
|
|
||||||
|
#### Scenario: Production code uses the new service API
|
||||||
|
- **WHEN** the production source tree is inspected after migration
|
||||||
|
- **THEN** notification framework code and consumers reference only the new package and service-oriented types
|
||||||
|
|
||||||
|
#### Scenario: Removed types are unavailable
|
||||||
|
- **WHEN** downstream code attempts to import an old notifier or message-wrapper type
|
||||||
|
- **THEN** that type is no longer part of the source or compiled API
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
+49
@@ -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;
|
||||||
|
}
|
||||||
+29
-9
@@ -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.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
@@ -10,20 +11,32 @@ import java.net.http.HttpRequest;
|
|||||||
import java.net.http.HttpResponse;
|
import java.net.http.HttpResponse;
|
||||||
import java.nio.charset.StandardCharsets;
|
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.httpClient = HttpClient.newHttpClient();
|
||||||
this.token = token;
|
this.token = token;
|
||||||
this.userName = userName;
|
this.userName = userName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void push(PushMessage message) throws IOException {
|
public void push(@NotNull ΩNotificationMessageΩ message) throws IOException {
|
||||||
|
validatePushoverMessage(message);
|
||||||
|
|
||||||
String body = formEncode(
|
String body = formEncode(
|
||||||
"token", token,
|
"token", token,
|
||||||
"user", userName,
|
"user", userName,
|
||||||
"message", message.text()
|
"message", message
|
||||||
);
|
);
|
||||||
|
|
||||||
HttpRequest request = HttpRequest.newBuilder()
|
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) {
|
private static String formEncode(String... keyValues) {
|
||||||
if (keyValues.length % 2 != 0) {
|
if (keyValues.length % 2 != 0) {
|
||||||
throw new IllegalArgumentException("keyValues must contain key/value pairs");
|
throw new IllegalArgumentException("keyValues must contain key/value pairs");
|
||||||
@@ -74,10 +93,11 @@ public class PushOverNotifier implements BoundNotifier<PushMessage> {
|
|||||||
ΩAPIKeyΩ apiKey = "SomeKey";
|
ΩAPIKeyΩ apiKey = "SomeKey";
|
||||||
ΩUserNameΩ userName = "userNmae";
|
ΩUserNameΩ userName = "userNmae";
|
||||||
|
|
||||||
BoundNotifier<PushMessage> boundNotifier = new PushOverNotifier(apiKey, userName);
|
BoundNotificationService notificationService =
|
||||||
|
new PushoverNotificationService(apiKey, userName);
|
||||||
|
|
||||||
PushMessage message = new PushMessage("Hello World!");
|
ΩNotificationMessageΩ message = "Hello World!";
|
||||||
boundNotifier.push(message);
|
notificationService.push(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final URI PUSHOVER_MESSAGES_URI =
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+83
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user