Package org.apache.jackrabbit.jcr2spi.hierarchy

Source Code of org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntryImpl

/*
* 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.jcr2spi.hierarchy;

import org.apache.jackrabbit.spi.Name;
import org.apache.jackrabbit.spi.Path;
import org.apache.jackrabbit.spi.NodeId;
import org.apache.jackrabbit.spi.Event;
import org.apache.jackrabbit.spi.ItemId;
import org.apache.jackrabbit.spi.QNodeDefinition;
import org.apache.jackrabbit.spi.QPropertyDefinition;
import org.apache.jackrabbit.spi.IdFactory;
import org.apache.jackrabbit.spi.PropertyId;
import org.apache.jackrabbit.spi.PathFactory;
import org.apache.jackrabbit.jcr2spi.state.NodeState;
import org.apache.jackrabbit.jcr2spi.state.ItemState;
import org.apache.jackrabbit.jcr2spi.state.ChangeLog;
import org.apache.jackrabbit.jcr2spi.state.Status;
import org.apache.jackrabbit.jcr2spi.state.PropertyState;
import org.apache.jackrabbit.jcr2spi.state.ItemStateLifeCycleListener;
import org.apache.jackrabbit.jcr2spi.util.StateUtility;
import org.apache.jackrabbit.spi.commons.name.NameConstants;
import org.apache.jackrabbit.spi.commons.name.PathBuilder;
import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
import org.apache.commons.collections.iterators.IteratorChain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.jcr.ItemExistsException;
import javax.jcr.RepositoryException;
import javax.jcr.PathNotFoundException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.InvalidItemStateException;
import java.util.Iterator;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Set;
import java.util.HashSet;
import java.util.Map;
import java.util.LinkedHashMap;

/**
* <code>NodeEntryImpl</code> implements common functionality for child
* node entry implementations.
*/
public class NodeEntryImpl extends HierarchyEntryImpl implements NodeEntry {

    private static Logger log = LoggerFactory.getLogger(NodeEntryImpl.class);

    /**
     * UniqueID identifying this NodeEntry or <code>null</code> if either
     * the underlying state has not been loaded yet or if it cannot be
     * identified with a unique ID.
     */
    private String uniqueID;

    /**
     * Insertion-ordered collection of NodeEntry objects.
     */
    private ChildNodeEntries childNodeEntries;

    /**
     * Map used to remember transiently removed or moved childNodeEntries, that
     * must not be retrieved from the persistent storage.
     */
    private ChildNodeAttic childNodeAttic;

    /**
     * Map of properties.<br>
     * Key = {@link Name} of property,<br>
     * Value = {@link PropertyEntry}.
     */
    private final ChildPropertyEntries properties;

    /**
     * Map of properties which are deleted and have been re-created as transient
     * property with the same name.
     */
    private final Map propertiesInAttic;

    /**
     * Upon transient 'move' ('rename') or 'reorder' of SNSs this
     * <code>NodeEntry</code> remembers the original parent, name and index
     * for later revert as well as for the creation of the
     * {@link #getWorkspaceId() workspace id}. Finally the revertInfo is
     * used to find the target of an <code>Event</code> indicating external
     * modification.
     *
     * @see #refresh(Event)
     */
    private RevertInfo revertInfo;

    /**
     * Creates a new <code>NodeEntryImpl</code>
     *
     * @param parent    the <code>NodeEntry</code> that owns this child item
     *                  reference.
     * @param name      the name of the child node.
     * @param factory   the entry factory.
     */
    private NodeEntryImpl(NodeEntryImpl parent, Name name, String uniqueID, EntryFactory factory) {
        super(parent, name, factory);
        this.uniqueID = uniqueID; // NOTE: don't use setUniqueID (for mod only)

        properties = new ChildPropertyEntriesImpl(this, factory);

        propertiesInAttic = new HashMap();
        childNodeAttic = new ChildNodeAttic();

        factory.notifyEntryCreated(this);
    }

    /**
     *
     * @return
     */
    static NodeEntry createRootEntry(EntryFactory factory) {
        return new NodeEntryImpl(null, NameConstants.ROOT, null, factory);
    }

    /**
     *
     * @param parent
     * @param name
     * @param uniqueId
     * @param factory
     * @return
     */
    static NodeEntry createNodeEntry(NodeEntryImpl parent, Name name, String uniqueId, EntryFactory factory) {
        return new NodeEntryImpl(parent, name, uniqueId, factory);
    }

    //-----------------------------------------------------< HierarchyEntry >---
    /**
     * Returns true.
     *
     * @inheritDoc
     * @see HierarchyEntry#denotesNode()
     */
    public boolean denotesNode() {
        return true;
    }

    /**
     * @inheritDoc
     * @see HierarchyEntry#invalidate(boolean)
     */
    public void invalidate(boolean recursive) {
        if (recursive) {
            // invalidate all child entries including properties present in the
            // attic (removed props shadowed by a new property with the same name).
            for (Iterator it = getAllChildEntries(true); it.hasNext();) {
                HierarchyEntry ce = (HierarchyEntry) it.next();
                ce.invalidate(recursive);
            }
        }
        // invalidate 'childNodeEntries'
        if (getStatus() != Status.NEW && childNodeEntries != null) {
            childNodeEntries.setStatus(ChildNodeEntries.STATUS_INVALIDATED);
        }
        // ... and invalidate the resolved state (if available)
        super.invalidate(recursive);
    }

    /**
     * If 'recursive' is true, the complete hierarchy below this entry is
     * traversed and reloaded. Otherwise only this entry and the direct
     * decendants are reloaded.
     *
     * @see HierarchyEntry#reload(boolean, boolean)
     */
    public void reload(boolean keepChanges, boolean recursive) {
        // reload this entry
        super.reload(keepChanges, recursive);

        // reload all children unless 'recursive' is false and the reload above
        // did not cause this entry to be removed -> therefore check status.
        if (recursive && !Status.isTerminal(getStatus())) {
            // recursivly reload all entries including props that are in the attic.
            for (Iterator it = getAllChildEntries(true); it.hasNext();) {
                HierarchyEntry ce = (HierarchyEntry) it.next();
                ce.reload(keepChanges, recursive);
            }
        }
    }

    /**
     * Calls {@link HierarchyEntryImpl#revert()} and moves all properties from the
     * attic back into th properties map. If this HierarchyEntry has been
     * transiently moved, it is in addition moved back to its old parent.
     * Similarly reordering of child node entries is reverted.
     *
     * @inheritDoc
     * @see HierarchyEntry#revert()
     */
    public void revert() throws RepositoryException {
        // move all properties from attic back to properties map
        if (!propertiesInAttic.isEmpty()) {
            properties.addAll(propertiesInAttic.values());
            propertiesInAttic.clear();
        }

        revertTransientChanges();

        // now make sure the attached state is reverted to the original state
        super.revert();
    }

    /**
     * @see HierarchyEntry#transientRemove()
     */
    public void transientRemove() throws RepositoryException {
        for (Iterator it = getAllChildEntries(false); it.hasNext();) {
            HierarchyEntry ce = (HierarchyEntry) it.next();
            ce.transientRemove();
        }

        if (!propertiesInAttic.isEmpty()) {
            // move all properties from attic back to properties map
            properties.addAll(propertiesInAttic.values());
            propertiesInAttic.clear();
        }

        // execute for this entry as well
        super.transientRemove();
    }

    /**
     * @see HierarchyEntry#remove()
     */
    public void remove() {
        removeEntry(this);
        if (getStatus() != Status.STALE_DESTROYED && parent.childNodeEntries != null) {
            NodeEntry removed = parent.childNodeEntries.remove(this);
            if (removed == null) {
                // try attic
                parent.childNodeAttic.remove(this);
            }
        }

        // TODO: deal with childNodeAttic
        // now traverse all child-entries and mark the attached states removed
        // without removing the child-entries themselves. this is not required
        // since this (i.e. the parent is removed as well).
        for (Iterator it = getAllChildEntries(true); it.hasNext();) {
            HierarchyEntryImpl ce = (HierarchyEntryImpl) it.next();
            removeEntry(ce);
        }
    }

    /**
     * If the underlying state is available and transiently modified, new or
     * stale, it gets added to the changeLog. Subsequently this call is repeated
     * recursively to collect all child states that meet the condition,
     * including those property states that have been moved to the attic.
     *
     * @inheritDoc
     * @see HierarchyEntry#collectStates(ChangeLog, boolean)
     */
    public synchronized void collectStates(ChangeLog changeLog, boolean throwOnStale) throws InvalidItemStateException {
        super.collectStates(changeLog, throwOnStale);

        // collect transient child states including properties in attic.
        for (Iterator it = getAllChildEntries(true); it.hasNext();) {
            HierarchyEntry ce = (HierarchyEntry) it.next();
            ce.collectStates(changeLog, throwOnStale);
        }
    }

    //----------------------------------------------------------< NodeEntry >---
    /**
     * @inheritDoc
     * @see NodeEntry#getId()
     */
    public NodeId getId() {
        IdFactory idFactory = factory.getIdFactory();
        if (uniqueID != null) {
            return idFactory.createNodeId(uniqueID);
        } else {
            PathFactory pf = factory.getPathFactory();
            if (parent == null) {
                // root node
                return idFactory.createNodeId((String) null, pf.getRootPath());
            } else {
                Path p = pf.create(getName(), getIndex());
                return idFactory.createNodeId(parent.getId(), p);
            }
        }
    }

    /**
     * @see NodeEntry#getWorkspaceId()
     */
    public NodeId getWorkspaceId() {
        IdFactory idFactory = factory.getIdFactory();
        if (uniqueID != null || parent == null) {
            // uniqueID and root-node -> internal id is always the same as getId().
            return getId();
        } else {
            PathFactory pf = factory.getPathFactory();
            NodeId parentId = (revertInfo != null) ? revertInfo.oldParent.getWorkspaceId() : parent.getWorkspaceId();
            return idFactory.createNodeId(parentId, pf.create(getWorkspaceName(), getWorkspaceIndex()));
        }
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getUniqueID()
     */
    public String getUniqueID() {
        return uniqueID;
    }

    /**
     * @inheritDoc
     * @see NodeEntry#setUniqueID(String)
     */
    public void setUniqueID(String uniqueID) {
        String old = this.uniqueID;
        boolean mod = (uniqueID == null) ? old != null : !uniqueID.equals(old);
        if (mod) {
            this.uniqueID = uniqueID;
            factory.notifyIdChange(this, old);
        }
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getIndex()
     */
    public int getIndex() {
        if (parent == null) {
            // the root state may never have siblings
            return Path.INDEX_DEFAULT;
        }

        NodeState state = (NodeState) internalGetItemState();
        try {
            if (state == null || !state.hasDefinition() || state.getDefinition().allowsSameNameSiblings()) {
                return parent.getChildIndex(this);
            } else {
                return Path.INDEX_DEFAULT;
            }
        } catch (RepositoryException e) {
            log.error("Error while building Index. ", e.getMessage());
            return Path.INDEX_UNDEFINED;
        }
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getNodeState()
     */
    public NodeState getNodeState() throws ItemNotFoundException, RepositoryException {
        return (NodeState) getItemState();
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getDeepEntry(Path)
     */
    public HierarchyEntry getDeepEntry(Path path) throws PathNotFoundException, RepositoryException {
        NodeEntryImpl entry = this;
        Path.Element[] elems = path.getElements();
        for (int i = 0; i < elems.length; i++) {
            Path.Element elem = (Path.Element) elems[i];
            // check for root element
            if (elem.denotesRoot()) {
                if (getParent() != null) {
                    throw new RepositoryException("NodeEntry out of 'hierarchy'" + path.toString());
                }
                continue;
            }

            int index = elem.getNormalizedIndex();
            Name name = elem.getName();

            // first try to resolve to known node or property entry
            NodeEntry cne = (entry.childNodeEntries == null) ? null : entry.getNodeEntry(name, index, false);
            if (cne != null) {
                entry = (NodeEntryImpl) cne;
            } else if (index == Path.INDEX_DEFAULT && i == path.getLength() - 1 && entry.properties.contains(name)) {
                // property must not have index && must be final path element
                return entry.properties.get(name);
            } else {
                // no valid entry
                // -> check for moved child entry in node-attic
                // -> check if child points to a removed/moved sns
                if (entry.childNodeEntries != null) {
                    List siblings = entry.childNodeEntries.get(name);
                    if (entry.containsAtticChild(siblings, name, index)) {
                        throw new PathNotFoundException(path.toString());
                    }
                }
               /*
                * Unknown entry (not-existing or not yet loaded):
                * Skip all intermediate entries and directly try to load the ItemState
                * (including building the itermediate entries. If that fails
                * ItemNotFoundException is thrown.
                *
                * Since 'path' might be ambigous (Node or Property):
                * 1) first try Node
                * 2) if the NameElement does not have SNS-index => try Property
                * 3) else throw
                */
                PathBuilder pb = new PathBuilder(factory.getPathFactory());
                for (int j = i; j < elems.length; j++) {
                    pb.addLast(elems[j]);
                }
                Path remainingPath = pb.getPath();

                NodeId parentId = entry.getId();
                IdFactory idFactory = factory.getIdFactory();

                NodeId nodeId = idFactory.createNodeId(parentId, remainingPath);
                NodeEntry ne = entry.loadNodeEntry(nodeId);
                if (ne != null) {
                    return ne;
                } else {
                    if (index != Path.INDEX_DEFAULT) {
                        throw new PathNotFoundException(path.toString());
                    }
                    // maybe a property entry exists
                    parentId = (remainingPath.getLength() == 1) ? parentId : idFactory.createNodeId(parentId, remainingPath.getAncestor(1));
                    PropertyId propId = idFactory.createPropertyId(parentId, remainingPath.getNameElement().getName());
                    PropertyEntry pe = entry.loadPropertyEntry(propId);
                    if (pe != null) {
                        return pe;
                    } else {
                        throw new PathNotFoundException(path.toString());
                    }
                }
            }
        }
        return entry;
    }

    /**
     * @see NodeEntry#lookupDeepEntry(Path)
     */
    public HierarchyEntry lookupDeepEntry(Path workspacePath) {
        NodeEntryImpl entry = this;
        for (int i = 0; i < workspacePath.getLength(); i++) {
            Path.Element elem = workspacePath.getElements()[i];
            // check for root element
            if (elem.denotesRoot()) {
                if (getParent() != null) {
                    log.warn("NodeEntry out of 'hierarchy'" + workspacePath.toString());
                    return null;
                }
                continue;
            }

            int index = elem.getNormalizedIndex();
            Name childName = elem.getName();

            // first try to resolve node
            NodeEntry cne = entry.lookupNodeEntry(null, childName, index);
            if (cne != null) {
                entry = (NodeEntryImpl) cne;
            } else if (index == Path.INDEX_DEFAULT && i == workspacePath.getLength() - 1) {
                // property must not have index && must be final path element
                return entry.lookupPropertyEntry(childName);
            } else {
                return null;
            }
        }
        return entry;
    }

    /**
     * @inheritDoc
     * @see NodeEntry#hasNodeEntry(Name)
     */
    public synchronized boolean hasNodeEntry(Name nodeName) {
        try {
            List namedEntries = childNodeEntries().get(nodeName);
            if (namedEntries.isEmpty()) {
                return false;
            } else {
                return EntryValidation.containsValidNodeEntry(namedEntries.iterator());
            }
        } catch (RepositoryException e) {
            log.debug("Unable to determine if a child node with name " + nodeName + " exists.");
            return false;
        }
    }

    /**
     * @inheritDoc
     * @see NodeEntry#hasNodeEntry(Name, int)
     */
    public synchronized boolean hasNodeEntry(Name nodeName, int index) {
        try {
            return getNodeEntry(nodeName, index) != null;
        } catch (RepositoryException e) {
            log.debug("Unable to determine if a child node with name " + nodeName + " exists.");
            return false;
        }
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getNodeEntry(Name, int)
     */
    public synchronized NodeEntry getNodeEntry(Name nodeName, int index) throws RepositoryException {
        return getNodeEntry(nodeName, index, false);
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getNodeEntry(Name, int, boolean)
     */
    public NodeEntry getNodeEntry(Name nodeName, int index, boolean loadIfNotFound) throws RepositoryException {
        // TODO: avoid loading the child-infos if childNodeEntries == null
        List entries = childNodeEntries().get(nodeName);
        NodeEntry cne = null;
        if (entries.size() >= index) {
            // position of entry might differ from index-1 if a SNS with lower
            // index has been transiently removed.
            for (int i = index-1; i < entries.size() && cne == null; i++) {
                NodeEntry ne = (NodeEntry) entries.get(i);
                if (EntryValidation.isValidNodeEntry(ne)) {
                    cne = ne;
                }
            }
        } else if (loadIfNotFound
                && !containsAtticChild(entries, nodeName, index)
                && Status.NEW != getStatus()) {

            PathFactory pf = factory.getPathFactory();
            NodeId cId = factory.getIdFactory().createNodeId(getId(), pf.create(nodeName, index));
            cne = loadNodeEntry(cId);
        }
        return cne;
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getNodeEntries()
     */
    public synchronized Iterator getNodeEntries() throws RepositoryException {
        Collection entries = new ArrayList();
        for (Iterator it = childNodeEntries().iterator(); it.hasNext();) {
            NodeEntry entry = (NodeEntry) it.next();
            if (EntryValidation.isValidNodeEntry(entry)) {
                entries.add(entry);
            }
        }
        return new RangeIteratorAdapter(Collections.unmodifiableCollection(entries));
    }

    /**
     * @see NodeEntry#getNodeEntries(Name)
     */
    public synchronized List getNodeEntries(Name nodeName) throws RepositoryException {
        List namedEntries = childNodeEntries().get(nodeName);
        if (namedEntries.isEmpty()) {
            return Collections.EMPTY_LIST;
        } else {
            List entries = new ArrayList();
            // get array of the list, since during validation the childNodeEntries
            // may be modified if upon NodeEntry.getItemState the entry gets removed.
            Object[] arr = namedEntries.toArray();
            for (int i = 0; i < arr.length; i++) {
                NodeEntry cne = (NodeEntry) arr[i];
                if (EntryValidation.isValidNodeEntry(cne)) {
                    entries.add(cne);
                }
            }
            return Collections.unmodifiableList(entries);
        }
    }

    /**
     * @inheritDoc
     * @see NodeEntry#addNodeEntry(Name, String, int)
     */
    public NodeEntry addNodeEntry(Name nodeName, String uniqueID, int index) throws RepositoryException {
        return internalAddNodeEntry(nodeName, uniqueID, index, childNodeEntries());
    }

    /**
     * @inheritDoc
     * @see NodeEntry#addNewNodeEntry(Name, String, Name, QNodeDefinition)
     */
    public NodeState addNewNodeEntry(Name nodeName, String uniqueID,
                                     Name primaryNodeType, QNodeDefinition definition) throws RepositoryException {
        NodeEntry entry = internalAddNodeEntry(nodeName, uniqueID, Path.INDEX_UNDEFINED, childNodeEntries());
        NodeState state = factory.getItemStateFactory().createNewNodeState(entry, primaryNodeType, definition);
        if (!entry.isAvailable()) {
            entry.setItemState(state);
        }
        return state;
    }

    /**
     *
     * @param nodeName
     * @param uniqueID
     * @param index
     * @param childEntries
     * @return
     */
    private NodeEntry internalAddNodeEntry(Name nodeName, String uniqueID,
                                           int index, ChildNodeEntries childEntries) {
        NodeEntry entry = factory.createNodeEntry(this, nodeName, uniqueID);
        childEntries.add(entry, index);
        return entry;
    }

    /**
     * @inheritDoc
     * @see NodeEntry#hasPropertyEntry(Name)
     */
    public synchronized boolean hasPropertyEntry(Name propName) {
        PropertyEntry entry = properties.get(propName);
        return EntryValidation.isValidPropertyEntry(entry);
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getPropertyEntry(Name)
     */
    public synchronized PropertyEntry getPropertyEntry(Name propName) {
        PropertyEntry entry = properties.get(propName);
        if (EntryValidation.isValidPropertyEntry(entry)) {
            return entry;
        } else {
            return null;
        }
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getPropertyEntry(Name, boolean)
     */
    public PropertyEntry getPropertyEntry(Name propName, boolean loadIfNotFound) throws RepositoryException {
        PropertyEntry entry = properties.get(propName);
        if (entry == null && loadIfNotFound && Status.NEW != getStatus()) {
            PropertyId propId = factory.getIdFactory().createPropertyId(getId(), propName);
            entry = loadPropertyEntry(propId);
        } else if (!EntryValidation.isValidPropertyEntry(entry)) {
            entry = null;
        }
        return entry;
    }

    /**
     * @inheritDoc
     * @see NodeEntry#getPropertyEntries()
     */
    public synchronized Iterator getPropertyEntries() {
        Collection props;
        if (getStatus() == Status.EXISTING_MODIFIED) {
            // filter out removed properties
            props = new ArrayList();
            // use array since upon validation the entry might be removed.
            Object[] arr = properties.getPropertyEntries().toArray();
            for (int i = 0; i < arr.length; i++) {
                PropertyEntry propEntry = (PropertyEntry) arr[i];
                if (EntryValidation.isValidPropertyEntry(propEntry)) {
                    props.add(propEntry);
                }
            }
        } else {
            // no need to filter out properties, there are no removed properties
            props = properties.getPropertyEntries();
        }
        return new RangeIteratorAdapter(Collections.unmodifiableCollection(props));
    }

    /**
     * @inheritDoc
     * @see NodeEntry#addPropertyEntry(Name)
     */
    public PropertyEntry addPropertyEntry(Name propName) throws ItemExistsException {
        // TODO: check for existing prop.
        return internalAddPropertyEntry(propName, true);
    }

    /**
     * Internal method that adds a PropertyEntry without checking of that entry
     * exists.
     *
     * @param propName
     * @param notifySpecial
     * @return
     */
    private PropertyEntry internalAddPropertyEntry(Name propName, boolean notifySpecial) {
        PropertyEntry entry = factory.createPropertyEntry(this, propName);
        properties.add(entry);

        // if property-name is jcr:uuid or jcr:mixin this affects this entry
        // and the attached nodeState.
        if (notifySpecial && StateUtility.isUuidOrMixin(propName)) {
            notifyUUIDorMIXINModified(entry);
        }
        return entry;
    }

    /**
     * @inheritDoc
     * @see NodeEntry#addPropertyEntries(Collection)
     */
    public void addPropertyEntries(Collection propNames) throws ItemExistsException, RepositoryException {
        Set diff = new HashSet();
        diff.addAll(properties.getPropertyNames());
        boolean containsExtra = diff.removeAll(propNames);

        // add all entries that are missing
        for (Iterator it = propNames.iterator(); it.hasNext();) {
            Name propName = (Name) it.next();
            if (!properties.contains(propName)) {
                // TODO: check again.
                // addPropertyEntries is used by WorkspaceItemStateFactory upon
                // creating a NodeState, in which case the uuid/mixins are set
                // anyway and not need exists to explicitely load the corresponding
                // property state in order to retrieve the values.
                internalAddPropertyEntry(propName, false);
            }
        }

        // if this entry has not yet been resolved or if it is 'invalidated'
        // all property entries, that are not contained within the specified
        // collection of property names are removed from this NodeEntry.
        ItemState state = internalGetItemState();
        if (containsExtra && (state == null || state.getStatus() == Status.INVALIDATED)) {
            for (Iterator it = diff.iterator(); it.hasNext();) {
                Name propName = (Name) it.next();
                PropertyEntry pEntry = properties.get(propName);
                if (pEntry != null) {
                    pEntry.remove();
                }
            }
        }
    }

    /**
     * @inheritDoc
     * @see NodeEntry#addNewPropertyEntry(Name, QPropertyDefinition)
     */
    public PropertyState addNewPropertyEntry(Name propName, QPropertyDefinition definition)
            throws ItemExistsException, RepositoryException {
        // check for an existing property
        PropertyEntry existing = properties.get(propName);
        if (existing != null) {
            try {
                PropertyState existingState = existing.getPropertyState();
                int status = existingState.getStatus();

                if (Status.isTerminal(status)) {
                    // an old property-entry that is not valid any more
                    properties.remove(propName);
                } else if (status == Status.EXISTING_REMOVED) {
                    // transiently removed -> move it to the attic
                    propertiesInAttic.put(propName, existing);
                } else {
                    // existing is still existing -> cannot add same-named property
                    throw new ItemExistsException(propName.toString());
                }
            } catch (ItemNotFoundException e) {
                // entry does not exist on the persistent layer
                // -> therefore remove from properties map
                properties.remove(propName);
            } catch (RepositoryException e) {
                // some other error -> remove from properties map
                properties.remove(propName);
            }
        }

        // add the property entry
        PropertyEntry entry = factory.createPropertyEntry(this, propName);
        properties.add(entry);

        PropertyState state = factory.getItemStateFactory().createNewPropertyState(entry, definition);
        if (!entry.isAvailable()) {
            entry.setItemState(state);
        }

        return state;
    }

    /**
     * @param propName
     */
    void internalRemovePropertyEntry(Name propName) {
        if (!properties.remove(propName)) {
            propertiesInAttic.remove(propName);
        }
        // special properties
        if (StateUtility.isUuidOrMixin(propName)) {
            notifyUUIDorMIXINRemoved(propName);
        }
    }

    /**
     * @inheritDoc
     * @see NodeEntry#orderBefore(NodeEntry)
     */
    public void orderBefore(NodeEntry beforeEntry) throws RepositoryException {
        if (Status.NEW == getStatus()) {
            // new states get remove upon revert
            parent.childNodeEntries().reorder(this, beforeEntry);
        } else {
            createSiblingRevertInfos();
            parent.createRevertInfo();
            // now reorder child entries on parent
            NodeEntry previousBefore = parent.childNodeEntries().reorder(this, beforeEntry);
            parent.revertInfo.reordered(this, previousBefore);
        }
    }

   /**
    * @see NodeEntry#move(Name, NodeEntry, boolean)
    */
   public NodeEntry move(Name newName, NodeEntry newParent, boolean transientMove) throws RepositoryException {
       if (parent == null) {
           // the root may never be moved
           throw new RepositoryException("Root cannot be moved.");
       }

       // for existing nodeEntry that are 'moved' for the first time, the
       // original data must be stored and this entry is moved to the attic.
       if (transientMove && !isTransientlyMoved() && Status.NEW != getStatus()) {
           createSiblingRevertInfos();
           createRevertInfo();
           parent.childNodeAttic.add(this);
       }

       NodeEntryImpl entry = (NodeEntryImpl) parent.childNodeEntries().remove(this);
       if (entry != this) {
           // should never occur
           String msg = "Internal error. Attempt to move NodeEntry (" + getName() + ") which is not connected to its parent.";
           log.error(msg);
           throw new RepositoryException(msg);
       }
       // set name and parent to new values
       parent = (NodeEntryImpl) newParent;
       name = newName;
       // register entry with its new parent
       parent.childNodeEntries().add(this);
       return this;
   }

    /**
     * @see NodeEntry#isTransientlyMoved()
     */
    public boolean isTransientlyMoved() {
        return revertInfo != null && revertInfo.isMoved();
    }

    /**
     * @param childEvent
     * @see NodeEntry#refresh(Event)
     */
    public void refresh(Event childEvent) {
        Name eventName = childEvent.getPath().getNameElement().getName();
        switch (childEvent.getType()) {
            case Event.NODE_ADDED:
                if (childNodeEntries == null) {
                    // childNodeEntries not yet loaded -> ignore
                    return;
                }

                int index = childEvent.getPath().getNameElement().getNormalizedIndex();
                String uniqueChildID = null;
                if (childEvent.getItemId().getPath() == null) {
                    uniqueChildID = childEvent.getItemId().getUniqueID();
                }

                // TODO: TOBEFIXED for SNSs
                // first check if no matching child entry exists.
                NodeEntry cne;
                if (uniqueChildID != null) {
                    cne = childNodeEntries.get(eventName, uniqueChildID);
                    if (cne == null) {
                        // entry may exist but without having uniqueID resolved
                        cne = childNodeEntries.get(eventName, index);
                    }
                } else {
                    cne = childNodeEntries.get(eventName, index);
                }
                if (cne == null) {
                    internalAddNodeEntry(eventName, uniqueChildID, index, childNodeEntries);
                } else {
                    // child already exists -> deal with NEW entries, that were
                    // added by some other session.
                    // TODO: TOBEFIXED
                }
                break;

            case Event.PROPERTY_ADDED:
                // create a new property reference if it has not been
                // added by some earlier 'add' event
                HierarchyEntry child = lookupEntry(childEvent.getItemId(), childEvent.getPath());
                if (child == null) {
                    internalAddPropertyEntry(eventName, true);
                } else {
                    child.reload(false, true);
                }
                break;

            case Event.NODE_REMOVED:
            case Event.PROPERTY_REMOVED:
                child = lookupEntry(childEvent.getItemId(), childEvent.getPath());
                if (child != null) {
                    child.remove();
                } // else: child-Entry has not been loaded yet -> ignore
                break;

            case Event.PROPERTY_CHANGED:
                child = lookupEntry(childEvent.getItemId(), childEvent.getPath());
                if (child == null) {
                    // prop-Entry has not been loaded yet -> add propEntry
                    internalAddPropertyEntry(eventName, true);
                } else if (child.isAvailable()) {
                    // Reload data from server and try to merge them with the
                    // current session-state. if the latter is transiently
                    // modified and merge fails it must be marked STALE afterwards.
                    child.reload(false, false);
                    // special cases: jcr:uuid and jcr:mixinTypes affect the parent
                    // (i.e. this NodeEntry) since both props are protected
                    if (StateUtility.isUuidOrMixin(eventName)) {
                        notifyUUIDorMIXINModified((PropertyEntry) child);
                    }
                } // else: existing entry but state not yet built -> ignore event
                break;
            default:
                // ILLEGAL
                throw new IllegalArgumentException("Illegal event type " + childEvent.getType() + " for NodeState.");
        }
    }
    //-------------------------------------------------< HierarchyEntryImpl >---
    /**
     * @inheritDoc
     * @see HierarchyEntryImpl#doResolve()
     * <p/>
     * Returns a <code>NodeState</code>.
     */
    ItemState doResolve() throws ItemNotFoundException, RepositoryException {
        return factory.getItemStateFactory().createNodeState(getWorkspaceId(), this);
    }

    /**
     * @see HierarchyEntryImpl#buildPath(boolean)
     */
    Path buildPath(boolean wspPath) throws RepositoryException {
        PathFactory pf = factory.getPathFactory();
        // shortcut for root state
        if (parent == null) {
            return pf.getRootPath();
        }
        // build path otherwise
        PathBuilder builder = new PathBuilder(pf);
        buildPath(builder, this, wspPath);
        return builder.getPath();
    }

    /**
     * Adds the path element of an item id to the path currently being built.
     * On exit, <code>builder</code> contains the path of this entry.
     *
     * @param builder
     * @param nEntry NodeEntryImpl of the state the path should be built for.
     * @param wspPath true if the workspace path should be built
     */
    private static void buildPath(PathBuilder builder, NodeEntryImpl nEntry, boolean wspPath) throws RepositoryException {
        NodeEntryImpl parentEntry = (wspPath && nEntry.revertInfo != null) ? nEntry.revertInfo.oldParent : nEntry.parent;
        // shortcut for root state
        if (parentEntry == null) {
            builder.addRoot();
            return;
        }

        // recursively build path of parent
        buildPath(builder, parentEntry, wspPath);

        int index = (wspPath) ? nEntry.getWorkspaceIndex() : nEntry.getIndex();
        Name name = (wspPath) ? nEntry.getWorkspaceName() : nEntry.getName();
        // add to path
        if (index == Path.INDEX_UNDEFINED) {
            throw new RepositoryException("Invalid index " + index + " with nodeEntry " + nEntry);
        }

        // TODO: check again. special treatment for default index for consistency with PathFormat.parse
        builder.addLast(name, index);
    }

    //-----------------------------------------------< private || protected >---
    /**
     * @throws IllegalArgumentException if <code>this</code> is not the parent
     * of the given <code>ItemState</code>.
     */
    synchronized void revertPropertyRemoval(PropertyEntry propertyEntry) {
        if (propertyEntry.getParent() != this) {
            throw new IllegalArgumentException("Internal error: Parent mismatch.");
        }
        Name propName = propertyEntry.getName();
        if (propertiesInAttic.containsKey(propName)) {
            properties.add((PropertyEntry) propertiesInAttic.remove(propName));
        } // else: propEntry has never been moved to the attic (see 'addPropertyEntry')
    }

    /**
     *
     * @param oldName
     * @param oldIndex
     * @return
     */
    boolean matches(Name oldName, int oldIndex) {
        return getWorkspaceName().equals(oldName) && getWorkspaceIndex() == oldIndex;
    }

    /**
     *
     * @param oldName
     * @return
     */
    boolean matches(Name oldName) {
        return getWorkspaceName().equals(oldName);
    }


    private Name getWorkspaceName() {
        if (revertInfo != null) {
            return revertInfo.oldName;
        } else {
            return getName();
        }
    }

    private int getWorkspaceIndex() {
        if (revertInfo != null) {
            return revertInfo.oldIndex;
        } else {
            return getIndex();
        }
    }

    /**
     *
     * @param childId
     * @return
     */
    private NodeEntry loadNodeEntry(NodeId childId) throws RepositoryException {
        try {
            NodeState state = factory.getItemStateFactory().createDeepNodeState(childId, this);
            return state.getNodeEntry();
        } catch (ItemNotFoundException e) {
            return null;
        }
    }

    /**
     *
     * @param childId
     * @return
     * @throws ItemNotFoundException
     * @throws RepositoryException
     */
    private PropertyEntry loadPropertyEntry(PropertyId childId) throws RepositoryException {
        try {
            PropertyState state = factory.getItemStateFactory().createDeepPropertyState(childId, this);
            return (PropertyEntry) state.getHierarchyEntry();
        } catch (ItemNotFoundException e) {
            return null;
        }
    }

    /**
     * Searches the child-entries of this NodeEntry for a matching child.
     * Since {@link #refresh(Event)} must always be called on the parent
     * NodeEntry, there is no need to check if a given event id would point
     * to this NodeEntry itself.
     *
     * @param eventId
     * @param eventPath
     * @return
     */
    private HierarchyEntry lookupEntry(ItemId eventId, Path eventPath) {
        Name childName = eventPath.getNameElement().getName();
        HierarchyEntry child;
        if (eventId.denotesNode()) {
            String uniqueChildID = (eventId.getPath() == null) ? eventId.getUniqueID() : null;
            int index = eventPath.getNameElement().getNormalizedIndex();
            child = lookupNodeEntry(uniqueChildID, childName, index);
        } else {
            child = lookupPropertyEntry(childName);
        }
        // a NEW hierarchyEntry may never be affected by an external modification
        // -> return null.
        return (child == null || child.getStatus() == Status.NEW) ? null : child;
    }

    private NodeEntry lookupNodeEntry(String uniqueChildId, Name childName, int index) {
        NodeEntry child = null;
        if (uniqueChildId != null) {
            child = childNodeAttic.get(uniqueChildId);
            if (child == null && childNodeEntries != null) {
                child = childNodeEntries.get(childName, uniqueChildId);
            }
        }
        if (child == null) {
            child = childNodeAttic.get(childName, index);
            if (child == null && childNodeEntries != null) {
                child = childNodeEntries.get(childName, index);
            }
        }
        return child;
    }

    private PropertyEntry lookupPropertyEntry(Name childName) {
        // for external prop-removal the attic must be consulted first
        // in order not access a NEW prop shadowing a transiently removed
        // property with the same name.
        PropertyEntry child = (PropertyEntry) propertiesInAttic.get(childName);
        if (child == null) {
            child = properties.get(childName);
        }
        return child;
    }

    /**
     * Deals with modified jcr:uuid and jcr:mixinTypes property.
     * See {@link #notifyUUIDorMIXINRemoved(Name)}
     *
     * @param child
     */
    private void notifyUUIDorMIXINModified(PropertyEntry child) {
        try {
            if (NameConstants.JCR_UUID.equals(child.getName())) {
                PropertyState ps = child.getPropertyState();
                setUniqueID(ps.getValue().getString());
            } else if (NameConstants.JCR_MIXINTYPES.equals(child.getName())) {
                NodeState state = (NodeState) internalGetItemState();
                if (state != null) {
                    PropertyState ps = child.getPropertyState();
                    state.setMixinTypeNames(StateUtility.getMixinNames(ps));
                } // nodestate not yet loaded -> ignore change
            }
        } catch (ItemNotFoundException e) {
            log.debug("Property with name " + child.getName() + " does not exist (anymore)");
        } catch (RepositoryException e) {
            log.debug("Unable to access child property " + child.getName(), e.getMessage());
        }
    }

    /**
     * Deals with removed jcr:uuid and jcr:mixinTypes property.
     * See {@link #notifyUUIDorMIXINModified(PropertyEntry)}
     *
     * @param propName
     */
    private void notifyUUIDorMIXINRemoved(Name propName) {
        if (NameConstants.JCR_UUID.equals(propName)) {
            setUniqueID(null);
        } else if (NameConstants.JCR_MIXINTYPES.equals(propName)) {
            NodeState state = (NodeState) internalGetItemState();
            if (state != null) {
                state.setMixinTypeNames(Name.EMPTY_ARRAY);
            }
        }
    }

    /**
     * @return The <code>ChildNodeEntries</code> defined for this
     * <code>NodeEntry</code>. Please note, that this method never returns
     * <code>null</code>, since the child node entries are loaded/reloaded
     * in case they have not been loaded yet.
     */
    private ChildNodeEntries childNodeEntries() throws InvalidItemStateException, RepositoryException {
        try {
            if (childNodeEntries == null) {
                childNodeEntries = new ChildNodeEntriesImpl(this, factory);
            } else if (childNodeEntries.getStatus() == ChildNodeEntries.STATUS_INVALIDATED) {
                childNodeEntries.reload();
            }
        } catch (ItemNotFoundException e) {
            log.debug("NodeEntry does not exist (anymore) -> remove.");
            remove();
            throw new InvalidItemStateException(e);
        }
        return childNodeEntries;
    }

    /**
     * Returns an Iterator over all children entries, that currently are loaded
     * with this NodeEntry. NOTE, that if the childNodeEntries have not been
     * loaded yet, no attempt is made to do so.
     *
     * @param includeAttic
     * @return
     */
    private Iterator getAllChildEntries(boolean includeAttic) {
        IteratorChain chain = new IteratorChain();
        // attic
        if (includeAttic) {
            Collection attic = propertiesInAttic.values();
            chain.addIterator(new ArrayList(attic).iterator());
        }
        // add props
        synchronized (properties) {
            Collection props = properties.getPropertyEntries();
            chain.addIterator(props.iterator());
        }
        // add childNodeEntries
        if (childNodeEntries != null) {
            chain.addIterator(childNodeEntries.iterator());
        }
        return chain;
    }

    /**
     * Returns the index of the given <code>NodeEntry</code>.
     *
     * @param cne  the <code>NodeEntry</code> instance.
     * @return the index of the child node entry.
     * @throws ItemNotFoundException if the given entry isn't a valid child of
     * this <code>NodeEntry</code>.
     */
    private int getChildIndex(NodeEntry cne) throws ItemNotFoundException, RepositoryException {
        List sns = childNodeEntries().get(cne.getName());
        // index is one based
        int index = Path.INDEX_DEFAULT;
        for (Iterator it = sns.iterator(); it.hasNext(); ) {
            NodeEntry entry = (NodeEntry) it.next();
            if (entry == cne) {
                return index;
            }
            // skip entries that belong to removed or invalid states.
            // NOTE, that in this case the nodestate must be available from the cne.
            if (EntryValidation.isValidNodeEntry(entry)) {
                index++;
            }
        }
        // not found (should not occur)
        throw new ItemNotFoundException("No valid child entry for NodeEntry " + cne);
    }

    /**
     * Returns true if the attic contains a matching child entry or if any of
     * the remaining child entries present in the siblings list has been modified
     * in a way that its original index is equal to the given child index.
     *
     * @param siblings
     * @param childName
     * @param childIndex
     * @return
     */
    private boolean containsAtticChild(List siblings, Name childName, int childIndex) {
        // check if a matching entry exists in the attic
        if (childNodeAttic.contains(childName, childIndex)) {
            return true;
        }
        // in case of reordered/moved SNSs we also have to look for a child
        // entry, which hold the given index before
        if (getStatus() == Status.EXISTING_MODIFIED) {
            for (Iterator it = siblings.iterator(); it.hasNext();) {
                NodeEntryImpl child = (NodeEntryImpl) it.next();
                if (!EntryValidation.isValidNodeEntry(child) || (child.revertInfo != null && child.revertInfo.oldIndex == childIndex)) {
                    return true;
                }
            }
        }
        return false;
    }
    /**
     * If 'revertInfo' is null it gets created from the current information
     * present on this entry.
     */
    private void createRevertInfo() {
        if (revertInfo == null) {
            revertInfo = new RevertInfo(parent, name, getIndex());
        }
    }

    /**
     * Special handling for MOVE and REORDER with same-name-siblings
     */
    private void createSiblingRevertInfos() throws RepositoryException {
        if (revertInfo != null) {
            return; // nothing to do
        }
        // for SNSs without UniqueID remember original index in order to
        // be able to build the workspaceID TODO: improve
        List sns = parent.childNodeEntries().get(name);
        if (sns.size() > 1) {
            for (Iterator it = sns.iterator(); it.hasNext();) {
                NodeEntryImpl sibling = (NodeEntryImpl) it.next();
                if (sibling.getUniqueID() == null && Status.NEW != sibling.getStatus()) {
                    sibling.createRevertInfo();
                }
            }
        }
    }

    /**
     * Revert a transient move and reordering of child entries
     */
    private void revertTransientChanges() throws RepositoryException {
        if (revertInfo == null) {
            return; // nothing to do
        }

        if (isTransientlyMoved())  {
            // move NodeEntry back to its original parent
            // TODO improve for simple renaming
            parent.childNodeEntries().remove(this);
            revertInfo.oldParent.childNodeAttic.remove(this);

            // now restore moved entry with the old name and index and re-add
            // it to its original parent (unless it got destroyed)
            parent = revertInfo.oldParent;
            name = revertInfo.oldName;
            ItemState state = internalGetItemState();
            if (state != null && !Status.isTerminal(state.getStatus())) {
                parent.childNodeEntries().add(this, revertInfo.oldIndex);
            }
        }
        // revert reordering of child-node-entries
        revertInfo.revertReordering();

        revertInfo.dispose();
        revertInfo = null;
    }

    /**
     * This entry has be set to 'EXISTING' again -> move and/or reordering of
     * child entries has been completed and the 'revertInfo' needs to be
     * reset/removed.
     */
    private void completeTransientChanges() {
        // old parent can forget this one
        // root entry does not have oldParent
        if (revertInfo.oldParent != null) {
            revertInfo.oldParent.childNodeAttic.remove(this);
        }
        revertInfo.dispose();
        revertInfo = null;
    }

    //--------------------------------------------------------< inner class >---
    /**
     * Upon move of this entry or upon reorder of its child-entries store
     * original hierarchy information for later revert and in order to be able
     * to build the workspace id(s).
     */
    private class RevertInfo implements ItemStateLifeCycleListener {

        private NodeEntryImpl oldParent;
        private Name oldName;
        private int oldIndex;

        private Map reorderedChildren;

        private RevertInfo(NodeEntryImpl oldParent, Name oldName, int oldIndex) {
            this.oldParent = oldParent;
            this.oldName = oldName;
            this.oldIndex = oldIndex;

            ItemState state = internalGetItemState();
            if (state != null) {
                state.addListener(this);
            } // else: should never be null.
        }

        private void dispose() {
            ItemState state = internalGetItemState();
            if (state != null) {
                state.removeListener(this);
            }

            if (reorderedChildren != null) {
                // special handling of SNS-children  TODO: improve
                // since reordered sns-children are not marked modified (unless they
                // got modified by some other action, their revertInfo
                // must be disposed manually
                for (Iterator it = reorderedChildren.keySet().iterator(); it.hasNext();) {
                    NodeEntry ne = (NodeEntry) it.next();
                    List sns = childNodeEntries.get(ne.getName());
                    if (sns.size() > 1) {
                        for (Iterator snsIt = sns.iterator(); snsIt.hasNext();) {
                            NodeEntryImpl sibling = (NodeEntryImpl) snsIt.next();
                            if (sibling.revertInfo != null && Status.EXISTING == sibling.getStatus()) {
                                sibling.revertInfo.dispose();
                                sibling.revertInfo = null;
                            }
                        }
                    }
                }
                reorderedChildren.clear();
            }
        }

        private boolean isMoved() {
            return oldParent != getParent() || !getName().equals(oldName);
        }

        private void reordered(NodeEntry insertEntry, NodeEntry previousBefore) {
            if (reorderedChildren == null) {
                reorderedChildren = new LinkedHashMap();
            }
            reorderedChildren.put(insertEntry, previousBefore);
        }

        private void revertReordering() {
            if (reorderedChildren == null) {
                return; // nothing to do
            }
            // revert all 'reorder' calls in in reverse other they were performed
            NodeEntry[] reordered = (NodeEntry[]) reorderedChildren.keySet().toArray(new NodeEntry[reorderedChildren.size()]);
            for (int i = reordered.length-1; i >= 0; i--) {
                NodeEntry ordered = reordered[i];
                if (isValidReorderedChild(ordered)) {
                    NodeEntry previousBefore = (NodeEntry) reorderedChildren.get(ordered);
                    if (previousBefore == null || isValidReorderedChild(previousBefore)) {
                        childNodeEntries.reorder(ordered, previousBefore);
                    }
                }
            }
        }

        private boolean isValidReorderedChild(NodeEntry child) {
            if (Status.isTerminal(child.getStatus())) {
                log.warn("Cannot revert reordering. 'previousBefore' does not exist any more.");
                return false;
            }
            if (child.isTransientlyMoved()) {
                // child has been moved away -> move back
                try {
                    child.revert();
                } catch (RepositoryException e) {
                    log.error("Internal error", e);
                    return false;
                }
            }
            return true;
        }

        /**
         * @see ItemStateLifeCycleListener#statusChanged(ItemState, int)
         */
        public void statusChanged(ItemState state, int previousStatus) {
            switch (state.getStatus()) {
                case Status.EXISTING:
                    // stop listening
                    state.removeListener(this);
                    completeTransientChanges();
                    break;

                case Status.REMOVED:
                case Status.STALE_DESTROYED:
                    // stop listening
                    state.removeListener(this);
                    // remove from the attic
                    try {
                        revertTransientChanges();
                    } catch (RepositoryException e) {
                        log.warn("Internal error", e);
                    }
                    break;
            }
        }
    }
}
TOP

Related Classes of org.apache.jackrabbit.jcr2spi.hierarchy.NodeEntryImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.