/*
 * 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.oak.jcr.session;

import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.singleton;
import static java.util.Objects.requireNonNull;
import static org.apache.jackrabbit.JcrConstants.JCR_MIXINTYPES;
import static org.apache.jackrabbit.JcrConstants.JCR_PRIMARYTYPE;
import static org.apache.jackrabbit.oak.api.Type.NAME;
import static org.apache.jackrabbit.oak.api.Type.NAMES;
import static org.apache.jackrabbit.oak.jcr.session.SessionImpl.checkIndexOnName;
import static org.apache.jackrabbit.oak.plugins.tree.TreeUtil.getNames;

import java.io.InputStream;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;

import javax.jcr.AccessDeniedException;
import javax.jcr.Binary;
import javax.jcr.InvalidItemStateException;
import javax.jcr.Item;
import javax.jcr.ItemExistsException;
import javax.jcr.ItemNotFoundException;
import javax.jcr.ItemVisitor;
import javax.jcr.NoSuchWorkspaceException;
import javax.jcr.Node;
import javax.jcr.NodeIterator;
import javax.jcr.PathNotFoundException;
import javax.jcr.Property;
import javax.jcr.PropertyIterator;
import javax.jcr.PropertyType;
import javax.jcr.RepositoryException;
import javax.jcr.UnsupportedRepositoryOperationException;
import javax.jcr.Value;
import javax.jcr.lock.Lock;
import javax.jcr.lock.LockManager;
import javax.jcr.nodetype.ConstraintViolationException;
import javax.jcr.nodetype.NodeDefinition;
import javax.jcr.nodetype.NodeType;
import javax.jcr.nodetype.NodeTypeManager;
import javax.jcr.version.OnParentVersionAction;
import javax.jcr.version.Version;
import javax.jcr.version.VersionException;
import javax.jcr.version.VersionHistory;

import org.apache.jackrabbit.JcrConstants;
import org.apache.jackrabbit.api.JackrabbitNode;
import org.apache.jackrabbit.commons.ItemNameMatcher;
import org.apache.jackrabbit.commons.iterator.NodeIteratorAdapter;
import org.apache.jackrabbit.commons.iterator.PropertyIteratorAdapter;
import org.apache.jackrabbit.oak.api.PropertyState;
import org.apache.jackrabbit.oak.api.Tree;
import org.apache.jackrabbit.oak.api.Tree.Status;
import org.apache.jackrabbit.oak.api.Type;
import org.apache.jackrabbit.oak.api.jmx.NodeCounterMBean;
import org.apache.jackrabbit.oak.commons.LazyValue;
import org.apache.jackrabbit.oak.commons.PathUtils;
import org.apache.jackrabbit.oak.commons.collections.IterableUtils;
import org.apache.jackrabbit.oak.commons.collections.IteratorUtils;
import org.apache.jackrabbit.oak.commons.collections.SetUtils;
import org.apache.jackrabbit.oak.jcr.delegate.NodeDelegate;
import org.apache.jackrabbit.oak.jcr.delegate.PropertyDelegate;
import org.apache.jackrabbit.oak.jcr.delegate.SessionDelegate;
import org.apache.jackrabbit.oak.jcr.delegate.VersionManagerDelegate;
import org.apache.jackrabbit.oak.jcr.lock.LockDeprecation;
import org.apache.jackrabbit.oak.jcr.session.operation.ItemOperation;
import org.apache.jackrabbit.oak.jcr.session.operation.NodeOperation;
import org.apache.jackrabbit.oak.jcr.session.operation.SessionOperation;
import org.apache.jackrabbit.oak.jcr.version.VersionHistoryImpl;
import org.apache.jackrabbit.oak.jcr.version.VersionImpl;
import org.apache.jackrabbit.oak.plugins.identifier.IdentifierManager;
import org.apache.jackrabbit.oak.plugins.memory.PropertyStates;
import org.apache.jackrabbit.oak.spi.nodetype.EffectiveNodeType;
import org.apache.jackrabbit.oak.plugins.tree.factories.RootFactory;
import org.apache.jackrabbit.oak.spi.nodetype.NodeTypeConstants;
import org.apache.jackrabbit.oak.spi.security.authorization.permission.Permissions;
import org.apache.jackrabbit.oak.plugins.tree.TreeUtil;
import org.apache.jackrabbit.value.ValueHelper;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * TODO document
 *
 * @param <T> the delegate type
 */
public class NodeImpl<T extends NodeDelegate> extends ItemImpl<T> implements JackrabbitNode {

    /**
     * Use an zero length MVP to check read permission on jcr:mixinTypes (OAK-7652)
     */
    private static final PropertyState EMPTY_MIXIN_TYPES = PropertyStates.createProperty(
            JcrConstants.JCR_MIXINTYPES, Collections.emptyList(), Type.NAMES);


    /**
     * The maximum returned value for {@link NodeIterator#getSize()}. If there
     * are more nodes, the method returns -1.
     */
    private static final long NODE_ITERATOR_MAX_SIZE = Long.MAX_VALUE;

    /**
     * logger instance
     */
    private static final Logger LOG = LoggerFactory.getLogger(NodeImpl.class);

    private static final int LOG_WARN_STRING_SIZE_THRESHOLD = Integer.getInteger(
            OakJcrConstants.WARN_LOG_STRING_SIZE_THRESHOLD_KEY,
            OakJcrConstants.DEFAULT_WARN_LOG_STRING_SIZE_THRESHOLD_VALUE);

    @Nullable
    public static NodeImpl<? extends NodeDelegate> createNodeOrNull(
            @Nullable NodeDelegate delegate, @NotNull SessionContext context)
            throws RepositoryException {
        if (delegate != null) {
            return createNode(delegate, context);
        } else {
            return null;
        }
    }

    @NotNull
    public static NodeImpl<? extends NodeDelegate> createNode(
                @NotNull NodeDelegate delegate, @NotNull SessionContext context)
                throws RepositoryException {
        PropertyDelegate pd = delegate.getPropertyOrNull(JCR_PRIMARYTYPE);
        String type = pd != null ? pd.getString() : null;
        if (JcrConstants.NT_VERSION.equals(type)) {
            VersionManagerDelegate vmd =
                    VersionManagerDelegate.create(context.getSessionDelegate());
            return new VersionImpl(vmd.createVersion(delegate), context);
        } else if (JcrConstants.NT_VERSIONHISTORY.equals(type)) {
            VersionManagerDelegate vmd =
                    VersionManagerDelegate.create(context.getSessionDelegate());
            return new VersionHistoryImpl(vmd.createVersionHistory(delegate), context);
        } else {
            return new NodeImpl<NodeDelegate>(delegate, context);
        }
    }

    public NodeImpl(T dlg, SessionContext sessionContext) {
        super(dlg, sessionContext);
    }

    //---------------------------------------------------------------< Item >---

    /**
     * @see javax.jcr.Item#isNode()
     */
    @Override
    public boolean isNode() {
        return true;
    }

    /**
     * @see javax.jcr.Item#getParent()
     */
    @Override
    @NotNull
    public Node getParent() throws RepositoryException {
        return perform(new NodeOperation<Node>(dlg, "getParent") {
            @NotNull
            @Override
            public Node perform() throws RepositoryException {
                if (node.isRoot()) {
                    throw new ItemNotFoundException("Root has no parent");
                } else {
                    NodeDelegate parent = node.getParent();
                    if (parent == null) {
                        throw new AccessDeniedException();
                    }
                    return createNode(parent, sessionContext);
                }
            }
        });
    }

    /**
     * @see javax.jcr.Item#isNew()
     */
    @Override
    public boolean isNew() {
        return sessionDelegate.safePerform(new NodeOperation<Boolean>(dlg, "isNew") {
            @NotNull
            @Override
            public Boolean perform() {
                return node.exists() && node.getStatus() == Status.NEW;
            }
        });
    }

    /**
     * @see javax.jcr.Item#isModified()
     */
    @Override
    public boolean isModified() {
        return sessionDelegate.safePerform(new NodeOperation<Boolean>(dlg, "isModified") {
            @NotNull
            @Override
            public Boolean perform() {
                return node.exists() && node.getStatus() == Status.MODIFIED;
            }
        });
    }

    /**
     * @see javax.jcr.Item#remove()
     */
    @Override
    public void remove() throws RepositoryException {
        sessionDelegate.performVoid(new ItemWriteOperation<Void>("remove") {
            @Override
            public void performVoid() throws RepositoryException {
                if (dlg.isRoot()) {
                    throw new RepositoryException("Cannot remove the root node");
                }

                dlg.remove();
            }

            @Override
            public String toString() {
                return format("removeNode [%s]", dlg.getPath());
            }
        });
    }

    @Override
    public void accept(ItemVisitor visitor) throws RepositoryException {
        visitor.visit(this);
    }

    //---------------------------------------------------------------< Node >---

    /**
     * @see Node#addNode(String)
     */
    @Override
    @NotNull
    public Node addNode(String relPath) throws RepositoryException {
        return addNode(relPath, null);
    }

    @Override @NotNull
    public Node addNode(final String relPath, String primaryNodeTypeName)
            throws RepositoryException {
        final String oakPath;

        try {
            oakPath = getOakPathOrThrowNotFound(relPath);
        } catch (PathNotFoundException ex) {
            throw new RepositoryException("cannot determine oak path for: " + relPath, ex);
        }

        final String oakTypeName;
        if (primaryNodeTypeName != null) {
            oakTypeName = getOakName(primaryNodeTypeName);
        } else {
            oakTypeName = null;
        }

        checkIndexOnName(relPath);
        return perform(new ItemWriteOperation<Node>("addNode") {
            @NotNull
            @Override
            public Node perform() throws RepositoryException {
                String oakName = PathUtils.getName(oakPath);
                String parentPath = PathUtils.getParentPath(oakPath);

                NodeDelegate parent = dlg.getChild(parentPath);
                if (parent == null) {
                    // is it a property?
                    String grandParentPath = PathUtils.getParentPath(parentPath);
                    NodeDelegate grandParent = dlg.getChild(grandParentPath);
                    if (grandParent != null) {
                        String propName = PathUtils.getName(parentPath);
                        if (grandParent.getPropertyOrNull(propName) != null) {
                            throw new ConstraintViolationException("Can't add new node to property.");
                        }
                    }

                    throw new PathNotFoundException(relPath);
                }

                if (parent.getChild(oakName) != null) {
                    throw new ItemExistsException(relPath);
                }

                // check for NODE_TYPE_MANAGEMENT permission here as we cannot
                // distinguish between user-supplied and system-generated
                // modification of that property in the PermissionValidator
                if (oakTypeName != null) {
                    PropertyState prop = PropertyStates.createProperty(JCR_PRIMARYTYPE, oakTypeName, NAME);
                    sessionContext.getAccessManager().checkPermissions(parent.getTree(), prop, Permissions.NODE_TYPE_MANAGEMENT);
                }

                NodeDelegate added = parent.addChild(oakName, oakTypeName);
                if (added == null) {
                    throw new ItemExistsException(format("Node [%s/%s] exists", getNodePath(),oakName));
                }
                return createNode(added, sessionContext);
            }

            @Override
            public String toString() {
                return format("addNode [%s/%s]", dlg.getPath(), relPath);
            }
        });
    }

    @Override
    public void orderBefore(final String srcChildRelPath, final String destChildRelPath) throws RepositoryException {
        sessionDelegate.performVoid(new ItemWriteOperation<Void>("orderBefore") {
            @Override
            public void performVoid() throws RepositoryException {
                getEffectiveNodeType().checkOrderableChildNodes();
                String oakSrcChildRelPath = getOakPathOrThrowNotFound(srcChildRelPath);
                String oakDestChildRelPath = null;
                if (destChildRelPath != null) {
                    oakDestChildRelPath = getOakPathOrThrowNotFound(destChildRelPath);
                }
                dlg.orderBefore(oakSrcChildRelPath, oakDestChildRelPath);

            }
        });
    }

    //-------------------------------------------------------< setProperty >--
    //
    // The setProperty() variants below follow the same pattern:
    //
    //     if (value != null) {
    //         return internalSetProperty(name, ...);
    //     } else {
    //         return internalRemoveProperty(name);
    //     }
    //
    // In addition the value and value type information is pre-processed
    // according to the method signature before being passed to
    // internalSetProperty(). The methods that take a non-nullable
    // primitive value as an argument can skip the if clause.
    //
    // Note that due to backwards compatibility reasons (OAK-395) none
    // of the methods will ever return null, even if asked to remove
    // a non-existing property! See internalRemoveProperty() for details.

    @Override @NotNull
    public Property setProperty(String name, Value value)
            throws RepositoryException {
        if (value != null) {
            return internalSetProperty(name, value, false);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, Value value, int type)
            throws RepositoryException {
        if (value != null) {
            boolean exactTypeMatch = true;
            if (type == PropertyType.UNDEFINED) {
                exactTypeMatch = false;
            } else {
                value = ValueHelper.convert(value, type, getValueFactory());
            }
            return internalSetProperty(name, value, exactTypeMatch);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, Value[] values)
            throws RepositoryException {
        if (values != null) {
            // TODO: type
            return internalSetProperty(name, values, ValueHelper.getType(values), false);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String jcrName, Value[] values, int type)
            throws RepositoryException {
        if (values != null) {
            boolean exactTypeMatch = true;
            if (type == PropertyType.UNDEFINED) {
                type = PropertyType.STRING;
                exactTypeMatch = false;
            }
            values = ValueHelper.convert(values, type, getValueFactory());
            return internalSetProperty(jcrName, values, type, exactTypeMatch);
        } else {
            return internalRemoveProperty(jcrName);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, String[] values)
            throws RepositoryException {
        if (values != null) {
            int type = PropertyType.STRING;
            Value[] vs = ValueHelper.convert(values, type, getValueFactory());
            return internalSetProperty(name, vs, type, false);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, String[] values, int type)
            throws RepositoryException {
        if (values != null) {
            boolean exactTypeMatch = true;
            if (type == PropertyType.UNDEFINED) {
                type = PropertyType.STRING;
                exactTypeMatch = false;
            }
            Value[] vs = ValueHelper.convert(values, type, getValueFactory());
            return internalSetProperty(name, vs, type, exactTypeMatch);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, String value)
            throws RepositoryException {
        if (value != null) {
            Value v = getValueFactory().createValue(value);
            return internalSetProperty(name, v, false);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, String value, int type)
            throws RepositoryException {
        if (value != null) {
            boolean exactTypeMatch = true;
            if (type == PropertyType.UNDEFINED) {
                type = PropertyType.STRING;
                exactTypeMatch = false;
            }
            Value v = getValueFactory().createValue(value, type);
            return internalSetProperty(name, v, exactTypeMatch);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull @SuppressWarnings("deprecation")
    public Property setProperty(String name, InputStream value)
            throws RepositoryException {
        if (value != null) {
            Value v = getValueFactory().createValue(value);
            return internalSetProperty(name, v, false);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, Binary value)
            throws RepositoryException {
        if (value != null) {
            Value v = getValueFactory().createValue(value);
            return internalSetProperty(name, v, false);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, boolean value)
            throws RepositoryException {
        Value v = getValueFactory().createValue(value);
        return internalSetProperty(name, v, false);
    }

    @Override @NotNull
    public Property setProperty(String name, double value)
            throws RepositoryException {
        Value v = getValueFactory().createValue(value);
        return internalSetProperty(name, v, false);
    }

    @Override @NotNull
    public Property setProperty(String name, BigDecimal value)
            throws RepositoryException {
        if (value != null) {
            Value v = getValueFactory().createValue(value);
            return internalSetProperty(name, v, false);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, long value)
            throws RepositoryException {
        Value v = getValueFactory().createValue(value);
        return internalSetProperty(name, v, false);
    }

    @Override @NotNull
    public Property setProperty(String name, Calendar value)
            throws RepositoryException {
        if (value != null) {
            Value v = getValueFactory().createValue(value);
            return internalSetProperty(name, v, false);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override @NotNull
    public Property setProperty(String name, Node value)
            throws RepositoryException {
        if (value != null) {
            Value v = getValueFactory().createValue(value);
            return internalSetProperty(name, v, false);
        } else {
            return internalRemoveProperty(name);
        }
    }

    @Override
    @NotNull
    public Node getNode(String relPath) throws RepositoryException {
        final String oakPath = getOakPathOrThrowNotFound(relPath);
        return perform(new NodeOperation<Node>(dlg, "getNode") {
            @NotNull
            @Override
            public Node perform() throws RepositoryException {
                NodeDelegate nd = node.getChild(oakPath);
                if (nd == null) {
                    throw new PathNotFoundException(oakPath);
                } else {
                    return createNode(nd, sessionContext);
                }
            }
        });
    }

    @Override
    @NotNull
    public NodeIterator getNodes() throws RepositoryException {
        return perform(new NodeOperation<NodeIterator>(dlg, "getNodes") {
            @NotNull
            @Override
            public NodeIterator perform() throws RepositoryException {
                Iterator<NodeDelegate> children = node.getChildren();
                return new NodeIteratorAdapter(nodeIterator(children)) {
                    private long size = -2;
                    @Override
                    public long getSize() {
                        if (size == -2) {
                            try {
                                size = node.getChildCount(NODE_ITERATOR_MAX_SIZE); // TODO: perform()
                                if (size == Long.MAX_VALUE) {
                                    size = -1;
                                }
                            } catch (InvalidItemStateException e) {
                                throw new IllegalStateException(
                                        "This iterator is no longer valid", e);
                            }
                        }
                        return size;
                    }
                };
            }
            @Override
            public String toString() {
                return format("getNodes [%s]", dlg.getPath());
            }
        });
    }

    @Override
    @NotNull
    public NodeIterator getNodes(final String namePattern)
            throws RepositoryException {
        return perform(new NodeOperation<NodeIterator>(dlg, "getNodes") {
            @NotNull
            @Override
            public NodeIterator perform() throws RepositoryException {
                Iterator<NodeDelegate> children = IteratorUtils.filter(
                        node.getChildren(),
                        // TODO: use Oak names
                        state -> ItemNameMatcher.matches(toJcrPath(state.getName()), namePattern));
                return new NodeIteratorAdapter(nodeIterator(children));
            }
            @Override
            public String toString() {
                return format("getNodes [%s]", dlg.getPath());
            }
        });
    }

    @Override
    @NotNull
    public NodeIterator getNodes(final String[] nameGlobs) throws RepositoryException {
        return perform(new NodeOperation<NodeIterator>(dlg, "getNodes") {
            @NotNull
            @Override
            public NodeIterator perform() throws RepositoryException {
                Iterator<NodeDelegate> children = IteratorUtils.filter(
                        node.getChildren(),
                        // TODO: use Oak names
                        state -> ItemNameMatcher.matches(toJcrPath(state.getName()), nameGlobs));
                return new NodeIteratorAdapter(nodeIterator(children));
            }
            @Override
            public String toString() {
                return format("getNodes [%s]", dlg.getPath());
            }
        });
    }

    @Override
    @NotNull
    public Property getProperty(String relPath) throws RepositoryException {
        final String oakPath = getOakPathOrThrowNotFound(relPath);
        return perform(new NodeOperation<PropertyImpl>(dlg, "getProperty") {
            @NotNull
            @Override
            public PropertyImpl perform() throws RepositoryException {
                PropertyDelegate pd = node.getPropertyOrNull(oakPath);
                if (pd == null) {
                    throw new PathNotFoundException(
                            oakPath + " not found on " + node.getPath());
                } else {
                    return new PropertyImpl(pd, sessionContext);
                }
            }
            @Override
            public String toString() {
                return format("getProperty [%s]", dlg.getPath());
            }
        });
    }

    @Override
    @NotNull
    public PropertyIterator getProperties() throws RepositoryException {
        return perform(new NodeOperation<PropertyIterator>(dlg, "getProperties") {
            @NotNull
            @Override
            public PropertyIterator perform() throws RepositoryException {
                Iterator<PropertyDelegate> properties = node.getProperties();
                long size = node.getPropertyCount();
                return new PropertyIteratorAdapter(
                        propertyIterator(properties), size);
            }
            @Override
            public String toString() {
                return format("getProperties [%s]", dlg.getPath());
            }
        });
    }

    @Override
    @NotNull
    public PropertyIterator getProperties(final String namePattern) throws RepositoryException {
        return perform(new NodeOperation<PropertyIterator>(dlg, "getProperties") {
            @NotNull
            @Override
            public PropertyIterator perform() throws RepositoryException {
                final PropertyIteratorDelegate delegate = new PropertyIteratorDelegate(node,
                    // TODO: use Oak names
                    entry -> ItemNameMatcher.matches(toJcrPath(entry.getName()), namePattern));
                return new PropertyIteratorAdapter(propertyIterator(delegate.iterator())){
                    @Override
                    public long getSize() {
                        return delegate.getSize();
                    }
                };
            }
        });
    }

    @Override
    @NotNull
    public PropertyIterator getProperties(final String[] nameGlobs) throws RepositoryException {
        return perform(new NodeOperation<PropertyIterator>(dlg, "getProperties") {
            @NotNull
            @Override
            public PropertyIterator perform() throws RepositoryException {
                final PropertyIteratorDelegate delegate = new PropertyIteratorDelegate(node,
                    // TODO: use Oak names
                    entry -> ItemNameMatcher.matches(toJcrPath(entry.getName()), nameGlobs));
                return new PropertyIteratorAdapter(propertyIterator(delegate.iterator())){
                    @Override
                    public long getSize() {
                        return delegate.getSize();
                    }
                };
            }
        });
    }

    /**
     * @see javax.jcr.Node#getPrimaryItem()
     */
    @Override
    @NotNull
    public Item getPrimaryItem() throws RepositoryException {
        return perform(new NodeOperation<Item>(dlg, "getPrimaryItem") {
            @NotNull
            @Override
            public Item perform() throws RepositoryException {
                // TODO: avoid nested calls
                String name = getPrimaryNodeType().getPrimaryItemName();
                if (name == null) {
                    throw new ItemNotFoundException(
                            "No primary item present on node " + NodeImpl.this);
                }
                if (hasProperty(name)) {
                    return getProperty(name);
                } else if (hasNode(name)) {
                    return getNode(name);
                } else {
                    throw new ItemNotFoundException(
                            "Primary item " + name + 
                            " does not exist on node " + NodeImpl.this);
                }
            }
        });
    }

    /**
     * @see javax.jcr.Node#getUUID()
     */
    @Override
    @NotNull
    public String getUUID() throws RepositoryException {
        return perform(new NodeOperation<String>(dlg, "getUUID") {
            @NotNull
            @Override
            public String perform() throws RepositoryException {
                // TODO: avoid nested calls
                if (isNodeType(NodeType.MIX_REFERENCEABLE)) {
                    return getIdentifier();
                }
                throw new UnsupportedRepositoryOperationException(format("Node [%s] is not referenceable.", getNodePath()));
            }
        });
    }

    @Override
    @NotNull
    public String getIdentifier() throws RepositoryException {
        // TODO: name mapping for path identifiers
        return perform(new NodeOperation<String>(dlg, "getIdentifier") {
            @NotNull
            @Override
            public String perform() throws RepositoryException {
                return node.getIdentifier();
            }
        });
    }

    @Override
    public int getIndex() throws RepositoryException {
        // as long as we do not support same name siblings, index always is 1
        return 1; // TODO
    }

    private PropertyIterator internalGetReferences(final String name, final boolean weak) throws RepositoryException {
        return perform(new NodeOperation<PropertyIterator>(dlg, "internalGetReferences") {
            @NotNull
            @Override
            public PropertyIterator perform() throws InvalidItemStateException {
                IdentifierManager idManager = sessionDelegate.getIdManager();

                Iterable<String> propertyOakPaths = idManager.getReferences(weak, node.getTree(), name); // TODO: oak name?
                Iterable<Property> properties = IterableUtils.transform(
                        propertyOakPaths,
                        oakPath -> {
                                PropertyDelegate pd = sessionDelegate.getProperty(oakPath);
                                return pd == null ? null : new PropertyImpl(pd, sessionContext);
                            }
                        );

                return new PropertyIteratorAdapter(sessionDelegate.sync(properties.iterator()));
            }
        });
    }

    /**
     * @see javax.jcr.Node#getReferences()
     */
    @Override
    @NotNull
    public PropertyIterator getReferences() throws RepositoryException {
        return internalGetReferences(null, false);
    }

    @Override
    @NotNull
    public PropertyIterator getReferences(final String name) throws RepositoryException {
        return internalGetReferences(name, false);
    }

    /**
     * @see javax.jcr.Node#getWeakReferences()
     */
    @Override
    @NotNull
    public PropertyIterator getWeakReferences() throws RepositoryException {
        return internalGetReferences(null, true);
    }

    @Override
    @NotNull
    public PropertyIterator getWeakReferences(String name) throws RepositoryException {
        return internalGetReferences(name, true);
    }

    @Override
    public boolean hasNode(String relPath) throws RepositoryException {
        try {
            final String oakPath = getOakPathOrThrow(relPath);
            return perform(new NodeOperation<Boolean>(dlg, "hasNode") {
                @NotNull
                @Override
                public Boolean perform() throws RepositoryException {
                    return node.getChild(oakPath) != null;
                }
            });
        } catch (PathNotFoundException e) {
            return false;
        }
    }

    @Override
    public boolean hasProperty(String relPath) throws RepositoryException {
        try {
            final String oakPath = getOakPathOrThrow(relPath);
            return perform(new NodeOperation<Boolean>(dlg, "hasProperty") {
                @NotNull
                @Override
                public Boolean perform() throws RepositoryException {
                    return node.getPropertyOrNull(oakPath) != null;
                }
                @Override
                public String toString() {
                    return format("hasProperty [%s]", dlg.getPath());
                }
            });
        } catch (PathNotFoundException e) {
            return false;
        }
    }

    @Override
    public boolean hasNodes() throws RepositoryException {
        return getNodes().hasNext();
    }

    @Override
    public boolean hasProperties() throws RepositoryException {
        return perform(new NodeOperation<Boolean>(dlg, "hasProperties") {
            @NotNull
            @Override
            public Boolean perform() throws RepositoryException {
                return node.getPropertyCount() != 0;
            }
        });
    }

    /**
     * @see javax.jcr.Node#getPrimaryNodeType()
     */
    @Override
    @NotNull
    public NodeType getPrimaryNodeType() throws RepositoryException {
        return perform(new NodeOperation<NodeType>(dlg, "getPrimaryNodeType") {
            @NotNull
            @Override
            public NodeType perform() throws RepositoryException {
                Tree tree = node.getTree();
                String primaryTypeName = getPrimaryTypeName(tree);
                if (primaryTypeName != null) {
                    return getNodeTypeManager().getNodeType(sessionContext.getJcrName(primaryTypeName));
                } else {
                    throw new RepositoryException("Unable to retrieve primary type for Node " + getNodePath());
                }
            }
        });
    }

    /**
     * @see javax.jcr.Node#getMixinNodeTypes()
     */
    @Override
    @NotNull
    public NodeType[] getMixinNodeTypes() throws RepositoryException {
        return perform(new NodeOperation<NodeType[]>(dlg, "getMixinNodeTypes") {
            @NotNull
            @Override
            public NodeType[] perform() throws RepositoryException {
                Tree tree = node.getTree();

                Iterator<String> mixinNames = getMixinTypeNames(tree).iterator();
                if (mixinNames.hasNext()) {
                    NodeTypeManager ntMgr = getNodeTypeManager();
                    List<NodeType> mixinTypes = new ArrayList<>();
                    while (mixinNames.hasNext()) {
                        mixinTypes.add(ntMgr.getNodeType(sessionContext.getJcrName(mixinNames.next())));
                    }
                    return mixinTypes.toArray(new NodeType[mixinTypes.size()]);
                } else {
                    return new NodeType[0];
                }
            }
        });
    }

    @Override
    public boolean isNodeType(String nodeTypeName) throws RepositoryException {
        final String oakName = getOakName(nodeTypeName);
        return perform(new NodeOperation<Boolean>(dlg, "isNodeType") {
            @NotNull
            @Override
            public Boolean perform() throws RepositoryException {
                Tree tree = node.getTree();
                return getNodeTypeManager().isNodeType(getPrimaryTypeName(tree), () -> getMixinTypeNames(tree), oakName);
            }
        });
    }

    @Override
    public void setPrimaryType(final String nodeTypeName) throws RepositoryException {
        final String oakTypeName = getOakName(requireNonNull(nodeTypeName));
        sessionDelegate.performVoid(new ItemWriteOperation<Void>("setPrimaryType") {
            @Override
            public void checkPreconditions() throws RepositoryException {
                super.checkPreconditions();
                if (!internalIsCheckedOut()) {
                    throw new VersionException(format("Cannot set primary type. Node [%s] is checked in.", getNodePath()));
                }
            }

            @Override
            public void performVoid() throws RepositoryException {
                internalSetPrimaryType(oakTypeName);
            }
        });
    }

    @Override
    public void addMixin(String mixinName) throws RepositoryException {
        final String oakTypeName = getOakName(requireNonNull(mixinName));
        if (JcrConstants.MIX_LOCKABLE.equals(oakTypeName)) {
            LockDeprecation.handleCall("addMixin " + JcrConstants.MIX_LOCKABLE);
        }
        sessionDelegate.performVoid(new ItemWriteOperation<Void>("addMixin") {
            @Override
            public void checkPreconditions() throws RepositoryException {
                super.checkPreconditions();
                if (!internalIsCheckedOut()) {
                    throw new VersionException(format(
                            "Cannot add mixin type. Node [%s] is checked in.", getNodePath()));
                }
            }
            @Override
            public void performVoid() throws RepositoryException {
                dlg.addMixin(NodeImpl.this::getMixinTypeNames, oakTypeName);
            }
        });
    }

    @Override
    public void removeMixin(final String mixinName) throws RepositoryException {
        final String oakTypeName = getOakName(requireNonNull(mixinName));
        sessionDelegate.performVoid(new ItemWriteOperation<Void>("removeMixin") {
            @Override
            public void checkPreconditions() throws RepositoryException {
                super.checkPreconditions();
                if (!internalIsCheckedOut()) {
                    throw new VersionException(format(
                            "Cannot remove mixin type. Node [%s] is checked in.", getNodePath()));
                }

                // check for NODE_TYPE_MANAGEMENT permission here as we cannot
                // distinguish between a combination of removeMixin and addMixin
                // and Node#remove plus subsequent addNode when it comes to
                // autocreated properties like jcr:create, jcr:uuid and so forth.
                Set<String> mixins = SetUtils.toLinkedSet(getNames(dlg.getTree(), JCR_MIXINTYPES));
                if (!mixins.isEmpty() && mixins.remove(getOakName(mixinName))) {
                    PropertyState prop = PropertyStates.createProperty(JCR_MIXINTYPES, mixins, NAMES);
                    sessionContext.getAccessManager().checkPermissions(dlg.getTree(), prop, Permissions.NODE_TYPE_MANAGEMENT);
                }
            }
            @Override
            public void performVoid() throws RepositoryException {
                dlg.removeMixin(oakTypeName);
            }
        });
    }

    @Override
    public boolean canAddMixin(String mixinName) throws RepositoryException {
        final String oakTypeName = getOakName(mixinName);
        return perform(new NodeOperation<Boolean>(dlg, "canAddMixin") {
            @NotNull
            @Override
            public Boolean perform() throws RepositoryException {
                PropertyState prop = PropertyStates.createProperty(JCR_MIXINTYPES, singleton(oakTypeName), NAMES);
                return sessionContext.getAccessManager().hasPermissions(
                        node.getTree(), prop, Permissions.NODE_TYPE_MANAGEMENT)
                        && !node.isProtected()
                        && getVersionManager().isCheckedOut(node)
                        && node.canAddMixin(oakTypeName);
            }
        });
    }

    @Override
    @NotNull
    public NodeDefinition getDefinition() throws RepositoryException {
        return perform(new NodeOperation<NodeDefinition>(dlg, "getDefinition") {
            @NotNull
            @Override
            public NodeDefinition perform() throws RepositoryException {
                NodeDelegate parent = node.getParent();
                if (parent == null) {
                    return getNodeTypeManager().getRootDefinition();
                } else {
                    return getNodeTypeManager().getDefinition(
                            parent.getTree(), node.getTree());
                }
            }
        });
    }

    @Override
    @NotNull
    public String getCorrespondingNodePath(final String workspaceName) throws RepositoryException {
        return toJcrPath(perform(new ItemOperation<String>(dlg, "getCorrespondingNodePath") {
            @NotNull
            @Override
            public String perform() throws RepositoryException {
                checkValidWorkspace(workspaceName);
                if (workspaceName.equals(sessionDelegate.getWorkspaceName())) {
                    return item.getPath();
                } else {
                    throw new UnsupportedRepositoryOperationException("OAK-118: Node.getCorrespondingNodePath at " + getNodePath());
                }
            }
        }));
    }


    @Override
    public void update(final String srcWorkspace) throws RepositoryException {
        sessionDelegate.performVoid(new ItemWriteOperation<Void>("update") {
            @Override
            public void performVoid() throws RepositoryException {
                checkValidWorkspace(srcWorkspace);

                // check for pending changes
                if (sessionDelegate.hasPendingChanges()) {
                    String msg = format("Unable to perform operation. Session has pending changes. Node [%s]", getNodePath());
                    LOG.debug(msg);
                    throw new InvalidItemStateException(msg);
                }

                if (!srcWorkspace.equals(sessionDelegate.getWorkspaceName())) {
                    throw new UnsupportedRepositoryOperationException("OAK-118: Node.update at " + getNodePath());
                }
            }
        });
    }

    /**
     * @see javax.jcr.Node#checkin()
     */
    @Override
    @NotNull
    public Version checkin() throws RepositoryException {
        return getVersionManager().checkin(getPath());
    }

    /**
     * @see javax.jcr.Node#checkout()
     */
    @Override
    public void checkout() throws RepositoryException {
        getVersionManager().checkout(getPath());
    }

    /**
     * @see javax.jcr.Node#doneMerge(javax.jcr.version.Version)
     */
    @Override
    public void doneMerge(Version version) throws RepositoryException {
        getVersionManager().doneMerge(getPath(), version);
    }

    /**
     * @see javax.jcr.Node#cancelMerge(javax.jcr.version.Version)
     */
    @Override
    public void cancelMerge(Version version) throws RepositoryException {
        getVersionManager().cancelMerge(getPath(), version);
    }

    /**
     * @see javax.jcr.Node#merge(String, boolean)
     */
    @Override
    @NotNull
    public NodeIterator merge(String srcWorkspace, boolean bestEffort) throws RepositoryException {
        return getVersionManager().merge(getPath(), srcWorkspace, bestEffort);
    }

    /**
     * @see javax.jcr.Node#isCheckedOut()
     */
    @Override
    public boolean isCheckedOut() throws RepositoryException {
        final SessionDelegate sessionDelegate = sessionContext.getSessionDelegate();
        return sessionDelegate.perform(new SessionOperation<Boolean>("isCheckedOut") {
            @NotNull
            @Override
            public Boolean perform() throws RepositoryException {
                return internalIsCheckedOut();
            }
        });
    }

    /**
     * @see javax.jcr.Node#restore(String, boolean)
     */
    @Override
    public void restore(String versionName, boolean removeExisting) throws RepositoryException {
        if (!isNodeType(NodeType.MIX_VERSIONABLE)) {
            throw new UnsupportedRepositoryOperationException(format("Node [%s] is not mix:versionable", getNodePath()));
        }
        getVersionManager().restore(getPath(), versionName, removeExisting);
    }

    /**
     * @see javax.jcr.Node#restore(javax.jcr.version.Version, boolean)
     */
    @Override
    public void restore(Version version, boolean removeExisting) throws RepositoryException {
        if (!isNodeType(NodeType.MIX_VERSIONABLE)) {
            throw new UnsupportedRepositoryOperationException(format("Node [%s] is not mix:versionable", getNodePath()));
        }
        String id = version.getContainingHistory().getVersionableIdentifier();
        if (getIdentifier().equals(id)) {
            getVersionManager().restore(version, removeExisting);
        } else {
            throw new VersionException(format("Version does not belong to the " +
                    "VersionHistory of this node [%s].", getNodePath()));
        }
    }

    /**
     * @see javax.jcr.Node#restore(Version, String, boolean)
     */
    @Override
    public void restore(Version version, String relPath, boolean removeExisting) throws RepositoryException {
        // additional checks are performed with subsequent calls.
        if (hasNode(relPath)) {
            // node at 'relPath' exists -> call restore on the target Node
            getNode(relPath).restore(version, removeExisting);
        } else {
            String absPath = PathUtils.concat(getPath(), relPath);
            getVersionManager().restore(absPath, version, removeExisting);
        }
    }

    /**
     * @see javax.jcr.Node#restoreByLabel(String, boolean)
     */
    @Override
    public void restoreByLabel(String versionLabel, boolean removeExisting) throws RepositoryException {
        getVersionManager().restoreByLabel(getPath(), versionLabel, removeExisting);
    }

    /**
     * @see javax.jcr.Node#getVersionHistory()
     */
    @Override
    @NotNull
    public VersionHistory getVersionHistory() throws RepositoryException {
        return getVersionManager().getVersionHistory(getPath());
    }

    /**
     * @see javax.jcr.Node#getBaseVersion()
     */
    @Override
    @NotNull
    public Version getBaseVersion() throws RepositoryException {
        return getVersionManager().getBaseVersion(getPath());
    }

    private LockManager getLockManager() throws RepositoryException {
        return getSession().getWorkspace().getLockManager();
    }

    @Override
    public boolean isLocked() throws RepositoryException {
        if (!LockDeprecation.isLockingSupported()) {
            return false;
        }
        // don't call LockManager.isLocked(String) to avoid duplicate tree resolution
        return sessionDelegate.perform(new SessionOperation<Boolean>("isLocked") {
            @NotNull
            @Override
            public Boolean perform() {
                return dlg.isLocked();
            }
        });
    }

    @Override
    public boolean holdsLock() throws RepositoryException {
        return getLockManager().holdsLock(getPath());
    }

    @Override @NotNull
    public Lock getLock() throws RepositoryException {
        return getLockManager().getLock(getPath());
    }

    @Override @NotNull
    public Lock lock(boolean isDeep, boolean isSessionScoped)
            throws RepositoryException {
        return getLockManager().lock(
                getPath(), isDeep, isSessionScoped, Long.MAX_VALUE, null);
    }

    @Override
    public void unlock() throws RepositoryException {
        getLockManager().unlock(getPath());
    }

    @Override @NotNull
    public NodeIterator getSharedSet() {
        return new NodeIteratorAdapter(singleton(this));
    }

    @Override
    public void removeSharedSet() throws RepositoryException {
        sessionDelegate.performVoid(new ItemWriteOperation<Void>("removeSharedSet") {
            @Override
            public void performVoid() throws RepositoryException {
                // TODO: avoid nested calls
                NodeIterator sharedSet = getSharedSet();
                while (sharedSet.hasNext()) {
                    sharedSet.nextNode().removeShare();
                }
            }
        });
    }

    @Override
    public void removeShare() throws RepositoryException {
        remove();
    }

    /**
     * @see javax.jcr.Node#followLifecycleTransition(String)
     */
    @Override
    public void followLifecycleTransition(String transition) throws RepositoryException {
        throw new UnsupportedRepositoryOperationException("Lifecycle Management is not supported");
    }

    /**
     * @see javax.jcr.Node#getAllowedLifecycleTransistions()
     */
    @Override
    @NotNull
    public String[] getAllowedLifecycleTransistions() throws RepositoryException {
        throw new UnsupportedRepositoryOperationException("Lifecycle Management is not supported");

    }

    //------------------------------------------------------------< internal >---
    public boolean internalIsCheckedOut() throws RepositoryException {
        return getVersionManager().isCheckedOut(dlg);
    }

    @Nullable
    private String getPrimaryTypeName(@NotNull Tree tree) {
        return TreeUtil.getPrimaryTypeName(tree, getReadOnlyTree(tree));
    }

    @NotNull
    private Iterable<String> getMixinTypeNames(@NotNull Tree tree) {
        if (tree.hasProperty(JcrConstants.JCR_MIXINTYPES) || canReadMixinTypes(tree)) {
            return TreeUtil.getMixinTypeNames(tree);
        } else {
            return TreeUtil.getMixinTypeNames(tree, getReadOnlyTree(tree));
        }
    }

    @NotNull
    private LazyValue<Tree> getReadOnlyTree(@NotNull Tree tree) {
        return new LazyValue<Tree>() {
            @Override
            protected Tree createValue() {
                return RootFactory.createReadOnlyRoot(sessionDelegate.getRoot()).getTree(tree.getPath());
            }
        };
    }

    private boolean canReadMixinTypes(@NotNull Tree tree) {
        return sessionContext.getAccessManager().hasPermissions(
                tree, EMPTY_MIXIN_TYPES, Permissions.READ_PROPERTY);
    }

    private EffectiveNodeType getEffectiveNodeType() throws RepositoryException {
        return getNodeTypeManager().getEffectiveNodeType(dlg.getTree());
    }

    private Iterator<Node> nodeIterator(Iterator<NodeDelegate> childNodes) {
        return sessionDelegate.sync(IteratorUtils.transform(
                childNodes,
                nodeDelegate -> new NodeImpl<NodeDelegate>(nodeDelegate, sessionContext)));
    }

    private Iterator<Property> propertyIterator(Iterator<PropertyDelegate> properties) {
        return sessionDelegate.sync(IteratorUtils.transform(
                properties,
                propertyDelegate -> new PropertyImpl(propertyDelegate, sessionContext)));
    }

    private void checkValidWorkspace(String workspaceName)
            throws RepositoryException {
        String[] workspaceNames =
                getSession().getWorkspace().getAccessibleWorkspaceNames();
        if (!asList(workspaceNames).contains(workspaceName)) {
            throw new NoSuchWorkspaceException(
                    "Workspace " + workspaceName + " does not exist");
        }
    }

    private void internalSetPrimaryType(final String nodeTypeName) throws RepositoryException {
        // TODO: figure out the right place for this check
        NodeType nt = getNodeTypeManager().getNodeType(nodeTypeName); // throws on not found
        if (nt.isAbstract() || nt.isMixin()) {
            throw new ConstraintViolationException(getNodePath());
        }
        // TODO: END

        PropertyState state = PropertyStates.createProperty(
                JCR_PRIMARYTYPE, getOakName(nodeTypeName), NAME);
        dlg.setProperty(state, true, true);

        Tree typeRoot = sessionDelegate.getRoot().getTree(NodeTypeConstants.NODE_TYPES_PATH);
        TreeUtil.autoCreateItems(
                dlg.getTree(), typeRoot.getChild(nodeTypeName), typeRoot, sessionDelegate.getAuthInfo().getUserID());

        dlg.setOrderableChildren(nt.hasOrderableChildNodes());
    }

    private Property internalSetProperty(
            final String jcrName, final Value value, final boolean exactTypeMatch)
            throws RepositoryException {
        final String oakName = getOakPathOrThrow(requireNonNull(jcrName));
        final PropertyState state = createSingleState(
                oakName, value, Type.fromTag(value.getType(), false));
        if (value.getType() == PropertyType.STRING) {
            logLargeStringProperties(jcrName, value.getString());
        }
        return perform(new ItemWriteOperation<Property>("internalSetProperty") {
            @Override
            public void checkPreconditions() throws RepositoryException {
                super.checkPreconditions();
                if (!internalIsCheckedOut() && getOPV(dlg.getTree(), state) != OnParentVersionAction.IGNORE) {
                    throw new VersionException(format(
                            "Cannot set property. Node [%s] is checked in.", getNodePath()));
                }
            }
            @NotNull
            @Override
            public Property perform() throws RepositoryException {
                return new PropertyImpl(
                        dlg.setProperty(state, exactTypeMatch, false),
                        sessionContext);
            }

            @Override
            public String toString() {
                return format("setProperty [%s/%s]", dlg.getPath(), jcrName);
            }
        });
    }

    private void logLargeStringProperties(String propertyName, String value) throws RepositoryException {
        if (value.length() > LOG_WARN_STRING_SIZE_THRESHOLD) {
            LOG.warn("String length: {} for property: {} at Node: {} is greater than configured value {}", value.length(), propertyName, this.getPath(), LOG_WARN_STRING_SIZE_THRESHOLD);
        }
    }

    private Property internalSetProperty(
            final String jcrName, final Value[] values,
            final int type, final boolean exactTypeMatch)
            throws RepositoryException {
        final String oakName = getOakPathOrThrow(requireNonNull(jcrName));
        final PropertyState state = createMultiState(
                oakName, compact(values), Type.fromTag(type, true));

        if (values.length > MV_PROPERTY_WARN_THRESHOLD) {
            LOG.warn("Large multi valued property [{}/{}] detected ({} values).",dlg.getPath(), jcrName, values.length);
        }
        for (Value value : values) {
            if (value != null && value.getType() == PropertyType.STRING) {
                logLargeStringProperties(jcrName, value.getString());
            }
        }
        return perform(new ItemWriteOperation<Property>("internalSetProperty") {
            @Override
            public void checkPreconditions() throws RepositoryException {
                super.checkPreconditions();
                if (!internalIsCheckedOut() && getOPV(dlg.getTree(), state) != OnParentVersionAction.IGNORE) {
                    throw new VersionException(format(
                            "Cannot set property. Node [%s] is checked in.", getNodePath()));
                }
            }
            @NotNull
            @Override
            public Property perform() throws RepositoryException {
                return new PropertyImpl(
                        dlg.setProperty(state, exactTypeMatch, false),
                        sessionContext);
            }

            @Override
            public String toString() {
                return format("setProperty [%s/%s]", dlg.getPath(), jcrName);
            }
        });
    }

    /**
     * Removes all {@code null} values from the given array.
     *
     * @param values value array
     * @return value list without {@code null} entries
     */
    private static List<Value> compact(Value[] values) {
        List<Value> list = new ArrayList<>(values.length);
        for (Value value : values) {
            if (value != null) {
                list.add(value);
            }
        }
        return list;
    }


    private Property internalRemoveProperty(final String jcrName)
            throws RepositoryException {
        final String oakName = getOakName(requireNonNull(jcrName));
        return perform(new ItemWriteOperation<Property>("internalRemoveProperty") {
            @Override
            public void checkPreconditions() throws RepositoryException {
                super.checkPreconditions();
                PropertyDelegate property = dlg.getPropertyOrNull(oakName);
                if (property != null &&
                        !internalIsCheckedOut() &&
                        getOPV(dlg.getTree(), property.getPropertyState()) != OnParentVersionAction.IGNORE) {
                    throw new VersionException(format(
                            "Cannot remove property. Node [%s] is checked in.", getNodePath()));
                }
            }
            @NotNull
            @Override
            public Property perform() throws RepositoryException {
                PropertyDelegate property = dlg.getPropertyOrNull(oakName);
                if (property != null) {
                    property.remove();
                } else {
                    // Return an instance which throws on access; see OAK-395
                    property = dlg.getProperty(oakName);
                }
                return new PropertyImpl(property, sessionContext);
            }

            @Override
            public String toString() {
                return format("removeProperty [%s]", jcrName);
            }
        });
    }

    //-----------------------------------------------------< JackrabbitNode >---

    /**
     * Simplified implementation of {@link JackrabbitNode#rename(String)}. In
     * contrast to the implementation in Jackrabbit 2.x which was operating on
     * the NodeState level directly, this implementation does a move plus
     * subsequent reorder on the JCR API due to a missing support for renaming
     * on the OAK API.
     *
     * Note, that this also has an impact on how permissions are enforced: In
     * Jackrabbit 2.x the rename just required permission to modify the child
     * collection on the parent, whereas a move did the full permission check.
     * With this simplified implementation that (somewhat inconsistent) difference
     * has been removed.
     *
     * @param newName The new name of this node.
     * @throws RepositoryException If an error occurs.
     */
    @Override
    public void rename(final String newName) throws RepositoryException {
        if (dlg.isRoot()) {
            throw new RepositoryException("Cannot rename the root node");
        }

        final String name = getName();
        if (newName.equals(name)) {
            // nothing to do
            return;
        }

        sessionDelegate.performVoid(new ItemWriteOperation<Void>("rename") {
            @Override
            public void performVoid() throws RepositoryException {
                Node parent = getParent();
                String beforeName = null;

                if (isOrderable(parent)) {
                    // remember position amongst siblings
                    NodeIterator nit = parent.getNodes();
                    while (nit.hasNext()) {
                        Node child = nit.nextNode();
                        if (name.equals(child.getName())) {
                            if (nit.hasNext()) {
                                beforeName = nit.nextNode().getName();
                            }
                            break;
                        }
                    }
                }

                String srcPath = getPath();
                String destPath = '/' + newName;
                String parentPath = parent.getPath();
                if (!"/".equals(parentPath)) {
                    destPath = parentPath + destPath;
                }
                sessionContext.getSession().move(srcPath, destPath);

                if (beforeName != null) {
                    // restore position within siblings
                    parent.orderBefore(newName, beforeName);
                }
            }
        });
    }

    private static boolean isOrderable(Node node) throws RepositoryException {
        if (node.getPrimaryNodeType().hasOrderableChildNodes()) {
            return true;
        }

        NodeType[] types = node.getMixinNodeTypes();
        for (NodeType type : types) {
            if (type.hasOrderableChildNodes()) {
                return true;
            }
        }
        return false;
    }

    /**
     * Simplified implementation of the {@link org.apache.jackrabbit.api.JackrabbitNode#setMixins(String[])}
     * method that adds all mixin types that are not yet present on this node
     * and removes all mixins that are no longer contained in the specified
     * array. Note, that this implementation will not work exactly like the
     * variant in Jackrabbit 2.x which first created the effective node type
     * and adjusted the set of child items accordingly.
     *
     * @param mixinNames
     * @throws RepositoryException
     */
    @Override
    public void setMixins(String[] mixinNames) throws RepositoryException {
        final Set<String> oakTypeNames = new LinkedHashSet<>();
        for (String mixinName : mixinNames) {
            oakTypeNames.add(getOakName(requireNonNull(mixinName)));
        }
        sessionDelegate.performVoid(new ItemWriteOperation<Void>("setMixins") {
            @Override
            public void checkPreconditions() throws RepositoryException {
                super.checkPreconditions();
                if (!internalIsCheckedOut()) {
                    throw new VersionException(format("Cannot set mixin types. Node [%s] is checked in.", getNodePath()));
                }

                // check for NODE_TYPE_MANAGEMENT permission here as we cannot
                // distinguish between a combination of removeMixin and addMixin
                // and Node#remove plus subsequent addNode when it comes to
                // autocreated properties like jcr:create, jcr:uuid and so forth.
                PropertyDelegate mixinProp = dlg.getPropertyOrNull(JCR_MIXINTYPES);
                if (mixinProp != null) {
                    sessionContext.getAccessManager().checkPermissions(dlg.getTree(), mixinProp.getPropertyState(), Permissions.NODE_TYPE_MANAGEMENT);
                }
            }
            @Override
            public void performVoid() throws RepositoryException {
                dlg.setMixins(oakTypeNames);
            }
        });
    }

    @Override
    public @Nullable JackrabbitNode getNodeOrNull(@NotNull String relPath) throws RepositoryException {
        final String oakPath = getOakPathOrThrowNotFound(relPath);
        return sessionDelegate.performNullable(new NodeOperation<JackrabbitNode>(dlg, "getNodeOrNull") {
            @Nullable
            @Override
            public JackrabbitNode performNullable() throws RepositoryException {
                NodeDelegate nd = node.getChild(oakPath);
                if (nd == null) {
                    return null;
                } else {
                    return createNode(nd, sessionContext);
                }
            }
        });
    }

    @Override
    public @Nullable Property getPropertyOrNull(@NotNull String relPath) throws RepositoryException {
        final String oakPath = getOakPathOrThrowNotFound(relPath);
        return sessionDelegate.performNullable(new NodeOperation<PropertyImpl>(dlg, "getPropertyOrNull") {
            @Nullable
            @Override
            public PropertyImpl performNullable() throws RepositoryException {
                PropertyDelegate pd = node.getPropertyOrNull(oakPath);
                if (pd == null) {
                    return null;
                } else {
                    return new PropertyImpl(pd, sessionContext);
                }
            }
        });
    }

    @Override
    public @Nullable Node getParentOrNull() throws RepositoryException {
        return sessionDelegate.performNullable(new NodeOperation<Node>(dlg, "getParentOrNull") {
            @Nullable
            @Override
            public Node performNullable() throws RepositoryException {
                if (node.isRoot()) {
                    return null;
                } else {
                    NodeDelegate parent = node.getParent();
                    if (parent == null) {
                        return null;
                    }
                    return createNode(parent, sessionContext);
                }
            }
        });
    }

    @Override
    public long getEstimatedDescendantNodeCount() {
        NodeCounterMBean nodeCounterMBean = sessionContext.getNodeCounterMBeanSupplier().get();
        return nodeCounterMBean != null ? nodeCounterMBean.getEstimatedNodeCount(dlg.getPath()) : -1;
    }

    /**
     * Provide current node path. Should be invoked from within
     * the SessionDelegate#perform and preferred instead of getPath
     * as it provides direct access to path
     */
    private String getNodePath(){
        return dlg.getPath();
    }

    private int getOPV(Tree nodeTree, PropertyState property)
            throws RepositoryException {
        return getNodeTypeManager().getDefinition(nodeTree,
                property, false).getOnParentVersion();

    }

    private static class PropertyIteratorDelegate {
        private final NodeDelegate node;
        private final Predicate<PropertyDelegate> predicate;

        PropertyIteratorDelegate(NodeDelegate node, Predicate<PropertyDelegate> predicate) {
            this.node = node;
            this.predicate = predicate;
        }

        public Iterator<PropertyDelegate> iterator() throws InvalidItemStateException {
            return IteratorUtils.filter(node.getProperties(), predicate::test);
        }

        public long getSize() {
            try {
                return IteratorUtils.size(iterator());
            } catch (InvalidItemStateException e) {
                throw new IllegalStateException(
                        "This iterator is no longer valid", e);
            }
        }

    }
}
