diff --git a/core-api/src/main/java/com/optimizely/ab/Optimizely.java b/core-api/src/main/java/com/optimizely/ab/Optimizely.java index b9a294670..02d2186d1 100644 --- a/core-api/src/main/java/com/optimizely/ab/Optimizely.java +++ b/core-api/src/main/java/com/optimizely/ab/Optimizely.java @@ -254,8 +254,12 @@ private void sendImpression(@Nonnull ProjectConfig projectConfig, logger.error("Unexpected exception in event dispatcher", e); } - notificationCenter.sendNotifications(NotificationCenter.NotificationType.Activate, experiment, userId, - filteredAttributes, variation, impressionEvent); + // Kept For backwards compatibility. + // This notification is deprecated and the new DecisionNotifications + // are sent via their respective method calls. + ActivateNotification activateNotification = new ActivateNotification( + experiment, userId, filteredAttributes, variation, impressionEvent); + notificationCenter.send(activateNotification); } else { logger.info("Experiment has \"Launched\" status so not dispatching event during activation."); } @@ -330,8 +334,10 @@ public void track(@Nonnull String eventName, logger.error("Unexpected exception in event dispatcher", e); } - notificationCenter.sendNotifications(NotificationCenter.NotificationType.Track, eventName, userId, + TrackNotification notification = new TrackNotification(eventName, userId, copiedAttributes, eventTags, conversionEvent); + + notificationCenter.send(notification); } //======== FeatureFlag APIs ========// @@ -419,7 +425,8 @@ public Boolean isFeatureEnabled(@Nonnull String featureKey, .withSource(decisionSource) .withSourceInfo(sourceInfo) .build(); - notificationCenter.sendNotifications(decisionNotification); + + notificationCenter.send(decisionNotification); logger.info("Feature \"{}\" is not enabled for user \"{}\".", featureKey, userId); return featureEnabled; @@ -695,7 +702,7 @@ T getFeatureVariableValueForType(@Nonnull String featureKey, .build(); - notificationCenter.sendNotifications(decisionNotification); + notificationCenter.send(decisionNotification); return (T) convertedValue; } @@ -790,7 +797,7 @@ public Variation getVariation(@Nonnull Experiment experiment, .withType(notificationType) .build(); - notificationCenter.sendNotifications(decisionNotification); + notificationCenter.send(decisionNotification); return variation; } @@ -919,6 +926,31 @@ private boolean validateUserId(String userId) { return copiedAttributes; } + //======== Notification APIs ========// + + public NotificationCenter getNotificationCenter() { + return notificationCenter; + } + + /** + * Convenience method for adding DecisionNotification Handlers + */ + public int addDecisionNotificationHandler(NotificationHandler handler) { + NotificationManager manager = notificationCenter + .getNotificationManager(DecisionNotification.class); + return manager.addHandler(handler); + } + + /** + * Convenience method for adding TrackNotification Handlers + */ + public int addTrackNotificationHandler(NotificationHandler handler) { + NotificationManager notificationManager = + notificationCenter.getNotificationManager(TrackNotification.class); + return notificationManager.addHandler(handler); + } + + //======== Builder ========// public static Builder builder(@Nonnull String datafile, diff --git a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java new file mode 100644 index 000000000..b5b6948b4 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotification.java @@ -0,0 +1,82 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +import com.optimizely.ab.annotations.VisibleForTesting; +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.Variation; +import com.optimizely.ab.event.LogEvent; + +import java.util.Map; + +/** + * ActivateNotification supplies notification for AB activatation. + * + * @deprecated in favor of {@link DecisionNotification} which provides notifications for Experiment, Feature + * and Rollout decisions. + */ +@Deprecated +public final class ActivateNotification { + + private final Experiment experiment; + private final String userId; + private final Map attributes; + private final Variation variation; + private final LogEvent event; + + @VisibleForTesting + ActivateNotification() { + this(null, null, null, null, null); + } + + /** + * @param experiment - The experiment object being activated. + * @param userId - The userId passed into activate. + * @param attributes - The filtered attribute list passed into activate + * @param variation - The variation that was returned from activate. + * @param event - The impression event that was triggered. + */ + public ActivateNotification(Experiment experiment, String userId, Map attributes, Variation variation, LogEvent event) { + this.experiment = experiment; + this.userId = userId; + this.attributes = attributes; + this.variation = variation; + this.event = event; + } + + public Experiment getExperiment() { + return experiment; + } + + public String getUserId() { + return userId; + } + + public Map getAttributes() { + return attributes; + } + + public Variation getVariation() { + return variation; + } + + public LogEvent getEvent() { + return event; + } + + +} diff --git a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListener.java b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListener.java index e152113dc..4ca602c77 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListener.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListener.java @@ -24,15 +24,24 @@ import javax.annotation.Nonnull; import java.util.Map; +/** + * ActivateNotificationListener handles the activate event notification. + * + * @deprecated along with {@link ActivateNotification} and users should implement + * NotificationHandler<DecisionNotification> directly. + */ @Deprecated -public abstract class ActivateNotificationListener implements NotificationListener, ActivateNotificationListenerInterface { +public abstract class ActivateNotificationListener implements NotificationHandler, NotificationListener, ActivateNotificationListenerInterface { /** * Base notify called with var args. This method parses the parameters and calls the abstract method. * * @param args - variable argument list based on the type of notification. + * + * @deprecated by {@link ActivateNotificationListener#handle(ActivateNotification)} */ @Override + @Deprecated public final void notify(Object... args) { assert (args[0] instanceof Experiment); Experiment experiment = (Experiment) args[0]; @@ -51,6 +60,17 @@ public final void notify(Object... args) { onActivate(experiment, userId, attributes, variation, logEvent); } + @Override + public final void handle(ActivateNotification message) { + onActivate( + message.getExperiment(), + message.getUserId(), + message.getAttributes(), + message.getVariation(), + message.getEvent() + ); + } + /** * onActivate called when an activate was triggered * diff --git a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java index 61a01daeb..c0a1e3a73 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/ActivateNotificationListenerInterface.java @@ -23,6 +23,12 @@ import javax.annotation.Nonnull; import java.util.Map; +/** + * ActivateNotificationListenerInterface provides and interface for activate event notification. + * + * @deprecated along with {@link ActivateNotification} and {@link ActivateNotificationListener} + * and users should implement NotificationHandler<DecisionNotification> directly. + */ @Deprecated public interface ActivateNotificationListenerInterface { /** diff --git a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java index 414eb393d..fd7195f63 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotification.java @@ -29,7 +29,7 @@ import static com.optimizely.ab.notification.DecisionNotification.ExperimentDecisionNotificationBuilder.EXPERIMENT_KEY; import static com.optimizely.ab.notification.DecisionNotification.ExperimentDecisionNotificationBuilder.VARIATION_KEY; -public class DecisionNotification { +public final class DecisionNotification { protected String type; protected String userId; protected Map attributes; diff --git a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotificationListener.java b/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotificationListener.java deleted file mode 100644 index f86b1ef27..000000000 --- a/core-api/src/main/java/com/optimizely/ab/notification/DecisionNotificationListener.java +++ /dev/null @@ -1,33 +0,0 @@ -/**************************************************************************** - * Copyright 2019, Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * http://www.apache.org/licenses/LICENSE-2.0 * - * * - * Unless required by applicable law or agreed to in writing, software * - * distributed under the License is distributed on an "AS IS" BASIS, * - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * - * See the License for the specific language governing permissions and * - * limitations under the License. * - ***************************************************************************/ - -package com.optimizely.ab.notification; - -import javax.annotation.Nonnull; - -public interface DecisionNotificationListener { - - /** - * onDecision called when an activate was triggered - * - * @param decisionNotification - The decision notification object containing: - * type - The notification type. - * userId - The userId passed to the API. - * attributes - The attribute map passed to the API. - * decisionInfo - The decision information containing all parameters passed in API. - */ - void onDecision(@Nonnull DecisionNotification decisionNotification); -} diff --git a/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java b/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java index 6f026adca..c1180ed97 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/NotificationCenter.java @@ -16,25 +16,39 @@ */ package com.optimizely.ab.notification; -import com.optimizely.ab.config.Experiment; -import com.optimizely.ab.config.Variation; -import com.optimizely.ab.event.LogEvent; +import com.optimizely.ab.OptimizelyRuntimeException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import javax.annotation.Nonnull; -import java.util.ArrayList; +import javax.annotation.Nullable; +import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; - /** - * This class handles impression and conversion notificationsListeners. It replaces NotificationBroadcaster and is intended to be - * more flexible. + * NotificationCenter handles all notification listeners. + * It replaces NotificationBroadcaster and is intended to be more flexible. + * + * NotificationCenter is a holder for a set of supported {@link NotificationManager} instances. + * If a notification object is sent via {@link NotificationCenter#send(Object)} that is not supported + * an {@link OptimizelyRuntimeException} will be thrown. This is an internal interface so + * usage should be restricted to the SDK. + * + * Supported notification classes are setup within {@link NotificationCenter#NotificationCenter()} + * as an unmodifiable map so additional notifications must be added there. + * + * Currently supported notification classes are: + * * {@link ActivateNotification} + * * {@link TrackNotification} + * * {@link DecisionNotification} with this class replacing {@link ActivateNotification} */ public class NotificationCenter { + + private static final Logger logger = LoggerFactory.getLogger(NotificationCenter.class); + private final Map notifierMap; + + // TODO move to DecisionNotification. public enum DecisionNotificationType { AB_TEST("ab-test"), FEATURE("feature"), @@ -51,12 +65,12 @@ public enum DecisionNotificationType { public String toString() { return key; } - } /** * NotificationType is used for the notification types supported. */ + @Deprecated public enum NotificationType { Activate(ActivateNotificationListener.class), // Activate was called. Track an impression event @@ -73,104 +87,77 @@ public Class getNotificationTypeClass() { } } - ; - - - // the notification id is incremented and is assigned as the callback id, it can then be used to remove the notification. - private AtomicInteger notificationListenerID = new AtomicInteger(); - - final private static Logger logger = LoggerFactory.getLogger(NotificationCenter.class); - - // notification holder holds the id as well as the notification. - private static class NotificationHolder { - int notificationId; - NotificationListener notificationListener; - DecisionNotificationListener decisionNotificationListener; - - NotificationHolder(int id, NotificationListener notificationListener) { - notificationId = id; - this.notificationListener = notificationListener; - } + public NotificationCenter() { + AtomicInteger counter = new AtomicInteger(); + Map validManagers = new HashMap<>(); + validManagers.put(ActivateNotification.class, new NotificationManager(counter)); + validManagers.put(TrackNotification.class, new NotificationManager(counter)); + validManagers.put(DecisionNotification.class, new NotificationManager(counter)); - NotificationHolder(int id, DecisionNotificationListener decisionNotificationListener) { - notificationId = id; - this.decisionNotificationListener = decisionNotificationListener; - } + notifierMap = Collections.unmodifiableMap(validManagers); } - /** - * Instantiate a new NotificationCenter - */ - public NotificationCenter() { - notificationsListeners.put(NotificationType.Activate, new ArrayList()); - notificationsListeners.put(NotificationType.Track, new ArrayList()); + @Nullable + @SuppressWarnings("unchecked") + public NotificationManager getNotificationManager(Class clazz) { + return notifierMap.get(clazz); } - // private list of notification by notification type. - // we used a list so that notification order can mean something. - private Map> notificationsListeners = new HashMap>(); - private List decisionListenerHolder = new ArrayList<>(); - /** * Convenience method to support lambdas as callbacks in later version of Java (8+). * - * @param decisionNotificationListener + * @param activateNotificationListener * @return greater than zero if added. + * + * @deprecated by {@link NotificationManager#addHandler(NotificationHandler)} */ - public int addDecisionNotificationListener(DecisionNotificationListener decisionNotificationListener) { - if (decisionNotificationListener != null) { - for (NotificationHolder holder : decisionListenerHolder) { - if (holder.decisionNotificationListener == decisionNotificationListener) { - // TODO: 3/27/2019 change log level from warn to info and to return existing listener ID - logger.warn("Notification listener was already added"); - return -1; - } - } - int id = this.notificationListenerID.incrementAndGet(); - decisionListenerHolder.add(new NotificationHolder(id, decisionNotificationListener)); - return id; - } else { + @Deprecated + public int addActivateNotificationListener(final ActivateNotificationListenerInterface activateNotificationListener) { + NotificationManager notificationManager = getNotificationManager(ActivateNotification.class); + if (notificationManager == null) { logger.warn("Notification listener was the wrong type. It was not added to the notification center."); return -1; } - } - /** - * Convenience method to support lambdas as callbacks in later version of Java (8+). - * - * @param activateNotificationListenerInterface - * @return greater than zero if added. - */ - @Deprecated - public int addActivateNotificationListener(final ActivateNotificationListenerInterface activateNotificationListenerInterface) { - if (activateNotificationListenerInterface instanceof ActivateNotificationListener) { - return addNotificationListener(NotificationType.Activate, (NotificationListener) activateNotificationListenerInterface); + if (activateNotificationListener instanceof ActivateNotificationListener) { + return notificationManager.addHandler((ActivateNotificationListener) activateNotificationListener); } else { - return addNotificationListener(NotificationType.Activate, new ActivateNotificationListener() { - @Override - public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { - activateNotificationListenerInterface.onActivate(experiment, userId, attributes, variation, event); - } - }); + return notificationManager.addHandler(message -> activateNotificationListener.onActivate( + message.getExperiment(), + message.getUserId(), + message.getAttributes(), + message.getVariation(), + message.getEvent() + )); } } /** * Convenience method to support lambdas as callbacks in later versions of Java (8+) * - * @param trackNotificationListenerInterface + * @param trackNotificationListener * @return greater than zero if added. + * + * @deprecated by {@link NotificationManager#addHandler(NotificationHandler)} */ - public int addTrackNotificationListener(final TrackNotificationListenerInterface trackNotificationListenerInterface) { - if (trackNotificationListenerInterface instanceof TrackNotificationListener) { - return addNotificationListener(NotificationType.Track, (NotificationListener) trackNotificationListenerInterface); + @Deprecated + public int addTrackNotificationListener(final TrackNotificationListenerInterface trackNotificationListener) { + NotificationManager notificationManager = getNotificationManager(TrackNotification.class); + if (notificationManager == null) { + logger.warn("Notification listener was the wrong type. It was not added to the notification center."); + return -1; + } + + if (trackNotificationListener instanceof TrackNotificationListener) { + return notificationManager.addHandler((TrackNotificationListener) trackNotificationListener); } else { - return addNotificationListener(NotificationType.Track, new TrackNotificationListener() { - @Override - public void onTrack(@Nonnull String eventKey, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Map eventTags, @Nonnull LogEvent event) { - trackNotificationListenerInterface.onTrack(eventKey, userId, attributes, eventTags, event); - } - }); + return notificationManager.addHandler(message -> trackNotificationListener.onTrack( + message.getEventKey(), + message.getUserId(), + message.getAttributes(), + message.getEventTags(), + message.getEvent() + )); } } @@ -180,66 +167,43 @@ public void onTrack(@Nonnull String eventKey, @Nonnull String userId, @Nonnull M * @param notificationType - enum NotificationType to add. * @param notificationListener - Notification to add. * @return the notification id used to remove the notification. It is greater than 0 on success. + * + * @deprecated by {@link NotificationManager#addHandler(NotificationHandler)} */ + @Deprecated public int addNotificationListener(NotificationType notificationType, NotificationListener notificationListener) { - Class clazz = notificationType.notificationTypeClass; + Class clazz = notificationType.getNotificationTypeClass(); if (clazz == null || !clazz.isInstance(notificationListener)) { logger.warn("Notification listener was the wrong type. It was not added to the notification center."); return -1; } - for (NotificationHolder holder : notificationsListeners.get(notificationType)) { - if (holder.notificationListener == notificationListener) { - logger.warn("Notification listener was already added"); - return -1; - } + switch (notificationType) { + case Track: + return addTrackNotificationListener((TrackNotificationListener) notificationListener); + case Activate: + return addActivateNotificationListener((ActivateNotificationListener) notificationListener); + default: + throw new OptimizelyRuntimeException("Unsupported notificationType"); } - int id = this.notificationListenerID.incrementAndGet(); - notificationsListeners.get(notificationType).add(new NotificationHolder(id, notificationListener)); - logger.info("Notification listener {} was added with id {}", notificationListener.toString(), id); - return id; } /** - * Remove the notification listener based on the notificationId passed back from addNotificationListener. + * Remove the notification listener based on the notificationId passed back from addDecisionNotificationHandler. * * @param notificationID the id passed back from add notification. * @return true if removed otherwise false (if the notification is already registered, it returns false). */ public boolean removeNotificationListener(int notificationID) { - - for (List notificationHolders : notificationsListeners.values()) { - if (removeNotificationListener(notificationID, notificationHolders)) { - return true; - } - } - - if (removeNotificationListener(notificationID, decisionListenerHolder)) { - return true; - } - - logger.warn("Notification listener with id {} not found", notificationID); - - return false; - } - - /** - * Helper method to iterate find NotificationHolder in an List identified by the notificationId - * - * @param notificationID the id passed back from add notification. - * @param notificationHolderList list from which to remove notification listener. - * @return true if removed otherwise false - */ - private boolean removeNotificationListener(int notificationID, List notificationHolderList) { - for (NotificationHolder holder : notificationHolderList) { - if (holder.notificationId == notificationID) { - notificationHolderList.remove(holder); + for (NotificationManager manager : notifierMap.values()) { + if (manager.remove(notificationID)) { logger.info("Notification listener removed {}", notificationID); return true; } } + logger.warn("Notification listener with id {} not found", notificationID); return false; } @@ -247,9 +211,8 @@ private boolean removeNotificationListener(int notificationID, List manager : notifierMap.values()) { + manager.clear(); } } @@ -257,36 +220,42 @@ public void clearAllNotificationListeners() { * Clear notification listeners by notification type. * * @param notificationType type of notificationsListeners to remove. + * + * @deprecated by {@link NotificationCenter#clearNotificationListeners(Class)} */ + @Deprecated public void clearNotificationListeners(NotificationType notificationType) { - notificationsListeners.get(notificationType).clear(); + switch (notificationType) { + case Track: + clearNotificationListeners(TrackNotification.class); + break; + case Activate: + clearNotificationListeners(ActivateNotification.class); + break; + default: + throw new OptimizelyRuntimeException("Unsupported notificationType"); + } } /** - * fire a notificaiton of Decision Notification type. - * - * @param decision containing Decision Notification object + * Clear notification listeners by notification class. */ - public void sendNotifications(DecisionNotification decision) { - for (NotificationHolder holder : decisionListenerHolder) { - try { - holder.decisionNotificationListener.onDecision(decision); - } catch (Exception e) { - logger.error("Unexpected exception calling notification listener {}", holder.notificationId, e); - } + public void clearNotificationListeners(Class clazz) { + NotificationManager notificationManager = getNotificationManager(clazz); + if (notificationManager == null) { + throw new OptimizelyRuntimeException("Unsupported notification type."); } + + notificationManager.clear(); } - // fire a notificaiton of a certain type. The arg list changes depending on the type of notification sent. - public void sendNotifications(NotificationType notificationType, Object... args) { - ArrayList holders = notificationsListeners.get(notificationType); - for (NotificationHolder holder : holders) { - try { - holder.notificationListener.notify(args); - } catch (Exception e) { - logger.error("Unexpected exception calling notification listener {}", holder.notificationId, e); - } + @SuppressWarnings("unchecked") + public void send(Object notification) { + NotificationManager handler = getNotificationManager(notification.getClass()); + if (handler == null) { + throw new OptimizelyRuntimeException("Unsupported notificationType"); } - } + handler.send(notification); + } } diff --git a/core-api/src/main/java/com/optimizely/ab/notification/NotificationHandler.java b/core-api/src/main/java/com/optimizely/ab/notification/NotificationHandler.java new file mode 100644 index 000000000..dd2cc9ebf --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/notification/NotificationHandler.java @@ -0,0 +1,28 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +/** + * NotificationHandler is a generic interface Optimizely notification listeners. + * This interface replaces {@link NotificationListener} which didn't provide adequate type safety. + * + * While this class adds generic handler implementations to be created, the domain of supported + * implementations is maintained by the {@link NotificationCenter} + */ +public interface NotificationHandler { + void handle(T message); +} diff --git a/core-api/src/main/java/com/optimizely/ab/notification/NotificationListener.java b/core-api/src/main/java/com/optimizely/ab/notification/NotificationListener.java index 75ac003b2..1caea5ca4 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/NotificationListener.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/NotificationListener.java @@ -20,7 +20,10 @@ * An interface class for Optimizely notification listeners. *

* We changed this from a abstract class to a interface to support lambdas moving forward in Java 8 and beyond. + * + * @deprecated in favor of the {@link NotificationHandler} interface. */ +@Deprecated public interface NotificationListener { /** diff --git a/core-api/src/main/java/com/optimizely/ab/notification/NotificationManager.java b/core-api/src/main/java/com/optimizely/ab/notification/NotificationManager.java new file mode 100644 index 000000000..77985d1e5 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/notification/NotificationManager.java @@ -0,0 +1,73 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * NotificationManger is a generic class for managing notifications for a given class. + * + * The NotificationManager is responsible for storing a collection of NotificationHandlers and mapping + * them to a globally unique integer so that they can be removed on demand. + */ +public class NotificationManager { + + private static final Logger logger = LoggerFactory.getLogger(NotificationManager.class); + + private final Map> handlers = new LinkedHashMap<>(); + private final AtomicInteger counter; + + NotificationManager(AtomicInteger counter) { + this.counter = counter; + } + + public int addHandler(NotificationHandler newHandler) { + + // Prevent registering a duplicate listener. + for (NotificationHandler handler: handlers.values()) { + if (handler.equals(newHandler)) { + logger.warn("Notification listener was already added"); + return -1; + } + } + + int notificationId = counter.incrementAndGet(); + handlers.put(notificationId, newHandler); + + return notificationId; + } + + public void send(T message) { + for (NotificationHandler handler: handlers.values()) { + handler.handle(message); + } + } + + public void clear() { + handlers.clear(); + } + + public boolean remove(int notificationID) { + NotificationHandler handler = handlers.remove(notificationID); + return handler != null; + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/notification/TrackNotification.java b/core-api/src/main/java/com/optimizely/ab/notification/TrackNotification.java new file mode 100644 index 000000000..436b027ef --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/notification/TrackNotification.java @@ -0,0 +1,86 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +import com.optimizely.ab.annotations.VisibleForTesting; +import com.optimizely.ab.event.LogEvent; + +import java.util.Map; + +/** + * TrackNotification encapsulates the arguments used to submit tracking calls. + */ +public final class TrackNotification { + + private final String eventKey; + private final String userId; + private final Map attributes; + private final Map eventTags; + private final LogEvent event; + + @VisibleForTesting + TrackNotification() { + this(null, null, null, null, null); + } + + /** + * @param eventKey - The event key that was triggered. + * @param userId - user id passed into track. + * @param attributes - filtered attributes list after passed into track + * @param eventTags - event tags if any were passed in. + * @param event - The event being recorded. + */ + public TrackNotification(String eventKey, String userId, Map attributes, Map eventTags, LogEvent event) { + this.eventKey = eventKey; + this.userId = userId; + this.attributes = attributes; + this.eventTags = eventTags; + this.event = event; + } + + public String getEventKey() { + return eventKey; + } + + public String getUserId() { + return userId; + } + + public Map getAttributes() { + return attributes; + } + + public Map getEventTags() { + return eventTags; + } + + public LogEvent getEvent() { + return event; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("TrackNotification{"); + sb.append("eventKey='").append(eventKey).append('\''); + sb.append(", userId='").append(userId).append('\''); + sb.append(", attributes=").append(attributes); + sb.append(", eventTags=").append(eventTags); + sb.append(", event=").append(event); + sb.append('}'); + return sb.toString(); + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/notification/TrackNotificationListener.java b/core-api/src/main/java/com/optimizely/ab/notification/TrackNotificationListener.java index c93e09a4b..d6eae25e2 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/TrackNotificationListener.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/TrackNotificationListener.java @@ -22,15 +22,22 @@ import com.optimizely.ab.event.LogEvent; /** - * This class handles the track event notification. + * TrackNotificationListener handles the track event notification. + * + * @deprecated and users should implement NotificationHandler<TrackNotification> directly. */ -public abstract class TrackNotificationListener implements NotificationListener, TrackNotificationListenerInterface { +@Deprecated +public abstract class TrackNotificationListener implements NotificationHandler, NotificationListener, TrackNotificationListenerInterface { + /** * Base notify called with var args. This method parses the parameters and calls the abstract method. * * @param args - variable argument list based on the type of notification. + * + * @deprecated by {@link TrackNotificationListener#handle(TrackNotification)} */ @Override + @Deprecated public final void notify(Object... args) { assert (args[0] instanceof String); String eventKey = (String) args[0]; @@ -52,6 +59,17 @@ public final void notify(Object... args) { onTrack(eventKey, userId, attributes, eventTags, logEvent); } + @Override + public final void handle(TrackNotification message) { + onTrack( + message.getEventKey(), + message.getUserId(), + message.getAttributes(), + message.getEventTags(), + message.getEvent() + ); + } + /** * onTrack is called when a track event is triggered * diff --git a/core-api/src/main/java/com/optimizely/ab/notification/TrackNotificationListenerInterface.java b/core-api/src/main/java/com/optimizely/ab/notification/TrackNotificationListenerInterface.java index 6e979d995..746de567f 100644 --- a/core-api/src/main/java/com/optimizely/ab/notification/TrackNotificationListenerInterface.java +++ b/core-api/src/main/java/com/optimizely/ab/notification/TrackNotificationListenerInterface.java @@ -21,6 +21,12 @@ import javax.annotation.Nonnull; import java.util.Map; +/** + * TrackNotificationListenerInterface provides an interface for track event notification. + * + * @deprecated and users should implement NotificationHandler<TrackNotification> directly. + */ +@Deprecated public interface TrackNotificationListenerInterface { /** * onTrack is called when a track event is triggered diff --git a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java index 4e482eab7..f13fec1a3 100644 --- a/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java +++ b/core-api/src/test/java/com/optimizely/ab/OptimizelyTest.java @@ -2643,24 +2643,23 @@ public void getVariationWithInvalidDatafile() throws Exception { /** * Helper method to return decisionListener **/ - private DecisionNotificationListener getDecisionListener(final String testType, - final String testUserId, - final Map testUserAttributes, - final Map testDecisionInfo) { - return new DecisionNotificationListener() { - @Override - public void onDecision(@Nonnull DecisionNotification decisionNotification) { - assertEquals(decisionNotification.getType(), testType); - assertEquals(decisionNotification.getUserId(), testUserId); - assertEquals(decisionNotification.getAttributes(), testUserAttributes); - for (Map.Entry entry : decisionNotification.getAttributes().entrySet()) { - assertEquals(testUserAttributes.get(entry.getKey()), entry.getValue()); - } - for (Map.Entry entry : decisionNotification.getDecisionInfo().entrySet()) { - assertEquals(testDecisionInfo.get(entry.getKey()), entry.getValue()); - } - isListenerCalled = true; + private NotificationHandler getDecisionListener( + final String testType, + final String testUserId, + final Map testUserAttributes, + final Map testDecisionInfo) + { + return decisionNotification -> { + assertEquals(decisionNotification.getType(), testType); + assertEquals(decisionNotification.getUserId(), testUserId); + assertEquals(decisionNotification.getAttributes(), testUserAttributes); + for (Map.Entry entry : decisionNotification.getAttributes().entrySet()) { + assertEquals(testUserAttributes.get(entry.getKey()), entry.getValue()); + } + for (Map.Entry entry : decisionNotification.getDecisionInfo().entrySet()) { + assertEquals(testDecisionInfo.get(entry.getKey()), entry.getValue()); } + isListenerCalled = true; }; } @@ -2688,7 +2687,8 @@ public void activateEndToEndWithDecisionListener() throws Exception { testDecisionInfoMap.put(EXPERIMENT_KEY, activatedExperiment.getKey()); testDecisionInfoMap.put(VARIATION_KEY, "Gred"); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.notificationCenter.getNotificationManager(DecisionNotification.class) + .addHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_TEST.toString(), userId, testUserAttributes, @@ -2727,7 +2727,7 @@ public void activateUserNullWithListener() throws Exception { testDecisionInfoMap.put(EXPERIMENT_KEY, activatedExperiment.getKey()); testDecisionInfoMap.put(VARIATION_KEY, null); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.AB_TEST.toString(), null, Collections.emptyMap(), @@ -2767,7 +2767,7 @@ public void activateUserNotInAudienceWithListener() throws Exception { testDecisionInfoMap.put(EXPERIMENT_KEY, activatedExperiment.getKey()); testDecisionInfoMap.put(VARIATION_KEY, null); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_TEST.toString(), genericUserId, testUserAttributes, @@ -2803,13 +2803,10 @@ public void getEnabledFeaturesWithListenerMultipleFeatureEnabled() throws Except .withConfig(validProjectConfig) .build(); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( - new DecisionNotificationListener() { - @Override - public void onDecision(@Nonnull DecisionNotification decisionNotification) { - isListenerCalled = true; - assertEquals(decisionNotification.getType(), NotificationCenter.DecisionNotificationType.FEATURE.toString()); - } + int notificationId = optimizely.addDecisionNotificationHandler( + decisionNotification -> { + isListenerCalled = true; + assertEquals(decisionNotification.getType(), NotificationCenter.DecisionNotificationType.FEATURE.toString()); }); ArrayList featureFlags = (ArrayList) optimizely.getEnabledFeatures(testUserId, @@ -2845,13 +2842,7 @@ public void getEnabledFeaturesWithNoFeatureEnabled() throws Exception { eq(Collections.emptyMap()) ); - int notificationId = spyOptimizely.notificationCenter.addDecisionNotificationListener( - new DecisionNotificationListener() { - @Override - public void onDecision(@Nonnull DecisionNotification decisionNotification) { - - } - }); + int notificationId = spyOptimizely.addDecisionNotificationHandler( decisionNotification -> { }); ArrayList featureFlags = (ArrayList) spyOptimizely.getEnabledFeatures(genericUserId, Collections.emptyMap()); @@ -2895,7 +2886,7 @@ public void isFeatureEnabledWithListenerUserInExperimentFeatureOn() throws Excep testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE.toString(), genericUserId, testUserAttributes, @@ -2951,7 +2942,7 @@ public void isFeatureEnabledWithListenerUserInExperimentFeatureOff() throws Exce testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE.toString(), genericUserId, testUserAttributes, @@ -3007,7 +2998,7 @@ public void isFeatureEnabledWithListenerUserNotInExperimentAndNotInRollOut() thr testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.ROLLOUT.toString()); testDecisionInfoMap.put(SOURCE_INFO, new HashMap<>()); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE.toString(), genericUserId, testUserAttributes, @@ -3054,7 +3045,7 @@ public void isFeatureEnabledWithListenerUserInRollOut() throws Exception { testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.ROLLOUT.toString()); testDecisionInfoMap.put(SOURCE_INFO, new HashMap<>()); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE.toString(), genericUserId, testUserAttributes, @@ -3109,7 +3100,7 @@ public void getFeatureVariableWithListenerUserInExperimentFeatureOn() throws Exc testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(), testUserId, testUserAttributes, @@ -3162,7 +3153,7 @@ public void getFeatureVariableWithListenerUserInExperimentFeatureOff() { testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(), userID, testUserAttributes, @@ -3212,7 +3203,7 @@ public void getFeatureVariableWithListenerUserInRollOutFeatureOn() throws Except testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.ROLLOUT.toString()); testDecisionInfoMap.put(SOURCE_INFO, Collections.EMPTY_MAP); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(), genericUserId, testUserAttributes, @@ -3262,7 +3253,7 @@ public void getFeatureVariableWithListenerUserNotInRollOutFeatureOff() { testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.ROLLOUT.toString()); testDecisionInfoMap.put(SOURCE_INFO, Collections.EMPTY_MAP); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(), genericUserId, testUserAttributes, @@ -3311,7 +3302,7 @@ public void getFeatureVariableIntegerWithListenerUserInRollOutFeatureOn() { testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.ROLLOUT.toString()); testDecisionInfoMap.put(SOURCE_INFO, Collections.EMPTY_MAP); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(), genericUserId, testUserAttributes, @@ -3363,7 +3354,7 @@ public void getFeatureVariableDoubleWithListenerUserInExperimentFeatureOn() thro testDecisionInfoMap.put(SOURCE, FeatureDecision.DecisionSource.FEATURE_TEST.toString()); testDecisionInfoMap.put(SOURCE_INFO, testSourceInfo); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.FEATURE_VARIABLE.toString(), genericUserId, testUserAttributes, @@ -3853,12 +3844,12 @@ public void trackEventWithListenerNullAttributes() throws Exception { public void onTrack(@Nonnull String eventKey, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Map eventTags, @Nonnull LogEvent event) { assertEquals(eventType.getKey(), eventKey); assertEquals(genericUserId, userId); - assertNull(attributes.isEmpty()); + assertNull(attributes); assertTrue(eventTags.isEmpty()); } }; - int notificationId = optimizely.notificationCenter.addNotificationListener(NotificationCenter.NotificationType.Track, trackNotification); + int notificationId = optimizely.addTrackNotificationHandler(trackNotification); // call track optimizely.track(eventType.getKey(), genericUserId, attributes); @@ -5771,7 +5762,7 @@ public void getVariationBucketingIdAttribute() throws Exception { testDecisionInfoMap.put(EXPERIMENT_KEY, experiment.getKey()); testDecisionInfoMap.put(VARIATION_KEY, bucketedVariation.getKey()); - int notificationId = optimizely.notificationCenter.addDecisionNotificationListener( + int notificationId = optimizely.addDecisionNotificationHandler( getDecisionListener(NotificationCenter.DecisionNotificationType.AB_TEST.toString(), testUserId, testUserAttributes, @@ -5812,6 +5803,34 @@ public void isValidReturnsTrueWhenClientIsValid() throws Exception { assertTrue(optimizely.isValid()); } + //======== Test Notification APIs ========// + + @Test + public void testGetNotificationCenter() { + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler).build(); + assertEquals(optimizely.notificationCenter, optimizely.getNotificationCenter()); + } + + @Test + public void testAddTrackNotificationHandler() { + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler).build(); + NotificationManager manager = optimizely.getNotificationCenter() + .getNotificationManager(TrackNotification.class); + + int notificationId = optimizely.addTrackNotificationHandler(trackNotification -> {}); + assertTrue(manager.remove(notificationId)); + } + + @Test + public void testAddDecisionNotificationHandler() { + Optimizely optimizely = Optimizely.builder(validDatafile, mockEventHandler).build(); + NotificationManager manager = optimizely.getNotificationCenter() + .getNotificationManager(DecisionNotification.class); + + int notificationId = optimizely.addDecisionNotificationHandler(decisionNotification -> {}); + assertTrue(manager.remove(notificationId)); + } + //======== Helper methods ========// private Experiment createUnknownExperiment() { diff --git a/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java b/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java new file mode 100644 index 000000000..f7fcda09b --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationListenerTest.java @@ -0,0 +1,75 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.Variation; +import com.optimizely.ab.event.LogEvent; +import org.junit.Before; +import org.junit.Test; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +public class ActivateNotificationListenerTest { + + private static final Experiment EXPERIMENT = mock(Experiment.class); + private static final Variation VARIATION = mock(Variation.class); + private static final String USER_ID = "userID"; + private static final Map USER_ATTRIBUTES = Collections.singletonMap("user", "attr"); + private static final LogEvent LOG_EVENT = new LogEvent( + LogEvent.RequestMethod.POST, + "endpoint", + Collections.emptyMap(), + null + ); + + private ActivateNotification activateNotification; + private ActivateNotificationListener activateNotificationListener; + + @Before + public void setUp() throws Exception { + activateNotification = new ActivateNotification(EXPERIMENT, USER_ID, USER_ATTRIBUTES, VARIATION, LOG_EVENT); + activateNotificationListener = new ActivateNotificationHandler(); + } + + @Test + public void testNotifyWithArgArray() { + activateNotificationListener.notify(EXPERIMENT, USER_ID, USER_ATTRIBUTES, VARIATION, LOG_EVENT); + } + + @Test + public void testNotifyWithActivateNotificationArg() { + activateNotificationListener.handle(activateNotification); + } + + private static class ActivateNotificationHandler extends ActivateNotificationListener { + + @Override + public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Variation variation, @Nonnull LogEvent event) { + assertEquals(EXPERIMENT, experiment); + assertEquals(USER_ID, userId); + assertEquals(USER_ATTRIBUTES, attributes); + assertEquals(VARIATION, variation); + assertEquals(LOG_EVENT, event); + } + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationTest.java b/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationTest.java new file mode 100644 index 000000000..567cc1434 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/notification/ActivateNotificationTest.java @@ -0,0 +1,78 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +import com.optimizely.ab.config.Experiment; +import com.optimizely.ab.config.Variation; +import com.optimizely.ab.event.LogEvent; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import java.util.Collections; +import java.util.Map; + +import static org.junit.Assert.*; +import static org.mockito.Mockito.mock; + +public class ActivateNotificationTest { + + private static final Experiment EXPERIMENT = mock(Experiment.class); + private static final Variation VARIATION = mock(Variation.class); + private static final String USER_ID = "userID"; + private static final Map USER_ATTRIBUTES = Collections.singletonMap("user", "attr"); + private static final LogEvent LOG_EVENT = new LogEvent( + LogEvent.RequestMethod.POST, + "endpoint", + Collections.emptyMap(), + null + ); + + private ActivateNotification activateNotification; + + @Before + public void setUp() throws Exception { + activateNotification = new ActivateNotification(EXPERIMENT, USER_ID, USER_ATTRIBUTES, VARIATION, LOG_EVENT); + } + + @Test + public void testGetExperiment() { + assertEquals(EXPERIMENT, activateNotification.getExperiment()); + } + + @Test + public void testGetUserId() { + assertEquals(USER_ID, activateNotification.getUserId()); + } + + @Test + public void testGetAttributes() { + assertEquals(USER_ATTRIBUTES, activateNotification.getAttributes()); + } + + @Test + public void testGetVariation() { + assertEquals(VARIATION, activateNotification.getVariation()); + } + + @Test + public void testGetEvent() { + assertEquals(LOG_EVENT, activateNotification.getEvent()); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java b/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java index 70bba7627..ed46e4fb0 100644 --- a/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java +++ b/core-api/src/test/java/com/optimizely/ab/notification/NotificationCenterTest.java @@ -17,15 +17,19 @@ package com.optimizely.ab.notification; import ch.qos.logback.classic.Level; +import com.optimizely.ab.OptimizelyRuntimeException; import com.optimizely.ab.config.Experiment; import com.optimizely.ab.config.Variation; import com.optimizely.ab.event.LogEvent; import com.optimizely.ab.internal.LogbackVerifier; +import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import javax.annotation.Nonnull; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import static junit.framework.TestCase.assertNotSame; @@ -43,17 +47,22 @@ public class NotificationCenterTest { public LogbackVerifier logbackVerifier = new LogbackVerifier(); @Before - public void initialize() { + public void setUp() { notificationCenter = new NotificationCenter(); activateNotification = mock(ActivateNotificationListener.class); trackNotification = mock(TrackNotificationListener.class); } + @After + public void tearDown() { + notificationCenter.clearAllNotificationListeners(); + } + @Test public void testAddWrongTrackNotificationListener() { int notificationId = notificationCenter.addNotificationListener(NotificationCenter.NotificationType.Activate, trackNotification); logbackVerifier.expectMessage(Level.WARN, "Notification listener was the wrong type. It was not added to the notification center."); - assertEquals(notificationId, -1); + assertEquals(-1, notificationId); assertFalse(notificationCenter.removeNotificationListener(notificationId)); } @@ -62,24 +71,21 @@ public void testAddWrongTrackNotificationListener() { public void testAddWrongActivateNotificationListener() { int notificationId = notificationCenter.addNotificationListener(NotificationCenter.NotificationType.Track, activateNotification); logbackVerifier.expectMessage(Level.WARN, "Notification listener was the wrong type. It was not added to the notification center."); - assertEquals(notificationId, -1); + assertEquals(-1, notificationId); assertFalse(notificationCenter.removeNotificationListener(notificationId)); } @Test public void testAddDecisionNotificationTwice() { - DecisionNotificationListener listener = new DecisionNotificationListener() { - @Override - public void onDecision(@Nonnull DecisionNotification decisionNotification) { + NotificationHandler handler = decisionNotification -> { }; + NotificationManager manager = + notificationCenter.getNotificationManager(DecisionNotification.class); - } - }; - int notificationId = notificationCenter.addDecisionNotificationListener(listener); - int notificationId2 = notificationCenter.addDecisionNotificationListener(listener); + int notificationId = manager.addHandler(handler); + int notificationId2 = manager.addHandler(handler); logbackVerifier.expectMessage(Level.WARN, "Notification listener was already added"); - assertEquals(notificationId2, -1); + assertEquals(-1, notificationId2); assertTrue(notificationCenter.removeNotificationListener(notificationId)); - notificationCenter.clearAllNotificationListeners(); } @Test @@ -93,9 +99,8 @@ public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @ int notificationId = notificationCenter.addNotificationListener(NotificationCenter.NotificationType.Activate, listener); int notificationId2 = notificationCenter.addNotificationListener(NotificationCenter.NotificationType.Activate, listener); logbackVerifier.expectMessage(Level.WARN, "Notification listener was already added"); - assertEquals(notificationId2, -1); + assertEquals(-1, notificationId2); assertTrue(notificationCenter.removeNotificationListener(notificationId)); - notificationCenter.clearAllNotificationListeners(); } @Test @@ -106,22 +111,16 @@ public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @ } }); - assertNotSame(notificationId, -1); + assertNotSame(-1, notificationId); assertTrue(notificationCenter.removeNotificationListener(notificationId)); - notificationCenter.clearAllNotificationListeners(); } @Test public void testAddDecisionNotification() { - int notificationId = notificationCenter.addDecisionNotificationListener(new DecisionNotificationListener() { - @Override - public void onDecision(@Nonnull DecisionNotification decisionNotification) { - - } - }); - assertNotSame(notificationId, -1); + NotificationManager manager = notificationCenter.getNotificationManager(DecisionNotification.class); + int notificationId = manager.addHandler(decisionNotification -> { }); + assertNotSame(-1, notificationId); assertTrue(notificationCenter.removeNotificationListener(notificationId)); - notificationCenter.clearAllNotificationListeners(); } @Test @@ -132,9 +131,8 @@ public void onTrack(@Nonnull String eventKey, @Nonnull String userId, @Nonnull M } }); - assertNotSame(notificationId, -1); + assertNotSame(-1, notificationId); assertTrue(notificationCenter.removeNotificationListener(notificationId)); - notificationCenter.clearAllNotificationListeners(); } @Test @@ -152,22 +150,16 @@ public void onTrack(@Nonnull String eventKey, @Nonnull String userId, @Nonnull M } }); - assertNotSame(notificationId, -1); + assertNotSame(-1, notificationId); assertTrue(notificationCenter.removeNotificationListener(notificationId)); - notificationCenter.clearAllNotificationListeners(); } @Test public void testAddDecisionNotificationInterface() { - int notificationId = notificationCenter.addDecisionNotificationListener(new DecisionNotificationListener() { - @Override - public void onDecision(@Nonnull DecisionNotification decisionNotification) { - - } - }); - assertNotSame(notificationId, -1); - assertTrue(notificationCenter.removeNotificationListener(notificationId)); - notificationCenter.clearAllNotificationListeners(); + NotificationManager manager = notificationCenter.getNotificationManager(DecisionNotification.class); + int notificationId = manager.addHandler(decisionNotification -> { }); + assertNotSame(-1, notificationId); + assertTrue(manager.remove(notificationId)); } @Test @@ -178,9 +170,68 @@ public void onActivate(@Nonnull Experiment experiment, @Nonnull String userId, @ } }); - assertNotSame(notificationId, -1); + assertNotSame(-1, notificationId); assertTrue(notificationCenter.removeNotificationListener(notificationId)); - notificationCenter.clearAllNotificationListeners(); } + @Test + @Deprecated + public void testClearNotificationByActivateType() { + NotificationManager manager = notificationCenter.getNotificationManager(ActivateNotification.class); + int id = manager.addHandler(message -> {}); + + notificationCenter.clearNotificationListeners(NotificationCenter.NotificationType.Activate); + assertFalse(manager.remove(id)); + } + + @Test + @Deprecated + public void testClearNotificationByTrackType() { + NotificationManager manager = notificationCenter.getNotificationManager(TrackNotification.class); + int id = manager.addHandler(message -> {}); + + notificationCenter.clearNotificationListeners(NotificationCenter.NotificationType.Track); + assertFalse(manager.remove(id)); + } + + @Test + @Deprecated + public void testAddActivateListenerInterface() { + int id = notificationCenter.addActivateNotificationListener((experiment, userId, attributes, variation, event) -> { }); + + NotificationManager manager = notificationCenter.getNotificationManager(ActivateNotification.class); + assertTrue(manager.remove(id)); + } + + @Test + @Deprecated + public void testAddTrackListenerInterface() { + int id = notificationCenter.addTrackNotificationListener((experiment, userId, attributes, variation, event) -> { }); + + NotificationManager manager = notificationCenter.getNotificationManager(TrackNotification.class); + assertTrue(manager.remove(id)); + } + + @Test(expected = OptimizelyRuntimeException.class) + public void testSendWithoutHandler() { + notificationCenter.send(new TestNotification("")); + } + + @Test + public void testSendWithHandler() { + testSendWithNotification(new TrackNotification()); + testSendWithNotification(new DecisionNotification()); + testSendWithNotification(new ActivateNotification()); + } + + private void testSendWithNotification(Object notification) { + TestNotificationHandler handler = new TestNotificationHandler<>(); + notificationCenter.getNotificationManager(notification.getClass()).addHandler(handler); + notificationCenter.send(notification); + + List messages = handler.getMessages(); + assertEquals(1, messages.size()); + assertEquals(notification, messages.get(0)); + + } } diff --git a/core-api/src/test/java/com/optimizely/ab/notification/NotificationManagerTest.java b/core-api/src/test/java/com/optimizely/ab/notification/NotificationManagerTest.java new file mode 100644 index 000000000..ceb2be138 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/notification/NotificationManagerTest.java @@ -0,0 +1,61 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +import org.junit.Before; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.Assert.*; + +public class NotificationManagerTest { + + private NotificationManager notificationManager; + private AtomicInteger counter; + + @Before + public void setUp() { + counter = new AtomicInteger(); + notificationManager = new NotificationManager<>(counter); + } + + @Test + public void testAddListener() { + assertEquals(1, notificationManager.addHandler(new TestNotificationHandler())); + assertEquals(2, notificationManager.addHandler(new TestNotificationHandler())); + assertEquals(3, notificationManager.addHandler(new TestNotificationHandler())); + } + + @Test + public void testSend() { + TestNotificationHandler handler = new TestNotificationHandler<>(); + assertEquals(1, notificationManager.addHandler(handler)); + + notificationManager.send(new TestNotification("message1")); + notificationManager.send(new TestNotification("message2")); + notificationManager.send(new TestNotification("message3")); + + List messages = handler.getMessages(); + assertEquals(3, messages.size()); + assertEquals("message1", messages.get(0).getMessage()); + assertEquals("message2", messages.get(1).getMessage()); + assertEquals("message3", messages.get(2).getMessage()); + + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/notification/TestNotification.java b/core-api/src/test/java/com/optimizely/ab/notification/TestNotification.java new file mode 100644 index 000000000..131d82896 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/notification/TestNotification.java @@ -0,0 +1,32 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +/** + * TestNotification used for unit testing NotificationCenter and NotificationManager + */ +class TestNotification { + private final String message; + + TestNotification(String message) { + this.message = message; + } + + String getMessage() { + return message; + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/notification/TestNotificationHandler.java b/core-api/src/test/java/com/optimizely/ab/notification/TestNotificationHandler.java new file mode 100644 index 000000000..8941edff5 --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/notification/TestNotificationHandler.java @@ -0,0 +1,36 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +import java.util.ArrayList; +import java.util.List; + +/** + * TestNotificationHandler used for unit testing NotificationCenter and NotificationManager + */ +class TestNotificationHandler implements NotificationHandler { + private final List messages = new ArrayList<>(); + + @Override + public void handle(T message) { + messages.add(message); + } + + List getMessages() { + return messages; + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/notification/TrackNotificationListenerTest.java b/core-api/src/test/java/com/optimizely/ab/notification/TrackNotificationListenerTest.java new file mode 100644 index 000000000..5ad9c21cb --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/notification/TrackNotificationListenerTest.java @@ -0,0 +1,72 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +import com.optimizely.ab.event.LogEvent; +import org.junit.Before; +import org.junit.Test; + +import javax.annotation.Nonnull; +import java.util.Collections; +import java.util.Map; + +import static org.junit.Assert.*; + +public class TrackNotificationListenerTest { + + private static final String EVENT_KEY = "eventKey"; + private static final String USER_ID = "userID"; + private static final Map USER_ATTRIBUTES = Collections.singletonMap("user", "attr"); + private static final Map EVENT_TAGS = Collections.singletonMap("event", "tag"); + private static final LogEvent LOG_EVENT = new LogEvent( + LogEvent.RequestMethod.POST, + "endpoint", + Collections.emptyMap(), + null + ); + + private TrackNotification trackNotification; + private TrackNotificationHandler trackNotificationHandler; + + @Before + public void setUp() throws Exception { + trackNotification = new TrackNotification(EVENT_KEY, USER_ID, USER_ATTRIBUTES, EVENT_TAGS, LOG_EVENT); + trackNotificationHandler = new TrackNotificationHandler(); + } + + @Test + public void testNotifyWithArgArray() { + trackNotificationHandler.notify(EVENT_KEY, USER_ID, USER_ATTRIBUTES, EVENT_TAGS, LOG_EVENT); + } + + @Test + public void testNotifyWithTrackNotificationArg() { + trackNotificationHandler.handle(trackNotification); + } + + private static class TrackNotificationHandler extends TrackNotificationListener { + + @Override + public void onTrack(@Nonnull String eventKey, @Nonnull String userId, @Nonnull Map attributes, @Nonnull Map eventTags, @Nonnull LogEvent event) { + assertEquals(EVENT_KEY, eventKey); + assertEquals(USER_ID, userId); + assertEquals(USER_ATTRIBUTES, attributes); + assertEquals(EVENT_TAGS, eventTags); + assertEquals(LOG_EVENT, event); + } + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/notification/TrackNotificationTest.java b/core-api/src/test/java/com/optimizely/ab/notification/TrackNotificationTest.java new file mode 100644 index 000000000..dbe8abddc --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/notification/TrackNotificationTest.java @@ -0,0 +1,77 @@ +/** + * + * Copyright 2019, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.optimizely.ab.notification; + +import com.optimizely.ab.event.LogEvent; +import org.junit.Before; +import org.junit.Test; + +import java.util.Collections; +import java.util.Map; + +import static org.junit.Assert.*; + +public class TrackNotificationTest { + + private static final String EVENT_KEY = "eventKey"; + private static final String USER_ID = "userID"; + private static final Map USER_ATTRIBUTES = Collections.singletonMap("user", "attr"); + private static final Map EVENT_TAGS = Collections.singletonMap("event", "tag"); + private static final LogEvent LOG_EVENT = new LogEvent( + LogEvent.RequestMethod.POST, + "endpoint", + Collections.emptyMap(), + null + ); + + private TrackNotification trackNotification; + + @Before + public void setUp() throws Exception { + trackNotification = new TrackNotification(EVENT_KEY, USER_ID, USER_ATTRIBUTES, EVENT_TAGS, LOG_EVENT); + } + + @Test + public void testGetEventKey() { + assertEquals(EVENT_KEY, trackNotification.getEventKey()); + } + + @Test + public void testGetUserId() { + assertEquals(USER_ID, trackNotification.getUserId()); + } + + @Test + public void testGetAttributes() { + assertEquals(USER_ATTRIBUTES, trackNotification.getAttributes()); + } + + @Test + public void testGetEventTags() { + assertEquals(EVENT_TAGS, trackNotification.getEventTags()); + } + + @Test + public void testGetEvent() { + assertEquals(LOG_EVENT, trackNotification.getEvent()); + } + + @Test + public void testToString() { + assertEquals("TrackNotification{eventKey='eventKey', userId='userID', attributes={user=attr}, eventTags={event=tag}, event=LogEvent{requestMethod=POST, endpointUrl='endpoint', requestParams={}, body=''}}", trackNotification.toString()); + } +} diff --git a/gradle.properties b/gradle.properties index c95091dad..505d5a83e 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Maven version -version = 2.0.0-SNAPSHOT +version = 2.0.0-test # Artifact paths mavenS3Bucket = optimizely-maven