private IndexOperationMessage handleEdgeDelete(final QueueMessage message) {

    Preconditions.checkNotNull(message, "Queue Message cannot be null for handleEdgeDelete");

    final AsyncEvent event = (AsyncEvent) message.getBody();

    Preconditions.checkNotNull(message, "QueueMessage Body cannot be null for handleEdgeDelete");
    Preconditions.checkArgument(
        event instanceof EdgeDeleteEvent,
        String.format(
            "Event Type for handleEdgeDelete must be EDGE_DELETE, got %s", event.getClass()));

    final EdgeDeleteEvent edgeDeleteEvent = (EdgeDeleteEvent) event;

    final ApplicationScope applicationScope = edgeDeleteEvent.getApplicationScope();
    final Edge edge = edgeDeleteEvent.getEdge();

    if (logger.isDebugEnabled()) {
      logger.debug("Deleting in app scope {} with edge {}", applicationScope, edge);
    }

    // default this observable's return to empty index operation message if nothing is emitted
    return eventBuilder
        .buildDeleteEdge(applicationScope, edge)
        .toBlocking()
        .lastOrDefault(new IndexOperationMessage());
  }
  private IndexOperationMessage handleEdgeIndex(final QueueMessage message) {

    Preconditions.checkNotNull(message, "Queue Message cannot be null for handleEdgeIndex");

    final AsyncEvent event = (AsyncEvent) message.getBody();

    Preconditions.checkNotNull(message, "QueueMessage Body cannot be null for handleEdgeIndex");
    Preconditions.checkArgument(
        event instanceof EdgeIndexEvent,
        String.format(
            "Event Type for handleEdgeIndex must be EDGE_INDEX, got %s", event.getClass()));

    final EdgeIndexEvent edgeIndexEvent = (EdgeIndexEvent) event;

    final EntityCollectionManager ecm =
        entityCollectionManagerFactory.createCollectionManager(
            edgeIndexEvent.getApplicationScope());

    // default this observable's return to empty index operation message if nothing is emitted
    return ecm.load(edgeIndexEvent.getEntityId())
        .flatMap(
            loadedEntity ->
                eventBuilder.buildNewEdge(
                    edgeIndexEvent.getApplicationScope(), loadedEntity, edgeIndexEvent.getEdge()))
        .toBlocking()
        .lastOrDefault(new IndexOperationMessage());
  }
  private IndexOperationMessage handleEntityIndexUpdate(final QueueMessage message) {

    Preconditions.checkNotNull(message, "Queue Message cannot be null for handleEntityIndexUpdate");

    final AsyncEvent event = (AsyncEvent) message.getBody();

    Preconditions.checkNotNull(
        message, "QueueMessage Body cannot be null for handleEntityIndexUpdate");
    Preconditions.checkArgument(
        event instanceof EntityIndexEvent,
        String.format(
            "Event Type for handleEntityIndexUpdate must be ENTITY_INDEX, got %s",
            event.getClass()));

    final EntityIndexEvent entityIndexEvent = (EntityIndexEvent) event;

    // process the entity immediately
    // only process the same version, otherwise ignore
    final EntityIdScope entityIdScope = entityIndexEvent.getEntityIdScope();
    final ApplicationScope applicationScope = entityIdScope.getApplicationScope();
    final Id entityId = entityIdScope.getId();
    final long updatedAfter = entityIndexEvent.getUpdatedAfter();

    final EntityIndexOperation entityIndexOperation =
        new EntityIndexOperation(applicationScope, entityId, updatedAfter);

    // default this observable's return to empty index operation message if nothing is emitted
    return eventBuilder
        .buildEntityIndex(entityIndexOperation)
        .toBlocking()
        .lastOrDefault(new IndexOperationMessage());
  }
  private IndexOperationMessage handleEntityDelete(final QueueMessage message) {

    Preconditions.checkNotNull(message, "Queue Message cannot be null for handleEntityDelete");

    final AsyncEvent event = (AsyncEvent) message.getBody();
    Preconditions.checkNotNull(message, "QueueMessage Body cannot be null for handleEntityDelete");
    Preconditions.checkArgument(
        event instanceof EntityDeleteEvent,
        String.format(
            "Event Type for handleEntityDelete must be ENTITY_DELETE, got %s", event.getClass()));

    final EntityDeleteEvent entityDeleteEvent = (EntityDeleteEvent) event;
    final ApplicationScope applicationScope =
        entityDeleteEvent.getEntityIdScope().getApplicationScope();
    final Id entityId = entityDeleteEvent.getEntityIdScope().getId();

    if (logger.isDebugEnabled())
      logger.debug(
          "Deleting entity id from index in app scope {} with entityId {}",
          applicationScope,
          entityId);

    final EventBuilderImpl.EntityDeleteResults entityDeleteResults =
        eventBuilder.buildEntityDelete(applicationScope, entityId);

    // Delete the entities and remove from graph separately
    entityDeleteResults.getEntitiesDeleted().toBlocking().lastOrDefault(null);

    entityDeleteResults.getCompactedNode().toBlocking().lastOrDefault(null);

    // default this observable's return to empty index operation message if nothing is emitted
    return entityDeleteResults
        .getIndexObservable()
        .toBlocking()
        .lastOrDefault(new IndexOperationMessage());
  }
  private void handleInitializeApplicationIndex(
      final AsyncEvent event, final QueueMessage message) {
    Preconditions.checkNotNull(
        message, "Queue Message cannot be null for handleInitializeApplicationIndex");
    Preconditions.checkArgument(
        event instanceof InitializeApplicationIndexEvent,
        String.format(
            "Event Type for handleInitializeApplicationIndex must be APPLICATION_INDEX, got %s",
            event.getClass()));

    final InitializeApplicationIndexEvent initializeApplicationIndexEvent =
        (InitializeApplicationIndexEvent) event;

    final IndexLocationStrategy indexLocationStrategy =
        initializeApplicationIndexEvent.getIndexLocationStrategy();
    final EntityIndex index = entityIndexFactory.createEntityIndex(indexLocationStrategy);
    index.initialize();
  }
  /**
   * calls the event handlers and returns a result with information on whether it needs to be ack'd
   * and whether it needs to be indexed
   *
   * @param messages
   * @return
   */
  private List<IndexEventResult> callEventHandlers(final List<QueueMessage> messages) {

    if (logger.isDebugEnabled()) {
      logger.debug("callEventHandlers with {} message(s)", messages.size());
    }

    Stream<IndexEventResult> indexEventResults =
        messages
            .stream()
            .map(
                message -> {
                  if (logger.isDebugEnabled()) {
                    logger.debug(
                        "Queue message with ID {} has been received {} time(s)",
                        message.getMessageId(),
                        message.getReceiveCount());
                  }

                  AsyncEvent event = null;
                  try {
                    event = (AsyncEvent) message.getBody();

                  } catch (ClassCastException cce) {
                    logger.error("Failed to deserialize message body", cce);
                    return new IndexEventResult(
                        Optional.absent(), Optional.absent(), System.currentTimeMillis());
                  }

                  if (event == null) {
                    logger.error("AsyncEvent type or event is null!");
                    return new IndexEventResult(
                        Optional.absent(), Optional.absent(), System.currentTimeMillis());
                  }

                  final AsyncEvent thisEvent = event;

                  if (logger.isDebugEnabled()) {
                    logger.debug("Processing event with type {}", event.getClass().getSimpleName());
                  }

                  try {

                    IndexOperationMessage single = new IndexOperationMessage();

                    // normal indexing event for an entity
                    if (event instanceof EntityIndexEvent) {

                      single = handleEntityIndexUpdate(message);

                    }
                    // normal indexing event for an edge
                    else if (event instanceof EdgeIndexEvent) {

                      single = handleEdgeIndex(message);

                    }
                    // deletes are 2-part, actual IO to delete data, then queue up a de-index
                    else if (event instanceof EdgeDeleteEvent) {

                      single = handleEdgeDelete(message);
                    }
                    // deletes are 2-part, actual IO to delete data, then queue up a de-index
                    else if (event instanceof EntityDeleteEvent) {

                      single = handleEntityDelete(message);
                    }
                    // initialization has special logic, therefore a special event type and no index
                    // operation message
                    else if (event instanceof InitializeApplicationIndexEvent) {

                      handleInitializeApplicationIndex(event, message);
                    }
                    // this is the main event that pulls the index doc from map persistence and
                    // hands to the index producer
                    else if (event instanceof ElasticsearchIndexEvent) {

                      handleIndexOperation((ElasticsearchIndexEvent) event);

                    } else if (event instanceof DeIndexOldVersionsEvent) {

                      single = handleDeIndexOldVersionEvent((DeIndexOldVersionsEvent) event);

                    } else {

                      throw new Exception(
                          "Unknown EventType for message: " + message.getStringBody().trim());
                    }

                    if (!(event instanceof ElasticsearchIndexEvent)
                        && !(event instanceof InitializeApplicationIndexEvent)
                        && single.isEmpty()) {
                      logger.warn(
                          "No index operation messages came back from event processing for msg: {} ",
                          message.getStringBody().trim());
                    }

                    // if no exception happens and the QueueMessage is returned in these results, it
                    // will get ack'd
                    return new IndexEventResult(
                        Optional.of(single), Optional.of(message), thisEvent.getCreationTime());

                  } catch (IndexDocNotFoundException e) {

                    // this exception is throw when we wait before trying quorum read on map
                    // persistence.
                    // return empty event result so the event's message doesn't get ack'd
                    if (logger.isDebugEnabled()) {
                      logger.debug(e.getMessage());
                    }
                    return new IndexEventResult(
                        Optional.absent(), Optional.absent(), thisEvent.getCreationTime());

                  } catch (Exception e) {

                    // NPEs don't have a detail message, so add something for our log statement to
                    // identify better
                    final String errorMessage;
                    if (e instanceof NullPointerException) {
                      errorMessage = "NullPointerException";
                    } else {
                      errorMessage = e.getMessage();
                    }

                    // if the event fails to process, log and return empty message result so it
                    // doesn't get ack'd
                    logger.error(
                        "{}. Failed to process message: {}",
                        errorMessage,
                        message.getStringBody().trim());
                    return new IndexEventResult(
                        Optional.absent(), Optional.absent(), thisEvent.getCreationTime());
                  }
                });

    return indexEventResults.collect(Collectors.toList());
  }