org.apache.ambari.server.state.services.AlertNoticeDispatchService.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.ambari.server.state.services.AlertNoticeDispatchService.java

Source

/**
 * 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.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.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
     */
    private 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<Runnable>(),
                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 {
        List<AlertNoticeEntity> pending = m_dao.findPendingNotices();
        if (pending.size() == 0) {
            return;
        }

        LOG.info("There are {} pending alert notices about to be dispatched...", pending.size());

        Map<AlertTargetEntity, List<AlertNoticeEntity>> aggregateMap = new HashMap<AlertTargetEntity, List<AlertNoticeEntity>>(
                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<AlertNoticeEntity>();
                aggregateMap.put(target, notices);
            }

            notices.add(notice);
        }

        // 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;
            }

            String targetType = target.getNotificationType();
            NotificationDispatcher dispatcher = m_dispatchFactory.getDispatcher(targetType);

            // create a single digest notification if supported
            if (dispatcher.isDigestSupported()) {
                Notification notification = buildNotificationFromTarget(target);
                notification.CallbackIds = new ArrayList<String>(notices.size());
                List<AlertHistoryEntity> histories = new ArrayList<AlertHistoryEntity>(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(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);
                }
            } else {
                // the dispatcher does not support digest, each notice must have a 1:1
                // notification created for it
                for (AlertNoticeEntity notice : notices) {
                    Notification 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(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 Notification 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
        Notification notification = new Notification();
        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<Recipient>(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 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(Notification 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());
        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 (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 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(Notification 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());
        AlertInfo alert = new AlertInfo(history);
        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 (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());
            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<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
     * 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
         */
        protected 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 state of the alert.
         *
         * @return
         */
        public AlertState getAlertState() {
            return m_history.getAlertState();
        }

        /**
         * Gets the descriptive name of the alert.
         *
         * @return
         */
        public String getAlertName() {
            return m_history.getAlertDefinition().getLabel();
        }

        /**
         * 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<String>();

        /**
         * The services that have at least 1 alert reported.
         */
        private final Set<String> m_services = new HashSet<String>();

        /**
         * 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<String, Map<AlertState, List<AlertHistoryEntity>>>();

        /**
         * A mapping of all services by state.
         */
        private final Map<String, Set<String>> m_servicesByState = new HashMap<String, Set<String>>();

        /**
         * A mapping of all alerts by the service that owns them.
         */
        private final Map<String, List<AlertHistoryEntity>> m_alertsByService = new HashMap<String, List<AlertHistoryEntity>>();

        /**
         * 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<AlertState, List<AlertHistoryEntity>>();
                    m_alertsByServiceAndState.put(serviceName, service);
                }

                List<AlertHistoryEntity> alertList = service.get(alertState);
                if (null == alertList) {
                    alertList = new ArrayList<AlertHistoryEntity>();
                    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<String>();
                    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<AlertHistoryEntity>();
                    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) {
            m_version = metaInfo.getServerVersion();
        }

        /**
         * @return the hostName
         */
        public String getHostName() {
            return m_hostName;
        }

        /**
         * @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;
        }
    }
}