/* * 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 javax.jcr.InvalidItemStateException; import javax.jcr.ReferentialIntegrityException; import javax.jcr.RepositoryException; import org.apache.jackrabbit.core.id.ItemId; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.id.NodeIdFactory; import org.apache.jackrabbit.core.id.PropertyId; import org.apache.jackrabbit.core.observation.EventStateCollectionFactory; import org.apache.jackrabbit.spi.Name; /** * Local <code>ItemStateManager</code> that isolates changes to * persistent states from other clients. */ public class LocalItemStateManager implements UpdatableItemStateManager, NodeStateListener { /** * cache of weak references to ItemState objects issued by this * ItemStateManager */ private final ItemStateCache cache; /** * Shared item state manager */ protected final SharedItemStateManager sharedStateMgr; /** * Event state collection factory. */ protected final EventStateCollectionFactory factory; /** * Flag indicating whether this item state manager is in edit mode */ private boolean editMode; /** * Change log */ private final ChangeLog changeLog = new ChangeLog(); /** * State change dispatcher. */ private final transient StateChangeDispatcher dispatcher = new StateChangeDispatcher(); /** * Creates a new <code>LocalItemStateManager</code> instance. * @param sharedStateMgr shared state manager * @param factory event state collection factory */ protected LocalItemStateManager(SharedItemStateManager sharedStateMgr, EventStateCollectionFactory factory, ItemStateCacheFactory cacheFactory) { cache = new ItemStateReferenceCache(cacheFactory); this.sharedStateMgr = sharedStateMgr; this.factory = factory; } /** * Creates a new {@code LocalItemStateManager} instance and registers it as an {@link ItemStateListener} * with the given {@link SharedItemStateManager}. * * @param sharedStateMgr the {@link SharedItemStateManager} * @param factory the {@link EventStateCollectionFactory} * @param cacheFactory the {@link ItemStateCacheFactory} * @return a new {@code LocalItemStateManager} instance */ public static LocalItemStateManager createInstance(SharedItemStateManager sharedStateMgr, EventStateCollectionFactory factory, ItemStateCacheFactory cacheFactory) { LocalItemStateManager mgr = new LocalItemStateManager(sharedStateMgr, factory, cacheFactory); sharedStateMgr.addListener(mgr); return mgr; } /** * Retrieve a node state from the parent shared state manager and * wraps it into a intermediate object that helps us handle local * modifications. * * @param id node id * @return node state * @throws NoSuchItemStateException * @throws ItemStateException */ protected NodeState getNodeState(NodeId id) throws NoSuchItemStateException, ItemStateException { // load from parent manager and wrap NodeState state = (NodeState) sharedStateMgr.getItemState(id); state = new NodeState(state, state.getStatus(), false); // put it in cache cache.cache(state); // set parent container state.setContainer(this); return state; } /** * Retrieve a property state from the parent shared state manager and * wraps it into a intermediate object that helps us handle local * modifications. * * @param id property id * @return property state * @throws NoSuchItemStateException * @throws ItemStateException */ protected PropertyState getPropertyState(PropertyId id) throws NoSuchItemStateException, ItemStateException { // load from parent manager and wrap PropertyState state = (PropertyState) sharedStateMgr.getItemState(id); state = new PropertyState(state, state.getStatus(), false); // put it in cache cache.cache(state); // set parent container state.setContainer(this); return state; } /** * Returns the change log that contains the current changes in this local * item state manager. * * @return the change log with the current changes. */ protected ChangeLog getChanges() { return changeLog; } //-----------------------------------------------------< ItemStateManager > /** * {@inheritDoc} */ public ItemState getItemState(ItemId id) throws NoSuchItemStateException, ItemStateException { // check change log ItemState state = changeLog.get(id); if (state != null) { return state; } // check cache. synchronized to ensure an entry is not created twice. synchronized (this) { state = cache.retrieve(id); if (state == null) { // regular behaviour if (id.denotesNode()) { state = getNodeState((NodeId) id); } else { state = getPropertyState((PropertyId) id); } } return state; } } /** * {@inheritDoc} */ public boolean hasItemState(ItemId id) { // check items in change log try { ItemState state = changeLog.get(id); if (state != null) { return true; } } catch (NoSuchItemStateException e) { return false; } // check cache if (cache.isCached(id)) { return true; } // regular behaviour return sharedStateMgr.hasItemState(id); } /** * {@inheritDoc} */ public NodeReferences getNodeReferences(NodeId id) throws NoSuchItemStateException, ItemStateException { // check change log NodeReferences refs = changeLog.getReferencesTo(id); if (refs != null) { return refs; } return sharedStateMgr.getNodeReferences(id); } /** * {@inheritDoc} */ public boolean hasNodeReferences(NodeId id) { // check change log if (changeLog.getReferencesTo(id) != null) { return true; } return sharedStateMgr.hasNodeReferences(id); } //--------------------------------------------< UpdatableItemStateManager > /** * {@inheritDoc} */ public synchronized void edit() throws IllegalStateException { if (editMode) { throw new IllegalStateException("Already in edit mode"); } changeLog.reset(); editMode = true; } /** * {@inheritDoc} */ public boolean inEditMode() { return editMode; } /** * {@inheritDoc} */ public NodeState createNew( NodeId id, Name nodeTypeName, NodeId parentId) throws RepositoryException { if (!editMode) { throw new RepositoryException("Not in edit mode"); } boolean nonRandomId = true; if (id == null) { id = getNodeIdFactory().newNodeId(); nonRandomId = false; } NodeState state = new NodeState( id, nodeTypeName, parentId, ItemState.STATUS_NEW, false); changeLog.added(state); state.setContainer(this); if (nonRandomId && !changeLog.deleted(id) && sharedStateMgr.hasItemState(id)) { throw new InvalidItemStateException( "Node " + id + " already exists"); } return state; } /** * Returns the local node state below the given transient one. If given * a fresh new node state, then a new local state is created and added * to the change log. * * @param transientState transient state * @return local node state * @throws RepositoryException if the local state could not be created */ public NodeState getOrCreateLocalState(NodeState transientState) throws RepositoryException { NodeState localState = (NodeState) transientState.getOverlayedState(); if (localState == null) { // The transient node state is new, create a new local state localState = new NodeState( transientState.getNodeId(), transientState.getNodeTypeName(), transientState.getParentId(), ItemState.STATUS_NEW, false); changeLog.added(localState); localState.setContainer(this); try { transientState.connect(localState); } catch (ItemStateException e) { // should never happen throw new RepositoryException(e); } } return localState; } /** * {@inheritDoc} */ public PropertyState createNew(Name propName, NodeId parentId) throws IllegalStateException { if (!editMode) { throw new IllegalStateException("Not in edit mode"); } PropertyState state = new PropertyState( new PropertyId(parentId, propName), ItemState.STATUS_NEW, false); changeLog.added(state); state.setContainer(this); return state; } /** * {@inheritDoc} */ public void store(ItemState state) throws IllegalStateException { if (!editMode) { throw new IllegalStateException("Not in edit mode"); } changeLog.modified(state); } /** * {@inheritDoc} */ public void destroy(ItemState state) throws IllegalStateException { assert state != null; if (!editMode) { throw new IllegalStateException("Not in edit mode"); } changeLog.deleted(state); } /** * {@inheritDoc} */ public void cancel() throws IllegalStateException { if (!editMode) { throw new IllegalStateException("Not in edit mode"); } changeLog.undo(sharedStateMgr); editMode = false; } /** * {@inheritDoc} */ public void update() throws ReferentialIntegrityException, StaleItemStateException, ItemStateException, IllegalStateException { if (!editMode) { throw new IllegalStateException("Not in edit mode"); } // JCR-1813: Only execute the update when there are some changes if (changeLog.hasUpdates()) { update(changeLog); changeLog.reset(); } editMode = false; } /** * End an update operation. Fetch the states and references from * the parent (shared) item manager, reconnect them to the items * collected in our (local) change log and overwrite the shared * items with our copies. * * @param changeLog change log containing local states and references * @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 in the meantime * @throws ItemStateException if an error occurs */ protected void update(ChangeLog changeLog) throws ReferentialIntegrityException, StaleItemStateException, ItemStateException { sharedStateMgr.update(changeLog, factory); changeLog.persisted(); } /** * {@inheritDoc} */ public void dispose() { sharedStateMgr.removeListener(this); // this LocalItemStateManager instance is no longer needed; // cached item states can now be safely discarded ItemState[] states = cache.retrieveAll(); for (int i = 0; i < states.length; i++) { ItemState state = states[i]; if (state != null) { dispatcher.notifyStateDiscarded(state); // let the item state know that it has been disposed state.onDisposed(); } } // clear cache cache.evictAll(); cache.dispose(); } /** * 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); } //----------------------------------------------------< ItemStateListener > /** * {@inheritDoc} * <p> * Notification handler gets called for both local states that this state manager * has created, as well as states that were created by the shared state manager * we're listening to. */ public void stateCreated(ItemState created) { ItemState local = null; if (created.getContainer() != this) { // shared state was created try { local = changeLog.get(created.getId()); if (local != null) { if (local.isNode() && local.getOverlayedState() != created) { // mid-air collision of concurrent node state creation // with same id (JCR-2272) if (local.getStatus() == ItemState.STATUS_NEW) { local.setStatus(ItemState.STATUS_UNDEFINED); // we need a state that is != NEW } } else { if (local.getOverlayedState() == created) { // underlying state has been permanently created local.pull(); local.setStatus(ItemState.STATUS_EXISTING); cache.cache(local); } } } } catch (NoSuchItemStateException e) { /* ignore */ } } else { // local state was created local = created; // just ensure that the newly created state is still cached. it can // happen during a restore operation that a state with the same id // is deleted and created (JCR-1197) if (!cache.isCached(created.getId())) { cache.cache(local); } } dispatcher.notifyStateCreated(created); } /** * {@inheritDoc} * <p> * Notification handler gets called for both local states that this state manager * has created, as well as states that were created by the shared state manager * we're listening to. */ public void stateModified(ItemState modified) { ItemState local; if (modified.getContainer() != this) { // shared state was modified local = cache.retrieve(modified.getId()); if (local != null && local.isConnected()) { // this instance represents existing state, update it local.pull(); } } else { // local state was modified local = modified; } if (local != null) { dispatcher.notifyStateModified(local); } else if (modified.isNode()) { // if the state is not ours (and is not cached) it could have // vanished from the weak-ref cache due to a gc. but there could // still be some listeners (e.g. CachingHierarchyManager) that want // to get notified. dispatcher.notifyNodeModified((NodeState) modified); } } /** * {@inheritDoc} * <p> * Notification handler gets called for both local states that this state manager * has created, as well as states that were created by the shared state manager * we're listening to. */ public void stateDestroyed(ItemState destroyed) { ItemState local = null; if (destroyed.getContainer() != this) { // shared state was destroyed local = cache.retrieve(destroyed.getId()); if (local != null && local.isConnected()) { local.setStatus(ItemState.STATUS_EXISTING_REMOVED); } } else { // local state was destroyed local = destroyed; } cache.evict(destroyed.getId()); if (local != null) { dispatcher.notifyStateDestroyed(local); } } /** * {@inheritDoc} * <p> * Notification handler gets called for both local states that this state manager * has created, as well as states that were created by the shared state manager * we're listening to. */ public void stateDiscarded(ItemState discarded) { ItemState local = null; if (discarded.getContainer() != this) { // shared state was discarded local = cache.retrieve(discarded.getId()); if (local != null && local.isConnected()) { local.setStatus(ItemState.STATUS_UNDEFINED); } } else { // local state was discarded local = discarded; } cache.evict(discarded.getId()); if (local != null) { dispatcher.notifyStateDiscarded(local); } } /** * {@inheritDoc} * <p> * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state * concerned must be a local state. */ public void nodeAdded(NodeState state, Name name, int index, NodeId id) { dispatcher.notifyNodeAdded(state, name, index, id); } /** * {@inheritDoc} * <p> * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state * concerned must be a local state. */ public void nodesReplaced(NodeState state) { dispatcher.notifyNodesReplaced(state); } /** * {@inheritDoc} * <p> * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state * concerned must be a local state. */ public void nodeModified(NodeState state) { dispatcher.notifyNodeModified(state); } /** * {@inheritDoc} * <p> * Optimization: shared state manager we're listening to does not deliver node state changes, therefore the state * concerned must be a local state. */ public void nodeRemoved(NodeState state, Name name, int index, NodeId id) { dispatcher.notifyNodeRemoved(state, name, index, id); } public NodeIdFactory getNodeIdFactory() { return sharedStateMgr.getNodeIdFactory(); } }