001/*
002 * The contents of this file are subject to the license and copyright
003 * detailed in the LICENSE and NOTICE files at the root of the source
004 * tree.
005 */
006
007package org.fcrepo.kernel.impl.observer;
008
009import static com.google.common.base.Preconditions.checkNotNull;
010
011import java.net.URI;
012import java.util.Collections;
013import java.util.Map;
014import java.util.Set;
015import java.util.concurrent.ConcurrentHashMap;
016import java.util.stream.Collectors;
017
018import javax.inject.Inject;
019
020import org.fcrepo.config.AuthPropsConfig;
021import org.fcrepo.kernel.api.Transaction;
022import org.fcrepo.kernel.api.identifiers.FedoraId;
023import org.fcrepo.kernel.api.models.ResourceFactory;
024import org.fcrepo.kernel.api.observer.EventAccumulator;
025import org.fcrepo.kernel.api.operations.ResourceOperation;
026
027import org.slf4j.Logger;
028import org.slf4j.LoggerFactory;
029import org.springframework.stereotype.Component;
030
031import com.google.common.collect.Multimap;
032import com.google.common.collect.MultimapBuilder;
033import com.google.common.eventbus.EventBus;
034
035/**
036 * @author pwinckles
037 */
038@Component
039public class EventAccumulatorImpl implements EventAccumulator {
040
041    private final static Logger LOG = LoggerFactory.getLogger(EventAccumulatorImpl.class);
042
043    private final Map<String, Multimap<FedoraId, EventBuilder>> transactionEventMap;
044
045    @Inject
046    private ResourceFactory resourceFactory;
047
048    @Inject
049    private EventBus eventBus;
050
051    @Inject
052    private AuthPropsConfig authPropsConfig;
053
054    public EventAccumulatorImpl() {
055        this.transactionEventMap = new ConcurrentHashMap<>();
056    }
057
058    @Override
059    public void recordEventForOperation(final Transaction transaction, final FedoraId fedoraId,
060                                        final ResourceOperation operation) {
061        checkNotNull(transaction, "transaction cannot be blank");
062        checkNotNull(fedoraId, "fedoraId cannot be null");
063
064        final String transactionId = transaction.getId();
065        final var events = transactionEventMap.computeIfAbsent(transactionId, key ->
066                MultimapBuilder.hashKeys().arrayListValues().build());
067        final var eventBuilder = ResourceOperationEventBuilder.fromResourceOperation(
068                fedoraId, operation, authPropsConfig.getUserAgentBaseUri());
069        events.put(fedoraId, eventBuilder);
070    }
071
072    @Override
073    public void emitEvents(final Transaction transaction, final String baseUrl, final String userAgent) {
074        LOG.debug("Emitting events for transaction {}", transaction.getId());
075
076        final var eventMap = transactionEventMap.remove(transaction.getId());
077
078        if (eventMap != null) {
079            eventMap.keySet().forEach(fedoraId -> {
080                final var events = eventMap.get(fedoraId);
081
082                try {
083                    final var mergedBuilder = events.stream()
084                            .reduce(EventBuilder::merge).get();
085
086                    final var event = mergedBuilder
087                            .withResourceTypes(loadResourceTypes(transaction, fedoraId))
088                            .withBaseUrl(baseUrl)
089                            .withUserAgent(userAgent)
090                            .build();
091
092                    LOG.debug("Emitting event: {}", event);
093                    eventBus.post(event);
094                } catch (final Exception e) {
095                    LOG.error("Failed to emit events: {}", events, e);
096                }
097            });
098        }
099    }
100
101    @Override
102    public void clearEvents(final Transaction transaction) {
103        LOG.trace("Clearing events for transaction {}", transaction.getId());
104        transactionEventMap.remove(transaction.getId());
105    }
106
107    private Set<String> loadResourceTypes(final Transaction transaction, final FedoraId fedoraId) {
108        try {
109            return resourceFactory.getResource(transaction, fedoraId).getTypes().stream()
110                    .map(URI::toString)
111                    .collect(Collectors.toSet());
112        } catch (final Exception e) {
113            LOG.debug("Could not load resource types for {}", fedoraId, e);
114            // This can happen if the resource no longer exists
115            return Collections.emptySet();
116        }
117    }
118
119}