/*
* 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 org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.id.ItemId;
import org.apache.jackrabbit.core.id.NodeId;
import org.apache.jackrabbit.core.SessionImpl;
import org.apache.jackrabbit.core.nodetype.NodeTypeImpl;
import org.apache.jackrabbit.core.state.ChangeLog;
import org.apache.jackrabbit.core.state.ItemState;
import org.apache.jackrabbit.core.state.ItemStateException;
import org.apache.jackrabbit.core.state.ItemStateManager;
import org.apache.jackrabbit.core.state.NoSuchItemStateException;
import org.apache.jackrabbit.core.state.NodeState;
import org.apache.jackrabbit.core.state.ChildNodeEntry;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl;
import org.apache.jackrabbit.spi.commons.name.PathBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.NamespaceException;
import javax.jcr.RepositoryException;
import javax.jcr.observation.ObservationManager;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.Collections;
/**
* The <code>EventStateCollection</code> class implements how {@link EventState}
* objects are created based on the {@link org.apache.jackrabbit.core.state.ItemState}s
* passed to the {@link #createEventStates} method.
* <p/>
* The basic sequence of method calls is:
* <ul>
* <li>{@link #createEventStates} or {@link #addAll} to create or add event
* states to the collection</li>
* <li>{@link #prepare} or {@link #prepareDeleted} to prepare the events. If
* this step is omitted, EventListeners might see events of deleted item
* they are not allowed to see.</li>
* <li>{@link #dispatch()} to dispatch the events to the EventListeners.</li>
* </ul>
*/
public final class EventStateCollection {
/**
* Logger instance for this class
*/
private static Logger log = LoggerFactory.getLogger(EventStateCollection.class);
/**
* List of events
*/
private final List<EventState> events = new ArrayList<EventState>();
/**
* Event dispatcher.
*/
private final EventDispatcher dispatcher;
/**
* The session that created these events
*/
private final SessionImpl session;
/**
* The prefix to use for the event paths or <code>null</code> if no prefix
* should be used.
*/
private final Path pathPrefix;
/**
* Timestamp when this collection was created.
*/
private long timestamp = System.currentTimeMillis();
/**
* The user data attached to this event state collection.
*/
private String userData;
/**
* Creates a new empty <code>EventStateCollection</code>.
* <p/>
* Because the item state manager in {@link #createEventStates} may represent
* only a subset of the over all item state hierarchy, this constructor
* also takes a path prefix argument. If non <code>null</code> all events
* created by this collection are prefixed with this path.
*
* @param dispatcher event dispatcher
* @param session the session that created these events.
* @param pathPrefix the path to prefix the event paths or <code>null</code>
* if no prefix should be used.
*/
public EventStateCollection(EventDispatcher dispatcher,
SessionImpl session,
Path pathPrefix) {
this.dispatcher = dispatcher;
this.session = session;
this.pathPrefix = pathPrefix;
if (session != null) {
try {
ObservationManager manager =
session.getWorkspace().getObservationManager();
this.userData = ((ObservationManagerImpl) manager).getUserData();
} catch (RepositoryException e) {
// should never happen because this
// implementation supports observation
this.userData = null;
}
} else {
this.userData = null;
}
}
/**
* Creates {@link EventState} instances from <code>ItemState</code>
* <code>changes</code>.
*
* @param rootNodeId the id of the root node.
* @param changes the changes on <code>ItemState</code>s.
* @param stateMgr an <code>ItemStateManager</code> to provide <code>ItemState</code>
* of items that are not contained in the <code>changes</code> collection.
* @throws ItemStateException if an error occurs while creating events
* states for the item state changes.
*/
public void createEventStates(NodeId rootNodeId, ChangeLog changes, ItemStateManager stateMgr) throws ItemStateException {
// create a hierarchy manager, that is based on the ChangeLog and
// the ItemStateProvider
ChangeLogBasedHierarchyMgr hmgr =
new ChangeLogBasedHierarchyMgr(rootNodeId, stateMgr, changes);
/**
* Important:
* Do NOT change the sequence of events generated unless there's
* a very good reason for it! Some internal SynchronousEventListener
* implementations depend on the order of the events fired.
* LockManagerImpl for example expects that for any given path a
* childNodeRemoved event is fired before a childNodeAdded event.
*/
// 1. modified items
for (Iterator<ItemState> it = changes.modifiedStates(); it.hasNext();) {
ItemState state = it.next();
if (state.isNode()) {
// node changed
// covers the following cases:
// 1) property added
// 2) property removed
// 3) child node added
// 4) child node removed
// 5) node moved/reordered
// 6) node reordered
// 7) shareable node added
// 8) shareable node removed
// cases 1) and 2) are detected with added and deleted states
// on the PropertyState itself.
// cases 3) and 4) are detected with added and deleted states
// on the NodeState itself.
// in case 5) two or three nodes change. two nodes are changed
// when a child node is renamed. three nodes are changed when
// a node is really moved. In any case we are only interested in
// the node that actually got moved.
// in case 6) only one node state changes. the state of the
// parent node.
// in case 7) parent of added shareable node has new child node
// entry.
// in case 8) parent of removed shareable node has removed child
// node entry.
NodeState n = (NodeState) state;
if (n.hasOverlayedState()) {
NodeId oldParentId = n.getOverlayedState().getParentId();
NodeId newParentId = n.getParentId();
if (newParentId != null && !oldParentId.equals(newParentId) &&
!n.isShareable()) {
// node moved
// generate node removed & node added event
NodeState oldParent;
try {
oldParent = (NodeState) changes.get(oldParentId);
} catch (NoSuchItemStateException e) {
// old parent has been deleted, retrieve from
// shared item state manager
oldParent = (NodeState) stateMgr.getItemState(oldParentId);
}
NodeTypeImpl oldParentNodeType = getNodeType(oldParent, session);
Set<Name> mixins = oldParent.getMixinTypeNames();
Path newPath = getPath(n.getNodeId(), hmgr);
Path oldPath = getZombiePath(n.getNodeId(), hmgr);
events.add(EventState.childNodeRemoved(oldParentId,
getParent(oldPath), n.getNodeId(),
oldPath.getNameElement(),
oldParentNodeType.getQName(),
mixins, session));
NodeState newParent = (NodeState) changes.get(newParentId);
NodeTypeImpl newParentNodeType = getNodeType(newParent, session);
mixins = newParent.getMixinTypeNames();
events.add(EventState.childNodeAdded(newParentId,
getParent(newPath), n.getNodeId(),
newPath.getNameElement(),
newParentNodeType.getQName(),
mixins, session));
events.add(EventState.nodeMoved(newParentId,
newPath, n.getNodeId(), oldPath,
newParentNodeType.getQName(), mixins,
session, false));
} else {
// a moved node always has a modified parent node
NodeState parent = null;
try {
// root node does not have a parent UUID
if (state.getParentId() != null) {
parent = (NodeState) changes.get(state.getParentId());
}
} catch (NoSuchItemStateException e) {
// should never happen actually. this would mean
// the parent of this modified node is deleted
String msg = "Parent of node " + state.getId() + " is deleted.";
log.error(msg);
throw new ItemStateException(msg, e);
}
if (parent != null) {
// check if node has been renamed
ChildNodeEntry moved = null;
for (ChildNodeEntry child : parent.getRemovedChildNodeEntries()) {
if (child.getId().equals(n.getNodeId())) {
// found node re-added with different name
moved = child;
}
}
if (moved != null) {
NodeTypeImpl nodeType = getNodeType(parent, session);
Set<Name> mixins = parent.getMixinTypeNames();
Path newPath = getPath(state.getId(), hmgr);
Path parentPath = getParent(newPath);
Path oldPath;
try {
if (moved.getIndex() == 0) {
oldPath = PathFactoryImpl.getInstance().create(parentPath, moved.getName(), false);
} else {
oldPath = PathFactoryImpl.getInstance().create(
parentPath, moved.getName(), moved.getIndex(), false);
}
} catch (RepositoryException e) {
// should never happen actually
String msg = "Malformed path for item: " + state.getId();
log.error(msg);
throw new ItemStateException(msg, e);
}
events.add(EventState.childNodeRemoved(
parent.getNodeId(), parentPath,
n.getNodeId(), oldPath.getNameElement(),
nodeType.getQName(), mixins, session));
events.add(EventState.childNodeAdded(
parent.getNodeId(), parentPath,
n.getNodeId(), newPath.getNameElement(),
nodeType.getQName(), mixins, session));
events.add(EventState.nodeMoved(
parent.getNodeId(), newPath, n.getNodeId(),
oldPath, nodeType.getQName(), mixins,
session, false));
}
}
}
}
// check if child nodes of modified node state have been reordered
List<ChildNodeEntry> reordered = n.getReorderedChildNodeEntries();
NodeTypeImpl nodeType = getNodeType(n, session);
Set<Name> mixins = n.getMixinTypeNames();
if (reordered.size() > 0) {
// create a node removed and a node added event for every
// reorder
for (ChildNodeEntry child : reordered) {
Path.Element addedElem = getPathElement(child);
Path parentPath = getPath(n.getNodeId(), hmgr);
// get removed index
NodeState overlayed = (NodeState) n.getOverlayedState();
ChildNodeEntry entry = overlayed.getChildNodeEntry(child.getId());
if (entry == null) {
throw new ItemStateException("Unable to retrieve old child index for item: " + child.getId());
}
Path.Element removedElem = getPathElement(entry);
events.add(EventState.childNodeRemoved(n.getNodeId(),
parentPath, child.getId(), removedElem,
nodeType.getQName(), mixins, session));
events.add(EventState.childNodeAdded(n.getNodeId(),
parentPath, child.getId(), addedElem,
nodeType.getQName(), mixins, session));
List<ChildNodeEntry> cne = n.getChildNodeEntries();
// index of the child node entry before which this
// child node entry was reordered
int idx = cne.indexOf(child) + 1;
Path.Element beforeElem = null;
if (idx < cne.size()) {
beforeElem = getPathElement(cne.get(idx));
}
events.add(EventState.nodeReordered(n.getNodeId(),
parentPath, child.getId(), addedElem,
removedElem, beforeElem, nodeType.getQName(), mixins,
session, false));
}
}
// create events if n is shareable
createShareableNodeEvents(n, changes, hmgr, stateMgr);
} else {
// property changed
Path path = getPath(state.getId(), hmgr);
NodeState parent = (NodeState) stateMgr.getItemState(state.getParentId());
NodeTypeImpl nodeType = getNodeType(parent, session);
Set<Name> mixins = parent.getMixinTypeNames();
events.add(EventState.propertyChanged(state.getParentId(),
getParent(path), path.getNameElement(),
nodeType.getQName(), mixins, session));
}
}
// 2. removed items
for (Iterator<ItemState> it = changes.deletedStates(); it.hasNext();) {
ItemState state = it.next();
if (state.isNode()) {
// node deleted
NodeState n = (NodeState) state;
NodeState parent = (NodeState) stateMgr.getItemState(n.getParentId());
NodeTypeImpl nodeType = getNodeType(parent, session);
Set<Name> mixins = parent.getMixinTypeNames();
Path path = getZombiePath(state.getId(), hmgr);
events.add(EventState.childNodeRemoved(n.getParentId(),
getParent(path),
n.getNodeId(),
path.getNameElement(),
nodeType.getQName(),
mixins,
session));
// create events if n is shareable
createShareableNodeEvents(n, changes, hmgr, stateMgr);
} else {
// property removed
// only create an event if node still exists
try {
NodeState n = (NodeState) changes.get(state.getParentId());
// node state exists -> only property removed
NodeTypeImpl nodeType = getNodeType(n, session);
Set<Name> mixins = n.getMixinTypeNames();
Path path = getZombiePath(state.getId(), hmgr);
events.add(EventState.propertyRemoved(state.getParentId(),
getParent(path),
path.getNameElement(),
nodeType.getQName(),
mixins,
session));
} catch (NoSuchItemStateException e) {
// node removed as well -> do not create an event
}
}
}
// 3. added items
for (Iterator<ItemState> it = changes.addedStates(); it.hasNext();) {
ItemState state = it.next();
if (state.isNode()) {
// node created
NodeState n = (NodeState) state;
NodeId parentId = n.getParentId();
// the parent of an added item is always modified or new
NodeState parent = (NodeState) changes.get(parentId);
NodeTypeImpl nodeType = getNodeType(parent, session);
Set<Name> mixins = parent.getMixinTypeNames();
Path path = getPath(n.getNodeId(), hmgr);
events.add(EventState.childNodeAdded(parentId,
getParent(path),
n.getNodeId(),
path.getNameElement(),
nodeType.getQName(),
mixins,
session));
// create events if n is shareable
createShareableNodeEvents(n, changes, hmgr, stateMgr);
} else {
// property created / set
NodeState n = (NodeState) changes.get(state.getParentId());
NodeTypeImpl nodeType = getNodeType(n, session);
Set<Name> mixins = n.getMixinTypeNames();
Path path = getPath(state.getId(), hmgr);
events.add(EventState.propertyAdded(state.getParentId(),
getParent(path),
path.getNameElement(),
nodeType.getQName(),
mixins,
session));
}
}
}
/**
* Adds all event states in the given collection to this collection
*
* @param c
*/
public void addAll(Collection<EventState> c) {
events.addAll(c);
}
/**
* Prepares already added events for dispatching.
*/
public void prepare() {
dispatcher.prepareEvents(this);
}
/**
* Prepares deleted items from <code>changes</code>.
*
* @param changes the changes to prepare.
*/
public void prepareDeleted(ChangeLog changes) {
dispatcher.prepareDeleted(this, changes);
}
/**
* Dispatches the events to the {@link javax.jcr.observation.EventListener}s.
*/
public void dispatch() {
dispatcher.dispatchEvents(this);
}
/**
* Returns the path prefix for this event state collection or <code>null</code>
* if no path prefix was set in the constructor of this collection. See
* also {@link EventStateCollection#EventStateCollection}.
*
* @return the path prefix for this event state collection.
*/
public Path getPathPrefix() {
return pathPrefix;
}
/**
* @return the timestamp when this collection was created.
*/
public long getTimestamp() {
return timestamp;
}
/**
* Sets a new timestamp for this collection.
*
* @param timestamp the new timestamp value.
*/
public void setTimestamp(long timestamp) {
this.timestamp = timestamp;
}
/**
* Returns an iterator over {@link EventState} instance.
*
* @return an iterator over {@link EventState} instance.
*/
Iterator<EventState> iterator() {
return events.iterator();
}
/**
* Return the list of events.
* @return list of events
*/
public List<EventState> getEvents() {
return Collections.unmodifiableList(events);
}
/**
* Return the session who is the origin of this events.
* @return event source
*/
public SessionImpl getSession() {
return session;
}
/**
* @return the user data attached to this event state collection.
*/
public String getUserData() {
return userData;
}
/**
* Sets the user data for this event state collection.
*
* @param userData the user data.
*/
public void setUserData(String userData) {
this.userData = userData;
}
//----------------------------< internal >----------------------------------
private void createShareableNodeEvents(NodeState n,
ChangeLog changes,
ChangeLogBasedHierarchyMgr hmgr,
ItemStateManager stateMgr)
throws ItemStateException {
if (n.isShareable()) {
// check if a share was added or removed
for (NodeId parentId : n.getAddedShares()) {
// ignore primary parent id
if (n.getParentId().equals(parentId)) {
continue;
}
NodeState parent = (NodeState) changes.get(parentId);
if (parent == null) {
// happens when mix:shareable is added to an existing node
// usually the parent node state is in the change log
// when a node is added to a shared set -> new child node
// entry on parent node state.
parent = (NodeState) stateMgr.getItemState(parentId);
}
Name ntName = getNodeType(parent, session).getQName();
EventState es = EventState.childNodeAdded(parentId,
getPath(parentId, hmgr),
n.getNodeId(),
getNameElement(n.getNodeId(), parentId, hmgr),
ntName,
parent.getMixinTypeNames(),
session);
es.setShareableNode(true);
events.add(es);
}
for (NodeId parentId : n.getRemovedShares()) {
// if this shareable node is removed, only create events for
// parent ids that are not primary
if (n.getParentId().equals(parentId)) {
continue;
}
NodeState parent = (NodeState) changes.get(parentId);
if (parent == null) {
// happens when mix:shareable is removed from an existing
// node. Usually the parent node state is in the change log
// when a node is removed to a shared set -> removed child
// node entry on parent node state.
parent = (NodeState) stateMgr.getItemState(parentId);
}
Name ntName = getNodeType(parent, session).getQName();
EventState es = EventState.childNodeRemoved(parentId,
getZombiePath(parentId, hmgr),
n.getNodeId(),
getZombieNameElement(n.getNodeId(), parentId, hmgr),
ntName,
parent.getMixinTypeNames(),
session);
es.setShareableNode(true);
events.add(es);
}
}
}
/**
* Resolves the node type name in <code>node</code> into a {@link javax.jcr.nodetype.NodeType}
* object using the {@link javax.jcr.nodetype.NodeTypeManager} of <code>session</code>.
*
* @param node the node.
* @param session the session.
* @return the {@link javax.jcr.nodetype.NodeType} of <code>node</code>.
* @throws ItemStateException if the nodetype cannot be resolved.
*/
private NodeTypeImpl getNodeType(NodeState node, SessionImpl session)
throws ItemStateException {
try {
return session.getNodeTypeManager().getNodeType(node.getNodeTypeName());
} catch (Exception e) {
// also catch eventual runtime exceptions here
// should never happen actually
String msg = "Item " + node.getNodeId() + " has unknown node type: " + node.getNodeTypeName();
log.error(msg);
throw new ItemStateException(msg, e);
}
}
/**
* Returns the path of the parent node of node at <code>path</code>..
*
* @param p the path.
* @return the parent path.
* @throws ItemStateException if <code>p</code> does not have a parent
* path. E.g. <code>p</code> designates root.
*/
private Path getParent(Path p) throws ItemStateException {
try {
return p.getAncestor(1);
} catch (RepositoryException e) {
// should never happen actually
String msg = "Unable to resolve parent for path: " + p;
log.error(msg);
throw new ItemStateException(msg, e);
}
}
/**
* Resolves the path of the Item with id <code>itemId</code>.
*
* @param itemId the id of the item.
* @return the path of the item.
* @throws ItemStateException if the path cannot be resolved.
*/
private Path getPath(ItemId itemId, HierarchyManager hmgr)
throws ItemStateException {
try {
return prefixPath(hmgr.getPath(itemId));
} catch (RepositoryException e) {
// should never happen actually
String msg = "Unable to resolve path for item: " + itemId;
log.error(msg);
throw new ItemStateException(msg, e);
}
}
/**
* Returns the name element for the node with the given <code>nodeId</code>
* and its parent with <code>parentId</code>. This method is only useful
* if <code>nodeId</code> denotes a shareable node.
*
* @param nodeId the node id of a shareable node.
* @param parentId the id of the parent node.
* @param hmgr the hierarchy manager.
* @return the name element for the node.
* @throws ItemStateException if an error occurs while resolving the name.
*/
private Path.Element getNameElement(NodeId nodeId,
NodeId parentId,
HierarchyManager hmgr)
throws ItemStateException {
try {
Name name = hmgr.getName(nodeId, parentId);
PathBuilder builder = new PathBuilder();
builder.addFirst(name);
return builder.getPath().getNameElement();
} catch (RepositoryException e) {
String msg = "Unable to get name for node with id: " + nodeId;
throw new ItemStateException(msg, e);
}
}
/**
* Returns the <i>zombie</i> (i.e. the old) name element for the node with
* the given <code>nodeId</code> and its parent with <code>parentId</code>.
* This method is only useful if <code>nodeId</code> denotes a shareable
* node.
*
* @param nodeId the node id of a shareable node.
* @param parentId the id of the parent node.
* @param hmgr the hierarchy manager.
* @return the name element for the node.
* @throws ItemStateException if an error occurs while resolving the name.
*/
private Path.Element getZombieNameElement(NodeId nodeId,
NodeId parentId,
ChangeLogBasedHierarchyMgr hmgr)
throws ItemStateException {
try {
Name name = hmgr.getZombieName(nodeId, parentId);
PathBuilder builder = new PathBuilder();
builder.addFirst(name);
return builder.getPath().getNameElement();
} catch (RepositoryException e) {
// should never happen actually
String msg = "Unable to resolve zombie name for item: " + nodeId;
log.error(msg);
throw new ItemStateException(msg, e);
}
}
/**
* Resolves the <i>zombie</i> (i.e. the old) path of the Item with id
* <code>itemId</code>.
*
* @param itemId the id of the item.
* @return the path of the item.
* @throws ItemStateException if the path cannot be resolved.
*/
private Path getZombiePath(ItemId itemId, ChangeLogBasedHierarchyMgr hmgr)
throws ItemStateException {
try {
return prefixPath(hmgr.getZombiePath(itemId));
} catch (RepositoryException e) {
// should never happen actually
String msg = "Unable to resolve zombie path for item: " + itemId;
log.error(msg);
throw new ItemStateException(msg, e);
}
}
/**
* Prefixes the Path <code>p</code> with {@link #pathPrefix}.
*
* @param p the Path to prefix.
* @return the prefixed path or <code>p</code> itself if {@link #pathPrefix}
* is <code>null</code>.
* @throws RepositoryException if the path cannot be prefixed.
*/
private Path prefixPath(Path p) throws RepositoryException {
if (pathPrefix == null) {
return p;
}
PathBuilder builder = new PathBuilder(pathPrefix.getElements());
Path.Element[] elements = p.getElements();
for (int i = 0; i < elements.length; i++) {
if (elements[i].denotesRoot()) {
continue;
}
builder.addLast(elements[i]);
}
return builder.getPath();
}
/**
* Returns the path element for the given child node <code>entry</code>.
*
* @param entry a child node entry.
* @return the path element for the given entry.
*/
private Path.Element getPathElement(ChildNodeEntry entry) {
Name name = entry.getName();
int index = (entry.getIndex() != 1) ? entry.getIndex() : 0;
return PathFactoryImpl.getInstance().createElement(name, index);
}
/**
* Get the longest common path of all event state paths.
*
* @return the longest common path
*/
public String getCommonPath() {
String common = null;
try {
for (int i = 0; i < events.size(); i++) {
EventState state = events.get(i);
String s = session.getJCRPath(state.getParentPath());
if (common == null) {
common = s;
} else if (!common.equals(s)) {
while (!s.startsWith(common)) {
int idx = s.lastIndexOf('/');
if (idx < 0) {
break;
}
common = s.substring(0, idx);
}
}
}
} catch (NamespaceException e) {
// ignore
}
return common;
}
}