Skip to content

Commit 429ad63

Browse files
Feature/notification listeners (#160)
* notification center * update tests and notifications * test for all callback values * fix some typos and add logging along with try/catch for sendNotifications. * adding another listener unit test * finish unit tests * tweak notificaion type, add javadocs and comments * added more tests and fix typo * refactor notification listener * update from notification to notificationListener where appropriate
1 parent a50e297 commit 429ad63

File tree

8 files changed

+926
-2
lines changed

8 files changed

+926
-2
lines changed

core-api/src/main/java/com/optimizely/ab/Optimizely.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import com.optimizely.ab.event.internal.payload.Event.ClientEngine;
4242
import com.optimizely.ab.internal.EventTagUtils;
4343
import com.optimizely.ab.notification.NotificationBroadcaster;
44+
import com.optimizely.ab.notification.NotificationCenter;
4445
import com.optimizely.ab.notification.NotificationListener;
4546

4647
import org.slf4j.Logger;
@@ -93,6 +94,8 @@ public class Optimizely {
9394
@VisibleForTesting final EventHandler eventHandler;
9495
@VisibleForTesting final ErrorHandler errorHandler;
9596
@VisibleForTesting final NotificationBroadcaster notificationBroadcaster = new NotificationBroadcaster();
97+
public final NotificationCenter notificationCenter = new NotificationCenter();
98+
9699
@Nullable private final UserProfileService userProfileService;
97100

98101
private Optimizely(@Nonnull ProjectConfig projectConfig,
@@ -206,6 +209,9 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig,
206209
}
207210

208211
notificationBroadcaster.broadcastExperimentActivated(experiment, userId, filteredAttributes, variation);
212+
213+
notificationCenter.sendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId,
214+
filteredAttributes, variation, impressionEvent);
209215
} else {
210216
logger.info("Experiment has \"Launched\" status so not dispatching event during activation.");
211217
}
@@ -292,6 +298,8 @@ public void track(@Nonnull String eventName,
292298

293299
notificationBroadcaster.broadcastEventTracked(eventName, userId, filteredAttributes, eventValue,
294300
conversionEvent);
301+
notificationCenter.sendNotifications(NotificationCenter.NotificationType.Track, eventName, userId,
302+
filteredAttributes, eventTags, conversionEvent);
295303
}
296304

297305
//======== FeatureFlag APIs ========//
@@ -688,6 +696,7 @@ public UserProfileService getUserProfileService() {
688696
*
689697
* @param listener listener to add
690698
*/
699+
@Deprecated
691700
public void addNotificationListener(@Nonnull NotificationListener listener) {
692701
notificationBroadcaster.addListener(listener);
693702
}
@@ -697,13 +706,15 @@ public void addNotificationListener(@Nonnull NotificationListener listener) {
697706
*
698707
* @param listener listener to remove
699708
*/
709+
@Deprecated
700710
public void removeNotificationListener(@Nonnull NotificationListener listener) {
701711
notificationBroadcaster.removeListener(listener);
702712
}
703713

704714
/**
705715
* Remove all {@link NotificationListener}.
706716
*/
717+
@Deprecated
707718
public void clearNotificationListeners() {
708719
notificationBroadcaster.clearListeners();
709720
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
*
3+
* Copyright 2017, Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package com.optimizely.ab.notification;
19+
20+
import com.optimizely.ab.config.Experiment;
21+
import com.optimizely.ab.config.Variation;
22+
import com.optimizely.ab.event.LogEvent;
23+
24+
import java.util.Map;
25+
26+
27+
public abstract class ActivateNotification extends NotificationListener {
28+
29+
/**
30+
* Base notify called with var args. This method parses the parameters and calls the abstract method.
31+
* @param args - variable argument list based on the type of notification.
32+
*/
33+
@Override
34+
public final void notify(Object... args) {
35+
assert(args[0] instanceof Experiment);
36+
Experiment experiment = (Experiment) args[0];
37+
assert(args[1] instanceof String);
38+
String userId = (String) args[1];
39+
assert(args[2] instanceof java.util.Map);
40+
Map<String, String> attributes = (Map<String, String>) args[2];
41+
assert(args[3] instanceof Variation);
42+
Variation variation = (Variation) args[3];
43+
assert(args[4] instanceof LogEvent);
44+
LogEvent logEvent = (LogEvent) args[4];
45+
46+
onActivate(experiment, userId, attributes, variation, logEvent);
47+
}
48+
49+
/**
50+
* onActivate called when an activate was triggered
51+
* @param experiment - The experiment object being activated.
52+
* @param userId - The userId passed into activate.
53+
* @param attributes - The filtered attribute list passed into activate
54+
* @param variation - The variation that was returned from activate.
55+
* @param event - The impression event that was triggered.
56+
*/
57+
public abstract void onActivate(@javax.annotation.Nonnull Experiment experiment,
58+
@javax.annotation.Nonnull String userId,
59+
@javax.annotation.Nonnull Map<String, String> attributes,
60+
@javax.annotation.Nonnull Variation variation,
61+
@javax.annotation.Nonnull LogEvent event) ;
62+
63+
}
64+

core-api/src/main/java/com/optimizely/ab/notification/NotificationBroadcaster.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
/**
3434
* Manages Optimizely SDK notification listeners and broadcasts messages.
3535
*/
36+
@Deprecated
3637
public class NotificationBroadcaster {
3738

3839
private static final Logger logger = LoggerFactory.getLogger(NotificationBroadcaster.class);
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
/**
2+
*
3+
* Copyright 2017, Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.notification;
18+
19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
22+
import java.util.ArrayList;
23+
import java.util.HashMap;
24+
import java.util.Map;
25+
26+
27+
/**
28+
* This class handles impression and conversion notificationsListeners. It replaces NotificationBroadcaster and is intended to be
29+
* more flexible.
30+
*/
31+
public class NotificationCenter {
32+
/**
33+
* NotificationType is used for the notification types supported.
34+
*/
35+
public enum NotificationType {
36+
37+
Activate(ActivateNotification.class), // Activate was called. Track an impression event
38+
Track(TrackNotification.class); // Track was called. Track a conversion event
39+
40+
private Class notificationTypeClass;
41+
42+
NotificationType(Class notificationClass) {
43+
this.notificationTypeClass = notificationClass;
44+
}
45+
46+
public Class getNotificationTypeClass() {
47+
return notificationTypeClass;
48+
}
49+
};
50+
51+
52+
// the notification id is incremented and is assigned as the callback id, it can then be used to remove the notification.
53+
private int notificationListenerID = 1;
54+
55+
final private static Logger logger = LoggerFactory.getLogger(NotificationCenter.class);
56+
57+
// notification holder holds the id as well as the notification.
58+
private static class NotificationHolder
59+
{
60+
int notificationId;
61+
NotificationListener notificationListener;
62+
63+
NotificationHolder(int id, NotificationListener notificationListener) {
64+
notificationId = id;
65+
this.notificationListener = notificationListener;
66+
}
67+
}
68+
69+
/**
70+
* Instantiate a new NotificationCenter
71+
*/
72+
public NotificationCenter() {
73+
notificationsListeners.put(NotificationType.Activate, new ArrayList<NotificationHolder>());
74+
notificationsListeners.put(NotificationType.Track, new ArrayList<NotificationHolder>());
75+
}
76+
77+
// private list of notification by notification type.
78+
// we used a list so that notification order can mean something.
79+
private Map<NotificationType, ArrayList<NotificationHolder>> notificationsListeners =new HashMap<NotificationType, ArrayList<NotificationHolder>>();
80+
81+
82+
/**
83+
* Add a notification listener to the notification center.
84+
*
85+
* @param notificationType - enum NotificationType to add.
86+
* @param notificationListener - Notification to add.
87+
* @return the notification id used to remove the notification. It is greater than 0 on success.
88+
*/
89+
public int addNotification(NotificationType notificationType, NotificationListener notificationListener) {
90+
91+
Class clazz = notificationType.notificationTypeClass;
92+
if (clazz == null || !clazz.isInstance(notificationListener)) {
93+
logger.warn("Notification listener was the wrong type. It was not added to the notification center.");
94+
return -1;
95+
}
96+
97+
for (NotificationHolder holder : notificationsListeners.get(notificationType)) {
98+
if (holder.notificationListener == notificationListener) {
99+
logger.warn("Notificication listener was already added");
100+
return -1;
101+
}
102+
}
103+
int id = notificationListenerID++;
104+
notificationsListeners.get(notificationType).add(new NotificationHolder(id, notificationListener ));
105+
logger.info("Notification listener {} was added with id {}", notificationListener.toString(), id);
106+
return id;
107+
}
108+
109+
/**
110+
* Remove the notification listener based on the notificationId passed back from addNotification.
111+
* @param notificationID the id passed back from add notification.
112+
* @return true if removed otherwise false (if the notification is already registered, it returns false).
113+
*/
114+
public boolean removeNotification(int notificationID) {
115+
for (NotificationType type : NotificationType.values()) {
116+
for (NotificationHolder holder : notificationsListeners.get(type)) {
117+
if (holder.notificationId == notificationID) {
118+
notificationsListeners.get(type).remove(holder);
119+
logger.info("Notification listener removed {}", notificationID);
120+
return true;
121+
}
122+
}
123+
}
124+
125+
logger.warn("Notification listener with id {} not found", notificationID);
126+
127+
return false;
128+
}
129+
130+
/**
131+
* Clear out all the notification listeners.
132+
*/
133+
public void clearAllNotifications() {
134+
for (NotificationType type : NotificationType.values()) {
135+
clearNotifications(type);
136+
}
137+
}
138+
139+
/**
140+
* Clear notification listeners by notification type.
141+
* @param notificationType type of notificationsListeners to remove.
142+
*/
143+
public void clearNotifications(NotificationType notificationType) {
144+
notificationsListeners.get(notificationType).clear();
145+
}
146+
147+
// fire a notificaiton of a certain type. The arg list changes depending on the type of notification sent.
148+
public void sendNotifications(NotificationType notificationType, Object ...args) {
149+
ArrayList<NotificationHolder> holders = notificationsListeners.get(notificationType);
150+
for (NotificationHolder holder : holders) {
151+
try {
152+
holder.notificationListener.notify(args);
153+
}
154+
catch (Exception e) {
155+
logger.error("Unexpected exception calling notification listener {}", holder.notificationId, e);
156+
}
157+
}
158+
}
159+
160+
}

core-api/src/main/java/com/optimizely/ab/notification/NotificationListener.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public abstract class NotificationListener {
4545
* @param eventValue an integer to be aggregated for the event
4646
* @param logEvent the log event sent to the event dispatcher
4747
*/
48+
@Deprecated
4849
public void onEventTracked(@Nonnull String eventKey,
4950
@Nonnull String userId,
5051
@Nonnull Map<String, String> attributes,
@@ -60,9 +61,18 @@ public void onEventTracked(@Nonnull String eventKey,
6061
* @param attributes a map of attributes about the user
6162
* @param variation the key of the variation that was bucketed
6263
*/
64+
@Deprecated
6365
public void onExperimentActivated(@Nonnull Experiment experiment,
6466
@Nonnull String userId,
6567
@Nonnull Map<String, String> attributes,
6668
@Nonnull Variation variation) {
6769
}
70+
71+
/**
72+
* This is the new method of notification. Implementation classes such as {@link com.optimizely.ab.notification.ActivateNotification}
73+
* will implement this call and provide another method with the correct parameters
74+
* Notify called when a notification is triggered via the {@link com.optimizely.ab.notification.NotificationCenter}
75+
* @param args - variable argument list based on the type of notification.
76+
*/
77+
public abstract void notify(Object... args);
6878
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
*
3+
* Copyright 2017, Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.notification;
18+
19+
import java.util.Map;
20+
import javax.annotation.Nonnull;
21+
22+
import com.optimizely.ab.event.LogEvent;
23+
24+
/**
25+
* This class handles the track event notification.
26+
*/
27+
public abstract class TrackNotification extends NotificationListener {
28+
29+
/**
30+
* Base notify called with var args. This method parses the parameters and calls the abstract method.
31+
* @param args - variable argument list based on the type of notification.
32+
*/
33+
@Override
34+
public final void notify(Object... args) {
35+
assert(args[0] instanceof String);
36+
String eventKey = (String) args[0];
37+
assert(args[1] instanceof String);
38+
String userId = (String) args[1];
39+
assert(args[2] instanceof java.util.Map);
40+
Map<String, String> attributes = (Map<String, String>) args[2];
41+
assert(args[3] instanceof java.util.Map);
42+
Map<String, ?> eventTags = (Map<String, ?>) args[3];
43+
assert(args[4] instanceof LogEvent);
44+
LogEvent logEvent = (LogEvent) args[4];
45+
46+
onTrack(eventKey, userId,attributes,eventTags, logEvent);
47+
}
48+
49+
/**
50+
* onTrack is called when a track event is triggered
51+
* @param eventKey - The event key that was triggered.
52+
* @param userId - user id passed into track.
53+
* @param attributes - filtered attributes list after passed into track
54+
* @param eventTags - event tags if any were passed in.
55+
* @param event - The event being recorded.
56+
*/
57+
public abstract void onTrack(@Nonnull String eventKey,
58+
@Nonnull String userId,
59+
@Nonnull Map<String, String> attributes,
60+
@Nonnull Map<String, ?> eventTags,
61+
@Nonnull LogEvent event) ;
62+
}

0 commit comments

Comments
 (0)