/* * 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.SessionImpl; import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl; import org.apache.jackrabbit.core.id.ItemId; import org.apache.jackrabbit.core.id.PropertyId; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.value.InternalValue; import org.apache.jackrabbit.core.state.ItemStateException; import org.apache.jackrabbit.spi.Path; import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.spi.commons.conversion.CachingPathResolver; import org.apache.jackrabbit.spi.commons.conversion.IllegalNameException; import org.apache.jackrabbit.spi.commons.conversion.NameResolver; import org.apache.jackrabbit.spi.commons.conversion.ParsingPathResolver; import org.apache.jackrabbit.spi.commons.conversion.PathResolver; import org.apache.jackrabbit.spi.commons.name.PathFactoryImpl; import javax.jcr.observation.Event; import org.slf4j.LoggerFactory; import org.slf4j.Logger; import javax.jcr.NamespaceException; import javax.jcr.Session; import javax.jcr.RepositoryException; import javax.jcr.nodetype.NoSuchNodeTypeException; import javax.jcr.nodetype.NodeType; import java.util.List; import java.util.Set; import java.util.HashSet; import java.util.Iterator; import java.util.Collections; import java.util.Map; import java.util.HashMap; /** * The <code>EventState</code> class encapsulates the session * independent state of an {@link javax.jcr.observation.Event}. */ public class EventState { /** * The logger instance for this class. */ private static final Logger log = LoggerFactory.getLogger(EventState.class); /** * The caching path resolver. */ private static CachingPathResolver cachingPathResolver; /** * The key <code>srcAbsPath</code> in the info map. */ static final String SRC_ABS_PATH = "srcAbsPath"; /** * The key <code>destAbsPath</code> in the info map. */ static final String DEST_ABS_PATH = "destAbsPath"; /** * The key <code>srcChildRelPath</code> in the info map. */ static final String SRC_CHILD_REL_PATH = "srcChildRelPath"; /** * The key <code>destChildRelPath</code> in the info map. */ static final String DEST_CHILD_REL_PATH = "destChildRelPath"; /** * The {@link javax.jcr.observation.Event} of this event. */ private final int type; /** * The Id of the parent node associated with this event. */ private final NodeId parentId; /** * The path of the parent node associated with this event. */ private final Path parentPath; /** * The UUID of a child node, in case this EventState is of type * {@link javax.jcr.observation.Event#NODE_ADDED} or * {@link javax.jcr.observation.Event#NODE_REMOVED}. */ private final NodeId childId; /** * The relative path of the child item associated with this event. * This is basically the name of the item with an optional index. */ private final Path childRelPath; /** * The node type name of the parent node. */ private final Name nodeType; /** * Set of mixin QNames assigned to the parent node. */ private final Set<Name> mixins; /** * Set of node types. This Set consists of the primary node type and all * mixin types assigned to the associated parent node of this event state. * </p> * This <code>Set</code> is initialized when * {@link #getNodeTypes(NodeTypeManagerImpl)} is called for the first time. */ private Set<NodeType> allTypes; /** * The session that caused this event. */ private final Session session; /** * Cached String representation of this <code>EventState</code>. */ private String stringValue; /** * Cached hashCode value for this <code>Event</code>. */ private int hashCode; /** * Flag indicating whether this is an external event, e.g. originating from * another node in a clustered environment. */ private final boolean external; /** * The info Map associated with this event. */ private Map<String, InternalValue> info = Collections.emptyMap(); /** * If set to <code>true</code>, indicates that the child node of a node * added or removed event is a shareable node. */ private boolean shareableNode; /** * Creates a new <code>EventState</code> instance. * * @param type the type of this event. * @param parentId the id of the parent node associated with this event. * @param parentPath the path of the parent node associated with this * event. * @param childId the id of the child node associated with this event. * If the event type is one of: <code>PROPERTY_ADDED</code>, * <code>PROPERTY_CHANGED</code> or <code>PROPERTY_REMOVED</code> * this parameter must be <code>null</code>. * @param childPath the relative path of the child item associated with * this event. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the {@link javax.jcr.Session} that caused this event. */ private EventState(int type, NodeId parentId, Path parentPath, NodeId childId, Path childPath, Name nodeType, Set<Name> mixins, Session session, boolean external) { int mask = (Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED); if ((type & mask) > 0) { if (childId != null) { throw new IllegalArgumentException("childId only allowed for Node events."); } } else { if (childId == null && type != Event.PERSIST) { throw new IllegalArgumentException("childId must not be null for Node events."); } } this.type = type; this.parentId = parentId; this.parentPath = parentPath; this.childId = childId; this.childRelPath = childPath; this.nodeType = nodeType; this.mixins = mixins; this.session = session; this.external = external; } //-----------------< factory methods >-------------------------------------- /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#NODE_ADDED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childId the id of the child node associated with this event. * @param childPath the relative path of the child node that was added. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that added the node. * @return an <code>EventState</code> instance. */ public static EventState childNodeAdded(NodeId parentId, Path parentPath, NodeId childId, Path childPath, Name nodeType, Set<Name> mixins, Session session) { return childNodeAdded(parentId, parentPath, childId, childPath, nodeType, mixins, session, false); } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#NODE_ADDED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childId the id of the child node associated with this event. * @param childPath the relative path of the child node that was added. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that added the node. * @param external flag indicating whether this is an external event * @return an <code>EventState</code> instance. */ public static EventState childNodeAdded(NodeId parentId, Path parentPath, NodeId childId, Path childPath, Name nodeType, Set<Name> mixins, Session session, boolean external) { return new EventState(Event.NODE_ADDED, parentId, parentPath, childId, childPath, nodeType, mixins, session, external); } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#NODE_REMOVED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childId the id of the child node associated with this event. * @param childPath the relative path of the child node that was removed. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that removed the node. * @return an <code>EventState</code> instance. */ public static EventState childNodeRemoved(NodeId parentId, Path parentPath, NodeId childId, Path childPath, Name nodeType, Set<Name> mixins, Session session) { return childNodeRemoved(parentId, parentPath, childId, childPath, nodeType, mixins, session, false); } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#NODE_REMOVED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childId the id of the child node associated with this event. * @param childPath the relative path of the child node that was removed. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that removed the node. * @param external flag indicating whether this is an external event * @return an <code>EventState</code> instance. */ public static EventState childNodeRemoved(NodeId parentId, Path parentPath, NodeId childId, Path childPath, Name nodeType, Set<Name> mixins, Session session, boolean external) { return new EventState(Event.NODE_REMOVED, parentId, parentPath, childId, childPath, nodeType, mixins, session, external); } /** * Creates a new {@link javax.jcr.observation.Event} of type * <code>NODE_MOVED</code>. The parent node associated with this event type * is the parent node of the destination of the move! * This method creates an event state without an info map. A caller of this * method must ensure that it is properly set afterwards. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childId the id of the child node associated with this event. * @param childPath the relative path of the child node that was moved. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that moved the node. * @param external flag indicating whether this is an external event * @return an <code>EventState</code> instance. */ public static EventState nodeMoved(NodeId parentId, Path parentPath, NodeId childId, Path childPath, Name nodeType, Set<Name> mixins, Session session, boolean external) { return new EventState(Event.NODE_MOVED, parentId, parentPath, childId, childPath, nodeType, mixins, session, external); } /** * Creates a new {@link javax.jcr.observation.Event} of type * <code>NODE_MOVED</code>. The parent node associated with this event type * is the parent node of the destination of the move! * * @param parentId the id of the parent node associated with this * <code>EventState</code>. * @param destPath the path of the destination of the move. * @param childId the id of the child node associated with this event. * @param srcPath the path of the source of the move. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that removed the node. * @param external flag indicating whether this is an external event * @return an <code>EventState</code> instance. * @throws ItemStateException if <code>destPath</code> does not have a * parent. */ public static EventState nodeMovedWithInfo( NodeId parentId, Path destPath, NodeId childId, Path srcPath, Name nodeType, Set<Name> mixins, Session session, boolean external) throws ItemStateException { try { EventState es = nodeMoved(parentId, destPath.getAncestor(1), childId, destPath, nodeType, mixins, session, external); Map<String, InternalValue> info = new HashMap<String, InternalValue>(); info.put(SRC_ABS_PATH, InternalValue.create(srcPath)); info.put(DEST_ABS_PATH, InternalValue.create(destPath)); es.setInfo(info); return es; } catch (RepositoryException e) { // should never happen actually String msg = "Unable to resolve parent for path: " + destPath; log.error(msg); throw new ItemStateException(msg, e); } } /** * Creates a new {@link javax.jcr.observation.Event} of type * <code>NODE_MOVED</code>. The parent node associated with this event type * is the parent node of the destination of the reorder! * * @param parentId the id of the parent node associated with this * <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childId the id of the child node associated with this * event. * @param destChildPath the name element of the node before it was reordered. * @param srcChildPath the name element of the reordered node before the * reorder operation. * @param beforeChildPath the name element of the node before which the * reordered node is placed. (may be <code>null</code> * if reordered to the end. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that removed the node. * @param external flag indicating whether this is an external event * @return an <code>EventState</code> instance. */ public static EventState nodeReordered(NodeId parentId, Path parentPath, NodeId childId, Path destChildPath, Path srcChildPath, Path beforeChildPath, Name nodeType, Set<Name> mixins, Session session, boolean external) { EventState es = nodeMoved( parentId, parentPath, childId, destChildPath, nodeType, mixins, session, external); Map<String, InternalValue> info = new HashMap<String, InternalValue>(); info.put(SRC_CHILD_REL_PATH, createValue(srcChildPath)); InternalValue value = null; if (beforeChildPath != null) { value = createValue(beforeChildPath); } info.put(DEST_CHILD_REL_PATH, value); es.setInfo(info); return es; } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#PROPERTY_ADDED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childPath the relative path of the property that was added. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that added the property. * @return an <code>EventState</code> instance. */ public static EventState propertyAdded(NodeId parentId, Path parentPath, Path childPath, Name nodeType, Set<Name> mixins, Session session) { return propertyAdded(parentId, parentPath, childPath, nodeType, mixins, session, false); } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#PROPERTY_ADDED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childPath the relative path of the property that was added. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that added the property. * @param external flag indicating whether this is an external event * @return an <code>EventState</code> instance. */ public static EventState propertyAdded(NodeId parentId, Path parentPath, Path childPath, Name nodeType, Set<Name> mixins, Session session, boolean external) { return new EventState(Event.PROPERTY_ADDED, parentId, parentPath, null, childPath, nodeType, mixins, session, external); } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#PROPERTY_REMOVED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childPath the relative path of the property that was removed. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that removed the property. * @return an <code>EventState</code> instance. */ public static EventState propertyRemoved(NodeId parentId, Path parentPath, Path childPath, Name nodeType, Set<Name> mixins, Session session) { return propertyRemoved(parentId, parentPath, childPath, nodeType, mixins, session, false); } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#PROPERTY_REMOVED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childPath the relative path of the property that was removed. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that removed the property. * @param external flag indicating whether this is an external event * @return an <code>EventState</code> instance. */ public static EventState propertyRemoved(NodeId parentId, Path parentPath, Path childPath, Name nodeType, Set<Name> mixins, Session session, boolean external) { return new EventState(Event.PROPERTY_REMOVED, parentId, parentPath, null, childPath, nodeType, mixins, session, external); } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#PROPERTY_CHANGED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childPath the relative path of the property that changed. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that changed the property. * @return an <code>EventState</code> instance. */ public static EventState propertyChanged(NodeId parentId, Path parentPath, Path childPath, Name nodeType, Set<Name> mixins, Session session) { return propertyChanged(parentId, parentPath, childPath, nodeType, mixins, session, false); } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#PROPERTY_CHANGED}. * * @param parentId the id of the parent node associated with * this <code>EventState</code>. * @param parentPath the path of the parent node associated with * this <code>EventState</code>. * @param childPath the relative path of the property that changed. * @param nodeType the node type of the parent node. * @param mixins mixins assigned to the parent node. * @param session the session that changed the property. * @param external flag indicating whether this is an external event * @return an <code>EventState</code> instance. */ public static EventState propertyChanged(NodeId parentId, Path parentPath, Path childPath, Name nodeType, Set<Name> mixins, Session session, boolean external) { return new EventState(Event.PROPERTY_CHANGED, parentId, parentPath, null, childPath, nodeType, mixins, session, external); } /** * Creates a new {@link javax.jcr.observation.Event} of type * {@link javax.jcr.observation.Event#PERSIST}. * * @param session the session that changed the property. * @param external flag indicating whether this is an external event * @return an <code>EventState</code> instance. */ public static EventState persist(Session session, boolean external) { return new EventState(Event.PERSIST, null, null, null, null, null, null, session, external); } /** * {@inheritDoc} */ public int getType() { return type; } /** * Returns the uuid of the parent node. * * @return the uuid of the parent node. */ public NodeId getParentId() { return parentId; } /** * Returns the path of the parent node. * * @return the path of the parent node. */ public Path getParentPath() { return parentPath; } /** * Returns the Id of a child node operation. * If this <code>EventState</code> was generated for a property * operation this method returns <code>null</code>. * * @return the id of a child node operation. */ public NodeId getChildId() { return childId; } /** * Returns the relative {@link Path} of the child * {@link javax.jcr.Item} associated with this event. * * @return the <code>Path</code> associated with this event. */ public Path getChildRelPath() { return childRelPath; } /** * Returns the node type of the parent node associated with this event. * * @return the node type of the parent associated with this event. */ public Name getNodeType() { return nodeType; } /** * Returns a set of <code>Name</code>s which are the names of the mixins * assigned to the parent node associated with this event. * * @return the mixin names as <code>Name</code>s. */ public Set<Name> getMixinNames() { return mixins; } /** * Returns the <code>Set</code> of {@link javax.jcr.nodetype.NodeType}s * assigned to the parent node associated with this event. This * <code>Set</code> includes the primary type as well as all the mixin types * assigned to the parent node. * * @return <code>Set</code> of {@link javax.jcr.nodetype.NodeType}s. */ public Set<NodeType> getNodeTypes(NodeTypeManagerImpl ntMgr) { if (allTypes == null) { Set<NodeType> tmp = new HashSet<NodeType>(); try { tmp.add(ntMgr.getNodeType(nodeType)); } catch (NoSuchNodeTypeException e) { log.warn("Unknown node type: " + nodeType); } Iterator<Name> it = mixins.iterator(); while (it.hasNext()) { Name mixinName = it.next(); try { tmp.add(ntMgr.getNodeType(mixinName)); } catch (NoSuchNodeTypeException e) { log.warn("Unknown node type: " + mixinName); } } allTypes = Collections.unmodifiableSet(tmp); } return allTypes; } /** * {@inheritDoc} */ public String getUserId() { return session.getUserID(); } /** * Returns the <code>Session</code> that caused / created this * <code>EventState</code>. * * @return the <code>Session</code> that caused / created this * <code>EventState</code>. */ Session getSession() { return session; } /** * Returns the id of the associated item of this <code>EventState</code>. * * @return the <code>ItemId</code> or <code>null</code> for {@link Event#PERSIST} events */ ItemId getTargetId() { if (type == Event.PERSIST) { return null; } else if (childId == null) { // property event return new PropertyId(parentId, childRelPath.getName()); } else { // node event return childId; } } /** * Return a flag indicating whether this is an externally generated event. * * @return <code>true</code> if this is an external event; * <code>false</code> otherwise */ boolean isExternal() { return external; } /** * @return an unmodifiable info Map. */ public Map<String, InternalValue> getInfo() { return info; } /** * Sets a new info map for this event. * * @param info the new info map. */ public void setInfo(Map<String, InternalValue> info) { this.info = Collections.unmodifiableMap(new HashMap<String, InternalValue>(info)); } /** * Returns a flag indicating whether the child node of this event is a * shareable node. Only applies to node added/removed events. * * @return <code>true</code> for a shareable child node, <code>false</code> * otherwise. */ boolean isShareableNode() { return shareableNode; } /** * Sets a new value for the {@link #shareableNode} flag. * * @param shareableNode whether the child node is shareable. * @see #isShareableNode() */ void setShareableNode(boolean shareableNode) { this.shareableNode = shareableNode; } /** * Returns a String representation of this <code>EventState</code>. * * @return a String representation of this <code>EventState</code>. */ public String toString() { if (stringValue == null) { StringBuilder sb = new StringBuilder(); sb.append("EventState: ").append(valueOf(type)); sb.append(", Parent: ").append(parentId); sb.append(", Child: ").append(childRelPath); sb.append(", UserId: ").append(session.getUserID()); sb.append(", Info: ").append(info); stringValue = sb.toString(); } return stringValue; } /** * Returns a hashCode for this <code>EventState</code>. * * @return a hashCode for this <code>EventState</code>. */ public int hashCode() { int h = hashCode; if (h == 0) { h = 37; h = 37 * h + type; h = 37 * h + (parentId != null ? parentId.hashCode() : 0); h = 37 * h + (childRelPath != null ? childRelPath.hashCode() : 0); h = 37 * h + session.hashCode(); h = 37 * h + info.hashCode(); hashCode = h; } return hashCode; } /** * Returns <code>true</code> if this <code>EventState</code> is equal to * another object. * * @param obj the reference object with which to compare. * @return <code>true</code> if object <code>obj</code> is equal to this * <code>EventState</code>; <code>false</code> otherwise. */ public boolean equals(Object obj) { if (obj == this) { return true; } if (obj instanceof EventState) { EventState other = (EventState) obj; return this.type == other.type && this.parentId.equals(other.parentId) && this.childRelPath.equals(other.childRelPath) && this.session.equals(other.session) && this.info.equals(other.info); } return false; } /** * Returns a String representation of <code>eventType</code>. * * @param eventType an event type defined by {@link Event}. * @return a String representation of <code>eventType</code>. */ public static String valueOf(int eventType) { if (eventType == Event.NODE_ADDED) { return "NodeAdded"; } else if (eventType == Event.NODE_MOVED) { return "NodeMoved"; } else if (eventType == Event.NODE_REMOVED) { return "NodeRemoved"; } else if (eventType == Event.PROPERTY_ADDED) { return "PropertyAdded"; } else if (eventType == Event.PROPERTY_CHANGED) { return "PropertyChanged"; } else if (eventType == Event.PROPERTY_REMOVED) { return "PropertyRemoved"; } else if (eventType == Event.PERSIST) { return "Persist"; } else { return "UnknownEventType"; } } /** * Creates an internal path value from the given path. * * @param path the path * @return an internal value wrapping the path */ private static InternalValue createValue(Path path) { return InternalValue.create(path); } /** * Get the longest common path of all event state paths. * * @param events The list of EventState * @param session The associated session; it can be null * @return the longest common path */ public static String getCommonPath(List<EventState> events, SessionImpl session) { String common = null; try { for (int i = 0; i < events.size(); i++) { EventState state = events.get(i); Path parentPath = state.getParentPath(); String s; if (session == null) { s = getJCRPath(parentPath); } else { s = session.getJCRPath(parentPath); } if (common == null) { common = s; } else if (!common.equals(s)) { // Assign the shorter path to common. if (s.length() < common.length()) { String temp = common; common = s; s = temp; } // Find the real common. while (!s.startsWith(common)) { int idx = s.lastIndexOf('/'); if (idx < 0) { break; } common = s.substring(0, idx + 1); } } } } catch (NamespaceException e) { log.debug("Problem in retrieving JCR path", e); } return common; } private static String getJCRPath(Path path) { setupCachingPathResolver(); String jcrPath; try { jcrPath = cachingPathResolver.getJCRPath(path); } catch (NamespaceException e) { jcrPath = ""; log.debug("Problem in retrieving JCR path", e); } return jcrPath; } private static void setupCachingPathResolver() { if (cachingPathResolver != null) { return; } PathResolver pathResolver = new ParsingPathResolver(PathFactoryImpl.getInstance(), new NameResolver() { public Name getQName(String name) throws IllegalNameException, NamespaceException { return null; } public String getJCRName(Name name) throws NamespaceException { return name.getLocalName(); } }); cachingPathResolver = new CachingPathResolver(pathResolver); } }