/** * Store a file referenced by $qtiResource into the final $testContent folder. If the path provided * by $qtiResource contains sub-directories, they will be created before copying the file (even * if $copy = false). * * @param Directory $testContent The pointer to the TAO Test Content folder. * @param oat\taoQtiItem\model\qti\Resource|string $qtiResource The QTI resource to be copied into $testContent. If given as a string, it must be the relative (to the IMS QTI Package) path to the resource file. * @param string $origin The path to the directory (root folder of extracted IMS QTI package) containing the QTI resource to be copied. * @param boolean $copy If set to false, the file will not be actually copied. * @param string $rename A new filename e.g. 'file.css' to be used at storage time. * @return string The path were the file was copied/has to be copied (depending on the $copy argument). * @throws InvalidArgumentException If one of the above arguments is invalid. * @throws common_Exception If the copy fails. */ public static function storeQtiResource(Directory $testContent, $qtiResource, $origin, $copy = true, $rename = '') { $fss = ServiceManager::getServiceManager()->get(FileSystemService::SERVICE_ID); $fs = $fss->getFileSystem($testContent->getFileSystem()->getId()); $contentPath = $testContent->getPrefix(); $ds = DIRECTORY_SEPARATOR; $contentPath = rtrim($contentPath, $ds); if ($qtiResource instanceof Resource) { $filePath = $qtiResource->getFile(); } else { if (is_string($qtiResource) === true) { $filePath = $qtiResource; } else { throw new InvalidArgumentException("The 'qtiResource' argument must be a string or a taoQTI_models_classes_QTI_Resource object."); } } $resourcePathinfo = pathinfo($filePath); if (empty($resourcePathinfo['dirname']) === false && $resourcePathinfo['dirname'] !== '.') { // The resource file is not at the root of the archive but in a sub-folder. // Let's copy it in the same way into the Test Content folder. $breadCrumb = $contentPath . $ds . str_replace('/', $ds, $resourcePathinfo['dirname']); $breadCrumb = rtrim($breadCrumb, $ds); $finalName = empty($rename) === true ? $resourcePathinfo['filename'] . '.' . $resourcePathinfo['extension'] : $rename; $finalPath = $breadCrumb . $ds . $finalName; } else { // The resource file is at the root of the archive. // Overwrite template test.xml (created by self::createContent() method above) file with the new one. $finalName = empty($rename) === true ? $resourcePathinfo['filename'] . '.' . $resourcePathinfo['extension'] : $rename; $finalPath = $contentPath . $ds . $finalName; } if ($copy === true) { $origin = str_replace('/', $ds, $origin); $origin = rtrim($origin, $ds); $sourcePath = $origin . $ds . str_replace('/', $ds, $filePath); if (is_readable($sourcePath) === false) { throw new common_Exception("An error occured while copying the QTI resource from '{$sourcePath}' to '{$finalPath}'."); } $fh = fopen($sourcePath, 'r'); $success = $fs->writeStream($finalPath, $fh); fclose($fh); if (!$success) { throw new common_Exception("An error occured while copying the QTI resource from '{$sourcePath}' to '{$finalPath}'."); } } return $finalPath; }
/** * Import a QTI Test and its dependent Items into the TAO Platform. * * @param core_kernel_classes_Class $targetClass The RDFS Class where Ontology resources must be created. * @param oat\taoQtiItem\model\qti\Resource $qtiTestResource The QTI Test Resource representing the IMS QTI Test to be imported. * @param taoQtiTest_models_classes_ManifestParser $manifestParser The parser used to retrieve the IMS Manifest. * @param string $folder The absolute path to the folder where the IMS archive containing the test content * @return common_report_Report A report about how the importation behaved. */ protected function importTest(core_kernel_classes_Class $targetClass, Resource $qtiTestResource, taoQtiTest_models_classes_ManifestParser $manifestParser, $folder) { $itemImportService = ImportService::singleton(); $itemService = taoItems_models_classes_ItemsService::singleton(); $testClass = $targetClass; // Create an RDFS resource in the knowledge base that will hold // the information about the imported QTI Test. $testResource = $this->createInstance($testClass); $qtiTestModelResource = new core_kernel_classes_Resource(INSTANCE_TEST_MODEL_QTI); $modelProperty = new core_kernel_classes_Property(PROPERTY_TEST_TESTMODEL); $testResource->editPropertyValues($modelProperty, $qtiTestModelResource); // Create the report that will hold information about the import // of $qtiTestResource in TAO. $report = new common_report_Report(common_report_Report::TYPE_INFO); // The class where the items that belong to the test will be imported. $itemClass = new core_kernel_classes_Class(TAO_ITEM_CLASS); $targetClass = $itemClass->createSubClass($testResource->getLabel()); // Load and validate the manifest $qtiManifestParser = new taoQtiTest_models_classes_ManifestParser($folder . 'imsmanifest.xml'); $qtiManifestParser->validate(); // Prepare Metadata mechanisms. $metadataMapping = oat\taoQtiItem\model\qti\Service::singleton()->getMetadataRegistry()->getMapping(); $metadataInjectors = array(); $metadataGuardians = array(); $metadataClassLookups = array(); $metadataValues = array(); $domManifest = new DOMDocument('1.0', 'UTF-8'); $domManifest->load($folder . 'imsmanifest.xml'); foreach ($metadataMapping['injectors'] as $injector) { $metadataInjectors[] = new $injector(); } foreach ($metadataMapping['guardians'] as $guardian) { $metadataGuardians[] = new $guardian(); } foreach ($metadataMapping['classLookups'] as $classLookup) { $metadataClassLookups[] = new $classLookup(); } foreach ($metadataMapping['extractors'] as $extractor) { $metadataExtractor = new $extractor(); $metadataValues = array_merge($metadataValues, $metadataExtractor->extract($domManifest)); } // Set up $report with useful information for client code (especially for rollback). $reportCtx = new stdClass(); $reportCtx->manifestResource = $qtiTestResource; $reportCtx->rdfsResource = $testResource; $reportCtx->itemClass = $targetClass; $reportCtx->items = array(); $report->setData($reportCtx); // Expected test.xml file location. $expectedTestFile = $folder . str_replace('/', DIRECTORY_SEPARATOR, $qtiTestResource->getFile()); // Already imported test items (qti xml file paths). $alreadyImportedTestItemFiles = array(); // -- Check if the file referenced by the test QTI resource exists. if (is_readable($expectedTestFile) === false) { $report->add(common_report_Report::createFailure(__('No file found at location "%s".', $qtiTestResource->getFile()))); } else { // -- Load the test in a QTISM flavour. $testDefinition = new XmlDocument(); try { $testDefinition->load($expectedTestFile, true); // -- Load all items related to test. $itemError = false; // discover test's base path. $dependencies = taoQtiTest_helpers_Utils::buildAssessmentItemRefsTestMap($testDefinition, $manifestParser, $folder); if (count($dependencies['items']) > 0) { foreach ($dependencies['items'] as $assessmentItemRefId => $qtiDependency) { if ($qtiDependency !== false) { if (Resource::isAssessmentItem($qtiDependency->getType())) { $resourceIdentifier = $qtiDependency->getIdentifier(); // Check if the item is already stored in the bank. foreach ($metadataGuardians as $guardian) { if (isset($metadataValues[$resourceIdentifier]) === true) { if (($guard = $guardian->guard($metadataValues[$resourceIdentifier])) !== false) { common_Logger::i("Item with identifier '{$resourceIdentifier}' already in Item Bank."); $msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was already stored in the Item Bank.', $resourceIdentifier); $report->add(common_report_Report::createInfo($msg, $guard)); $reportCtx->items[$assessmentItemRefId] = $guard; // Simply do not import again. continue 2; } } } // Determine target class from metadata, if possible. // This is applied to items, not for test definitions. // The test definitions' target class will not be affected // by class lookups. $lookupTargetClass = false; foreach ($metadataClassLookups as $classLookup) { if (isset($metadataValues[$resourceIdentifier]) === true) { if (($lookupTargetClass = $classLookup->lookup($metadataValues[$resourceIdentifier])) !== false) { break; } } } $qtiFile = $folder . str_replace('/', DIRECTORY_SEPARATOR, $qtiDependency->getFile()); // Skip if $qtiFile already imported (multiple assessmentItemRef "hrefing" the same file). if (array_key_exists($qtiFile, $alreadyImportedTestItemFiles) === false) { $isApip = $qtiDependency->getType() === 'imsqti_apipitem_xmlv2p1'; $itemReport = $itemImportService->importQtiItem($folder, $qtiDependency, $lookupTargetClass !== false ? $lookupTargetClass : $targetClass, $isApip, $dependencies['dependencies']); $rdfItem = $itemReport->getData(); if ($rdfItem) { $reportCtx->items[$assessmentItemRefId] = $rdfItem; $alreadyImportedTestItemFiles[$qtiFile] = $rdfItem; $itemReport->setMessage(__('IMS QTI Item referenced as "%s" in the IMS Manifest file successfully imported.', $qtiDependency->getIdentifier())); } else { $itemReport->setType(common_report_Report::TYPE_ERROR); $itemReport->setMessage(__('IMS QTI Item referenced as "%s" in the IMS Manifest file could not be imported.', $qtiDependency->getIdentifier())); $itemError = $itemError === false ? true : $itemError; } $report->add($itemReport); } else { $reportCtx->items[$assessmentItemRefId] = $alreadyImportedTestItemFiles[$qtiFile]; } } } else { $msg = __('The dependency to the IMS QTI AssessmentItemRef "%s" in the IMS Manifest file could not be resolved.', $assessmentItemRefId); $report->add(common_report_Report::createFailure($msg)); $itemError = $itemError === false ? true : $itemError; } } // If items did not produce errors, we import the test definition. if ($itemError === false) { common_Logger::i('Importing test...'); // Second step is to take care of the test definition and the related media (auxiliary files). // 1. Import test definition (i.e. the QTI-XML Test file). $testContent = $this->importTestDefinition($testResource, $testDefinition, $qtiTestResource, $reportCtx->items, $folder, $report); if ($testContent !== false) { // 2. Import test auxilliary files (e.g. stylesheets, images, ...). $this->importTestAuxiliaryFiles($testContent, $qtiTestResource, $folder, $report); // 3. Give meaningful names to resources. $testTitle = $testDefinition->getDocumentComponent()->getTitle(); $testResource->setLabel($testDefinition->getDocumentComponent()->getTitle()); $targetClass->setLabel($testDefinition->getDocumentComponent()->getTitle()); // 4. if $targetClass does not contain any instances (because everything resolved by class lookups), // Just delete it. if ($targetClass->countInstances() == 0) { $targetClass->delete(); } } } else { $msg = __("One or more dependent IMS QTI Items could not be imported."); $report->add(common_report_Report::createFailure($msg)); } } else { // No depencies found (i.e. no item resources bound to the test). $msg = __("No reference to any IMS QTI Item found."); $report->add(common_report_Report::createFailure($msg)); } } catch (StorageException $e) { // Source of the exception = $testDefinition->load() // What is the reason ? $finalErrorString = ''; $eStrs = array(); if (($libXmlErrors = $e->getErrors()) !== null) { foreach ($libXmlErrors as $libXmlError) { $eStrs[] = __('XML error at line %1$d column %2$d "%3$s".', $libXmlError->line, $libXmlError->column, trim($libXmlError->message)); } } $finalErrorString = implode("\n", $eStrs); if (empty($finalErrorString) === true) { // Not XML malformation related. No info from LibXmlErrors extracted. if (($previous = $e->getPrevious()) != null) { // Useful information could be found here. $finalErrorString = $previous->getMessage(); if ($previous instanceof UnmarshallingException) { $domElement = $previous->getDOMElement(); $finalErrorString = __('Inconsistency at line %1d:', $domElement->getLineNo()) . ' ' . $previous->getMessage(); } } else { $finalErrorString = __("Unknown error."); } } $msg = __("Error found in the IMS QTI Test:\n%s", $finalErrorString); $report->add(common_report_Report::createFailure($msg)); } } if ($report->containsError() === false) { $report->setType(common_report_Report::TYPE_SUCCESS); $msg = __("IMS QTI Test referenced as \"%s\" in the IMS Manifest file successfully imported.", $qtiTestResource->getIdentifier()); $report->setMessage($msg); } else { $report->setType(common_report_Report::TYPE_ERROR); $msg = __("The IMS QTI Test referenced as \"%s\" in the IMS Manifest file could not be imported.", $qtiTestResource->getIdentifier()); $report->setMessage($msg); } return $report; }
/** * @param $folder * @param \taoQtiTest_models_classes_QtiResource $qtiItemResource * @param $itemClass * @param bool|false $extractApip * @param array $dependencies * @return common_report_Report * @throws common_exception_Error */ public function importQtiItem($folder, Resource $qtiItemResource, $itemClass, $extractApip = false, $dependencies = array()) { try { //load the information about resources in the manifest $itemService = taoItems_models_classes_ItemsService::singleton(); $qtiService = Service::singleton(); // The metadata import feature needs a DOM representation of the manifest. $domManifest = new DOMDocument('1.0', 'UTF-8'); $domManifest->load($folder . 'imsmanifest.xml'); $metadataMapping = $qtiService->getMetadataRegistry()->getMapping(); $metadataInjectors = array(); $metadataGuardians = array(); $metadataClassLookups = array(); $metadataValues = array(); foreach ($metadataMapping['injectors'] as $injector) { $metadataInjectors[] = new $injector(); } foreach ($metadataMapping['guardians'] as $guardian) { $metadataGuardians[] = new $guardian(); } foreach ($metadataMapping['classLookups'] as $classLookup) { $metadataClassLookups[] = new $classLookup(); } foreach ($metadataMapping['extractors'] as $extractor) { $metadataExtractor = new $extractor(); $metadataValues = array_merge($metadataValues, $metadataExtractor->extract($domManifest)); } $sources = MediaService::singleton()->getWritableSources(); $sharedStorage = array_shift($sources); $sharedFiles = array(); try { $resourceIdentifier = $qtiItemResource->getIdentifier(); // Use the guardians to check whether or not the item has to be imported. foreach ($metadataGuardians as $guardian) { if (isset($metadataValues[$resourceIdentifier]) === true) { if (($guard = $guardian->guard($metadataValues[$resourceIdentifier])) !== false) { $msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was already stored in the Item Bank.', $qtiItemResource->getIdentifier()); $report = common_report_Report::createInfo($msg, $guard); // Simply do not import again. return $report; } } } $targetClass = false; // Use the classLookups to determine where the item has to go. foreach ($metadataClassLookups as $classLookup) { if (isset($metadataValues[$resourceIdentifier]) === true) { if (($targetClass = $classLookup->lookup($metadataValues[$resourceIdentifier])) !== false) { break; } } } $qtiFile = $folder . $qtiItemResource->getFile(); $qtiModel = $this->createQtiItemModel($qtiFile); $rdfItem = $this->createRdfItem($targetClass !== false ? $targetClass : $itemClass, $qtiModel); $name = $rdfItem->getLabel(); $itemContent = $itemService->getItemContent($rdfItem); $xincluded = array(); foreach ($qtiModel->getBody()->getComposingElements('oat\\taoQtiItem\\model\\qti\\Xinclude') as $xincludeEle) { $xincluded[] = $xincludeEle->attr('href'); } $local = new LocalItemSource(array('item' => $rdfItem)); foreach ($qtiItemResource->getAuxiliaryFiles() as $auxResource) { // file on FS $auxFile = $folder . str_replace('/', DIRECTORY_SEPARATOR, $auxResource); // rel path in item $auxPath = str_replace(DIRECTORY_SEPARATOR, '/', helpers_File::getRelPath($qtiFile, $auxFile)); if (!empty($sharedStorage) && in_array($auxPath, $xincluded)) { $md5 = md5_file($auxFile); if (isset($sharedFiles[$md5])) { $info = $sharedFiles[$md5]; \common_Logger::i('Auxiliary file \'' . $auxPath . '\' linked to shared storage.'); } else { // TODO cleanup sharedstimulus import/export // move to taoQti item or library // validate the shared stimulus SharedStimulusImporter::isValidSharedStimulus($auxFile); // embed assets in the shared stimulus $newXmlFile = SharedStimulusPackageImporter::embedAssets($auxFile); $info = $sharedStorage->add($newXmlFile, basename($auxFile), $name); if (method_exists($sharedStorage, 'forceMimeType')) { // add() does not return link, so we need to parse it $resolver = new ItemMediaResolver($rdfItem, ''); $asset = $resolver->resolve($info['uri']); $sharedStorage->forceMimeType($asset->getMediaIdentifier(), 'application/qti+xml'); } $sharedFiles[$md5] = $info; \common_Logger::i('Auxiliary file \'' . $auxPath . '\' added to shared storage.'); } } else { // store locally, in a safe directory $safePath = ''; if (dirname($auxPath) !== '.') { $safePath = str_replace('../', '', dirname($auxPath)) . '/'; } $info = $local->add($auxFile, basename($auxFile), $safePath); \common_Logger::i('Auxiliary file \'' . $auxPath . '\' copied.'); } // replace uri if changed if ($auxPath != ltrim($info['uri'], '/')) { $itemContent = str_replace($auxPath, $info['uri'], $itemContent); } } foreach ($qtiItemResource->getDependencies() as $dependency) { // file on FS if (isset($dependencies[$dependency])) { $auxFile = $dependencies[$dependency]->getFile(); $auxFile = $folder . str_replace('/', DIRECTORY_SEPARATOR, $auxFile); // rel path in item $auxPath = str_replace(DIRECTORY_SEPARATOR, '/', helpers_File::getRelPath($qtiFile, $auxFile)); if (!empty($sharedStorage) && in_array($auxPath, $xincluded)) { $md5 = md5_file($auxFile); if (isset($sharedFiles[$md5])) { $info = $sharedFiles[$md5]; \common_Logger::i('Auxiliary file \'' . $auxPath . '\' linked to shared storage.'); } else { // TODO cleanup sharedstimulus import/export // move to taoQti item or library // validate the shared stimulus SharedStimulusImporter::isValidSharedStimulus($auxFile); // embed assets in the shared stimulus $newXmlFile = SharedStimulusPackageImporter::embedAssets($auxFile); $info = $sharedStorage->add($newXmlFile, basename($auxFile), $name); if (method_exists($sharedStorage, 'forceMimeType')) { // add() does not return link, so we need to parse it $resolver = new ItemMediaResolver($rdfItem, ''); $asset = $resolver->resolve($info['uri']); $sharedStorage->forceMimeType($asset->getMediaIdentifier(), 'application/qti+xml'); } $sharedFiles[$md5] = $info; \common_Logger::i('Auxiliary file \'' . $auxPath . '\' added to shared storage.'); } } else { // store locally, in a safe directory $safePath = ''; if (dirname($auxPath) !== '.') { $safePath = str_replace('../', '', dirname($auxPath)) . '/'; } $info = $local->add($auxFile, basename($auxFile), $safePath); \common_Logger::i('Auxiliary file \'' . $auxPath . '\' copied.'); } // replace uri if changed if ($auxPath != ltrim($info['uri'], '/')) { $itemContent = str_replace($auxPath, $info['uri'], $itemContent); } } } // Finally, import metadata. $this->importItemMetadata($metadataValues, $qtiItemResource, $rdfItem, $metadataInjectors); // And Apip if wanted if ($extractApip) { $this->storeApip($qtiFile, $rdfItem); } $itemService->setItemContent($rdfItem, $itemContent); $msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was successfully imported.', $qtiItemResource->getIdentifier()); $report = common_report_Report::createSuccess($msg, $rdfItem); } catch (ParsingException $e) { $report = new common_report_Report(common_report_Report::TYPE_ERROR, $e->getUserMessage()); } catch (ValidationException $ve) { $report = \common_report_Report::createFailure(__('IMS QTI Item referenced as "%s" in the IMS Manifest file could not be imported.', $qtiItemResource->getIdentifier())); $report->add($ve->getReport()); } catch (Exception $e) { // an error occured during a specific item $report = new common_report_Report(common_report_Report::TYPE_ERROR, __("An unknown error occured while importing the IMS QTI Package.")); common_Logger::e($e->getMessage()); } } catch (ValidationException $ve) { $validationReport = \common_report_Report::createFailure("The IMS Manifest file could not be validated"); $validationReport->add($ve->getReport()); $report->setMessage(__("No Items could be imported from the given IMS QTI package.")); $report->setType(common_report_Report::TYPE_ERROR); $report->add($validationReport); } catch (common_exception_UserReadableException $e) { $report = new common_report_Report(common_report_Report::TYPE_ERROR, __($e->getUserMessage())); $report->add($e); } return $report; }
/** * Import a QTI Test and its dependent Items into the TAO Platform. * * @param core_kernel_classes_Class $targetClass The RDFS Class where Ontology resources must be created. * @param oat\taoQtiItem\model\qti\Resource $qtiTestResource The QTI Test Resource representing the IMS QTI Test to be imported. * @param taoQtiTest_models_classes_ManifestParser $manifestParser The parser used to retrieve the IMS Manifest. * @param string $folder The absolute path to the folder where the IMS archive containing the test content * @return common_report_Report A report about how the importation behaved. */ protected function importTest(core_kernel_classes_Class $targetClass, Resource $qtiTestResource, taoQtiTest_models_classes_ManifestParser $manifestParser, $folder) { $itemImportService = ImportService::singleton(); $itemService = taoItems_models_classes_ItemsService::singleton(); $testClass = $targetClass; // Create an RDFS resource in the knowledge base that will hold // the information about the imported QTI Test. $testResource = $this->createInstance($testClass); $qtiTestModelResource = new core_kernel_classes_Resource(INSTANCE_TEST_MODEL_QTI); $modelProperty = new core_kernel_classes_Property(PROPERTY_TEST_TESTMODEL); $testResource->editPropertyValues($modelProperty, $qtiTestModelResource); // Create the report that will hold information about the import // of $qtiTestResource in TAO. $report = new common_report_Report(common_report_Report::TYPE_INFO); // The class where the items that belong to the test will be imported. $itemClass = new core_kernel_classes_Class(TAO_ITEM_CLASS); $targetClass = $itemClass->createSubClass($testResource->getLabel()); // Load and validate the manifest $qtiManifestParser = new taoQtiTest_models_classes_ManifestParser($folder . 'imsmanifest.xml'); $qtiManifestParser->validate(); // Set up $report with useful information for client code (especially for rollback). $reportCtx = new stdClass(); $reportCtx->manifestResource = $qtiTestResource; $reportCtx->rdfsResource = $testResource; $reportCtx->itemClass = $targetClass; $reportCtx->items = array(); $report->setData($reportCtx); // Expected test.xml file location. $expectedTestFile = $folder . str_replace('/', DIRECTORY_SEPARATOR, $qtiTestResource->getFile()); // Already imported test items (qti xml file paths). $alreadyImportedTestItemFiles = array(); // -- Check if the file referenced by the test QTI resource exists. if (is_readable($expectedTestFile) === false) { $report->add(common_report_Report::createFailure(__('No file found at location "%s".', $qtiTestResource->getFile()))); } else { // -- Load the test in a QTISM flavour. $testDefinition = new XmlDocument(); try { $testDefinition->load($expectedTestFile, true); // -- Load all items related to test. $itemError = false; // discover test's base path. $dependencies = taoQtiTest_helpers_Utils::buildAssessmentItemRefsTestMap($testDefinition, $manifestParser, $folder); if (count($dependencies) > 0) { foreach ($dependencies as $assessmentItemRefId => $qtiDependency) { if ($qtiDependency !== false) { if (Resource::isAssessmentItem($qtiDependency->getType())) { $qtiFile = $folder . str_replace('/', DIRECTORY_SEPARATOR, $qtiDependency->getFile()); // Skip if $qtiFile already imported (multiple assessmentItemRef "hrefing" the same file). if (array_key_exists($qtiFile, $alreadyImportedTestItemFiles) === false) { $itemReport = $itemImportService->importQTIFile($qtiFile, $targetClass); $rdfItem = $itemReport->getData(); if ($rdfItem) { $itemPath = taoItems_models_classes_ItemsService::singleton()->getItemFolder($rdfItem); foreach ($qtiDependency->getAuxiliaryFiles() as $auxResource) { // $auxResource is a relativ URL, so we need to replace the slashes with directory separators $auxPath = $folder . str_replace('/', DIRECTORY_SEPARATOR, $auxResource); // does the file referenced by $auxPath exist? if (is_readable($auxPath) === true) { $relPath = helpers_File::getRelPath($qtiFile, $auxPath); $destPath = $itemPath . $relPath; tao_helpers_File::copy($auxPath, $destPath, true); } else { $msg = __('Auxiliary file not found at location "%s".', $auxResource); $itemReport->add(new common_report_Report(common_report_Report::TYPE_WARNING, $msg)); } } $reportCtx->items[$assessmentItemRefId] = $rdfItem; $alreadyImportedTestItemFiles[$qtiFile] = $rdfItem; $itemReport->setMessage(__('IMS QTI Item referenced as "%s" in the IMS Manifest file successfully imported.', $qtiDependency->getIdentifier())); } else { $itemReport->setType(common_report_Report::TYPE_ERROR); $itemReport->setMessage(__('IMS QTI Item referenced as "%s" in the IMS Manifest file could not be imported.', $qtiDependency->getIdentifier())); $itemError = $itemError === false ? true : $itemError; } $report->add($itemReport); } else { $reportCtx->items[$assessmentItemRefId] = $alreadyImportedTestItemFiles[$qtiFile]; } } } else { $msg = __('The dependency to the IMS QTI AssessmentItemRef "%s" in the IMS Manifest file could not be resolved.', $assessmentItemRefId); $report->add(common_report_Report::createFailure($msg)); $itemError = $itemError === false ? true : $itemError; } } // If items did not produce errors, we import the test definition. if ($itemError === false) { common_Logger::i('Importing test...'); // Second step is to take care of the test definition and the related media (auxiliary files). // 1. Import test definition (i.e. the QTI-XML Test file). $testContent = $this->importTestDefinition($testResource, $testDefinition, $qtiTestResource, $reportCtx->items, $folder, $report); if ($testContent !== false) { // 2. Import test auxilliary files (e.g. stylesheets, images, ...). $this->importTestAuxiliaryFiles($testContent, $qtiTestResource, $folder, $report); // 3. Give meaningful names to resources. $testTitle = $testDefinition->getDocumentComponent()->getTitle(); $testResource->setLabel($testDefinition->getDocumentComponent()->getTitle()); $targetClass->setLabel($testDefinition->getDocumentComponent()->getTitle()); } } else { $msg = __("One or more dependent IMS QTI Items could not be imported."); $report->add(common_report_Report::createFailure($msg)); } } else { // No depencies found (i.e. no item resources bound to the test). $msg = __("No reference to any IMS QTI Item found."); $report->add(common_report_Report::createFailure($msg)); } } catch (StorageException $e) { // Source of the exception = $testDefinition->load() // What is the reason ? $finalErrorString = ''; $eStrs = array(); if (($libXmlErrors = $e->getErrors()) !== null) { foreach ($libXmlErrors as $libXmlError) { $eStrs[] = __('XML error at line %1$d column %2$d "%3$s".', $libXmlError->line, $libXmlError->column, trim($libXmlError->message)); } } $finalErrorString = implode("\n", $eStrs); if (empty($finalErrorString) === true) { // Not XML malformation related. No info from LibXmlErrors extracted. if (($previous = $e->getPrevious()) != null) { // Useful information could be found here. $finalErrorString = $previous->getMessage(); if ($previous instanceof UnmarshallingException) { $domElement = $previous->getDOMElement(); $finalErrorString = __('Inconsistency at line %1d:', $domElement->getLineNo()) . ' ' . $previous->getMessage(); } } else { $finalErrorString = __("Unknown error."); } } $msg = __("Error found in the IMS QTI Test:\n%s", $finalErrorString); $report->add(common_report_Report::createFailure($msg)); } } if ($report->containsError() === false) { $report->setType(common_report_Report::TYPE_SUCCESS); $msg = __("IMS QTI Test referenced as \"%s\" in the IMS Manifest file successfully imported.", $qtiTestResource->getIdentifier()); $report->setMessage($msg); } else { $report->setType(common_report_Report::TYPE_ERROR); $msg = __("The IMS QTI Test referenced as \"%s\" in the IMS Manifest file could not be imported.", $qtiTestResource->getIdentifier()); $report->setMessage($msg); } return $report; }
/** * Import dependencies files * * @param QtiResource $qtiItemResource * @param $dependencies * @return $this * @throws AssetManagerException */ public function importDependencyFiles(QtiResource $qtiItemResource, $dependencies) { $qtiFile = $this->getSource() . \helpers_File::urlToPath($qtiItemResource->getFile()); foreach ($qtiItemResource->getDependencies() as $dependenciesFile) { if (!isset($dependencies[$dependenciesFile])) { continue; } $absolutePath = $this->getAbsolutePath($dependencies[$dependenciesFile]->getFile()); $relativePath = $this->getRelativePath($qtiFile, $absolutePath); try { $this->importAsset($absolutePath, $relativePath); } catch (\common_Exception $e) { throw new AssetManagerException('Error occurs during dependency assets handling for item: ' . $qtiItemResource->getIdentifier() . ', assetFile: ' . $relativePath, 0, $e); } } return $this; }
/** * @param $folder * @param \oat\taoQtiItem\model\qti\Resource $qtiItemResource * @param $itemClass * @param array $dependencies * @param array $metadataValues * @param array $metadataInjectors * @param array $metadataGuardians * @param array $metadataClassLookups * @param array $sharedFiles * @param array $createdClass * @return common_report_Report * @throws common_exception_Error */ public function importQtiItem($folder, Resource $qtiItemResource, $itemClass, array $dependencies = array(), array $metadataValues = array(), array $metadataInjectors = array(), array $metadataGuardians = array(), array $metadataClassLookups = array(), array $sharedFiles = array(), &$createdClasses = array()) { try { $qtiService = Service::singleton(); //load the information about resources in the manifest try { $resourceIdentifier = $qtiItemResource->getIdentifier(); // Use the guardians to check whether or not the item has to be imported. foreach ($metadataGuardians as $guardian) { if (isset($metadataValues[$resourceIdentifier]) === true) { if (($guard = $guardian->guard($metadataValues[$resourceIdentifier])) !== false) { \common_Logger::i("Resource '{$resourceIdentifier}' is already stored in the database and will not be imported."); $msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was already stored in the Item Bank.', $resourceIdentifier); $report = common_report_Report::createInfo($msg, $guard); // Simply do not import again. return $report; } } } $targetClass = false; // Use the classLookups to determine where the item has to go. foreach ($metadataClassLookups as $classLookup) { if (isset($metadataValues[$resourceIdentifier]) === true) { \common_Logger::i("Target Class Lookup for resource '{$resourceIdentifier}' ..."); if (($targetClass = $classLookup->lookup($metadataValues[$resourceIdentifier])) !== false) { \common_Logger::i("Class Lookup Successful. Resource '{$resourceIdentifier}' will be stored in RDFS Class '" . $targetClass->getUri() . "'."); if ($classLookup instanceof MetadataClassLookupClassCreator) { $createdClasses = $classLookup->createdClasses(); } break; } } } $qtiFile = $folder . helpers_File::urlToPath($qtiItemResource->getFile()); common_Logger::i('file :: ' . $qtiItemResource->getFile()); $qtiModel = $this->createQtiItemModel($qtiFile); $rdfItem = $this->createRdfItem($targetClass !== false ? $targetClass : $itemClass, $qtiModel); $itemAssetManager = new AssetManager(); $itemAssetManager->setItemContent($qtiModel->toXML()); $itemAssetManager->setSource($folder); /** * Load asset handler following priority handler defined by you * The first applicable will be used to import assets */ /** Portable element handler */ $peHandler = new PortableAssetHandler($qtiModel, dirname($qtiFile)); $itemAssetManager->loadAssetHandler($peHandler); /** Shared stimulus handler */ $sharedStimulusHandler = new SharedStimulusAssetHandler(); $sharedStimulusHandler->setQtiModel($qtiModel)->setItemSource(new ItemMediaResolver($rdfItem, ''))->setSharedFiles($sharedFiles)->setParentPath($rdfItem->getLabel()); $itemAssetManager->loadAssetHandler($sharedStimulusHandler); /** Local storage handler */ $localHandler = new LocalAssetHandler(); $localHandler->setItemSource(new LocalItemSource(array('item' => $rdfItem))); $itemAssetManager->loadAssetHandler($localHandler); $itemAssetManager->importAuxiliaryFiles($qtiItemResource)->importDependencyFiles($qtiItemResource, $dependencies); $itemAssetManager->finalize(); $qtiModel = $this->createQtiItemModel($itemAssetManager->getItemContent(), false); $qtiService->saveDataItemToRdfItem($qtiModel, $rdfItem); // Finally, import metadata. $this->importResourceMetadata($metadataValues, $qtiItemResource, $rdfItem, $metadataInjectors); $eventManager = ServiceManager::getServiceManager()->get(EventManager::CONFIG_ID); $eventManager->trigger(new ItemImported($rdfItem, $qtiModel)); $msg = __('The IMS QTI Item referenced as "%s" in the IMS Manifest file was successfully imported.', $qtiItemResource->getIdentifier()); $report = common_report_Report::createSuccess($msg, $rdfItem); } catch (ParsingException $e) { $message = $e->getUserMessage(); $report = new common_report_Report(common_report_Report::TYPE_ERROR, $message); } catch (ValidationException $ve) { $report = common_report_Report::createFailure(__('IMS QTI Item referenced as "%s" in the IMS Manifest file could not be imported.', $qtiItemResource->getIdentifier())); $report->add($ve->getReport()); } catch (XmlStorageException $e) { $files = array(); $message = __('There are errors in the following shared stimulus : ') . PHP_EOL; /** @var \LibXMLError $error */ foreach ($e->getErrors() as $error) { if (!in_array($error->file, $files)) { $files[] = $error->file; $message .= '- ' . basename($error->file) . ' :' . PHP_EOL; } $message .= $error->message . ' at line : ' . $error->line . PHP_EOL; } $report = new common_report_Report(common_report_Report::TYPE_ERROR, $message); } catch (PortableElementInvalidModelException $pe) { $report = common_report_Report::createFailure(__('IMS QTI Item referenced as "%s" contains a portable element and cannot be imported.', $qtiItemResource->getIdentifier())); $report->add($pe->getReport()); if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists()) { $rdfItem->delete(); } } catch (PortableElementException $e) { // an error occured during a specific item if ($e instanceof common_exception_UserReadableException) { $msg = __('Error on item %1$s : %2$s', $qtiItemResource->getIdentifier(), $e->getUserMessage()); } else { $msg = __('Error on item %s', $qtiItemResource->getIdentifier()); common_Logger::d($e->getMessage()); } $report = new common_report_Report(common_report_Report::TYPE_ERROR, $msg); if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists()) { $rdfItem->delete(); } } catch (Exception $e) { // an error occured during a specific item $report = new common_report_Report(common_report_Report::TYPE_ERROR, __("An unknown error occured while importing the IMS QTI Package.")); if (isset($rdfItem) && !is_null($rdfItem) && $rdfItem->exists()) { $rdfItem->delete(); } common_Logger::e($e->getMessage()); } } catch (ValidationException $ve) { $validationReport = common_report_Report::createFailure("The IMS Manifest file could not be validated"); $validationReport->add($ve->getReport()); $report->setMessage(__("No Items could be imported from the given IMS QTI package.")); $report->setType(common_report_Report::TYPE_ERROR); $report->add($validationReport); } catch (common_exception_UserReadableException $e) { $report = new common_report_Report(common_report_Report::TYPE_ERROR, __($e->getUserMessage())); $report->add($e); } return $report; }
/** * Import the Test itself by importing its QTI-XML definition into the system, after * the QTI Items composing the test were also imported. * * The $itemMapping argument makes the implementation of this method able to know * what are the items that were imported. The $itemMapping is an associative array * where keys are the assessmentItemRef's identifiers and the values are the core_kernel_classes_Resources of * the items that are now stored in the system. * * When this method returns false, it means that an error occured at the level of the content of the imported test * itself e.g. an item referenced by the test is not present in the content package. In this case, $report might * contain useful information to return to the client. * * @param core_kernel_classes_Resource $testResource A Test Resource the new content must be bind to. * @param XmlDocument $testDefinition An XmlAssessmentTestDocument object. * @param Resource $qtiResource The manifest resource describing the test to be imported. * @param array $itemMapping An associative array that represents the mapping between assessmentItemRef elements and the imported items. * @param string $extractionFolder The absolute path to the temporary folder containing the content of the imported IMS QTI Package Archive. * @param common_report_Report $report A Report object to be filled during the import. * @return Directory The newly created test content. * @throws taoQtiTest_models_classes_QtiTestServiceException If an unexpected runtime error occurs. */ protected function importTestDefinition(core_kernel_classes_Resource $testResource, XmlDocument $testDefinition, Resource $qtiResource, array $itemMapping, $extractionFolder, common_report_Report $report) { foreach ($itemMapping as $itemRefId => $itemResource) { $itemRef = $testDefinition->getDocumentComponent()->getComponentByIdentifier($itemRefId); $itemRef->setHref($itemResource->getUri()); } $oldFile = $this->getQtiTestFile($testResource); $oldFile->delete(); $ds = DIRECTORY_SEPARATOR; $path = dirname($qtiResource->getFile()) . $ds . TAOQTITEST_FILENAME; $dir = $this->getQtiTestDir($testResource); $newFile = $dir->getFile($path); $newFile->write($testDefinition->saveToString()); return $this->getQtiTestDir($testResource); }