com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmManager.java Source code

Java tutorial

Introduction

Here is the source code for com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmManager.java

Source

/*************************************************************************
 * Copyright 2009-2015 Eucalyptus Systems, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see http://www.gnu.org/licenses/.
 *
 * Please contact Eucalyptus Systems, Inc., 6755 Hollister Ave., Goleta
 * CA 93117, USA or visit http://www.eucalyptus.com/licenses/ if you need
 * additional information or have any questions.
 *
 * This file may incorporate work covered under the following copyright
 * and permission notice:
 *
 *   Copyright 2010-2014 Amazon.com, Inc. or its affiliates. All Rights
 *   Reserved.
 *
 *   Licensed under the Apache License, Version 2.0 (the "License").
 *   You may not use this file except in compliance with the License.
 *   A copy of the License is located at
 *
 *   http://aws.amazon.com/apache2.0
 *
 *   or in the "license" file accompanying this file. This file is
 *   distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
 *   ANY KIND, either express or implied. See the License for the specific
 *   language governing permissions and limitations under the License.
 ************************************************************************/

package com.eucalyptus.cloudwatch.common.internal.domain.alarms;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.ExecutionException;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import com.eucalyptus.cloudwatch.common.internal.domain.InvalidTokenException;
import com.eucalyptus.entities.TransactionResource;
import com.eucalyptus.util.async.CheckedListenableFuture;
import com.eucalyptus.util.async.Futures;
import net.sf.json.JSONObject;

import org.apache.log4j.Logger;
import org.apache.tools.ant.taskdefs.Exec;
import org.hibernate.Criteria;
import org.hibernate.criterion.Junction;
import org.hibernate.criterion.Order;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;

import com.eucalyptus.auth.principal.AccountFullName;
import com.eucalyptus.autoscaling.common.AutoScaling;
import com.eucalyptus.autoscaling.common.msgs.AutoScalingMessage;
import com.eucalyptus.autoscaling.common.msgs.ExecutePolicyType;
import com.eucalyptus.cloudwatch.common.CloudWatchMetadata;
import com.eucalyptus.cloudwatch.common.CloudWatchResourceName;
import com.eucalyptus.cloudwatch.common.internal.domain.DimensionEntity;
import com.eucalyptus.cloudwatch.common.internal.domain.NextTokenUtils;
import com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmEntity.ComparisonOperator;
import com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmEntity.StateValue;
import com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmEntity.Statistic;
import com.eucalyptus.cloudwatch.common.internal.domain.alarms.AlarmHistory.HistoryItemType;
import com.eucalyptus.cloudwatch.common.internal.domain.metricdata.MetricEntity.MetricType;
import com.eucalyptus.cloudwatch.common.internal.domain.metricdata.Units;
import com.eucalyptus.component.id.Eucalyptus;
import com.eucalyptus.compute.common.ComputeMessage;
import com.eucalyptus.compute.common.backend.StopInstancesType;
import com.eucalyptus.compute.common.backend.TerminateInstancesType;
import com.eucalyptus.crypto.util.Timestamps;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.util.CollectionUtils;
import com.eucalyptus.util.Callback;
import com.eucalyptus.util.DispatchingClient;
import com.google.common.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Sets;

public class AlarmManager {
    private static final Logger LOG = Logger.getLogger(AlarmManager.class);

    public static Long countMetricAlarms(String accountId) {
        try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
            Criteria criteria = Entities.createCriteria(AlarmEntity.class);
            criteria = criteria.setProjection(Projections.rowCount());
            if (accountId != null) {
                criteria = criteria.add(Restrictions.eq("accountId", accountId));
            }
            return (Long) criteria.uniqueResult();
        }
    }

    public static void putMetricAlarm(String accountId, Boolean actionsEnabled, Collection<String> alarmActions,
            String alarmDescription, String alarmName, ComparisonOperator comparisonOperator,
            Map<String, String> dimensionMap, Integer evaluationPeriods, Collection<String> insufficientDataActions,
            String metricName, MetricType metricType, String namespace, Collection<String> okActions,
            Integer period, Statistic statistic, Double threshold, Units unit) {

        if (dimensionMap == null) {
            dimensionMap = Maps.newHashMap();
        } else if (dimensionMap.size() > AlarmEntity.MAX_DIM_NUM) {
            throw new IllegalArgumentException("Too many dimensions for metric, " + dimensionMap.size());
        }

        AlarmEntity alarmEntity = new AlarmEntity();
        alarmEntity.setAccountId(accountId);
        alarmEntity.setAlarmName(alarmName);
        try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
            boolean inDb = false;
            Criteria criteria = Entities.createCriteria(AlarmEntity.class)
                    .add(Restrictions.eq("accountId", accountId)).add(Restrictions.eq("alarmName", alarmName));
            AlarmEntity inDbAlarm = (AlarmEntity) criteria.uniqueResult();
            if (inDbAlarm != null) {
                inDb = true;
                alarmEntity = inDbAlarm;
            }
            alarmEntity.setActionsEnabled(actionsEnabled);
            alarmEntity.setAlarmActions(alarmActions);
            alarmEntity.setAlarmDescription(alarmDescription);
            alarmEntity.setComparisonOperator(comparisonOperator);
            TreeSet<DimensionEntity> dimensions = Sets.newTreeSet();
            for (Map.Entry<String, String> entry : dimensionMap.entrySet()) {
                DimensionEntity d = new DimensionEntity();
                d.setName(entry.getKey());
                d.setValue(entry.getValue());
                dimensions.add(d);
            }
            alarmEntity.setDimensions(dimensions);
            alarmEntity.setEvaluationPeriods(evaluationPeriods);
            alarmEntity.setInsufficientDataActions(insufficientDataActions);
            alarmEntity.setMetricName(metricName);
            alarmEntity.setMetricType(metricType);
            alarmEntity.setNamespace(namespace);
            alarmEntity.setOkActions(okActions);
            alarmEntity.setPeriod(period);
            alarmEntity.setStatistic(statistic);
            alarmEntity.setThreshold(threshold);
            alarmEntity.setUnit(unit);
            Date now = new Date();
            alarmEntity.setAlarmConfigurationUpdatedTimestamp(now);
            if (!inDb) {
                alarmEntity.setStateValue(StateValue.INSUFFICIENT_DATA);
                alarmEntity.setStateReason("Unchecked: Initial alarm creation");
                alarmEntity.setStateUpdatedTimestamp(now);
                // TODO: revisit (we are not firing actions on alarm creation, but may after one period)
                alarmEntity.setLastActionsUpdatedTimestamp(now);
                JSONObject historyDataJSON = new JSONObject();
                historyDataJSON.element("version", "1.0");
                historyDataJSON.element("type", "Create");
                JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity(alarmEntity);
                historyDataJSON.element("createdAlarm", historyDataDeletedAlarmJSON);
                String historyData = historyDataJSON.toString();
                AlarmManager.addAlarmHistoryItem(accountId, alarmName, historyData,
                        HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" created", now);
                Entities.persist(alarmEntity);
            } else {
                JSONObject historyDataJSON = new JSONObject();
                historyDataJSON.element("version", "1.0");
                historyDataJSON.element("type", "Update");
                JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity(alarmEntity);
                historyDataJSON.element("updatedAlarm", historyDataDeletedAlarmJSON);
                String historyData = historyDataJSON.toString();
                AlarmManager.addAlarmHistoryItem(accountId, alarmName, historyData,
                        HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" updated", now);
            }
            db.commit();
        }
    }

    static void addAlarmHistoryItem(String accountId, String alarmName, String historyData,
            HistoryItemType historyItemType, String historySummary, Date now) {
        if (now == null)
            now = new Date();
        try (final TransactionResource db = Entities.transactionFor(AlarmHistory.class)) {
            AlarmHistory alarmHistory = createAlarmHistoryItem(accountId, alarmName, historyData, historyItemType,
                    historySummary, now);
            Entities.persist(alarmHistory);
            db.commit();
        }
    }

    public static AlarmHistory createAlarmHistoryItem(String accountId, String alarmName, String historyData,
            HistoryItemType historyItemType, String historySummary, Date now) {
        AlarmHistory alarmHistory = new AlarmHistory();
        alarmHistory.setAccountId(accountId);
        alarmHistory.setAlarmName(alarmName);
        alarmHistory.setHistoryData(historyData);
        alarmHistory.setHistoryItemType(historyItemType);
        alarmHistory.setHistorySummary(historySummary);
        alarmHistory.setTimestamp(now);
        return alarmHistory;
    }

    public static boolean deleteAlarms(final String accountId, final Collection<String> alarmNames,
            final Predicate<CloudWatchMetadata.AlarmMetadata> filter) {
        return modifySelectedAlarms(accountId, alarmNames, filter, new Predicate<AlarmEntity>() {
            private final Date now = new Date();

            @Override
            public boolean apply(final AlarmEntity alarmEntity) {
                final String alarmName = alarmEntity.getAlarmName();
                JSONObject historyDataJSON = new JSONObject();
                historyDataJSON.element("version", "1.0");
                historyDataJSON.element("type", "Delete");
                JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity(alarmEntity);
                historyDataJSON.element("deletedAlarm", historyDataDeletedAlarmJSON);
                String historyData = historyDataJSON.toString();
                AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmName, historyData,
                        HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" deleted", now);
                Entities.delete(alarmEntity);
                return true;
            }
        });
    }

    private static JSONObject getJSONObjectFromAlarmEntity(AlarmEntity alarmEntity) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.element("threshold", alarmEntity.getThreshold());
        jsonObject.element("namespace", alarmEntity.getNamespace());
        jsonObject.element("stateValue", alarmEntity.getStateValue().toString());
        ArrayList<JSONObject> dimensions = new ArrayList<JSONObject>();
        if (alarmEntity.getDimensions() != null) {
            for (DimensionEntity dimensionEntity : alarmEntity.getDimensions()) {
                JSONObject dimension = new JSONObject();
                dimension.element("name", dimensionEntity.getName());
                dimension.element("value", dimensionEntity.getValue());
                dimensions.add(dimension);
            }
        }
        jsonObject.element("dimensions", dimensions);
        jsonObject.element("okactions",
                alarmEntity.getOkActions() != null ? alarmEntity.getOkActions() : new ArrayList<String>());
        jsonObject.element("alarmActions",
                alarmEntity.getAlarmActions() != null ? alarmEntity.getAlarmActions() : new ArrayList<String>());
        jsonObject.element("evaluationPeriods", alarmEntity.getEvaluationPeriods());
        jsonObject.element("comparisonOperator", alarmEntity.getComparisonOperator().toString());
        jsonObject.element("metricName", alarmEntity.getMetricName());
        jsonObject.element("period", alarmEntity.getPeriod());
        jsonObject.element("alarmName", alarmEntity.getAlarmName());
        jsonObject.element("insufficientDataActions",
                alarmEntity.getInsufficientDataActions() != null ? alarmEntity.getInsufficientDataActions()
                        : new ArrayList<String>());
        jsonObject.element("actionsEnabled", alarmEntity.getActionsEnabled());
        jsonObject.element("alarmDescription", alarmEntity.getAlarmDescription());
        jsonObject.element("statistic", alarmEntity.getStatistic());
        jsonObject.element("alarmArn", alarmEntity.getResourceName());
        jsonObject.element("alarmConfigurationUpdatedTimestamp", Timestamps
                .formatIso8601UTCLongDateMillisTimezone(alarmEntity.getAlarmConfigurationUpdatedTimestamp()));
        jsonObject.element("stateUpdatedTimestamp",
                Timestamps.formatIso8601UTCLongDateMillisTimezone(alarmEntity.getStateUpdatedTimestamp()));
        return jsonObject;
    }

    /**
     * @return False if enable rejected due to filter
     */
    public static boolean enableAlarmActions(final String accountId, final Collection<String> alarmNames,
            final Predicate<CloudWatchMetadata.AlarmMetadata> filter) {
        return modifySelectedAlarms(accountId, alarmNames, filter, new Predicate<AlarmEntity>() {
            private final Date now = new Date();

            @Override
            public boolean apply(final AlarmEntity alarmEntity) {
                final String alarmName = alarmEntity.getAlarmName();
                if (!Boolean.TRUE.equals(alarmEntity.getActionsEnabled())) {
                    JSONObject historyDataJSON = new JSONObject();
                    historyDataJSON.element("version", "1.0");
                    historyDataJSON.element("type", "Update");
                    JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity(alarmEntity);
                    historyDataJSON.element("updatedAlarm", historyDataDeletedAlarmJSON);
                    String historyData = historyDataJSON.toString();
                    AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmName, historyData,
                            HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" updated", now);
                    alarmEntity.setActionsEnabled(Boolean.TRUE);
                }
                return true;
            }
        });
    }

    /**
     * @return False if disable rejected due to filter
     */
    public static boolean disableAlarmActions(final String accountId, final Collection<String> alarmNames,
            final Predicate<CloudWatchMetadata.AlarmMetadata> filter) {
        return modifySelectedAlarms(accountId, alarmNames, filter, new Predicate<AlarmEntity>() {
            private final Date now = new Date();

            @Override
            public boolean apply(final AlarmEntity alarmEntity) {
                final String alarmName = alarmEntity.getAlarmName();
                if (!Boolean.FALSE.equals(alarmEntity.getActionsEnabled())) {
                    JSONObject historyDataJSON = new JSONObject();
                    historyDataJSON.element("version", "1.0");
                    historyDataJSON.element("type", "Update");
                    JSONObject historyDataDeletedAlarmJSON = getJSONObjectFromAlarmEntity(alarmEntity);
                    historyDataJSON.element("updatedAlarm", historyDataDeletedAlarmJSON);
                    String historyData = historyDataJSON.toString();
                    AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmName, historyData,
                            HistoryItemType.ConfigurationUpdate, "Alarm \"" + alarmName + "\" updated", now);
                    alarmEntity.setActionsEnabled(Boolean.FALSE);
                }
                return true;
            }
        });
    }

    private static boolean modifySelectedAlarms(final String accountId, final Collection<String> alarmNames,
            final Predicate<CloudWatchMetadata.AlarmMetadata> filter, final Predicate<AlarmEntity> update) {
        final Map<String, Collection<String>> accountToNamesMap = buildAccountIdToAlarmNamesMap(accountId,
                alarmNames);
        try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
            final Criteria criteria = Entities.createCriteria(AlarmEntity.class);
            final Junction disjunction = Restrictions.disjunction();
            for (final Map.Entry<String, Collection<String>> entry : accountToNamesMap.entrySet()) {
                final Junction conjunction = Restrictions.conjunction();
                conjunction.add(Restrictions.eq("accountId", entry.getKey()));
                conjunction.add(Restrictions.in("alarmName", entry.getValue()));
                disjunction.add(conjunction);
            }
            criteria.add(disjunction);
            criteria.addOrder(Order.asc("creationTimestamp"));
            criteria.addOrder(Order.asc("naturalId"));
            final Collection<AlarmEntity> alarmEntities = (Collection<AlarmEntity>) criteria.list();
            if (!Iterables.all(alarmEntities, filter)) {
                return false;
            }
            CollectionUtils.each(alarmEntities, update);
            db.commit();
            return true;
        }
    }

    private static Map<String, Collection<String>> buildAccountIdToAlarmNamesMap(@Nullable final String accountId,
            @Nullable final Collection<String> alarmNames) {
        final Multimap<String, String> alarmNamesMultimap = HashMultimap.create();
        if (alarmNames != null) {
            if (accountId != null) {
                alarmNamesMultimap.putAll(accountId, alarmNames); // An ARN is also a valid name
            }
            CollectionUtils.putAll(
                    Optional.presentInstances(Iterables.transform(alarmNames,
                            CloudWatchResourceName.asArnOfType(CloudWatchResourceName.Type.alarm))),
                    alarmNamesMultimap, CloudWatchResourceName.toNamespace(), CloudWatchResourceName.toName());
        }
        return alarmNamesMultimap.asMap();
    }

    public static void setAlarmState(final String accountId, final String alarmName, final String stateReason,
            final String stateReasonData, final StateValue stateValue,
            final Predicate<CloudWatchMetadata.AlarmMetadata> filter) throws AlarmNotFoundException {
        try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
            AlarmEntity alarmEntity = (AlarmEntity) Entities.createCriteria(AlarmEntity.class)
                    .add(Restrictions.eq("accountId", accountId)).add(Restrictions.eq("alarmName", alarmName))
                    .uniqueResult();
            if (alarmEntity == null && CloudWatchResourceName.isResourceName().apply(alarmName))
                try {
                    final CloudWatchResourceName arn = CloudWatchResourceName.parse(alarmName,
                            CloudWatchResourceName.Type.alarm);
                    alarmEntity = (AlarmEntity) Entities.createCriteria(AlarmEntity.class)
                            .add(Restrictions.eq("accountId", arn.getNamespace()))
                            .add(Restrictions.eq("alarmName", arn.getName())).uniqueResult();
                } catch (CloudWatchResourceName.InvalidResourceNameException e) {
                }
            if (alarmEntity == null || !filter.apply(alarmEntity)) {
                throw new AlarmNotFoundException("Could not find alarm with name '" + alarmName + "'");
            }
            StateValue oldStateValue = alarmEntity.getStateValue();
            if (stateValue != oldStateValue) {
                Date evaluationDate = new Date();
                AlarmState newState = createAlarmState(stateValue, stateReason, stateReasonData);
                AlarmManager.changeAlarmState(alarmEntity, newState, evaluationDate);
                AlarmManager.executeActions(alarmEntity, newState, true, evaluationDate);
            }
            db.commit();
        }
    }

    public static List<AlarmEntity> describeAlarms(@Nullable final String accountId,
            @Nullable final String actionPrefix, @Nullable final String alarmNamePrefix,
            @Nullable final Collection<String> alarmNames, @Nullable final Integer maxRecords,
            @Nullable final StateValue stateValue, @Nullable final String nextToken,
            final Predicate<? super CloudWatchMetadata.AlarmMetadata> filter) throws InvalidTokenException {
        final List<AlarmEntity> results = Lists.newArrayList();
        try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
            boolean first = true;
            String token = nextToken;
            while (token != null || first) {
                first = false;
                final Date nextTokenCreatedTime = NextTokenUtils.getNextTokenCreatedTime(token, AlarmEntity.class);
                final Criteria criteria = Entities.createCriteria(AlarmEntity.class);
                if (accountId != null) {
                    criteria.add(Restrictions.eq("accountId", accountId));
                }
                if (actionPrefix != null) {
                    final Junction actionsOf = Restrictions.disjunction();
                    for (int i = 1; i <= AlarmEntity.MAX_OK_ACTIONS_NUM; i++) {
                        actionsOf.add(Restrictions.like("okAction" + i, actionPrefix + "%")); // May need Restrictions.ilike for case insensitive
                    }
                    for (int i = 1; i <= AlarmEntity.MAX_ALARM_ACTIONS_NUM; i++) {
                        actionsOf.add(Restrictions.like("alarmAction" + i, actionPrefix + "%")); // May need Restrictions.ilike for case insensitive
                    }
                    for (int i = 1; i <= AlarmEntity.MAX_INSUFFICIENT_DATA_ACTIONS_NUM; i++) {
                        actionsOf.add(Restrictions.like("insufficientDataAction" + i, actionPrefix + "%")); // May need Restrictions.ilike for case insensitive
                    }
                    criteria.add(actionsOf);
                }
                if (alarmNamePrefix != null) {
                    criteria.add(Restrictions.like("alarmName", alarmNamePrefix + "%"));
                }
                if (alarmNames != null && !alarmNames.isEmpty()) {
                    criteria.add(Restrictions.in("alarmName", alarmNames));
                }
                if (stateValue != null) {
                    criteria.add(Restrictions.eq("stateValue", stateValue));
                }
                NextTokenUtils.addNextTokenConstraints(maxRecords == null ? null : maxRecords - results.size(),
                        token, nextTokenCreatedTime, criteria);
                final List<AlarmEntity> alarmEntities = (List<AlarmEntity>) criteria.list();
                Iterables.addAll(results, Iterables.filter(alarmEntities, filter));
                token = maxRecords == null || (maxRecords != null
                        && (results.size() >= maxRecords || alarmEntities.size() < maxRecords)) ? null
                                : alarmEntities.get(alarmEntities.size() - 1).getNaturalId();
            }
            db.commit();
        }
        return results;
    }

    public static Collection<AlarmEntity> describeAlarmsForMetric(@Nullable final String accountId,
            @Nonnull final Map<String, String> dimensionMap, @Nullable final String metricName,
            @Nullable final String namespace, @Nullable final Integer period, @Nullable final Statistic statistic,
            @Nullable final Units unit, @Nonnull final Predicate<? super CloudWatchMetadata.AlarmMetadata> filter) {
        final List<AlarmEntity> results = Lists.newArrayList();
        try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
            final Criteria criteria = Entities.createCriteria(AlarmEntity.class);
            if (accountId != null) {
                criteria.add(Restrictions.eq("accountId", accountId));
            }
            final Set<DimensionEntity> dimensions = Sets.newTreeSet();
            for (final Map.Entry<String, String> entry : dimensionMap.entrySet()) {
                final DimensionEntity d = new DimensionEntity();
                d.setName(entry.getKey());
                d.setValue(entry.getValue());
                dimensions.add(d);
            }
            int dimIndex = 1;
            for (final DimensionEntity d : dimensions) {
                criteria.add(Restrictions.eq("dim" + dimIndex + "Name", d.getName()));
                criteria.add(Restrictions.eq("dim" + dimIndex + "Value", d.getValue()));
                dimIndex++;
            }
            while (dimIndex <= AlarmEntity.MAX_DIM_NUM) {
                criteria.add(Restrictions.isNull("dim" + dimIndex + "Name"));
                criteria.add(Restrictions.isNull("dim" + dimIndex + "Value"));
                dimIndex++;
            }

            if (metricName != null) {
                criteria.add(Restrictions.eq("metricName", metricName));
            }
            if (namespace != null) {
                criteria.add(Restrictions.eq("namespace", namespace));
            }
            if (period != null) {
                criteria.add(Restrictions.eq("period", period));
            }
            if (statistic != null) {
                criteria.add(Restrictions.eq("statistic", statistic));
            }
            if (unit != null) {
                criteria.add(Restrictions.eq("unit", unit));
            }
            final List<AlarmEntity> alarmEntities = (List<AlarmEntity>) criteria.list();
            Iterables.addAll(results, Iterables.filter(alarmEntities, filter));
            db.commit();
        }
        return results;
    }

    public static List<AlarmHistory> describeAlarmHistory(@Nullable final String accountId,
            @Nullable final String alarmName, @Nullable final Date endDate,
            @Nullable final HistoryItemType historyItemType, @Nullable final Integer maxRecords,
            @Nullable final Date startDate, @Nullable final String nextToken, final Predicate<AlarmHistory> filter)
            throws InvalidTokenException {
        final List<AlarmHistory> results = Lists.newArrayList();
        try (final TransactionResource db = Entities.transactionFor(AlarmHistory.class)) {
            final Map<String, Collection<String>> accountToNamesMap = alarmName == null
                    ? Collections.<String, Collection<String>>emptyMap()
                    : buildAccountIdToAlarmNamesMap(accountId, Collections.singleton(alarmName));
            boolean first = true;
            String token = nextToken;
            while (token != null || first) {
                first = false;
                final Date nextTokenCreatedTime = NextTokenUtils.getNextTokenCreatedTime(token, AlarmHistory.class);
                final Criteria criteria = Entities.createCriteria(AlarmHistory.class);
                final Junction disjunction = Restrictions.disjunction();
                for (final Map.Entry<String, Collection<String>> entry : accountToNamesMap.entrySet()) {
                    final Junction conjunction = Restrictions.conjunction();
                    conjunction.add(Restrictions.eq("accountId", entry.getKey()));
                    conjunction.add(Restrictions.in("alarmName", entry.getValue()));
                    disjunction.add(conjunction);
                }
                criteria.add(disjunction);
                if (historyItemType != null) {
                    criteria.add(Restrictions.eq("historyItemType", historyItemType));
                }
                if (startDate != null) {
                    criteria.add(Restrictions.ge("timestamp", startDate));
                }
                if (endDate != null) {
                    criteria.add(Restrictions.le("timestamp", endDate));
                }
                NextTokenUtils.addNextTokenConstraints(maxRecords == null ? null : maxRecords - results.size(),
                        token, nextTokenCreatedTime, criteria);
                final List<AlarmHistory> alarmHistoryEntities = (List<AlarmHistory>) criteria.list();
                Iterables.addAll(results, Iterables.filter(alarmHistoryEntities, filter));
                token = maxRecords == null || (maxRecords != null
                        && (results.size() >= maxRecords || alarmHistoryEntities.size() < maxRecords)) ? null
                                : alarmHistoryEntities.get(alarmHistoryEntities.size() - 1).getNaturalId();
            }
            db.commit();
        }
        return results;
    }

    /**
     * Delete all alarm history before a certain date
     * @param before the date to delete before (inclusive)
     */
    public static void deleteAlarmHistory(Date before) {
        try (final TransactionResource db = Entities.transactionFor(AlarmHistory.class)) {
            Map<String, Date> criteria = Maps.newHashMap();
            criteria.put("before", before);
            Entities.deleteAllMatching(AlarmHistory.class, "WHERE timestamp < :before", criteria);
            db.commit();
        }
    }

    public static void changeAlarmState(AlarmEntity alarmEntity, AlarmState newState, Date now) {
        LOG.debug("Updating alarm " + alarmEntity.getAlarmName() + " from " + alarmEntity.getStateValue() + " to "
                + newState.getStateValue());
        alarmEntity.setStateUpdatedTimestamp(now);
        JSONObject historyDataJSON = getJSONObjectForStateChange(alarmEntity, newState);
        String historyData = historyDataJSON.toString();
        AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
                HistoryItemType.StateUpdate,
                " Alarm updated from " + alarmEntity.getStateValue() + " to " + newState.getStateValue(), now);
        alarmEntity.setStateReason(newState.getStateReason());
        alarmEntity.setStateReasonData(newState.getStateReasonData());
        alarmEntity.setStateValue(newState.getStateValue());
        alarmEntity.setStateUpdatedTimestamp(now);
    }

    private static JSONObject getJSONObjectForStateChange(AlarmEntity alarmEntity, AlarmState newState) {
        JSONObject historyDataJSON = new JSONObject();
        historyDataJSON.element("version", "1.0");
        historyDataJSON.element("oldState", getJSONObjectFromState(alarmEntity.getStateValue(),
                alarmEntity.getStateReason(), alarmEntity.getStateReasonData()));
        historyDataJSON.element("newState", getJSONObjectFromState(newState.getStateValue(),
                newState.getStateReason(), newState.getStateReasonData()));
        return historyDataJSON;
    }

    private static JSONObject getJSONObjectFromState(StateValue stateValue, String stateReason,
            String stateReasonData) {
        JSONObject jsonObject = new JSONObject();
        jsonObject.element("stateValue", stateValue.toString());
        jsonObject.element("stateReason", stateReason);
        if (stateReasonData != null) {
            jsonObject.element("stateReasonData", stateReasonData);
        }
        return jsonObject;
    }

    public static void executeActions(AlarmEntity alarmEntity, AlarmState state, boolean stateJustChanged,
            Date now) {
        if (alarmEntity.getActionsEnabled()) {
            Collection<String> actions = AlarmUtils.getActionsByState(alarmEntity, state);
            for (String action : actions) {
                Action actionToExecute = ActionManager.getAction(action, alarmEntity.getDimensionMap());
                if (actionToExecute == null) {
                    LOG.warn("Unsupported action " + action); // TODO: do not let it in to start with...
                }
                // always execute autoscaling actions, but others only on state change...
                else if (actionToExecute.alwaysExecute() || stateJustChanged) {
                    LOG.debug("Executing alarm " + alarmEntity.getAccountId() + "/" + alarmEntity.getAlarmName()
                            + " action " + action);
                    actionToExecute.executeAction(action, alarmEntity.getDimensionMap(), alarmEntity, now);
                }
            }
        }
        alarmEntity.setLastActionsUpdatedTimestamp(now);
    }

    public static Collection<AlarmHistory> executeActionsAndRecord(AlarmEntity alarmEntity, AlarmState state,
            boolean stateJustChanged, Date now, List<AlarmHistory> historyList) {
        List<AlarmHistory> alarmHistoryList = Lists.newArrayList();
        if (alarmEntity.getActionsEnabled()) {
            Collection<String> actions = AlarmUtils.getActionsByState(alarmEntity, state);
            for (String action : actions) {
                Action actionToExecute = ActionManager.getAction(action, alarmEntity.getDimensionMap());
                if (actionToExecute == null) {
                    LOG.warn("Unsupported action " + action); // TODO: do not let it in to start with...
                }
                // always execute autoscaling actions, but others only on state change...
                else if (actionToExecute.alwaysExecute() || stateJustChanged) {
                    LOG.debug("Executing alarm " + alarmEntity.getAccountId() + "/" + alarmEntity.getAlarmName()
                            + " action " + action);
                    alarmHistoryList.add(actionToExecute.executeActionAndRecord(action,
                            alarmEntity.getDimensionMap(), alarmEntity, now));
                }
            }
        }
        return alarmHistoryList;
    }

    private static String createStateReasonData(StateValue stateValue, List<Double> relevantDataPoints,
            List<Double> recentDataPoints, ComparisonOperator comparisonOperator, Double threshold,
            String stateReason, Integer period, Date queryDate, Statistic statistic) {
        JSONObject stateReasonDataJSON = new JSONObject();
        stateReasonDataJSON.element("version", "1.0");
        stateReasonDataJSON.element("queryDate", Timestamps.formatIso8601UTCLongDateMillisTimezone(queryDate));
        stateReasonDataJSON.element("statistic", statistic.toString());
        stateReasonDataJSON.element("recentDatapoints", pruneNullsAtBeginning(recentDataPoints));
        stateReasonDataJSON.element("period", period);
        stateReasonDataJSON.element("threshold", threshold);
        String stateReasonData = stateReasonDataJSON.toString();
        return stateReasonData;
    }

    private static List<Double> pruneNullsAtBeginning(List<Double> recentDataPoints) {
        ArrayList<Double> returnValue = new ArrayList<Double>();
        boolean foundNotNull = false;
        for (Double recentDataPoint : recentDataPoints) {
            if (recentDataPoint != null) {
                foundNotNull = true;
            }
            if (foundNotNull) {
                returnValue.add(recentDataPoint);
            }
        }
        return returnValue;
    }

    private static String createStateReason(StateValue stateValue, List<Double> relevantDataPoints,
            ComparisonOperator comparisonOperator, Double threshold) {
        String stateReason = null;
        if (stateValue == StateValue.INSUFFICIENT_DATA) {
            stateReason = "Insufficient Data: " + relevantDataPoints.size() + AlarmUtils.matchSingularPlural(
                    relevantDataPoints.size(), " datapoint was ", " datapoints were ") + "unknown.";
        } else {
            stateReason = "Threshold Crossed: " + relevantDataPoints.size()
                    + AlarmUtils.matchSingularPlural(relevantDataPoints.size(), " datapoint ", " datapoints ")
                    + AlarmUtils.makeDoubleList(relevantDataPoints)
                    + AlarmUtils.matchSingularPlural(relevantDataPoints.size(), " was ", " were ")
                    + (stateValue == StateValue.OK ? " not " : "")
                    + AlarmUtils.comparisonOperatorString(comparisonOperator) + " the threshold (" + threshold
                    + ").";
        }
        return stateReason;
    }

    public static AlarmState createAlarmState(StateValue stateValue, List<Double> relevantDataPoints,
            List<Double> recentDataPoints, ComparisonOperator comparisonOperator, Double threshold, Integer period,
            Date queryDate, Statistic statistic) {
        String stateReason = createStateReason(stateValue, relevantDataPoints, comparisonOperator, threshold);
        return createAlarmState(stateValue, relevantDataPoints, recentDataPoints, comparisonOperator, threshold,
                stateReason, period, queryDate, statistic);
    }

    static AlarmState createAlarmState(StateValue stateValue, List<Double> relevantDataPoints,
            List<Double> recentDataPoints, ComparisonOperator comparisonOperator, Double threshold,
            String stateReason, Integer period, Date queryDate, Statistic statistic) {
        String stateReasonData = createStateReasonData(stateValue, relevantDataPoints, recentDataPoints,
                comparisonOperator, threshold, stateReason, period, queryDate, statistic);
        return new AlarmState(stateValue, stateReason, stateReasonData);
    }

    private static AlarmState createAlarmState(StateValue stateValue, String stateReason, String stateReasonData) {
        return new AlarmState(stateValue, stateReason, stateReasonData);
    }

    public static AlarmHistory createChangeAlarmStateHistoryItem(AlarmEntity alarmEntity, AlarmState newState,
            Date evaluationDate) {
        JSONObject historyDataJSON = getJSONObjectForStateChange(alarmEntity, newState);
        String historyData = historyDataJSON.toString();
        return createAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
                HistoryItemType.StateUpdate,
                " Alarm updated from " + alarmEntity.getStateValue() + " to " + newState.getStateValue(),
                evaluationDate);
    }

    public static void changeAlarmStateBatch(Map<String, AlarmState> statesToUpdate, Date evaluationDate) {
        if (statesToUpdate.isEmpty())
            return;
        try (final TransactionResource db = Entities.transactionFor(AlarmEntity.class)) {
            Criteria criteria = Entities.createCriteria(AlarmEntity.class);
            criteria = criteria.add(Restrictions.in("naturalId", statesToUpdate.keySet()));
            List<AlarmEntity> result = criteria.list();
            for (AlarmEntity alarmEntity : result) {
                AlarmState newState = statesToUpdate.get(alarmEntity.getNaturalId());
                if (newState != null) {
                    alarmEntity.setStateReason(newState.getStateReason());
                    alarmEntity.setStateReasonData(newState.getStateReasonData());
                    alarmEntity.setStateValue(newState.getStateValue());
                    alarmEntity.setStateUpdatedTimestamp(evaluationDate);
                }
            }
            db.commit();
        }
    }

    public static void addAlarmHistoryEvents(List<AlarmHistory> historyList) {
        try (final TransactionResource db = Entities.transactionFor(AlarmHistory.class)) {
            for (AlarmHistory alarmHistory : historyList) {
                Entities.persist(alarmHistory);
            }
            db.commit();
        }
    }

    private static class AutoScalingClient extends DispatchingClient<AutoScalingMessage, AutoScaling> {
        public AutoScalingClient(final String userId) {
            super(userId, AutoScaling.class);
        }

        public AutoScalingClient(final AccountFullName accountFullName) {
            super(accountFullName, AutoScaling.class);
        }
    }

    private static class EucalyptusClient extends DispatchingClient<ComputeMessage, Eucalyptus> {
        public EucalyptusClient(final String userId) {
            super(userId, Eucalyptus.class);
        }

        public EucalyptusClient(final AccountFullName accountFullName) {
            super(accountFullName, Eucalyptus.class);
        }
    }

    private static abstract class Action {
        public abstract boolean filter(final String actionURN, final Map<String, String> dimensionMap);

        public abstract void executeAction(final String actionARN, final Map<String, String> dimensionMap,
                final AlarmEntity alarmEntity, final Date now);

        public abstract boolean alwaysExecute();

        <R> Callback.Checked<R> getCallback(final String action, final AlarmEntity alarmEntity, final Date now) {
            return new Callback.Checked<R>() {
                @Override
                public void fire(R input) {
                    success(action, alarmEntity, now);
                }

                @Override
                public void fireException(Throwable t) {
                    failure(action, alarmEntity, now, t);
                }
            };
        }

        <R> Callback.Checked<R> getRecordCallback(final String action, final AlarmEntity alarmEntity,
                final Date now, final CheckedListenableFuture<AlarmHistory> resultFuture) {
            return new Callback.Checked<R>() {
                @Override
                public void fire(R input) {
                    resultFuture.set(recordSuccess(action, alarmEntity, now));
                }

                @Override
                public void fireException(Throwable t) {
                    resultFuture.setException(t);
                }
            };
        }

        public void success(final String actionARN, final AlarmEntity alarmEntity, final Date now) {
            JSONObject historyDataJSON = getSuccessJSON(actionARN, alarmEntity);
            String historyData = historyDataJSON.toString();
            AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
                    HistoryItemType.Action, " Successfully executed action " + actionARN, now);
        }

        private JSONObject getSuccessJSON(String actionARN, AlarmEntity alarmEntity) {
            JSONObject historyDataJSON = new JSONObject();
            historyDataJSON.element("actionState", "Succeeded");
            historyDataJSON.element("notificationResource", actionARN);
            historyDataJSON.element("stateUpdateTimestamp",
                    Timestamps.formatIso8601UTCLongDateMillisTimezone(alarmEntity.getStateUpdatedTimestamp()));
            return historyDataJSON;
        }

        public void failure(final String actionARN, final AlarmEntity alarmEntity, final Date now,
                Throwable cause) {
            JSONObject historyDataJSON = getFailureJSON(actionARN, alarmEntity, cause);
            String historyData = historyDataJSON.toString();
            AlarmManager.addAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(), historyData,
                    HistoryItemType.Action, " Failed to execute action " + actionARN, now);
        }

        private JSONObject getFailureJSON(String actionARN, AlarmEntity alarmEntity, Throwable cause) {
            JSONObject historyDataJSON = new JSONObject();
            historyDataJSON.element("actionState", "Failed");
            historyDataJSON.element("notificationResource", actionARN);
            historyDataJSON.element("stateUpdateTimestamp",
                    Timestamps.formatIso8601UTCLongDateMillisTimezone(alarmEntity.getStateUpdatedTimestamp()));
            historyDataJSON.element("error",
                    cause.getMessage() != null ? cause.getMessage() : cause.getClass().getName());
            return historyDataJSON;
        }

        public AlarmHistory recordSuccess(final String actionARN, final AlarmEntity alarmEntity, final Date now) {
            JSONObject historyDataJSON = getSuccessJSON(actionARN, alarmEntity);
            String historyData = historyDataJSON.toString();
            return AlarmManager.createAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(),
                    historyData, HistoryItemType.Action, " Successfully executed action " + actionARN, now);
        }

        public AlarmHistory recordFailure(final String actionARN, final AlarmEntity alarmEntity, final Date now,
                Throwable cause) {
            JSONObject historyDataJSON = getFailureJSON(actionARN, alarmEntity, cause);
            String historyData = historyDataJSON.toString();
            return AlarmManager.createAlarmHistoryItem(alarmEntity.getAccountId(), alarmEntity.getAlarmName(),
                    historyData, HistoryItemType.Action, " Failed to execute action " + actionARN, now);
        }

        public abstract AlarmHistory executeActionAndRecord(final String action,
                final Map<String, String> dimensionMap, final AlarmEntity alarmEntity, final Date now);
    }

    private static class ExecuteAutoScalingPolicyAction extends Action {
        @Override
        public boolean filter(String action, Map<String, String> dimensionMap) {
            return (action != null && action.startsWith("arn:aws:autoscaling:"));
        }

        @Override
        public void executeAction(final String action, final Map<String, String> dimensionMap,
                final AlarmEntity alarmEntity, final Date now) {
            ExecutePolicyType executePolicyType = new ExecutePolicyType();
            executePolicyType.setPolicyName(action);
            executePolicyType.setHonorCooldown(true);
            Callback.Checked<AutoScalingMessage> callback = getCallback(action, alarmEntity, now);
            try {
                AutoScalingClient client = new AutoScalingClient(
                        AccountFullName.getInstance(alarmEntity.getAccountId()));
                client.init();
                client.dispatch(executePolicyType, callback);
            } catch (Exception ex) {
                failure(action, alarmEntity, now, ex);
            }
        }

        @Override
        public AlarmHistory executeActionAndRecord(final String action, final Map<String, String> dimensionMap,
                final AlarmEntity alarmEntity, final Date now) {
            ExecutePolicyType executePolicyType = new ExecutePolicyType();
            executePolicyType.setPolicyName(action);
            executePolicyType.setHonorCooldown(true);
            CheckedListenableFuture<AlarmHistory> alarmHistoryFuture = Futures.newGenericeFuture();
            Callback.Checked<AutoScalingMessage> callback = getRecordCallback(action, alarmEntity, now,
                    alarmHistoryFuture);
            try {
                AutoScalingClient client = new AutoScalingClient(
                        AccountFullName.getInstance(alarmEntity.getAccountId()));
                client.init();
                client.dispatch(executePolicyType, callback);
            } catch (Exception ex) {
                alarmHistoryFuture.set(recordFailure(action, alarmEntity, now, ex));
            }
            try {
                return alarmHistoryFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                Throwable cause = (e instanceof ExecutionException) ? e.getCause() : e;
                return recordFailure(action, alarmEntity, now, e);
            }
        }

        @Override
        public boolean alwaysExecute() {
            return true;
        }

    }

    private static class TerminateInstanceAction extends Action {

        @Override
        public boolean filter(String action, Map<String, String> dimensionMap) {
            if (action == null)
                return false;
            // Example:
            // arn:aws:automate:us-east-1:ec2:terminate
            if (!action.startsWith("arn:aws:automate:"))
                return false;
            if (!action.endsWith(":ec2:terminate"))
                return false;
            if (dimensionMap == null)
                return false;
            return (dimensionMap.containsKey("InstanceId"));
        }

        @Override
        public void executeAction(final String action, final Map<String, String> dimensionMap,
                final AlarmEntity alarmEntity, final Date now) {
            TerminateInstancesType terminateInstances = new TerminateInstancesType();
            terminateInstances.getInstancesSet().add(dimensionMap.get("InstanceId"));
            Callback.Checked<ComputeMessage> callback = getCallback(action, alarmEntity, now);
            try {
                EucalyptusClient client = new EucalyptusClient(
                        AccountFullName.getInstance(alarmEntity.getAccountId()));
                client.init();
                client.dispatch(terminateInstances, callback);
            } catch (Exception ex) {
                failure(action, alarmEntity, now, ex);
            }
        }

        @Override
        public boolean alwaysExecute() {
            return false;
        }

        @Override
        public AlarmHistory executeActionAndRecord(final String action, final Map<String, String> dimensionMap,
                final AlarmEntity alarmEntity, final Date now) {
            TerminateInstancesType terminateInstances = new TerminateInstancesType();
            terminateInstances.getInstancesSet().add(dimensionMap.get("InstanceId"));
            CheckedListenableFuture<AlarmHistory> alarmHistoryFuture = Futures.newGenericeFuture();
            Callback.Checked<ComputeMessage> callback = getRecordCallback(action, alarmEntity, now,
                    alarmHistoryFuture);
            try {
                EucalyptusClient client = new EucalyptusClient(
                        AccountFullName.getInstance(alarmEntity.getAccountId()));
                client.init();
                client.dispatch(terminateInstances, callback);
            } catch (Exception ex) {
                alarmHistoryFuture.set(recordFailure(action, alarmEntity, now, ex));
            }
            try {
                return alarmHistoryFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                Throwable cause = (e instanceof ExecutionException) ? e.getCause() : e;
                return recordFailure(action, alarmEntity, now, e);
            }
        }
    }

    private static class StopInstanceAction extends Action {

        @Override
        public boolean filter(String action, Map<String, String> dimensionMap) {
            if (action == null)
                return false;
            // Example:
            // arn:aws:automate:us-east-1:ec2:stop
            if (!action.startsWith("arn:aws:automate:"))
                return false;
            if (!action.endsWith(":ec2:stop"))
                return false;
            if (dimensionMap == null)
                return false;
            return (dimensionMap.containsKey("InstanceId"));
        }

        @Override
        public void executeAction(final String action, final Map<String, String> dimensionMap,
                final AlarmEntity alarmEntity, final Date now) {
            StopInstancesType stopInstances = new StopInstancesType();
            stopInstances.getInstancesSet().add(dimensionMap.get("InstanceId"));
            Callback.Checked<ComputeMessage> callback = getCallback(action, alarmEntity, now);
            try {
                EucalyptusClient client = new EucalyptusClient(
                        AccountFullName.getInstance(alarmEntity.getAccountId()));
                client.init();
                client.dispatch(stopInstances, callback);
            } catch (Exception ex) {
                failure(action, alarmEntity, now, ex);
            }
        }

        @Override
        public boolean alwaysExecute() {
            return false;
        }

        @Override
        public AlarmHistory executeActionAndRecord(String action, Map<String, String> dimensionMap,
                AlarmEntity alarmEntity, Date now) {
            StopInstancesType stopInstances = new StopInstancesType();
            stopInstances.getInstancesSet().add(dimensionMap.get("InstanceId"));
            CheckedListenableFuture<AlarmHistory> alarmHistoryFuture = Futures.newGenericeFuture();
            Callback.Checked<ComputeMessage> callback = getRecordCallback(action, alarmEntity, now,
                    alarmHistoryFuture);
            try {
                EucalyptusClient client = new EucalyptusClient(
                        AccountFullName.getInstance(alarmEntity.getAccountId()));
                client.init();
                client.dispatch(stopInstances, callback);
            } catch (Exception ex) {
                alarmHistoryFuture.set(recordFailure(action, alarmEntity, now, ex));
            }
            try {
                return alarmHistoryFuture.get();
            } catch (InterruptedException | ExecutionException e) {
                Throwable cause = (e instanceof ExecutionException) ? e.getCause() : e;
                return recordFailure(action, alarmEntity, now, e);
            }
        }
    }

    public static class ActionManager {
        private static List<Action> actions = new ArrayList<Action>();
        static {
            actions.add(new StopInstanceAction());
            actions.add(new TerminateInstanceAction());
            actions.add(new ExecuteAutoScalingPolicyAction());

        }

        public static Action getAction(String action, Map<String, String> dimensionMap) {
            for (Action actionFromList : actions) {
                if (actionFromList.filter(action, dimensionMap)) {
                    return actionFromList;
                }
            }
            return null;
        }
    }

}