/**
  * Accepts a list of XML nodes, each representing an object that must be either created, updated or deleted,
  * depending on the presence of respectively a @created or @deleted attribute. The node names must be equal to that
  * of an entity and the names of the child nodes must be equal to the property names applicable to that entity.
  * Node names are compared case-insensitive.
  *
  * Special attributes are
  *     deleted - Change-index indicating that this is an existing (persisted) object that must be deleted.
  *     id - Object identifier (primary key).
  *
  * Every object-node must have an @id, either permanent (numerical value) or temporary (starting with a non-digit).
  * Temporary ID's will be substituted with the permanent (persisted) ID lateron.
  *
  * Child nodes who's name equals that of an entity are NOT recursively parsed and processed. Only their @id
  * attribute is used to add or update foreign keys.
  *
  * RETURN
  * A map of (temporary and) permanent id's per entity is returned. This map enables the caller to substitute
  * temporary id's at the client side.
  *
  * ERROR
  * It is an error if the name of a given node doesn't match that of an entity, or if the names of child nodes
  * don't match that of properties of the entity.
  *
  * HOW IT WORKS
  * The nodes are scanned recursively (so $xmlElements can be a 'flat' list of object nodes or a nested xml tree or
  * a combination of both) and grouped in three arrays, one with nodes of objects that must be created, changed or
  * deleted. The 'created' group is processed first. The nodes in this group are sorted based on their entity, so the
  * order of creation allows for substituting any foreign key of successively created objects. This is to prevent
  * violating foreign key constraints.
  * Then the 'changed' group is processed. Foreign keys are substitued before updating each object in the database.
  * Foreign keys of objects that are in the 'deleted' group will be set to NULL.
  * Finally the objects in the 'deleted' group are deleted.
  */
 private function processXmlObjects($appName, array $xmlElements, $restResponseCode)
 {
     $schema = $this->metaData->getSchema($appName);
     $this->processQueryParams($schema, NULL);
     $mySQLi = $schema->getMySQLi();
     try {
         $account = PersistentAccount::getAccount($schema, AuthHandler::getSignedInAccountId($appName));
         $audit = new PersistentAudit($schema, $account);
         $objectFetcher = $this->getObjectFetcher($appName);
         $restParser = new RestParser();
         // Parse all nodes...
         $restParser->parse($schema, $xmlElements, $this->temporaryIdMap, $objectFetcher);
         // ...set the foreign key id's...
         $restParser->applyParsedRelationships();
         // ...and divide the ParsedObjects in groups (CREATED, CHANGED, DELETED).
         $restParser->groupParsedObjects($schema, $objectFetcher);
         // Any CHANGED object can have existing (persisted) relationships that don't exist in the parsed dataset.
         // Detect these now.
         $restParser->detectObsoleteConnections($schema, $objectFetcher);
         // Create all CREATED objects.
         foreach ($restParser->getCreatedObjects() as $createdObject) {
             $entity = $createdObject->getEntity();
             // Note: the id of a $createdParsedObject is always a temporary id.
             $temporaryId = $createdObject->getId();
             // Substitute any temporary id's in foreign key properties.
             $createdObject->substituteTemporaryIds($this->temporaryIdMap);
             // Create the object and add the newly created object id to the temporaryIdMap.
             $persistedId = $this->objectModifier->createObject($schema, $entity, $createdObject->getPropertyValues(), $audit);
             $createdObject->setId($persistedId);
             $this->temporaryIdMap->setId($entity, $temporaryId, $persistedId);
         }
         // Update all objects in the CHANGED group.
         foreach ($restParser->getChangedObjects() as $changedObject) {
             if ($changedObject->getScope()->includes(Scope::TAG_PROPERTIES) != Scope::INCLUDES_ALL) {
                 continue;
             }
             $changedObject->substituteTemporaryIds($this->temporaryIdMap);
             $isPersisted = $this->objectModifier->modifyObject($schema, $changedObject->getEntity(), $changedObject->getId(), $changedObject->getPropertyValues(), $audit);
             if ($isPersisted) {
                 $this->temporaryIdMap->setId($changedObject->getEntity(), NULL, $changedObject->getId());
             }
         }
         // Create and/or delete all link-relationships.
         foreach ($restParser->getChangedAndTouchedObjects() as $changedObject) {
             $changedObject->establishLinks($schema, $this->objectModifier, $audit);
         }
         foreach ($restParser->getCreatedObjects() as $createdObject) {
             $createdObject->establishLinks($schema, $this->objectModifier, $audit);
         }
         // Delete all objects in the DELETED group.
         foreach ($restParser->getDeletedObjects() as $deletedObject) {
             $this->objectModifier->deleteObjectTree($schema, $deletedObject->getEntity(), $deletedObject->getId(), $audit);
         }
         // Update the PUBLISHED state.
         foreach ($restParser->getPublishedObjects() as $publishedObject) {
             $this->objectPublisher->publish($schema, $publishedObject->getEntity(), $publishedObject->getId(), $audit);
             // Purge - permanently delete - all 'terminated' objects that are not part of the published data.
             $this->objectModifier->purge($schema, $publishedObject->getEntity(), $publishedObject->getId(), $audit);
         }
         // Commit the database transaction.
         $mySQLi->commit();
         // Save temporaryIds in the session.
         $_SESSION[$this->sessionId] = $this->temporaryIdMap->serializeTemporaryIds();
         return new RestResponse($restResponseCode, $this->temporaryIdMap);
     } catch (Exception $e) {
         Bootstrap::logException($e);
         $mySQLi->rollback();
         return new RestResponse(RestResponse::SERVER_ERROR, $e);
     }
 }