com.eucalyptus.reporting.ReportingDataVerifier.java Source code

Java tutorial

Introduction

Here is the source code for com.eucalyptus.reporting.ReportingDataVerifier.java

Source

/*************************************************************************
 * Copyright 2009-2012 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.
 ************************************************************************/
package com.eucalyptus.reporting;

import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.persistence.EntityTransaction;

import com.eucalyptus.objectstorage.entities.Bucket;
import org.hibernate.Criteria;
import org.hibernate.ScrollMode;
import org.hibernate.ScrollableResults;
import com.eucalyptus.address.Address;
import com.eucalyptus.address.Addresses;
import com.eucalyptus.auth.Accounts;
import com.eucalyptus.auth.AuthException;
import com.eucalyptus.auth.DatabaseAuthProvider;
import com.eucalyptus.auth.api.AccountProvider;
import com.eucalyptus.auth.principal.Account;
import com.eucalyptus.auth.principal.User;
import com.eucalyptus.blockstorage.Snapshot;
import com.eucalyptus.blockstorage.State;
import com.eucalyptus.blockstorage.Volume;
import com.eucalyptus.entities.Entities;
import com.eucalyptus.entities.TransactionException;
import com.eucalyptus.entities.Transactions;
import com.eucalyptus.objectstorage.entities.ObjectEntity;
import com.eucalyptus.objectstorage.util.ObjectStorageProperties;
import com.eucalyptus.reporting.domain.ReportingAccountCrud;
import com.eucalyptus.reporting.domain.ReportingUserCrud;
import com.eucalyptus.reporting.event_store.ReportingElasticIpAttachEvent;
import com.eucalyptus.reporting.event_store.ReportingElasticIpCreateEvent;
import com.eucalyptus.reporting.event_store.ReportingElasticIpDeleteEvent;
import com.eucalyptus.reporting.event_store.ReportingElasticIpDetachEvent;
import com.eucalyptus.reporting.event_store.ReportingElasticIpEventStore;
import com.eucalyptus.reporting.event_store.ReportingEventSupport;
import com.eucalyptus.reporting.event_store.ReportingS3ObjectCreateEvent;
import com.eucalyptus.reporting.event_store.ReportingS3ObjectDeleteEvent;
import com.eucalyptus.reporting.event_store.ReportingS3ObjectEventStore;
import com.eucalyptus.reporting.event_store.ReportingVolumeAttachEvent;
import com.eucalyptus.reporting.event_store.ReportingVolumeCreateEvent;
import com.eucalyptus.reporting.event_store.ReportingVolumeDeleteEvent;
import com.eucalyptus.reporting.event_store.ReportingVolumeDetachEvent;
import com.eucalyptus.reporting.event_store.ReportingVolumeEventStore;
import com.eucalyptus.reporting.event_store.ReportingVolumeSnapshotCreateEvent;
import com.eucalyptus.reporting.event_store.ReportingVolumeSnapshotDeleteEvent;
import com.eucalyptus.reporting.event_store.ReportingVolumeSnapshotEventStore;
import com.eucalyptus.util.Callback;
import com.eucalyptus.util.Exceptions;
import com.eucalyptus.util.HasNaturalId;
import com.eucalyptus.vm.VmInstances;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

/**
 * Class to verify / create reporting data.
 *
 * Create a view of the current cloud status from reporting data or from
 * cloud metadata. Views can be compared and synchronized.
 */
public final class ReportingDataVerifier {

    // Accounts.getAccountProvider() will be null during a DB upgrade
    private static Supplier<AccountProvider> accountProviderSupplier = Suppliers
            .memoize(new Supplier<AccountProvider>() {
                @Override
                public AccountProvider get() {
                    return Accounts.getAccountProvider() != null ? Accounts.getAccountProvider()
                            : new DatabaseAuthProvider();
                }
            });

    /**
     * Update the reporting database to ensure events are present for existing items.
     *
     * @return A description of the changes made
     */
    public static String addMissingReportingEvents() {
        final View liveView = getLiveView();
        final View reportingView = getReportingView();
        final EventDescriptionBag eventDescriptions = getEventDifferences(liveView, reportingView);
        addReportingEvents(eventDescriptions);
        return "Live:\n" + liveView + "\nReporting:\n" + reportingView + "\nGap:\n" + eventDescriptions;
    }

    /**
     * Get the "event" view of the cloud metadata.
     *
     * @return The "live" view
     */
    public static View getLiveView() {
        return new LiveViewBuilder().buildView();
    }

    /**
     * Get the view of the reporting database / event store
     *
     * @return The reporting view
     */
    public static View getReportingView() {
        return new ReportingViewBuilder().buildView();
    }

    /**
     * Get the events neccessary to align the current view with the target view.
     *
     * @param target The desired "event" state
     * @param current The current state
     * @return The bag describing the necessary events
     */
    public static EventDescriptionBag getEventDifferences(final View target, final View current) {
        final EventDescriptionBag description = new EventDescriptionBag();

        final Set<Class<?>> types = Sets.newHashSet(Iterables
                .transform(Iterables.concat(target.typedResourceHolders, current.typedResourceHolders), type()));
        for (final Class<?> type : types) {
            final TypedResourceHolder targetHolder = target.getHolderOrNull(type);
            final TypedResourceHolder currentHolder = current.getHolderOrNull(type);

            if (targetHolder == null) {
                description.delete.add(currentHolder);
            } else if (currentHolder == null) {
                description.create.add(targetHolder);
            } else { // determine difference
                final TypedResourceHolder add = new TypedResourceHolder(type);
                final TypedResourceHolder del = new TypedResourceHolder(type);
                final TypedResourceHolder att = new TypedResourceHolder(type);
                final TypedResourceHolder det = new TypedResourceHolder(type);

                // create copies containing only items that differ
                List<ResourceWithRelation<?>> targetCopy = Lists.newArrayList(targetHolder.resources);
                List<ResourceWithRelation<?>> currentCopy = Lists.newArrayList(currentHolder.resources);
                targetCopy.removeAll(currentHolder.resources);
                currentCopy.removeAll(targetHolder.resources);

                final Set<ResourceKey> keys = Sets
                        .newHashSet(Iterables.transform(Iterables.concat(targetCopy, currentCopy), key()));
                for (final ResourceKey key : keys) {
                    final List<ResourceWithRelation<?>> findIn;
                    final TypedResourceHolder addTo;
                    if (!Iterables.contains(Iterables.transform(targetCopy, key()), key)) {
                        findIn = currentCopy;
                        addTo = del;
                    } else if (!Iterables.contains(Iterables.transform(currentCopy, key()), key)) {
                        findIn = targetCopy;
                        addTo = add;
                    } else if (Iterables.find(targetCopy, withKeyMatching(key)).relationId == null) {
                        findIn = currentCopy;
                        addTo = det;
                    } else {
                        findIn = targetCopy;
                        addTo = att;
                    }
                    addTo.resources.add(Iterables.find(findIn, withKeyMatching(key)));
                }

                if (!add.resources.isEmpty())
                    description.create.add(add);
                if (!del.resources.isEmpty())
                    description.delete.add(del);
                if (!att.resources.isEmpty())
                    description.attach.add(att);
                if (!det.resources.isEmpty())
                    description.detach.add(det);
            }
        }

        return description;
    }

    /**
     * Add events as described in the given bag
     *
     * @param eventDescriptions The description of the events to add.
     */
    public static void addReportingEvents(final EventDescriptionBag eventDescriptions) {
        final long timestamp = System.currentTimeMillis();
        final Set<String> verifiedUserIds = Sets.newHashSet();
        addCreateEvents(verifiedUserIds, eventDescriptions.create);
        addDeleteEvents(timestamp, eventDescriptions.delete);
        addAttachmentStateEvents(timestamp, verifiedUserIds, eventDescriptions.attach, true);
        addAttachmentStateEvents(timestamp, verifiedUserIds, eventDescriptions.detach, false);
    }

    private static Predicate<ResourceWithRelation<?>> withKeyMatching(final ResourceKey key) {
        return Predicates.compose(Predicates.equalTo(key), key());
    }

    private static void addCreateEvents(final Set<String> verifiedUserIds,
            final List<TypedResourceHolder> holders) {
        final Map<String, User> accountNumberToAccountAdminMap = Maps.newHashMap();
        for (TypedResourceHolder holder : holders) {
            for (ResourceWithRelation resource : holder.resources) {
                if (Address.class.equals(holder.type)) {
                    final ReportingElasticIpEventStore store = ReportingElasticIpEventStore.getInstance();
                    final Address address = findAddress(resource.resourceKey.toString());
                    if (address != null && ensureUserAndAccount(verifiedUserIds, address.getUserId())) {
                        store.insertCreateEvent(address.getCreationTimestamp().getTime(), address.getUserId(),
                                address.getDisplayName());
                    }
                } else if (ObjectEntity.class.equals(holder.type)) {
                    final ReportingS3ObjectEventStore store = ReportingS3ObjectEventStore.getInstance();
                    final S3ObjectKey key = (S3ObjectKey) resource.resourceKey;
                    final ObjectEntity objectInfo = findObjectInfo(key);
                    String accountId = null;
                    try {
                        accountId = Accounts.lookupAccountById(objectInfo.getOwnerCanonicalId()).getAccountNumber();
                    } catch (Exception e) {
                        accountId = null;
                    }

                    final User user = objectInfo == null ? null
                            : getAccountAdmin(accountNumberToAccountAdminMap, accountId);
                    if (objectInfo != null && user != null
                            && ensureUserAndAccount(verifiedUserIds, user.getUserId())) {
                        store.insertS3ObjectCreateEvent(objectInfo.getBucket().getBucketName(),
                                objectInfo.getObjectKey(), objectInfo.getVersionId(), objectInfo.getSize(),
                                objectInfo.getCreationTimestamp().getTime(), user.getUserId());
                    }
                } else if (Snapshot.class.equals(holder.type)) {
                    final ReportingVolumeSnapshotEventStore store = ReportingVolumeSnapshotEventStore.getInstance();
                    final Snapshot snapshot = findSnapshot(resource.resourceKey.toString());
                    final Volume volume = snapshot == null ? null : findVolumeById(snapshot.getParentVolume());
                    if (snapshot != null && volume != null
                            && ensureUserAndAccount(verifiedUserIds, snapshot.getOwnerUserId())) {
                        store.insertCreateEvent(snapshot.getNaturalId(), volume.getNaturalId(),
                                snapshot.getDisplayName(), snapshot.getCreationTimestamp().getTime(),
                                snapshot.getOwnerUserId(), snapshot.getVolumeSize());
                    }
                } else if (Volume.class.equals(holder.type)) {
                    final ReportingVolumeEventStore store = ReportingVolumeEventStore.getInstance();
                    final Volume volume = findVolume(resource.resourceKey.toString());
                    if (volume != null && ensureUserAndAccount(verifiedUserIds, volume.getOwnerUserId())) {
                        store.insertCreateEvent(volume.getNaturalId(), volume.getDisplayName(),
                                volume.getCreationTimestamp().getTime(), volume.getOwnerUserId(),
                                volume.getPartition(), volume.getSize());
                    }
                }
            }
        }
    }

    private static void addDeleteEvents(final long timestamp, final List<TypedResourceHolder> holders) {
        for (TypedResourceHolder holder : holders) {
            for (ResourceWithRelation resource : holder.resources) {
                if (Address.class.equals(holder.type)) {
                    final ReportingElasticIpEventStore store = ReportingElasticIpEventStore.getInstance();
                    store.insertDeleteEvent(resource.resourceKey.toString(), timestamp);
                } else if (ObjectEntity.class.equals(holder.type)) {
                    final ReportingS3ObjectEventStore store = ReportingS3ObjectEventStore.getInstance();
                    final S3ObjectKey key = (S3ObjectKey) resource.resourceKey;
                    store.insertS3ObjectDeleteEvent(key.bucketName, key.objectKey, key.objectVersion, timestamp);
                } else if (Snapshot.class.equals(holder.type)) {
                    final ReportingVolumeSnapshotEventStore store = ReportingVolumeSnapshotEventStore.getInstance();
                    store.insertDeleteEvent(resource.resourceKey.toString(), timestamp);
                } else if (Volume.class.equals(holder.type)) {
                    final ReportingVolumeEventStore store = ReportingVolumeEventStore.getInstance();
                    store.insertDeleteEvent(resource.resourceKey.toString(), timestamp);
                }
            }
        }
    }

    private static void addAttachmentStateEvents(final long timestamp, final Set<String> verifiedUserIds,
            final List<TypedResourceHolder> holders, final boolean attach) {
        for (TypedResourceHolder holder : holders) {
            for (ResourceWithRelation resource : holder.resources) {
                if (Address.class.equals(holder.type)) {
                    final ReportingElasticIpEventStore store = ReportingElasticIpEventStore.getInstance();
                    if (attach) {
                        store.insertAttachEvent(resource.resourceKey.toString(), resource.relationId, timestamp);
                    } else {
                        store.insertDetachEvent(resource.resourceKey.toString(), resource.relationId, timestamp);
                    }
                } else if (Volume.class.equals(holder.type)) {
                    final Volume volume = findVolume(resource.resourceKey.toString());
                    if (volume != null && ensureUserAndAccount(verifiedUserIds, volume.getOwnerUserId())) {
                        final ReportingVolumeEventStore store = ReportingVolumeEventStore.getInstance();
                        if (attach) {
                            store.insertAttachEvent(volume.getNaturalId(), resource.relationId, volume.getSize(),
                                    timestamp);
                        } else {
                            store.insertDetachEvent(volume.getNaturalId(), resource.relationId, timestamp);
                        }
                    }
                }
            }
        }
    }

    private static ResourceWithRelation<SimpleResourceKey> resource(final String resourceId,
            final String relationId) {
        return new ResourceWithRelation<SimpleResourceKey>(new SimpleResourceKey(resourceId), relationId);
    }

    private static ResourceWithRelation<S3ObjectKey> s3ObjectResource(final String bucketName,
            final String objectKey, final String objectVersion) {
        return s3ObjectResource(new S3ObjectKey(bucketName, objectKey, objectVersion));
    }

    private static ResourceWithRelation<S3ObjectKey> s3ObjectResource(final S3ObjectKey key) {
        return new ResourceWithRelation<S3ObjectKey>(key, null);
    }

    private static void append(final StringBuilder builder, final List<TypedResourceHolder> typedResourceHolders) {
        for (final TypedResourceHolder typedResourceHolder : typedResourceHolders) {
            builder.append("Type: ").append(typedResourceHolder.type.getSimpleName()).append("\n");
            for (ResourceWithRelation<?> resource : typedResourceHolder.resources) {
                builder.append(resource).append("\n");
            }
        }
    }

    private static boolean ensureUserAndAccount(final Set<String> userIds, final String userId) {
        boolean verified;

        if (userIds.contains(userId)) {
            verified = true;
        } else {
            try {
                final User user = getAccountProvider().lookupUserById(userId);
                final Account account = user.getAccount();
                ReportingAccountCrud.getInstance().createOrUpdateAccount(account.getAccountNumber(),
                        account.getName());
                ReportingUserCrud.getInstance().createOrUpdateUser(user.getUserId(), account.getAccountNumber(),
                        user.getName());
                verified = true;
            } catch (AuthException e) {
                verified = false;
            }
        }

        return verified;
    }

    private static User getAccountAdmin(final Map<String, User> accountNumberToAccountAdminMap,
            final String accountNumber) {
        User user = accountNumberToAccountAdminMap.get(accountNumber);

        if (user == null && !accountNumberToAccountAdminMap.containsKey(accountNumber)) {
            try {
                user = getAccountProvider().lookupAccountById(accountNumber).lookupAdmin();
                accountNumberToAccountAdminMap.put(accountNumber, user);
            } catch (AuthException e) {
                accountNumberToAccountAdminMap.put(accountNumber, null);
            }
        }

        return user;
    }

    private static AccountProvider getAccountProvider() {
        return accountProviderSupplier.get();
    }

    private static Address findAddress(final String uuid) {
        return Iterables.getFirst(Iterables.filter(
                Iterables.concat(Addresses.getInstance().listValues(),
                        Addresses.getInstance().listDisabledValues()),
                Predicates.compose(Predicates.equalTo(uuid), naturalId())), null);
    }

    private static ObjectEntity findObjectInfo(final S3ObjectKey key) {
        try {
            final ObjectEntity objectInfo = new ObjectEntity();
            objectInfo.setBucket(new Bucket(key.bucketName));
            objectInfo.setObjectKey(key.objectKey);
            objectInfo.setVersionId(
                    key.objectVersion == null ? ObjectStorageProperties.NULL_VERSION_ID : key.objectVersion);
            final List<ObjectEntity> infos = Transactions.findAll(objectInfo);
            if (infos.isEmpty()) {
                return null;
            }
            ObjectEntity result = infos.get(0);
            for (final ObjectEntity current : infos) {
                if (current.getCreationTimestamp().after(result.getCreationTimestamp())) {
                    result = current;
                }
            }
            return result;
        } catch (TransactionException e) {
            return null;
        }
    }

    private static Snapshot findSnapshot(final String uuid) {
        return findOrNull(Snapshot.naturalId(uuid));
    }

    private static Volume findVolume(final String uuid) {
        return findOrNull(Volume.naturalId(uuid));
    }

    private static Volume findVolumeById(final String id) {
        return findOrNull(Volume.named(null, id));
    }

    private static <T> T findOrNull(final T example) {
        try {
            return Transactions.find(example);
        } catch (TransactionException e) {
            return null;
        }
    }

    private static Function<HasNaturalId, String> naturalId() {
        return new Function<HasNaturalId, String>() {
            @Override
            public String apply(final HasNaturalId hasNaturalId) {
                return hasNaturalId.getNaturalId();
            }
        };
    }

    private static Function<TypedResourceHolder, Class<?>> type() {
        return new Function<TypedResourceHolder, Class<?>>() {
            @Override
            public Class<?> apply(final TypedResourceHolder typedResourceHolder) {
                return typedResourceHolder.type;
            }
        };
    }

    private static Function<ResourceWithRelation<?>, ResourceKey> key() {
        return new Function<ResourceWithRelation<?>, ResourceKey>() {
            @Override
            public ResourceKey apply(final ResourceWithRelation<?> resource) {
                return resource.resourceKey;
            }
        };
    }

    public static class EventDescriptionBag {
        private final List<TypedResourceHolder> create = Lists.newArrayList();
        private final List<TypedResourceHolder> delete = Lists.newArrayList();
        private final List<TypedResourceHolder> attach = Lists.newArrayList();
        private final List<TypedResourceHolder> detach = Lists.newArrayList();

        public String toString() {
            final StringBuilder builder = new StringBuilder();

            builder.append("Creation required:\n");
            append(builder, create);
            builder.append("Deletion required:\n");
            append(builder, delete);
            builder.append("Attach required:\n");
            append(builder, attach);
            builder.append("Detach required:\n");
            append(builder, detach);

            return builder.toString();
        }
    }

    /**
     * A view of current status.
     *
     * We care about:
     *  - Allocated Elastic IPs and their association target
     *  - Existing S3 objects
     *  - Existing volumes and their attachment target
     *  - Existing snapshots
     */
    public static class View {
        private final List<TypedResourceHolder> typedResourceHolders = Lists.newArrayList();

        private <CM> void add(@Nonnull final Class<CM> type, @Nonnull final String resourceId,
                @Nullable final String relationId) {
            add(type, resource(resourceId, relationId));
        }

        private <CM> void add(@Nonnull final Class<CM> type, @Nonnull final ResourceWithRelation<?> resource) {
            final TypedResourceHolder holder = getHolder(type);
            holder.resources.add(resource);
        }

        @Nonnull
        private <CM> TypedResourceHolder getHolder(@Nonnull final Class<CM> type) {
            TypedResourceHolder holder = getHolderOrNull(type);

            if (holder == null) {
                holder = new TypedResourceHolder(type);
                typedResourceHolders.add(holder);
            }

            return holder;
        }

        private <CM> TypedResourceHolder getHolderOrNull(final Class<CM> type) {
            return Iterables.get(Iterables.filter(typedResourceHolders, new Predicate<TypedResourceHolder>() {
                @Override
                public boolean apply(final TypedResourceHolder typedResourceHolder) {
                    return type.equals(typedResourceHolder.type);
                }
            }), 0, null);
        }

        public String toString() {
            final StringBuilder builder = new StringBuilder();
            append(builder, typedResourceHolders);
            return builder.toString();
        }
    }

    private static abstract class ViewBuilder {
        abstract View buildView();
    }

    private static final class LiveViewBuilder extends ViewBuilder {
        @Override
        View buildView() {
            try {
                final View view = new View();

                // Addresses
                for (final Address address : Iterables.concat(Addresses.getInstance().listValues(),
                        Addresses.getInstance().listDisabledValues())) {
                    if (address.isAllocated() && !address.isSystemOwned()) {
                        final String id = address.getNaturalId();
                        String relatedId = null;
                        if (address.isAssigned()) {
                            relatedId = address.getInstanceUuid();
                        }
                        view.add(Address.class, id, relatedId);
                    }
                }

                // S3 Objects
                for (final ObjectEntity objectInfo : Transactions.findAll(new ObjectEntity())) {
                    if (Boolean.FALSE.equals(objectInfo.getIsDeleteMarker())) {
                        view.add(ObjectEntity.class, s3ObjectResource(objectInfo.getBucket().getBucketName(),
                                objectInfo.getObjectKey(), objectInfo.getVersionId()));
                    }
                }

                // Volumes
                for (final Volume volume : Transactions.findAll(Volume.named(null, null))) {
                    if (volume.isReady()) {
                        final String id = volume.getNaturalId();
                        String relatedId = null;
                        try {
                            relatedId = VmInstances.lookupVolumeAttachment(volume.getDisplayName()).getVmInstance()
                                    .getInstanceId();
                        } catch (NoSuchElementException ex1) {
                            /** no attachment **/
                        }
                        view.add(Volume.class, id, relatedId);
                    }
                }

                // Snapshots
                for (final Snapshot snapshot : Transactions.findAll(Snapshot.named(null, null))) {
                    if (EnumSet.of(State.EXTANT, State.BUSY).contains(snapshot.getState())) {
                        view.add(Snapshot.class, snapshot.getNaturalId(), null);
                    }
                }

                return view;
            } catch (TransactionException e) {
                throw Exceptions.toUndeclared(e);
            }
        }

    }

    private static final class ReportingViewBuilder extends ViewBuilder {
        private static final class RelationTimestamp {
            private final String relationId;
            private final long timestamp;

            private RelationTimestamp(final String relationId, final long timestamp) {
                this.relationId = relationId;
                this.timestamp = timestamp;
            }
        }

        @Override
        View buildView() {
            // Build address info, map key denotes existence
            final Map<String, RelationTimestamp> addressRelationMap = Maps.newHashMap();
            foreach(ReportingElasticIpCreateEvent.class, new Callback<ReportingElasticIpCreateEvent>() {
                @Override
                public void fire(final ReportingElasticIpCreateEvent input) {
                    RelationTimestamp rt = addressRelationMap.get(input.getIp());
                    if (rt == null || rt.timestamp < input.getTimestampMs()) {
                        addressRelationMap.put(input.getIp(), new RelationTimestamp(null, input.getTimestampMs()));
                    }
                }
            });
            foreach(ReportingElasticIpDeleteEvent.class, new Callback<ReportingElasticIpDeleteEvent>() {
                @Override
                public void fire(final ReportingElasticIpDeleteEvent input) {
                    RelationTimestamp rt = addressRelationMap.get(input.getIp());
                    if (rt == null || rt.timestamp < input.getTimestampMs()) {
                        addressRelationMap.remove(input.getIp());
                    }
                }
            });
            foreach(ReportingElasticIpAttachEvent.class, new Callback<ReportingElasticIpAttachEvent>() {
                @Override
                public void fire(final ReportingElasticIpAttachEvent input) {
                    if (addressRelationMap.containsKey(input.getIp())) {
                        RelationTimestamp rt = addressRelationMap.get(input.getIp());
                        if (rt == null || rt.relationId == null || rt.timestamp < input.getTimestampMs()) {
                            addressRelationMap.put(input.getIp(),
                                    new RelationTimestamp(input.getInstanceUuid(), input.getTimestampMs()));
                        }
                    }
                }
            });
            foreach(ReportingElasticIpDetachEvent.class, new Callback<ReportingElasticIpDetachEvent>() {
                @Override
                public void fire(final ReportingElasticIpDetachEvent input) {
                    if (addressRelationMap.containsKey(input.getIp())) {
                        RelationTimestamp rt = addressRelationMap.get(input.getIp());
                        if (rt != null && rt.relationId != null && rt.relationId.equals(input.getInstanceUuid())
                                && rt.timestamp < input.getTimestampMs()) {
                            addressRelationMap.put(input.getIp(), null);
                        }
                    }
                }
            });

            // Build S3 info
            final List<S3ObjectKey> s3ObjectList = Lists.newLinkedList();
            foreach(ReportingS3ObjectCreateEvent.class, new Callback<ReportingS3ObjectCreateEvent>() {
                @Override
                public void fire(final ReportingS3ObjectCreateEvent input) {
                    s3ObjectList.add(new S3ObjectKey(input.getS3BucketName(), input.getS3ObjectKey(),
                            input.getObjectVersion()));
                }
            });
            foreach(ReportingS3ObjectDeleteEvent.class, new Callback<ReportingS3ObjectDeleteEvent>() {
                @Override
                public void fire(final ReportingS3ObjectDeleteEvent input) {
                    s3ObjectList.remove(new S3ObjectKey(input.getS3BucketName(), input.getS3ObjectKey(),
                            input.getObjectVersion()));
                }
            });

            // Build volume info
            final Map<String, RelationTimestamp> volumeRelationMap = Maps.newHashMap();
            foreach(ReportingVolumeCreateEvent.class, new Callback<ReportingVolumeCreateEvent>() {
                @Override
                public void fire(final ReportingVolumeCreateEvent input) {
                    volumeRelationMap.put(input.getUuid(), null);
                }
            });
            foreach(ReportingVolumeDeleteEvent.class, new Callback<ReportingVolumeDeleteEvent>() {
                @Override
                public void fire(final ReportingVolumeDeleteEvent input) {
                    volumeRelationMap.remove(input.getUuid());
                }
            });
            foreach(ReportingVolumeAttachEvent.class, new Callback<ReportingVolumeAttachEvent>() {
                @Override
                public void fire(final ReportingVolumeAttachEvent input) {
                    if (volumeRelationMap.containsKey(input.getVolumeUuid())) {
                        RelationTimestamp rt = volumeRelationMap.get(input.getVolumeUuid());
                        if (rt == null || rt.timestamp < input.getTimestampMs()) {
                            volumeRelationMap.put(input.getVolumeUuid(),
                                    new RelationTimestamp(input.getInstanceUuid(), input.getTimestampMs()));
                        }
                    }
                }
            });
            foreach(ReportingVolumeDetachEvent.class, new Callback<ReportingVolumeDetachEvent>() {
                @Override
                public void fire(final ReportingVolumeDetachEvent input) {
                    if (volumeRelationMap.containsKey(input.getVolumeUuid())) {
                        RelationTimestamp rt = volumeRelationMap.get(input.getVolumeUuid());
                        if (rt != null && rt.relationId.equals(input.getInstanceUuid())
                                && rt.timestamp < input.getTimestampMs()) {
                            volumeRelationMap.put(input.getVolumeUuid(), null);
                        }
                    }
                }
            });

            // Build snapshot info
            final List<String> snapshotList = Lists.newLinkedList();
            foreach(ReportingVolumeSnapshotCreateEvent.class, new Callback<ReportingVolumeSnapshotCreateEvent>() {
                @Override
                public void fire(final ReportingVolumeSnapshotCreateEvent input) {
                    snapshotList.add(input.getUuid());
                }
            });
            foreach(ReportingVolumeSnapshotDeleteEvent.class, new Callback<ReportingVolumeSnapshotDeleteEvent>() {
                @Override
                public void fire(final ReportingVolumeSnapshotDeleteEvent input) {
                    snapshotList.remove(input.getUuid());
                }
            });

            // Build view for extant resources
            final View view = new View();

            for (final Map.Entry<String, RelationTimestamp> addressEntry : addressRelationMap.entrySet()) {
                view.add(Address.class, addressEntry.getKey(),
                        addressEntry.getValue() == null ? null : addressEntry.getValue().relationId);
            }

            for (final S3ObjectKey s3ObjectKey : s3ObjectList) {
                view.add(ObjectEntity.class, ReportingDataVerifier.s3ObjectResource(s3ObjectKey));
            }

            for (final Map.Entry<String, RelationTimestamp> volumeEntry : volumeRelationMap.entrySet()) {
                view.add(Volume.class, volumeEntry.getKey(),
                        volumeEntry.getValue() == null ? null : volumeEntry.getValue().relationId);
            }

            for (final String snapshotUuid : snapshotList) {
                view.add(Snapshot.class, snapshotUuid, null);
            }

            return view;
        }

        private <RET extends ReportingEventSupport> void foreach(final Class<RET> type,
                final Callback<RET> callback) {
            final EntityTransaction transaction = Entities.get(type);
            ScrollableResults results = null;
            try {
                results = Entities.createCriteria(type).setCacheable(false).setReadOnly(true)
                        .setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY).scroll(ScrollMode.FORWARD_ONLY);

                while (results.next()) {
                    final Object result = results.get(0);
                    if (type.isInstance(result)) {
                        callback.fire(type.cast(result));
                    }
                }
            } finally {
                if (results != null)
                    try {
                        results.close();
                    } finally {
                        transaction.rollback();
                    }
                else {
                    transaction.rollback();
                }
            }
        }
    }

    private static final class TypedResourceHolder {
        private final Class<?> type;
        private final List<ResourceWithRelation<?>> resources;

        private TypedResourceHolder(final Class<?> type) {
            this.type = type;
            this.resources = Lists.newArrayList();
        }
    }

    private static abstract class ResourceKey {
    }

    private static final class SimpleResourceKey extends ResourceKey {
        private final String key;

        private SimpleResourceKey(final String key) {
            this.key = key;
        }

        @Override
        public boolean equals(final Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            final SimpleResourceKey that = (SimpleResourceKey) o;

            return key.equals(that.key);
        }

        @Override
        public int hashCode() {
            return key.hashCode();
        }

        public String toString() {
            return key;
        }
    }

    private static final class S3ObjectKey extends ResourceKey {
        @Nonnull
        private final String bucketName;
        @Nonnull
        private final String objectKey;
        @Nullable
        private final String objectVersion;

        private S3ObjectKey(@Nonnull final String bucketName, @Nonnull final String objectKey,
                @Nullable final String objectVersion) {
            this.bucketName = bucketName;
            this.objectKey = objectKey;
            this.objectVersion = objectVersion;
        }

        @SuppressWarnings("RedundantIfStatement")
        @Override
        public boolean equals(final Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            final S3ObjectKey that = (S3ObjectKey) o;

            if (!bucketName.equals(that.bucketName))
                return false;
            if (!objectKey.equals(that.objectKey))
                return false;
            if (objectVersion != null ? !objectVersion.equals(that.objectVersion) : that.objectVersion != null)
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = bucketName.hashCode();
            result = 31 * result + objectKey.hashCode();
            result = 31 * result + (objectVersion != null ? objectVersion.hashCode() : 0);
            return result;
        }

        @Override
        public String toString() {
            return "ObjectKey[" + "bucket:" + bucketName + "; object:" + objectKey + "; version:" + objectVersion
                    + ']';
        }
    }

    private static class ResourceWithRelation<KT extends ResourceKey> {
        @Nonnull
        private final KT resourceKey;
        @Nullable
        private final String relationId;

        private ResourceWithRelation(@Nonnull final KT resourceKey, @Nullable final String relationId) {
            this.resourceKey = resourceKey;
            this.relationId = relationId;
        }

        public String toString() {
            final StringBuilder builder = new StringBuilder();

            builder.append("key:").append(resourceKey);
            if (relationId != null) {
                builder.append("; relation:").append(relationId);
            }

            return builder.toString();
        }

        @SuppressWarnings("RedundantIfStatement")
        @Override
        public boolean equals(final Object o) {
            if (this == o)
                return true;
            if (o == null || getClass() != o.getClass())
                return false;

            final ResourceWithRelation that = (ResourceWithRelation) o;

            if (relationId != null ? !relationId.equals(that.relationId) : that.relationId != null)
                return false;
            if (!resourceKey.equals(that.resourceKey))
                return false;

            return true;
        }

        @Override
        public int hashCode() {
            int result = resourceKey.hashCode();
            result = 31 * result + (relationId != null ? relationId.hashCode() : 0);
            return result;
        }
    }

}