Skip to content

Commit d476308

Browse files
Merge pull request #200 from wttech/executor-notifier-id-configurable
Executor - configurable notifier ID
2 parents 87aa827 + e848a5e commit d476308

File tree

14 files changed

+194
-117
lines changed

14 files changed

+194
-117
lines changed

README.md

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,36 @@ It works seamlessly across AEM on-premise, AMS, and AEMaaCS environments.
2929

3030
## Table of Contents
3131

32-
- [Key Features](#key-features)
32+
- [AEM Content Manager (ACM)](#aem-content-manager-acm)
33+
- [Table of Contents](#table-of-contents)
34+
- [Key Features](#key-features)
3335
- [All-in-one Solution](#all-in-one-solution)
3436
- [New Approach](#new-approach)
3537
- [Content Management](#content-management)
3638
- [Permissions Management](#permissions-management)
37-
- [Data Imports & Exports](#data-imports--exports)
38-
- [Installation](#installation)
39-
- [Compatibility](#compatibility)
40-
- [Documentation](#documentation)
39+
- [Data Imports \& Exports](#data-imports--exports)
40+
- [Installation](#installation)
41+
- [Compatibility](#compatibility)
42+
- [Documentation](#documentation)
4143
- [Usage](#usage)
4244
- [Console](#console)
43-
- [Content Scripts](#content-scripts)
44-
- [Minimal Example](#minimal-example)
45-
- [Arguments Example](#arguments-example)
46-
- [ACL Example](#acl-example)
47-
- [Repo Example](#repo-example)
48-
- [History](#history)
49-
- [Extension Scripts](#extension-scripts)
45+
- [Content scripts](#content-scripts)
46+
- [Minimal example](#minimal-example)
47+
- [Arguments example](#arguments-example)
48+
- [ACL example](#acl-example)
49+
- [Repo example](#repo-example)
50+
- [History](#history)
51+
- [Extension scripts](#extension-scripts)
52+
- [Example extension script](#example-extension-script)
5053
- [Snippets](#snippets)
54+
- [Example snippet](#example-snippet)
5155
- [Mocks](#mocks)
52-
- [Development](#development)
53-
- [Authors](#authors)
54-
- [Contributing](#contributing)
55-
- [License](#license)
56+
- [Notifications](#notifications)
57+
- [Development](#development)
58+
- [Releasing](#releasing)
59+
- [Authors](#authors)
60+
- [Contributing](#contributing)
61+
- [License](#license)
5662

5763
## Key Features
5864

@@ -403,6 +409,29 @@ This feature is disabled by default, but you can enable it in the [OSGi configur
403409
<img src="docs/screenshot-scripts-mock-tab.png" width="720" alt="ACM Mocks - List">
404410
<img src="docs/screenshot-scripts-mock-code.png" width="720" alt="ACM Mocks - Code">
405411
412+
### Notifications
413+
414+
ACM offers a flexible notification service supporting multiple channels, including Slack and Microsoft Teams, with no additional coding required.
415+
416+
To receive notifications about automatic code executions, simply configure a notifier with a unique ID (`acm`) in the OSGi configuration.
417+
418+
For Slack integration, create a file at *ui.config/src/main/content/jcr_root/apps/{project}/osgiconfig/config/dev.vml.es.acm.core.notification.slack.SlackFactory.config* with the following content:
419+
420+
```ini
421+
enabled=B"true"
422+
id="acm"
423+
webhookUrl="https://hooks.slack.com/services/XXXXXXXXX/YYYYYYYYYYY/ZZZZZZZZZZZZZZZZZZZZZZZZ"
424+
timeoutMillis=I"5000"
425+
```
426+
To customize notifications triggered by the [executor service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/code/Executor.java#L32), use its OSGi configuration. This allows you to control which script executions should trigger notifications and which should be excluded, providing fine-grained management over notification behavior.
427+
428+
The notification service is a general-purpose feature that can be used for any kind of messaging, not just notifications related to ACM code execution. You can also define multiple notifiers with different IDs to target various channels or teams. In your Groovy scripts or project-specific OSGi bundles, use the `notifier` [service](https://github.com/wttech/acm/blob/main/core/src/main/java/dev/vml/es/acm/core/notification/NotificationManager.java) to send messages to a specific notifier or the default one:
429+
430+
```groovy
431+
notifier.sendMessageTo("acme", "ACME Project Notifications", "An important event occurred.")
432+
notifier.sendMessage("ACME Project Notifications", "Let's start the day with a coffee!") // uses the 'default' notifier
433+
```
434+
406435
## Development
407436

408437
1. All-in-one command (incremental building and deployment of 'all' distribution, both backend & frontend)

core/src/main/java/dev/vml/es/acm/core/AcmConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ public class AcmConstants {
66

77
public static final String CODE = "acm";
88

9+
public static final String NOTIFIER_ID = "acm";
10+
911
public static final String SETTINGS_ROOT = "/conf/acm/settings";
1012

1113
public static final String VAR_ROOT = "/var/acm";

core/src/main/java/dev/vml/es/acm/core/code/Executor.java

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
import dev.vml.es.acm.core.notification.NotificationManager;
99
import dev.vml.es.acm.core.osgi.InstanceInfo;
1010
import dev.vml.es.acm.core.osgi.OsgiContext;
11+
import dev.vml.es.acm.core.util.DateUtils;
1112
import dev.vml.es.acm.core.util.ResolverUtils;
1213
import dev.vml.es.acm.core.util.StringUtil;
13-
import java.text.SimpleDateFormat;
1414
import java.util.Arrays;
1515
import java.util.Date;
1616
import java.util.LinkedHashMap;
@@ -66,6 +66,9 @@ public class Executor {
6666
description = "Enables notifications for completed executions.")
6767
boolean notificationEnabled() default true;
6868

69+
@AttributeDefinition(name = "Notification Notifier ID")
70+
String notificationNotifierId() default AcmConstants.NOTIFIER_ID;
71+
6972
@AttributeDefinition(
7073
name = "Notification Executable IDs",
7174
description = "Allow to control with regular expressions which executables should be notified about.")
@@ -214,7 +217,7 @@ private void handleHistory(ExecutionContext context, ImmediateExecution executio
214217
private void handleNotifications(ExecutionContext context, ImmediateExecution execution) {
215218
String executableId = execution.getExecutable().getId();
216219
if (!config.notificationEnabled()
217-
|| !notifier.isConfigured()
220+
|| !notifier.isConfigured(config.notificationNotifierId())
218221
|| Arrays.stream(config.notificationExecutableIds())
219222
.noneMatch(regex -> Pattern.matches(regex, executableId))) {
220223
return;
@@ -236,8 +239,8 @@ private void handleNotifications(ExecutionContext context, ImmediateExecution ex
236239

237240
Map<String, Object> fields = new LinkedHashMap<>();
238241
fields.put("Status", execution.getStatus().name().toLowerCase());
239-
fields.put("Time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
240-
fields.put("Duration", execution.getDuration() + "ms");
242+
fields.put("Time", DateUtils.humanFormat().format(new Date()));
243+
fields.put("Duration", StringUtil.formatDuration(execution.getDuration()));
241244

242245
InstanceInfo instanceInfo = context.getCodeContext().getOsgiContext().getInstanceInfo();
243246
InstanceSettings instanceSettings = new InstanceSettings(instanceInfo);
@@ -249,12 +252,12 @@ private void handleNotifications(ExecutionContext context, ImmediateExecution ex
249252
fields.put("Instance", instanceDesc);
250253

251254
int detailsMaxLength = config.notificationDetailsLength();
252-
String output = StringUtils.defaultIfBlank(execution.getOutput(), "(empty)");
253-
String error = StringUtils.defaultIfBlank(execution.getError(), "(empty)");
254-
fields.put("Output", detailsMaxLength < 0 ? output : StringUtil.abbreviateStart(output, detailsMaxLength));
255+
String output = StringUtil.markdownCode(execution.getOutput(), "(none)");
256+
String error = StringUtil.markdownCode(execution.getError(), "(none)");
257+
fields.put("Output", detailsMaxLength < 0 ? output : StringUtil.abbreviateStart(output, detailsMaxLength, "[...] "));
255258
fields.put("Error", detailsMaxLength < 0 ? error : StringUtils.abbreviate(error, detailsMaxLength));
256259

257-
notifier.sendMessage(title, text, fields);
260+
notifier.sendMessageTo(config.notificationNotifierId(), title, text, fields);
258261
}
259262

260263
public Description describe(ExecutionContext context) {

core/src/main/java/dev/vml/es/acm/core/notification/NotificationManager.java

Lines changed: 32 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -33,56 +33,55 @@ public class NotificationManager {
3333
service = SlackFactory.class)
3434
private final Collection<SlackFactory> slackFactories = new CopyOnWriteArrayList<>();
3535

36-
// === Multi-notifier ===
37-
3836
public boolean isConfigured() {
39-
return hasAnyDefaultNotifier();
37+
return isConfigured(NotifierFactory.ID_DEFAULT);
38+
}
39+
40+
public boolean isConfigured(String notifierId) {
41+
return isSlackConfigured(notifierId) || isTeamsConfigured(notifierId);
4042
}
4143

4244
public void sendMessage(String text) {
43-
sendMessage(null, text, Collections.emptyMap());
45+
sendMessageTo(NotifierFactory.ID_DEFAULT, text);
4446
}
4547

4648
public void sendMessage(String title, String text) {
47-
sendMessage(title, text, Collections.emptyMap());
49+
sendMessageTo(NotifierFactory.ID_DEFAULT, title, text);
4850
}
4951

5052
public void sendMessage(String title, String text, Map<String, Object> fields) {
51-
if (!hasAnyDefaultNotifier()) {
52-
throw new NotificationException(
53-
String.format("Notifier '%s' (Slack or Teams) not configured!", NotifierFactory.ID_DEFAULT));
54-
}
55-
findSlackDefault()
56-
.ifPresent(slack ->
57-
slack.sendPayload(buildSlackMessage(title, text, fields).build()));
58-
findTeamsDefault()
59-
.ifPresent(teams ->
60-
teams.sendPayload(buildTeamsMessage(title, text, fields).build()));
53+
sendMessageTo(NotifierFactory.ID_DEFAULT, title, text, fields);
6154
}
6255

63-
private boolean hasAnyDefaultNotifier() {
64-
return findSlackDefault().isPresent() || findTeamsDefault().isPresent();
56+
public void sendMessageTo(String notifierId, String text) {
57+
sendMessageTo(notifierId, null, text, Collections.emptyMap());
6558
}
6659

67-
// === Teams ===
68-
69-
public boolean isTeamsConfigured() {
70-
return findTeamsDefault().isPresent();
60+
public void sendMessageTo(String notifierId, String title, String text) {
61+
sendMessageTo(notifierId, title, text, Collections.emptyMap());
7162
}
7263

73-
public void sendTeamsMessage(String text) {
74-
sendTeamsMessage(null, text, Collections.emptyMap());
64+
public void sendMessageTo(String notifierId, String title, String text, Map<String, Object> fields) {
65+
Optional<Slack> slackOpt = findSlackById(notifierId);
66+
Optional<Teams> teamsOpt = findTeamsById(notifierId);
67+
if (!slackOpt.isPresent() && !teamsOpt.isPresent()) {
68+
throw new NotificationException(
69+
String.format("Notifier '%s' not configured for Slack or Teams!", notifierId));
70+
}
71+
slackOpt.ifPresent(slack -> slack.sendPayload(
72+
buildSlackPayload().message(title, text, fields).build()));
73+
teamsOpt.ifPresent(teams -> teams.sendPayload(
74+
buildTeamsPayload().message(title, text, fields).build()));
7575
}
7676

77-
public void sendTeamsMessage(String title, String text) {
78-
sendTeamsMessage(title, text, Collections.emptyMap());
77+
// === Teams ===
78+
79+
public boolean isTeamsConfigured() {
80+
return isTeamsConfigured(NotifierFactory.ID_DEFAULT);
7981
}
8082

81-
public void sendTeamsMessage(String title, String text, Map<String, Object> fields) {
82-
Teams teamsDefault = findTeamsDefault()
83-
.orElseThrow(() -> new NotificationException(
84-
String.format("Teams notifier '%s' not configured!", NotifierFactory.ID_DEFAULT)));
85-
teamsDefault.sendPayload(buildTeamsMessage(title, text, fields).build());
83+
public boolean isTeamsConfigured(String notifierId) {
84+
return findTeamsById(notifierId).isPresent();
8685
}
8786

8887
public Stream<Teams> findTeams() {
@@ -110,43 +109,18 @@ public Teams getTeamsDefault() {
110109
String.format("Teams notifier '%s' not configured!", NotifierFactory.ID_DEFAULT)));
111110
}
112111

113-
public TeamsPayload.Builder buildTeamsMessage(String title, String text, Map<String, Object> fields) {
114-
TeamsPayload.Builder payload = buildTeamsPayload();
115-
if (StringUtils.isNotBlank(title)) {
116-
payload.title(title);
117-
}
118-
if (StringUtils.isNotBlank(text)) {
119-
payload.text(text);
120-
}
121-
if (fields != null && !fields.isEmpty()) {
122-
payload.facts(fields);
123-
}
124-
return payload;
125-
}
126-
127112
public TeamsPayload.Builder buildTeamsPayload() {
128113
return new TeamsPayload.Builder();
129114
}
130115

131116
// ===[ Slack ]===
132117

133118
public boolean isSlackConfigured() {
134-
return findSlackDefault().isPresent();
135-
}
136-
137-
public void sendSlackMessage(String text) {
138-
sendSlackMessage(null, text, Collections.emptyMap());
139-
}
140-
141-
public void sendSlackMessage(String title, String text) {
142-
sendSlackMessage(title, text, Collections.emptyMap());
119+
return isSlackConfigured(NotifierFactory.ID_DEFAULT);
143120
}
144121

145-
public void sendSlackMessage(String title, String text, Map<String, Object> fields) {
146-
Slack slackDefault = findSlackDefault()
147-
.orElseThrow(() -> new NotificationException(
148-
String.format("Slack notifier '%s' not configured!", NotifierFactory.ID_DEFAULT)));
149-
slackDefault.sendPayload(buildSlackMessage(title, text, fields).build());
122+
public boolean isSlackConfigured(String notifierId) {
123+
return findSlackById(notifierId).isPresent();
150124
}
151125

152126
public Stream<Slack> findSlack() {
@@ -174,23 +148,6 @@ public Slack getSlackDefault() {
174148
String.format("Slack notifier '%s' not configured!", NotifierFactory.ID_DEFAULT)));
175149
}
176150

177-
public SlackPayload.Builder buildSlackMessage(String title, String text, Map<String, Object> fields) {
178-
SlackPayload.Builder payload = buildSlackPayload();
179-
if (StringUtils.isNotBlank(title)) {
180-
payload.header(title);
181-
}
182-
if (StringUtils.isNotBlank(title) && StringUtils.isNotBlank(text)) {
183-
payload.divider();
184-
}
185-
if (StringUtils.isNotBlank(text)) {
186-
payload.sectionMarkdown(text);
187-
}
188-
if (fields != null && !fields.isEmpty()) {
189-
payload.fieldsMarkdown(fields);
190-
}
191-
return payload;
192-
}
193-
194151
public SlackPayload.Builder buildSlackPayload() {
195152
return new SlackPayload.Builder();
196153
}

core/src/main/java/dev/vml/es/acm/core/notification/Notifier.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import java.io.Closeable;
44
import java.io.Serializable;
5+
import java.util.Map;
56

67
public interface Notifier<P extends Serializable> extends Closeable {
78

@@ -26,4 +27,9 @@ public interface Notifier<P extends Serializable> extends Closeable {
2627
* Send a payload to notification service in structured format.
2728
*/
2829
void sendPayload(P payload);
30+
31+
/**
32+
* Send a message to notification service in structured format.
33+
*/
34+
void sendMessage(String title, String text, Map<String, Object> fields);
2935
}

core/src/main/java/dev/vml/es/acm/core/notification/NotifierFactory.java

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import java.util.Optional;
77
import java.util.function.Supplier;
88
import org.apache.commons.lang3.StringUtils;
9-
import org.osgi.framework.Constants;
109
import org.slf4j.Logger;
1110
import org.slf4j.LoggerFactory;
1211

@@ -16,18 +15,16 @@ public abstract class NotifierFactory<N extends Notifier<? extends Serializable>
1615

1716
private static final Logger LOG = LoggerFactory.getLogger(NotifierFactory.class);
1817

19-
private static final String PID_DEFAULT = "default";
20-
21-
private String configPid;
18+
private String configId;
2219

2320
private N notifier;
2421

2522
protected void create(Map<String, Object> props, Supplier<N> supplier) {
26-
this.configPid = getConfigPid(props);
23+
this.configId = getConfigId(props);
2724
try {
2825
this.notifier = supplier.get();
2926
} catch (Exception e) {
30-
LOG.error("Cannot create notifier for PID '{}'!", configPid, e);
27+
LOG.error("Cannot create notifier for ID '{}'!", configId, e);
3128
}
3229
}
3330

@@ -36,16 +33,15 @@ protected void destroy(Map<String, Object> props) {
3633
try {
3734
this.notifier.close();
3835
} catch (IOException e) {
39-
LOG.error("Cannot clean up notifier for PID '{}'!", configPid, e);
36+
LOG.error("Cannot clean up notifier for ID '{}'!", configId, e);
4037
}
4138
this.notifier = null;
42-
this.configPid = null;
39+
this.configId = null;
4340
}
4441
}
4542

46-
private String getConfigPid(Map<String, Object> props) {
47-
String pid = (String) props.getOrDefault(Constants.SERVICE_PID, PID_DEFAULT);
48-
return StringUtils.substringAfter(pid, "~");
43+
private String getConfigId(Map<String, Object> props) {
44+
return StringUtils.defaultIfBlank((String) props.get("id"), ID_DEFAULT);
4945
}
5046

5147
public N getNotifier() {

core/src/main/java/dev/vml/es/acm/core/notification/slack/Slack.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import dev.vml.es.acm.core.notification.Notifier;
44
import dev.vml.es.acm.core.util.JsonUtils;
55
import java.io.IOException;
6+
import java.util.Map;
7+
68
import org.apache.commons.lang3.ArrayUtils;
79
import org.apache.http.HttpHeaders;
810
import org.apache.http.client.config.RequestConfig;
@@ -55,6 +57,12 @@ public boolean isEnabled() {
5557
return enabled;
5658
}
5759

60+
@Override
61+
public void sendMessage(String title, String text, Map<String, Object> fields) {
62+
SlackPayload payload = SlackPayload.builder().message(title, text, fields).build();
63+
sendPayload(payload);
64+
}
65+
5866
@Override
5967
public void sendPayload(SlackPayload payload) {
6068
try {

0 commit comments

Comments
 (0)