/*
* 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.state;
import org.apache.commons.collections.iterators.IteratorChain;
import org.apache.jackrabbit.core.CachingHierarchyManager;
import org.apache.jackrabbit.core.HierarchyManager;
import org.apache.jackrabbit.core.ItemId;
import org.apache.jackrabbit.core.NodeId;
import org.apache.jackrabbit.core.PropertyId;
import org.apache.jackrabbit.core.ZombieHierarchyManager;
import org.apache.jackrabbit.core.util.Dumpable;
import org.apache.jackrabbit.name.PathResolver;
import org.apache.jackrabbit.name.QName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.jcr.InvalidItemStateException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;
/**
* Item state manager that handles both transient and persistent items.
*/
public class SessionItemStateManager
implements UpdatableItemStateManager, Dumpable, NodeStateListener {
private static Logger log = LoggerFactory.getLogger(SessionItemStateManager.class);
/**
* State manager that allows updates
*/
private final UpdatableItemStateManager stateMgr;
/**
* Hierarchy manager
*/
private CachingHierarchyManager hierMgr;
/**
* map of those states that have been removed transiently
*/
private final ItemStateStore atticStore;
/**
* map of new or modified transient states
*/
private final ItemStateStore transientStore;
/**
* ItemStateManager view of the states in the attic; lazily instantiated
* in {@link #getAttic()}
*/
private AtticItemStateManager attic;
/**
* State change dispatcher.
*/
private final transient StateChangeDispatcher dispatcher = new StateChangeDispatcher();
/**
* Creates a new <code>SessionItemStateManager</code> instance.
*
* @param rootNodeId the root node id
* @param stateMgr the local item state manager
* @param resolver path resolver for outputting user-friendly paths
*/
public SessionItemStateManager(NodeId rootNodeId,
LocalItemStateManager stateMgr,
PathResolver resolver) {
transientStore = new ItemStateMap();
atticStore = new ItemStateMap();
this.stateMgr = stateMgr;
stateMgr.addListener(this);
// create hierarchy manager that uses both transient and persistent state
hierMgr = new CachingHierarchyManager(rootNodeId, this, resolver);
addListener(hierMgr);
}
/**
* Returns the hierarchy manager
*
* @return the hierarchy manager
*/
public HierarchyManager getHierarchyMgr() {
return hierMgr;
}
/**
* Returns an attic-aware hierarchy manager, i.e. an hierarchy manager that
* is also able to build/resolve paths of those items that have been moved
* or removed (i.e. moved to the attic).
*
* @return an attic-aware hierarchy manager
*/
public HierarchyManager getAtticAwareHierarchyMgr() {
return new ZombieHierarchyManager(hierMgr, this, getAttic());
}
//-------------------------------------------------------------< Dumpable >
/**
* {@inheritDoc}
*/
public void dump(PrintStream ps) {
ps.println("SessionItemStateManager (" + this + ")");
ps.println();
ps.print("[transient] ");
if (transientStore instanceof Dumpable) {
((Dumpable) transientStore).dump(ps);
} else {
ps.println(transientStore.toString());
}
ps.println();
ps.print("[attic] ");
if (atticStore instanceof Dumpable) {
((Dumpable) atticStore).dump(ps);
} else {
ps.println(atticStore.toString());
}
ps.println();
}
//-----------------------------------------------------< ItemStateManager >
/**
* {@inheritDoc}
*/
public ItemState getItemState(ItemId id)
throws NoSuchItemStateException, ItemStateException {
// first check if the specified item has been transiently removed
if (atticStore.contains(id)) {
/**
* check if there's new transient state for the specified item
* (e.g. if a property with name 'x' has been removed and a new
* property with same name has been created);
* this will throw a NoSuchItemStateException if there's no new
* transient state
*/
return getTransientItemState(id);
}
// check if there's transient state for the specified item
if (transientStore.contains(id)) {
return getTransientItemState(id);
}
// check if there's persistent state for the specified item
if (stateMgr.hasItemState(id)) {
return stateMgr.getItemState(id);
}
throw new NoSuchItemStateException(id.toString());
}
/**
* {@inheritDoc}
*/
public boolean hasItemState(ItemId id) {
// first check if the specified item has been transiently removed
if (atticStore.contains(id)) {
/**
* check if there's new transient state for the specified item
* (e.g. if a property with name 'x' has been removed and a new
* property with same name has been created);
*/
return transientStore.contains(id);
}
// check if there's transient state for the specified item
if (transientStore.contains(id)) {
return true;
}
// check if there's persistent state for the specified item
return stateMgr.hasItemState(id);
}
/**
* {@inheritDoc}
*/
public NodeReferences getNodeReferences(NodeReferencesId id)
throws NoSuchItemStateException, ItemStateException {
return stateMgr.getNodeReferences(id);
}
/**
* {@inheritDoc}
*/
public boolean hasNodeReferences(NodeReferencesId id) {
return stateMgr.hasNodeReferences(id);
}
//--------------------------------------------< UpdatableItemStateManager >
/**
* {@inheritDoc}
*/
public void edit() throws IllegalStateException {
stateMgr.edit();
}
/**
* {@inheritDoc}
*/
public boolean inEditMode() {
return stateMgr.inEditMode();
}
/**
* {@inheritDoc}
*/
public NodeState createNew(NodeId id, QName nodeTypeName,
NodeId parentId)
throws IllegalStateException {
return stateMgr.createNew(id, nodeTypeName, parentId);
}
/**
* Customized variant of {@link #createNew(NodeId, QName, NodeId)} that
* connects the newly created persistent state with the transient state.
*/
public NodeState createNew(NodeState transientState)
throws IllegalStateException {
NodeState persistentState = createNew(transientState.getNodeId(),
transientState.getNodeTypeName(),
transientState.getParentId());
transientState.connect(persistentState);
return persistentState;
}
/**
* {@inheritDoc}
*/
public PropertyState createNew(QName propName, NodeId parentId)
throws IllegalStateException {
return stateMgr.createNew(propName, parentId);
}
/**
* Customized variant of {@link #createNew(QName, NodeId)} that
* connects the newly created persistent state with the transient state.
*/
public PropertyState createNew(PropertyState transientState)
throws IllegalStateException {
PropertyState persistentState = createNew(transientState.getName(),
transientState.getParentId());
transientState.connect(persistentState);
return persistentState;
}
/**
* {@inheritDoc}
*/
public void store(ItemState state) throws IllegalStateException {
stateMgr.store(state);
}
/**
* {@inheritDoc}
*/
public void destroy(ItemState state) throws IllegalStateException {
stateMgr.destroy(state);
}
/**
* {@inheritDoc}
*/
public void cancel() throws IllegalStateException {
stateMgr.cancel();
}
/**
* {@inheritDoc}
*/
public void update()
throws ReferentialIntegrityException, StaleItemStateException,
ItemStateException, IllegalStateException {
stateMgr.update();
}
/**
* {@inheritDoc}
*/
public void dispose() {
// discard all transient changes
disposeAllTransientItemStates();
// dispose our (i.e. 'local') state manager
stateMgr.dispose();
}
//< more methods for listing and retrieving transient ItemState instances >
/**
* @param id
* @return
* @throws NoSuchItemStateException
* @throws ItemStateException
*/
public ItemState getTransientItemState(ItemId id)
throws NoSuchItemStateException, ItemStateException {
ItemState state = transientStore.get(id);
if (state != null) {
return state;
} else {
throw new NoSuchItemStateException(id.toString());
}
}
/**
*
* @param id
* @return
*/
public boolean hasTransientItemState(ItemId id) {
return transientStore.contains(id);
}
/**
*
* @param id
* @return
*/
public boolean hasTransientItemStateInAttic(ItemId id) {
return atticStore.contains(id);
}
/**
* @return <code>true</code> if this manager has any transient state;
* <code>false</code> otherwise.
*/
public boolean hasAnyTransientItemStates() {
return !transientStore.isEmpty();
}
/**
* Returns an iterator over those transient item state instances that are
* direct or indirect descendents of the item state with the given
* <code>parentId</code>. The transient item state instance with the given
* <code>parentId</code> itself (if there is such) will not be included.
* <p/>
* The instances are returned in depth-first tree traversal order.
*
* @param parentId the id of the common parent of the transient item state
* instances to be returned.
* @return an iterator over descendant transient item state instances
* @throws InvalidItemStateException if any descendant item state has been
* deleted externally
* @throws RepositoryException if another error occurs
*/
public Iterator getDescendantTransientItemStates(NodeId parentId)
throws InvalidItemStateException, RepositoryException {
if (transientStore.isEmpty()) {
return Collections.EMPTY_LIST.iterator();
}
// build ordered collection of descendant transient states
// sorted by decreasing relative depth
// use an array of lists to group the descendants by relative depth;
// the depth is used as array index
List[] la = new List[10];
try {
Iterator iter = transientStore.values().iterator();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
// determine relative depth: > 0 means it's a descendant
int depth;
try {
depth = hierMgr.getRelativeDepth(parentId, state.getId());
} catch (ItemNotFoundException infe) {
/**
* one of the parents of the specified item has been
* removed externally; as we don't know its path,
* we can't determine if it is a descendant;
* InvalidItemStateException should only be thrown if
* a descendant is affected;
* => throw InvalidItemStateException for now
* todo FIXME
*/
// unable to determine relative depth, assume that the item
// (or any of its ancestors) has been removed externally
String msg = state.getId()
+ ": the item seems to have been removed externally.";
log.debug(msg);
throw new InvalidItemStateException(msg);
}
if (depth < 1) {
// not a descendant
continue;
}
// ensure capacity
if (depth > la.length) {
List[] old = la;
la = new List[depth + 10];
System.arraycopy(old, 0, la, 0, old.length);
}
List list = la[depth - 1];
if (list == null) {
list = new ArrayList();
la[depth - 1] = list;
}
list.add(state);
}
} catch (RepositoryException re) {
log.warn("inconsistent hierarchy state", re);
}
// create an iterator over the collected descendants
// in decreasing depth order
IteratorChain resultIter = new IteratorChain();
for (int i = la.length - 1; i >= 0; i--) {
List list = la[i];
if (list != null) {
resultIter.addIterator(list.iterator());
}
}
/**
* if the resulting iterator chain is empty return
* EMPTY_LIST.iterator() instead because older versions
* of IteratorChain (pre Commons Collections 3.1)
* would throw UnsupportedOperationException in this
* situation
*/
if (resultIter.getIterators().isEmpty()) {
return Collections.EMPTY_LIST.iterator();
}
return resultIter;
}
/**
* Same as <code>{@link #getDescendantTransientItemStates(NodeId)}</code>
* except that item state instances in the attic are returned.
*
* @param parentId the id of the common parent of the transient item state
* instances to be returned.
* @return an iterator over descendant transient item state instances in the attic
*/
public Iterator getDescendantTransientItemStatesInAttic(NodeId parentId) {
if (atticStore.isEmpty()) {
return Collections.EMPTY_LIST.iterator();
}
// build ordered collection of descendant transient states in attic
// sorted by decreasing relative depth
// use a special attic-aware hierarchy manager
ZombieHierarchyManager zombieHierMgr =
new ZombieHierarchyManager(hierMgr, this, getAttic());
// use an array of lists to group the descendants by relative depth;
// the depth is used as array index
List[] la = new List[10];
try {
Iterator iter = atticStore.values().iterator();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
// determine relative depth: > 0 means it's a descendant
int depth = zombieHierMgr.getRelativeDepth(parentId, state.getId());
if (depth < 1) {
// not a descendant
continue;
}
// ensure capacity
if (depth > la.length) {
List[] old = la;
la = new List[depth + 10];
System.arraycopy(old, 0, la, 0, old.length);
}
List list = la[depth - 1];
if (list == null) {
list = new ArrayList();
la[depth - 1] = list;
}
list.add(state);
}
} catch (RepositoryException re) {
log.warn("inconsistent hierarchy state", re);
}
// create an iterator over the collected descendants
// in decreasing depth order
IteratorChain resultIter = new IteratorChain();
for (int i = la.length - 1; i >= 0; i--) {
List list = la[i];
if (list != null) {
resultIter.addIterator(list.iterator());
}
}
/**
* if the resulting iterator chain is empty return
* EMPTY_LIST.iterator() instead because older versions
* of IteratorChain (pre Commons Collections 3.1)
* would throw UnsupportedOperationException in this
* situation
*/
if (resultIter.getIterators().isEmpty()) {
return Collections.EMPTY_LIST.iterator();
}
return resultIter;
}
/**
* Return a flag indicating whether the specified item is in the transient
* item state manager's attic space.
*
* @param id item id
* @return <code>true</code> if the item state is in the attic space;
* <code>false</code> otherwise
*/
public boolean isItemStateInAttic(ItemId id) {
return atticStore.contains(id);
}
//------< methods for creating & discarding transient ItemState instances >
/**
* @param id
* @param nodeTypeName
* @param parentId
* @param initialStatus
* @return
* @throws ItemStateException
*/
public NodeState createTransientNodeState(NodeId id, QName nodeTypeName, NodeId parentId, int initialStatus)
throws ItemStateException {
// check map; synchronized to ensure an entry is not created twice.
synchronized (transientStore) {
if (transientStore.contains(id)) {
String msg = "there's already a node state instance with id " + id;
log.debug(msg);
throw new ItemStateException(msg);
}
NodeState state = new NodeState(id, nodeTypeName, parentId,
initialStatus, true);
// put transient state in the map
transientStore.put(state);
state.setContainer(this);
return state;
}
}
/**
* @param overlayedState
* @param initialStatus
* @return
* @throws ItemStateException
*/
public NodeState createTransientNodeState(NodeState overlayedState, int initialStatus)
throws ItemStateException {
ItemId id = overlayedState.getNodeId();
// check map; synchronized to ensure an entry is not created twice.
synchronized (transientStore) {
if (transientStore.contains(id)) {
String msg = "there's already a node state instance with id " + id;
log.debug(msg);
throw new ItemStateException(msg);
}
NodeState state = new NodeState(overlayedState, initialStatus, true);
// put transient state in the map
transientStore.put(state);
state.setContainer(this);
return state;
}
}
/**
* @param parentId
* @param propName
* @param initialStatus
* @return
* @throws ItemStateException
*/
public PropertyState createTransientPropertyState(NodeId parentId, QName propName, int initialStatus)
throws ItemStateException {
PropertyId id = new PropertyId(parentId, propName);
// check map; synchronized to ensure an entry is not created twice.
synchronized (transientStore) {
if (transientStore.contains(id)) {
String msg = "there's already a property state instance with id " + id;
log.debug(msg);
throw new ItemStateException(msg);
}
PropertyState state = new PropertyState(id, initialStatus, true);
// put transient state in the map
transientStore.put(state);
state.setContainer(this);
return state;
}
}
/**
* @param overlayedState
* @param initialStatus
* @return
* @throws ItemStateException
*/
public PropertyState createTransientPropertyState(PropertyState overlayedState, int initialStatus)
throws ItemStateException {
PropertyId id = overlayedState.getPropertyId();
// check map; synchronized to ensure an entry is not created twice.
synchronized (transientStore) {
if (transientStore.contains(id)) {
String msg = "there's already a property state instance with id " + id;
log.debug(msg);
throw new ItemStateException(msg);
}
PropertyState state = new PropertyState(overlayedState, initialStatus, true);
// put transient state in the map
transientStore.put(state);
state.setContainer(this);
return state;
}
}
/**
* Disconnect a transient item state from its underlying persistent state.
* Notifies the <code>HierarchyManager</code> about the changed identity.
*
* @param state the transient <code>ItemState</code> instance that should
* be disconnected
*/
public void disconnectTransientItemState(ItemState state) {
state.disconnect();
}
/**
* Disposes the specified transient item state instance, i.e. discards it
* and clears it from cache.
*
* @param state the transient <code>ItemState</code> instance that should
* be disposed
* @see ItemState#discard()
*/
public void disposeTransientItemState(ItemState state) {
// discard item state, this will invalidate the wrapping Item
// instance of the transient state
state.discard();
// remove from map
transientStore.remove(state.getId());
// give the instance a chance to prepare to get gc'ed
state.onDisposed();
}
/**
* Transfers the specified transient item state instance from the 'active'
* cache to the attic.
*
* @param state the transient <code>ItemState</code> instance that should
* be moved to the attic
*/
public void moveTransientItemStateToAttic(ItemState state) {
// remove from map
transientStore.remove(state.getId());
// add to attic
atticStore.put(state);
}
/**
* Disposes the specified transient item state instance in the attic, i.e.
* discards it and removes it from the attic.
*
* @param state the transient <code>ItemState</code> instance that should
* be disposed @see ItemState#discard()
*/
public void disposeTransientItemStateInAttic(ItemState state) {
// discard item state, this will invalidate the wrapping Item
// instance of the transient state
state.discard();
// remove from attic
atticStore.remove(state.getId());
// give the instance a chance to prepare to get gc'ed
state.onDisposed();
}
/**
* Disposes all transient item states in the cache and in the attic.
*/
public void disposeAllTransientItemStates() {
// dispose item states in transient map & attic
// (use temp collection to avoid ConcurrentModificationException)
Collection tmp = new ArrayList(transientStore.values());
Iterator iter = tmp.iterator();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
disposeTransientItemState(state);
}
tmp = new ArrayList(atticStore.values());
iter = tmp.iterator();
while (iter.hasNext()) {
ItemState state = (ItemState) iter.next();
disposeTransientItemStateInAttic(state);
}
}
/**
* Add an <code>ItemStateListener</code>
*
* @param listener the new listener to be informed on modifications
*/
public void addListener(ItemStateListener listener) {
dispatcher.addListener(listener);
}
/**
* Remove an <code>ItemStateListener</code>
*
* @param listener an existing listener
*/
public void removeListener(ItemStateListener listener) {
dispatcher.removeListener(listener);
}
/**
* Return the attic item state provider that holds all items
* moved into the attic.
*
* @return attic
*/
ItemStateManager getAttic() {
if (attic == null) {
attic = new AtticItemStateManager();
}
return attic;
}
//----------------------------------------------------< ItemStateListener >
/**
* {@inheritDoc}
* <p/>
* Notification handler gets called for both transient states that this state manager
* has created, as well as states that were created by the local state manager
* we're listening to.
*/
public void stateCreated(ItemState created) {
ItemState visibleState = created;
if (created.getContainer() != this) {
// local state was created
ItemState transientState = transientStore.get(created.getId());
if (transientState != null) {
// underlying state has been permanently created
transientState.pull();
transientState.setStatus(ItemState.STATUS_EXISTING);
visibleState = transientState;
}
}
dispatcher.notifyStateCreated(visibleState);
}
/**
* {@inheritDoc}
* <p/>
* Notification handler gets called for both transient states that this state manager
* has created, as well as states that were created by the local state manager
* we're listening to.
*/
public void stateModified(ItemState modified) {
ItemState visibleState = modified;
if (modified.getContainer() != this) {
// local state was modified
ItemState transientState = transientStore.get(modified.getId());
if (transientState != null) {
if (transientState.isNode() && !transientState.isStale()) {
// try to silently merge non-conflicting changes (JCR-584)
NodeStateMerger.MergeContext context =
new NodeStateMerger.MergeContext() {
public boolean isAdded(ItemId id) {
ItemState is = transientStore.get(id);
return is != null
&& is.getStatus() == ItemState.STATUS_NEW;
}
public boolean isDeleted(ItemId id) {
return atticStore.contains(id);
}
};
if (NodeStateMerger.merge((NodeState) transientState, context)) {
// merge succeeded
return;
}
}
transientState.setStatus(ItemState.STATUS_STALE_MODIFIED);
visibleState = transientState;
}
}
dispatcher.notifyStateModified(visibleState);
}
/**
* {@inheritDoc}
* <p/>
* Notification handler gets called for both transient states that this state manager
* has created, as well as states that were created by the local state manager
* we're listening to.
*/
public void stateDestroyed(ItemState destroyed) {
ItemState visibleState = destroyed;
if (destroyed.getContainer() != this) {
// local state was destroyed
ItemState transientState = transientStore.get(destroyed.getId());
if (transientState != null) {
transientState.setStatus(ItemState.STATUS_STALE_DESTROYED);
visibleState = transientState;
}
}
dispatcher.notifyStateDestroyed(visibleState);
}
/**
* {@inheritDoc}
* <p/>
* Notification handler gets called for both transient states that this state manager
* has created, as well as states that were created by the local state manager
* we're listening to.
*/
public void stateDiscarded(ItemState discarded) {
ItemState visibleState = discarded;
if (discarded.getContainer() != this) {
// local state was discarded
ItemState transientState = transientStore.get(discarded.getId());
if (transientState != null) {
transientState.setStatus(ItemState.STATUS_UNDEFINED);
visibleState = transientState;
}
}
dispatcher.notifyStateDiscarded(visibleState);
}
/**
* {@inheritDoc}
* <p/>
* Pass notification to listeners if a transient state was modified
* or if the local state is not overlayed.
*/
public void nodeAdded(NodeState state, QName name, int index, NodeId id) {
if (state.getContainer() == this || !transientStore.contains(state.getId())) {
dispatcher.notifyNodeAdded(state, name, index, id);
}
}
/**
* {@inheritDoc}
* <p/>
* Pass notification to listeners if a transient state was modified
* or if the local state is not overlayed.
*/
public void nodesReplaced(NodeState state) {
if (state.getContainer() == this || !transientStore.contains(state.getId())) {
dispatcher.notifyNodesReplaced(state);
}
}
/**
* {@inheritDoc}
* <p/>
* Pass notification to listeners if a transient state was modified
* or if the local state is not overlayed.
*/
public void nodeModified(NodeState state) {
if (state.getContainer() == this || !transientStore.contains(state.getId())) {
dispatcher.notifyNodeModified(state);
}
}
/**
* {@inheritDoc}
* <p/>
* Pass notification to listeners if a transient state was modified
* or if the local state is not overlayed.
*/
public void nodeRemoved(NodeState state, QName name, int index, NodeId id) {
if (state.getContainer() == this || !transientStore.contains(state.getId())) {
dispatcher.notifyNodeRemoved(state, name, index, id);
}
}
//--------------------------------------------------------< inner classes >
/**
* ItemStateManager view of the states in the attic
*
* @see SessionItemStateManager#getAttic
*/
private class AtticItemStateManager implements ItemStateManager {
/**
* {@inheritDoc}
*/
public ItemState getItemState(ItemId id)
throws NoSuchItemStateException, ItemStateException {
ItemState state = atticStore.get(id);
if (state != null) {
return state;
} else {
throw new NoSuchItemStateException(id.toString());
}
}
/**
* {@inheritDoc}
*/
public boolean hasItemState(ItemId id) {
return atticStore.contains(id);
}
/**
* {@inheritDoc}
*/
public NodeReferences getNodeReferences(NodeReferencesId id)
throws NoSuchItemStateException, ItemStateException {
// n/a
throw new ItemStateException("getNodeReferences() not implemented");
}
/**
* {@inheritDoc}
*/
public boolean hasNodeReferences(NodeReferencesId id) {
// n/a
return false;
}
}
}