blob: 83981c79b7553db9fd7bb43f895781deaf8a568b [file] [log] [blame]
/*
* 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.jackrabbit.core.ItemId;
import org.apache.jackrabbit.core.NodeId;
import org.apache.jackrabbit.core.PropertyId;
import org.apache.jackrabbit.core.RepositoryImpl;
import org.apache.jackrabbit.core.cluster.UpdateEventChannel;
import org.apache.jackrabbit.core.persistence.PersistenceManager;
import org.apache.jackrabbit.core.persistence.bundle.CachingPersistenceManager;
import org.apache.jackrabbit.core.version.XAVersionManager;
import org.apache.jackrabbit.core.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.core.nodetype.NodeDefId;
import org.apache.jackrabbit.core.nodetype.NodeTypeConflictException;
import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
import org.apache.jackrabbit.core.nodetype.PropDef;
import org.apache.jackrabbit.core.nodetype.NodeDef;
import org.apache.jackrabbit.core.observation.EventStateCollection;
import org.apache.jackrabbit.core.observation.EventStateCollectionFactory;
import org.apache.jackrabbit.core.util.Dumpable;
import org.apache.jackrabbit.core.value.InternalValue;
import org.apache.jackrabbit.core.virtual.VirtualItemStateProvider;
import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.jcr.PropertyType;
import javax.jcr.ReferentialIntegrityException;
import javax.jcr.RepositoryException;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NoSuchNodeTypeException;
import java.io.PrintStream;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import java.util.HashMap;
/**
* Shared <code>ItemStateManager</code> (SISM). Caches objects returned from a
* <code>PersistenceManager</code>. Objects returned by this item state
* manager are shared among all sessions.
* <p/>
* A shared item state manager operates on a <code>PersistenceManager</code>
* (PM) that is used to load and store the item states. Additionally, a SISM can
* have <code>VirtualItemStateProvider</code>s (VISP) that are used to provide
* additional, non-persistent, read-only states. Examples of VISP are the
* content representation of the NodeTypes (/jcr:system/jcr:nodeTypes) and the
* version store (/jcr:system/jcr:versionStore). those 2 VISP are added to the
* SISM during initialization of a workspace. i.e. they are 'mounted' to all
* workspaces. we assume, that VISP cannot be added dynamically, neither during
* runtime nor by configuration.
* <p/>
* The states from the VISP are readonly. by the exception for node references.
* remember that the referrers are stored in a {@link NodeReferences} state,
* having the ID of the target state.
* <br/>
* there are 5 types of referential relations to be distinguished:
* <ol>
* <li> normal --> normal (references from 'normal' states to 'normal' states)
* this is the normal case and will be handled by the SISM.
*
* <li> normal --> virtual (references from 'normal' states to 'virtual' states)
* those references should be handled by the VISP rather by the SISM.
*
* <li> virtual --> normal (references from 'virtual' states to 'normal' states)
* such references are not supported. eg. references of versioned nodes do
* not impose any constraints on the referenced nodes.
*
* <li> virtual --> virtual (references from 'virtual' states to 'virtual'
* states of the same VISP).
* intra-virtual references are handled by the item state manager of the VISP.
*
* <li> virtual --> virtual' (references from 'virtual' states to 'virtual'
* states of different VISP).
* those do currently not occurr and are therfor not supported.
* </ol>
* <p/>
* if VISP are not dynamic, there is not risk that NV-type references can dangle
* (since a VISP cannot be 'unmounted', leaving eventual references dangling).
* although multi-workspace-referrers are not explicitelt supported, the
* architecture of <code>NodeReferences</code> support multiple referrers with
* the same PropertyId. So the number of references can be tracked (an example
* of multi-workspace-refferres is a version referenced by the jcr:baseVersion
* of several (corresponding) nodes in multiple workspaces).
* <br/>
* As mentioned, VN-type references should not impose any constraints on the
* referrers (e.g. a normal node referenced by a versioned reference property).
* In case of the version store, the VN-type references are not stored at
* all, but reinforced as NN-type references in the normal states in case of a
* checkout operation.
* <br/>
* VV-type references should be handled by the respective VISP. they look as
* NN-type references in the scope if the VISP anyway...so no special treatment
* should be neccessairy.
* <br/>
* VV'-type references are currently not possible, since the version store and
* virtual nodetype representation don't allow such references.
*/
public class SharedItemStateManager
implements ItemStateManager, ItemStateListener, Dumpable {
/**
* Logger instance
*/
private static Logger log = LoggerFactory.getLogger(SharedItemStateManager.class);
/**
* cache of weak references to ItemState objects issued by this
* ItemStateManager
*/
private final ItemStateCache cache;
/**
* Persistence Manager used for loading and storing items
*/
private final PersistenceManager persistMgr;
/**
* node type registry used for identifying referenceable nodes
*/
private final NodeTypeRegistry ntReg;
/**
* Flag indicating whether this item state manager uses node references to
* verify integrity of its reference properties.
*/
private final boolean usesReferences;
/**
* id of root node
*/
private final NodeId rootNodeId;
/**
* Virtual item state providers
*/
private VirtualItemStateProvider[] virtualProviders =
new VirtualItemStateProvider[0];
/**
* State change dispatcher.
*/
private final transient StateChangeDispatcher dispatcher = new StateChangeDispatcher();
/**
* The locking strategy.
*/
private ISMLocking ismLocking;
/**
* Update event channel.
*/
private UpdateEventChannel eventChannel;
/**
* Creates a new <code>SharedItemStateManager</code> instance.
*
* @param persistMgr
* @param rootNodeId
* @param ntReg
*/
public SharedItemStateManager(PersistenceManager persistMgr,
NodeId rootNodeId,
NodeTypeRegistry ntReg,
boolean usesReferences,
ItemStateCacheFactory cacheFactory,
ISMLocking locking)
throws ItemStateException {
cache = new ItemStateReferenceCache(cacheFactory);
this.persistMgr = persistMgr;
this.ntReg = ntReg;
this.usesReferences = usesReferences;
this.rootNodeId = rootNodeId;
this.ismLocking = locking;
// create root node state if it doesn't yet exist
if (!hasNonVirtualItemState(rootNodeId)) {
createRootNodeState(rootNodeId, ntReg);
}
}
/**
* enables or disables the write-lock hack. this should only be called by
* the {@link XAVersionManager}.
*
* @param noLockHack
*/
public void setNoLockHack(boolean noLockHack) {
if (ismLocking instanceof DefaultISMLocking) {
((DefaultISMLocking) ismLocking).setNoLockHack(noLockHack);
}
}
/**
* Set an update event channel
*
* @param eventChannel update event channel
*/
public void setEventChannel(UpdateEventChannel eventChannel) {
this.eventChannel = eventChannel;
}
/**
* Sets a new locking strategy.
*
* @param ismLocking the locking strategy for this item state manager.
*/
public void setISMLocking(ISMLocking ismLocking) {
if (ismLocking == null) {
throw new NullPointerException();
}
this.ismLocking = ismLocking;
}
//-----------------------------------------------------< ItemStateManager >
/**
* {@inheritDoc}
*/
public ItemState getItemState(ItemId id)
throws NoSuchItemStateException, ItemStateException {
ISMLocking.ReadLock readLock = acquireReadLock(id);
try {
// check the virtual root ids (needed for overlay)
for (int i = 0; i < virtualProviders.length; i++) {
if (virtualProviders[i].isVirtualRoot(id)) {
return virtualProviders[i].getItemState(id);
}
}
// check internal first
if (hasNonVirtualItemState(id)) {
return getNonVirtualItemState(id);
}
// check if there is a virtual state for the specified item
for (int i = 0; i < virtualProviders.length; i++) {
if (virtualProviders[i].hasItemState(id)) {
return virtualProviders[i].getItemState(id);
}
}
} finally {
readLock.release();
}
throw new NoSuchItemStateException(id.toString());
}
/**
* {@inheritDoc}
*/
public boolean hasItemState(ItemId id) {
ISMLocking.ReadLock readLock;
try {
readLock = acquireReadLock(id);
} catch (ItemStateException e) {
return false;
}
try {
if (cache.isCached(id)) {
return true;
}
// check the virtual root ids (needed for overlay)
for (int i = 0; i < virtualProviders.length; i++) {
if (virtualProviders[i].isVirtualRoot(id)) {
return true;
}
}
// check if this manager has the item state
if (hasNonVirtualItemState(id)) {
return true;
}
// otherwise check virtual ones
for (int i = 0; i < virtualProviders.length; i++) {
if (virtualProviders[i].hasItemState(id)) {
return true;
}
}
} finally {
readLock.release();
}
return false;
}
/**
* {@inheritDoc}
*/
public NodeReferences getNodeReferences(NodeReferencesId id)
throws NoSuchItemStateException, ItemStateException {
ISMLocking.ReadLock readLock = acquireReadLock(id.getTargetId());
try {
// check persistence manager
try {
return persistMgr.load(id);
} catch (NoSuchItemStateException e) {
// ignore
}
// check virtual providers
for (int i = 0; i < virtualProviders.length; i++) {
try {
return virtualProviders[i].getNodeReferences(id);
} catch (NoSuchItemStateException e) {
// ignore
}
}
} finally {
readLock.release();
}
// throw
throw new NoSuchItemStateException(id.toString());
}
/**
* {@inheritDoc}
*/
public boolean hasNodeReferences(NodeReferencesId id) {
ISMLocking.ReadLock readLock;
try {
readLock = acquireReadLock(id.getTargetId());
} catch (ItemStateException e) {
return false;
}
try {
// check persistence manager
try {
if (persistMgr.exists(id)) {
return true;
}
} catch (ItemStateException e) {
// ignore
}
// check virtual providers
for (int i = 0; i < virtualProviders.length; i++) {
if (virtualProviders[i].hasNodeReferences(id)) {
return true;
}
}
} finally {
readLock.release();
}
return false;
}
//----------------------------------------------------< ItemStateListener >
/**
* {@inheritDoc}
* <p/>
* Notifications are received for items that this manager created itself or items that are
* managed by one of the virtual providers.
*/
public void stateCreated(ItemState created) {
if (created.getContainer() == this) {
// shared state was created
cache.cache(created);
}
dispatcher.notifyStateCreated(created);
}
/**
* {@inheritDoc}
* <p/>
* Notifications are received for items that this manager created itself or items that are
* managed by one of the virtual providers.
*/
public void stateModified(ItemState modified) {
dispatcher.notifyStateModified(modified);
}
/**
* {@inheritDoc}
* <p/>
* Notifications are received for items that this manager created itself or items that are
* managed by one of the virtual providers.
*/
public void stateDestroyed(ItemState destroyed) {
if (destroyed.getContainer() == this) {
// shared state was destroyed
cache.evict(destroyed.getId());
}
dispatcher.notifyStateDestroyed(destroyed);
}
/**
* {@inheritDoc}
* <p/>
* Notifications are received for items that this manager created itself or items that are
* managed by one of the virtual providers.
*/
public void stateDiscarded(ItemState discarded) {
if (discarded.getContainer() == this) {
// shared state was discarded
cache.evict(discarded.getId());
}
dispatcher.notifyStateDiscarded(discarded);
}
//-------------------------------------------------------------< Dumpable >
/**
* {@inheritDoc}
*/
public void dump(PrintStream ps) {
ps.println("SharedItemStateManager (" + this + ")");
if (cache instanceof Dumpable) {
ps.println();
ps.print("[referenceCache] ");
((Dumpable) cache).dump(ps);
}
}
//-------------------------------------------------< misc. public methods >
/**
* Disposes this <code>SharedItemStateManager</code> and frees resources.
*/
public void dispose() {
// clear cache
cache.evictAll();
}
/**
* Adds a new virtual item state provider.<p/>
* NOTE: This method is not synchronized, because it is called right after
* creation only by the same thread and therefore concurrency issues
* do not occur. Should this ever change, the synchronization status
* has to be re-examined.
*
* @param prov
*/
public void addVirtualItemStateProvider(VirtualItemStateProvider prov) {
VirtualItemStateProvider[] provs =
new VirtualItemStateProvider[virtualProviders.length + 1];
System.arraycopy(virtualProviders, 0, provs, 0, virtualProviders.length);
provs[virtualProviders.length] = prov;
virtualProviders = provs;
prov.addListener(this);
}
/**
* Object representing a single update operation.
*/
class Update implements org.apache.jackrabbit.core.cluster.Update {
/**
* Local change log.
*/
private final ChangeLog local;
/**
* Event state collection factory.
*/
private final EventStateCollectionFactory factory;
/**
* Virtual provider containing references to be left out when updating
* references.
*/
private final VirtualItemStateProvider virtualProvider;
/**
* Shared change log.
*/
private ChangeLog shared;
/**
* Virtual node references.
*/
private List[] virtualNodeReferences;
/**
* Events to dispatch.
*/
private EventStateCollection events;
/**
* The write lock we currently hold or <code>null</code> if none is
* hold.
*/
private ISMLocking.WriteLock writeLock;
/**
* Map of attributes stored for this update operation.
*/
private HashMap attributes;
/**
* Create a new instance of this class.
*/
public Update(ChangeLog local, EventStateCollectionFactory factory,
VirtualItemStateProvider virtualProvider) {
this.local = local;
this.factory = factory;
this.virtualProvider = virtualProvider;
}
/**
* Begin update operation. Prepares everything upto the point where
* the persistence manager's <code>store</code> method may be invoked.
* If this method succeeds, a write lock will have been acquired on the
* item state manager and either {@link #end()} or {@link #cancel()} has
* to be called in order to release it.
*/
public void begin() throws ItemStateException, ReferentialIntegrityException {
shared = new ChangeLog();
virtualNodeReferences = new List[virtualProviders.length];
/* let listener know about change */
if (eventChannel != null) {
eventChannel.updateCreated(this);
}
try {
writeLock = acquireWriteLock(local);
} finally {
if (writeLock == null && eventChannel != null) {
eventChannel.updateCancelled(this);
}
}
boolean succeeded = false;
try {
if (usesReferences) {
/**
* Update node references based on modifications in change log
* (added/modified/removed REFERENCE properties)
*/
updateReferences(local, virtualProvider);
}
/**
* Check whether reference targets exist/were not removed
*/
checkReferentialIntegrity(local);
/**
* prepare the events. this needs to be after the referential
* integrity check, since another transaction could have modified
* the states.
*/
try {
events = factory.createEventStateCollection();
} catch (RepositoryException e) {
String msg = "Unable to create event state collection.";
log.error(msg);
throw new ItemStateException(msg, e);
}
/**
* Reconnect all items contained in the change log to their
* respective shared item and add the shared items to a
* new change log.
*/
for (Iterator iter = local.modifiedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
state.connect(getItemState(state.getId()));
if (state.isStale()) {
boolean merged = false;
if (state.isNode()) {
NodeStateMerger.MergeContext context =
new NodeStateMerger.MergeContext() {
public boolean isAdded(ItemId id) {
try {
ItemState is = local.get(id);
return is != null
&& is.getStatus() == ItemState.STATUS_NEW;
} catch (NoSuchItemStateException e) {
return false;
}
}
public boolean isDeleted(ItemId id) {
return local.deleted(id);
}
public boolean isModified(ItemId id) {
return local.isModified(id);
}
public boolean allowsSameNameSiblings(NodeId id) {
NodeState ns;
try {
if (local.has(id)) {
ns = (NodeState) local.get(id);
} else {
ns = (NodeState) getItemState(id);
}
} catch (ItemStateException e) {
return false;
}
NodeDef def = ntReg.getNodeDef(ns.getDefinitionId());
return def != null ? def.allowsSameNameSiblings() : false;
}
};
merged = NodeStateMerger.merge((NodeState) state, context);
}
if (!merged) {
String msg = state.getId() + " has been modified externally";
log.debug(msg);
throw new StaleItemStateException(msg);
}
// merge succeeded, fall through
}
// update modification count (will be persisted as well)
state.getOverlayedState().touch();
shared.modified(state.getOverlayedState());
}
for (Iterator iter = local.deletedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
state.connect(getItemState(state.getId()));
if (state.isStale()) {
String msg = state.getId() + " has been modified externally";
log.debug(msg);
throw new StaleItemStateException(msg);
}
shared.deleted(state.getOverlayedState());
}
for (Iterator iter = local.addedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
state.connect(createInstance(state));
shared.added(state.getOverlayedState());
}
// filter out virtual node references for later processing
// (see comment above)
for (Iterator iter = local.modifiedRefs(); iter.hasNext();) {
NodeReferences refs = (NodeReferences) iter.next();
boolean virtual = false;
NodeId id = refs.getId().getTargetId();
for (int i = 0; i < virtualProviders.length; i++) {
if (virtualProviders[i].hasItemState(id)) {
List virtualRefs = virtualNodeReferences[i];
if (virtualRefs == null) {
virtualRefs = new LinkedList();
virtualNodeReferences[i] = virtualRefs;
}
virtualRefs.add(refs);
virtual = true;
break;
}
}
if (!virtual) {
// if target of node reference does not lie in a virtual
// space, add to modified set of normal provider.
shared.modified(refs);
}
}
checkAddedChildNodes();
/* create event states */
events.createEventStates(rootNodeId, local, SharedItemStateManager.this);
/* let listener know about change */
if (eventChannel != null) {
eventChannel.updatePrepared(this);
}
/* Push all changes from the local items to the shared items */
local.push();
succeeded = true;
} finally {
if (!succeeded) {
cancel();
}
}
}
/**
* Verify the added child nodes of the added or modified states exist.
* If they don't exist, most likely the problem is that the same session
* is used concurrently.
*/
private void checkAddedChildNodes() throws ItemStateException {
for (Iterator iter = local.addedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
checkAddedChildNode(state);
}
for (Iterator iter = local.modifiedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
checkAddedChildNode(state);
}
}
private void checkAddedChildNode(ItemState state) throws ItemStateException {
if (state.isNode()) {
NodeState nodeState = (NodeState) state;
for (Iterator cneIt = nodeState.getAddedChildNodeEntries().iterator(); cneIt.hasNext();) {
NodeState.ChildNodeEntry cne = (NodeState.ChildNodeEntry) cneIt.next();
NodeId id = cne.getId();
if (local.get(id) == null && !id.equals(RepositoryImpl.VERSION_STORAGE_NODE_ID)
&& !id.equals(RepositoryImpl.NODETYPES_NODE_ID) && !cache.isCached(id)
&& !persistMgr.exists(id)) {
String msg = "Trying to add a non-existing child node: " + id;
log.debug(msg);
throw new ItemStateException(msg);
}
}
}
}
/**
* End update operation. This will store the changes to the associated
* <code>PersistenceManager</code>. At the end of this operation, an
* eventual read or write lock on the item state manager will have
* been released.
* @throws ItemStateException if some error occurs
*/
public void end() throws ItemStateException {
boolean succeeded = false;
try {
/* Store items in the underlying persistence manager */
long t0 = System.currentTimeMillis();
persistMgr.store(shared);
succeeded = true;
long t1 = System.currentTimeMillis();
if (log.isDebugEnabled()) {
log.debug("persisting change log " + shared + " took " + (t1 - t0) + "ms");
}
} finally {
if (!succeeded) {
cancel();
}
}
ISMLocking.ReadLock readLock = null;
try {
// downgrade to read lock
readLock = writeLock.downgrade();
writeLock = null;
// Let the shared item listeners know about the change
// JCR-2171: This must happen after downgrading the lock!
shared.persisted();
/* notify virtual providers about node references */
for (int i = 0; i < virtualNodeReferences.length; i++) {
List virtualRefs = virtualNodeReferences[i];
if (virtualRefs != null) {
for (Iterator iter = virtualRefs.iterator(); iter.hasNext();) {
NodeReferences refs = (NodeReferences) iter.next();
virtualProviders[i].setNodeReferences(refs);
}
}
}
/* dispatch the events */
events.dispatch();
/* let listener know about finished operation */
if (eventChannel != null) {
eventChannel.updateCommitted(this);
}
} catch (InterruptedException e) {
throw new ItemStateException("Interrupted while downgrading to read lock");
} finally {
if (writeLock != null) {
// exception occured before downgrading lock
writeLock.release();
writeLock = null;
} else if (readLock != null) {
readLock.release();
}
}
}
/**
* Cancel update operation. At the end of this operation, the write lock
* on the item state manager will have been released.
*/
public void cancel() {
try {
/* let listener know about cancelled operation */
if (eventChannel != null) {
eventChannel.updateCancelled(this);
}
local.disconnect();
for (Iterator iter = shared.modifiedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
try {
state.copy(loadItemState(state.getId()), false);
} catch (ItemStateException e) {
state.discard();
}
}
for (Iterator iter = shared.deletedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
try {
state.copy(loadItemState(state.getId()), false);
} catch (ItemStateException e) {
state.discard();
}
}
for (Iterator iter = shared.addedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
state.discard();
}
} finally {
if (writeLock != null) {
writeLock.release();
writeLock = null;
}
}
}
/**
* {@inheritDoc}
*/
public void setAttribute(String name, Object value) {
if (attributes == null) {
attributes = new HashMap();
}
attributes.put(name, value);
}
/**
* {@inheritDoc}
*/
public Object getAttribute(String name) {
if (attributes != null) {
return attributes.get(name);
}
return null;
}
/**
* {@inheritDoc}
*/
public ChangeLog getChanges() {
return local;
}
/**
* {@inheritDoc}
*/
public EventStateCollection getEvents() {
return events;
}
}
/**
* Begin update operation. This will return an object that can itself be
* ended/cancelled.
*/
public Update beginUpdate(ChangeLog local, EventStateCollectionFactory factory,
VirtualItemStateProvider virtualProvider)
throws ReferentialIntegrityException, StaleItemStateException,
ItemStateException {
Update update = new Update(local, factory, virtualProvider);
update.begin();
return update;
}
/**
* Store modifications registered in a <code>ChangeLog</code>. The items
* contained in the <tt>ChangeLog</tt> are not states returned by this
* item state manager but rather must be reconnected to items provided
* by this state manager.<p/>
* After successfully storing the states the observation manager is informed
* about the changes, if an observation manager is passed to this method.<p/>
* NOTE: This method is not synchronized, because all methods it invokes
* on instance members (such as {@link PersistenceManager#store} are
* considered to be thread-safe. Should this ever change, the
* synchronization status has to be re-examined.
*
* @param local change log containing local items
* @param factory event state collection factory
* @throws ReferentialIntegrityException if a new or modified REFERENCE
* property refers to a non-existent
* target or if a removed node is still
* being referenced
* @throws StaleItemStateException if at least one of the affected item
* states has become stale
* @throws ItemStateException if another error occurs
*/
public void update(ChangeLog local, EventStateCollectionFactory factory)
throws ReferentialIntegrityException, StaleItemStateException,
ItemStateException {
beginUpdate(local, factory, null).end();
}
/**
* Handle an external update.
*
* @param external external change containing only node and property ids.
* @param events events to deliver
*/
public void externalUpdate(ChangeLog external, EventStateCollection events) {
boolean holdingWriteLock = false;
ISMLocking.WriteLock wLock = null;
try {
wLock = acquireWriteLock(external);
holdingWriteLock = true;
doExternalUpdate(external);
} catch (ItemStateException e) {
String msg = "Unable to acquire write lock.";
log.error(msg);
}
ISMLocking.ReadLock rLock = null;
try {
if (wLock != null) {
rLock = wLock.downgrade();
holdingWriteLock = false;
events.dispatch();
}
} catch (InterruptedException e) {
String msg = "Unable to downgrade to read lock.";
log.error(msg);
} finally {
if (holdingWriteLock) {
if (wLock != null) {
wLock.release();
}
} else {
if (rLock != null) {
rLock.release();
}
}
}
}
/**
* Perform the external update. While executing this method, the
* <code>writeLock</code> on this manager is held.
*
* @param external external change containing only node and property ids.
*/
protected void doExternalUpdate(ChangeLog external) {
// workaround to flush cache of persistence manager
if (persistMgr instanceof CachingPersistenceManager) {
((CachingPersistenceManager) persistMgr).onExternalUpdate(external);
}
ChangeLog shared = new ChangeLog();
// Build a copy of the external change log, consisting of shared
// states we have in our cache. Inform listeners about this
// change.
Iterator modifiedStates = external.modifiedStates();
while (modifiedStates.hasNext()) {
ItemState state = (ItemState) modifiedStates.next();
state = cache.retrieve(state.getId());
if (state != null) {
try {
ItemState currentState = loadItemState(state.getId());
state.copy(currentState, true);
shared.modified(state);
} catch (NoSuchItemStateException e) {
// This is likely to happen because a subsequent delete
// of this very state has not yet been transmitted.
String msg = "Unable to retrieve state: " + state.getId() + ", ignored.";
log.info(msg);
state.discard();
} catch (ItemStateException e) {
String msg = "Unable to retrieve state: " + state.getId();
log.warn(msg);
state.discard();
}
}
}
Iterator deletedStates = external.deletedStates();
while (deletedStates.hasNext()) {
ItemState state = (ItemState) deletedStates.next();
state = cache.retrieve(state.getId());
if (state != null) {
shared.deleted(state);
}
}
shared.persisted();
}
/**
* 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);
}
//-------------------------------------------------------< implementation >
/**
* Create a new node state instance
*
* @param id uuid
* @param nodeTypeName node type name
* @param parentId parent UUID
* @return new node state instance
*/
private NodeState createInstance(NodeId id, Name nodeTypeName,
NodeId parentId) {
NodeState state = persistMgr.createNew(id);
state.setNodeTypeName(nodeTypeName);
state.setParentId(parentId);
state.setStatus(ItemState.STATUS_NEW);
state.setContainer(this);
return state;
}
/**
* Create root node state
*
* @param rootNodeId root node id
* @param ntReg node type registry
* @return root node state
* @throws ItemStateException if an error occurs
*/
private NodeState createRootNodeState(NodeId rootNodeId,
NodeTypeRegistry ntReg)
throws ItemStateException {
NodeState rootState = createInstance(rootNodeId, NameConstants.REP_ROOT, null);
NodeState jcrSystemState = createInstance(RepositoryImpl.SYSTEM_ROOT_NODE_ID, NameConstants.REP_SYSTEM, rootNodeId);
// FIXME need to manually setup root node by creating mandatory jcr:primaryType property
// @todo delegate setup of root node to NodeTypeInstanceHandler
// id of the root node's definition
NodeDefId nodeDefId;
// definition of jcr:primaryType property
PropDef propDef;
// id of the jcr:system node's definition
NodeDefId jcrSystemDefId;
try {
nodeDefId = ntReg.getRootNodeDef().getId();
EffectiveNodeType ent = ntReg.getEffectiveNodeType(NameConstants.REP_ROOT);
propDef = ent.getApplicablePropertyDef(NameConstants.JCR_PRIMARYTYPE,
PropertyType.NAME, false);
jcrSystemDefId = ent.getApplicableChildNodeDef(NameConstants.JCR_SYSTEM, NameConstants.REP_SYSTEM, ntReg).getId();
} catch (NoSuchNodeTypeException nsnte) {
String msg = "internal error: failed to create root node";
log.error(msg, nsnte);
throw new ItemStateException(msg, nsnte);
} catch (ConstraintViolationException cve) {
String msg = "internal error: failed to create root node";
log.error(msg, cve);
throw new ItemStateException(msg, cve);
}
rootState.setDefinitionId(nodeDefId);
jcrSystemState.setDefinitionId(jcrSystemDefId);
// create jcr:primaryType property on root node state
rootState.addPropertyName(propDef.getName());
PropertyState prop = createInstance(propDef.getName(), rootNodeId);
prop.setValues(new InternalValue[]{InternalValue.create(NameConstants.REP_ROOT)});
prop.setType(propDef.getRequiredType());
prop.setMultiValued(propDef.isMultiple());
prop.setDefinitionId(propDef.getId());
// create jcr:primaryType property on jcr:system node state
jcrSystemState.addPropertyName(propDef.getName());
PropertyState primaryTypeProp = createInstance(propDef.getName(), jcrSystemState.getNodeId());
primaryTypeProp.setValues(new InternalValue[]{InternalValue.create(NameConstants.REP_SYSTEM)});
primaryTypeProp.setType(propDef.getRequiredType());
primaryTypeProp.setMultiValued(propDef.isMultiple());
primaryTypeProp.setDefinitionId(propDef.getId());
// add child node entry for jcr:system node
rootState.addChildNodeEntry(NameConstants.JCR_SYSTEM, RepositoryImpl.SYSTEM_ROOT_NODE_ID);
// add child node entry for virtual jcr:versionStorage
jcrSystemState.addChildNodeEntry(NameConstants.JCR_VERSIONSTORAGE, RepositoryImpl.VERSION_STORAGE_NODE_ID);
// add child node entry for virtual jcr:nodeTypes
jcrSystemState.addChildNodeEntry(NameConstants.JCR_NODETYPES, RepositoryImpl.NODETYPES_NODE_ID);
ChangeLog changeLog = new ChangeLog();
changeLog.added(rootState);
changeLog.added(prop);
changeLog.added(jcrSystemState);
changeLog.added(primaryTypeProp);
persistMgr.store(changeLog);
changeLog.persisted();
return rootState;
}
/**
* Returns the item state for the given id without considering virtual
* item state providers.
*/
private ItemState getNonVirtualItemState(ItemId id)
throws NoSuchItemStateException, ItemStateException {
// check cache; synchronized to ensure an entry is not created twice.
synchronized (cache) {
ItemState state = cache.retrieve(id);
if (state == null) {
// not found in cache, load from persistent storage
state = loadItemState(id);
state.setStatus(ItemState.STATUS_EXISTING);
// put it in cache
cache.cache(state);
// set parent container
state.setContainer(this);
}
return state;
}
}
/**
* Checks if this item state manager has the given item state without
* considering the virtual item state managers.
*/
private boolean hasNonVirtualItemState(ItemId id) {
if (cache.isCached(id)) {
return true;
}
try {
if (id.denotesNode()) {
return persistMgr.exists((NodeId) id);
} else {
return persistMgr.exists((PropertyId) id);
}
} catch (ItemStateException ise) {
return false;
}
}
/**
* Create a new item state instance
*
* @param other other state associated with new instance
* @return new node state instance
*/
private ItemState createInstance(ItemState other) {
if (other.isNode()) {
NodeState ns = (NodeState) other;
return createInstance(ns.getNodeId(), ns.getNodeTypeName(), ns.getParentId());
} else {
PropertyState ps = (PropertyState) other;
return createInstance(ps.getName(), ps.getParentId());
}
}
/**
* Create a new property state instance
*
* @param propName property name
* @param parentId parent Id
* @return new property state instance
*/
private PropertyState createInstance(Name propName, NodeId parentId) {
PropertyState state = persistMgr.createNew(new PropertyId(parentId, propName));
state.setStatus(ItemState.STATUS_NEW);
state.setContainer(this);
return state;
}
/**
* Load item state from persistent storage.
*
* @param id item id
* @return item state
*/
private ItemState loadItemState(ItemId id)
throws NoSuchItemStateException, ItemStateException {
ItemState state;
if (id.denotesNode()) {
state = persistMgr.load((NodeId) id);
} else {
state = persistMgr.load((PropertyId) id);
}
return state;
}
/**
* Determines whether the specified node is <i>referenceable</i>, i.e.
* whether the mixin type <code>mix:referenceable</code> is either
* directly assigned or indirectly inherited.
*
* @param state node state to check
* @return true if the specified node is <i>referenceable</i>, false otherwise.
* @throws ItemStateException if an error occurs
*/
private boolean isReferenceable(NodeState state) throws ItemStateException {
// shortcut: check some wellknown built-in types first
Name primary = state.getNodeTypeName();
Set mixins = state.getMixinTypeNames();
if (mixins.contains(NameConstants.MIX_REFERENCEABLE)
|| mixins.contains(NameConstants.MIX_VERSIONABLE)
|| primary.equals(NameConstants.NT_RESOURCE)) {
return true;
}
// build effective node type
Name[] types = new Name[mixins.size() + 1];
mixins.toArray(types);
// primary type
types[types.length - 1] = primary;
try {
return ntReg.getEffectiveNodeType(types).includesNodeType(NameConstants.MIX_REFERENCEABLE);
} catch (NodeTypeConflictException ntce) {
String msg = "internal error: failed to build effective node type for node "
+ state.getNodeId();
log.debug(msg);
throw new ItemStateException(msg, ntce);
} catch (NoSuchNodeTypeException nsnte) {
String msg = "internal error: failed to build effective node type for node "
+ state.getNodeId();
log.debug(msg);
throw new ItemStateException(msg, nsnte);
}
}
/**
* Updates the target node references collections based on the modifications
* in the change log (i.e. added/removed/modified <code>REFERENCE</code>
* properties).
* <p/>
* <b>Important node:</b> For consistency reasons this method must only be
* called <i>once</i> per change log and the change log should not be modified
* anymore afterwards.
*
* @param changes change log
* @param virtualProvider virtual provider that may already contain a
* node references object
* @throws ItemStateException if an error occurs
*/
protected void updateReferences(ChangeLog changes,
VirtualItemStateProvider virtualProvider)
throws ItemStateException {
// process added REFERENCE properties
for (Iterator iter = changes.addedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
if (!state.isNode()) {
PropertyState prop = (PropertyState) state;
if (prop.getType() == PropertyType.REFERENCE) {
// this is a new REFERENCE property:
// add the new 'reference'
InternalValue[] vals = prop.getValues();
for (int i = 0; vals != null && i < vals.length; i++) {
NodeReferencesId refsId = new NodeReferencesId(
vals[i].getUUID());
if (virtualProvider != null
&& virtualProvider.hasNodeReferences(refsId)) {
continue;
}
NodeReferences refs =
getOrCreateNodeReferences(refsId, changes);
// add reference
refs.addReference(prop.getPropertyId());
// update change log
changes.modified(refs);
}
}
}
}
// process modified REFERENCE properties
for (Iterator iter = changes.modifiedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
if (!state.isNode()) {
PropertyState newProp = (PropertyState) state;
PropertyState oldProp =
(PropertyState) getItemState(state.getId());
// check old type
if (oldProp.getType() == PropertyType.REFERENCE) {
// this is a modified REFERENCE property:
// remove the old 'reference' from the target
InternalValue[] vals = oldProp.getValues();
for (int i = 0; vals != null && i < vals.length; i++) {
NodeReferencesId refsId = new NodeReferencesId(
vals[i].getUUID());
if (virtualProvider != null
&& virtualProvider.hasNodeReferences(refsId)) {
continue;
}
// either get node references from change log or load from
// persistence manager
NodeReferences refs = changes.get(refsId);
if (refs == null) {
refs = getNodeReferences(refsId);
}
// remove reference
refs.removeReference(oldProp.getPropertyId());
// update change log
changes.modified(refs);
}
}
// check new type
if (newProp.getType() == PropertyType.REFERENCE) {
// this is a modified REFERENCE property:
// add the new 'reference' to the target
InternalValue[] vals = newProp.getValues();
for (int i = 0; vals != null && i < vals.length; i++) {
NodeReferencesId refsId = new NodeReferencesId(
vals[i].getUUID());
if (virtualProvider != null
&& virtualProvider.hasNodeReferences(refsId)) {
continue;
}
NodeReferences refs =
getOrCreateNodeReferences(refsId, changes);
// add reference
refs.addReference(newProp.getPropertyId());
// update change log
changes.modified(refs);
}
}
}
}
// process removed REFERENCE properties
for (Iterator iter = changes.deletedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
if (!state.isNode()) {
PropertyState prop = (PropertyState) state;
if (prop.getType() == PropertyType.REFERENCE) {
// this is a removed REFERENCE property:
// remove the 'reference' from the target
InternalValue[] vals = prop.getValues();
for (int i = 0; vals != null && i < vals.length; i++) {
NodeReferencesId refsId = new NodeReferencesId(
vals[i].getUUID());
if (virtualProvider != null
&& virtualProvider.hasNodeReferences(refsId)) {
continue;
}
// either get node references from change log or
// load from persistence manager
NodeReferences refs = changes.get(refsId);
if (refs == null) {
refs = getNodeReferences(refsId);
}
// remove reference
refs.removeReference(prop.getPropertyId());
// update change log
changes.modified(refs);
}
}
}
}
}
/**
* Returns a node references object using the following rules:<p/>
* <ul>
* <li>1. return a modified instance from the change log (if one exists)</li>
* <li>2. return an existing instance from <i>this</i> item state manager
* (if one exists)</li>
* <li>3. create and return a new instance</li>
* </ul>
*
* @param refsId node references id
* @param changes change log
* @return a node references object
* @throws ItemStateException if an error occurs
*/
private NodeReferences getOrCreateNodeReferences(NodeReferencesId refsId,
ChangeLog changes)
throws ItemStateException {
// check change log
NodeReferences refs = changes.get(refsId);
if (refs == null) {
// not yet in change log:
// either load existing or create new
if (hasNodeReferences(refsId)) {
refs = getNodeReferences(refsId);
} else {
refs = new NodeReferences(refsId);
}
}
return refs;
}
/**
* Verifies that
* <ul>
* <li>no referenceable nodes are deleted if they are still being referenced</li>
* <li>targets of modified node references exist</li>
* </ul>
*
* @param changes change log
* @throws ReferentialIntegrityException if a new or modified REFERENCE
* property refers to a non-existent
* target or if a removed node is still
* being referenced
* @throws ItemStateException if another error occurs
*/
protected void checkReferentialIntegrity(ChangeLog changes)
throws ReferentialIntegrityException, ItemStateException {
// check whether removed referenceable nodes are still being referenced
for (Iterator iter = changes.deletedStates(); iter.hasNext();) {
ItemState state = (ItemState) iter.next();
if (state.isNode()) {
NodeState node = (NodeState) state;
if (isReferenceable(node)) {
NodeReferencesId refsId = new NodeReferencesId(node.getNodeId());
// either get node references from change log or
// load from persistence manager
NodeReferences refs = changes.get(refsId);
if (refs == null) {
if (!hasNodeReferences(refsId)) {
continue;
}
refs = getNodeReferences(refsId);
}
// in some versioning operations (such as restore) a node
// may actually be deleted and then again added with the
// same UUID, i.e. the node is still referenceable.
if (refs.hasReferences() && !changes.has(node.getNodeId())) {
String msg = node.getNodeId()
+ ": the node cannot be removed because it is still being referenced.";
log.debug(msg);
throw new ReferentialIntegrityException(msg);
}
}
}
}
// check whether targets of modified node references exist
for (Iterator iter = changes.modifiedRefs(); iter.hasNext();) {
NodeReferences refs = (NodeReferences) iter.next();
NodeId id = refs.getTargetId();
// no need to check existence of target if there are no references
if (refs.hasReferences()) {
// please note:
// virtual providers are indirectly checked via 'hasItemState()'
if (!changes.has(id) && !hasItemState(id)) {
String msg = "Target node " + id
+ " of REFERENCE property does not exist";
log.debug(msg);
throw new ReferentialIntegrityException(msg);
}
}
}
}
/**
* Acquires the read lock on this item state manager.
*
* @param id the id of the item for which to acquire a read lock.
* @throws ItemStateException if the read lock cannot be acquired.
*/
private ISMLocking.ReadLock acquireReadLock(ItemId id) throws ItemStateException {
try {
return ismLocking.acquireReadLock(id);
} catch (InterruptedException e) {
throw new ItemStateException("Interrupted while acquiring read lock");
}
}
/**
* Acquires the write lock on this item state manager.
*
* @param changeLog the change log for which to acquire a write lock.
* @throws ItemStateException if the write lock cannot be acquired.
*/
private ISMLocking.WriteLock acquireWriteLock(ChangeLog changeLog) throws ItemStateException {
try {
return ismLocking.acquireWriteLock(changeLog);
} catch (InterruptedException e) {
throw new ItemStateException("Interrupted while acquiring write lock");
}
}
}