Skip to content

Commit 2778e94

Browse files
CIS-3607 Notification Type Autodiscovery (#28)
Implement a method for autodiscovering notification types at application startup.
1 parent 6b02e5e commit 2778e94

File tree

5 files changed

+159
-15
lines changed

5 files changed

+159
-15
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add automatic registration of notification types defined by a `NotificationTypeProvider` interface (CIS-3607)
13+
1014
## [2.1.0] - 2026-03-04
1115

1216
### Added
@@ -21,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2125

2226
### Changed
2327

24-
- Potentially Breaking: Redesigned NotificationViewer interface to encourage thread safety. This only impacts applications that used the prepare method in 1.0.0. This has been replaced by prepareCache.
28+
- Potentially Breaking: Redesigned NotificationViewer interface to encourage thread safety. This only impacts applications that used the prepare method in 1.0.0. This has been replaced by prepareCache.
2529
- Updated documentation for NotificationViewer
2630

2731
## [1.0.0] - 2025-10-21

NOTIFICATION_REGISTRATION.md

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,8 @@ public class ClientWelcomeDispatcher implements NotificationDispatcher {
5252
public DispatchResult handleDispatch(Notification notification) {
5353
var participant = (Participant) notification.getRecipient();
5454
try {
55-
var deliveryDetails = messageDeliveryService.sendEmail(notificationProperties.getEmail(),
56-
participant.getEmail(),
55+
var deliveryDetails = messageDeliveryService.sendEmail(notificationProperties.getEmail(),
56+
participant.getEmail(),
5757
"Message Subject",
5858
"Message Content");
5959
return new DispatchResult(true, messageContent, recipient,
@@ -173,11 +173,11 @@ This is how it will manifest in the Recipient and Info columns of the list page:
173173

174174
Because the methods `getRecipientView` and `getMetadataView` will be called for every Notification, applications may want to set up a ThreadLocal cache to optimize these queries on the full list first. In this case, applications can override the `NotificationViewer` default method `prepareCache`:
175175

176-
```
176+
```java
177177
/**
178178
* Applications that need to fetch data for each notification can prefetch and cache in a ThreadLocal to optimize
179179
* performance.
180-
*
180+
*
181181
* @param notifications
182182
* the list of notifications that will be viewed
183183
* @return an AutoCloseable that will clear the cache when closed; default has no cache and returns a no-op
@@ -191,7 +191,7 @@ Because the methods `getRecipientView` and `getMetadataView` will be called for
191191

192192
This is how the implementation above would be rewritten to use a cache:
193193

194-
```
194+
```java
195195
public class SurveyReminderViewer {
196196

197197
private final ParticipantService participantService;
@@ -258,7 +258,45 @@ public class SurveyReminderViewer {
258258

259259
## Register the Notification Type
260260

261-
With all the necessary classes defined, use the [`NotificationTypeRegistry`](src/main/java/org/octri/notification/registry/NotificationTypeRegistry.java) to handle the new custom type.
261+
Once all of the necessary classes have been defined for your new notification, it needs to be registered wih the [`NotificationTypeRegistry`](src/main/java/org/octri/notification/registry/NotificationTypeRegistry.java). Notifications may be manually registered with the [`NotificationTypeRegistry`](src/main/java/org/octri/notification/registry/NotificationTypeRegistry.java) or registered automatically using the [`NotificationTypeProvider`](src/main/java/org/octri/notification/registry/NotificationTypeProvider.java) interface. While manual registration may be appropriate for simple applications, automatic registration simplifies the addition of new notification types and is recommended for most cases.
262+
263+
### Automatic Registration
264+
265+
[`NotificationTypeProvider`](src/main/java/org/octri/notification/registry/NotificationTypeProvider.java) is a convenience interface that allows notification types to be automatically discovered and registered with the type registry. Implementing classes return the objects needed to process a notification type. Most methods have default implementations, so a minimal implementation only needs to provide a unique string to identify the notification and a `NotificationDispatcher`.
266+
267+
```java
268+
public interface NotificationTypeProvider {
269+
270+
String getNotificationType();
271+
272+
default ProcessingMode getProcessingMode() {
273+
return ProcessingMode.SCHEDULED;
274+
}
275+
276+
default Class<? extends NotificationMetadata> getNotificationMetadata() {
277+
return EmptyMetadata.class;
278+
}
279+
280+
default NotificationValidator getNotificationValidator() {
281+
return NotificationValidator.NOOP;
282+
};
283+
284+
NotificationDispatcher getNotificationDispatcher();
285+
286+
default NotificationViewer getNotificationViewer() {
287+
return new EmptyMetadataViewer();
288+
}
289+
290+
}
291+
```
292+
293+
Concrete implementations of `NotificationTypeProvider` can be `@Component` classes that get the needed dependencies through constructor injection.
294+
295+
On application startup, [the library's autoconfiguration](src/main/java/org/octri/notification/config/NotificationAutoConfiguration.java) finds all of the `NotificationTypeProvider` beans in the application context and registers their notifications with the `NotificationTypeRegistry`.
296+
297+
### Manual Registration
298+
299+
To manually register a new notification type, use the [`NotificationTypeRegistry`](src/main/java/org/octri/notification/registry/NotificationTypeRegistry.java) directly.
262300

263301
```java
264302
@Component

src/main/java/org/octri/notification/config/NotificationAutoConfiguration.java

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@
44
import org.octri.notification.controller.NotificationController;
55
import org.octri.notification.converter.NotificationStatusMvcConverter;
66
import org.octri.notification.registry.NotificationStatusRegistry;
7+
import org.octri.notification.registry.NotificationTypeProvider;
78
import org.octri.notification.registry.NotificationTypeRegistry;
89
import org.octri.notification.repository.NotificationRepository;
910
import org.octri.notification.service.NotificationService;
11+
import org.slf4j.Logger;
12+
import org.slf4j.LoggerFactory;
1013
import org.springframework.boot.autoconfigure.AutoConfiguration;
1114
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
1215
import org.springframework.boot.autoconfigure.domain.EntityScan;
1316
import org.springframework.boot.context.properties.EnableConfigurationProperties;
1417
import org.springframework.context.annotation.Bean;
1518
import org.springframework.context.annotation.Import;
19+
import org.springframework.context.event.ContextRefreshedEvent;
20+
import org.springframework.context.event.EventListener;
1621
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
22+
import org.springframework.util.Assert;
1723

1824
/**
1925
* Configuration for the notification library.
@@ -26,8 +32,10 @@
2632
@Import({ NotificationBatchConfig.class, TwilioStatusBatchConfig.class, NotificationController.class })
2733
public class NotificationAutoConfiguration {
2834

35+
private static final Logger log = LoggerFactory.getLogger(NotificationAutoConfiguration.class);
36+
2937
/**
30-
*
38+
*
3139
* @return a bean for the notification type registry
3240
*/
3341
@Bean
@@ -36,7 +44,7 @@ public NotificationTypeRegistry notificationTypeRegistry() {
3644
}
3745

3846
/**
39-
*
47+
*
4048
* @return a bean for the notification status registry
4149
*/
4250
@Bean
@@ -45,7 +53,7 @@ public NotificationStatusRegistry notificationStatusRegistry() {
4553
}
4654

4755
/**
48-
*
56+
*
4957
* @param notificationStatusRegistry
5058
* the notification status registry
5159
* @return a bean for the notification status MVC converter
@@ -57,7 +65,7 @@ public NotificationStatusMvcConverter notificationStatusMvcConverter(
5765
}
5866

5967
/**
60-
*
68+
*
6169
* @param notificationRepository
6270
* the notification repository
6371
* @param notificationTypeRegistry
@@ -73,4 +81,30 @@ public NotificationService notificationService(NotificationRepository notificati
7381
return new NotificationService(notificationRepository, notificationTypeRegistry, notificationBatchJob);
7482
}
7583

84+
/**
85+
* Discovers notification types from {@link NotificationTypeProvider} beans and registers them with the
86+
* {@link NotificationTypeRegistry}.
87+
*
88+
* @param event
89+
*/
90+
@EventListener
91+
public void autoRegisterNotificationTypes(ContextRefreshedEvent event) {
92+
var applicationContext = event.getApplicationContext();
93+
var notificationTypeRegistry = applicationContext.getBean(NotificationTypeRegistry.class);
94+
Assert.notNull(notificationTypeRegistry, "NotificationTypeRegistry bean not found in application context");
95+
96+
log.info("Auto-registering notification types from NotificationTypeProvider beans");
97+
applicationContext.getBeansOfType(NotificationTypeProvider.class).values().forEach(provider -> {
98+
log.debug("Discovered provider {} for notification type {}", provider.getClass().getSimpleName(),
99+
provider.getNotificationType());
100+
notificationTypeRegistry.register(
101+
provider.getNotificationType(),
102+
provider.getProcessingMode(),
103+
provider.getNotificationMetadata(),
104+
provider.getNotificationValidator(),
105+
provider.getNotificationDispatcher(),
106+
provider.getNotificationViewer());
107+
});
108+
}
109+
76110
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package org.octri.notification.registry;
2+
3+
import org.octri.notification.dispatch.NotificationDispatcher;
4+
import org.octri.notification.domain.ProcessingMode;
5+
import org.octri.notification.metadata.EmptyMetadata;
6+
import org.octri.notification.metadata.NotificationMetadata;
7+
import org.octri.notification.validator.NotificationValidator;
8+
import org.octri.notification.view.EmptyMetadataViewer;
9+
import org.octri.notification.view.NotificationViewer;
10+
11+
/**
12+
* Interface for objects that provide the dependencies needed to process a type of notification. Notification types
13+
* provided by instances of this interface are discovered at runtime and automatically registered with the
14+
* {@link NotificationTypeRegistry}.
15+
*/
16+
public interface NotificationTypeProvider {
17+
18+
/**
19+
* Unique type name for this type of notification
20+
*
21+
* @return
22+
*/
23+
String getNotificationType();
24+
25+
/**
26+
* Processing mode for this type of notification. Defaults to SCHEDULED.
27+
*
28+
* @return
29+
*/
30+
default ProcessingMode getProcessingMode() {
31+
return ProcessingMode.SCHEDULED;
32+
}
33+
34+
/**
35+
* Class of NotificationMetadata used by this type of notification. Defaults to {@link EmptyMetadata}.
36+
*
37+
* @return
38+
*/
39+
default Class<? extends NotificationMetadata> getNotificationMetadata() {
40+
return EmptyMetadata.class;
41+
}
42+
43+
/**
44+
* Validator used to determine if a notification of this type should be delivered.
45+
*
46+
* @return
47+
*/
48+
default NotificationValidator getNotificationValidator() {
49+
return NotificationValidator.NOOP;
50+
};
51+
52+
/**
53+
* Dispatcher used to deliver notifications of this type.
54+
*
55+
* @return
56+
*/
57+
NotificationDispatcher getNotificationDispatcher();
58+
59+
/**
60+
* Viewer used to display metadata for this type of notification. Defaults to {@link EmptyMetadataViewer}.
61+
*
62+
* @return
63+
*/
64+
default NotificationViewer getNotificationViewer() {
65+
return new EmptyMetadataViewer();
66+
}
67+
68+
}

src/main/java/org/octri/notification/registry/NotificationTypeRegistry.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ public class NotificationTypeRegistry {
2323
private final Map<String, NotificationHandler> handlers = new ConcurrentHashMap<>();
2424

2525
/**
26-
*
26+
*
2727
* @param type
2828
* a unique string representation of the Notification type
2929
* @param mode
@@ -45,23 +45,23 @@ public void register(String type, ProcessingMode mode, Class<? extends Notificat
4545
mode.name(),
4646
validator.getClass().getSimpleName(),
4747
dispatcher.getClass().getSimpleName(),
48-
viewer.getClass().getName());
48+
viewer.getClass().getSimpleName());
4949
if (handlers.containsKey(type)) {
5050
logger.warn("Notification type {} is already registered. Overwriting.", type);
5151
}
5252
handlers.put(type, new NotificationHandler(mode, metadataClass, validator, dispatcher, viewer));
5353
}
5454

5555
/**
56-
*
56+
*
5757
* @return the set of types that are registered
5858
*/
5959
public Set<String> getRegisteredTypes() {
6060
return handlers.keySet();
6161
}
6262

6363
/**
64-
*
64+
*
6565
* @param type
6666
* the type string
6767
* @return the NotificationHandler for the given type

0 commit comments

Comments
 (0)