content: /*
   * 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.services;
  
  import java.io.File;
  import java.io.FileInputStream;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.StringWriter;
  import java.io.Writer;
  import java.lang.reflect.Type;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.Set;
  import java.util.concurrent.Executor;
  import java.util.concurrent.LinkedBlockingQueue;
  import java.util.concurrent.ThreadFactory;
  import java.util.concurrent.ThreadPoolExecutor;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.atomic.AtomicInteger;
  
  import javax.xml.bind.JAXBContext;
  import javax.xml.bind.Unmarshaller;
  import javax.xml.bind.annotation.XmlAttribute;
  import javax.xml.bind.annotation.XmlElement;
  import javax.xml.bind.annotation.XmlRootElement;
  
  import org.apache.ambari.server.AmbariService;
  import org.apache.ambari.server.api.services.AmbariMetaInfo;
  import org.apache.ambari.server.configuration.Configuration;
  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.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.AlertDefinitionEntity;
  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.apache.ambari.server.state.alert.AlertNotification;
  import org.apache.commons.io.IOUtils;
  import org.apache.velocity.VelocityContext;
  import org.apache.velocity.app.Velocity;
  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.Provider;
  
  /**
   * The {@link AlertNoticeDispatchService} is used to scan the database for
   * {@link AlertNoticeEntity} that are in the {@link NotificationState#PENDING}
   * state. 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.
   * <p/>
   * This class uses the templates that are defined via
   * {@link Configuration#getAlertTemplateFile()} or the fallback internal
   * template {@code alert-templates.xml}. These files are parsed during
   * {@link #startUp()}. If there is a problem parsing them, the service will
   * still startup normally, producing an error in logs. It will fall back to
   * simple string concatenation for {@link Notification} content in this case.
   */
  @AmbariService
  public class AlertNoticeDispatchService extends AbstractScheduledService {
    /**
     * Logger.
     */
    private static final Logger LOG = LoggerFactory.getLogger(AlertNoticeDispatchService.class);
  
    /**
     * The log tag to pass to Apache Velocity during rendering.
     */
    private static final String VELOCITY_LOG_TAG = "ambari-alerts";
  
    /**
     * The internal Ambari templates that ship.
     */
    private static final String AMBARI_ALERT_TEMPLATES = "alert-templates.xml";
  
    /**
     * The property containing the dispatch authentication username.
     */
    public static final String AMBARI_DISPATCH_CREDENTIAL_USERNAME = "ambari.dispatch.credential.username";
  
    /**
     * The property containing the dispatch authentication password.
     */
    public static final String AMBARI_DISPATCH_CREDENTIAL_PASSWORD = "ambari.dispatch.credential.password";
  
    /**
     * The property containing the dispatch recipients
     */
    public static final String AMBARI_DISPATCH_RECIPIENTS = "ambari.dispatch.recipients";
  
    /**
     * The context key for Ambari information to be passed to Velocity.
     */
    private static final String VELOCITY_AMBARI_KEY = "ambari";
  
    /**
     * The context key for alert summary information to be passed to Velocity.
     */
    private static final String VELOCITY_SUMMARY_KEY = "summary";
  
    /**
     * The context key for a single alert's information to be passed to Velocity.
     */
    private static final String VELOCITY_ALERT_KEY = "alert";
  
    /**
     * The context key for dispatch target information to be passed to Velocity.
     */
    private static final String VELOCITY_DISPATCH_KEY = "dispatch";
  
    /**
     * Gson used to convert JSON properties to a map.
     */
    private final Gson m_gson;
  
    /**
     * Dispatch DAO to query pending {@link AlertNoticeEntity} instances from.
     */
    @Inject
    private AlertDispatchDAO m_dao;
  
    /**
     * The factory used to get an {@link NotificationDispatcher} instance to
     * submit to the {@link #m_executor}.
     */
    @Inject
    private DispatchFactory m_dispatchFactory;
  
    /**
     * The alert templates to use when rendering content for a
     * {@link Notification}.
     */
    private AlertTemplates m_alertTemplates;
  
    /**
     * The configuration instance to get Ambari properties.
     */
    @Inject
    private Configuration m_configuration;
  
    /**
     * Ambari meta information used fro alert {@link Notification}s.
     */
    @Inject
    private Provider<AmbariMetaInfo> m_metaInfo;
  
    /**
     * The executor responsible for dispatching.
     */
    private Executor m_executor;
  
    /**
     * Constructor.
     */
    public AlertNoticeDispatchService() {
      m_executor = new ThreadPoolExecutor(0, 2, 5L, TimeUnit.MINUTES,
        new LinkedBlockingQueue<>(), new AlertDispatchThreadFactory(),
          new ThreadPoolExecutor.CallerRunsPolicy());
  
      GsonBuilder gsonBuilder = new GsonBuilder();
      gsonBuilder.registerTypeAdapter(AlertTargetProperties.class,
          new AlertTargetPropertyDeserializer());
  
      m_gson = gsonBuilder.create();
    }
  
    /**
     * {@inheritDoc}
     * <p/>
     * Parse the XML template for {@link Notification} content. If there is a
     * problem parsing the content, the service will still startup normally but
     * the {@link Notification} content will fallback to plaintext.
     */
    @Override
    protected void startUp() throws Exception {
      super.startUp();
  
      InputStream inputStream = null;
      String alertTemplatesFile = null;
  
      try {
        alertTemplatesFile = m_configuration.getAlertTemplateFile();
        if (null != alertTemplatesFile) {
          File file = new File(alertTemplatesFile);
          inputStream = new FileInputStream(file);
        }
      } catch (Exception exception) {
        LOG.warn("Unable to load alert template file {}", alertTemplatesFile,
            exception);
      }
  
      try {
        JAXBContext context = JAXBContext.newInstance(AlertTemplates.class);
        Unmarshaller unmarshaller = context.createUnmarshaller();
  
        // if the file provided via the configuration is not available, use
        // the internal one
        if (null == inputStream) {
          inputStream = ClassLoader.getSystemResourceAsStream(AMBARI_ALERT_TEMPLATES);
        }
  
        m_alertTemplates = (AlertTemplates) unmarshaller.unmarshal(inputStream);
      } catch (Exception exception) {
        LOG.error(
            "Unable to load alert template file {}, outbound notifications will not be formatted",
            AMBARI_ALERT_TEMPLATES, exception);
      } finally {
        if (null != inputStream) {
          IOUtils.closeQuietly(inputStream);
        }
      }
    }
  
    /**
     * Sets the {@link Executor} to use when dispatching {@link Notification}s.
     * This should only be used by unit tests to provide a mock executor.
     *
     * @param executor
     *          the executor to use (not {@code null).
  
     */
    protected void setExecutor(Executor executor) {
      m_executor = executor;
    }
  
    /**
     * {@inheritDoc}
     */
    @Override
    protected void runOneIteration() throws Exception {
      Map<AlertTargetEntity, List<AlertNoticeEntity>> aggregateMap;
      try {
        aggregateMap = getGroupedNotices();
      } catch (Exception e) {
        LOG.error("Caught exception during alert notices preparing.", e);
        return;
      }
  
      // now that all of the notices are grouped by target, dispatch them
      Set<AlertTargetEntity> targets = aggregateMap.keySet();
      for (AlertTargetEntity target : targets) {
        List<AlertNoticeEntity> notices = aggregateMap.get(target);
        if (null == notices || notices.size() == 0) {
          continue;
        }
        try {
          String targetType = target.getNotificationType();
          NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(targetType);
  
          // create a single digest notification if supported
          if (dispatcher.isDigestSupported()) {
            createSingleNotice(dispatcher, target, notices);
          } else {
            createSeparateNotices(dispatcher, target, notices);
          }
        } catch (Exception e) {
          LOG.error("Caught exception during Alert Notice dispatching.", e);
        }
      }
    }
  
    /**
     * Retrieves all pending notices to dispatch and groups them by target.
     * @return grouped notices.
     */
    private Map<AlertTargetEntity, List<AlertNoticeEntity>> getGroupedNotices() {
      List<AlertNoticeEntity> pending = m_dao.findPendingNotices();
      if (pending.size() == 0) {
        return Collections.EMPTY_MAP;
      }
  
      LOG.info("There are {} pending alert notices about to be dispatched...",
          pending.size());
  
      Map<AlertTargetEntity, List<AlertNoticeEntity>> aggregateMap = new HashMap<>(pending.size());
  
      // combine all histories by target
      for (AlertNoticeEntity notice : pending) {
        AlertTargetEntity target = notice.getAlertTarget();
  
        List<AlertNoticeEntity> notices = aggregateMap.get(target);
        if (null == notices) {
          notices = new ArrayList<>();
          aggregateMap.put(target, notices);
        }
  
        // at this point, notices have been processed but not yet delivered
        notice.setNotifyState(NotificationState.DISPATCHED);
        notice = m_dao.merge(notice);
  
        notices.add(notice);
      }
      return aggregateMap;
    }
  
    /**
     * Creates a single digest notification. Should be used when dispatcher supports digest.
     * @param dispatcher dispatches the created notifications.
     * @param target where notifications will be sent.
     * @param notices notices should be dispatched.
     */
    private void createSingleNotice(NotificationDispatcher dispatcher, AlertTargetEntity target,
                                             List<AlertNoticeEntity> notices) {
      AlertNotification notification = buildNotificationFromTarget(target);
      notification.CallbackIds = new ArrayList<>(notices.size());
      List<AlertHistoryEntity> histories = new ArrayList<>(
          notices.size());
  
      // add callback IDs so that the notices can be marked as DELIVERED or
      // FAILED, and create a list of just the alert histories
      for (AlertNoticeEntity notice : notices) {
        AlertHistoryEntity history = notice.getAlertHistory();
        histories.add(history);
  
        notification.CallbackIds.add(notice.getUuid());
      }
  
  
      // populate the subject and body fields; if there is a problem
      // generating the content, then mark the notices as FAILED
      try {
        renderDigestNotificationContent(dispatcher, notification, histories, target);
  
        // dispatch
        DispatchRunnable runnable = new DispatchRunnable(dispatcher, notification);
        m_executor.execute(runnable);
      } catch (Exception exception) {
        LOG.error("Unable to create notification for alerts", exception);
  
        // there was a problem generating content for the target; mark all
        // notices as FAILED and skip this target
        // mark these as failed
        notification.Callback.onFailure(notification.CallbackIds);
      }
    }
  
    /**
     * If the dispatcher does not support digest, each notice must have a 1:1 notification created for it.
     * @param dispatcher dispatches the created notifications.
     * @param target where notifications will be sent.
     * @param notices notices should be dispatched.
     */
    private void createSeparateNotices(NotificationDispatcher dispatcher, AlertTargetEntity target,
                                        List<AlertNoticeEntity> notices) {
      for (AlertNoticeEntity notice : notices) {
        AlertNotification notification = buildNotificationFromTarget(target);
        AlertHistoryEntity history = notice.getAlertHistory();
        notification.CallbackIds = Collections.singletonList(notice.getUuid());
  
        // populate the subject and body fields; if there is a problem
        // generating the content, then mark the notices as FAILED
        try {
          renderNotificationContent(dispatcher, notification, history, target);
  
          // dispatch
          DispatchRunnable runnable = new DispatchRunnable(dispatcher, notification);
          m_executor.execute(runnable);
        } catch (Exception exception) {
          LOG.error("Unable to create notification for alert", exception);
  
          // mark these as failed
          notification.Callback.onFailure(notification.CallbackIds);
        }
      }
    }
  
    /**
     * {@inheritDoc}
     * <p/>
     * Returns a schedule that starts after 2 minute and runs every 2 minutes
     * after {@link #runOneIteration()} completes.
     */
    @Override
    protected Scheduler scheduler() {
      return Scheduler.newFixedDelaySchedule(2, 2, TimeUnit.MINUTES);
    }
  
    /**
     * Initializes a {@link Notification} instance from an
     * {@link AlertTargetEntity}. This method does most of the boilerplate work to
     * get a {@link Notification} that is almost ready to send.
     * <p/>
     * The {@link Notification} will not have any of the callback IDs or content
     * set.
     *
     * @param target
     *          the alert target
     * @return the initialized notification
     */
    private AlertNotification buildNotificationFromTarget(AlertTargetEntity target) {
      String propertiesJson = target.getProperties();
  
      AlertTargetProperties targetProperties = m_gson.fromJson(propertiesJson,
          AlertTargetProperties.class);
  
      Map<String, String> properties = targetProperties.Properties;
  
      // create an initialize the notification
      AlertNotification notification = new AlertNotification();
      notification.Callback = new AlertNoticeDispatchCallback();
      notification.DispatchProperties = properties;
  
      // 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;
      }
  
      // create recipients
      if (null != targetProperties.Recipients) {
        List<Recipient> recipients = new ArrayList<>(
          targetProperties.Recipients.size());
  
        for (String stringRecipient : targetProperties.Recipients) {
          Recipient recipient = new Recipient();
          recipient.Identifier = stringRecipient;
          recipients.add(recipient);
        }
  
        notification.Recipients = recipients;
      }
  
      return notification;
    }
  
    /**
     * Generates digest content for the {@link Notification} using the
     * {@link #m_alertTemplates} and the list of alerts passed in. If there is a
     * problem with the templates, this will fallback to non-formatted content.
     *
     * @param dispatcher
     *          the dispatcher for this notification type (not {@code null}).
     * @param notification
     *          the notification (not {@code null}).
     * @param histories
     *          the alerts to generate the content from (not {@code null}.
     * @param target
     *          the target of the {@link Notification}.
     */
    private void renderDigestNotificationContent(NotificationDispatcher dispatcher,
        AlertNotification notification, List<AlertHistoryEntity> histories, AlertTargetEntity target)
        throws IOException {
      String targetType = target.getNotificationType();
  
      // build the velocity objects for template rendering
      AmbariInfo ambari = new AmbariInfo(m_metaInfo.get(), m_configuration);
      AlertSummaryInfo summary = new AlertSummaryInfo(histories);
      DispatchInfo dispatch = new DispatchInfo(target);
  
      // get the template for this target type
      final Writer subjectWriter = new StringWriter();
      final Writer bodyWriter = new StringWriter();
      final AlertTemplate template = m_alertTemplates.getTemplate(targetType);
  
      if (dispatcher.isNotificationContentGenerationRequired()) {
        if (null != template) {
          // create the velocity context for template rendering
          VelocityContext velocityContext = new VelocityContext();
          velocityContext.put(VELOCITY_AMBARI_KEY, ambari);
          velocityContext.put(VELOCITY_SUMMARY_KEY, summary);
          velocityContext.put(VELOCITY_DISPATCH_KEY, dispatch);
  
          // render the template and assign the content to the notification
          String subjectTemplate = template.getSubject();
          String bodyTemplate = template.getBody();
  
          // render the subject
          Velocity.evaluate(velocityContext, subjectWriter, VELOCITY_LOG_TAG, subjectTemplate);
  
          // render the body
          Velocity.evaluate(velocityContext, bodyWriter, VELOCITY_LOG_TAG, bodyTemplate);
        } else {
          // a null template is possible from parsing incorrectly or not
          // having the correct type defined for the target
          for (AlertHistoryEntity alert : histories) {
            subjectWriter.write("Apache Ambari Alert Summary");
            bodyWriter.write(alert.getAlertState().name());
            bodyWriter.write(" ");
            bodyWriter.write(alert.getAlertDefinition().getLabel());
            bodyWriter.write(" ");
            bodyWriter.write(alert.getAlertText());
            bodyWriter.write("\n");
          }
        }
      }
  
      notification.Subject = subjectWriter.toString();
      notification.Body = bodyWriter.toString();
    }
  
    /**
     * Generates the content for the {@link Notification} using the
     * {@link #m_alertTemplates} and the single alert passed in. If there is a
     * problem with the templates, this will fallback to non-formatted content.
     *
     * @param dispatcher
     *          the dispatcher for this notification type (not {@code null}).
     * @param notification
     *          the notification (not {@code null}).
     * @param history
     *          the alert to generate the content from (not {@code null}.
     * @param target
     *          the target of the {@link Notification}.
     */
    private void renderNotificationContent(NotificationDispatcher dispatcher,
        AlertNotification notification, AlertHistoryEntity history, AlertTargetEntity target)
        throws IOException {
      String targetType = target.getNotificationType();
  
      // build the velocity objects for template rendering
      AmbariInfo ambari = new AmbariInfo(m_metaInfo.get(), m_configuration);
      AlertInfo alert = new AlertInfo(history);
      DispatchInfo dispatch = new DispatchInfo(target);
  
      // set the alert info on the notification subclass so that dispatchers
      // can use it directly
      notification.setAlertInfo(alert);
  
      // get the template for this target type
      final Writer subjectWriter = new StringWriter();
      final Writer bodyWriter = new StringWriter();
      final AlertTemplate template = m_alertTemplates.getTemplate(targetType);
  
      if (dispatcher.isNotificationContentGenerationRequired()) {
        if (null != template) {
          // create the velocity context for template rendering
          VelocityContext velocityContext = new VelocityContext();
          velocityContext.put(VELOCITY_AMBARI_KEY, ambari);
          velocityContext.put(VELOCITY_ALERT_KEY, alert);
          velocityContext.put(VELOCITY_DISPATCH_KEY, dispatch);
  
          // render the template and assign the content to the notification
          String subjectTemplate = template.getSubject();
          String bodyTemplate = template.getBody();
  
          // render the subject
          Velocity.evaluate(velocityContext, subjectWriter, VELOCITY_LOG_TAG, subjectTemplate);
  
          // render the body
          Velocity.evaluate(velocityContext, bodyWriter, VELOCITY_LOG_TAG, bodyTemplate);
        } else {
          // a null template is possible from parsing incorrectly or not
          // having the correct type defined for the target
          subjectWriter.write(alert.getAlertState().name());
          subjectWriter.write(" ");
          subjectWriter.write(alert.getAlertName());
  
          bodyWriter.write(alert.getAlertState().name());
          bodyWriter.write(" ");
          bodyWriter.write(alert.getAlertName());
          bodyWriter.write(" ");
          bodyWriter.write(alert.getAlertText());
          if (alert.hasHostName()) {
            bodyWriter.write(" ");
            bodyWriter.append(alert.getHostName());
          }
          bodyWriter.write("\n");
        }
      }
  
      notification.Subject = subjectWriter.toString();
      notification.Body = bodyWriter.toString();
    }
  
    /**
     * 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<>();
  
        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
     * system.
     */
    private static final class AlertDispatchThreadFactory implements
        ThreadFactory {
  
      private static final AtomicInteger s_threadIdPool = new AtomicInteger(1);
  
      /**
       * {@inheritDoc}
       */
      @Override
      public Thread newThread(Runnable r) {
        Thread thread = new Thread(r, "alert-dispatch-"
            + s_threadIdPool.getAndIncrement());
  
        thread.setDaemon(false);
        thread.setPriority(Thread.NORM_PRIORITY - 1);
  
        return thread;
      }
    }
  
    /**
     * The {@link AlertNoticeDispatchCallback} is used to receive a callback from
     * the dispatch framework and then update the {@link AlertNoticeEntity}
     * {@link NotificationState}.
     */
    private final class AlertNoticeDispatchCallback implements DispatchCallback {
  
      /**
       * {@inheritDoc}
       */
      @Override
      public void onSuccess(List<String> callbackIds) {
        for (String callbackId : callbackIds) {
          updateAlertNotice(callbackId, NotificationState.DELIVERED);
        }
      }
  
      /**
       * {@inheritDoc}
       */
      @Override
      public void onFailure(List<String> callbackIds) {
        for (String callbackId : callbackIds) {
          updateAlertNotice(callbackId, NotificationState.FAILED);
        }
      }
  
      /**
       * Updates the {@link AlertNoticeEntity} matching the given UUID with the
       * specified state.
       *
       * @param uuid
       * @param state
       */
      private void updateAlertNotice(String uuid, NotificationState state) {
        try {
          AlertNoticeEntity entity = m_dao.findNoticeByUuid(uuid);
          if (null == entity) {
            LOG.warn("Unable to find an alert notice with UUID {}", uuid);
            return;
          }
  
          entity.setNotifyState(state);
          m_dao.merge(entity);
        } catch (Exception exception) {
          LOG.error(
              "Unable to update the alert notice with UUID {} to {}, notifications will continue to be sent",
              uuid, state, exception);
        }
      }
    }
  
    /**
     * The {@link AlertInfo} class encapsulates all information about a single
     * alert for a single outbound {@link Notification}.
     */
    public final static class AlertInfo {
      private final AlertHistoryEntity m_history;
  
      /**
       * Constructor.
       *
       * @param history
       */
      public AlertInfo(AlertHistoryEntity history) {
        m_history = history;
      }
  
      /**
       * Gets the host name or {@code null} if none.
       *
       * @return
       */
      public String getHostName() {
        return m_history.getHostName();
      }
  
      /**
       * Gets whether there is a host associated with the alert. Some alerts like
       * aggregate alerts don't have hosts.
       *
       * @return
       */
      public boolean hasHostName() {
        return m_history.getHostName() != null;
      }
  
      /**
       * Gets the service name or {@code null} if none.
       *
       * @return
       */
      public String getServiceName() {
        return m_history.getServiceName();
      }
  
      /**
       * Gets the service component name, or {@code null} if none.
       *
       * @return
       */
      public String getComponentName() {
        return m_history.getComponentName();
      }
  
      /**
       * Gets whether there is a component associated with an alert. Some alerts
       * don't have an associated component.
       *
       * @return
       */
      public boolean hasComponentName() {
        return m_history.getComponentName() != null;
      }
  
      /**
       *  Gets the time that the alert was received
       * @return
         */
      public long getAlertTimestamp() {
        return m_history.getAlertTimestamp();
      }
  
      /**
       * Gets the state of the alert.
       *
       * @return
       */
      public AlertState getAlertState() {
        return m_history.getAlertState();
      }
  
      /**
       * Gets the definition id of the alert.
       *
       * @return
       */
      public Long getAlertDefinitionId() {
        return m_history.getAlertDefinitionId();
      }
  
      /**
       * Gets the hash of alert definition entity.
       *
       * @return
       */
      public int getAlertDefinitionHash() {
        return m_history.getAlertDefinitionHash();
      }
  
  
      /**
       * Gets the descriptive name of the alert.
       *
       * @return
       */
      public String getAlertName() {
        return m_history.getAlertDefinition().getLabel();
      }
  
      /**
       * Gets the alert definition for this alert.
       *
       * @return the alert definition.
       */
      public AlertDefinitionEntity getAlertDefinition() {
        return m_history.getAlertDefinition();
      }
  
      /**
       * Gets the text of the alert.
       *
       * @return
       */
      public String getAlertText() {
        return m_history.getAlertText();
      }
    }
  
    /**
     * The {@link AlertSummaryInfo} class encapsulates all of the alert
     * information for the {@link Notification}. This includes customized
     * structures to better organize information about each of the services,
     * hosts, and alert states.
     */
    public final static class AlertSummaryInfo {
      private int m_okCount = 0;
      private int m_warningCount = 0;
      private int m_criticalCount = 0;
      private int m_unknownCount = 0;
  
      /**
       * The hosts that have at least 1 alert reported.
       */
      private final Set<String> m_hosts = new HashSet<>();
  
      /**
       * The services that have at least 1 alert reported.
       */
      private final Set<String> m_services = new HashSet<>();
  
      /**
       * All of the alerts for the {@link Notification}.
       */
      private final List<AlertHistoryEntity> m_alerts;
  
      /**
       * A mapping of service to alerts where the alerts are also grouped by state
       * for that service.
       */
      private final Map<String, Map<AlertState, List<AlertHistoryEntity>>> m_alertsByServiceAndState = new HashMap<>();
  
      /**
       * A mapping of all services by state.
       */
      private final Map<String, Set<String>> m_servicesByState = new HashMap<>();
  
      /**
       * A mapping of all alerts by the service that owns them.
       */
      private final Map<String, List<AlertHistoryEntity>> m_alertsByService = new HashMap<>();
  
      /**
       * Constructor.
       *
       * @param histories
       */
      protected AlertSummaryInfo(List<AlertHistoryEntity> histories) {
        m_alerts = histories;
  
        // group all alerts by their service and severity
        for (AlertHistoryEntity history : m_alerts) {
          AlertState alertState = history.getAlertState();
          String serviceName = history.getServiceName();
          String hostName = history.getHostName();
  
          if (null != hostName) {
            m_hosts.add(hostName);
          }
  
          if (null != serviceName) {
            m_services.add(serviceName);
          }
  
          // group alerts by service name & state
          Map<AlertState, List<AlertHistoryEntity>> service = m_alertsByServiceAndState.get(serviceName);
          if (null == service) {
            service = new HashMap<>();
            m_alertsByServiceAndState.put(serviceName, service);
          }
  
          List<AlertHistoryEntity> alertList = service.get(alertState);
          if (null == alertList) {
            alertList = new ArrayList<>();
            service.put(alertState, alertList);
          }
  
          alertList.add(history);
  
          // group services by alert states
          Set<String> services = m_servicesByState.get(alertState.name());
          if (null == services) {
            services = new HashSet<>();
            m_servicesByState.put(alertState.name(), services);
          }
  
          services.add(serviceName);
  
          // group alerts by service
          List<AlertHistoryEntity> alertsByService = m_alertsByService.get(serviceName);
          if (null == alertsByService) {
            alertsByService = new ArrayList<>();
            m_alertsByService.put(serviceName, alertsByService);
          }
  
          alertsByService.add(history);
  
          // keep track of totals
          switch (alertState) {
            case CRITICAL:
              m_criticalCount++;
              break;
            case OK:
              m_okCount++;
              break;
            case UNKNOWN:
              m_unknownCount++;
              break;
            case WARNING:
              m_warningCount++;
              break;
            default:
              m_unknownCount++;
              break;
          }
        }
      }
  
      /**
       * Gets the total number of OK alerts in the {@link Notification}.
       *
       * @return the OK count.
       */
      public int getOkCount() {
        return m_okCount;
      }
  
      /**
       * Gets the total number of WARNING alerts in the {@link Notification}.
       *
       * @return the WARNING count.
       */
      public int getWarningCount() {
        return m_warningCount;
      }
  
      /**
       * Gets the total number of CRITICAL alerts in the {@link Notification}.
       *
       * @return the CRITICAL count.
       */
      public int getCriticalCount() {
        return m_criticalCount;
      }
  
      /**
       * Gets the total number of UNKNOWN alerts in the {@link Notification}.
       *
       * @return the UNKNOWN count.
       */
      public int getUnknownCount() {
        return m_unknownCount;
      }
  
      /**
       * Gets the total count of all alerts in the {@link Notification}
       *
       * @return the total count of all alerts.
       */
      public int getTotalCount() {
        return m_okCount + m_warningCount + m_criticalCount + m_unknownCount;
      }
  
      /**
       * Gets all of the services that have alerts being reporting in this
       * notification dispatch.
       *
       * @return the list of services.
       */
      public Set<String> getServices() {
        return m_services;
      }
  
      /**
       * Gets all of the alerts in the {@link Notification}.
       *
       * @return all of the alerts.
       */
      public List<AlertHistoryEntity> getAlerts() {
        return m_alerts;
      }
  
      /**
       * Gets all of the alerts in the {@link Notification} by service name.
       *
       * @param serviceName
       *          the service name.
       * @return the alerts for that service, or {@code null} none.
       */
      public List<AlertHistoryEntity> getAlerts(String serviceName) {
        return m_alertsByService.get(serviceName);
      }
  
      /**
       * Gets all of the alerts for a given service and alert state level.
       *
       * @param serviceName
       *          the name of the service.
       * @param alertState
       *          the alert state level.
       * @return the list of alerts or {@code null} for none.
       */
      public List<AlertHistoryEntity> getAlerts(String serviceName,
          String alertState) {
  
        Map<AlertState, List<AlertHistoryEntity>> serviceAlerts = m_alertsByServiceAndState.get(serviceName);
        if (null == serviceAlerts) {
          return null;
        }
  
        AlertState state = AlertState.valueOf(alertState);
        return serviceAlerts.get(state);
      }
  
      /**
       * Gets a list of services that have an alert being reporting for the given
       * state.
       *
       * @param alertState
       *          the state to get the services for.
       * @return the services or {@code null} if none.
       */
      public Set<String> getServicesByAlertState(String alertState) {
        return m_servicesByState.get(alertState);
      }
    }
  
    /**
     * The {@link AmbariInfo} class is used to provide the template engine with
     * information about the Ambari installation.
     */
    public final static class AmbariInfo {
      private String m_hostName = null;
      private String m_url = null;
      private String m_version = null;
  
      /**
       * Constructor.
       *
       * @param metaInfo
       */
      protected AmbariInfo(AmbariMetaInfo metaInfo, Configuration m_configuration) {
        m_url = m_configuration.getAmbariDisplayUrl();
        m_version = metaInfo.getServerVersion();
      }
  
      /**
       * @return the hostName
       */
      public String getHostName() {
        return m_hostName;
      }
  
      public boolean hasUrl() {
        return m_url != null;
      }
  
      /**
       * @return the url
       */
      public String getUrl() {
        return m_url;
      }
  
      /**
       * Gets the Ambari server version.
       *
       * @return the version
       */
      public String getServerVersion() {
        return m_version;
      }
    }
  
    /**
     * The {@link DispatchInfo} class is used to provide the template engine with
     * information about the intended target of the notification.
     */
    public static final class DispatchInfo {
      private String m_targetName;
      private String m_targetDescription;
  
      /**
       * Constructor.
       *
       * @param target
       *          the {@link AlertTargetEntity} receiving the notification.
       */
      protected DispatchInfo(AlertTargetEntity target) {
        m_targetName = target.getTargetName();
        m_targetDescription = target.getDescription();
      }
  
      /**
       * Gets the name of the notification target.
       *
       * @return the name of the target.
       */
      public String getTargetName() {
        return m_targetName;
      }
  
      /**
       * Gets the description of the notification target.
       *
       * @return the target description.
       */
      public String getTargetDescription() {
        return m_targetDescription;
      }
    }
  
    /**
     * The {@link AlertTemplates} class represnts the {@link AlertTemplates} that
     * have been loaded, either by the {@link Configuration} or by the backup
     * {@code alert-templates.xml} file.
     */
    @XmlRootElement(name = "alert-templates")
    private final static class AlertTemplates {
      /**
       * The alert templates defined.
       */
      @XmlElement(name = "alert-template", required = true)
      private List<AlertTemplate> m_templates;
  
      /**
       * Gets the alert template given the specified template type.
       *
       * @param type
       *          the template type.
       * @return the template, or {@code null} if none.
       * @see AlertTargetEntity#getNotificationType()
       */
      public AlertTemplate getTemplate(String type) {
        for (AlertTemplate template : m_templates) {
          if (type.equals(template.getType())) {
            return template;
          }
        }
  
        return null;
      }
    }
  
    /**
     * The {@link AlertTemplate} class represents a template for a specified alert
     * target type that can be used when creating the content for dispatching
     * {@link Notification}s.
     */
    private final static class AlertTemplate {
      /**
       * The type that this template is for.
       *
       * @see AlertTargetEntity#getNotificationType()
       */
      @XmlAttribute(name = "type", required = true)
      private String m_type;
  
      /**
       * The subject template for the {@link Notification}.
       */
      @XmlElement(name = "subject", required = true)
      private String m_subject;
  
      /**
       * The body template for the {@link Notification}.
       */
      @XmlElement(name = "body", required = true)
      private String m_body;
  
      /**
       * Gets the template type.
       *
       * @return the template type.
       */
      public String getType() {
        return m_type;
      }
  
      /**
       * Gets the subject template.
       *
       * @return the subject template.
       */
      public String getSubject() {
        return m_subject;
      }
  
      /**
       * Gets the body template.
       *
       * @return the body template.
       */
      public String getBody() {
        return m_body;
      }
    }
  }
  