org.zenoss.zep.dao.impl.EventSummaryDaoImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.zenoss.zep.dao.impl.EventSummaryDaoImpl.java

Source

/*****************************************************************************
 * 
 * Copyright (C) Zenoss, Inc. 2010-2012, 2014 all rights reserved.
 * 
 * This content is made available according to terms specified in
 * License.zenoss under the directory where your Zenoss product is installed.
 * 
 ****************************************************************************/

package org.zenoss.zep.dao.impl;

import com.codahale.metrics.MetricRegistry;
import com.codahale.metrics.annotation.Timed;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.RowMapperResultSetExtractor;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import org.springframework.jdbc.core.simple.SimpleJdbcOperations;
import org.springframework.jdbc.support.MetaDataAccessException;
import org.springframework.transaction.support.TransactionSynchronizationAdapter;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.StringUtils;
import org.zenoss.protobufs.JsonFormat;
import org.zenoss.protobufs.model.Model.ModelElementType;
import org.zenoss.protobufs.zep.Zep.Event;
import org.zenoss.protobufs.zep.Zep.EventActor;
import org.zenoss.protobufs.zep.Zep.EventAuditLog;
import org.zenoss.protobufs.zep.Zep.EventDetail;
import org.zenoss.protobufs.zep.Zep.EventDetailSet;
import org.zenoss.protobufs.zep.Zep.EventNote;
import org.zenoss.protobufs.zep.Zep.EventSeverity;
import org.zenoss.protobufs.zep.Zep.EventStatus;
import org.zenoss.protobufs.zep.Zep.EventSummary;
import org.zenoss.protobufs.zep.Zep.EventSummaryOrBuilder;
import org.zenoss.protobufs.zep.Zep.EventTag;
import org.zenoss.zep.Counters;
import org.zenoss.zep.UUIDGenerator;
import org.zenoss.zep.ZepConstants;
import org.zenoss.zep.ZepException;
import org.zenoss.zep.annotations.TransactionalReadOnly;
import org.zenoss.zep.annotations.TransactionalRollbackAllExceptions;
import org.zenoss.zep.dao.EventBatch;
import org.zenoss.zep.dao.EventBatchParams;
import org.zenoss.zep.dao.EventSummaryDao;
import org.zenoss.zep.dao.impl.compat.DatabaseCompatibility;
import org.zenoss.zep.dao.impl.compat.DatabaseType;
import org.zenoss.zep.dao.impl.compat.NestedTransactionCallback;
import org.zenoss.zep.dao.impl.compat.NestedTransactionContext;
import org.zenoss.zep.dao.impl.compat.NestedTransactionService;
import org.zenoss.zep.dao.impl.compat.TypeConverter;
import org.zenoss.zep.dao.impl.compat.TypeConverterUtils;
import org.zenoss.zep.plugins.EventPreCreateContext;

import javax.sql.DataSource;
import java.io.IOException;
import java.lang.reflect.Proxy;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.*;
import java.util.concurrent.*;

import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_AGENT_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_AUDIT_JSON;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_CLEARED_BY_EVENT_UUID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_CLEAR_FINGERPRINT_HASH;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_CLOSED_STATUS;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_CURRENT_USER_NAME;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_CURRENT_USER_UUID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_DETAILS_JSON;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_ELEMENT_IDENTIFIER;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_ELEMENT_SUB_IDENTIFIER;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_ELEMENT_SUB_TITLE;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_ELEMENT_SUB_TYPE_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_ELEMENT_SUB_UUID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_ELEMENT_TITLE;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_ELEMENT_TYPE_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_ELEMENT_UUID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_EVENT_CLASS_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_EVENT_CLASS_KEY_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_EVENT_CLASS_MAPPING_UUID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_EVENT_COUNT;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_EVENT_GROUP_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_EVENT_KEY_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_FINGERPRINT;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_FINGERPRINT_HASH;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_FIRST_SEEN;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_LAST_SEEN;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_MESSAGE;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_MONITOR_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_NT_EVENT_CODE;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_SEVERITY_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_STATUS_CHANGE;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_STATUS_ID;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_SUMMARY;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_SYSLOG_FACILITY;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_SYSLOG_PRIORITY;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_TAGS_JSON;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_UPDATE_TIME;
import static org.zenoss.zep.dao.impl.EventConstants.COLUMN_UUID;
import static org.zenoss.zep.dao.impl.EventConstants.MAX_CURRENT_USER_NAME;
import static org.zenoss.zep.dao.impl.EventConstants.MAX_FINGERPRINT;
import static org.zenoss.zep.dao.impl.EventConstants.TABLE_EVENT_ARCHIVE;
import static org.zenoss.zep.dao.impl.EventConstants.TABLE_EVENT_SUMMARY;

public class EventSummaryDaoImpl implements EventSummaryDao {

    private static final Logger logger = LoggerFactory.getLogger(EventSummaryDaoImpl.class);

    @Autowired
    protected MetricRegistry metricRegistry;

    private final ConcurrentMap<String, List<Event>> deduping = new ConcurrentHashMap<String, List<Event>>();

    private final DataSource dataSource;

    private final SimpleJdbcOperations template;

    private final SimpleJdbcInsert insert;

    private volatile List<String> archiveColumnNames;

    private EventDaoHelper eventDaoHelper;

    private UUIDGenerator uuidGenerator;

    private DatabaseCompatibility databaseCompatibility;

    private TypeConverter<String> uuidConverter;

    private NestedTransactionService nestedTransactionService;

    private RowMapper<EventSummary.Builder> eventDedupMapper;

    private Counters counters;

    public EventSummaryDaoImpl(DataSource dataSource) throws MetaDataAccessException {
        this.dataSource = dataSource;
        this.template = (SimpleJdbcOperations) Proxy.newProxyInstance(SimpleJdbcOperations.class.getClassLoader(),
                new Class<?>[] { SimpleJdbcOperations.class }, new SimpleJdbcTemplateProxy(dataSource));
        this.insert = new SimpleJdbcInsert(dataSource).withTableName(TABLE_EVENT_SUMMARY);
    }

    public void setEventDaoHelper(EventDaoHelper eventDaoHelper) {
        this.eventDaoHelper = eventDaoHelper;
    }

    public void setUuidGenerator(UUIDGenerator uuidGenerator) {
        this.uuidGenerator = uuidGenerator;
    }

    public void setDatabaseCompatibility(final DatabaseCompatibility databaseCompatibility) {
        this.databaseCompatibility = databaseCompatibility;
        this.uuidConverter = databaseCompatibility.getUUIDConverter();

        // When we perform de-duping of events, we select a subset of just the fields we care about to determine
        // the de-duping behavior (depending on the timestamps on the event, we may perform merging or update either
        // the first_seen or last_seen dates appropriately). This mapper converts the subset of fields to an
        // EventSummaryOrBuilder object which has convenient accessor methods to retrieve the fields by name.
        this.eventDedupMapper = new RowMapper<EventSummary.Builder>() {
            @Override
            public EventSummary.Builder mapRow(ResultSet rs, int rowNum) throws SQLException {
                final TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
                final EventSummary.Builder oldSummaryBuilder = EventSummary.newBuilder();
                oldSummaryBuilder.setCount(rs.getInt(COLUMN_EVENT_COUNT));
                oldSummaryBuilder.setFirstSeenTime(timestampConverter.fromDatabaseType(rs, COLUMN_FIRST_SEEN));
                oldSummaryBuilder.setLastSeenTime(timestampConverter.fromDatabaseType(rs, COLUMN_LAST_SEEN));
                oldSummaryBuilder.setStatus(EventStatus.valueOf(rs.getInt(COLUMN_STATUS_ID)));
                oldSummaryBuilder
                        .setStatusChangeTime(timestampConverter.fromDatabaseType(rs, COLUMN_STATUS_CHANGE));
                oldSummaryBuilder.setUuid(uuidConverter.fromDatabaseType(rs, COLUMN_UUID));

                final Event.Builder occurrenceBuilder = oldSummaryBuilder.addOccurrenceBuilder(0);
                final String detailsJson = rs.getString(COLUMN_DETAILS_JSON);
                if (detailsJson != null) {
                    try {
                        occurrenceBuilder.addAllDetails(
                                JsonFormat.mergeAllDelimitedFrom(detailsJson, EventDetail.getDefaultInstance()));
                    } catch (IOException e) {
                        throw new SQLException(e.getLocalizedMessage(), e);
                    }
                }
                return oldSummaryBuilder;
            }
        };
    }

    public void setNestedTransactionService(NestedTransactionService nestedTransactionService) {
        this.nestedTransactionService = nestedTransactionService;
    }

    public void setCounters(final Counters counters) {
        this.counters = counters;
    }

    @Override
    @Timed
    @TransactionalRollbackAllExceptions
    public String create(Event event, final EventPreCreateContext context) throws ZepException {

        /*
         * Clear events are dropped if they don't clear any corresponding events.
         */
        final List<String> clearedEventUuids;
        final boolean createClearHash;
        if (event.getSeverity() == EventSeverity.SEVERITY_CLEAR) {
            final Event finalEvent = event;
            try {
                clearedEventUuids = metricRegistry.timer(getClass().getName() + ".clearEvents")
                        .time(new Callable<List<String>>() {
                            @Override
                            public List<String> call() throws Exception {
                                return clearEvents(finalEvent, context);
                            }
                        });
            } catch (ZepException e) {
                throw e;
            } catch (Exception e) {
                throw new ZepException(e);
            }

            if (clearedEventUuids.isEmpty()) {
                logger.debug("Clear event didn't clear any events, dropping: {}", event);
                return null;
            }
            // Clear events always get created in CLOSED status
            if (event.getStatus() != EventStatus.STATUS_CLOSED) {
                event = Event.newBuilder(event).setStatus(EventStatus.STATUS_CLOSED).build();
            }
            createClearHash = false;
        } else {
            createClearHash = true;
            clearedEventUuids = Collections.emptyList();
        }

        /*
         * Closed events have a unique fingerprint_hash in summary to allow multiple rows
         * but only allow one active event (where the de-duplication occurs).
         */
        final String fingerprint = DaoUtils.truncateStringToUtf8(event.getFingerprint(), MAX_FINGERPRINT);
        final byte[] fingerprintHash;
        final String uuid;
        if (ZepConstants.CLOSED_STATUSES.contains(event.getStatus())) {
            fingerprintHash = DaoUtils.sha1(fingerprint + '|' + System.currentTimeMillis());
            uuid = saveEventByFingerprint(fingerprintHash, Collections.singleton(event), context, createClearHash);
        } else {
            fingerprintHash = DaoUtils.sha1(fingerprint);
            final String hashAsString = new String(fingerprintHash).intern();
            final Event finalEvent = event;
            try {
                metricRegistry.timer(getClass().getName() + ".queueDedup").time(new Callable() {
                    @Override
                    public Object call() throws Exception {
                        boolean queued = false;
                        while (!queued) {
                            List<Event> events = deduping.get(hashAsString);
                            if (events == null) {
                                deduping.putIfAbsent(hashAsString, Collections.EMPTY_LIST);
                                continue;
                            }
                            List<Event> newEvents = Lists.newArrayList(events);
                            newEvents.add(finalEvent);
                            queued = deduping.replace(hashAsString, events, newEvents);
                        }
                        return null;
                    }
                });
            } catch (ZepException e) {
                throw e;
            } catch (Exception e) {
                throw new ZepException(e);
            }

            try {
                uuid = metricRegistry.timer(getClass().getName() + ".dedupSync").time(new Callable<String>() {
                    @Override
                    public String call() throws Exception {
                        synchronized (hashAsString) {
                            List<Event> events = deduping.remove(hashAsString);
                            if (events == null)
                                events = Collections.EMPTY_LIST;
                            return saveEventByFingerprint(fingerprintHash, events, context, createClearHash);
                        }
                    }
                });
            } catch (ZepException e) {
                throw e;
            } catch (Exception e) {
                throw new ZepException(e);
            }
        }
        if (uuid == null && !clearedEventUuids.isEmpty()) {
            // This only happens if another thread was processing the same dup and grabbed ours,
            // AND in the time between that thread leaving its critical section and this thread
            // querying for the event_summary by fingerprint_hash, the record we're interested in
            // got deleted (archived).
            //
            // Anyway, probably not a big deal.
            logger.info("Rare race condition thwarted update of clearedByEventUuid for {} events.",
                    clearedEventUuids.size());
            return null;
        }
        try {
            metricRegistry.timer(getClass().getName() + ".dedupClearEvents").time(new Callable<Object>() {
                @Override
                public Object call() throws Exception {
                    // Mark cleared events as cleared by this event
                    if (!clearedEventUuids.isEmpty()) {
                        final EventSummaryUpdateFields updateFields = new EventSummaryUpdateFields();
                        updateFields.setClearedByEventUuid(uuid);
                        update(clearedEventUuids, EventStatus.STATUS_CLEARED, updateFields,
                                ZepConstants.OPEN_STATUSES);
                    }
                    return null;
                }
            });
        } catch (ZepException e) {
            throw e;
        } catch (Exception e) {
            throw new ZepException(e);
        }
        return uuid;
    }

    private Map<String, Object> getInsertFields(EventSummaryOrBuilder summary, EventPreCreateContext context,
            boolean createClearHash) throws ZepException {
        TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
        Event event = summary.getOccurrence(0);
        final Map<String, Object> fields = eventDaoHelper.createOccurrenceFields(event);
        fields.put(COLUMN_STATUS_ID, summary.getStatus().getNumber());
        fields.put(COLUMN_CLOSED_STATUS, ZepConstants.CLOSED_STATUSES.contains(summary.getStatus()));
        fields.put(COLUMN_UPDATE_TIME, timestampConverter.toDatabaseType(summary.getUpdateTime()));
        fields.put(COLUMN_FIRST_SEEN, timestampConverter.toDatabaseType(summary.getFirstSeenTime()));
        fields.put(COLUMN_STATUS_CHANGE, timestampConverter.toDatabaseType(summary.getStatusChangeTime()));
        fields.put(COLUMN_LAST_SEEN, timestampConverter.toDatabaseType(summary.getLastSeenTime()));
        fields.put(COLUMN_EVENT_COUNT, summary.getCount());
        if (!summary.hasUuid() || summary.getUuid() == null) {
            throw new NullPointerException("missing uuid");
        }
        fields.put(COLUMN_UUID, uuidConverter.toDatabaseType(summary.getUuid()));
        if (createClearHash) {
            fields.put(COLUMN_CLEAR_FINGERPRINT_HASH,
                    EventDaoUtils.createClearHash(event, context.getClearFingerprintGenerator()));
        }

        return fields;
    }

    private String saveEventByFingerprint(final byte[] fingerprintHash, final Collection<Event> events,
            final EventPreCreateContext context, final boolean createClearHash) throws ZepException {
        try {
            return metricRegistry.timer(getClass().getName() + ".saveEventByFingerprint")
                    .time(new Callable<String>() {
                        @Override
                        public String call() throws Exception {
                            final List<EventSummary.Builder> oldSummaryList = template.getJdbcOperations().query(
                                    "SELECT event_count,first_seen,last_seen,details_json,status_id,status_change,uuid"
                                            + " FROM event_summary WHERE fingerprint_hash=? FOR UPDATE",
                                    new RowMapperResultSetExtractor<EventSummary.Builder>(eventDedupMapper, 1),
                                    fingerprintHash);
                            final EventSummary.Builder summary;
                            if (!oldSummaryList.isEmpty()) {
                                summary = oldSummaryList.get(0);
                            } else {
                                summary = EventSummary.newBuilder();
                                summary.setCount(0);
                                summary.addOccurrenceBuilder(0);
                            }

                            boolean isNewer = false;
                            for (Event event : events) {
                                isNewer = merge(summary, event) || isNewer;
                            }

                            if (!events.isEmpty()) {
                                summary.setUpdateTime(System.currentTimeMillis());
                                final long dedupCount;
                                if (!oldSummaryList.isEmpty()) {
                                    dedupCount = events.size();
                                    final Map<String, Object> fields = getUpdateFields(summary, isNewer, context,
                                            createClearHash);
                                    final StringBuilder updateSql = new StringBuilder("UPDATE event_summary SET ");
                                    int i = 0;
                                    for (String fieldName : fields.keySet()) {
                                        if (++i > 1)
                                            updateSql.append(',');
                                        updateSql.append(fieldName).append("=:").append(fieldName);
                                    }
                                    updateSql.append(" WHERE fingerprint_hash=:fingerprint_hash");
                                    fields.put("fingerprint_hash", fingerprintHash);
                                    template.update(updateSql.toString(), fields);
                                    final String indexSql = "INSERT INTO event_summary_index_queue (uuid, update_time) SELECT uuid, "
                                            + String.valueOf(System.currentTimeMillis())
                                            + " FROM event_summary WHERE fingerprint_hash=:fingerprint_hash";
                                    template.update(indexSql, fields);
                                } else {
                                    dedupCount = events.size() - 1;
                                    summary.setUuid(uuidGenerator.generate().toString());
                                    final Map<String, Object> fields = getInsertFields(summary, context,
                                            createClearHash);
                                    fields.put(COLUMN_FINGERPRINT_HASH, fingerprintHash);
                                    insert.execute(fields);
                                    indexSignal(summary.getUuid(), System.currentTimeMillis());
                                }
                                if (dedupCount > 0) {
                                    TransactionSynchronizationManager
                                            .registerSynchronization(new TransactionSynchronizationAdapter() {
                                                @Override
                                                public void afterCommit() {
                                                    counters.addToDedupedEventCount(dedupCount);
                                                }
                                            });
                                }
                            }
                            return summary.getUuid();
                        }
                    });
        } catch (ZepException e) {
            throw e;
        } catch (Exception e) {
            throw new ZepException(e);
        }
    }

    private boolean merge(EventSummary.Builder merged, Event occurrence) throws ZepException {
        boolean isNewer = false;
        merged.setCount(merged.getCount() + occurrence.getCount());
        if (!merged.hasLastSeenTime() || occurrence.getCreatedTime() >= merged.getLastSeenTime()) {
            isNewer = true;
            merged.setLastSeenTime(occurrence.getCreatedTime());
            Event.Builder ob = merged.getOccurrenceBuilder(0);
            EventActor.Builder ab = ob.getActorBuilder();
            if (occurrence.hasEventGroup())
                ob.setEventGroup(occurrence.getEventGroup());
            if (occurrence.hasEventClass())
                ob.setEventClass(occurrence.getEventClass());
            if (occurrence.hasEventClassKey())
                ob.setEventClassKey(occurrence.getEventClassKey());
            if (occurrence.hasEventClassMappingUuid())
                ob.setEventClassMappingUuid(occurrence.getEventClassMappingUuid());
            if (occurrence.hasEventKey())
                ob.setEventKey(occurrence.getEventKey());
            if (occurrence.hasSeverity())
                ob.setSeverity(occurrence.getSeverity());
            if (occurrence.hasMonitor())
                ob.setMonitor(occurrence.getMonitor());
            if (occurrence.hasAgent())
                ob.setAgent(occurrence.getAgent());
            if (occurrence.hasSyslogFacility())
                ob.setSyslogFacility(occurrence.getSyslogFacility());
            if (occurrence.hasSyslogPriority())
                ob.setSyslogPriority(occurrence.getSyslogPriority());
            if (occurrence.hasNtEventCode())
                ob.setNtEventCode(occurrence.getNtEventCode());
            if (occurrence.hasSummary())
                ob.setSummary(occurrence.getSummary());
            if (occurrence.hasMessage())
                ob.setMessage(occurrence.getMessage());
            List<EventTag> tagsList = occurrence.getTagsList();
            if (tagsList != null && !tagsList.isEmpty()) {
                ob.clearTags();
                ob.addAllTags(tagsList);
            }
            if (!ob.hasFingerprint() || ob.getFingerprint() == null && occurrence.hasFingerprint()) {
                ob.setFingerprint(occurrence.getFingerprint());
            }
            EventActor actor = occurrence.getActor();
            if (actor != null) {
                if (actor.hasElementUuid())
                    ab.setElementUuid(actor.getElementUuid());
                if (actor.hasElementTypeId())
                    ab.setElementTypeId(actor.getElementTypeId());
                if (actor.hasElementIdentifier())
                    ab.setElementIdentifier(actor.getElementIdentifier());
                if (actor.hasElementTitle())
                    ab.setElementTitle(actor.getElementTitle());
                if (actor.hasElementSubUuid())
                    ab.setElementSubUuid(actor.getElementSubUuid());
                if (actor.hasElementSubTypeId())
                    ab.setElementSubTypeId(actor.getElementSubTypeId());
                if (actor.hasElementSubIdentifier())
                    ab.setElementSubIdentifier(actor.getElementSubIdentifier());
                if (actor.hasElementSubTitle())
                    ab.setElementSubTitle(actor.getElementSubTitle());
            }

            // Update status except for ACKNOWLEDGED -> {NEW|SUPPRESSED}
            // Stays in ACKNOWLEDGED in these cases
            boolean updateStatus = true;
            EventStatus oldStatus = merged.hasStatus() ? merged.getStatus() : null;
            EventStatus newStatus = occurrence.getStatus();
            if (oldStatus == EventStatus.STATUS_ACKNOWLEDGED) {
                switch (newStatus) {
                case STATUS_NEW:
                case STATUS_SUPPRESSED:
                    updateStatus = false;
                    break;
                }
            }
            if (updateStatus && oldStatus != newStatus) {
                merged.setStatus(occurrence.getStatus());
                merged.setStatusChangeTime(occurrence.getCreatedTime());
            }
            if (!merged.hasStatusChangeTime()) {
                merged.setStatusChangeTime(occurrence.getCreatedTime());
            }

            // Merge event details
            List<EventDetail> newDetails = occurrence.getDetailsList();
            if (!newDetails.isEmpty()) {
                List<EventDetail> oldDetails = ob.getDetailsList();
                if (oldDetails.isEmpty()) {
                    ob.addAllDetails(newDetails);
                } else {
                    ob.clearDetails();
                    String json = eventDaoHelper.mergeDetailsToJson(oldDetails, newDetails);
                    try {
                        ob.addAllDetails(JsonFormat.mergeAllDelimitedFrom(json, EventDetail.getDefaultInstance()));
                    } catch (IOException e) {
                        throw new ZepException(e);
                    }
                }
            }
        } else {
            // This is the case where the event that we're processing is OLDER
            // than the last seen time on the summary.

            // Merge event details - order swapped b/c of out of order event
            List<EventDetail> oldDetails = occurrence.getDetailsList();
            if (!oldDetails.isEmpty()) {
                Event.Builder ob = merged.getOccurrenceBuilder(0);
                List<EventDetail> newDetails = ob.getDetailsList();
                if (newDetails.isEmpty()) {
                    ob.addAllDetails(oldDetails);
                } else {
                    ob.clearDetails();
                    String json = eventDaoHelper.mergeDetailsToJson(oldDetails, newDetails);
                    try {
                        ob.addAllDetails(JsonFormat.mergeAllDelimitedFrom(json, EventDetail.getDefaultInstance()));
                    } catch (IOException e) {
                        throw new ZepException(e);
                    }
                }
            }
        }

        long firstSeen = occurrence.hasFirstSeenTime() ? occurrence.getFirstSeenTime()
                : occurrence.getCreatedTime();
        if (!merged.hasFirstSeenTime() || firstSeen < merged.getFirstSeenTime()) {
            merged.setFirstSeenTime(firstSeen);
        }
        return isNewer;
    }

    /**
     * When an event is de-duped, if the event occurrence has a created time greater than or equal to the current
     * last_seen for the event summary, these fields from the event summary row are overwritten by values from the new
     * event occurrence. Special handling is performed when de-duping for event status and event details.
     */
    private static final List<String> UPDATE_FIELD_NAMES = Arrays.asList(COLUMN_EVENT_GROUP_ID,
            COLUMN_EVENT_CLASS_ID, COLUMN_EVENT_CLASS_KEY_ID, COLUMN_EVENT_CLASS_MAPPING_UUID, COLUMN_EVENT_KEY_ID,
            COLUMN_SEVERITY_ID, COLUMN_ELEMENT_UUID, COLUMN_ELEMENT_TYPE_ID, COLUMN_ELEMENT_IDENTIFIER,
            COLUMN_ELEMENT_TITLE, COLUMN_ELEMENT_SUB_UUID, COLUMN_ELEMENT_SUB_TYPE_ID,
            COLUMN_ELEMENT_SUB_IDENTIFIER, COLUMN_ELEMENT_SUB_TITLE, COLUMN_LAST_SEEN, COLUMN_MONITOR_ID,
            COLUMN_AGENT_ID, COLUMN_SYSLOG_FACILITY, COLUMN_SYSLOG_PRIORITY, COLUMN_NT_EVENT_CODE,
            COLUMN_CLEAR_FINGERPRINT_HASH, COLUMN_SUMMARY, COLUMN_MESSAGE, COLUMN_TAGS_JSON);

    private Map<String, Object> getUpdateFields(EventSummaryOrBuilder summary, boolean isNewer,
            EventPreCreateContext context, boolean createClearHash) throws ZepException {
        TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
        Map<String, Object> fields = new HashMap<String, Object>();
        Map<String, Object> insertFields = getInsertFields(summary, context, createClearHash);
        if (isNewer) {
            for (String fieldName : UPDATE_FIELD_NAMES) {
                fields.put(fieldName, insertFields.get(fieldName));
            }
            fields.put(COLUMN_STATUS_ID, insertFields.get(COLUMN_STATUS_ID));
            fields.put(COLUMN_CLOSED_STATUS, insertFields.get(COLUMN_CLOSED_STATUS));
            fields.put(COLUMN_STATUS_CHANGE, timestampConverter.toDatabaseType(summary.getStatusChangeTime()));
        }
        fields.put(COLUMN_EVENT_COUNT, summary.getCount());
        fields.put(COLUMN_UPDATE_TIME, timestampConverter.toDatabaseType(summary.getUpdateTime()));
        fields.put(COLUMN_DETAILS_JSON, insertFields.get(COLUMN_DETAILS_JSON));
        fields.put(COLUMN_FIRST_SEEN, timestampConverter.toDatabaseType(summary.getFirstSeenTime()));
        return fields;
    }

    private List<String> clearEvents(Event event, EventPreCreateContext context) throws ZepException {
        TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
        final List<byte[]> clearHashes = EventDaoUtils.createClearHashes(event, context);
        if (clearHashes.isEmpty()) {
            logger.debug("Clear event didn't contain any clear hashes: {}, {}", event, context);
            return Collections.emptyList();
        }
        final long lastSeen = event.getCreatedTime();

        Map<String, Object> fields = new HashMap<String, Object>(2);
        fields.put("_clear_created_time", timestampConverter.toDatabaseType(lastSeen));
        fields.put("_clear_hashes", clearHashes);

        long updateTime = System.currentTimeMillis();
        String indexSql = "INSERT INTO event_summary_index_queue (uuid, update_time) " + "SELECT uuid, "
                + String.valueOf(updateTime) + " FROM event_summary " + "WHERE last_seen <= :_clear_created_time "
                + "AND clear_fingerprint_hash IN (:_clear_hashes) " + "AND closed_status = FALSE ";
        this.template.update(indexSql, fields);

        /* Find events that this clear event would clear. */
        final String sql = "SELECT uuid FROM event_summary " + "WHERE last_seen <= :_clear_created_time "
                + "AND clear_fingerprint_hash IN (:_clear_hashes) " + "AND closed_status = FALSE " + "FOR UPDATE";

        final List<String> results = this.template.query(sql, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                return uuidConverter.fromDatabaseType(rs, COLUMN_UUID);
            }
        }, fields);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                counters.addToClearedEventCount(results.size());
            }
        });
        return results;
    }

    /**
     * When re-identifying or de-identifying events, we recalculate the clear_fingerprint_hash for the event to either
     * include (re-identify) or exclude (de-identify) the UUID of the sub_element. This mapper retrieves a subset of
     * fields for the event in order to recalculate the clear_fingerprint_hash.
     */
    private static class IdentifyMapper implements RowMapper<Map<String, Object>> {
        private final Map<String, Object> fields;
        private final String elementSubUuid;

        public IdentifyMapper(Map<String, Object> fields, String elementSubUuid) {
            this.fields = Collections.unmodifiableMap(fields);
            this.elementSubUuid = elementSubUuid;
        }

        @Override
        public Map<String, Object> mapRow(ResultSet rs, int rowNum) throws SQLException {
            Map<String, Object> updateFields = new HashMap<String, Object>(fields);
            Event.Builder event = Event.newBuilder();
            EventActor.Builder actor = event.getActorBuilder();
            actor.setElementIdentifier(rs.getString(COLUMN_ELEMENT_IDENTIFIER));
            String elementSubIdentifier = rs.getString(COLUMN_ELEMENT_SUB_IDENTIFIER);
            if (elementSubIdentifier != null) {
                actor.setElementSubIdentifier(elementSubIdentifier);
            }
            if (this.elementSubUuid != null) {
                actor.setElementSubUuid(this.elementSubUuid);
            }
            event.setEventClass(rs.getString("event_class_name"));
            String eventKey = rs.getString("event_key_name");
            if (eventKey != null) {
                event.setEventKey(eventKey);
            }

            updateFields.put(COLUMN_UUID, rs.getObject(COLUMN_UUID));
            updateFields.put(COLUMN_CLEAR_FINGERPRINT_HASH, EventDaoUtils.createClearHash(event.build()));
            return updateFields;
        }
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int reidentify(ModelElementType type, String id, String uuid, String title, String parentUuid)
            throws ZepException {
        TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
        long updateTime = System.currentTimeMillis();

        final Map<String, Object> fields = new HashMap<String, Object>();
        fields.put("_uuid", uuidConverter.toDatabaseType(uuid));
        fields.put("_uuid_str", uuid);
        fields.put("_type_id", type.getNumber());
        fields.put("_id", id);
        fields.put("_title", DaoUtils.truncateStringToUtf8(title, EventConstants.MAX_ELEMENT_TITLE));
        fields.put(COLUMN_UPDATE_TIME, timestampConverter.toDatabaseType(updateTime));

        String indexSql = "INSERT INTO event_summary_index_queue (uuid, update_time) " + "SELECT uuid, "
                + String.valueOf(updateTime) + " FROM event_summary "
                + "WHERE element_uuid IS NULL AND element_type_id=:_type_id AND element_identifier=:_id";
        this.template.update(indexSql, fields);

        int numRows = 0;
        String updateSql = "UPDATE event_summary SET element_uuid=:_uuid, element_title=:_title,"
                + " update_time=:update_time WHERE element_uuid IS NULL AND element_type_id=:_type_id"
                + " AND element_identifier=:_id";
        numRows += this.template.update(updateSql, fields);

        if (parentUuid != null) {
            fields.put("_parent_uuid", uuidConverter.toDatabaseType(parentUuid));
            indexSql = "INSERT INTO event_summary_index_queue (uuid, update_time) " + "SELECT es.uuid, "
                    + String.valueOf(updateTime) + " "
                    + "FROM event_summary es INNER JOIN event_class ON es.event_class_id = event_class.id "
                    + "LEFT JOIN event_key ON es.event_key_id = event_key.id "
                    + "WHERE es.element_uuid=:_parent_uuid AND es.element_sub_uuid IS NULL AND "
                    + "es.element_sub_type_id=:_type_id AND es.element_sub_identifier=:_id";
            this.template.update(indexSql, fields);

            String selectSql = "SELECT uuid,element_identifier,element_sub_identifier,"
                    + "event_class.name AS event_class_name,event_key.name AS event_key_name FROM event_summary es"
                    + " INNER JOIN event_class ON es.event_class_id = event_class.id"
                    + " LEFT JOIN event_key on es.event_key_id = event_key.id"
                    + " WHERE es.element_uuid=:_parent_uuid AND es.element_sub_uuid IS NULL"
                    + " AND es.element_sub_type_id=:_type_id AND es.element_sub_identifier=:_id FOR UPDATE";
            // MySQL locks all joined rows, PostgreSQL requires you to specify the rows from each table to lock
            if (this.databaseCompatibility.getDatabaseType() == DatabaseType.POSTGRESQL) {
                selectSql += " OF es";
            }
            List<Map<String, Object>> updateFields = this.template.query(selectSql,
                    new IdentifyMapper(fields, uuid), fields);

            String updateSubElementSql = "UPDATE event_summary SET element_sub_uuid=:_uuid, "
                    + "element_sub_title=:_title, update_time=:update_time, "
                    + "clear_fingerprint_hash=:clear_fingerprint_hash WHERE uuid=:uuid";
            int[] updated = this.template.batchUpdate(updateSubElementSql,
                    updateFields.toArray(new Map[updateFields.size()]));
            for (int updatedRows : updated) {
                numRows += updatedRows;
            }
        }
        return numRows;
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int deidentify(String uuid) throws ZepException {
        TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
        long updateTime = System.currentTimeMillis();

        final Map<String, Object> fields = new HashMap<String, Object>(2);
        fields.put("_uuid", uuidConverter.toDatabaseType(uuid));
        fields.put(COLUMN_UPDATE_TIME, timestampConverter.toDatabaseType(updateTime));

        String indexSql = "INSERT INTO event_summary_index_queue (uuid, update_time) " + "SELECT uuid, "
                + String.valueOf(updateTime) + " FROM event_summary " + "WHERE element_uuid=:_uuid";
        this.template.update(indexSql, fields);

        int numRows = 0;
        String updateElementSql = "UPDATE event_summary SET element_uuid=NULL, update_time=:update_time"
                + " WHERE element_uuid=:_uuid";
        numRows += this.template.update(updateElementSql, fields);

        indexSql = "INSERT INTO event_summary_index_queue (uuid, update_time) " + "SELECT uuid, "
                + String.valueOf(updateTime) + " FROM event_summary " + "WHERE element_sub_uuid=:_uuid";
        this.template.update(indexSql, fields);

        String selectSql = "SELECT uuid,element_identifier,element_sub_identifier,"
                + "event_class.name AS event_class_name,event_key.name AS event_key_name FROM event_summary es"
                + " INNER JOIN event_class ON es.event_class_id = event_class.id"
                + " LEFT JOIN event_key on es.event_key_id = event_key.id WHERE element_sub_uuid=:_uuid FOR UPDATE";
        // MySQL locks all joined rows, PostgreSQL requires you to specify the rows from each table to lock
        if (this.databaseCompatibility.getDatabaseType() == DatabaseType.POSTGRESQL) {
            selectSql += " OF es";
        }
        List<Map<String, Object>> updateFields = this.template.query(selectSql, new IdentifyMapper(fields, null),
                fields);

        String updateSubElementSql = "UPDATE event_summary SET element_sub_uuid=NULL, update_time=:update_time, "
                + "clear_fingerprint_hash=:clear_fingerprint_hash WHERE uuid=:uuid";
        int[] updated = this.template.batchUpdate(updateSubElementSql,
                updateFields.toArray(new Map[updateFields.size()]));
        for (int updatedRows : updated) {
            numRows += updatedRows;
        }
        return numRows;
    }

    @Override
    @TransactionalReadOnly
    @Timed
    public EventSummary findByUuid(String uuid) throws ZepException {
        final Map<String, Object> fields = Collections.singletonMap(COLUMN_UUID,
                uuidConverter.toDatabaseType(uuid));
        List<EventSummary> summaries = this.template.query("SELECT * FROM event_summary WHERE uuid=:uuid",
                new EventSummaryRowMapper(this.eventDaoHelper, this.databaseCompatibility), fields);
        return (summaries.size() > 0) ? summaries.get(0) : null;
    }

    @Override
    @Deprecated
    @Timed
    /** @deprecated use {@link #findByKey(Collection) instead}. */
    public List<EventSummary> findByUuids(final List<String> uuids) throws ZepException {
        return findByUuids((Collection) uuids);
    }

    @TransactionalReadOnly
    private List<EventSummary> findByUuids(final Collection<String> uuids) throws ZepException {
        if (uuids.isEmpty()) {
            return Collections.emptyList();
        }
        Map<String, List<Object>> fields = Collections.singletonMap("uuids",
                TypeConverterUtils.batchToDatabaseType(uuidConverter, uuids));
        return this.template.query("SELECT * FROM event_summary WHERE uuid IN(:uuids)",
                new EventSummaryRowMapper(this.eventDaoHelper, this.databaseCompatibility), fields);
    }

    @Override
    @TransactionalReadOnly
    @Timed
    /**
     * This implementation only makes use of the UUID field to lookup the events.
     */
    public List<EventSummary> findByKey(final Collection<EventSummary> toLookup) throws ZepException {
        if (toLookup == null || toLookup.isEmpty())
            return Collections.emptyList();
        Set<String> uuids = Sets.newHashSetWithExpectedSize(toLookup.size());
        for (EventSummary event : toLookup)
            uuids.add(event.getUuid());
        return findByUuids(uuids);
    }

    @Override
    @TransactionalReadOnly
    @Timed
    public EventBatch listBatch(EventBatchParams batchParams, long maxUpdateTime, int limit) throws ZepException {
        return this.eventDaoHelper.listBatch(this.template, TABLE_EVENT_SUMMARY, null, batchParams, maxUpdateTime,
                limit, new EventSummaryRowMapper(eventDaoHelper, databaseCompatibility));
    }

    private static final EnumSet<EventStatus> AUDIT_LOG_STATUSES = EnumSet.of(EventStatus.STATUS_NEW,
            EventStatus.STATUS_ACKNOWLEDGED, EventStatus.STATUS_CLOSED, EventStatus.STATUS_CLEARED);

    private static List<Integer> getSeverityIds(EventSeverity maxSeverity, boolean inclusiveSeverity) {
        List<Integer> severityIds = EventDaoHelper.getSeverityIdsLessThan(maxSeverity);
        if (inclusiveSeverity) {
            severityIds.add(maxSeverity.getNumber());
        }
        return severityIds;
    }

    @Override
    @Timed
    public long getAgeEligibleEventCount(long duration, TimeUnit unit, EventSeverity maxSeverity,
            boolean inclusiveSeverity) {
        List<Integer> severityIds = getSeverityIds(maxSeverity, inclusiveSeverity);
        // Aging disabled.
        if (severityIds.isEmpty()) {
            return 0;
        }
        String sql = "SELECT count(*) FROM event_summary WHERE closed_status = FALSE AND "
                + "last_seen < :_last_seen AND severity_id IN (:_severity_ids)";
        Map<String, Object> fields = createSharedFields(duration, unit);
        fields.put("_severity_ids", severityIds);
        return template.queryForInt(sql, fields);
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int ageEvents(long agingInterval, TimeUnit unit, EventSeverity maxSeverity, int limit,
            boolean inclusiveSeverity) throws ZepException {
        TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
        long agingIntervalMs = unit.toMillis(agingInterval);
        if (agingIntervalMs < 0 || agingIntervalMs == Long.MAX_VALUE) {
            throw new ZepException("Invalid aging interval: " + agingIntervalMs);
        }
        if (limit <= 0) {
            throw new ZepException("Limit can't be negative: " + limit);
        }
        List<Integer> severityIds = getSeverityIds(maxSeverity, inclusiveSeverity);
        if (severityIds.isEmpty()) {
            logger.debug("Not aging events - min severity specified");
            return 0;
        }
        long now = System.currentTimeMillis();
        long ageTs = now - agingIntervalMs;

        Map<String, Object> fields = new HashMap<String, Object>();
        fields.put(COLUMN_STATUS_ID, EventStatus.STATUS_AGED.getNumber());
        fields.put(COLUMN_CLOSED_STATUS, ZepConstants.CLOSED_STATUSES.contains(EventStatus.STATUS_AGED));
        fields.put(COLUMN_STATUS_CHANGE, timestampConverter.toDatabaseType(now));
        fields.put(COLUMN_UPDATE_TIME, timestampConverter.toDatabaseType(now));
        fields.put(COLUMN_LAST_SEEN, timestampConverter.toDatabaseType(ageTs));
        fields.put("_severity_ids", severityIds);
        fields.put("_limit", limit);

        final String updateSql;
        if (databaseCompatibility.getDatabaseType() == DatabaseType.MYSQL) {
            String indexSql = "INSERT INTO event_summary_index_queue (uuid, update_time) " + "SELECT uuid, "
                    + String.valueOf(now) + " " + "FROM event_summary " + " WHERE last_seen < :last_seen AND"
                    + " severity_id IN (:_severity_ids) AND" + " closed_status = FALSE LIMIT :_limit";
            this.template.update(indexSql, fields);

            // Use UPDATE ... LIMIT
            updateSql = "UPDATE event_summary SET"
                    + " status_id=:status_id,status_change=:status_change,update_time=:update_time"
                    + ",closed_status=:closed_status"
                    + " WHERE last_seen < :last_seen AND severity_id IN (:_severity_ids)"
                    + " AND closed_status = FALSE LIMIT :_limit";
        } else if (databaseCompatibility.getDatabaseType() == DatabaseType.POSTGRESQL) {
            String indexSql = "INSERT INTO event_summary_index_queue (uuid, update_time) " + "SELECT uuid, "
                    + String.valueOf(now) + " " + "FROM event_summary "
                    + " WHERE uuid IN (SELECT uuid FROM event_summary WHERE"
                    + " last_seen < :last_seen AND severity_id IN (:_severity_ids)"
                    + " AND closed_status = FALSE LIMIT :_limit)";
            this.template.update(indexSql, fields);

            // Use UPDATE ... WHERE pk IN (SELECT ... LIMIT)
            updateSql = "UPDATE event_summary SET"
                    + " status_id=:status_id,status_change=:status_change,update_time=:update_time"
                    + ",closed_status=:closed_status" + " WHERE uuid IN (SELECT uuid FROM event_summary WHERE"
                    + " last_seen < :last_seen AND severity_id IN (:_severity_ids)"
                    + " AND closed_status = FALSE LIMIT :_limit)";
        } else {
            throw new IllegalStateException(
                    "Unsupported database type: " + databaseCompatibility.getDatabaseType());
        }
        final int numRows = this.template.update(updateSql, fields);
        if (numRows > 0) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    counters.addToAgedEventCount(numRows);
                }
            });
        }
        return numRows;
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int addNote(String uuid, EventNote note) throws ZepException {
        // Add the uuid to the event_summary_index_queue
        final long updateTime = System.currentTimeMillis();
        this.indexSignal(uuid, updateTime);

        return this.eventDaoHelper.addNote(TABLE_EVENT_SUMMARY, uuid, note, template);
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int updateDetails(String uuid, EventDetailSet details) throws ZepException {
        // Add the uuid to the event_summary_index_queue
        final long updateTime = System.currentTimeMillis();
        this.indexSignal(uuid, updateTime);

        return this.eventDaoHelper.updateDetails(TABLE_EVENT_SUMMARY, uuid, details.getDetailsList(), template);
    }

    private static class EventSummaryUpdateFields {
        private String currentUserUuid;
        private String currentUserName;
        private String clearedByEventUuid;

        public static final EventSummaryUpdateFields EMPTY_FIELDS = new EventSummaryUpdateFields();

        public Map<String, Object> toMap(TypeConverter<String> uuidConverter) {
            Map<String, Object> m = new HashMap<String, Object>();
            Object currentUuid = null;
            if (this.currentUserUuid != null) {
                currentUuid = uuidConverter.toDatabaseType(this.currentUserUuid);
            }
            m.put(COLUMN_CURRENT_USER_UUID, currentUuid);
            m.put(COLUMN_CURRENT_USER_NAME, currentUserName);

            Object clearedUuid = null;
            if (this.clearedByEventUuid != null) {
                clearedUuid = uuidConverter.toDatabaseType(this.clearedByEventUuid);
            }
            m.put(COLUMN_CLEARED_BY_EVENT_UUID, clearedUuid);
            return m;
        }

        public String getCurrentUserUuid() {
            return currentUserUuid;
        }

        public void setCurrentUserUuid(String currentUserUuid) {
            this.currentUserUuid = currentUserUuid;
        }

        public String getCurrentUserName() {
            return currentUserName;
        }

        public void setCurrentUserName(String currentUserName) {
            if (currentUserName == null) {
                this.currentUserName = null;
            } else {
                this.currentUserName = DaoUtils.truncateStringToUtf8(currentUserName, MAX_CURRENT_USER_NAME);
            }
        }

        public String getClearedByEventUuid() {
            return clearedByEventUuid;
        }

        public void setClearedByEventUuid(String clearedByEventUuid) {
            this.clearedByEventUuid = clearedByEventUuid;
        }
    }

    private int update(final List<String> uuids, final EventStatus status,
            final EventSummaryUpdateFields updateFields, final Collection<EventStatus> currentStatuses)
            throws ZepException {
        if (uuids.isEmpty()) {
            return 0;
        }
        TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
        final long now = System.currentTimeMillis();
        final Map<String, Object> fields = updateFields.toMap(uuidConverter);
        fields.put(COLUMN_STATUS_ID, status.getNumber());
        fields.put(COLUMN_STATUS_CHANGE, timestampConverter.toDatabaseType(now));
        fields.put(COLUMN_CLOSED_STATUS, ZepConstants.CLOSED_STATUSES.contains(status));
        fields.put(COLUMN_UPDATE_TIME, timestampConverter.toDatabaseType(now));
        fields.put("_uuids", TypeConverterUtils.batchToDatabaseType(uuidConverter, uuids));
        // If we aren't acknowledging events, we need to clear out the current user name / UUID values
        if (status != EventStatus.STATUS_ACKNOWLEDGED) {
            fields.put(COLUMN_CURRENT_USER_NAME, null);
            fields.put(COLUMN_CURRENT_USER_UUID, null);
        }

        StringBuilder sb = new StringBuilder("SELECT uuid,fingerprint,audit_json FROM event_summary");
        StringBuilder sbw = new StringBuilder(" WHERE uuid IN (:_uuids)");
        /*
         * This is required to support well-defined transitions between states. We only allow
         * updates to move events between states that make sense.
         */
        if (!currentStatuses.isEmpty()) {
            final List<Integer> currentStatusIds = new ArrayList<Integer>(currentStatuses.size());
            for (EventStatus currentStatus : currentStatuses) {
                currentStatusIds.add(currentStatus.getNumber());
            }
            fields.put("_current_status_ids", currentStatusIds);
            sbw.append(" AND status_id IN (:_current_status_ids)");
        }
        /*
         * Disallow acknowledging an event again as the same user name / user uuid. If the event is not
         * already acknowledged, we will allow it to be acknowledged (assuming state filter above doesn't
         * exclude it). Otherwise, we will only acknowledge it again if *either* the user name or user
         * uuid has changed. If neither of these fields have changed, it is a NO-OP.
         */
        if (status == EventStatus.STATUS_ACKNOWLEDGED) {
            fields.put("_status_acknowledged", EventStatus.STATUS_ACKNOWLEDGED.getNumber());
            sbw.append(" AND (status_id != :_status_acknowledged OR ");
            if (updateFields.getCurrentUserName() == null) {
                sbw.append("current_user_name IS NOT NULL");
            } else {
                sbw.append("(current_user_name IS NULL OR current_user_name != :current_user_name)");
            }
            sbw.append(" OR ");
            if (updateFields.getCurrentUserUuid() == null) {
                sbw.append("current_user_uuid IS NOT NULL");
            } else {
                sbw.append("(current_user_uuid IS NULL OR current_user_uuid != :current_user_uuid)");
            }
            sbw.append(")");
        }
        String selectSql = sb.toString() + sbw.toString() + " FOR UPDATE";

        final long updateTime = System.currentTimeMillis();
        final String indexSql = "INSERT INTO event_summary_index_queue (uuid, update_time) " + "SELECT uuid, "
                + String.valueOf(updateTime) + " " + "FROM event_summary " + sbw.toString();
        this.template.update(indexSql, fields);

        /*
         * If this is a significant status change, also add an audit note
         */
        final String newAuditJson;
        if (AUDIT_LOG_STATUSES.contains(status)) {
            EventAuditLog.Builder builder = EventAuditLog.newBuilder();
            builder.setTimestamp(now);
            builder.setNewStatus(status);
            if (updateFields.getCurrentUserUuid() != null) {
                builder.setUserUuid(updateFields.getCurrentUserUuid());
            }
            if (updateFields.getCurrentUserName() != null) {
                builder.setUserName(updateFields.getCurrentUserName());
            }
            try {
                newAuditJson = JsonFormat.writeAsString(builder.build());
            } catch (IOException e) {
                throw new ZepException(e);
            }
        } else {
            newAuditJson = null;
        }

        List<Map<String, Object>> result = this.template.query(selectSql, new RowMapper<Map<String, Object>>() {
            @Override
            public Map<String, Object> mapRow(ResultSet rs, int rowNum) throws SQLException {
                final String fingerprint = rs.getString(COLUMN_FINGERPRINT);
                final String currentAuditJson = rs.getString(COLUMN_AUDIT_JSON);

                Map<String, Object> updateFields = new HashMap<String, Object>(fields);
                final String newFingerprint;
                // When closing an event, give it a unique fingerprint hash
                if (ZepConstants.CLOSED_STATUSES.contains(status)) {
                    updateFields.put(COLUMN_CLOSED_STATUS, Boolean.TRUE);
                    newFingerprint = EventDaoUtils.join('|', fingerprint, Long.toString(now));
                }
                // When re-opening an event, give it the true fingerprint_hash. This is required to correctly
                // de-duplicate events.
                else {
                    updateFields.put(COLUMN_CLOSED_STATUS, Boolean.FALSE);
                    newFingerprint = fingerprint;
                }

                final StringBuilder auditJson = new StringBuilder();
                if (newAuditJson != null) {
                    auditJson.append(newAuditJson);
                }
                if (currentAuditJson != null) {
                    if (auditJson.length() > 0) {
                        auditJson.append(",\n");
                    }
                    auditJson.append(currentAuditJson);
                }
                String updatedAuditJson = (auditJson.length() > 0) ? auditJson.toString() : null;
                updateFields.put(COLUMN_FINGERPRINT_HASH, DaoUtils.sha1(newFingerprint));
                updateFields.put(COLUMN_AUDIT_JSON, updatedAuditJson);
                updateFields.put(COLUMN_UUID, rs.getObject(COLUMN_UUID));
                return updateFields;
            }
        }, fields);

        final String updateSql = "UPDATE event_summary SET status_id=:status_id,status_change=:status_change,"
                + "closed_status=:closed_status,update_time=:update_time,"
                + (status != EventStatus.STATUS_CLOSED && status != EventStatus.STATUS_CLEARED
                        ? "current_user_uuid=:current_user_uuid,current_user_name=:current_user_name,"
                        : "")
                + "cleared_by_event_uuid=:cleared_by_event_uuid,fingerprint_hash=:fingerprint_hash,"
                + "audit_json=:audit_json WHERE uuid=:uuid";

        int numRows = 0;
        for (final Map<String, Object> update : result) {
            try {
                numRows += this.nestedTransactionService
                        .executeInNestedTransaction(new NestedTransactionCallback<Integer>() {
                            @Override
                            public Integer doInNestedTransaction(NestedTransactionContext context)
                                    throws DataAccessException {
                                return template.update(updateSql, update);
                            }
                        });
            } catch (DuplicateKeyException e) {
                /*
                 * Ignore duplicate key errors on update. This will occur if there is an active
                 * event with the same fingerprint.
                 */
            }
        }
        return numRows;
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int acknowledge(List<String> uuids, String userUuid, String userName) throws ZepException {
        /* NEW | ACKNOWLEDGED | SUPPRESSED -> ACKNOWLEDGED */
        Set<EventStatus> currentStatuses = ZepConstants.OPEN_STATUSES;
        EventSummaryUpdateFields userfields = new EventSummaryUpdateFields();
        userfields.setCurrentUserName(userName);
        userfields.setCurrentUserUuid(userUuid);
        return update(uuids, EventStatus.STATUS_ACKNOWLEDGED, userfields, currentStatuses);
    }

    private Map<String, Object> createSharedFields(long duration, TimeUnit unit) {
        TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
        long delta = System.currentTimeMillis() - unit.toMillis(duration);
        Object lastSeen = timestampConverter.toDatabaseType(delta);
        Map<String, Object> fields = new HashMap<String, Object>();
        fields.put("_last_seen", lastSeen);
        return fields;
    }

    @Override
    @Timed
    public long getArchiveEligibleEventCount(long duration, TimeUnit unit) {
        String sql = "SELECT COUNT(*) FROM event_summary WHERE closed_status = TRUE AND last_seen < :_last_seen";
        Map<String, Object> fields = createSharedFields(duration, unit);
        return template.queryForInt(sql, fields);
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int archive(long duration, TimeUnit unit, int limit) throws ZepException {
        Map<String, Object> fields = createSharedFields(duration, unit);
        fields.put("_limit", limit);

        final String sql = "SELECT uuid FROM event_summary WHERE closed_status = TRUE AND "
                + "last_seen < :_last_seen LIMIT :_limit FOR UPDATE";
        final List<String> uuids = this.template.query(sql, new RowMapper<String>() {
            @Override
            public String mapRow(ResultSet rs, int rowNum) throws SQLException {
                return uuidConverter.fromDatabaseType(rs, COLUMN_UUID);
            }
        }, fields);
        return archive(uuids);
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int close(List<String> uuids, String userUuid, String userName) throws ZepException {
        /* NEW | ACKNOWLEDGED | SUPPRESSED -> CLOSED */
        List<EventStatus> currentStatuses = Arrays.asList(EventStatus.STATUS_NEW, EventStatus.STATUS_ACKNOWLEDGED,
                EventStatus.STATUS_SUPPRESSED);
        EventSummaryUpdateFields userfields = new EventSummaryUpdateFields();
        userfields.setCurrentUserName(userName);
        userfields.setCurrentUserUuid(userUuid);
        return update(uuids, EventStatus.STATUS_CLOSED, userfields, currentStatuses);
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int reopen(List<String> uuids, String userUuid, String userName) throws ZepException {
        /* CLOSED | CLEARED | AGED | ACKNOWLEDGED | SUPPRESSED -> NEW */
        List<EventStatus> currentStatuses = Arrays.asList(EventStatus.STATUS_CLOSED, EventStatus.STATUS_CLEARED,
                EventStatus.STATUS_AGED, EventStatus.STATUS_ACKNOWLEDGED, EventStatus.STATUS_SUPPRESSED);
        EventSummaryUpdateFields userfields = new EventSummaryUpdateFields();
        userfields.setCurrentUserName(userName);
        userfields.setCurrentUserUuid(userUuid);
        return update(uuids, EventStatus.STATUS_NEW, userfields, currentStatuses);
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int suppress(List<String> uuids) throws ZepException {
        /* NEW -> SUPPRESSED */
        List<EventStatus> currentStatuses = Arrays.asList(EventStatus.STATUS_NEW);
        return update(uuids, EventStatus.STATUS_SUPPRESSED, EventSummaryUpdateFields.EMPTY_FIELDS, currentStatuses);
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public int archive(List<String> uuids) throws ZepException {
        if (uuids.isEmpty()) {
            return 0;
        }
        if (this.archiveColumnNames == null) {
            try {
                this.archiveColumnNames = DaoUtils.getColumnNames(this.dataSource, TABLE_EVENT_ARCHIVE);
            } catch (MetaDataAccessException e) {
                throw new ZepException(e.getLocalizedMessage(), e);
            }
        }

        TypeConverter<Long> timestampConverter = databaseCompatibility.getTimestampConverter();
        Map<String, Object> fields = new HashMap<String, Object>();
        fields.put(COLUMN_UPDATE_TIME, timestampConverter.toDatabaseType(System.currentTimeMillis()));
        fields.put("_uuids", TypeConverterUtils.batchToDatabaseType(uuidConverter, uuids));
        StringBuilder selectColumns = new StringBuilder();

        for (Iterator<String> it = this.archiveColumnNames.iterator(); it.hasNext();) {
            String columnName = it.next();
            if (fields.containsKey(columnName)) {
                selectColumns.append(':').append(columnName);
            } else {
                selectColumns.append(columnName);
            }
            if (it.hasNext()) {
                selectColumns.append(',');
            }
        }

        final long updateTime = System.currentTimeMillis();
        /* signal event_summary table rows to get indexed */
        this.template.update("INSERT INTO event_summary_index_queue (uuid, update_time) " + "SELECT uuid, "
                + String.valueOf(updateTime) + " " + "FROM event_summary"
                + " WHERE uuid IN (:_uuids) AND closed_status = TRUE", fields);

        String insertSql = String.format("INSERT INTO event_archive (%s) SELECT %s FROM event_summary"
                + " WHERE uuid IN (:_uuids) AND closed_status = TRUE ON DUPLICATE KEY UPDATE summary=event_summary.summary",
                StringUtils.collectionToCommaDelimitedString(this.archiveColumnNames), selectColumns);

        this.template.update(insertSql, fields);
        final int updated = this.template
                .update("DELETE FROM event_summary WHERE uuid IN (:_uuids) AND closed_status = TRUE", fields);
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
            @Override
            public void afterCommit() {
                counters.addToArchivedEventCount(updated);
            }
        });
        return updated;
    }

    @Override
    @TransactionalRollbackAllExceptions
    @Timed
    public void importEvent(EventSummary eventSummary) throws ZepException {
        final long updateTime = System.currentTimeMillis();
        final EventSummary.Builder summaryBuilder = EventSummary.newBuilder(eventSummary);
        final Event.Builder eventBuilder = summaryBuilder.getOccurrenceBuilder(0);
        summaryBuilder.setUpdateTime(updateTime);
        EventDaoHelper.addMigrateUpdateTimeDetail(eventBuilder, updateTime);

        final EventSummary summary = summaryBuilder.build();
        final Map<String, Object> fields = this.eventDaoHelper.createImportedSummaryFields(summary);

        /*
         * Closed events have a unique fingerprint_hash in summary to allow multiple rows
         * but only allow one active event (where the de-duplication occurs).
         */
        if (ZepConstants.CLOSED_STATUSES.contains(eventSummary.getStatus())) {
            String uniqueFingerprint = (String) fields.get(COLUMN_FINGERPRINT) + '|' + updateTime;
            fields.put(COLUMN_FINGERPRINT_HASH, DaoUtils.sha1(uniqueFingerprint));
            fields.put(COLUMN_CLOSED_STATUS, Boolean.TRUE);
        } else {
            fields.put(COLUMN_FINGERPRINT_HASH, DaoUtils.sha1((String) fields.get(COLUMN_FINGERPRINT)));
            fields.put(COLUMN_CLOSED_STATUS, Boolean.FALSE);
        }

        if (eventSummary.getOccurrence(0).getSeverity() != EventSeverity.SEVERITY_CLEAR) {
            fields.put(COLUMN_CLEAR_FINGERPRINT_HASH, EventDaoUtils.createClearHash(eventSummary.getOccurrence(0)));
        }

        this.insert.execute(fields);
    }

    @TransactionalRollbackAllExceptions
    private void indexSignal(final String eventUuid, final long updateTime) throws ZepException {
        final String insertSql = "INSERT INTO event_summary_index_queue (uuid, update_time) "
                + "VALUES (:uuid, :update_time)";

        Map<String, Object> fields = new HashMap<String, Object>();
        fields.put(COLUMN_UPDATE_TIME, updateTime);
        fields.put("uuid", this.uuidConverter.toDatabaseType(eventUuid));

        this.template.update(insertSql, fields);
    }
}