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