001/**
002 * Copyright 2015 DuraSpace, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.fcrepo.kernel.impl.observer.eventmappings;
017
018import static com.google.common.collect.Multimaps.index;
019import static org.modeshape.jcr.api.JcrConstants.JCR_CONTENT;
020import static org.slf4j.LoggerFactory.getLogger;
021import static java.util.Arrays.asList;
022import static javax.jcr.observation.Event.PROPERTY_ADDED;
023import static javax.jcr.observation.Event.PROPERTY_CHANGED;
024import static javax.jcr.observation.Event.PROPERTY_REMOVED;
025
026import java.util.Iterator;
027import java.util.List;
028
029import javax.jcr.RepositoryException;
030import javax.jcr.observation.Event;
031
032import org.fcrepo.kernel.exception.RepositoryRuntimeException;
033import org.fcrepo.kernel.observer.FedoraEvent;
034import org.fcrepo.kernel.observer.eventmappings.InternalExternalEventMapper;
035
036import org.slf4j.Logger;
037
038import com.google.common.base.Function;
039import com.google.common.collect.Multimap;
040
041/**
042 * Maps all JCR {@link Event}s concerning one JCR node to one
043 * {@link FedoraEvent}. Adds the types of those JCR events together to calculate
044 * the final type of the emitted FedoraEvent. TODO stop aggregating events in
045 * the heap and make this a purely iterative algorithm, if possible
046 *
047 * @author ajs6f
048 * @since Feb 27, 2014
049 */
050public class AllNodeEventsOneEvent implements InternalExternalEventMapper {
051
052    private static final List<Integer> PROPERTY_EVENT_TYPES = asList(PROPERTY_ADDED, PROPERTY_CHANGED,
053            PROPERTY_REMOVED);
054
055    /**
056     * Extracts the node identifier from a JCR {@link Event}.
057     */
058    private static final Function<Event, String> EXTRACT_NODE_ID = new Function<Event, String>() {
059
060        @Override
061        public String apply(final Event ev) {
062            // build id from nodepath+user to collapse multiple nodes from adding/removing content nodes
063            final String id = FedoraEvent.getPath(ev).replaceAll("/" + JCR_CONTENT,"") + "-" + ev.getUserID();
064            LOGGER.debug("Sorting an event by identifier: {}", id);
065            return id;
066        }
067    };
068
069    @Override
070    public Iterator<FedoraEvent> apply(final Iterator<Event> events) {
071        return new FedoraEventIterator(events);
072    }
073
074    private static class FedoraEventIterator implements Iterator {
075
076        private final Iterator<Event> events;
077
078        // sort JCR events into a Multimap keyed by the node ID involved
079        private final Multimap<String, Event> sortedEvents;
080
081        private final Iterator<String> nodeIds;
082
083        public FedoraEventIterator(final Iterator<Event> events) {
084            this.events = events;
085            sortedEvents = index(events, EXTRACT_NODE_ID);
086            nodeIds = sortedEvents.keySet().iterator();
087        }
088
089        @Override
090        public boolean hasNext() {
091            return nodeIds.hasNext();
092        }
093
094        @Override
095        public FedoraEvent next() {
096            final Iterator<Event> nodeSpecificEvents = sortedEvents.get(nodeIds.next()).iterator();
097            // we can safely call next() immediately on nodeSpecificEvents
098            // because if
099            // there was no event at all, there would appear no entry in our
100            // Multimap under this key
101            final Event firstEvent = nodeSpecificEvents.next();
102            final FedoraEvent fedoraEvent = new FedoraEvent(firstEvent);
103
104            addProperty(fedoraEvent, firstEvent);
105            while (nodeSpecificEvents.hasNext()) {
106                // add the event type and property name to the event we are building up to emit
107                // we could aggregate other information here if that seems useful
108                final Event otherEvent = nodeSpecificEvents.next();
109                fedoraEvent.addType(otherEvent.getType());
110                addProperty(fedoraEvent, otherEvent);
111            }
112            return fedoraEvent;
113        }
114
115        @Override
116        public void remove() {
117            // the underlying Multimap is immutable anyway
118            throw new UnsupportedOperationException();
119        }
120
121        private void addProperty( final FedoraEvent fedoraEvent, final Event ev ) {
122            try {
123                if ( ev.getPath().contains(JCR_CONTENT)) {
124                    fedoraEvent.addProperty("fedora:hasContent");
125                }
126                if (PROPERTY_EVENT_TYPES.contains(ev.getType())) {
127                    final String eventPath = ev.getPath();
128                    fedoraEvent.addProperty(eventPath.substring(eventPath.lastIndexOf('/') + 1));
129                } else {
130                    LOGGER.trace("Not adding non-event property: {}, {}", fedoraEvent, ev);
131                }
132            } catch (final RepositoryException e) {
133                throw new RepositoryRuntimeException(e);
134            }
135        }
136    }
137
138    private final static Logger LOGGER = getLogger(AllNodeEventsOneEvent.class);
139}