You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by jo...@apache.org on 2014/09/23 18:02:25 UTC

git commit: AMBARI-7443 - Alerts: Implement Email Dispatcher (jonathanhurley)

Repository: ambari
Updated Branches:
  refs/heads/branch-alerts-dev e6cc05872 -> d838ca95b


AMBARI-7443 - Alerts: Implement Email Dispatcher (jonathanhurley)


Project: http://git-wip-us.apache.org/repos/asf/ambari/repo
Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/d838ca95
Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/d838ca95
Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/d838ca95

Branch: refs/heads/branch-alerts-dev
Commit: d838ca95b644a3222c27b7415f2711373affcd83
Parents: e6cc058
Author: Jonathan Hurley <jh...@hortonworks.com>
Authored: Mon Sep 22 23:45:06 2014 -0700
Committer: Jonathan Hurley <jh...@hortonworks.com>
Committed: Tue Sep 23 09:02:11 2014 -0700

----------------------------------------------------------------------
 .../internal/AlertTargetResourceProvider.java   |  15 +-
 .../server/events/AlertStateChangeEvent.java    |   2 +-
 .../events/listeners/AlertReceivedListener.java |  46 +++--
 .../listeners/AlertServiceStateListener.java    |   1 -
 .../notifications/DispatchCredentials.java      |  44 +++++
 .../server/notifications/DispatchFactory.java   |  16 ++
 .../server/notifications/Notification.java      |  18 ++
 .../ambari/server/notifications/Recipient.java  |  45 +++++
 .../dispatchers/EmailDispatcher.java            | 129 +++++++++++++-
 .../apache/ambari/server/orm/dao/AlertsDAO.java | 114 +++++++-----
 .../server/orm/entities/AlertCurrentEntity.java |  13 +-
 .../services/AlertNoticeDispatchService.java    | 175 ++++++++++++++++++-
 .../stacks/HDP/2.0.6/services/HDFS/alerts.json  |   2 +-
 .../AlertTargetResourceProviderTest.java        |  58 ++++++
 .../notifications/EmailDispatcherTest.java      | 117 +++++++++++++
 .../server/notifications/MockDispatcher.java    |  46 +++++
 .../ambari/server/orm/dao/AlertsDAOTest.java    | 133 +++++++++-----
 .../alerts/AlertStateChangedEventTest.java      | 139 +++++++++++++++
 18 files changed, 984 insertions(+), 129 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProvider.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProvider.java b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProvider.java
index f2b82d6..b0cc52b 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProvider.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProvider.java
@@ -20,6 +20,7 @@ package org.apache.ambari.server.controller.internal;
 import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
@@ -45,7 +46,6 @@ import org.apache.ambari.server.state.alert.AlertTarget;
 import org.apache.commons.lang.StringUtils;
 
 import com.google.gson.Gson;
-import com.google.gson.JsonObject;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 
@@ -351,22 +351,19 @@ public class AlertTargetResourceProvider extends
    *         {@code null} if none.
    */
   private String extractProperties( Map<String, Object> requestMap ){
-    JsonObject jsonObject = new JsonObject();
+    Map<String, Object> normalizedMap = new HashMap<String, Object>(
+        requestMap.size());
+
     for (Entry<String, Object> entry : requestMap.entrySet()) {
       String key = entry.getKey();
       String propCat = PropertyHelper.getPropertyCategory(key);
 
       if (propCat.equals(ALERT_TARGET_PROPERTIES)) {
         String propKey = PropertyHelper.getPropertyName(key);
-        jsonObject.addProperty(propKey, entry.getValue().toString());
+        normalizedMap.put(propKey, entry.getValue());
       }
     }
 
-    String properties = null;
-    if (jsonObject.entrySet().size() > 0) {
-      properties = jsonObject.toString();
-    }
-
-    return properties;
+    return s_gson.toJson(normalizedMap);
   }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/events/AlertStateChangeEvent.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/events/AlertStateChangeEvent.java b/ambari-server/src/main/java/org/apache/ambari/server/events/AlertStateChangeEvent.java
index ab2c3dd..efb7119 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/events/AlertStateChangeEvent.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/events/AlertStateChangeEvent.java
@@ -25,7 +25,7 @@ import org.apache.ambari.server.state.AlertState;
  * The {@link AlertStateChangeEvent} is fired when an {@link Alert} instance has
  * its {@link AlertState} changed.
  */
-public final class AlertStateChangeEvent extends AlertEvent {
+public class AlertStateChangeEvent extends AlertEvent {
 
   /**
    * The prior alert state.

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertReceivedListener.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertReceivedListener.java b/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertReceivedListener.java
index fb7a608..e87ba7d 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertReceivedListener.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertReceivedListener.java
@@ -28,8 +28,8 @@ import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
 import org.apache.ambari.server.orm.entities.AlertHistoryEntity;
 import org.apache.ambari.server.state.Alert;
 import org.apache.ambari.server.state.AlertState;
-import org.apache.commons.logging.Log;
-import org.apache.commons.logging.LogFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.google.common.eventbus.AllowConcurrentEvents;
 import com.google.common.eventbus.Subscribe;
@@ -46,7 +46,7 @@ public class AlertReceivedListener {
   /**
    * Logger.
    */
-  private static Log LOG = LogFactory.getLog(AlertReceivedListener.class);
+  private static final Logger LOG = LoggerFactory.getLogger(AlertReceivedListener.class);
 
   @Inject
   private AlertsDAO m_alertsDao;
@@ -70,30 +70,33 @@ public class AlertReceivedListener {
     m_alertEventPublisher.register(this);
   }
 
-
   /**
-   * Adds an alert.  Checks for a new state before creating a new history record.
+   * Adds an alert. Checks for a new state before creating a new history record.
    *
-   * @param clusterId the id for the cluster
-   * @param alert the alert to add
+   * @param clusterId
+   *          the id for the cluster
+   * @param alert
+   *          the alert to add
    */
   @Subscribe
   @AllowConcurrentEvents
   public void onAlertEvent(AlertReceivedEvent event) {
-    LOG.debug(event);
+    if (LOG.isDebugEnabled()) {
+      LOG.debug(event.toString());
+    }
 
     long clusterId = event.getClusterId();
     Alert alert = event.getAlert();
 
     AlertCurrentEntity current = null;
-    
+
     if (null == alert.getHost()) {
       current = m_alertsDao.findCurrentByNameNoHost(clusterId, alert.getName());
     } else {
       current = m_alertsDao.findCurrentByHostAndName(clusterId, alert.getHost(),
           alert.getName());
     }
-    
+
     if (null == current) {
       AlertDefinitionEntity definition = m_definitionDao.findByName(clusterId,
           alert.getName());
@@ -111,13 +114,20 @@ public class AlertReceivedListener {
       current.setLatestTimestamp(Long.valueOf(alert.getTimestamp()));
       current.setLatestText(alert.getText());
 
-      m_alertsDao.merge(current);
+      current = m_alertsDao.merge(current);
     } else {
-      AlertState oldState = current.getAlertHistory().getAlertState();
+      LOG.debug(
+          "Alert State Changed: CurrentId {}, CurrentTimestamp {}, HistoryId {}, HistoryState {}",
+          current.getAlertId(), current.getLatestTimestamp(),
+          current.getAlertHistory().getAlertId(),
+          current.getAlertHistory().getAlertState());
+
+      AlertHistoryEntity oldHistory = current.getAlertHistory();
+      AlertState oldState = oldHistory.getAlertState();
 
       // insert history, update current
       AlertHistoryEntity history = createHistory(clusterId,
-          current.getAlertHistory().getAlertDefinition(), alert);
+          oldHistory.getAlertDefinition(), alert);
 
       // manually create the new history entity since we are merging into
       // an existing current entity
@@ -127,8 +137,14 @@ public class AlertReceivedListener {
       current.setLatestTimestamp(Long.valueOf(alert.getTimestamp()));
       current.setOriginalTimestamp(Long.valueOf(alert.getTimestamp()));
 
-      m_alertsDao.merge(current);
-      
+      current = m_alertsDao.merge(current);
+
+      LOG.debug(
+          "Alert State Merged: CurrentId {}, CurrentTimestamp {}, HistoryId {}, HistoryState {}",
+          current.getAlertId(), current.getLatestTimestamp(),
+          current.getAlertHistory().getAlertId(),
+          current.getAlertHistory().getAlertState());
+
       // broadcast the alert changed event for other subscribers
       AlertStateChangeEvent alertChangedEvent = new AlertStateChangeEvent(
           event.getClusterId(), event.getAlert(), current.getAlertHistory(),

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertServiceStateListener.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertServiceStateListener.java b/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertServiceStateListener.java
index f1ce617..6215cc1 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertServiceStateListener.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/events/listeners/AlertServiceStateListener.java
@@ -94,7 +94,6 @@ public class AlertServiceStateListener {
     publisher.register(this);
   }
 
-
   /**
    * Handles service installed events by populating the database with all known
    * alert definitions for the newly installed service and creates the service's

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchCredentials.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchCredentials.java b/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchCredentials.java
new file mode 100644
index 0000000..9514474
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchCredentials.java
@@ -0,0 +1,44 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ambari.server.notifications;
+
+/**
+ * The {@link DispatchCredentials} represent a generic username/password model
+ * that can be passed to a {@link NotificationDispatcher} in order to authentice
+ * with a backend dispatcher.
+ */
+public class DispatchCredentials {
+
+  /**
+   * The username.
+   */
+  public String UserName;
+
+  /**
+   * The password.
+   */
+  public String Password;
+
+  /**
+   * Constructor.
+   *
+   */
+  public DispatchCredentials() {
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java b/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java
index 3414035..13f2da2 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/notifications/DispatchFactory.java
@@ -50,6 +50,22 @@ public final class DispatchFactory {
   }
 
   /**
+   * Registers a dispatcher instance with a type.
+   *
+   * @param type
+   *          the type
+   * @param dispatcher
+   *          the dispatcher to register with the type.
+   */
+  public void register(String type, NotificationDispatcher dispatcher) {
+    if (null == dispatcher) {
+      m_dispatchers.remove(type);
+    } else {
+      m_dispatchers.put(type, dispatcher);
+    }
+  }
+
+  /**
    * Gets a dispatcher based on the type.
    *
    * @param type

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/notifications/Notification.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/notifications/Notification.java b/ambari-server/src/main/java/org/apache/ambari/server/notifications/Notification.java
index 08c5242..12dffd7 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/notifications/Notification.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/notifications/Notification.java
@@ -18,6 +18,7 @@
 package org.apache.ambari.server.notifications;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * The {@link Notification} class is a generic way to relay content through an
@@ -36,6 +37,23 @@ public class Notification {
   public String Body;
 
   /**
+   * The optional recipients of the notification. Some dispatchers may not
+   * require explicit recipients.
+   */
+  public List<Recipient> Recipients;
+
+  /**
+   * A map of all of the properties that a {@link NotificationDispatcher} needs
+   * in order to dispatch this notification.
+   */
+  public Map<String, String> DispatchProperties;
+
+  /**
+   * The optional credentials used to authenticate with the dispatcher.
+   */
+  public DispatchCredentials Credentials;
+
+  /**
    * An optional callback implementation that the dispatcher can use to report
    * success/failure on delivery.
    */

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/notifications/Recipient.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/notifications/Recipient.java b/ambari-server/src/main/java/org/apache/ambari/server/notifications/Recipient.java
new file mode 100644
index 0000000..933038b
--- /dev/null
+++ b/ambari-server/src/main/java/org/apache/ambari/server/notifications/Recipient.java
@@ -0,0 +1,45 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ambari.server.notifications;
+
+/**
+ * The {@link Recipient} class represents a target of a {@link Notification}.
+ */
+public class Recipient {
+
+  /**
+   * A string that the concrete {@link NotificationDispatcher} can use to build
+   * a backend recipient that will work with the dispatch mechanism.
+   */
+  public String Identifier;
+
+  /**
+   * Constructor.
+   *
+   */
+  public Recipient() {
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String toString() {
+    return Identifier;
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java b/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java
index d0858d3..a5dad84 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/notifications/dispatchers/EmailDispatcher.java
@@ -17,8 +17,24 @@
  */
 package org.apache.ambari.server.notifications.dispatchers;
 
-import org.apache.ambari.server.notifications.NotificationDispatcher;
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.Timer;
+
+import javax.mail.Authenticator;
+import javax.mail.Message;
+import javax.mail.Message.RecipientType;
+import javax.mail.MessagingException;
+import javax.mail.PasswordAuthentication;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+
+import org.apache.ambari.server.notifications.DispatchCredentials;
 import org.apache.ambari.server.notifications.Notification;
+import org.apache.ambari.server.notifications.NotificationDispatcher;
+import org.apache.ambari.server.notifications.Recipient;
 import org.apache.ambari.server.state.alert.TargetType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -27,7 +43,14 @@ import com.google.inject.Singleton;
 
 /**
  * The {@link EmailDispatcher} class is used to dispatch {@link Notification}
- * via JavaMail.
+ * via JavaMail. This class currently does not attempt to reuse an existing
+ * {@link Session} or {@link Transport} since each {@link Notification} could
+ * have a different target server with different properties.
+ * <p/>
+ * In the future, this class could keep various {@link Transport} instances open
+ * on a {@link Timer}, but those instances would need to be hashed so that the
+ * proper instance is retrieved from the properties of the incoming
+ * {@link Notification}.
  */
 @Singleton
 public class EmailDispatcher implements NotificationDispatcher {
@@ -52,9 +75,105 @@ public class EmailDispatcher implements NotificationDispatcher {
   public void dispatch(Notification notification) {
     LOG.info("Sending email: {}", notification);
 
-    // callback to inform the interested parties about the successful dispatch
-    if (null != notification.Callback) {
-      notification.Callback.onSuccess(notification.CallbackIds);
+    if (null == notification.DispatchProperties) {
+      LOG.error("Unable to dispatch an email notification that does not contain SMTP properties");
+
+      if (null != notification.Callback) {
+        notification.Callback.onFailure(notification.CallbackIds);
+      }
+
+      return;
+    }
+
+    // convert properties to JavaMail properties
+    Properties properties = new Properties();
+    for (Entry<String, String> entry : notification.DispatchProperties.entrySet()) {
+      properties.put(entry.getKey(), entry.getValue());
+    }
+
+    // notifications must have recipients
+    if (null == notification.Recipients) {
+      LOG.error("Unable to dispatch an email notification that does not have recipients");
+
+      if (null != notification.Callback) {
+        notification.Callback.onFailure(notification.CallbackIds);
+      }
+
+      return;
+    }
+
+    // create a simple email authentication for username/password
+    final Session session;
+    EmailAuthenticator authenticator = null;
+
+    if (null != notification.Credentials) {
+      authenticator = new EmailAuthenticator(notification.Credentials);
+    }
+
+    session = Session.getInstance(properties, authenticator);
+
+    try {
+      Message message = new MimeMessage(session);
+
+      for (Recipient recipient : notification.Recipients) {
+        InternetAddress address = new InternetAddress(recipient.Identifier);
+        message.addRecipient(RecipientType.TO, address);
+      }
+
+      message.setSubject(notification.Subject);
+      message.setText(notification.Body);
+
+      Transport.send(message);
+
+      if (LOG.isDebugEnabled()) {
+        LOG.debug("Successfully dispatched email to {}",
+            notification.Recipients);
+      }
+
+      // callback to inform the interested parties about the successful dispatch
+      if (null != notification.Callback) {
+        notification.Callback.onSuccess(notification.CallbackIds);
+      }
+    } catch (Exception exception) {
+      LOG.error("Unable to dispatch notification via Email", exception);
+
+      // callback failure
+      if (null != notification.Callback) {
+        notification.Callback.onFailure(notification.CallbackIds);
+      }
+    } finally {
+      try {
+        session.getTransport().close();
+      } catch (MessagingException me) {
+        LOG.warn("Dispatcher unable to close SMTP transport", me);
+      }
+    }
+  }
+
+  /**
+   * The {@link EmailAuthenticator} class is used to provide a username and
+   * password combination to an SMTP server.
+   */
+  private static final class EmailAuthenticator extends Authenticator{
+
+    private final DispatchCredentials m_credentials;
+
+    /**
+     * Constructor.
+     *
+     * @param credentials
+     */
+    private EmailAuthenticator(DispatchCredentials credentials) {
+      m_credentials = credentials;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected PasswordAuthentication getPasswordAuthentication() {
+      return new PasswordAuthentication(m_credentials.UserName,
+          m_credentials.Password);
     }
   }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/AlertsDAO.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/AlertsDAO.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/AlertsDAO.java
index aba41a5..a28b448 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/AlertsDAO.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/dao/AlertsDAO.java
@@ -30,6 +30,8 @@ import org.apache.ambari.server.orm.entities.AlertCurrentEntity;
 import org.apache.ambari.server.orm.entities.AlertHistoryEntity;
 import org.apache.ambari.server.state.AlertState;
 import org.apache.ambari.server.state.alert.Scope;
+import org.eclipse.persistence.config.HintValues;
+import org.eclipse.persistence.config.QueryHints;
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -57,7 +59,7 @@ public class AlertsDAO {
 
   /**
    * Gets an alert with the specified ID.
-   * 
+   *
    * @param alertId
    *          the ID of the alert to retrieve.
    * @return the alert or {@code null} if none exists.
@@ -68,7 +70,7 @@ public class AlertsDAO {
 
   /**
    * Gets all alerts stored in the database across all clusters.
-   * 
+   *
    * @return all alerts or an empty list if none exist (never {@code null}).
    */
   public List<AlertHistoryEntity> findAll() {
@@ -80,7 +82,7 @@ public class AlertsDAO {
 
   /**
    * Gets all alerts stored in the database for the given cluster.
-   * 
+   *
    * @param clusterId
    *          the ID of the cluster.
    * @return all alerts in the specified cluster or an empty list if none exist
@@ -98,7 +100,7 @@ public class AlertsDAO {
   /**
    * Gets all alerts stored in the database for the given cluster that have one
    * of the specified alert states.
-   * 
+   *
    * @param clusterId
    *          the ID of the cluster.
    * @param alertStates
@@ -126,7 +128,7 @@ public class AlertsDAO {
    * Gets all alerts stored in the database for the given cluster and that fall
    * withing the specified date range. Dates are expected to be in milliseconds
    * since the epoch, normalized to UTC time.
-   * 
+   *
    * @param clusterId
    *          the ID of the cluster.
    * @param startDate
@@ -141,8 +143,9 @@ public class AlertsDAO {
    */
   public List<AlertHistoryEntity> findAll(long clusterId, Date startDate,
       Date endDate) {
-    if (null == startDate && null == endDate)
+    if (null == startDate && null == endDate) {
       return Collections.emptyList();
+    }
 
     TypedQuery<AlertHistoryEntity> query = null;
 
@@ -174,15 +177,16 @@ public class AlertsDAO {
       query.setParameter("beforeDate", endDate.getTime());
     }
 
-    if (null == query)
+    if (null == query) {
       return Collections.emptyList();
+    }
 
     return daoUtils.selectList(query);
   }
 
   /**
    * Gets the current alerts.
-   * 
+   *
    * @return the current alerts or an empty list if none exist (never
    *         {@code null}).
    */
@@ -196,7 +200,7 @@ public class AlertsDAO {
 
   /**
    * Gets a current alert with the specified ID.
-   * 
+   *
    * @param alertId
    *          the ID of the alert to retrieve.
    * @return the alert or {@code null} if none exists.
@@ -208,7 +212,7 @@ public class AlertsDAO {
 
   /**
    * Gets the current alerts for a given cluster.
-   * 
+   *
    * @return the current alerts for the given clusteror an empty list if none
    *         exist (never {@code null}).
    */
@@ -218,14 +222,15 @@ public class AlertsDAO {
         "AlertCurrentEntity.findByCluster", AlertCurrentEntity.class);
 
     query.setParameter("clusterId", Long.valueOf(clusterId));
+    query = setQueryRefreshHint(query);
 
     return daoUtils.selectList(query);
   }
-  
+
   /**
    * Retrieves the summary information for a particular scope.  The result is a DTO
    * since the columns are aggregated and don't fit to an entity.
-   * 
+   *
    * @param clusterId the cluster id
    * @param serviceName the service name. Use {@code null} to not filter on service.
    * @param hostName the host name.  Use {@code null} to not filter on host.
@@ -240,41 +245,41 @@ public class AlertsDAO {
     sb.append("SUM(CASE WHEN history.alertState = %s.%s THEN 1 ELSE 0 END), ");
     sb.append("SUM(CASE WHEN history.alertState = %s.%s THEN 1 ELSE 0 END)) ");
     sb.append("FROM AlertCurrentEntity alert JOIN alert.alertHistory history WHERE history.clusterId = :clusterId");
-    
+
     if (null != serviceName) {
       sb.append(" AND history.serviceName = :serviceName");
     }
-    
+
     if (null != hostName) {
       sb.append(" AND history.hostName = :hostName");
     }
-    
+
     String str = String.format(sb.toString(),
         AlertSummaryDTO.class.getName(),
         AlertState.class.getName(), AlertState.OK.name(),
         AlertState.class.getName(), AlertState.WARNING.name(),
         AlertState.class.getName(), AlertState.CRITICAL.name(),
         AlertState.class.getName(), AlertState.UNKNOWN.name());
-    
+
     TypedQuery<AlertSummaryDTO> query = entityManagerProvider.get().createQuery(
         str, AlertSummaryDTO.class);
-    
+
     query.setParameter("clusterId", Long.valueOf(clusterId));
-    
+
     if (null != serviceName) {
       query.setParameter("serviceName", serviceName);
     }
-    
+
     if (null != hostName) {
       query.setParameter("hostName", hostName);
     }
-    
+
     return daoUtils.selectSingle(query);
   }
-  
+
   /**
    * Gets the current alerts for a given service.
-   * 
+   *
    * @return the current alerts for the given service or an empty list if none
    *         exist (never {@code null}).
    */
@@ -288,12 +293,13 @@ public class AlertsDAO {
     query.setParameter("serviceName", serviceName);
     query.setParameter("inlist", EnumSet.of(Scope.ANY, Scope.SERVICE));
 
+    query = setQueryRefreshHint(query);
     return daoUtils.selectList(query);
   }
 
   /**
    * Gets the current alerts for a given host.
-   * 
+   *
    * @return the current alerts for the given host or an empty list if none
    *         exist (never {@code null}).
    */
@@ -307,13 +313,14 @@ public class AlertsDAO {
     query.setParameter("hostName", hostName);
     query.setParameter("inlist", EnumSet.of(Scope.ANY, Scope.HOST));
 
+    query = setQueryRefreshHint(query);
     return daoUtils.selectList(query);
   }
-  
+
   @RequiresSession
   public AlertCurrentEntity findCurrentByHostAndName(long clusterId, String hostName,
       String alertName) {
-    
+
     TypedQuery<AlertCurrentEntity> query = entityManagerProvider.get().createNamedQuery(
         "AlertCurrentEntity.findByHostAndName", AlertCurrentEntity.class);
 
@@ -321,6 +328,7 @@ public class AlertsDAO {
     query.setParameter("hostName", hostName);
     query.setParameter("definitionName", alertName);
 
+    query = setQueryRefreshHint(query);
     return daoUtils.selectOne(query);
   }
 
@@ -328,7 +336,7 @@ public class AlertsDAO {
    * Removes alert history and current alerts for the specified alert defintiion
    * ID. This will invoke {@link EntityManager#clear()} when completed since the
    * JPQL statement will remove entries without going through the EM.
-   * 
+   *
    * @param definitionId
    *          the ID of the definition to remove.
    */
@@ -352,7 +360,7 @@ public class AlertsDAO {
 
   /**
    * Remove a current alert whose history entry matches the specfied ID.
-   * 
+   *
    * @param   historyId the ID of the history entry.
    * @return  the number of alerts removed.
    */
@@ -367,7 +375,7 @@ public class AlertsDAO {
 
   /**
    * Persists a new alert.
-   * 
+   *
    * @param alert
    *          the alert to persist (not {@code null}).
    */
@@ -378,7 +386,7 @@ public class AlertsDAO {
 
   /**
    * Refresh the state of the alert from the database.
-   * 
+   *
    * @param alert
    *          the alert to refresh (not {@code null}).
    */
@@ -389,7 +397,7 @@ public class AlertsDAO {
 
   /**
    * Merge the speicified alert with the existing alert in the database.
-   * 
+   *
    * @param alert
    *          the alert to merge (not {@code null}).
    * @return the updated alert with merged content (never {@code null}).
@@ -401,7 +409,7 @@ public class AlertsDAO {
 
   /**
    * Removes the specified alert from the database.
-   * 
+   *
    * @param alert
    *          the alert to remove.
    */
@@ -415,7 +423,7 @@ public class AlertsDAO {
 
   /**
    * Persists a new current alert.
-   * 
+   *
    * @param alert
    *          the current alert to persist (not {@code null}).
    */
@@ -426,7 +434,7 @@ public class AlertsDAO {
 
   /**
    * Refresh the state of the current alert from the database.
-   * 
+   *
    * @param alert
    *          the current alert to refresh (not {@code null}).
    */
@@ -437,7 +445,7 @@ public class AlertsDAO {
 
   /**
    * Merge the speicified current alert with the existing alert in the database.
-   * 
+   *
    * @param alert
    *          the current alert to merge (not {@code null}).
    * @return the updated current alert with merged content (never {@code null}).
@@ -449,7 +457,7 @@ public class AlertsDAO {
 
   /**
    * Removes the specified current alert from the database.
-   * 
+   *
    * @param alert
    *          the current alert to remove.
    */
@@ -457,9 +465,9 @@ public class AlertsDAO {
   public void remove(AlertCurrentEntity alert) {
     entityManagerProvider.get().remove(merge(alert));
   }
-  
+
   /**
-   * Finds the aggregate counts for an alert name, across all hosts. 
+   * Finds the aggregate counts for an alert name, across all hosts.
    * @param clusterId the cluster id
    * @param alertName the name of the alert to find the aggregate
    * @return the summary data
@@ -474,22 +482,22 @@ public class AlertsDAO {
     sb.append("SUM(CASE WHEN history.alertState = %s.%s THEN 1 ELSE 0 END)) ");
     sb.append("FROM AlertCurrentEntity alert JOIN alert.alertHistory history WHERE history.clusterId = :clusterId");
     sb.append(" AND history.alertDefinition.definitionName = :definitionName");
-    
+
     String str = String.format(sb.toString(),
         AlertSummaryDTO.class.getName(),
         AlertState.class.getName(), AlertState.WARNING.name(),
         AlertState.class.getName(), AlertState.CRITICAL.name(),
         AlertState.class.getName(), AlertState.UNKNOWN.name());
-    
+
     TypedQuery<AlertSummaryDTO> query = entityManagerProvider.get().createQuery(
         str, AlertSummaryDTO.class);
-    
+
     query.setParameter("clusterId", Long.valueOf(clusterId));
     query.setParameter("definitionName", alertName);
-    
-    return daoUtils.selectSingle(query);    
+
+    return daoUtils.selectSingle(query);
   }
-  
+
   /**
    * Locate the current alert for the provided service and alert name, but when
    * host is not set ({@code IS NULL}).
@@ -500,7 +508,7 @@ public class AlertsDAO {
    */
   @RequiresSession
   public AlertCurrentEntity findCurrentByNameNoHost(long clusterId, String alertName) {
-    
+
     TypedQuery<AlertCurrentEntity> query = entityManagerProvider.get().createNamedQuery(
         "AlertCurrentEntity.findByNameAndNoHost", AlertCurrentEntity.class);
 
@@ -510,4 +518,22 @@ public class AlertsDAO {
     return daoUtils.selectOne(query);
   }
 
+  /**
+   * Sets {@link QueryHints#REFRESH} on the specified query so that child
+   * entities are not stale.
+   * <p/>
+   * See <a
+   * href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=398067">https://bugs
+   * .eclipse.org/bugs/show_bug.cgi?id=398067</a>
+   *
+   * @param query
+   * @return
+   */
+  private <T> TypedQuery<T> setQueryRefreshHint(TypedQuery<T> query) {
+    // !!! https://bugs.eclipse.org/bugs/show_bug.cgi?id=398067
+    // ensure that an associated entity with a JOIN is not stale; this causes
+    // the associated AlertHistoryEntity to be stale
+    query.setHint(QueryHints.REFRESH, HintValues.TRUE);
+    return query;
+  }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/AlertCurrentEntity.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/AlertCurrentEntity.java b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/AlertCurrentEntity.java
index 5b54d57..8ca297b 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/AlertCurrentEntity.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/orm/entities/AlertCurrentEntity.java
@@ -47,11 +47,11 @@ import org.apache.ambari.server.state.MaintenanceState;
 @TableGenerator(name = "alert_current_id_generator", table = "ambari_sequences", pkColumnName = "sequence_name", valueColumnName = "sequence_value", pkColumnValue = "alert_current_id_seq", initialValue = 0, allocationSize = 1)
 @NamedQueries({
     @NamedQuery(name = "AlertCurrentEntity.findAll", query = "SELECT alert FROM AlertCurrentEntity alert"),
-    @NamedQuery(name = "AlertCurrentEntity.findByCluster", query = "SELECT alert FROM AlertCurrentEntity alert JOIN alert.alertHistory history WHERE history.clusterId = :clusterId"),
-    @NamedQuery(name = "AlertCurrentEntity.findByService", query = "SELECT alert FROM AlertCurrentEntity alert JOIN alert.alertHistory history WHERE history.clusterId = :clusterId AND history.serviceName = :serviceName AND history.alertDefinition.scope IN :inlist"),
-    @NamedQuery(name = "AlertCurrentEntity.findByHost", query = "SELECT alert FROM AlertCurrentEntity alert JOIN alert.alertHistory history WHERE history.clusterId = :clusterId AND history.hostName = :hostName AND history.alertDefinition.scope IN :inlist"),
-    @NamedQuery(name = "AlertCurrentEntity.findByHostAndName", query = "SELECT alert FROM AlertCurrentEntity alert JOIN alert.alertHistory history WHERE history.clusterId = :clusterId AND history.alertDefinition.definitionName = :definitionName AND history.hostName = :hostName"),
-    @NamedQuery(name = "AlertCurrentEntity.findByNameAndNoHost", query = "SELECT alert FROM AlertCurrentEntity alert JOIN alert.alertHistory history WHERE history.clusterId = :clusterId AND history.alertDefinition.definitionName = :definitionName AND history.hostName IS NULL"),
+    @NamedQuery(name = "AlertCurrentEntity.findByCluster", query = "SELECT alert FROM AlertCurrentEntity alert WHERE alert.alertHistory.clusterId = :clusterId"),
+    @NamedQuery(name = "AlertCurrentEntity.findByService", query = "SELECT alert FROM AlertCurrentEntity alert WHERE alert.alertHistory.clusterId = :clusterId AND alert.alertHistory.serviceName = :serviceName AND alert.alertHistory.alertDefinition.scope IN :inlist"),
+    @NamedQuery(name = "AlertCurrentEntity.findByHost", query = "SELECT alert FROM AlertCurrentEntity alert WHERE alert.alertHistory.clusterId = :clusterId AND alert.alertHistory.hostName = :hostName AND alert.alertHistory.alertDefinition.scope IN :inlist"),
+    @NamedQuery(name = "AlertCurrentEntity.findByHostAndName", query = "SELECT alert FROM AlertCurrentEntity alert WHERE alert.alertHistory.clusterId = :clusterId AND alert.alertHistory.alertDefinition.definitionName = :definitionName AND alert.alertHistory.hostName = :hostName"),
+    @NamedQuery(name = "AlertCurrentEntity.findByNameAndNoHost", query = "SELECT alert FROM AlertCurrentEntity alert WHERE alert.alertHistory.clusterId = :clusterId AND alert.alertHistory.alertDefinition.definitionName = :definitionName AND alert.alertHistory.hostName IS NULL"),
     @NamedQuery(name = "AlertCurrentEntity.removeByHistoryId", query = "DELETE FROM AlertCurrentEntity alert WHERE alert.alertHistory.alertId = :historyId"),
     @NamedQuery(name = "AlertCurrentEntity.removeByDefinitionId", query = "DELETE FROM AlertCurrentEntity alert WHERE alert.alertDefinition.definitionId = :definitionId") })
 public class AlertCurrentEntity {
@@ -77,8 +77,7 @@ public class AlertCurrentEntity {
   /**
    * Unidirectional one-to-one association to {@link AlertHistoryEntity}
    */
-  @OneToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE,
-      CascadeType.REFRESH })
+  @OneToOne(cascade = { CascadeType.PERSIST, CascadeType.REFRESH })
   @JoinColumn(name = "history_id", unique = true, nullable = false)
   private AlertHistoryEntity alertHistory;
 

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java b/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java
index 7025e14..72487b3 100644
--- a/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java
+++ b/ambari-server/src/main/java/org/apache/ambari/server/state/services/AlertNoticeDispatchService.java
@@ -17,10 +17,13 @@
  */
 package org.apache.ambari.server.state.services;
 
+import java.lang.reflect.Type;
+import java.text.MessageFormat;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.ThreadFactory;
@@ -29,19 +32,32 @@ import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicInteger;
 
 import org.apache.ambari.server.events.AlertEvent;
+import org.apache.ambari.server.notifications.DispatchCallback;
+import org.apache.ambari.server.notifications.DispatchCredentials;
 import org.apache.ambari.server.notifications.DispatchFactory;
 import org.apache.ambari.server.notifications.DispatchRunnable;
-import org.apache.ambari.server.notifications.DispatchCallback;
-import org.apache.ambari.server.notifications.NotificationDispatcher;
 import org.apache.ambari.server.notifications.Notification;
+import org.apache.ambari.server.notifications.NotificationDispatcher;
+import org.apache.ambari.server.notifications.Recipient;
 import org.apache.ambari.server.orm.dao.AlertDispatchDAO;
+import org.apache.ambari.server.orm.entities.AlertHistoryEntity;
 import org.apache.ambari.server.orm.entities.AlertNoticeEntity;
 import org.apache.ambari.server.orm.entities.AlertTargetEntity;
+import org.apache.ambari.server.state.AlertState;
 import org.apache.ambari.server.state.NotificationState;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.util.concurrent.AbstractScheduledService;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonDeserializationContext;
+import com.google.gson.JsonDeserializer;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -49,6 +65,10 @@ import com.google.inject.Singleton;
  * The {@link AlertNoticeDispatchService} is used to scan the database for
  * {@link AlertNoticeEntity} that are in the {@link NotificationState#PENDING}.
  * It will then process them through the dispatch system.
+ * <p/>
+ * The dispatch system will then make a callback to
+ * {@link AlertNoticeDispatchCallback} so that the {@link NotificationState} can
+ * be updated to its final value.
  */
 @Singleton
 public class AlertNoticeDispatchService extends AbstractScheduledService {
@@ -59,6 +79,26 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
   private static final Logger LOG = LoggerFactory.getLogger(AlertNoticeDispatchService.class);
 
   /**
+   * The property containing the dispatch authentication username.
+   */
+  private static final String AMBARI_DISPATCH_CREDENTIAL_USERNAME = "ambari.dispatch.credential.username";
+
+  /**
+   * The property containing the dispatch authentication password.
+   */
+  private static final String AMBARI_DISPATCH_CREDENTIAL_PASSWORD = "ambari.dispatch.credential.password";
+
+  /**
+   * The property containing the dispatch recipients
+   */
+  private static final String AMBARI_DISPATCH_RECIPIENTS = "ambari.dispatch.recipients";
+
+  /**
+   * Gson used to convert JSON properties to a map.
+   */
+  private final Gson m_gson;
+
+  /**
    * Dispatch DAO to query pending {@link AlertNoticeEntity} instances from.
    */
   @Inject
@@ -84,6 +124,12 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
         TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>(),
         new AlertDispatchThreadFactory(),
         new ThreadPoolExecutor.CallerRunsPolicy());
+
+    GsonBuilder gsonBuilder = new GsonBuilder();
+    gsonBuilder.registerTypeAdapter(AlertTargetProperties.class,
+        new AlertTargetPropertyDeserializer());
+
+    m_gson = gsonBuilder.create();
   }
 
   /**
@@ -122,16 +168,82 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
         continue;
       }
 
+      String propertiesJson = target.getProperties();
+      AlertTargetProperties targetProperties = m_gson.fromJson(propertiesJson,
+          AlertTargetProperties.class);
+
+      Map<String, String> properties = targetProperties.Properties;
+
       Notification notification = new Notification();
-      notification.Subject = target.getTargetName();
-      notification.Body = target.getDescription();
       notification.Callback = new AlertNoticeDispatchCallback();
       notification.CallbackIds = new ArrayList<String>(notices.size());
 
+      // !!! FIXME: temporary until velocity templates are implemented
+      String subject = "OK ({0}), Warning ({1}), Critical ({2})";
+      StringBuilder buffer = new StringBuilder(512);
+
+      int okCount = 0;
+      int warningCount = 0;
+      int criticalCount = 0;
+
       for (AlertNoticeEntity notice : notices) {
+        AlertHistoryEntity history = notice.getAlertHistory();
         notification.CallbackIds.add(notice.getUuid());
+
+        AlertState alertState = history.getAlertState();
+        switch (alertState) {
+          case CRITICAL:
+            criticalCount++;
+            break;
+          case OK:
+            okCount++;
+            break;
+          case UNKNOWN:
+            // !!! hmmmmmm
+            break;
+          case WARNING:
+            warningCount++;
+            break;
+          default:
+            break;
+        }
+
+        buffer.append(history.getAlertLabel());
+        buffer.append(": ");
+        buffer.append(history.getAlertText());
+        buffer.append("\n");
       }
 
+      notification.Subject = MessageFormat.format(subject, okCount,
+          warningCount, criticalCount);
+
+      notification.Body = buffer.toString();
+
+      // set dispatch credentials
+      if (properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_USERNAME)
+          && properties.containsKey(AMBARI_DISPATCH_CREDENTIAL_PASSWORD)) {
+        DispatchCredentials credentials = new DispatchCredentials();
+        credentials.UserName = properties.get(AMBARI_DISPATCH_CREDENTIAL_USERNAME);
+        credentials.Password = properties.get(AMBARI_DISPATCH_CREDENTIAL_PASSWORD);
+        notification.Credentials = credentials;
+      }
+
+      if (null != targetProperties.Recipients) {
+        List<Recipient> recipients = new ArrayList<Recipient>(
+            targetProperties.Recipients.size());
+
+        for (String stringRecipient : targetProperties.Recipients) {
+          Recipient recipient = new Recipient();
+          recipient.Identifier = stringRecipient;
+          recipients.add(recipient);
+        }
+
+        notification.Recipients = recipients;
+      }
+
+      // set all other dispatch properties
+      notification.DispatchProperties = properties;
+
       NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(target.getNotificationType());
       DispatchRunnable runnable = new DispatchRunnable(dispatcher, notification);
 
@@ -151,6 +263,61 @@ public class AlertNoticeDispatchService extends AbstractScheduledService {
   }
 
   /**
+   * The {@link AlertTargetProperties} separates out the dispatcher properties
+   * from the list of recipients which is a JSON array and not a String.
+   */
+  private static final class AlertTargetProperties {
+    /**
+     * The properties to pass to the concrete dispatcher.
+     */
+    public Map<String, String> Properties;
+
+    /**
+     * The recipients of the notice.
+     */
+    public List<String> Recipients;
+  }
+
+  /**
+   * The {@link AlertTargetPropertyDeserializer} is used to dump the majority of
+   * JSON serialized properties into a {@link Map} of {@link String} while at
+   * the same time, converting
+   * {@link AlertNoticeDispatchService#AMBARI_DISPATCH_RECIPIENTS} into a list.
+   */
+  private static final class AlertTargetPropertyDeserializer implements
+      JsonDeserializer<AlertTargetProperties> {
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public AlertTargetProperties deserialize(JsonElement json, Type typeOfT,
+        JsonDeserializationContext context) throws JsonParseException {
+
+      AlertTargetProperties properties = new AlertTargetProperties();
+      properties.Properties = new HashMap<String, String>();
+
+      final JsonObject jsonObject = json.getAsJsonObject();
+      Set<Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
+
+      for (Entry<String, JsonElement> entry : entrySet) {
+        String entryKey = entry.getKey();
+        JsonElement entryValue = entry.getValue();
+
+        if (entryKey.equals(AMBARI_DISPATCH_RECIPIENTS)) {
+          Type listType = new TypeToken<List<String>>() {}.getType();
+          JsonArray jsonArray = entryValue.getAsJsonArray();
+          properties.Recipients = context.deserialize(jsonArray, listType);
+        } else {
+          properties.Properties.put(entryKey, entryValue.getAsString());
+        }
+      }
+
+      return properties;
+    }
+  }
+
+  /**
    * A custom {@link ThreadFactory} for the threads that will handle dispatching
    * {@link AlertNoticeEntity} instances. Threads created will have slightly
    * reduced priority since {@link AlertEvent} instances are not critical to the

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json
----------------------------------------------------------------------
diff --git a/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json b/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json
index 88503af..620c89f 100644
--- a/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json
+++ b/ambari-server/src/main/resources/stacks/HDP/2.0.6/services/HDFS/alerts.json
@@ -2,7 +2,7 @@
   "service": [
     {
       "name": "percent_datanode",
-      "label": "Percent DataNodes live",
+      "label": "Percent DataNodes Live",
       "interval": 1,
       "scope": "SERVICE",
       "source": {

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProviderTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProviderTest.java b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProviderTest.java
index 1c964a3..b96a4f3 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProviderTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/controller/internal/AlertTargetResourceProviderTest.java
@@ -187,6 +187,39 @@ public class AlertTargetResourceProviderTest {
    * @throws Exception
    */
   @Test
+  public void testCreateWithRecipientArray() throws Exception {
+    Capture<List<AlertTargetEntity>> listCapture = new Capture<List<AlertTargetEntity>>();
+
+    m_dao.createTargets(capture(listCapture));
+    expectLastCall();
+
+    replay(m_amc, m_dao);
+
+    AlertTargetResourceProvider provider = createProvider(m_amc);
+    Map<String, Object> requestProps = getRecipientCreationProperties();
+
+    Request request = PropertyHelper.getCreateRequest(
+        Collections.singleton(requestProps), null);
+    provider.createResources(request);
+
+    Assert.assertTrue(listCapture.hasCaptured());
+    AlertTargetEntity entity = listCapture.getValue().get(0);
+    Assert.assertNotNull(entity);
+
+    assertEquals(ALERT_TARGET_NAME, entity.getTargetName());
+    assertEquals(ALERT_TARGET_DESC, entity.getDescription());
+    assertEquals(ALERT_TARGET_TYPE, entity.getNotificationType());
+    assertEquals(
+        "{\"ambari.dispatch.recipients\":\"[\\\"ambari@ambari.apache.org\\\"]\"}",
+        entity.getProperties());
+
+    verify(m_amc, m_dao);
+  }
+
+  /**
+   * @throws Exception
+   */
+  @Test
   @SuppressWarnings("unchecked")
   public void testUpdateResources() throws Exception {
     Capture<AlertTargetEntity> entityCapture = new Capture<AlertTargetEntity>();
@@ -331,6 +364,31 @@ public class AlertTargetResourceProviderTest {
   }
 
   /**
+   * Gets the maps of properties that simulate a deserialzied JSON request with
+   * a nested JSON array.
+   *
+   * @return
+   * @throws Exception
+   */
+  private Map<String, Object> getRecipientCreationProperties() throws Exception {
+    Map<String, Object> requestProps = new HashMap<String, Object>();
+    requestProps.put(AlertTargetResourceProvider.ALERT_TARGET_NAME,
+        ALERT_TARGET_NAME);
+
+    requestProps.put(AlertTargetResourceProvider.ALERT_TARGET_DESCRIPTION,
+        ALERT_TARGET_DESC);
+
+    requestProps.put(
+        AlertTargetResourceProvider.ALERT_TARGET_NOTIFICATION_TYPE,
+        ALERT_TARGET_TYPE);
+
+    requestProps.put(AlertTargetResourceProvider.ALERT_TARGET_PROPERTIES
+        + "/ambari.dispatch.recipients", "[\"ambari@ambari.apache.org\"]");
+
+    return requestProps;
+  }
+
+  /**
   *
   */
   private class MockModule implements Module {

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/test/java/org/apache/ambari/server/notifications/EmailDispatcherTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/notifications/EmailDispatcherTest.java b/ambari-server/src/test/java/org/apache/ambari/server/notifications/EmailDispatcherTest.java
new file mode 100644
index 0000000..1e7689f
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/notifications/EmailDispatcherTest.java
@@ -0,0 +1,117 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ambari.server.notifications;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import org.apache.ambari.server.orm.InMemoryDefaultTestModule;
+import org.apache.ambari.server.state.alert.TargetType;
+import org.easymock.EasyMock;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.util.Modules;
+
+/**
+ *
+ */
+public class EmailDispatcherTest {
+
+  private Injector m_injector;
+  private DispatchFactory m_dispatchFactory;
+
+  @Before
+  public void before() throws Exception {
+    m_injector = Guice.createInjector(Modules.override(
+        new InMemoryDefaultTestModule()).with(new MockModule()));
+
+    m_dispatchFactory = m_injector.getInstance(DispatchFactory.class);
+  }
+
+  /**
+   * Tests that an email without recipients causes a callback error.
+   */
+  @Test
+  public void testNoRecipients() {
+    Notification notification = new Notification();
+    DispatchCallback callback = EasyMock.createMock(DispatchCallback.class);
+    notification.Callback = callback;
+
+    List<String> callbackIds = new ArrayList<String>();
+    callbackIds.add(UUID.randomUUID().toString());
+    notification.CallbackIds = callbackIds;
+
+    callback.onFailure(callbackIds);
+
+    EasyMock.expectLastCall();
+    EasyMock.replay(callback);
+
+    NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(TargetType.EMAIL.name());
+    dispatcher.dispatch(notification);
+
+    EasyMock.verify(callback);
+  }
+
+  /**
+   * Tests that an email without properties causes a callback error.
+   */
+  @Test
+  public void testNoEmailPropeties() {
+    Notification notification = new Notification();
+    DispatchCallback callback = EasyMock.createMock(DispatchCallback.class);
+    notification.Callback = callback;
+    notification.Recipients = new ArrayList<Recipient>();
+
+    Recipient recipient = new Recipient();
+    recipient.Identifier = "foo";
+
+    notification.Recipients.add(recipient);
+
+    List<String> callbackIds = new ArrayList<String>();
+    callbackIds.add(UUID.randomUUID().toString());
+    notification.CallbackIds = callbackIds;
+
+    callback.onFailure(callbackIds);
+
+    EasyMock.expectLastCall();
+    EasyMock.replay(callback);
+
+    NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(TargetType.EMAIL.name());
+    dispatcher.dispatch(notification);
+
+    EasyMock.verify(callback);
+  }
+
+  /**
+   *
+   */
+  private class MockModule implements Module {
+    /**
+     *
+     */
+    @Override
+    public void configure(Binder binder) {
+    }
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/test/java/org/apache/ambari/server/notifications/MockDispatcher.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/notifications/MockDispatcher.java b/ambari-server/src/test/java/org/apache/ambari/server/notifications/MockDispatcher.java
new file mode 100644
index 0000000..616551f
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/notifications/MockDispatcher.java
@@ -0,0 +1,46 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ambari.server.notifications;
+
+/**
+ *
+ */
+public class MockDispatcher implements NotificationDispatcher {
+
+  /**
+   * Constructor.
+   *
+   */
+  public MockDispatcher() {
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public String getType() {
+    return "MOCK";
+  }
+
+  /**
+   * {@inheritDoc}
+   */
+  @Override
+  public void dispatch(Notification notification) {
+  }
+}

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/test/java/org/apache/ambari/server/orm/dao/AlertsDAOTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/orm/dao/AlertsDAOTest.java b/ambari-server/src/test/java/org/apache/ambari/server/orm/dao/AlertsDAOTest.java
index 9e638e8..6e4d4af 100644
--- a/ambari-server/src/test/java/org/apache/ambari/server/orm/dao/AlertsDAOTest.java
+++ b/ambari-server/src/test/java/org/apache/ambari/server/orm/dao/AlertsDAOTest.java
@@ -22,6 +22,7 @@ package org.apache.ambari.server.orm.dao;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
 import java.util.ArrayList;
 import java.util.Calendar;
@@ -63,7 +64,7 @@ public class AlertsDAOTest {
   private AlertDefinitionDAO definitionDao;
 
   /**
-   * 
+   *
    */
   @Before
   public void setup() throws Exception {
@@ -88,7 +89,7 @@ public class AlertsDAOTest {
       definition.setSourceType(SourceType.SCRIPT);
       definitionDao.create(definition);
     }
-    
+
     List<AlertDefinitionEntity> definitions = definitionDao.findAll();
     assertNotNull(definitions);
     assertEquals(5, definitions.size());
@@ -142,7 +143,7 @@ public class AlertsDAOTest {
   }
 
   /**
-   * 
+   *
    */
   @After
   public void teardown() {
@@ -152,7 +153,7 @@ public class AlertsDAOTest {
 
 
   /**
-   * 
+   *
    */
   @Test
   public void testFindAll() {
@@ -162,7 +163,7 @@ public class AlertsDAOTest {
   }
 
   /**
-   * 
+   *
    */
   @Test
   public void testFindAllCurrent() {
@@ -172,16 +173,16 @@ public class AlertsDAOTest {
   }
 
   /**
-   * 
+   *
    */
   @Test
   public void testFindCurrentByService() {
     List<AlertCurrentEntity> currentAlerts = dao.findCurrent();
     AlertCurrentEntity current = currentAlerts.get(0);
     AlertHistoryEntity history = current.getAlertHistory();
-    
+
     assertNotNull(history);
-    
+
     currentAlerts = dao.findCurrentByService(clusterId,
         history.getServiceName());
 
@@ -193,7 +194,7 @@ public class AlertsDAOTest {
     assertNotNull(currentAlerts);
     assertEquals(0, currentAlerts.size());
   }
-  
+
   /**
    * Test looking up current by a host name.
    */
@@ -211,7 +212,7 @@ public class AlertsDAOTest {
     hostDef.setSource("HostService");
     hostDef.setSourceType(SourceType.SCRIPT);
     definitionDao.create(hostDef);
-    
+
     // history for the definition
     AlertHistoryEntity history = new AlertHistoryEntity();
     history.setServiceName(hostDef.getServiceName());
@@ -222,14 +223,14 @@ public class AlertsDAOTest {
     history.setAlertTimestamp(Long.valueOf(1L));
     history.setHostName("h2");
     history.setAlertState(AlertState.OK);
-    
+
     // current for the history
     AlertCurrentEntity current = new AlertCurrentEntity();
     current.setOriginalTimestamp(1L);
     current.setLatestTimestamp(2L);
     current.setAlertHistory(history);
     dao.create(current);
-    
+
     List<AlertCurrentEntity> currentAlerts = dao.findCurrentByHost(clusterId, history.getHostName());
 
     assertNotNull(currentAlerts);
@@ -239,10 +240,10 @@ public class AlertsDAOTest {
 
     assertNotNull(currentAlerts);
     assertEquals(0, currentAlerts.size());
-  }  
+  }
 
   /**
-   * 
+   *
    */
   @Test
   public void testFindByState() {
@@ -250,7 +251,7 @@ public class AlertsDAOTest {
     allStates.add(AlertState.OK);
     allStates.add(AlertState.WARNING);
     allStates.add(AlertState.CRITICAL);
-        
+
     List<AlertHistoryEntity> history = dao.findAll(clusterId, allStates);
     assertNotNull(history);
     assertEquals(50, history.size());
@@ -263,21 +264,21 @@ public class AlertsDAOTest {
         Collections.singletonList(AlertState.CRITICAL));
     assertNotNull(history);
     assertEquals(10, history.size());
-    
+
     history = dao.findAll(clusterId,
         Collections.singletonList(AlertState.WARNING));
     assertNotNull(history);
-    assertEquals(0, history.size());        
+    assertEquals(0, history.size());
   }
 
   /**
-   * 
+   *
    */
   @Test
   public void testFindByDate() {
     calendar.clear();
     calendar.set(2014, Calendar.JANUARY, 1);
-    
+
     // on or after 1/1/2014
     List<AlertHistoryEntity> history = dao.findAll(clusterId,
         calendar.getTime(), null);
@@ -311,21 +312,21 @@ public class AlertsDAOTest {
     assertNotNull(history);
     assertEquals(0, history.size());
   }
-  
+
   @Test
   public void testFindCurrentByHostAndName() throws Exception {
     AlertCurrentEntity entity = dao.findCurrentByHostAndName(clusterId.longValue(), "h2", "Alert Definition 1");
     assertNull(entity);
-    
+
     entity = dao.findCurrentByHostAndName(clusterId.longValue(), "h1", "Alert Definition 1");
-    
+
     assertNotNull(entity);
     assertNotNull(entity.getAlertHistory());
     assertNotNull(entity.getAlertHistory().getAlertDefinition());
   }
-  
+
   /**
-   * 
+   *
    */
   @Test
   public void testFindCurrentSummary() throws Exception {
@@ -341,12 +342,12 @@ public class AlertsDAOTest {
     dao.merge(h2);
     h3.setAlertState(AlertState.UNKNOWN);
     dao.merge(h3);
-    
+
     int ok = 0;
     int warn = 0;
     int crit = 0;
     int unk = 0;
-    
+
     for (AlertCurrentEntity h : dao.findCurrentByCluster(clusterId.longValue())) {
       switch (h.getAlertHistory().getAlertState()) {
         case CRITICAL:
@@ -362,22 +363,22 @@ public class AlertsDAOTest {
           warn++;
           break;
       }
-        
+
     }
-      
+
     summary = dao.findCurrentCounts(clusterId.longValue(), null, null);
     // !!! db-to-db compare
     assertEquals(ok, summary.getOkCount());
     assertEquals(warn, summary.getWarningCount());
     assertEquals(crit, summary.getCriticalCount());
     assertEquals(unk, summary.getCriticalCount());
-    
+
     // !!! expected
     assertEquals(2, summary.getOkCount());
     assertEquals(1, summary.getWarningCount());
     assertEquals(1, summary.getCriticalCount());
     assertEquals(1, summary.getCriticalCount());
-    
+
     summary = dao.findCurrentCounts(clusterId.longValue(), "Service 0", null);
     assertEquals(1, summary.getOkCount());
     assertEquals(0, summary.getWarningCount());
@@ -389,15 +390,14 @@ public class AlertsDAOTest {
     assertEquals(1, summary.getWarningCount());
     assertEquals(1, summary.getCriticalCount());
     assertEquals(1, summary.getCriticalCount());
-    
+
     summary = dao.findCurrentCounts(clusterId.longValue(), "foo", null);
     assertEquals(0, summary.getOkCount());
     assertEquals(0, summary.getWarningCount());
     assertEquals(0, summary.getCriticalCount());
     assertEquals(0, summary.getCriticalCount());
-    
   }
-  
+
   @Test
   public void testFindAggregates() throws Exception {
     // definition
@@ -412,7 +412,7 @@ public class AlertsDAOTest {
     definition.setSource("SourceScript");
     definition.setSourceType(SourceType.SCRIPT);
     definitionDao.create(definition);
-    
+
     // history record #1 and current
     AlertHistoryEntity history = new AlertHistoryEntity();
     history.setAlertDefinition(definition);
@@ -425,13 +425,13 @@ public class AlertsDAOTest {
     history.setComponentName("");
     history.setHostName("h1");
     history.setServiceName("ServiceName");
-    
+
     AlertCurrentEntity current = new AlertCurrentEntity();
     current.setAlertHistory(history);
     current.setLatestTimestamp(Long.valueOf(1L));
     current.setOriginalTimestamp(Long.valueOf(1L));
     dao.merge(current);
-    
+
     // history record #2 and current
     history = new AlertHistoryEntity();
     history.setAlertDefinition(definition);
@@ -444,35 +444,84 @@ public class AlertsDAOTest {
     history.setComponentName("");
     history.setHostName("h2");
     history.setServiceName("ServiceName");
-    
+
     current = new AlertCurrentEntity();
     current.setAlertHistory(history);
     current.setLatestTimestamp(Long.valueOf(1L));
     current.setOriginalTimestamp(Long.valueOf(1L));
     dao.merge(current);
-    
+
     AlertSummaryDTO summary = dao.findAggregateCounts(clusterId.longValue(), "many_per_cluster");
     assertEquals(2, summary.getOkCount());
     assertEquals(0, summary.getWarningCount());
     assertEquals(0, summary.getCriticalCount());
     assertEquals(0, summary.getUnknownCount());
-    
+
     AlertCurrentEntity c = dao.findCurrentByHostAndName(clusterId.longValue(),
         "h2", "many_per_cluster");
     AlertHistoryEntity h = c.getAlertHistory();
     h.setAlertState(AlertState.CRITICAL);
     dao.merge(h);
-    
+
     summary = dao.findAggregateCounts(clusterId.longValue(), "many_per_cluster");
     assertEquals(2, summary.getOkCount());
     assertEquals(0, summary.getWarningCount());
     assertEquals(1, summary.getCriticalCount());
     assertEquals(0, summary.getUnknownCount());
-    
+
     summary = dao.findAggregateCounts(clusterId.longValue(), "foo");
     assertEquals(0, summary.getOkCount());
     assertEquals(0, summary.getWarningCount());
     assertEquals(0, summary.getCriticalCount());
     assertEquals(0, summary.getUnknownCount());
-  }  
+  }
+
+  /**
+   * Tests <a
+   * href="https://bugs.eclipse.org/bugs/show_bug.cgi?id=398067">https:/
+   * /bugs.eclipse.org/bugs/show_bug.cgi?id=398067</a> which causes an inner
+   * entity to be stale.
+   */
+  @Test
+  public void testJPAInnerEntityStaleness() {
+    List<AlertCurrentEntity> currents = dao.findCurrent();
+    AlertCurrentEntity current = currents.get(0);
+    AlertHistoryEntity oldHistory = current.getAlertHistory();
+
+    AlertHistoryEntity newHistory = new AlertHistoryEntity();
+    newHistory.setAlertDefinition(oldHistory.getAlertDefinition());
+    newHistory.setAlertInstance(oldHistory.getAlertInstance());
+    newHistory.setAlertLabel(oldHistory.getAlertLabel());
+
+    if (oldHistory.getAlertState() == AlertState.OK) {
+      newHistory.setAlertState(AlertState.CRITICAL);
+    } else {
+      newHistory.setAlertState(AlertState.OK);
+    }
+
+    newHistory.setAlertText("New History");
+    newHistory.setClusterId(oldHistory.getClusterId());
+    newHistory.setAlertTimestamp(System.currentTimeMillis());
+    newHistory.setComponentName(oldHistory.getComponentName());
+    newHistory.setHostName(oldHistory.getHostName());
+    newHistory.setServiceName(oldHistory.getServiceName());
+
+    dao.create(newHistory);
+
+    assertTrue(newHistory.getAlertId().longValue() != oldHistory.getAlertId().longValue());
+
+    current.setAlertHistory(newHistory);
+    dao.merge(current);
+
+    AlertCurrentEntity newCurrent = dao.findCurrentByHostAndName(
+        newHistory.getClusterId(),
+        newHistory.getHostName(),
+        newHistory.getAlertDefinition().getDefinitionName());
+
+    assertEquals(newHistory.getAlertId(),
+        newCurrent.getAlertHistory().getAlertId());
+
+    assertEquals(newHistory.getAlertState(),
+        newCurrent.getAlertHistory().getAlertState());
+  }
 }

http://git-wip-us.apache.org/repos/asf/ambari/blob/d838ca95/ambari-server/src/test/java/org/apache/ambari/server/state/alerts/AlertStateChangedEventTest.java
----------------------------------------------------------------------
diff --git a/ambari-server/src/test/java/org/apache/ambari/server/state/alerts/AlertStateChangedEventTest.java b/ambari-server/src/test/java/org/apache/ambari/server/state/alerts/AlertStateChangedEventTest.java
new file mode 100644
index 0000000..312f297
--- /dev/null
+++ b/ambari-server/src/test/java/org/apache/ambari/server/state/alerts/AlertStateChangedEventTest.java
@@ -0,0 +1,139 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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 org.apache.ambari.server.state.alerts;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.ambari.server.events.AlertStateChangeEvent;
+import org.apache.ambari.server.events.listeners.AlertServiceStateListener;
+import org.apache.ambari.server.events.listeners.AlertStateChangedListener;
+import org.apache.ambari.server.events.publishers.AlertEventPublisher;
+import org.apache.ambari.server.orm.GuiceJpaInitializer;
+import org.apache.ambari.server.orm.InMemoryDefaultTestModule;
+import org.apache.ambari.server.orm.dao.AlertDispatchDAO;
+import org.apache.ambari.server.orm.entities.AlertDefinitionEntity;
+import org.apache.ambari.server.orm.entities.AlertGroupEntity;
+import org.apache.ambari.server.orm.entities.AlertHistoryEntity;
+import org.apache.ambari.server.orm.entities.AlertNoticeEntity;
+import org.apache.ambari.server.orm.entities.AlertTargetEntity;
+import org.easymock.EasyMock;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.google.inject.Binder;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.persist.PersistService;
+import com.google.inject.util.Modules;
+
+/**
+ * Tests that {@link AlertStateChangeEvent} instances cause
+ * {@link AlertNoticeEntity} instances to be created.
+ */
+public class AlertStateChangedEventTest {
+
+  private AlertEventPublisher eventPublisher;
+  private AlertDispatchDAO dispatchDao;
+  private Injector injector;
+
+  /**
+   *
+   */
+  @Before
+  public void setup() throws Exception {
+    injector = Guice.createInjector(Modules.override(
+        new InMemoryDefaultTestModule()).with(new MockModule()));
+
+    injector.getInstance(GuiceJpaInitializer.class);
+
+    // force singleton init via Guice so the listener registers with the bus
+    injector.getInstance(AlertServiceStateListener.class);
+    injector.getInstance(AlertStateChangedListener.class);
+
+    dispatchDao = injector.getInstance(AlertDispatchDAO.class);
+    eventPublisher = injector.getInstance(AlertEventPublisher.class);
+  }
+
+  /**
+   * @throws Exception
+   */
+  @After
+  public void teardown() throws Exception {
+    injector.getInstance(PersistService.class).stop();
+    injector = null;
+  }
+
+  /**
+   * Tests that an {@link AlertStateChangeEvent} causes
+   * {@link AlertNoticeEntity} instances to be written.
+   *
+   * @throws Exception
+   */
+  @Test
+  public void testAlertNoticeCreationFromEvent() throws Exception {
+    AlertHistoryEntity history = EasyMock.createNiceMock(AlertHistoryEntity.class);
+    AlertStateChangeEvent event = EasyMock.createNiceMock(AlertStateChangeEvent.class);
+    EasyMock.expect(event.getNewHistoricalEntry()).andReturn(history).atLeastOnce();
+
+    EasyMock.replay(history, event);
+
+    // async publishing
+    eventPublisher.publish(event);
+    Thread.sleep(2000);
+
+    EasyMock.verify(dispatchDao, history, event);
+  }
+
+  /**
+   *
+   */
+  private class MockModule implements Module {
+    /**
+    *
+    */
+    @Override
+    public void configure(Binder binder) {
+      AlertTargetEntity alertTarget = EasyMock.createMock(AlertTargetEntity.class);
+      AlertGroupEntity alertGroup = EasyMock.createMock(AlertGroupEntity.class);
+      List<AlertGroupEntity> groups = new ArrayList<AlertGroupEntity>();
+      Set<AlertTargetEntity> targets = new HashSet<AlertTargetEntity>();
+
+      targets.add(alertTarget);
+      groups.add(alertGroup);
+
+      EasyMock.expect(alertGroup.getAlertTargets()).andReturn(targets).once();
+
+      AlertDispatchDAO dispatchDao = EasyMock.createMock(AlertDispatchDAO.class);
+      EasyMock.expect(
+          dispatchDao.findGroupsByDefinition(EasyMock.anyObject(AlertDefinitionEntity.class))).andReturn(
+          groups).once();
+
+      dispatchDao.create(EasyMock.anyObject(AlertNoticeEntity.class));
+      EasyMock.expectLastCall().once();
+
+      binder.bind(AlertDispatchDAO.class).toInstance(dispatchDao);
+
+      EasyMock.replay(alertTarget, alertGroup, dispatchDao);
+    }
+  }
+}