Implement IronNotify Java SDK

- NotifyClient with global and instance-based usage
- Fluent EventBuilder API
- HTTP transport with OkHttp
- Offline queue with JSON persistence
- Severity levels and notification actions
- Thread-safe with synchronized blocks
- CompletableFuture for async operations
- Options builder pattern
- Full README with examples
This commit is contained in:
David Friedel 2025-12-25 10:54:29 +00:00
parent dbdfc61b8e
commit 2e73e0c51f
15 changed files with 2189 additions and 2 deletions

49
.gitignore vendored Normal file
View File

@ -0,0 +1,49 @@
# Compiled class files
*.class
# Log files
*.log
# Package files
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Gradle
.gradle/
build/
# IDE
.idea/
*.iml
*.ipr
*.iws
.project
.classpath
.settings/
.vscode/
*.swp
*.swo
# OS
.DS_Store
Thumbs.db
# Local storage
.ironnotify/

360
README.md
View File

@ -1,2 +1,358 @@
# ironnotify-java # IronNotify SDK for Java
IronNotify SDK for Java - Event notifications and alerts
Event notifications and alerts SDK for Java applications. Send notifications, receive real-time updates, and manage notification state.
[![Maven Central](https://img.shields.io/maven-central/v/com.ironservices/notify.svg)](https://search.maven.org/artifact/com.ironservices/notify)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
## Installation
### Maven
```xml
<dependency>
<groupId>com.ironservices</groupId>
<artifactId>notify</artifactId>
<version>1.0.0</version>
</dependency>
```
### Gradle
```groovy
implementation 'com.ironservices:notify:1.0.0'
```
## Quick Start
### Send a Simple Notification
```java
import com.ironservices.notify.*;
import java.util.Map;
public class Main {
public static void main(String[] args) {
// Initialize
IronNotify.init("ak_live_xxxxx");
// Send a simple notification
SendResult result = IronNotify.notify(
"order.created",
"New Order Received",
"Order #1234 has been placed",
SeverityLevel.SUCCESS,
Map.of("order_id", "1234", "amount", 99.99)
);
if (result.isSuccess()) {
System.out.println("Notification sent: " + result.getNotificationId());
}
// Shutdown when done
IronNotify.shutdown();
}
}
```
### Fluent Event Builder
```java
import com.ironservices.notify.*;
import java.time.Duration;
public class Main {
public static void main(String[] args) {
IronNotify.init("ak_live_xxxxx");
// Build complex notifications with the fluent API
SendResult result = IronNotify.event("payment.failed")
.withTitle("Payment Failed")
.withMessage("Payment could not be processed")
.withSeverity(SeverityLevel.ERROR)
.withMetadata("order_id", "1234")
.withMetadata("reason", "Card declined")
.withAction("Retry Payment", "/orders/1234/retry", null, "primary")
.withAction(EventBuilder.action("Contact Support")
.handler("open_support")
.build())
.forUser("user-123")
.withDeduplicationKey("payment-failed-1234")
.expiresIn(Duration.ofHours(24))
.send();
if (result.isQueued()) {
System.out.println("Notification queued for later");
}
IronNotify.shutdown();
}
}
```
### Async Support
```java
import com.ironservices.notify.*;
import java.util.concurrent.CompletableFuture;
public class Main {
public static void main(String[] args) {
IronNotify.init("ak_live_xxxxx");
// Async notification
CompletableFuture<SendResult> future = IronNotify.notifyAsync(
"user.signup",
"New User Registered"
);
future.thenAccept(result -> {
System.out.println("Notification sent: " + result.isSuccess());
});
// Async event builder
IronNotify.event("order.shipped")
.withTitle("Order Shipped")
.forUser("user-123")
.sendAsync()
.thenAccept(result -> System.out.println("Done!"));
// Wait for async operations
future.join();
IronNotify.shutdown();
}
}
```
### Using the Client Directly
```java
import com.ironservices.notify.*;
public class Main {
public static void main(String[] args) {
NotifyClient client = new NotifyClient(
NotifyOptions.builder()
.apiKey("ak_live_xxxxx")
.debug(true)
.build()
);
// Send notification
SendResult result = client.notify("event.type", "Title");
// Use event builder
result = client.event("event.type")
.withTitle("Title")
.send();
// Clean up
client.close();
}
}
```
## Configuration
```java
import com.ironservices.notify.*;
import java.time.Duration;
NotifyClient client = new NotifyClient(
NotifyOptions.builder()
.apiKey("ak_live_xxxxx")
.apiBaseUrl("https://api.ironnotify.com")
.webSocketUrl("wss://ws.ironnotify.com")
.debug(false)
.enableOfflineQueue(true)
.maxOfflineQueueSize(100)
.autoReconnect(true)
.maxReconnectAttempts(5)
.reconnectDelay(Duration.ofSeconds(1))
.httpTimeout(Duration.ofSeconds(30))
.build()
);
```
### Configuration Options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | String | required | Your API key (ak_live_xxx or ak_test_xxx) |
| `apiBaseUrl` | String | https://api.ironnotify.com | API base URL |
| `webSocketUrl` | String | wss://ws.ironnotify.com | WebSocket URL |
| `debug` | boolean | false | Enable debug logging |
| `enableOfflineQueue` | boolean | true | Queue notifications when offline |
| `maxOfflineQueueSize` | int | 100 | Max offline queue size |
| `autoReconnect` | boolean | true | Auto-reconnect WebSocket |
| `maxReconnectAttempts` | int | 5 | Max reconnection attempts |
| `reconnectDelay` | Duration | 1s | Base reconnection delay |
| `httpTimeout` | Duration | 30s | HTTP request timeout |
## Severity Levels
```java
SeverityLevel.INFO // "info"
SeverityLevel.SUCCESS // "success"
SeverityLevel.WARNING // "warning"
SeverityLevel.ERROR // "error"
SeverityLevel.CRITICAL // "critical"
```
## Actions
```java
// Simple action with URL
IronNotify.event("order.shipped")
.withTitle("Order Shipped")
.withAction("Track Package", "https://tracking.example.com/123")
.send();
// Action with handler
IronNotify.event("order.shipped")
.withTitle("Order Shipped")
.withAction("View Order", null, "view_order", "default")
.send();
// Using action builder
IronNotify.event("order.shipped")
.withTitle("Order Shipped")
.withAction(EventBuilder.action("Track Package")
.url("https://tracking.example.com/123")
.style("primary")
.build())
.send();
```
## Deduplication
Prevent duplicate notifications:
```java
IronNotify.event("reminder")
.withTitle("Daily Reminder")
.withDeduplicationKey("daily-reminder-2024-01-15")
.send();
```
## Grouping
Group related notifications:
```java
IronNotify.event("comment.new")
.withTitle("New Comment")
.withGroupKey("post-123-comments")
.send();
```
## Expiration
```java
import java.time.Duration;
import java.time.Instant;
// Expires in 1 hour
IronNotify.event("flash_sale")
.withTitle("Flash Sale!")
.expiresIn(Duration.ofHours(1))
.send();
// Expires at specific time
IronNotify.event("event_reminder")
.withTitle("Event Tomorrow")
.expiresAt(Instant.now().plus(Duration.ofDays(1)))
.send();
```
## Managing Notifications
### Get Notifications
```java
import java.util.List;
import java.io.IOException;
// Get all notifications
List<Notification> notifications = client.getNotifications();
// With options
List<Notification> unread = client.getNotifications(10, 0, true);
// Async
client.getNotificationsAsync(10, 0, false)
.thenAccept(list -> System.out.println("Got " + list.size() + " notifications"));
```
### Mark as Read
```java
// Mark single notification
client.markAsRead("notification-id");
// Mark all as read
client.markAllAsRead();
```
### Get Unread Count
```java
int count = client.getUnreadCount();
System.out.printf("You have %d unread notifications%n", count);
```
## Real-Time Notifications
```java
NotifyClient client = new NotifyClient(new NotifyOptions("ak_live_xxxxx"));
client.onNotification(notification -> {
System.out.println("New notification: " + notification.getTitle());
});
client.onUnreadCountChange(count -> {
System.out.println("Unread count: " + count);
});
client.onConnectionStateChange(state -> {
System.out.println("Connection state: " + state);
});
client.connect();
client.subscribeToUser("user-123");
client.subscribeToApp();
```
## Offline Support
Notifications are automatically queued when offline:
```java
// This will be queued if offline
IronNotify.notify("event", "Title");
// Manually flush the queue
IronNotify.flush();
// Or async
IronNotify.flushAsync().join();
```
## Requirements
- Java 11+
- OkHttp 4.x
- Gson 2.x
## Thread Safety
The client is thread-safe and can be used from multiple threads concurrently.
## Links
- [Documentation](https://www.ironnotify.com/docs)
- [Dashboard](https://www.ironnotify.com)
## License
MIT License - see [LICENSE](LICENSE) for details.

101
pom.xml Normal file
View File

@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.ironservices</groupId>
<artifactId>notify</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<name>IronNotify Java SDK</name>
<description>Event notifications and alerts SDK for Java applications</description>
<url>https://github.com/IronServices/ironnotify-java</url>
<licenses>
<license>
<name>MIT License</name>
<url>https://opensource.org/licenses/MIT</url>
</license>
</licenses>
<developers>
<developer>
<name>IronServices</name>
<email>support@ironservices.com</email>
<organization>IronServices</organization>
<organizationUrl>https://www.ironservices.com</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/IronServices/ironnotify-java.git</connection>
<developerConnection>scm:git:ssh://github.com:IronServices/ironnotify-java.git</developerConnection>
<url>https://github.com/IronServices/ironnotify-java/tree/main</url>
</scm>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.4</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<source>11</source>
<target>11</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>3.3.0</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>3.6.0</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,26 @@
package com.ironservices.notify;
/**
* WebSocket connection state.
*/
public enum ConnectionState {
DISCONNECTED("disconnected"),
CONNECTING("connecting"),
CONNECTED("connected"),
RECONNECTING("reconnecting");
private final String value;
ConnectionState(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}

View File

@ -0,0 +1,215 @@
package com.ironservices.notify;
import java.time.Duration;
import java.time.Instant;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* Builder for creating notifications with a fluent API.
*/
public class EventBuilder {
private final NotifyClient client;
private final String eventType;
private String title;
private String message;
private SeverityLevel severity = SeverityLevel.INFO;
private Map<String, Object> metadata = new HashMap<>();
private List<NotificationAction> actions = new ArrayList<>();
private String userId;
private String groupKey;
private String deduplicationKey;
private Instant expiresAt;
EventBuilder(NotifyClient client, String eventType) {
this.client = client;
this.eventType = eventType;
}
/**
* Sets the notification title.
*/
public EventBuilder withTitle(String title) {
this.title = title;
return this;
}
/**
* Sets the notification message.
*/
public EventBuilder withMessage(String message) {
this.message = message;
return this;
}
/**
* Sets the severity level.
*/
public EventBuilder withSeverity(SeverityLevel severity) {
this.severity = severity;
return this;
}
/**
* Adds a metadata entry.
*/
public EventBuilder withMetadata(String key, Object value) {
this.metadata.put(key, value);
return this;
}
/**
* Adds multiple metadata entries.
*/
public EventBuilder withMetadata(Map<String, Object> metadata) {
this.metadata.putAll(metadata);
return this;
}
/**
* Adds an action button with a URL.
*/
public EventBuilder withAction(String label, String url) {
this.actions.add(new NotificationAction(label, url, null, "default"));
return this;
}
/**
* Adds an action button with options.
*/
public EventBuilder withAction(String label, String url, String action, String style) {
this.actions.add(new NotificationAction(label, url, action, style));
return this;
}
/**
* Adds an action using the ActionBuilder.
*/
public EventBuilder withAction(NotificationAction action) {
this.actions.add(action);
return this;
}
/**
* Sets the target user ID.
*/
public EventBuilder forUser(String userId) {
this.userId = userId;
return this;
}
/**
* Sets the group key for grouping related notifications.
*/
public EventBuilder withGroupKey(String groupKey) {
this.groupKey = groupKey;
return this;
}
/**
* Sets the deduplication key.
*/
public EventBuilder withDeduplicationKey(String key) {
this.deduplicationKey = key;
return this;
}
/**
* Sets the expiration time from now.
*/
public EventBuilder expiresIn(Duration duration) {
this.expiresAt = Instant.now().plus(duration);
return this;
}
/**
* Sets the expiration time.
*/
public EventBuilder expiresAt(Instant instant) {
this.expiresAt = instant;
return this;
}
/**
* Builds the notification payload.
*/
public NotificationPayload build() {
if (title == null || title.isEmpty()) {
throw new IllegalStateException("Notification title is required");
}
NotificationPayload payload = new NotificationPayload(eventType, title);
payload.setMessage(message);
payload.setSeverity(severity);
payload.setUserId(userId);
payload.setGroupKey(groupKey);
payload.setDeduplicationKey(deduplicationKey);
payload.setExpiresAt(expiresAt);
if (!metadata.isEmpty()) {
payload.setMetadata(metadata);
}
if (!actions.isEmpty()) {
payload.setActions(actions);
}
return payload;
}
/**
* Sends the notification synchronously.
*/
public SendResult send() {
NotificationPayload payload = build();
return client.sendPayload(payload);
}
/**
* Sends the notification asynchronously.
*/
public CompletableFuture<SendResult> sendAsync() {
NotificationPayload payload = build();
return client.sendPayloadAsync(payload);
}
/**
* Creates an action builder for complex actions.
*/
public static ActionBuilder action(String label) {
return new ActionBuilder(label);
}
/**
* Builder for notification actions.
*/
public static class ActionBuilder {
private final NotificationAction action;
ActionBuilder(String label) {
this.action = new NotificationAction(label);
}
public ActionBuilder url(String url) {
action.setUrl(url);
return this;
}
public ActionBuilder handler(String actionHandler) {
action.setAction(actionHandler);
return this;
}
public ActionBuilder style(String style) {
action.setStyle(style);
return this;
}
public NotificationAction build() {
return action;
}
}
}

View File

@ -0,0 +1,173 @@
package com.ironservices.notify;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
/**
* Static facade for the IronNotify SDK.
* Provides convenient static methods that delegate to the global client.
*/
public final class IronNotify {
private IronNotify() {
// Utility class
}
/**
* Initializes the SDK with an API key.
*/
public static void init(String apiKey) {
NotifyClient.init(apiKey);
}
/**
* Initializes the SDK with options.
*/
public static void init(NotifyOptions options) {
NotifyClient.init(options);
}
/**
* Sends a notification.
*/
public static SendResult notify(String eventType, String title) {
return NotifyClient.get().notify(eventType, title);
}
/**
* Sends a notification with options.
*/
public static SendResult notify(String eventType, String title, String message,
SeverityLevel severity, Map<String, Object> metadata) {
return NotifyClient.get().notify(eventType, title, message, severity, metadata);
}
/**
* Sends a notification asynchronously.
*/
public static CompletableFuture<SendResult> notifyAsync(String eventType, String title) {
return NotifyClient.get().notifyAsync(eventType, title);
}
/**
* Sends a notification asynchronously with options.
*/
public static CompletableFuture<SendResult> notifyAsync(String eventType, String title, String message,
SeverityLevel severity, Map<String, Object> metadata) {
return NotifyClient.get().notifyAsync(eventType, title, message, severity, metadata);
}
/**
* Creates an event builder.
*/
public static EventBuilder event(String eventType) {
return NotifyClient.get().event(eventType);
}
/**
* Gets notifications.
*/
public static List<Notification> getNotifications() throws IOException {
return NotifyClient.get().getNotifications();
}
/**
* Gets notifications with options.
*/
public static List<Notification> getNotifications(Integer limit, Integer offset, boolean unreadOnly) throws IOException {
return NotifyClient.get().getNotifications(limit, offset, unreadOnly);
}
/**
* Gets notifications asynchronously.
*/
public static CompletableFuture<List<Notification>> getNotificationsAsync() {
return NotifyClient.get().getNotificationsAsync();
}
/**
* Gets notifications asynchronously with options.
*/
public static CompletableFuture<List<Notification>> getNotificationsAsync(Integer limit, Integer offset, boolean unreadOnly) {
return NotifyClient.get().getNotificationsAsync(limit, offset, unreadOnly);
}
/**
* Gets the unread count.
*/
public static int getUnreadCount() throws IOException {
return NotifyClient.get().getUnreadCount();
}
/**
* Gets the unread count asynchronously.
*/
public static CompletableFuture<Integer> getUnreadCountAsync() {
return NotifyClient.get().getUnreadCountAsync();
}
/**
* Marks a notification as read.
*/
public static boolean markAsRead(String notificationId) throws IOException {
return NotifyClient.get().markAsRead(notificationId);
}
/**
* Marks all notifications as read.
*/
public static boolean markAllAsRead() throws IOException {
return NotifyClient.get().markAllAsRead();
}
/**
* Connects to real-time notifications.
*/
public static void connect() {
NotifyClient.get().connect();
}
/**
* Disconnects from real-time notifications.
*/
public static void disconnect() {
NotifyClient.get().disconnect();
}
/**
* Subscribes to a user's notifications.
*/
public static void subscribeToUser(String userId) {
NotifyClient.get().subscribeToUser(userId);
}
/**
* Subscribes to app-wide notifications.
*/
public static void subscribeToApp() {
NotifyClient.get().subscribeToApp();
}
/**
* Flushes the offline queue.
*/
public static void flush() {
NotifyClient.get().flush();
}
/**
* Flushes the offline queue asynchronously.
*/
public static CompletableFuture<Void> flushAsync() {
return NotifyClient.get().flushAsync();
}
/**
* Shuts down the SDK.
*/
public static void shutdown() {
NotifyClient.shutdown();
}
}

View File

@ -0,0 +1,119 @@
package com.ironservices.notify;
import java.time.Instant;
import java.util.List;
import java.util.Map;
/**
* A notification received from the server.
*/
public class Notification {
private String id;
private String eventType;
private String title;
private String message;
private SeverityLevel severity;
private Map<String, Object> metadata;
private List<NotificationAction> actions;
private String userId;
private String groupKey;
private boolean read;
private Instant createdAt;
private Instant expiresAt;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getEventType() {
return eventType;
}
public void setEventType(String eventType) {
this.eventType = eventType;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public SeverityLevel getSeverity() {
return severity;
}
public void setSeverity(SeverityLevel severity) {
this.severity = severity;
}
public Map<String, Object> getMetadata() {
return metadata;
}
public void setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
}
public List<NotificationAction> getActions() {
return actions;
}
public void setActions(List<NotificationAction> actions) {
this.actions = actions;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public String getGroupKey() {
return groupKey;
}
public void setGroupKey(String groupKey) {
this.groupKey = groupKey;
}
public boolean isRead() {
return read;
}
public void setRead(boolean read) {
this.read = read;
}
public Instant getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Instant createdAt) {
this.createdAt = createdAt;
}
public Instant getExpiresAt() {
return expiresAt;
}
public void setExpiresAt(Instant expiresAt) {
this.expiresAt = expiresAt;
}
}

View File

@ -0,0 +1,63 @@
package com.ironservices.notify;
/**
* Action button on a notification.
*/
public class NotificationAction {
private String label;
private String url;
private String action;
private String style;
public NotificationAction() {
this.style = "default";
}
public NotificationAction(String label) {
this.label = label;
this.style = "default";
}
public NotificationAction(String label, String url, String action, String style) {
this.label = label;
this.url = url;
this.action = action;
this.style = style != null ? style : "default";
}
public String getLabel() {
return label;
}
public NotificationAction setLabel(String label) {
this.label = label;
return this;
}
public String getUrl() {
return url;
}
public NotificationAction setUrl(String url) {
this.url = url;
return this;
}
public String getAction() {
return action;
}
public NotificationAction setAction(String action) {
this.action = action;
return this;
}
public String getStyle() {
return style;
}
public NotificationAction setStyle(String style) {
this.style = style;
return this;
}
}

View File

@ -0,0 +1,121 @@
package com.ironservices.notify;
import java.time.Instant;
import java.util.List;
import java.util.Map;
/**
* Payload for creating a notification.
*/
public class NotificationPayload {
private String eventType;
private String title;
private String message;
private SeverityLevel severity;
private Map<String, Object> metadata;
private List<NotificationAction> actions;
private String userId;
private String groupKey;
private String deduplicationKey;
private Instant expiresAt;
public NotificationPayload() {
this.severity = SeverityLevel.INFO;
}
public NotificationPayload(String eventType, String title) {
this.eventType = eventType;
this.title = title;
this.severity = SeverityLevel.INFO;
}
public String getEventType() {
return eventType;
}
public NotificationPayload setEventType(String eventType) {
this.eventType = eventType;
return this;
}
public String getTitle() {
return title;
}
public NotificationPayload setTitle(String title) {
this.title = title;
return this;
}
public String getMessage() {
return message;
}
public NotificationPayload setMessage(String message) {
this.message = message;
return this;
}
public SeverityLevel getSeverity() {
return severity;
}
public NotificationPayload setSeverity(SeverityLevel severity) {
this.severity = severity;
return this;
}
public Map<String, Object> getMetadata() {
return metadata;
}
public NotificationPayload setMetadata(Map<String, Object> metadata) {
this.metadata = metadata;
return this;
}
public List<NotificationAction> getActions() {
return actions;
}
public NotificationPayload setActions(List<NotificationAction> actions) {
this.actions = actions;
return this;
}
public String getUserId() {
return userId;
}
public NotificationPayload setUserId(String userId) {
this.userId = userId;
return this;
}
public String getGroupKey() {
return groupKey;
}
public NotificationPayload setGroupKey(String groupKey) {
this.groupKey = groupKey;
return this;
}
public String getDeduplicationKey() {
return deduplicationKey;
}
public NotificationPayload setDeduplicationKey(String deduplicationKey) {
this.deduplicationKey = deduplicationKey;
return this;
}
public Instant getExpiresAt() {
return expiresAt;
}
public NotificationPayload setExpiresAt(Instant expiresAt) {
this.expiresAt = expiresAt;
return this;
}
}

View File

@ -0,0 +1,347 @@
package com.ironservices.notify;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.function.Consumer;
/**
* IronNotify client for sending and receiving notifications.
*/
public class NotifyClient implements AutoCloseable {
private final NotifyOptions options;
private final Transport transport;
private final OfflineQueue queue;
private volatile boolean isOnline = true;
private volatile ConnectionState connectionState = ConnectionState.DISCONNECTED;
private Consumer<Notification> onNotification;
private Consumer<Integer> onUnreadCountChange;
private Consumer<ConnectionState> onConnectionStateChange;
private static NotifyClient globalClient;
private static final Object globalLock = new Object();
/**
* Creates a new NotifyClient with the given options.
*/
public NotifyClient(NotifyOptions options) {
if (options.getApiKey() == null || options.getApiKey().isEmpty()) {
throw new IllegalArgumentException("API key is required");
}
this.options = options;
this.transport = new Transport(
options.getApiBaseUrl(),
options.getApiKey(),
options.getHttpTimeout(),
options.isDebug()
);
if (options.isEnableOfflineQueue()) {
this.queue = new OfflineQueue(options.getMaxOfflineQueueSize(), options.isDebug());
} else {
this.queue = null;
}
if (options.isDebug()) {
System.out.println("[IronNotify] Client initialized");
}
}
/**
* Initializes the global client.
*/
public static void init(String apiKey) {
init(new NotifyOptions(apiKey));
}
/**
* Initializes the global client with options.
*/
public static void init(NotifyOptions options) {
synchronized (globalLock) {
if (globalClient != null) {
globalClient.close();
}
globalClient = new NotifyClient(options);
}
}
/**
* Gets the global client.
*/
public static NotifyClient get() {
synchronized (globalLock) {
if (globalClient == null) {
throw new IllegalStateException("IronNotify not initialized. Call init() first.");
}
return globalClient;
}
}
/**
* Sends a notification.
*/
public SendResult notify(String eventType, String title) {
return notify(eventType, title, null, SeverityLevel.INFO, null);
}
/**
* Sends a notification with options.
*/
public SendResult notify(String eventType, String title, String message,
SeverityLevel severity, Map<String, Object> metadata) {
NotificationPayload payload = new NotificationPayload(eventType, title);
payload.setMessage(message);
payload.setSeverity(severity);
payload.setMetadata(metadata);
return sendPayload(payload);
}
/**
* Sends a notification asynchronously.
*/
public CompletableFuture<SendResult> notifyAsync(String eventType, String title) {
return notifyAsync(eventType, title, null, SeverityLevel.INFO, null);
}
/**
* Sends a notification asynchronously with options.
*/
public CompletableFuture<SendResult> notifyAsync(String eventType, String title, String message,
SeverityLevel severity, Map<String, Object> metadata) {
NotificationPayload payload = new NotificationPayload(eventType, title);
payload.setMessage(message);
payload.setSeverity(severity);
payload.setMetadata(metadata);
return sendPayloadAsync(payload);
}
/**
* Creates an event builder.
*/
public EventBuilder event(String eventType) {
return new EventBuilder(this, eventType);
}
/**
* Sends a notification payload.
*/
public SendResult sendPayload(NotificationPayload payload) {
SendResult result = transport.send(payload);
if (!result.isSuccess() && options.isEnableOfflineQueue() && queue != null) {
queue.add(payload);
isOnline = false;
return SendResult.queued(result.getError());
}
return result;
}
/**
* Sends a notification payload asynchronously.
*/
public CompletableFuture<SendResult> sendPayloadAsync(NotificationPayload payload) {
return transport.sendAsync(payload).thenApply(result -> {
if (!result.isSuccess() && options.isEnableOfflineQueue() && queue != null) {
queue.add(payload);
isOnline = false;
return SendResult.queued(result.getError());
}
return result;
});
}
/**
* Gets notifications.
*/
public List<Notification> getNotifications() throws IOException {
return getNotifications(null, null, false);
}
/**
* Gets notifications with options.
*/
public List<Notification> getNotifications(Integer limit, Integer offset, boolean unreadOnly) throws IOException {
return transport.getNotifications(limit, offset, unreadOnly);
}
/**
* Gets notifications asynchronously.
*/
public CompletableFuture<List<Notification>> getNotificationsAsync() {
return getNotificationsAsync(null, null, false);
}
/**
* Gets notifications asynchronously with options.
*/
public CompletableFuture<List<Notification>> getNotificationsAsync(Integer limit, Integer offset, boolean unreadOnly) {
return transport.getNotificationsAsync(limit, offset, unreadOnly);
}
/**
* Gets the unread notification count.
*/
public int getUnreadCount() throws IOException {
return transport.getUnreadCount();
}
/**
* Gets the unread notification count asynchronously.
*/
public CompletableFuture<Integer> getUnreadCountAsync() {
return transport.getUnreadCountAsync();
}
/**
* Marks a notification as read.
*/
public boolean markAsRead(String notificationId) throws IOException {
return transport.markAsRead(notificationId);
}
/**
* Marks a notification as read asynchronously.
*/
public CompletableFuture<Boolean> markAsReadAsync(String notificationId) {
return transport.markAsReadAsync(notificationId);
}
/**
* Marks all notifications as read.
*/
public boolean markAllAsRead() throws IOException {
return transport.markAllAsRead();
}
/**
* Marks all notifications as read asynchronously.
*/
public CompletableFuture<Boolean> markAllAsReadAsync() {
return transport.markAllAsReadAsync();
}
/**
* Sets the notification handler.
*/
public void onNotification(Consumer<Notification> handler) {
this.onNotification = handler;
}
/**
* Sets the unread count change handler.
*/
public void onUnreadCountChange(Consumer<Integer> handler) {
this.onUnreadCountChange = handler;
}
/**
* Sets the connection state change handler.
*/
public void onConnectionStateChange(Consumer<ConnectionState> handler) {
this.onConnectionStateChange = handler;
}
/**
* Gets the current connection state.
*/
public ConnectionState getConnectionState() {
return connectionState;
}
/**
* Connects to real-time notifications.
*/
public void connect() {
setConnectionState(ConnectionState.CONNECTED);
if (options.isDebug()) {
System.out.println("[IronNotify] Connected (WebSocket not implemented)");
}
}
/**
* Disconnects from real-time notifications.
*/
public void disconnect() {
setConnectionState(ConnectionState.DISCONNECTED);
}
/**
* Subscribes to a user's notifications.
*/
public void subscribeToUser(String userId) {
if (options.isDebug()) {
System.out.println("[IronNotify] Subscribed to user: " + userId);
}
}
/**
* Subscribes to app-wide notifications.
*/
public void subscribeToApp() {
if (options.isDebug()) {
System.out.println("[IronNotify] Subscribed to app notifications");
}
}
/**
* Flushes the offline queue.
*/
public void flush() {
if (queue == null || queue.isEmpty()) {
return;
}
if (!transport.isOnline()) {
return;
}
isOnline = true;
List<NotificationPayload> notifications = queue.getAll();
for (int i = notifications.size() - 1; i >= 0; i--) {
SendResult result = transport.send(notifications.get(i));
if (result.isSuccess()) {
queue.remove(i);
} else {
break;
}
}
}
/**
* Flushes the offline queue asynchronously.
*/
public CompletableFuture<Void> flushAsync() {
return CompletableFuture.runAsync(this::flush);
}
private void setConnectionState(ConnectionState state) {
this.connectionState = state;
if (onConnectionStateChange != null) {
onConnectionStateChange.accept(state);
}
}
@Override
public void close() {
disconnect();
transport.close();
}
/**
* Closes the global client.
*/
public static void shutdown() {
synchronized (globalLock) {
if (globalClient != null) {
globalClient.close();
globalClient = null;
}
}
}
}

View File

@ -0,0 +1,187 @@
package com.ironservices.notify;
import java.time.Duration;
/**
* Configuration options for the IronNotify client.
*/
public class NotifyOptions {
private String apiKey;
private String apiBaseUrl = "https://api.ironnotify.com";
private String webSocketUrl = "wss://ws.ironnotify.com";
private boolean debug = false;
private boolean enableOfflineQueue = true;
private int maxOfflineQueueSize = 100;
private boolean autoReconnect = true;
private int maxReconnectAttempts = 5;
private Duration reconnectDelay = Duration.ofSeconds(1);
private Duration httpTimeout = Duration.ofSeconds(30);
public NotifyOptions() {
}
public NotifyOptions(String apiKey) {
this.apiKey = apiKey;
}
public String getApiKey() {
return apiKey;
}
public NotifyOptions setApiKey(String apiKey) {
this.apiKey = apiKey;
return this;
}
public String getApiBaseUrl() {
return apiBaseUrl;
}
public NotifyOptions setApiBaseUrl(String apiBaseUrl) {
this.apiBaseUrl = apiBaseUrl;
return this;
}
public String getWebSocketUrl() {
return webSocketUrl;
}
public NotifyOptions setWebSocketUrl(String webSocketUrl) {
this.webSocketUrl = webSocketUrl;
return this;
}
public boolean isDebug() {
return debug;
}
public NotifyOptions setDebug(boolean debug) {
this.debug = debug;
return this;
}
public boolean isEnableOfflineQueue() {
return enableOfflineQueue;
}
public NotifyOptions setEnableOfflineQueue(boolean enableOfflineQueue) {
this.enableOfflineQueue = enableOfflineQueue;
return this;
}
public int getMaxOfflineQueueSize() {
return maxOfflineQueueSize;
}
public NotifyOptions setMaxOfflineQueueSize(int maxOfflineQueueSize) {
this.maxOfflineQueueSize = maxOfflineQueueSize;
return this;
}
public boolean isAutoReconnect() {
return autoReconnect;
}
public NotifyOptions setAutoReconnect(boolean autoReconnect) {
this.autoReconnect = autoReconnect;
return this;
}
public int getMaxReconnectAttempts() {
return maxReconnectAttempts;
}
public NotifyOptions setMaxReconnectAttempts(int maxReconnectAttempts) {
this.maxReconnectAttempts = maxReconnectAttempts;
return this;
}
public Duration getReconnectDelay() {
return reconnectDelay;
}
public NotifyOptions setReconnectDelay(Duration reconnectDelay) {
this.reconnectDelay = reconnectDelay;
return this;
}
public Duration getHttpTimeout() {
return httpTimeout;
}
public NotifyOptions setHttpTimeout(Duration httpTimeout) {
this.httpTimeout = httpTimeout;
return this;
}
/**
* Creates a builder for NotifyOptions.
*/
public static Builder builder() {
return new Builder();
}
/**
* Builder for NotifyOptions.
*/
public static class Builder {
private final NotifyOptions options = new NotifyOptions();
public Builder apiKey(String apiKey) {
options.setApiKey(apiKey);
return this;
}
public Builder apiBaseUrl(String apiBaseUrl) {
options.setApiBaseUrl(apiBaseUrl);
return this;
}
public Builder webSocketUrl(String webSocketUrl) {
options.setWebSocketUrl(webSocketUrl);
return this;
}
public Builder debug(boolean debug) {
options.setDebug(debug);
return this;
}
public Builder enableOfflineQueue(boolean enable) {
options.setEnableOfflineQueue(enable);
return this;
}
public Builder maxOfflineQueueSize(int size) {
options.setMaxOfflineQueueSize(size);
return this;
}
public Builder autoReconnect(boolean autoReconnect) {
options.setAutoReconnect(autoReconnect);
return this;
}
public Builder maxReconnectAttempts(int attempts) {
options.setMaxReconnectAttempts(attempts);
return this;
}
public Builder reconnectDelay(Duration delay) {
options.setReconnectDelay(delay);
return this;
}
public Builder httpTimeout(Duration timeout) {
options.setHttpTimeout(timeout);
return this;
}
public NotifyOptions build() {
if (options.getApiKey() == null || options.getApiKey().isEmpty()) {
throw new IllegalArgumentException("API key is required");
}
return options;
}
}
}

View File

@ -0,0 +1,111 @@
package com.ironservices.notify;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
/**
* Offline queue for storing notifications when offline.
*/
class OfflineQueue {
private final int maxSize;
private final boolean debug;
private final List<NotificationPayload> queue;
private final Path storagePath;
private final Gson gson;
private final Object lock = new Object();
OfflineQueue(int maxSize, boolean debug) {
this.maxSize = maxSize;
this.debug = debug;
this.queue = new ArrayList<>();
this.storagePath = Paths.get(System.getProperty("user.home"), ".ironnotify", "offline_queue.json");
this.gson = new GsonBuilder().create();
loadFromStorage();
}
void add(NotificationPayload payload) {
synchronized (lock) {
if (queue.size() >= maxSize) {
queue.remove(0);
if (debug) {
System.out.println("[IronNotify] Offline queue full, dropping oldest notification");
}
}
queue.add(payload);
saveToStorage();
if (debug) {
System.out.println("[IronNotify] Notification queued for later: " + payload.getEventType());
}
}
}
List<NotificationPayload> getAll() {
synchronized (lock) {
return new ArrayList<>(queue);
}
}
void remove(int index) {
synchronized (lock) {
if (index >= 0 && index < queue.size()) {
queue.remove(index);
saveToStorage();
}
}
}
void clear() {
synchronized (lock) {
queue.clear();
saveToStorage();
}
}
int size() {
synchronized (lock) {
return queue.size();
}
}
boolean isEmpty() {
synchronized (lock) {
return queue.isEmpty();
}
}
private void loadFromStorage() {
try {
if (Files.exists(storagePath)) {
String json = new String(Files.readAllBytes(storagePath));
Type listType = new TypeToken<List<NotificationPayload>>(){}.getType();
List<NotificationPayload> loaded = gson.fromJson(json, listType);
if (loaded != null) {
queue.addAll(loaded);
}
}
} catch (Exception e) {
// Ignore errors loading from storage
}
}
private void saveToStorage() {
try {
Files.createDirectories(storagePath.getParent());
String json = gson.toJson(queue);
Files.write(storagePath, json.getBytes());
} catch (Exception e) {
// Ignore errors saving to storage
}
}
}

View File

@ -0,0 +1,46 @@
package com.ironservices.notify;
/**
* Result of sending a notification.
*/
public class SendResult {
private final boolean success;
private final String notificationId;
private final String error;
private final boolean queued;
private SendResult(boolean success, String notificationId, String error, boolean queued) {
this.success = success;
this.notificationId = notificationId;
this.error = error;
this.queued = queued;
}
public static SendResult success(String notificationId) {
return new SendResult(true, notificationId, null, false);
}
public static SendResult failure(String error) {
return new SendResult(false, null, error, false);
}
public static SendResult queued(String error) {
return new SendResult(false, null, error, true);
}
public boolean isSuccess() {
return success;
}
public String getNotificationId() {
return notificationId;
}
public String getError() {
return error;
}
public boolean isQueued() {
return queued;
}
}

View File

@ -0,0 +1,38 @@
package com.ironservices.notify;
import com.google.gson.annotations.SerializedName;
/**
* Severity level for notifications.
*/
public enum SeverityLevel {
@SerializedName("info")
INFO("info"),
@SerializedName("success")
SUCCESS("success"),
@SerializedName("warning")
WARNING("warning"),
@SerializedName("error")
ERROR("error"),
@SerializedName("critical")
CRITICAL("critical");
private final String value;
SeverityLevel(String value) {
this.value = value;
}
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
}
}

View File

@ -0,0 +1,235 @@
package com.ironservices.notify;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import okhttp3.*;
import java.io.IOException;
import java.lang.reflect.Type;
import java.time.Duration;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/**
* HTTP transport for IronNotify API.
*/
class Transport {
private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
private final String baseUrl;
private final String apiKey;
private final boolean debug;
private final OkHttpClient client;
private final Gson gson;
Transport(String baseUrl, String apiKey, Duration timeout, boolean debug) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
this.debug = debug;
this.client = new OkHttpClient.Builder()
.connectTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
.readTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
.writeTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)
.build();
this.gson = new GsonBuilder()
.setFieldNamingStrategy(field -> {
String name = field.getName();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < name.length(); i++) {
char c = name.charAt(i);
if (Character.isUpperCase(c)) {
if (i > 0) {
sb.append('_');
}
sb.append(Character.toLowerCase(c));
} else {
sb.append(c);
}
}
return sb.toString();
})
.create();
}
SendResult send(NotificationPayload payload) {
try {
String json = gson.toJson(payload);
if (debug) {
System.out.println("[IronNotify] Sending notification: " + payload.getEventType());
}
Request request = new Request.Builder()
.url(baseUrl + "/api/v1/notify")
.header("Authorization", "Bearer " + apiKey)
.post(RequestBody.create(json, JSON))
.build();
try (Response response = client.newCall(request).execute()) {
String body = response.body() != null ? response.body().string() : "";
if (response.isSuccessful()) {
SendResponse result = gson.fromJson(body, SendResponse.class);
return SendResult.success(result != null ? result.notificationId : null);
}
ErrorResponse error = gson.fromJson(body, ErrorResponse.class);
return SendResult.failure(error != null && error.error != null
? error.error
: "HTTP " + response.code() + ": " + body);
}
} catch (IOException e) {
return SendResult.failure(e.getMessage());
}
}
CompletableFuture<SendResult> sendAsync(NotificationPayload payload) {
return CompletableFuture.supplyAsync(() -> send(payload));
}
List<Notification> getNotifications(Integer limit, Integer offset, boolean unreadOnly) throws IOException {
HttpUrl.Builder urlBuilder = HttpUrl.parse(baseUrl + "/api/v1/notifications").newBuilder();
if (limit != null && limit > 0) {
urlBuilder.addQueryParameter("limit", String.valueOf(limit));
}
if (offset != null && offset > 0) {
urlBuilder.addQueryParameter("offset", String.valueOf(offset));
}
if (unreadOnly) {
urlBuilder.addQueryParameter("unread_only", "true");
}
Request request = new Request.Builder()
.url(urlBuilder.build())
.header("Authorization", "Bearer " + apiKey)
.get()
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("HTTP " + response.code());
}
String body = response.body() != null ? response.body().string() : "[]";
Type listType = new TypeToken<List<Notification>>(){}.getType();
return gson.fromJson(body, listType);
}
}
CompletableFuture<List<Notification>> getNotificationsAsync(Integer limit, Integer offset, boolean unreadOnly) {
return CompletableFuture.supplyAsync(() -> {
try {
return getNotifications(limit, offset, unreadOnly);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
int getUnreadCount() throws IOException {
Request request = new Request.Builder()
.url(baseUrl + "/api/v1/notifications/unread-count")
.header("Authorization", "Bearer " + apiKey)
.get()
.build();
try (Response response = client.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new IOException("HTTP " + response.code());
}
String body = response.body() != null ? response.body().string() : "{}";
CountResponse result = gson.fromJson(body, CountResponse.class);
return result != null ? result.count : 0;
}
}
CompletableFuture<Integer> getUnreadCountAsync() {
return CompletableFuture.supplyAsync(() -> {
try {
return getUnreadCount();
} catch (IOException e) {
throw new RuntimeException(e);
}
});
}
boolean markAsRead(String notificationId) throws IOException {
Request request = new Request.Builder()
.url(baseUrl + "/api/v1/notifications/" + notificationId + "/read")
.header("Authorization", "Bearer " + apiKey)
.post(RequestBody.create("", JSON))
.build();
try (Response response = client.newCall(request).execute()) {
return response.isSuccessful();
}
}
CompletableFuture<Boolean> markAsReadAsync(String notificationId) {
return CompletableFuture.supplyAsync(() -> {
try {
return markAsRead(notificationId);
} catch (IOException e) {
return false;
}
});
}
boolean markAllAsRead() throws IOException {
Request request = new Request.Builder()
.url(baseUrl + "/api/v1/notifications/read-all")
.header("Authorization", "Bearer " + apiKey)
.post(RequestBody.create("", JSON))
.build();
try (Response response = client.newCall(request).execute()) {
return response.isSuccessful();
}
}
CompletableFuture<Boolean> markAllAsReadAsync() {
return CompletableFuture.supplyAsync(() -> {
try {
return markAllAsRead();
} catch (IOException e) {
return false;
}
});
}
boolean isOnline() {
try {
Request request = new Request.Builder()
.url(baseUrl + "/health")
.get()
.build();
try (Response response = client.newCall(request).execute()) {
return response.isSuccessful();
}
} catch (IOException e) {
return false;
}
}
void close() {
client.dispatcher().executorService().shutdown();
client.connectionPool().evictAll();
}
private static class SendResponse {
String notificationId;
}
private static class ErrorResponse {
String error;
}
private static class CountResponse {
int count;
}
}