Skip to content

Commit 00dc258

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 e57e149 commit 00dc258

File tree

8 files changed

+901
-2
lines changed

8 files changed

+901
-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
@@ -40,6 +40,7 @@
4040
import com.optimizely.ab.internal.EventTagUtils;
4141
import com.optimizely.ab.internal.ReservedEventKey;
4242
import com.optimizely.ab.notification.NotificationBroadcaster;
43+
import com.optimizely.ab.notification.NotificationCenter;
4344
import com.optimizely.ab.notification.NotificationListener;
4445
import org.slf4j.Logger;
4546
import org.slf4j.LoggerFactory;
@@ -91,6 +92,8 @@ public class Optimizely {
9192
@VisibleForTesting final EventHandler eventHandler;
9293
@VisibleForTesting final ErrorHandler errorHandler;
9394
@VisibleForTesting final NotificationBroadcaster notificationBroadcaster = new NotificationBroadcaster();
95+
public final NotificationCenter notificationCenter = new NotificationCenter();
96+
9497
@Nullable private final UserProfileService userProfileService;
9598

9699
private Optimizely(@Nonnull ProjectConfig projectConfig,
@@ -194,6 +197,9 @@ Variation activate(@Nonnull ProjectConfig projectConfig,
194197
}
195198

196199
notificationBroadcaster.broadcastExperimentActivated(experiment, userId, filteredAttributes, variation);
200+
201+
notificationCenter.sendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId,
202+
filteredAttributes, variation, impressionEvent);
197203
} else {
198204
logger.info("Experiment has \"Launched\" status so not dispatching event during activation.");
199205
}
@@ -302,6 +308,8 @@ public void track(@Nonnull String eventName,
302308

303309
notificationBroadcaster.broadcastEventTracked(eventName, userId, filteredAttributes, eventValue,
304310
conversionEvent);
311+
notificationCenter.sendNotifications(NotificationCenter.NotificationType.Track, eventName, userId,
312+
filteredAttributes, eventTags, conversionEvent);
305313
}
306314

307315
//======== live variable getters ========//
@@ -552,6 +560,7 @@ public UserProfileService getUserProfileService() {
552560
*
553561
* @param listener listener to add
554562
*/
563+
@Deprecated
555564
public void addNotificationListener(@Nonnull NotificationListener listener) {
556565
notificationBroadcaster.addListener(listener);
557566
}
@@ -561,13 +570,15 @@ public void addNotificationListener(@Nonnull NotificationListener listener) {
561570
*
562571
* @param listener listener to remove
563572
*/
573+
@Deprecated
564574
public void removeNotificationListener(@Nonnull NotificationListener listener) {
565575
notificationBroadcaster.removeListener(listener);
566576
}
567577

568578
/**
569579
* Remove all {@link NotificationListener}.
570580
*/
581+
@Deprecated
571582
public void clearNotificationListeners() {
572583
notificationBroadcaster.clearListeners();
573584
}
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)