/* * Licensed to DuraSpace under one or more contributor license agreements. * See the NOTICE file distributed with this work for additional information * regarding copyright ownership. * * DuraSpace licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except in * compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.fcrepo.kernel.modeshape.observer; import static com.codahale.metrics.MetricRegistry.name; import static com.google.common.collect.Iterators.filter; import static java.util.stream.Collectors.toSet; import static java.util.stream.Stream.concat; import static java.util.stream.Stream.of; import static javax.jcr.observation.Event.NODE_ADDED; import static javax.jcr.observation.Event.NODE_MOVED; import static javax.jcr.observation.Event.NODE_REMOVED; import static javax.jcr.observation.Event.PROPERTY_ADDED; import static javax.jcr.observation.Event.PROPERTY_CHANGED; import static javax.jcr.observation.Event.PROPERTY_REMOVED; import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_BINARY; import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_CONTAINER; import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_REPOSITORY_ROOT; import static org.fcrepo.kernel.api.FedoraTypes.FEDORA_RESOURCE; import static org.fcrepo.kernel.api.FedoraTypes.LDP_BASIC_CONTAINER; import static org.fcrepo.kernel.api.FedoraTypes.LDP_CONTAINER; import static org.fcrepo.kernel.api.FedoraTypes.LDP_NON_RDF_SOURCE; import static org.fcrepo.kernel.api.FedoraTypes.LDP_RDF_SOURCE; import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_DELETION; import static org.fcrepo.kernel.api.observer.EventType.RESOURCE_RELOCATION; import static org.fcrepo.kernel.modeshape.FedoraSessionImpl.getJcrSession; import static org.fcrepo.kernel.modeshape.FedoraJcrConstants.ROOT; import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.JCR_NAMESPACE; import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.JCR_NT_NAMESPACE; import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.MIX_NAMESPACE; import static org.fcrepo.kernel.modeshape.RdfJcrLexicon.MODE_NAMESPACE; import static org.fcrepo.kernel.modeshape.utils.NamespaceTools.getNamespaceRegistry; import static org.fcrepo.kernel.modeshape.utils.UncheckedFunction.uncheck; import static org.fcrepo.kernel.modeshape.utils.StreamUtils.iteratorToStream; import static org.slf4j.LoggerFactory.getLogger; import org.fcrepo.metrics.RegistryService; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Stream; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import javax.inject.Inject; import javax.jcr.NamespaceRegistry; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.observation.Event; import javax.jcr.observation.EventListener; import org.fcrepo.kernel.api.FedoraRepository; import org.fcrepo.kernel.api.exception.RepositoryRuntimeException; import org.fcrepo.kernel.api.models.FedoraResource; import org.fcrepo.kernel.api.observer.FedoraEvent; import org.fcrepo.kernel.modeshape.FedoraResourceImpl; import org.fcrepo.kernel.modeshape.observer.eventmappings.InternalExternalEventMapper; import org.slf4j.Logger; import com.codahale.metrics.Counter; import com.google.common.collect.ImmutableSet; import com.google.common.eventbus.EventBus; /** * Simple JCR EventListener that filters JCR Events through a Fedora EventFilter, maps the results through a mapper, * and puts the resulting stream onto the internal Fedora EventBus as a stream of FedoraEvents. * * @author eddies * @author ajs6f * @since Feb 7, 2013 */ public class SimpleObserver implements EventListener { private static final Logger LOGGER = getLogger(SimpleObserver.class); private static final Set<String> filteredNamespaces = ImmutableSet.of( JCR_NAMESPACE, MIX_NAMESPACE, JCR_NT_NAMESPACE, MODE_NAMESPACE); /** * A simple counter of events that pass through this observer */ static final Counter EVENT_COUNTER = RegistryService.getInstance().getMetrics().counter(name(SimpleObserver.class, "onEvent")); static final Integer EVENT_TYPES = NODE_ADDED + NODE_REMOVED + NODE_MOVED + PROPERTY_ADDED + PROPERTY_CHANGED + PROPERTY_REMOVED; /** * Note: This function maps a FedoraEvent to a Stream of some number of FedoraEvents. This is because a MOVE event * may lead to an arbitrarily large number of additional events for any child resources. In the event of this not * being a MOVE event, the same FedoraEvent is returned, wrapped in a Stream. For a MOVEd resource, the resource in * question will be translated to two FedoraEvents: a MOVED event for the new resource location and a REMOVED event * corresponding to the old location. The same pair of FedoraEvents will also be generated for each child resource. */ private static Function<FedoraEvent, Stream<FedoraEvent>> handleMoveEvents(final Session session) { return evt -> { if (evt.getTypes().contains(RESOURCE_RELOCATION)) { final Map<String, String> movePath = evt.getInfo(); final String dest = movePath.get("destAbsPath"); final String src = movePath.get("srcAbsPath"); try { final FedoraResource resource = new FedoraResourceImpl(session.getNode(evt.getPath())); return concat(of(evt), resource.getChildren(true).map(FedoraResource::getPath) .flatMap(path -> of( new FedoraEventImpl(RESOURCE_RELOCATION, path, evt.getResourceTypes(), evt.getUserID(), evt.getDate(), evt.getInfo()), new FedoraEventImpl(RESOURCE_DELETION, path.replaceFirst(dest, src), evt.getResourceTypes(), evt.getUserID(), evt.getDate(), evt.getInfo())))); } catch (final RepositoryException ex) { throw new RepositoryRuntimeException(ex); } } return of(evt); }; } /** * Note: Certain RDF types are generated dynamically. These are added here, based on * certain type hints. */ private static Function<String, Stream<String>> dynamicTypes = type -> { if (type.equals(ROOT)) { return of(FEDORA_REPOSITORY_ROOT, FEDORA_RESOURCE, FEDORA_CONTAINER, LDP_CONTAINER, LDP_RDF_SOURCE, LDP_BASIC_CONTAINER); } else if (type.equals(FEDORA_CONTAINER)) { return of(FEDORA_CONTAINER, LDP_CONTAINER, LDP_RDF_SOURCE); } else if (type.equals(FEDORA_BINARY)) { return of(FEDORA_BINARY, LDP_NON_RDF_SOURCE); } else { return of(type); } }; private static Function<FedoraEvent, FedoraEvent> filterAndDerefResourceTypes(final Session session) { final NamespaceRegistry registry = getNamespaceRegistry(session); return evt -> { final Set<String> resourceTypes = evt.getResourceTypes().stream() .flatMap(dynamicTypes).map(type -> type.split(":")) .filter(pair -> pair.length == 2).map(uncheck(pair -> new String[]{registry.getURI(pair[0]), pair[1]})) .filter(pair -> !filteredNamespaces.contains(pair[0])).map(pair -> pair[0] + pair[1]).collect(toSet()); return new FedoraEventImpl(evt.getTypes(), evt.getPath(), resourceTypes, evt.getUserID(), evt.getDate(), evt.getInfo()); }; } @Inject private FedoraRepository repository; @Inject private EventBus eventBus; @Inject private InternalExternalEventMapper eventMapper; @Inject private EventFilter eventFilter; // THIS SESSION SHOULD NOT BE USED TO LOOK UP NODES // it is used only to register and deregister this observer to the JCR private Session session; /** * Register this observer with the JCR event listeners * * @throws RepositoryException if repository exception occurred */ @PostConstruct public void buildListener() throws RepositoryException { LOGGER.debug("Constructing an observer for JCR events..."); session = getJcrSession(repository.login()); session.getWorkspace().getObservationManager() .addEventListener(this, EVENT_TYPES, "/", true, null, null, false); session.save(); } /** * logout of the session * * @throws RepositoryException if repository exception occurred */ @PreDestroy public void stopListening() throws RepositoryException { try { LOGGER.debug("Destroying an observer for JCR events..."); session.getWorkspace().getObservationManager().removeEventListener(this); } finally { session.logout(); } } /** * Filter JCR events and transform them into our own FedoraEvents. * * @param events the JCR events */ @Override public void onEvent(final javax.jcr.observation.EventIterator events) { Session lookupSession = null; try { lookupSession = getJcrSession(repository.login()); @SuppressWarnings("unchecked") final Iterator<Event> filteredEvents = filter(events, eventFilter::test); eventMapper.apply(iteratorToStream(filteredEvents)) .map(filterAndDerefResourceTypes(lookupSession)) .flatMap(handleMoveEvents(lookupSession)) .forEach(this::post); } finally { if (lookupSession != null) { lookupSession.logout(); } } } private void post(final FedoraEvent evt) { eventBus.post(evt); EVENT_COUNTER.inc(); } }