001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.fcrepo.kernel.impl.observer;
020
021import com.google.common.collect.Multimap;
022import com.google.common.collect.MultimapBuilder;
023import com.google.common.eventbus.EventBus;
024import org.fcrepo.kernel.api.identifiers.FedoraId;
025import org.fcrepo.kernel.api.models.ResourceFactory;
026import org.fcrepo.kernel.api.observer.EventAccumulator;
027import org.fcrepo.kernel.api.operations.ResourceOperation;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030import org.springframework.stereotype.Component;
031
032import javax.inject.Inject;
033import java.net.URI;
034import java.util.Collections;
035import java.util.Map;
036import java.util.Set;
037import java.util.concurrent.ConcurrentHashMap;
038import java.util.stream.Collectors;
039
040import static com.google.common.base.Preconditions.checkNotNull;
041import static com.google.common.base.Strings.emptyToNull;
042
043/**
044 * @author pwinckles
045 */
046@Component
047public class EventAccumulatorImpl implements EventAccumulator {
048
049    private final static Logger LOG = LoggerFactory.getLogger(EventAccumulatorImpl.class);
050
051    private final Map<String, Multimap<FedoraId, EventBuilder>> transactionEventMap;
052
053    @Inject
054    private ResourceFactory resourceFactory;
055
056    @Inject
057    private EventBus eventBus;
058
059    public EventAccumulatorImpl() {
060        this.transactionEventMap = new ConcurrentHashMap<>();
061    }
062
063    @Override
064    public void recordEventForOperation(final String transactionId, final FedoraId fedoraId,
065                                        final ResourceOperation operation) {
066        checkNotNull(emptyToNull(transactionId), "transactionId cannot be blank");
067        checkNotNull(fedoraId, "fedoraId cannot be null");
068
069        final var events = transactionEventMap.computeIfAbsent(transactionId, key ->
070                MultimapBuilder.hashKeys().arrayListValues().build());
071        final var eventBuilder = ResourceOperationEventBuilder.fromResourceOperation(fedoraId, operation);
072        events.put(fedoraId, eventBuilder);
073    }
074
075    @Override
076    public void emitEvents(final String transactionId, final String baseUrl, final String userAgent) {
077        LOG.debug("Emitting events for transaction {}", transactionId);
078
079        final var eventMap = transactionEventMap.remove(transactionId);
080
081        if (eventMap != null) {
082            eventMap.keySet().forEach(fedoraId -> {
083                final var events = eventMap.get(fedoraId);
084
085                try {
086                    final var mergedBuilder = events.stream()
087                            .reduce(EventBuilder::merge).get();
088
089                    final var event = mergedBuilder
090                            .withResourceTypes(loadResourceTypes(fedoraId))
091                            .withBaseUrl(baseUrl)
092                            .withUserAgent(userAgent)
093                            .build();
094
095                    LOG.debug("Emitting event: {}", event);
096                    eventBus.post(event);
097                } catch (Exception e) {
098                    LOG.error("Failed to emit events: {}", events, e);
099                }
100            });
101        }
102    }
103
104    @Override
105    public void clearEvents(final String transactionId) {
106        LOG.trace("Clearing events for transaction {}", transactionId);
107        transactionEventMap.remove(transactionId);
108    }
109
110    private Set<String> loadResourceTypes(final FedoraId fedoraId) {
111        try {
112            return resourceFactory.getResource(fedoraId).getTypes().stream()
113                    .map(URI::toString)
114                    .collect(Collectors.toSet());
115        } catch (Exception e) {
116            LOG.debug("Could not load resource types for {}", fedoraId, e);
117            // This can happen if the resource no longer exists
118            return Collections.emptySet();
119        }
120    }
121
122}