<?php 
 
namespace Jackalope; 
 
use ArrayIterator; 
use Iterator; 
use IteratorAggregate; 
use Exception; 
use InvalidArgumentException; 
use Jackalope\NodeType\NodeType; 
use LogicException; 
use PHPCR\AccessDeniedException; 
use PHPCR\Lock\LockException; 
use PHPCR\NamespaceException; 
use PHPCR\NodeType\NodeDefinitionInterface; 
use PHPCR\NodeType\NodeTypeInterface; 
use PHPCR\PropertyType; 
use PHPCR\NodeInterface; 
use PHPCR\NodeType\ConstraintViolationException; 
use PHPCR\NodeType\NoSuchNodeTypeException; 
use PHPCR\RepositoryException; 
use PHPCR\PathNotFoundException; 
use PHPCR\ItemNotFoundException; 
use PHPCR\InvalidItemStateException; 
use PHPCR\ItemExistsException; 
use PHPCR\UnsupportedRepositoryOperationException; 
use PHPCR\Util\PathHelper; 
use PHPCR\Util\NodeHelper; 
use PHPCR\Util\UUIDHelper; 
use PHPCR\ValueFormatException; 
use PHPCR\Version\VersionException; 
 
/** 
 * {@inheritDoc} 
 * 
 * @license http://www.apache.org/licenses Apache License Version 2.0, January 2004 
 * @license http://opensource.org/licenses/MIT MIT License 
 * 
 * @api 
 */ 
class Node extends Item implements IteratorAggregate, NodeInterface 
{ 
    /** 
     * The index if this is a same-name sibling. 
     * 
     * TODO: fully implement same-name siblings 
     * @var int 
     */ 
    protected $index = 1; 
 
    /** 
     * The primary type name of this node 
     * 
     * @var string 
     */ 
    protected $primaryType; 
 
    /** 
     * mapping of property name to PropertyInterface objects. 
     * 
     * all properties are instantiated in the constructor 
     * 
     * OPTIMIZE: lazy instantiate property objects, just have local array of values 
     * 
     * @var Property[] 
     */ 
    protected $properties = []; 
 
    /** 
     * keep track of properties to be deleted until the save operation was successful. 
     * 
     * this is needed in order to track deletions in case of refresh 
     * 
     * keys are the property names, values the properties (in state deleted) 
     */ 
    protected $deletedProperties = []; 
 
    /** 
     * ordered list of the child node names 
     * 
     * @var array 
     */ 
    protected $nodes = []; 
 
    /** 
     * ordered list of the child node names as known to be at the backend 
     * 
     * used to calculate reordering operations if orderBefore() was used 
     * 
     * @var array 
     */ 
    protected $originalNodesOrder = null; 
 
    /** 
     * Cached instance of the node definition that defines this node 
     * 
     * @var NodeDefinitionInterface 
     * @see Node::getDefinition() 
     */ 
    protected $definition; 
 
    /** 
     * Create a new node instance with data from the storage layer 
     * 
     * This is only to be called by the Factory::get() method even inside the 
     * Jackalope implementation to allow for custom implementations of Nodes. 
     * 
     * @param FactoryInterface $factory       the object factory 
     * @param array            $rawData       in the format as returned from TransportInterface::getNode 
     * @param string           $path          the absolute path of this node 
     * @param Session          $session 
     * @param ObjectManager    $objectManager 
     * @param boolean          $new           set to true if this is a new node being created. 
     *      Defaults to false which means the node is loaded from storage. 
     * 
     * @see TransportInterface::getNode() 
     * 
     * @throws RepositoryException 
     * 
     * @private 
     */ 
    public function __construct(FactoryInterface $factory, $rawData, $path, Session $session, ObjectManager $objectManager, $new = false) 
    { 
        parent::__construct($factory, $path, $session, $objectManager, $new); 
        $this->isNode = true; 
 
        $this->parseData($rawData, false); 
    } 
 
    /** 
     * Initialize or update this object with raw data from backend. 
     * 
     * @param array $rawData in the format as returned from Jackalope\Transport\TransportInterface 
     * @param boolean $update whether to initialize this object or update 
     * @param boolean $keepChanges only used if $update is true, same as $keepChanges in refresh() 
     * 
     * @see Node::__construct() 
     * @see Node::refresh() 
     * 
     * @throws \InvalidArgumentException 
     * @throws LockException 
     * @throws ConstraintViolationException 
     * @throws RepositoryException 
     * @throws ValueFormatException 
     * @throws VersionException 
     */ 
    private function parseData($rawData, $update, $keepChanges = false) 
    { 
        //TODO: refactor to use hash array instead of stdClass struct 
 
        if ($update) { 
            // keep backup of old state so we can remove what needs to be removed 
            $oldNodes = array_flip(array_values($this->nodes)); 
            $oldProperties = $this->properties; 
        } 
        /* 
         * we collect all nodes coming from the backend. if we update with 
         * $keepChanges, we use this to update the node list rather than losing 
         * reorders 
         * 
         * properties are easy as they are not ordered. 
         */ 
        $nodesInBackend = []; 
 
        foreach ($rawData as $key => $value) { 
            $node = false; // reset to avoid trouble 
            if (is_object($value)) { 
                // this is a node. add it if 
                if (! $update // init new node 
                    || ! $keepChanges // want to discard changes 
                    || isset($oldNodes[$key]) // it was already existing before reloading 
                    || ! ($node = $this->objectManager->getCachedNode($this->path . '/' . $key)) // we know nothing about it 
                ) { 
                    // for all those cases, if the node was moved away or is deleted in current session, we do not add it 
                    if (! $this->objectManager->isNodeMoved($this->path . '/' . $key) 
                        && ! $this->objectManager->isNodeDeleted($this->path . '/' . $key) 
                    ) { 
                        // otherwise we (re)load a node from backend but a child has been moved away already 
                        $nodesInBackend[] = $key; 
                    } 
                } 
                if ($update) { 
                    unset($oldNodes[$key]); 
                } 
            } else { 
                //property or meta information 
 
                /* Property type declarations start with :, the value then is 
                 * the type string from the NodeType constants. We skip that and 
                 * look at the type when we encounter the value of the property. 
                 * 
                 * If its a binary data, we only get the type declaration and 
                 * no data. Then the $value of the type declaration is not the 
                 * type string for binary, but the number of bytes of the 
                 * property - resp. array of number of bytes. 
                 * 
                 * The magic property ::NodeIteratorSize tells this node has no 
                 * children. Ignore that info for now. We might optimize with 
                 * this info once we do prefetch nodes. 
                 */ 
                if (0 === strpos($key, ':')) { 
                    if ((is_int($value) || is_array($value)) 
                         && $key != '::NodeIteratorSize' 
                    ) { 
                        // This is a binary property and we just got its length with no data 
                        $key = substr($key, 1); 
                        if (!isset($rawData->$key)) { 
                            $binaries[$key] = $value; 
                            if ($update) { 
                                unset($oldProperties[$key]); 
                            } 
                            if (isset($this->properties[$key])) { 
                                // refresh existing binary, this will only happen in update 
                                // only update length 
                                if (! ($keepChanges && $this->properties[$key]->isModified())) { 
                                    $this->properties[$key]->_setLength($value); 
                                    if ($this->properties[$key]->isDirty()) { 
                                        $this->properties[$key]->setClean(); 
                                    } 
                                } 
                            } else { 
                                // this will always fall into the creation mode 
                                $this->_setProperty($key, $value, PropertyType::BINARY, true); 
                            } 
                        } 
                    } //else this is a type declaration 
 
                    //skip this entry (if its binary, its already processed 
                    continue; 
                } 
 
                if ($update && array_key_exists($key, $this->properties)) { 
                    unset($oldProperties[$key]); 
                    $prop = $this->properties[$key]; 
                    if ($keepChanges && $prop->isModified()) { 
                        continue; 
                    } 
                } elseif ($update && array_key_exists($key, $this->deletedProperties)) { 
                    if ($keepChanges) { 
                        // keep the delete 
                        continue; 
                    } else { 
                        // restore the property 
                        $this->properties[$key] = $this->deletedProperties[$key]; 
                        $this->properties[$key]->setClean(); 
                        // now let the loop update the value. no need to talk to ObjectManager as it 
                        // does not store property deletions 
                    } 
                } 
 
                switch ($key) { 
                    case 'jcr:index': 
                        $this->index = $value; 
                        break; 
                    case 'jcr:primaryType': 
                        $this->primaryType = $value; 
                        // type information is exposed as property too, 
                        // although there exist more specific methods 
                        $this->_setProperty('jcr:primaryType', $value, PropertyType::NAME, true); 
                        break; 
                    case 'jcr:mixinTypes': 
                        // type information is exposed as property too, 
                        // although there exist more specific methods 
                        $this->_setProperty($key, $value, PropertyType::NAME, true); 
                        break; 
 
                    // OPTIMIZE: do not instantiate properties until needed 
                    default: 
                        if (isset($rawData->{':' . $key})) { 
                            /* 
                             * this is an inconsistency between jackrabbit and 
                             * dbal transport: jackrabbit has type name, dbal 
                             * delivers numeric type. 
                             * we should eventually fix the format returned by 
                             * transport and either have jackrabbit transport 
                             * do the conversion or let dbal store a string 
                             * value instead of numerical. 
                             */ 
                            $type = is_numeric($rawData->{':' . $key}) 
                                    ? $rawData->{':' . $key} 
                                    : PropertyType::valueFromName($rawData->{':' . $key}); 
                        } else { 
                            $type = $this->valueConverter->determineType($value); 
                        } 
                        $this->_setProperty($key, $value, $type, true); 
                        break; 
                } 
            } 
        } 
 
        if ($update) { 
            if ($keepChanges) { 
                // we keep changes. merge new nodes to the right place 
                $previous = null; 
                $newFromBackend = array_diff($nodesInBackend, array_intersect($this->nodes, $nodesInBackend)); 
 
                foreach ($newFromBackend as $name) { 
                    $pos = array_search($name, $nodesInBackend); 
                    if (is_array($this->originalNodesOrder)) { 
                        // update original order to send the correct reorderings 
                        array_splice($this->originalNodesOrder, $pos, 0, $name); 
                    } 
                    if ($pos === 0) { 
                        array_unshift($this->nodes, $name); 
                    } else { 
                        // do we find the predecessor of the new node in the list? 
                        $insert = array_search($nodesInBackend[$pos-1], $this->nodes); 
                        if (false !== $insert) { 
                            array_splice($this->nodes, $insert + 1, 0, $name); 
                        } else { 
                            // failed to find predecessor, add to the end 
                            $this->nodes[] = $name; 
                        } 
                    } 
                } 
            } else { 
                // discard changes, just overwrite node list 
                $this->nodes = $nodesInBackend; 
                $this->originalNodesOrder = null; 
            } 
            foreach ($oldProperties as $name => $property) { 
                if (! ($keepChanges && ($property->isNew()))) { 
                    // may not call remove(), we don't want another delete with 
                    // the backend to be attempted 
                    $this->properties[$name]->setDeleted(); 
                    unset($this->properties[$name]); 
                } 
            } 
 
            // notify nodes that where not received again that they disappeared 
            foreach ($oldNodes as $name => $index) { 
                if ($this->objectManager->purgeDisappearedNode($this->path . '/' . $name, $keepChanges)) { 
                    // drop, it was not a new child 
                    if ($keepChanges) { // otherwise we overwrote $this->nodes with the backend 
                        $id = array_search($name, $this->nodes); 
                        if (false !== $id) { 
                            unset($this->nodes[$id]); 
                        } 
                    } 
                } 
            } 
        } else { 
            // new node loaded from backend 
            $this->nodes = $nodesInBackend; 
        } 
    } 
 
    /** 
     * Creates a new node at the specified $relPath 
     * 
     * {@inheritDoc} 
     * 
     * In Jackalope, the child node type definition is immediately applied if no 
     * primaryNodeTypeName is specified. 
     * 
     * The PathNotFoundException and ConstraintViolationException are thrown 
     * immediately. 
     * Version and Lock related exceptions are delayed until save. 
     * 
     * @api 
     */ 
    public function addNode($relPath, $primaryNodeTypeName = null) 
    { 
        $relPath = (string)$relPath; 
 
        $this->checkState(); 
 
        $ntm = $this->session->getWorkspace()->getNodeTypeManager(); 
 
        // are we not the immediate parent? 
        if (strpos($relPath, '/') !== false) { 
            // forward to real parent 
 
            $relPath = PathHelper::absolutizePath($relPath, $this->getPath(), true); 
            $parentPath = PathHelper::getParentPath($relPath); 
            $newName = PathHelper::getNodeName($relPath); 
 
            try { 
                $parentNode = $this->objectManager->getNodeByPath($parentPath); 
            } catch (ItemNotFoundException $e) { 
                //we have to throw a different exception if there is a property 
                // with that name than if there is nothing at the path at all. 
                // lets see if the property exists 
                if ($this->session->propertyExists($parentPath)) { 
                    throw new ConstraintViolationException("Node '{$this->path}': Not allowed to add a node below property at $parentPath"); 
                } 
 
                throw new PathNotFoundException($e->getMessage(), $e->getCode(), $e); 
            } 
 
            return $parentNode->addNode($newName, $primaryNodeTypeName); 
        } 
 
        if (null === $primaryNodeTypeName) { 
            if ($this->primaryType === 'rep:root') { 
                $primaryNodeTypeName = 'nt:unstructured'; 
            } else { 
                $type = $ntm->getNodeType($this->primaryType); 
                $nodeDefinitions = $type->getChildNodeDefinitions(); 
                foreach ($nodeDefinitions as $def) { 
                    if (!is_null($def->getDefaultPrimaryType())) { 
                        $primaryNodeTypeName = $def->getDefaultPrimaryTypeName(); 
                        break; 
                    } 
                } 
            } 
 
            if (is_null($primaryNodeTypeName)) { 
                throw new ConstraintViolationException("No matching child node definition found for `$relPath' in type `{$this->primaryType}' for node '{$this->path}'. Please specify the type explicitly."); 
            } 
        } 
 
        // create child node 
        //sanity check: no index allowed. TODO: we should verify this is a valid node name 
        if (false !== strpos($relPath, ']')) { 
            throw new RepositoryException("The node '{$this->path}' does not allow an index in name of newly created node: $relPath"); 
        } 
        if (in_array($relPath, $this->nodes, true)) { 
            throw new ItemExistsException("The node '{$this->path}' already has a child named '$relPath''."); //TODO: same-name siblings if nodetype allows for them 
        } 
 
        $data = ['jcr:primaryType' => $primaryNodeTypeName]; 
        $path = $this->getChildPath($relPath); 
        $node = $this->factory->get(Node::class, [$data, $path, $this->session, $this->objectManager, true]); 
 
        $this->addChildNode($node, false); // no need to check the state, we just checked when entering this method 
        $this->objectManager->addNode($path, $node); 
 
        if (is_array($this->originalNodesOrder)) { 
            // new nodes are added at the end 
            $this->originalNodesOrder[] = $relPath; 
        } 
        //by definition, adding a node sets the parent to modified 
        $this->setModified(); 
 
        return $node; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     * @throws InvalidArgumentException 
     * @throws ItemExistsException 
     * @throws PathNotFoundException 
     * @throws RepositoryException 
     */ 
    public function addNodeAutoNamed($nameHint = null, $primaryNodeTypeName = null) 
    { 
        $name = NodeHelper::generateAutoNodeName( 
            $this->nodes, 
            $this->session->getWorkspace()->getNamespaceRegistry()->getNamespaces(), 
            'jcr', 
            $nameHint 
        ); 
 
        return $this->addNode($name, $primaryNodeTypeName); 
    } 
 
    /** 
     * Jackalope implements this feature and updates the position of the 
     * existing child at srcChildRelPath to be in the list immediately before 
     * destChildRelPath. 
     * 
     * {@inheritDoc} 
     * 
     * Jackalope has no implementation-specific ordering restriction so no 
     * \PHPCR\ConstraintViolationException is expected. VersionException and 
     * LockException are not tested immediately but thrown on save. 
     * 
     * @api 
     */ 
    public function orderBefore($srcChildRelPath, $destChildRelPath) 
    { 
        if ($srcChildRelPath === $destChildRelPath) { 
            //nothing to move 
            return; 
        } 
 
        if (null === $this->originalNodesOrder) { 
            $this->originalNodesOrder = $this->nodes; 
        } 
 
        $this->nodes = NodeHelper::orderBeforeArray($srcChildRelPath, $destChildRelPath, $this->nodes); 
        $this->setModified(); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws PathNotFoundException 
     * 
     * @api 
     * @throws AccessDeniedException 
     * @throws ItemNotFoundException 
     * @throws \InvalidArgumentException 
     */ 
    public function rename($newName) 
    { 
        $names = (array) $this->getParent()->getNodeNames(); 
        $pos = array_search($this->name, $names); 
        $next = isset($names[$pos + 1]) ? $names[$pos + 1] : null; 
 
        $newPath = $this->parentPath . '/' . $newName; 
 
        if (substr($newPath, 0, 2) === '//') { 
            $newPath = substr($newPath, 1); 
        } 
 
        $this->session->move($this->path, $newPath); 
        if ($next) { 
            $this->getParent()->orderBefore($newName, $next); 
        } 
    } 
 
    /** 
     * Determine whether the children of this node need to be reordered 
     * 
     * @return boolean 
     * 
     * @private 
     */ 
    public function needsChildReordering() 
    { 
        return (bool) $this->originalNodesOrder; 
    } 
 
    /** 
     * Returns the orderBefore commands to be applied to the childnodes 
     * to get from the original order to the new one 
     * 
     * @return array of arrays with 2 fields: name of node to order before second name 
     * 
     * @throws AccessDeniedException 
     * @throws ItemNotFoundException 
     * 
     * @private 
     */ 
    public function getOrderCommands() 
    { 
        if (! $this->originalNodesOrder) { 
            return []; 
        } 
 
        $reorders = NodeHelper::calculateOrderBefore($this->originalNodesOrder, $this->nodes); 
        $this->originalNodesOrder = null; 
 
        return $reorders; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @param boolean $validate When false, node types are not asked to validate 
     *                          whether operation is allowed 
     * 
     * @throws InvalidItemStateException 
     * @throws NamespaceException 
     * @throws \InvalidArgumentException 
     * @throws AccessDeniedException 
     * @throws ItemNotFoundException 
     * 
     * @api 
     */ 
    public function setProperty($name, $value, $type = PropertyType::UNDEFINED, $validate = true) 
    { 
        $this->checkState(); 
 
        // abort early when the node value is not changed 
        // for multivalue, === is only true when array keys and values match. this is exactly what we need. 
        try { 
            if (array_key_exists($name, $this->properties) && $this->properties[$name]->getValue() === $value) { 
                return $this->properties[$name]; 
            } 
        } catch (RepositoryException $e) { 
            // if anything goes wrong trying to get the property value, move on and don't return early 
        } 
 
        if ($validate && 'jcr:uuid' === $name && !$this->isNodeType('mix:referenceable')) { 
            throw new ConstraintViolationException('You can only change the uuid of newly created nodes that have "referenceable" mixin.'); 
        } 
 
        if ($validate) { 
            if (is_array($value)) { 
                foreach ($value as $key => $v) { 
                    if (null === $v) { 
                        unset($value[$key]); 
                    } 
                } 
            } 
            $types = $this->getMixinNodeTypes(); 
            array_push($types, $this->getPrimaryNodeType()); 
            if (null !== $value) { 
                $exception = null; 
                foreach ($types as $nt) { 
                    /** @var $nt NodeType */ 
                    try { 
                        $nt->canSetProperty($name, $value, true); 
                        $exception = null; 
                        break; // exit loop, we found a valid definition 
                    } catch (RepositoryException $e) { 
                        if (null === $exception) { 
                            $exception = $e; 
                        } 
                    } 
                } 
                if (null !== $exception) { 
                    $types = 'Primary type '.$this->primaryType; 
                    if (isset($this->properties['jcr:mixinTypes'])) { 
                        $types .= ', mixins '.implode(',', $this->getPropertyValue('jcr:mixinTypes', PropertyType::STRING)); 
                    } 
                    $msg = sprintf('Can not set property %s on node %s. Node types do not allow for this: %s', $name, $this->path, $types); 
 
                    throw new ConstraintViolationException($msg, 0, $exception); 
                } 
            } else { 
                // $value is null for property removal 
                // if any type forbids, throw exception 
                foreach ($types as $nt) { 
                    /** @var $nt \Jackalope\NodeType\NodeType */ 
                    $nt->canRemoveProperty($name, true); 
                } 
            } 
        } 
 
        //try to get a namespace for the set property 
        if (strpos($name, ':') !== false) { 
            list($prefix) = explode(':', $name); 
            //Check if the namespace exists. If not, throw an NamespaceException 
            $this->session->getNamespaceURI($prefix); 
        } 
 
        if (is_null($value)) { 
            if (isset($this->properties[$name])) { 
                $this->properties[$name]->remove(); 
            } 
 
            return null; 
        } 
 
        // if the property is the UUID, then register the UUID against the path 
        // of this node. 
        if ('jcr:uuid' === $name) { 
            $this->objectManager->registerUuid($value, $this->getPath()); 
        } 
 
        return $this->_setProperty($name, $value, $type, false); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * 
     * @api 
     */ 
    public function getNode($relPath) 
    { 
        $this->checkState(); 
 
        $relPath = (string) $relPath; 
 
        if ('' === $relPath || '/' === $relPath[0]) { 
            throw new PathNotFoundException("$relPath is not a relative path"); 
        } 
 
        try { 
            $node = $this->objectManager->getNodeByPath(PathHelper::absolutizePath($relPath, $this->path)); 
        } catch (ItemNotFoundException $e) { 
            throw new PathNotFoundException($e->getMessage(), $e->getCode(), $e); 
        } 
 
        return $node; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getNodes($nameFilter = null, $typeFilter = null) 
    { 
        $this->checkState(); 
 
        $names = self::filterNames($nameFilter, $this->nodes); 
        $result = []; 
 
        if (count($names)) { 
            $paths = []; 
            foreach ($names as $name) { 
                $paths[] = PathHelper::absolutizePath($name, $this->path); 
            } 
            $nodes = $this->objectManager->getNodesByPath($paths, Node::class, $typeFilter); 
 
            // OPTIMIZE if we lazy-load in ObjectManager we should not do this loop 
            foreach ($nodes as $node) { 
                $result[$node->getName()] = $node; 
            } 
        } 
 
        return new ArrayIterator($result); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getNodeNames($nameFilter = null, $typeFilter = null) 
    { 
        $this->checkState(); 
 
        if (null !== $typeFilter) { 
            return $this->objectManager->filterChildNodeNamesByType($this, $nameFilter, $typeFilter); 
        } 
 
        $names = self::filterNames($nameFilter, $this->nodes); 
 
        return new ArrayIterator($names); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * 
     * @api 
     */ 
    public function getProperty($relPath) 
    { 
        $this->checkState(); 
 
        if (false === strpos($relPath, '/')) { 
            if (!isset($this->properties[$relPath])) { 
                throw new PathNotFoundException("Property $relPath in ".$this->path); 
            } 
            if ($this->properties[$relPath]->isDeleted()) { 
                throw new PathNotFoundException("Property '$relPath' of " . $this->path . ' is deleted'); 
            } 
 
            return $this->properties[$relPath]; 
        } 
 
        return $this->session->getProperty($this->getChildPath($relPath)); 
    } 
 
    /** 
     * This method is only meant for the transport to be able to still build a 
     * store request for afterwards deleted nodes to support the operationslog. 
     * 
     * @return Property[] with just the jcr:primaryType property in it 
     * 
     * @see \Jackalope\Transport\WritingInterface::storeNodes 
     * 
     * @throws InvalidItemStateException 
     * @throws RepositoryException 
     * 
     * @private 
     */ 
    public function getPropertiesForStoreDeletedNode() 
    { 
        if (! $this->isDeleted()) { 
            throw new InvalidItemStateException('You are not supposed to call this on a not deleted node'); 
        } 
        $myProperty = $this->properties['jcr:primaryType']; 
        $myProperty->setClean(); 
        $path = $this->getChildPath('jcr:primaryType'); 
        $property = $this->factory->get( 
            'Property', 
            [['type' => $myProperty->getType(), 'value' => $myProperty->getValue()], 
                $path, 
                $this->session, 
                $this->objectManager 
            ] 
        ); 
        $myProperty->setDeleted(); 
 
        return ['jcr:primaryType' => $property]; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * @throws \InvalidArgumentException 
     * 
     * @api 
     */ 
    public function getPropertyValue($name, $type = null) 
    { 
        $this->checkState(); 
 
        $val = $this->getProperty($name)->getValue(); 
        if (null !== $type) { 
            $val = $this->valueConverter->convertType($val, $type); 
        } 
 
        return $val; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws \InvalidArgumentException 
     * 
     * @throws InvalidItemStateException 
     * @throws PathNotFoundException 
     * @throws ValueFormatException 
     * 
     * @api 
     */ 
    public function getPropertyValueWithDefault($relPath, $defaultValue) 
    { 
        if ($this->hasProperty($relPath)) { 
            return $this->getPropertyValue($relPath); 
        } 
 
        return $defaultValue; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getProperties($nameFilter = null) 
    { 
        $this->checkState(); 
 
        //OPTIMIZE: lazy iterator? 
        $names = self::filterNames($nameFilter, array_keys($this->properties)); 
        $result = []; 
 
        foreach ($names as $name) { 
            //we know for sure the properties exist, as they come from the 
            // array keys of the array we are accessing 
            $result[$name] = $this->properties[$name]; 
        } 
 
        return new ArrayIterator($result); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws \InvalidArgumentException 
     * @throws InvalidItemStateException 
     * @throws ValueFormatException 
     * @throws ItemNotFoundException 
     * 
     * @api 
     */ 
    public function getPropertiesValues($nameFilter = null, $dereference = true) 
    { 
        $this->checkState(); 
 
        // OPTIMIZE: do not create properties in constructor, go over array here 
        $names = self::filterNames($nameFilter, array_keys($this->properties)); 
        $result = []; 
 
        foreach ($names as $name) { 
            //we know for sure the properties exist, as they come from the 
            // array keys of the array we are accessing 
            $type = $this->properties[$name]->getType(); 
            if (! $dereference && 
                    (PropertyType::REFERENCE === $type 
                    || PropertyType::WEAKREFERENCE === $type 
                    || PropertyType::PATH === $type) 
            ) { 
                $result[$name] = $this->properties[$name]->getString(); 
            } else { 
                // OPTIMIZE: collect the paths and call objectmanager->getNodesByPath once 
                $result[$name] = $this->properties[$name]->getValue(); 
            } 
        } 
 
        return $result; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getPrimaryItem() 
    { 
        try { 
            $primary_item = null; 
            $item_name = $this->getPrimaryNodeType()->getPrimaryItemName(); 
 
            if ($item_name !== null) { 
                $primary_item = $this->session->getItem($this->path . '/' . $item_name); 
            } 
        } catch (Exception $ex) { 
            throw new RepositoryException("An error occured while reading the primary item of the node '{$this->path}': " . $ex->getMessage()); 
        } 
 
        if ($primary_item === null) { 
            throw new ItemNotFoundException("No primary item found for node '{$this->path}'"); 
        } 
 
        return $primary_item; 
    } 
 
    /** 
     * @return string a universally unique id. 
     */ 
    protected function generateUuid() 
    { 
        return UUIDHelper::generateUUID(); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws \InvalidArgumentException 
     * 
     * @throws AccessDeniedException 
     * @throws InvalidItemStateException 
     * @throws ItemNotFoundException 
     * @throws LockException 
     * @throws NamespaceException 
     * @throws ConstraintViolationException 
     * @throws ValueFormatException 
     * @throws VersionException 
     * @throws PathNotFoundException 
     * 
     * @api 
     */ 
    public function getIdentifier() 
    { 
        $this->checkState(); 
 
        if ($this->isNodeType('mix:referenceable')) { 
            if (empty($this->properties['jcr:uuid'])) { 
                $this->setProperty('jcr:uuid', $this->generateUuid()); 
            } 
 
            return $this->getPropertyValue('jcr:uuid'); 
        } 
 
        return $this->getPath(); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getIndex() 
    { 
        $this->checkState(); 
 
        return $this->index; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getReferences($name = null) 
    { 
        $this->checkState(); 
 
        return $this->objectManager->getReferences($this->path, $name); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getWeakReferences($name = null) 
    { 
        $this->checkState(); 
 
        return $this->objectManager->getWeakReferences($this->path, $name); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function hasNode($relPath) 
    { 
        $this->checkState(); 
 
        if (false === strpos($relPath, '/')) { 
            return array_search($relPath, $this->nodes) !== false; 
        } 
 
        if (! strlen($relPath) || $relPath[0] === '/') { 
            throw new InvalidArgumentException("'$relPath' is not a relative path"); 
        } 
 
        return $this->session->nodeExists($this->getChildPath($relPath)); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function hasProperty($relPath) 
    { 
        $this->checkState(); 
 
        if (false === strpos($relPath, '/')) { 
            return isset($this->properties[$relPath]); 
        } 
        if (! strlen($relPath) || $relPath[0] === '/') { 
            throw new InvalidArgumentException("'$relPath' is not a relative path"); 
        } 
 
        return $this->session->propertyExists($this->getChildPath($relPath)); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function hasNodes() 
    { 
        $this->checkState(); 
 
        return count($this->nodes) !== 0; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function hasProperties() 
    { 
        $this->checkState(); 
 
        return count($this->properties) !== 0; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getPrimaryNodeType() 
    { 
        $this->checkState(); 
 
        $ntm = $this->session->getWorkspace()->getNodeTypeManager(); 
 
        return $ntm->getNodeType($this->primaryType); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getMixinNodeTypes() 
    { 
        $this->checkState(); 
 
        if (!isset($this->properties['jcr:mixinTypes'])) { 
            return []; 
        } 
 
        $res = []; 
        $ntm = $this->session->getWorkspace()->getNodeTypeManager(); 
 
        foreach ($this->properties['jcr:mixinTypes']->getValue() as $type) { 
            $res[] = $ntm->getNodeType($type); 
        } 
 
        return $res; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function isNodeType($nodeTypeName) 
    { 
        $this->checkState(); 
 
        // is it the primary type? 
        if ($this->primaryType === $nodeTypeName) { 
            return true; 
        } 
        // is it one of the mixin types? 
        if (isset($this->properties['jcr:mixinTypes'])) { 
            if (in_array($nodeTypeName, $this->properties["jcr:mixinTypes"]->getValue())) { 
                return true; 
            } 
        } 
        $ntm = $this->session->getWorkspace()->getNodeTypeManager(); 
        // is the primary type a subtype of the type? 
        if ($ntm->getNodeType($this->primaryType)->isNodeType($nodeTypeName)) { 
            return true; 
        } 
        // if there are no mixin types, then we now know this node is not of that type 
        if (! isset($this->properties["jcr:mixinTypes"])) { 
            return false; 
        } 
        // is it an ancestor of any of the mixin types? 
        foreach ($this->properties['jcr:mixinTypes'] as $mixin) { 
            if ($ntm->getNodeType($mixin)->isNodeType($nodeTypeName)) { 
                return true; 
            } 
        } 
 
        return false; 
    } 
 
    /** 
     * Changes the primary node type of this node to nodeTypeName. 
     * 
     * {@inheritDoc} 
     * 
     * Jackalope only validates type conflicts on save. 
     * 
     * @throws InvalidItemStateException 
     * 
     * @api 
     */ 
    public function setPrimaryType($nodeTypeName) 
    { 
        $this->checkState(); 
 
        throw new NotImplementedException('Write'); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * Jackalope validates type conflicts only on save, not immediately. 
     * It is possible to add mixin types after the first save. 
     * 
     * @api 
     */ 
    public function addMixin($mixinName) 
    { 
        // Check if mixinName exists as a mixin type 
        $typemgr = $this->session->getWorkspace()->getNodeTypeManager(); 
        $nodeType = $typemgr->getNodeType($mixinName); 
        if (! $nodeType->isMixin()) { 
            throw new ConstraintViolationException("Trying to add a mixin '$mixinName' that is a primary type"); 
        } 
 
        $this->checkState(); 
 
        // TODO handle LockException & VersionException cases 
        if ($this->hasProperty('jcr:mixinTypes')) { 
            if (!in_array($mixinName, $this->properties['jcr:mixinTypes']->getValue())) { 
                $this->properties['jcr:mixinTypes']->addValue($mixinName); 
                $this->setModified(); 
            } 
        } else { 
            $this->setProperty('jcr:mixinTypes', [$mixinName], PropertyType::NAME); 
            $this->setModified(); 
        } 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * 
     * @throws \InvalidArgumentException 
     * @throws AccessDeniedException 
     * @throws ItemNotFoundException 
     * @throws PathNotFoundException 
     * @throws NamespaceException 
     * @throws ValueFormatException 
     * 
     * @api 
     */ 
    public function removeMixin($mixinName) 
    { 
        $this->checkState(); 
 
        // check if node type is assigned 
        if (! $this->hasProperty('jcr:mixinTypes')) { 
            throw new NoSuchNodeTypeException("Node does not have type $mixinName"); 
        } 
 
        $mixins = $this->getPropertyValue('jcr:mixinTypes'); 
        $key = array_search($mixinName, $mixins); 
        if (false === $key) { 
            throw new NoSuchNodeTypeException("Node does not have type $mixinName"); 
        } 
 
        unset($mixins[$key]); 
        $this->setProperty('jcr:mixinTypes', $mixins); // might be empty array which is fine 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws \InvalidArgumentException 
     * @throws AccessDeniedException 
     * @throws InvalidItemStateException 
     * @throws ItemNotFoundException 
     * @throws NamespaceException 
     * @throws PathNotFoundException 
     * @throws ValueFormatException 
     * 
     * @api 
     */ 
    public function setMixins(array $mixinNames) 
    { 
        $toRemove = []; 
        if ($this->hasProperty('jcr:mixinTypes')) { 
            foreach ($this->getPropertyValue('jcr:mixinTypes') as $mixin) { 
                if (false !== $key = array_search($mixin, $mixinNames)) { 
                    unset($mixinNames[$key]); 
                } else { 
                    $toRemove[] = $mixin; 
                } 
            } 
        } 
        if (! (count($toRemove) || count($mixinNames))) { 
            return; // nothing to do 
        } 
 
        // make sure the new types actually exist before we add anything 
        $ntm = $this->session->getWorkspace()->getNodeTypeManager(); 
        foreach ($mixinNames as $mixinName) { 
            $nodeType = $ntm->getNodeType($mixinName); 
            if (! $nodeType->isMixin()) { 
                throw new ConstraintViolationException("Trying to add a mixin '$mixinName' that is a primary type"); 
            } 
        } 
 
        foreach ($mixinNames as $type) { 
            $this->addMixin($type); 
        } 
 
        foreach ($toRemove as $type) { 
            $this->removeMixin($type); 
        } 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * 
     * @api 
     */ 
    public function canAddMixin($mixinName) 
    { 
        $this->checkState(); 
 
        throw new NotImplementedException('Write'); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getDefinition() 
    { 
        $this->checkState(); 
 
        if ('rep:root' === $this->primaryType) { 
            throw new NotImplementedException('what is the definition of the root node?'); 
        } 
 
        if (empty($this->definition)) { 
            $this->definition = $this->findItemDefinition(function (NodeTypeInterface $nt) { 
                return $nt->getChildNodeDefinitions(); 
            }); 
        } 
 
        return $this->definition; 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function update($srcWorkspace) 
    { 
        $this->checkState(); 
 
        if ($this->isNew()) { 
            //no node in workspace 
            return; 
        } 
 
        $this->getSession()->getTransport()->updateNode($this, $srcWorkspace); 
        $this->setDirty(); 
        $this->setChildrenDirty(); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * 
     * @api 
     */ 
    public function getCorrespondingNodePath($workspaceName) 
    { 
        $this->checkState(); 
 
        return $this->getSession() 
            ->getTransport() 
            ->getNodePathForIdentifier($this->getIdentifier(), $workspaceName); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function getSharedSet() 
    { 
        $this->checkState(); 
 
        throw new NotImplementedException(); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * 
     * @api 
     */ 
    public function removeSharedSet() 
    { 
        $this->checkState(); 
        $this->setModified(); 
 
        throw new NotImplementedException('Write'); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * 
     * @api 
     */ 
    public function removeShare() 
    { 
        $this->checkState(); 
        $this->setModified(); 
 
        throw new NotImplementedException('Write'); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function isCheckedOut() 
    { 
        $this->checkState(); 
 
        $workspace = $this->session->getWorkspace(); 
        $versionManager = $workspace->getVersionManager(); 
 
        return $versionManager->isCheckedOut($this->getPath()); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @api 
     */ 
    public function isLocked() 
    { 
        $this->checkState(); 
 
        throw new NotImplementedException(); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * 
     * @api 
     */ 
    public function followLifecycleTransition($transition) 
    { 
        $this->checkState(); 
        $this->setModified(); 
 
        throw new NotImplementedException('Write'); 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * @throws InvalidItemStateException 
     * 
     * @api 
     */ 
    public function getAllowedLifecycleTransitions() 
    { 
        $this->checkState(); 
 
        throw new NotImplementedException('Write'); 
    } 
 
    /** 
     * Refresh this node 
     * 
     * {@inheritDoc} 
     * 
     * This is also called internally to refresh when the node is accessed in 
     * state DIRTY. 
     * 
     * @see Item::checkState 
     */ 
    protected function refresh($keepChanges, $internal = false) 
    { 
        if (! $internal && $this->isDeleted()) { 
            throw new InvalidItemStateException('This item has been removed and can not be refreshed'); 
        } 
        $deleted = false; 
 
        // Get properties and children from backend 
        try { 
            $json = $this->objectManager->getTransport()->getNode( 
                is_null($this->oldPath) 
                    ? $this->path 
                    : $this->oldPath 
            ); 
        } catch (ItemNotFoundException $ex) { 
 
            // The node was deleted in another session 
            if (! $this->objectManager->purgeDisappearedNode($this->path, $keepChanges)) { 
                throw new LogicException($this->path . " should be purged and not kept"); 
            } 
            $keepChanges = false; // delete never keeps changes 
            if (! $internal) { 
                // this is not an internal update 
                $deleted = true; 
            } 
 
            // continue with empty data, parseData will notify all cached 
            // children and all properties that we are removed 
            $json = []; 
        } 
 
        $this->parseData($json, true, $keepChanges); 
 
        if ($deleted) { 
            $this->setDeleted(); 
        } 
    } 
 
    /** 
     * Remove this node 
     * 
     * {@inheritDoc} 
     * 
     * A jackalope node needs to notify the parent node about this if it is 
     * cached, in addition to \PHPCR\ItemInterface::remove() 
     * 
     * @uses Node::unsetChildNode() 
     * 
     * @api 
     */ 
    public function remove() 
    { 
        $this->checkState(); 
        $parent = $this->getParent(); 
 
        $parentNodeType = $parent->getPrimaryNodeType(); 
        //will throw a ConstraintViolationException if this node can't be removed 
        $parentNodeType->canRemoveNode($this->getName(), true); 
 
        if ($parent) { 
            $parent->unsetChildNode($this->name, true); 
        } 
        // once we removed ourselves, $this->getParent() won't work anymore. do this last 
        parent::remove(); 
    } 
 
    /** 
     * Removes the reference in the internal node storage 
     * 
     * @param string $name  the name of the child node to unset 
     * @param bool   $check whether a state check should be done - set to false 
     *      during internal update operations 
     * 
     * @throws ItemNotFoundException If there is no child with $name 
     * @throws InvalidItemStateException 
     * 
     * @private 
     */ 
    public function unsetChildNode($name, $check) 
    { 
        if ($check) { 
            $this->checkState(); 
        } 
 
        $key = array_search($name, $this->nodes); 
        if ($key === false) { 
            if (! $check) { 
                // inside a refresh operation 
                return; 
            } 
            throw new ItemNotFoundException("Could not remove child node because it's already gone"); 
        } 
 
        unset($this->nodes[$key]); 
 
        if (null !== $this->originalNodesOrder) { 
            $this->originalNodesOrder = array_flip($this->originalNodesOrder); 
            unset($this->originalNodesOrder[$name]); 
            $this->originalNodesOrder = array_flip($this->originalNodesOrder); 
        } 
    } 
 
    /** 
     * Adds child node to this node for internal reference 
     * 
     * @param NodeInterface $node  The name of the child node 
     * @param boolean $check whether to check state 
     * @param string  $name  is used in cases where $node->getName would not return the correct name (during move operation) 
     * 
     * @throws InvalidItemStateException 
     * @throws RepositoryException 
     * 
     * @private 
     */ 
    public function addChildNode(NodeInterface $node, $check, $name = null) 
    { 
        if ($check) { 
            $this->checkState(); 
        } 
 
        if (is_null($name)) { 
            $name = $node->getName(); 
        } 
 
        $nt = $this->getPrimaryNodeType(); 
        //will throw a ConstraintViolationException if this node can't be added 
        $nt->canAddChildNode($name, $node->getPrimaryNodeType()->getName(), true); 
 
        // TODO: same name siblings 
 
        $this->nodes[] = $name; 
 
        if (null !== $this->originalNodesOrder) { 
            $this->originalNodesOrder[] = $name; 
        } 
    } 
 
    /** 
     * Removes the reference in the internal node storage 
     * 
     * @param string $name the name of the property to unset. 
     * 
     * @throws ItemNotFoundException If this node has no property with name $name 
     * @throws InvalidItemStateException 
     * @throws RepositoryException 
     * 
     * @private 
     */ 
    public function unsetProperty($name) 
    { 
        $this->checkState(); 
        $this->setModified(); 
 
        if (!array_key_exists($name, $this->properties)) { 
            throw new ItemNotFoundException('Implementation Error: Could not remove property from node because it is already gone'); 
        } 
        $this->deletedProperties[$name] = $this->properties[$name]; 
        unset($this->properties[$name]); 
    } 
 
    /** 
     * In addition to calling parent method, tell all properties and clean deletedProperties 
     */ 
    public function confirmSaved() 
    { 
        foreach ($this->properties as $property) { 
            if ($property->isModified() || $property->isNew()) { 
                $property->confirmSaved(); 
            } 
        } 
        $this->deletedProperties = []; 
        parent::confirmSaved(); 
    } 
 
    /** 
     * In addition to calling parent method, tell all properties 
     */ 
    public function setPath($path, $move = false) 
    { 
        parent::setPath($path, $move); 
        foreach ($this->properties as $property) { 
            $property->setPath($path.'/'.$property->getName(), $move); 
        } 
    } 
 
    /** 
     * Make sure $p is an absolute path 
     * 
     * If its a relative path, prepend the path to this node, otherwise return as is 
     * 
     * @param string $p the relative or absolute property or node path 
     * 
     * @return string the absolute path to this item, with relative paths resolved against the current node 
     */ 
    protected function getChildPath($p) 
    { 
        if ('' == $p) { 
            throw new InvalidArgumentException("Name can not be empty"); 
        } 
        if ($p[0] == '/') { 
            return $p; 
        } 
        //relative path, combine with base path for this node 
        $path = $this->path === '/' ? '/' : $this->path.'/'; 
 
        return $path . $p; 
    } 
 
    /** 
     * Filter the list of names according to the filter expression / array 
     * 
     * @param string|array $filter according to getNodes|getProperties 
     * @param array        $names  list of names to filter 
     * 
     * @return array the names in $names that match the filter 
     */ 
    protected static function filterNames($filter, $names) 
    { 
        if ($filter !== null) { 
            $filtered = []; 
            $filter = (array) $filter; 
            foreach ($filter as $k => $f) { 
                $f = trim($f); 
                $filter[$k] = strtr($f, [ 
                    '*'=>'.*', //wildcard 
                    '.'  => '\\.', //escape regexp 
                    '\\' => '\\\\', 
                    '{'  => '\\{', 
                    '}'  => '\\}', 
                    '('  => '\\(', 
                    ')'  => '\\)', 
                    '+'  => '\\+', 
                    '^'  => '\\^', 
                    '$'  => '\\$' 
                ]); 
            } 
            foreach ($names as $name) { 
                foreach ($filter as $f) { 
                    if (preg_match('/^'.$f.'$/', $name)) { 
                        $filtered[] = $name; 
                    } 
                } 
            } 
        } else { 
            $filtered = $names; 
        } 
 
        return $filtered; 
    } 
 
    /** 
     * Provide Traversable interface: redirect to getNodes with no filter 
     * 
     * @return Iterator over all child nodes 
     * @throws RepositoryException 
     */ 
    #[\ReturnTypeWillChange] 
    public function getIterator() 
    { 
        $this->checkState(); 
 
        return $this->getNodes(); 
    } 
 
    /** 
     * Implement really setting the property without any notification. 
     * 
     * Implement the setProperty, but also used from constructor or in refresh, 
     * when the backend has a new property that is not yet loaded in memory. 
     * 
     * @param string  $name 
     * @param mixed   $value 
     * @param string  $type 
     * @param boolean $internal whether we are setting this node through api or internally 
     * 
     * @return Property 
     * 
     * @throws InvalidArgumentException 
     * @throws LockException 
     * @throws ConstraintViolationException 
     * @throws RepositoryException 
     * @throws UnsupportedRepositoryOperationException 
     * @throws ValueFormatException 
     * @throws VersionException 
     * 
     * @see Node::setProperty 
     * @see Node::refresh 
     * @see Node::__construct 
     */ 
    protected function _setProperty($name, $value, $type, $internal) 
    { 
        if ($name === '' || false !== strpos($name, '/')) { 
            throw new InvalidArgumentException("The name '$name' is no valid property name"); 
        } 
 
        if (!isset($this->properties[$name])) { 
            $path = $this->getChildPath($name); 
            $property = $this->factory->get( 
                Property::class, 
                [ 
                    ['type' => $type, 'value' => $value], 
                    $path, 
                    $this->session, 
                    $this->objectManager, 
                    ! $internal 
                ] 
            ); 
            $this->properties[$name] = $property; 
            if (! $internal) { 
                $this->setModified(); 
            } 
        } else { 
            if ($internal) { 
                $this->properties[$name]->_setValue($value, $type); 
                if ($this->properties[$name]->isDirty()) { 
                    $this->properties[$name]->setClean(); 
                } 
            } else { 
                $this->properties[$name]->setValue($value, $type); 
            } 
        } 
 
        return $this->properties[$name]; 
    } 
 
    /** 
     * Overwrite to set the properties dirty as well. 
     * 
     * @private 
     */ 
    public function setDirty($keepChanges = false, $targetState = false) 
    { 
        parent::setDirty($keepChanges, $targetState); 
 
        foreach ($this->properties as $property) { 
            if ($keepChanges && self::STATE_NEW !== $property->getState()) { 
                // if we want to keep changes, we do not want to set new properties dirty. 
                $property->setDirty($keepChanges, $targetState); 
            } 
        } 
    } 
 
    /** 
     * Mark all cached children as dirty. 
     * 
     * @private 
     */ 
    public function setChildrenDirty() 
    { 
        foreach ($this->objectManager->getCachedDescendants($this->getPath()) as $childNode) { 
            $childNode->setDirty(); 
        } 
    } 
 
    /** 
     * In addition to set this item deleted, set all properties to deleted. 
     * 
     * They will be automatically deleted by the backend, but the user might 
     * still have a reference to one of the property objects. 
     * 
     * @private 
     */ 
    public function setDeleted() 
    { 
        parent::setDeleted(); 
        foreach ($this->properties as $property) { 
            $property->setDeleted(); // not all properties are tracked in objectmanager 
        } 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * Additionally, notifies all properties of this node. Child nodes are not 
     * notified, it is the job of the ObjectManager to know which nodes are 
     * cached and notify them. 
     */ 
    public function beginTransaction() 
    { 
        parent::beginTransaction(); 
 
        // Notify the children properties 
        foreach ($this->properties as $prop) { 
            $prop->beginTransaction(); 
        } 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * Additionally, notifies all properties of this node. Child nodes are not 
     * notified, it is the job of the ObjectManager to know which nodes are 
     * cached and notify them. 
     */ 
    public function commitTransaction() 
    { 
        parent::commitTransaction(); 
 
        foreach ($this->properties as $prop) { 
            $prop->commitTransaction(); 
        } 
    } 
 
    /** 
     * {@inheritDoc} 
     * 
     * Additionally, notifies all properties of this node. Child nodes are not 
     * notified, it is the job of the ObjectManager to know which nodes are 
     * cached and notify them. 
     */ 
    public function rollbackTransaction() 
    { 
        parent::rollbackTransaction(); 
 
        foreach ($this->properties as $prop) { 
            $prop->rollbackTransaction(); 
        } 
    } 
}