public static function deserializeTemporaryIds(Schema $schema, $serializedIds)
     $temporaryIdMap = new TemporaryIdMap();
     foreach (explode("\n", $serializedIds) as $entry) {
         $elements = explode(" ", $entry);
         if (count($elements) == 3) {
             $entity = $schema->getObjectEntity($elements[0]);
             $temporaryIdMap->setId($entity, $elements[1], $elements[2]);
     return $temporaryIdMap;
  * XML nodes represent objects that are CREATED, CHANGED or DELETED, depending on the presence or absence of
  * a @deleted-attribute and a persisted or temporary id.
  *   CREATED means that the object has been created in menory and must be persisted (inserted) in the database.
  *   CHANGED means that the object has been modified in menory and that its persistent version in the database must
  *           be updated.
  *   DELETED means that the object has been deleted and that its persistent version must be removed (deleted) from
  *           the database.
  * Nodes without such attribute or id represent either properties of an object, references to related objects or
  * XML place holders. For each object node that has no @delete-attribute but does have child nodes (i.e. it is not
  * a reference), then a default action will be used:
  *   @id attribute absent or empty => CREATED
  *   @id attribute with temporary id => CREATED
  *   @id attribute with persisted id => CHANGED
  * A node represents an object if its node name equals the name of an ObjectEntity. Only object nodes can have a
  * @delete-attribute. If the modify action is CHANGED or DELETED, then the node *must* have an @id attribute with
  * a known (persisted) id. If the action is CREATED, the node may have an @id attribute with a temporary id. A
  * temporary id will be generated for objects with action CREATED that have no @id attribute.
  * Any object node that is child of a CREATED object node will be considered CREATED if it has no @delete-attribute
  * and if it either has no @id attribute or if the @id attribute contains a temporary id. If such node has an
  * @id attribute with a persisted id, then it will be treated as a reference to an object related to the object of
  * the parent node.
  * Nodes with names that are not ObjectEntity names will only be ignored if their parent node is not an object node.
  * However, if such node is a child of an object node, then its names *must* correspond with that of a property
  * of the parent object. Equally named sibling nodes are not allowed as children of an object node, since the
  * properties of the object must be uniquely specified.
  * Names of node and attribute are parsed case-insensitive.
  * Object nodes with a certain id and a non-NULL modify-action may occur only once. Multiple occurrences of the
  * same object is only allowed if a modify-action is specifeid for only one of them.
  * @param Schema $schema
  * @param DOMElement $xmlElement
  * @param TemporaryIdMap $temporaryIdMap
 private function parseElementTree(Schema $schema, DOMElement $xmlElement, TemporaryIdMap $temporaryIdMap)
     // Get the entity and its id.
     $entityName = $xmlElement->nodeName;
     $entity = $schema->getObjectEntity($entityName, FALSE);
     $id = $xmlElement->getAttribute(XmlConstants::ID);
     // Determine what to do with this node: create, update or delete the corresponding object or do nothing.
     $modifyingAction = NULL;
     if ($entity != NULL) {
         $modifyingAction = $this->determineModifyingAction($xmlElement, $entity, $id, $temporaryIdMap);
         // If a modify-action was determined for this object...
         if ($modifyingAction != NULL) {
             // ...then beware of multiple object definitions.
             foreach ($this->parsedObjects as $alreadyParsedObject) {
                 if ($alreadyParsedObject->getEntity() == $entity and $alreadyParsedObject->getId() == $id) {
                     throw new Exception("Object '" . $entityName . "[{$id}]' is specified more than once.", RestResponse::CLIENT_ERROR);
     // Parse the property nodes and entity (reference) nodes and nested entity nodes. Determine for each node if it
     // represents a property or a related object. The names of property nodes must correspond to property names of
     // the parsed entity. However, entity nodes must always contain (at least) an @id attribute.
     $content = array();
     $childEntityNodes = array();
     for ($i = 0; $i < $xmlElement->childNodes->length; $i++) {
         $childNode = $xmlElement->childNodes->item($i);
         // Only consider 'true' child nodes; skip text nodes.
         if ($childNode->nodeType == XML_ELEMENT_NODE) {
             $parseRecursively = TRUE;
             // If we're parsing the content of an entity node...
             if ($entity != NULL) {
                 // ...then check if the node name equals that of a property of the entity at hand.
                 $propertyName = strtolower($childNode->nodeName);
                 $property = $entity->getProperty($propertyName, FALSE);
                 if ($property != NULL) {
                     // Yes it does, so add the property value to the content map.
                     // However, first verify that properties are specified only once.
                     if (array_key_exists($propertyName, $content)) {
                         throw new Exception("Property '{$childNode->nodeName}' of entity '" . $entity->getName() . "' is specified more than once.", RestResponse::CLIENT_ERROR);
                     $content[$propertyName] = $childNode->nodeValue;
                     $parseRecursively = FALSE;
                 } else {
                     // ...else ceck if the child node describes an entity node.
                     $childEntity = $schema->getObjectEntity($childNode->nodeName, FALSE);
                     if ($childEntity == NULL) {
                         // The child node neither describes a property, nor an entity.
                         // This is an error if we're parsing an entity node.
                         throw new Exception("Unknown property '{$childNode->nodeName}' of entity '" . $entity->getName() . "'.", RestResponse::CLIENT_ERROR);
                     // So the child node is an entity. It can represent an existing object, a newly created object
                     // or a reference. In either case we must create an ObjectRef of it and add that to
                     // $relatedObjectRefs.
                     // The child node must have an @id attribute.
                     $childId = $childNode->getAttribute(XmlConstants::ID);
                     if ($childId == NULL) {
                         // Check if the child node is an object reference.
                         if ($childNode->childNodes->length == 0) {
                             throw new Exception("No id was specified for a reference to entity '{$childNode->nodeName}'.", RestResponse::CLIENT_ERROR);
                         // Generate a temporary id for the child node and add it to the XML, so the same id will be
                         // parsed lateron.
                         $childId = TemporaryIdMap::generateTemporaryId();
                         $childNode->setAttribute(XmlConstants::ID, $childId);
                     } else {
                         // Check if the childId has been mapped to a persisted id in an earlier request.
                         $persistedChildId = $temporaryIdMap->getPersistedId($childEntity, $childId, FALSE);
                         if ($persistedChildId != NULL) {
                             // If so, then replace the temporary id with its persisted version.
                             $childId = $persistedChildId;
                             $childNode->setAttribute(XmlConstants::ID, $persistedChildId);
             if ($parseRecursively) {
                 // Add the node to the list of nodes that must be parsed recursively.
                 $childEntityNodes[] = $childNode;
     // Add a ParsedObject to the list if any action must be performed on it.
     $parsedObject = NULL;
     if ($entity != NULL) {
         $parsedObject = new ParsedObject($entity, $id, $content, $modifyingAction, $xmlElement->getAttribute(XmlConstants::SCOPE));
         if ($modifyingAction != NULL) {
             $this->parsedObjects[] = $parsedObject;
         // Mark the object if it must be published.
         if ($xmlElement->hasAttribute(XmlConstants::PUBLISHED)) {
             $this->publishedObjects[] = $parsedObject;
     // Recursively parse the child node(s) and pass the currens ParsedObject as the parent.
     foreach ($childEntityNodes as $childEntityNode) {
         $parsedChildObject = $this->parseElementTree($schema, $childEntityNode, $temporaryIdMap);
         if ($parsedObject != NULL and $parsedChildObject != NULL) {
             // Record the relationship in both directions.
     return $parsedObject;
 public function establishLinks(Schema $schema, ObjectModifier $objectModifier, Audit $audit)
     if ($this->scope->includes(Scope::TAG_ASSOCIATES) == Scope::INCLUDES_NONE) {
     // Create and/or delete all remaining relationships.
     // Group all relationships per relatedEntity.
     $relatedEntityObjectsMap = array();
     foreach ($this->relatedObjects as $relatedObject) {
         $relatedEntityName = $relatedObject->entity->getName();
         if (!array_key_exists($relatedEntityName, $relatedEntityObjectsMap)) {
             $relatedEntityObjectsMap[$relatedEntityName] = array();
         $relatedEntityObjectsMap[$relatedEntityName][] = $relatedObject;
     // Now add or delete links corresponding to the specifoed relatedObjects.
     $processedLinkRelationships = array();
     foreach ($relatedEntityObjectsMap as $relatedEntityName => $relatedObjects) {
         $linkRelationship = $schema->getLinkRelationship($this->entity, $schema->getObjectEntity($relatedEntityName), FALSE);
         if ($linkRelationship != NULL) {
             $otherIds = array();
             foreach ($relatedObjects as $relatedObject) {
                 $otherIds[] = $relatedObject->id;
             $objectModifier->establishLinks($schema, $linkRelationship->getFkEntity(), $linkRelationship->getFkColumnName($this->entity), $this->id, $linkRelationship->getFkColumnName($relatedObject->entity), $otherIds, $audit);
             // Remove the reversed direction of the links, so they won't be processed twice.
             foreach ($relatedObjects as $relatedObject) {
             // Keep track of all processed link-relationships.
             $processedLinkRelationships[] = $linkRelationship;
     // Delete any link that was NOT specified in the ParsedObject.
     foreach ($this->entity->getRelationships() as $relationship) {
         $oppositeEntity = $relationship->getOppositeEntity($this->entity);
         if (!$relationship->getFkEntity()->isObjectEntity() and $oppositeEntity->isObjectEntity() and array_search($relationship, $processedLinkRelationships) === FALSE) {
             // Call establishLinks() with an empty list of referring id's, so any persisted link will be deleted.
             $objectModifier->establishLinks($schema, $relationship->getFkEntity(), $relationship->getFkColumnName($this->entity), $this->id, $relationship->getFkColumnName($oppositeEntity), array(), $audit);