/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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.apache.jackrabbit.core.observation;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import javax.jcr.RepositoryException;
import javax.jcr.Session;
import javax.jcr.observation.Event;
import javax.jcr.observation.EventIterator;
import javax.jcr.observation.EventListener;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.security.authorization.Permission;
import org.apache.jackrabbit.core.state.ItemState;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.PathFactory;
import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* The <code>EventConsumer</code> class combines the {@link
* javax.jcr.observation.EventListener} with the implementation of specified
* filter for the listener: {@link EventFilter}.
* <p>
* Collections of {@link EventState} objects will be dispatched to {@link
* #consumeEvents}.
*/
class EventConsumer {
/**
* The default Logger instance for this class.
*/
private static final Logger log = LoggerFactory.getLogger(EventConsumer.class);
private final PathFactory pathFactory = PathFactoryImpl.getInstance();
/**
* The <code>Session</code> associated with this <code>EventConsumer</code>.
*/
private final SessionImpl session;
/**
* The listener part of this <code>EventConsumer</code>.
*/
private final EventListener listener;
/**
* The <code>EventFilter</code> for this <code>EventConsumer</code>.
*/
private final EventFilter filter;
/**
* A map of <code>Set</code> objects that hold references to
* <code>ItemId</code>s of denied <code>ItemState</code>s. The map uses the
* <code>EventStateCollection</code> as the key to reference a deny Set.
*/
private final Map<EventStateCollection, Set<ItemId>> accessDenied = Collections.synchronizedMap(new WeakHashMap<EventStateCollection, Set<ItemId>>());
/**
* cached hash code value
*/
private int hashCode;
/**
* An <code>EventConsumer</code> consists of a <code>Session</code>, the
* attached <code>EventListener</code> and an <code>EventFilter</code>.
*
* @param session the <code>Session</code> that created this
* <code>EventConsumer</code>.
* @param listener the actual <code>EventListener</code> to call back.
* @param filter only pass an <code>Event</code> to the listener if the
* <code>EventFilter</code> allows the <code>Event</code>.
* @throws NullPointerException if <code>session</code>, <code>listener</code>
* or <code>filter</code> is<code>null</code>.
*/
EventConsumer(SessionImpl session, EventListener listener, EventFilter filter)
throws NullPointerException {
if (session == null) {
throw new NullPointerException("session");
}
if (listener == null) {
throw new NullPointerException("listener");
}
if (filter == null) {
throw new NullPointerException("filter");
}
this.session = session;
this.listener = listener;
this.filter = filter;
}
/**
* Returns the <code>Session</code> that is associated
* with this <code>EventConsumer</code>.
*
* @return the <code>Session</code> of this <code>EventConsumer</code>.
*/
Session getSession() {
return session;
}
/**
* Returns the <code>EventListener</code> that is associated with this
* <code>EventConsumer</code>.
*
* @return the <code>EventListener</code> of this <code>EventConsumer</code>.
*/
EventListener getEventListener() {
return listener;
}
/**
* Checks for what {@link EventState}s this <code>EventConsumer</code> has
* enough access rights to see the event.
*
* @param events the collection of {@link EventState}s.
*/
void prepareEvents(EventStateCollection events) {
Iterator<EventState> it = events.iterator();
Set<ItemId> denied = null;
while (it.hasNext()) {
EventState state = it.next();
if (state.getType() == Event.NODE_REMOVED
|| state.getType() == Event.PROPERTY_REMOVED) {
if (session.equals(state.getSession())) {
// if we created the event, we can be sure that
// we have enough access rights to see the event
continue;
}
// check read permission
ItemId targetId = state.getTargetId();
boolean granted = false;
try {
granted = canRead(state);
} catch (RepositoryException e) {
log.warn("Unable to check access rights for item: " + targetId);
}
if (!granted) {
if (denied == null) {
denied = new HashSet<ItemId>();
}
denied.add(targetId);
}
}
}
if (denied != null) {
accessDenied.put(events, denied);
}
}
/**
* Checks for which deleted <code>ItemStates</code> this
* <code>EventConsumer</code> has enough access rights to see the event.
*
* @param events the collection of {@link EventState}s.
* @param deletedItems Iterator of deleted <code>ItemState</code>s.
*/
void prepareDeleted(EventStateCollection events, Iterable<ItemState> deletedItems) {
Set<ItemId> denied = null;
Set<ItemId> deletedIds = new HashSet<ItemId>();
for (ItemState state : deletedItems) {
deletedIds.add(state.getId());
}
for (Iterator<EventState> it = events.iterator(); it.hasNext();) {
EventState evState = it.next();
ItemId targetId = evState.getTargetId();
if (deletedIds.contains(targetId)) {
// check read permission
boolean granted = false;
try {
granted = canRead(evState);
} catch (RepositoryException e) {
log.warn("Unable to check access rights for item: " + targetId);
}
if (!granted) {
if (denied == null) {
denied = new HashSet<ItemId>();
}
denied.add(targetId);
}
}
}
if (denied != null) {
accessDenied.put(events, denied);
}
}
/**
* Dispatches the events to the <code>EventListener</code>.
*
* @param events a collection of {@link EventState}s
* to dispatch.
*/
void consumeEvents(EventStateCollection events) throws RepositoryException {
// Set of ItemIds of denied ItemStates
Set<ItemId> denied = accessDenied.remove(events);
if (denied == null) {
denied = new HashSet<ItemId>();
}
// check permissions
for (Iterator<EventState> it = events.iterator(); it.hasNext() && session.isLive();) {
EventState state = it.next();
if (state.getType() == Event.NODE_ADDED
|| state.getType() == Event.PROPERTY_ADDED
|| state.getType() == Event.PROPERTY_CHANGED) {
ItemId targetId = state.getTargetId();
if (!canRead(state)) {
denied.add(targetId);
}
}
}
// only deliver if session is still live
if (!session.isLive()) {
return;
}
// check if filtered iterator has at least one event
EventIterator it = new FilteredEventIterator(
session, events.iterator(), events.getTimestamp(),
events.getUserData(), filter, denied, false);
if (it.hasNext()) {
long time = System.currentTimeMillis();
listener.onEvent(it);
time = System.currentTimeMillis() - time;
if (log.isDebugEnabled()) {
log.debug("listener {} processed events in {} ms.",
listener.getClass().getName(), time);
}
} else {
// otherwise skip this listener
}
}
/**
* Returns <code>true</code> if this <code>EventConsumer</code> is equal to
* some other object, <code>false</code> otherwise.
* <p>
* Two <code>EventConsumer</code>s are considered equal if they refer to the
* same <code>Session</code> and the <code>EventListener</code>s they
* reference are equal. Note that the <code>EventFilter</code> is ignored in
* this check.
*
* @param obj the reference object with which to compare.
* @return <code>true</code> if this <code>EventConsumer</code> is equal the
* other <code>EventConsumer</code>.
*/
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof EventConsumer) {
EventConsumer other = (EventConsumer) obj;
return session.equals(other.session)
&& listener.equals(other.listener);
}
return false;
}
/**
* Returns the hash code for this <code>EventConsumer</code>.
*
* @return the hash code for this <code>EventConsumer</code>.
*/
public int hashCode() {
if (hashCode == 0) {
hashCode = session.hashCode() ^ listener.hashCode();
}
return hashCode;
}
/**
* Returns <code>true</code> if the item corresponding to the specified
* <code>eventState</code> can be read the the current session.
*
* @param eventState
* @return
* @throws RepositoryException
*/
private boolean canRead(EventState eventState) throws RepositoryException {
Path targetPath = pathFactory.create(eventState.getParentPath(), eventState.getChildRelPath().getName(), eventState.getChildRelPath().getNormalizedIndex(), true);
return session.getAccessManager().isGranted(targetPath, Permission.READ);
}
}