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. $parsedObject->addRelatedObject($parsedChildObject); $parsedChildObject->addRelatedObject($parsedObject); } } return $parsedObject; }
public function establishLinks(Schema $schema, ObjectModifier $objectModifier, Audit $audit) { if ($this->scope->includes(Scope::TAG_ASSOCIATES) == Scope::INCLUDES_NONE) { return; } // 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) { $relatedObject->removeRelatedObject($this); } // 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); } } }