* @package taoQtiTest */ class taoQtiTest_models_classes_QtiTestCompiler extends taoTests_models_classes_TestCompiler implements ContainerProvider { const ADAPTIVE_SECTION_MAP_FILENAME = 'adaptive-section-map.json'; const ADAPTIVE_PLACEHOLDER_CATEGORY = 'x-tao-qti-adaptive-placeholder'; const COMPILATION_INFO_FILENAME = 'compilation-info.json'; /** * The list of mime types of files that are accepted to be put * into the public compilation directory. * * @var array */ private static $publicMimeTypes = ['text/css', 'image/png', 'image/jpeg', 'image/gif', 'text/html', 'application/x-shockwave-flash', 'video/x-flv', 'image/bmp', 'image/svg+xml', 'audio/mpeg', 'audio/ogg', 'video/quicktime', 'video/webm', 'video/ogg', 'application/pdf', 'application/x-font-woff', 'application/vnd.ms-fontobject', 'application/x-font-ttf', 'image/svg+xml', 'image/svg+xml']; /** * The public compilation directory. * * @var tao_models_classes_service_StorageDirectory */ private $publicDirectory = null; /** * The private compilation directory. * * @var tao_models_classes_service_StorageDirectory */ private $privateDirectory = null; /** * The rendering engine that will be used to create rubric block templates. * * @var XhtmlRenderingEngine */ private $renderingEngine = null; /** * The Post renderer to be used in template oriented rendering. * * @var MarkupPostRenderer */ private $markupPostRenderer = null; /** * The CSS Scoper will scope CSS files to their related rubric block. * * @var CssScoper */ private $cssScoper = null; /** * An additional path to be used when test definitions are located in sub-directories. * * @var string */ private $extraPath; private $compilationInfo = []; /** * Whenever or not rubric block css should be scoped * @var boolean */ private $settingCssScope = true; /** * Whenever or not the new client test runner should be used * @var boolean */ private $settingClientContainer = true; /** * Get the public compilation directory. * * @return tao_models_classes_service_StorageDirectory */ protected function getPublicDirectory() { return $this->publicDirectory; } /** * Set the public compilation directory. * * @param tao_models_classes_service_StorageDirectory $directory */ protected function setPublicDirectory(tao_models_classes_service_StorageDirectory $directory) { $this->publicDirectory = $directory; } /** * Get the private compilation directory. * * @return tao_models_classes_service_StorageDirectory */ protected function getPrivateDirectory() { return $this->privateDirectory; } /** * Set the private compilation directory. * * @param tao_models_classes_service_StorageDirectory $directory */ protected function setPrivateDirectory(tao_models_classes_service_StorageDirectory $directory) { $this->privateDirectory = $directory; } /** * Get the rendering engine that will be used to render rubric block templates. * * @return XhtmlRenderingEngine */ protected function getRenderingEngine() { return $this->renderingEngine; } /** * Set the rendering engine that will be used to render rubric block templates. * * @param XhtmlRenderingEngine $renderingEngine */ protected function setRenderingEngine(XhtmlRenderingEngine $renderingEngine) { $this->renderingEngine = $renderingEngine; } /** * Get the markup post renderer to be used after template oriented rendering. * * @return MarkupPostRenderer */ protected function getMarkupPostRenderer() { return $this->markupPostRenderer; } /** * Set the markup post renderer to be used after template oriented rendering. * * @param MarkupPostRenderer $markupPostRenderer */ protected function setMarkupPostRenderer(MarkupPostRenderer $markupPostRenderer) { $this->markupPostRenderer = $markupPostRenderer; } /** * Get the CSS Scoper tool that will scope CSS files to their related rubric block. * * @return CssScoper */ protected function getCssScoper() { return $this->cssScoper; } /** * Set the CSS Scoper tool that will scope CSS files to their related rubric block. * * @param CssScoper $cssScoper */ protected function setCssScoper(CssScoper $cssScoper) { $this->cssScoper = $cssScoper; } /** * Get the extra path to be used when test definition is located * in sub-directories. * * @return string */ protected function getExtraPath() { return $this->extraPath; } /** * Set the extra path to be used when test definition is lovated in sub-directories. * * @param string $extraPath */ protected function setExtraPath($extraPath) { $this->extraPath = $extraPath; } /** * Initialize the compilation by: * * * 1. Spawning public and private compilation directoryies. * * 2. Instantiating appropriate rendering engine and CSS utilities. * * for the next compilation process. */ protected function initCompilation() { $ds = DIRECTORY_SEPARATOR; // Initialize public and private compilation directories. $this->setPrivateDirectory($this->spawnPrivateDirectory()); $this->setPublicDirectory($this->spawnPublicDirectory()); // Extra path. $testService = taoQtiTest_models_classes_QtiTestService::singleton(); $testDefinitionDir = dirname($testService->getRelTestPath($this->getResource())); $this->setExtraPath($testDefinitionDir); // Initialize rendering engine. $renderingEngine = new XhtmlRenderingEngine(); $renderingEngine->setStylesheetPolicy(XhtmlRenderingEngine::STYLESHEET_SEPARATE); $renderingEngine->setXmlBasePolicy(XhtmlRenderingEngine::XMLBASE_PROCESS); $renderingEngine->setFeedbackShowHidePolicy(XhtmlRenderingEngine::TEMPLATE_ORIENTED); $renderingEngine->setViewPolicy(XhtmlRenderingEngine::TEMPLATE_ORIENTED); $renderingEngine->setPrintedVariablePolicy(XhtmlRenderingEngine::TEMPLATE_ORIENTED); $renderingEngine->setStateName(taoQtiTest_models_classes_QtiTestService::TEST_RENDERING_STATE_NAME); $renderingEngine->setRootBase(taoQtiTest_models_classes_QtiTestService::TEST_PLACEHOLDER_BASE_URI . rtrim($this->getExtraPath(), $ds)); $renderingEngine->setViewsName(taoQtiTest_models_classes_QtiTestService::TEST_VIEWS_NAME); $this->setRenderingEngine($renderingEngine); // Initialize CSS Scoper. $this->setCssScoper(new CssScoper()); // Initialize Post Markup Renderer. $this->setMarkupPostRenderer(new MarkupPostRenderer(true, true, true)); // Initialize the index that will contains info about items $this->setContext(new QtiTestCompilerIndex()); } /** * Compile a QTI Test and the related QTI Items. * * The compilation process occurs as follows: * * * 1. The resources composing the test are copied into the private compilation directory. * * 2. The test definition is packed (test and items put together in a single definition). * * 3. The items composing the test are compiled. * * 4. The rubric blocks are rendered into PHP templates. * * 5. The test definition is compiled into PHP source code for maximum performance. * * 6. The resources composing the test that have to be accessed at delivery time are compied into the public compilation directory. * * 7. The Service Call definition enabling TAO to run the compiled test is built. * * @return tao_models_classes_service_ServiceCall A ServiceCall object that represent the way to call the newly compiled test. * @throws taoQtiTest_models_classes_QtiTestCompilationFailedException If an error occurs during the compilation. */ public function compile() { $report = new common_report_Report(common_report_Report::TYPE_INFO); try { // 0. Initialize compilation (compilation directories, renderers, ...). $this->initCompilation(); // 1. Copy the resources composing the test into the private complilation directory. $this->copyPrivateResources(); // 2. Compact the test definition itself. $compiledDoc = $this->compactTest(); // 3. Compile the items of the test. $itemReport = $this->compileItems($compiledDoc); $report->add($itemReport); if ($itemReport->getType() != common_report_Report::TYPE_SUCCESS) { common_Logger::e($report->getMessage(), $report->getErrors()); $msg = 'Failed item compilation.'; $code = taoQtiTest_models_classes_QtiTestCompilationFailedException::ITEM_COMPILATION; throw new taoQtiTest_models_classes_QtiTestCompilationFailedException($msg, $this->getResource(), $code); } // 4. Explode the rubric blocks in the test into rubric block refs. $this->explodeRubricBlocks($compiledDoc); // 5. Update test definition with additional runtime info. $assessmentTest = $compiledDoc->getDocumentComponent(); //$this->updateTestDefinition($assessmentTest); // 6. Compile rubricBlocks and serialize on disk. $this->compileRubricBlocks($assessmentTest); // 7. Copy the needed files into the public directory. $this->copyPublicResources(); // 8. Compile adaptive components of the test. $this->compileAdaptive($assessmentTest); // 9. Compile the test definition into PHP source code and put it // into the private directory. $this->compileTest($assessmentTest); // 9.1. Compile test meta data into JSON file. $this->compileTestMetadata($this->getResource()); // 10. Compile the test meta data into PHP array source code and put it // into the private directory. $this->compileMeta($assessmentTest); // 11. Compile the test index in JSON content and put it into the private directory. $this->compileIndex(); // 12. Build the service call. $serviceCall = $this->buildServiceCall(); // 13. Record some compilation info. $this->buildCompilationInfo(); common_Logger::t("QTI Test successfully compiled."); $report->setType(common_report_Report::TYPE_SUCCESS); $report->setMessage(__('QTI Test "%s" successfully published.', $this->getResource()->getLabel())); $report->setData($serviceCall); } catch (XmlStorageException $e) { $report = $this->prepareXmlStorageExceptionReport($e, $report); } catch (Exception $e) { common_Logger::e($e->getMessage()); // All exception that were not catched in the compilation steps // above have a last chance here. $report->setType(common_report_Report::TYPE_ERROR); $report->setMessage(__('QTI Test "%s" publishing failed.', $this->getResource()->getLabel())); } // Reset time outs to initial value. helpers_TimeOutHelper::reset(); return $report; } /** * @param XmlStorageException $e * @param common_report_Report $report * @return common_report_Report * @throws common_exception_Error */ private function prepareXmlStorageExceptionReport(XmlStorageException $e, common_report_Report $report) { $details[] = $e->getMessage(); $subReport = new common_report_Report(common_report_Report::TYPE_ERROR, __('The QTI Test XML or one of its dependencies is malformed or empty.')); $itemReport = new common_report_Report(common_report_Report::TYPE_ERROR, $e->getMessage()); while (($previous = $e->getPrevious()) != null) { $details[] = $previous->getMessage(); $e = $e->getPrevious(); } if (method_exists($e, 'getErrors')) { /** @var LibXMLError $error */ foreach ($e->getErrors() as $error) { $itemReport->add(new common_report_Report(common_report_Report::TYPE_ERROR, $error->message)); } } else { $itemReport->add(new common_report_Report(common_report_Report::TYPE_ERROR, $e->getMessage())); } $subReport->add($itemReport); common_Logger::e(implode("\n", $details)); $report->add($subReport); $report->setType(common_report_Report::TYPE_ERROR); $report->setMessage(__('QTI Test "%s" publishing failed.', $this->getResource()->getLabel())); return $report; } /** * {@inheritDoc} * @see \oat\taoDelivery\model\container\delivery\ContainerProvider::getContainer() */ public function getContainer() { $registry = DeliveryContainerRegistry::getRegistry(); $registry->setServiceLocator($this->getServiceLocator()); if ($this->useClientTestRunner()) { // client container $container = $registry->getDeliveryContainer('qtiTest', [ 'source' => $this->getResource()->getUri(), 'private' => $this->getPrivateDirectory()->getId(), 'public' => $this->getPublicDirectory()->getId() ]); } else { $serviceCall = $this->buildServiceCall(); $container = $registry->getDeliveryContainer('service', $serviceCall); } return $container; } /** * Compact the test and items in a single QTI-XML Compact Document. * * @return XmlCompactDocument. */ protected function compactTest() { $testService = taoQtiTest_models_classes_QtiTestService::singleton(); $test = $this->getResource(); common_Logger::t('Compacting QTI test ' . $test->getLabel() . '...'); $resolver = new taoQtiTest_helpers_ItemResolver(Service::singleton()); $originalDoc = $testService->getDoc($test); $compiledDoc = XmlCompactDocument::createFromXmlAssessmentTestDocument($originalDoc, $resolver, $resolver); common_Logger::t("QTI Test XML transformed in a compact version."); return $compiledDoc; } /** * Compile the items referended by $compactDoc. * * @param XmlCompactDocument $compactDoc An XmlCompactDocument object referencing the items of the test. * @throws taoQtiTest_models_classes_QtiTestCompilationFailedException If the test does not refer to at least one item. * @return common_report_Report */ protected function compileItems(XmlCompactDocument $compactDoc) { $report = new common_report_Report(common_report_Report::TYPE_SUCCESS, __('Items Compilation')); $iterator = new QtiComponentIterator($compactDoc->getDocumentComponent(), ['assessmentItemRef']); $itemCount = 0; foreach ($iterator as $assessmentItemRef) { // Each item could take some time to be compiled, making the request to timeout. helpers_TimeOutHelper::setTimeOutLimit(helpers_TimeOutHelper::SHORT); $subReport = $this->useClientTestRunner() ? $this->compileJsonItem($assessmentItemRef) : $this->legacyCompileItem($assessmentItemRef); $report->add($subReport); if ($subReport->getType() != common_report_Report::TYPE_SUCCESS) { $report->setType(common_report_Report::TYPE_ERROR); } // Count the item even if it fails to avoid false "no item" error. $itemCount++; common_Logger::t("QTI Item successfully compiled and registered as a service call in the QTI Test Definition."); } if ($itemCount === 0) { $report->setType(common_report_Report::TYPE_ERROR); $report->setMessage(__("A QTI Test must contain at least one QTI Item to be compiled. None found.")); } return $report; } /** * * @param AssessmentItemRef $assessmentItemRef * @return common_report_Report */ protected function legacyCompileItem(AssessmentItemRef &$assessmentItemRef) { $item = new core_kernel_classes_Resource($assessmentItemRef->getHref()); $report = $this->subCompile($item); if ($report->getType() == common_report_Report::TYPE_SUCCESS) { $itemService = $report->getdata(); $inputValues = tao_models_classes_service_ServiceCallHelper::getInputValues($itemService, []); $assessmentItemRef->setHref($inputValues['itemUri'] . '|' . $inputValues['itemPath'] . '|' . $inputValues['itemDataPath']); // Ask for item ref information compilation for fast later usage. $this->compileAssessmentItemRefHrefIndex($assessmentItemRef); } return $report; } /** * * @param AssessmentItemRef $item * @return common_report_Report */ protected function compileJsonItem(AssessmentItemRef &$assessmentItemRef) { $jsonCompiler = new QtiJsonItemCompiler( new core_kernel_classes_Resource($assessmentItemRef->getHref()), $this->getStorage() ); $jsonCompiler->setServiceLocator($this->getServiceLocator()); $jsonCompiler->setContext($this->getContext()); $report = $jsonCompiler->compileJson(); if ($report->getType() == common_report_Report::TYPE_SUCCESS) { // store $itemUri, $publicDirId, $privateDirId in a string $assessmentItemRef->setHref(implode('|', $report->getdata())); $this->compileAssessmentItemRefHrefIndex($assessmentItemRef); } return $report; } /** * Explode the rubric blocks of the test definition into separate QTI-XML files and * remove the compact XML document from the file system (useless for * the rest of the compilation process). * * @param XmlCompactDocument $compiledDoc */ protected function explodeRubricBlocks(XmlCompactDocument $compiledDoc) { common_Logger::t("Exploding QTI rubricBlocks..."); $privateDir = $this->getPrivateDirectory(); $explodedRubricBlocks = $compiledDoc->explodeRubricBlocks(); foreach ($explodedRubricBlocks as $href => $rubricBlock) { $doc = new XmlDocument(); $doc->setDocumentComponent($rubricBlock); $data = $doc->saveToString(); $privateDir->write($href, $data); } } /** * Update the test definition with additional data, such as TAO specific * rules and variables. * * @param AssessmentTest $assessmentTest */ protected function updateTestDefinition(AssessmentTest $assessmentTest) { // Call TestCategoryRulesService to generate additional rules if enabled. $config = $this->getTaoQtiTestExtension()->getConfig('TestCompiler'); if (isset($config['enable-category-rules-generation']) && $config['enable-category-rules-generation'] === true) { common_Logger::t('Automatic Category Rules Generation will occur...'); $testCategoryRulesService = $this->getServiceLocator()->get(TestCategoryRulesService::SERVICE_ID); $testCategoryRulesService->apply($assessmentTest); } } /** * Copy the resources (e.g. images) of the test to the private compilation directory. */ protected function copyPrivateResources() { $testService = taoQtiTest_models_classes_QtiTestService::singleton(); $testDefinitionDir = $testService->getQtiTestDir($this->getResource()); $privateDir = $this->getPrivateDirectory(); $iterator = $testDefinitionDir->getFlyIterator(Directory::ITERATOR_RECURSIVE | Directory::ITERATOR_FILE); foreach ($iterator as $object) { $relPath = $testDefinitionDir->getRelPath($object); $privateDir->getFile($relPath)->write($object->readStream()); } } /** * Build the Service Call definition that makes TAO able to run the compiled test * later on at delivery time. * * @return tao_models_classes_service_ServiceCall */ protected function buildServiceCall() { $service = new tao_models_classes_service_ServiceCall(new core_kernel_classes_Resource(RunnerService::INSTANCE_TEST_RUNNER_SERVICE)); $param = new tao_models_classes_service_ConstantParameter( // Test Definition URI passed to the QtiTestRunner service. new core_kernel_classes_Resource(taoQtiTest_models_classes_QtiTestService::INSTANCE_FORMAL_PARAM_TEST_DEFINITION), $this->getResource() ); $service->addInParameter($param); $param = new tao_models_classes_service_ConstantParameter( // Test Compilation URI passed to the QtiTestRunner service. new core_kernel_classes_Resource(taoQtiTest_models_classes_QtiTestService::INSTANCE_FORMAL_PARAM_TEST_COMPILATION), $this->getPrivateDirectory()->getId() . '|' . $this->getPublicDirectory()->getId() ); $service->addInParameter($param); return $service; } /** * Compile the RubricBlocRefs' contents into a separate rubric block PHP template. * * @param AssessmentTest $assessmentTest The AssessmentTest object you want to compile the rubrickBlocks. */ protected function compileRubricBlocks(AssessmentTest $assessmentTest) { common_Logger::t("Compiling QTI rubricBlocks..."); $rubricBlockRefs = $assessmentTest->getComponentsByClassName('rubricBlockRef'); $testService = taoQtiTest_models_classes_QtiTestService::singleton(); $sourceDir = $testService->getQtiTestDir($this->getResource()); foreach ($rubricBlockRefs as $rubricRef) { $rubricRefHref = $rubricRef->getHref(); $cssScoper = $this->getCssScoper(); $renderingEngine = $this->getRenderingEngine(); $markupPostRenderer = $this->getMarkupPostRenderer(); $publicCompiledDocDir = $this->getPublicDirectory(); $privateCompiledDocDir = $this->getPrivateDirectory(); // -- loading... common_Logger::t("Loading rubricBlock '" . $rubricRefHref . "'..."); $rubricDoc = new XmlDocument(); $rubricDoc->loadFromString($this->getPrivateDirectory()->read($rubricRefHref)); common_Logger::t("rubricBlock '" . $rubricRefHref . "' successfully loaded."); // -- rendering... common_Logger::t("Rendering rubricBlock '" . $rubricRefHref . "'..."); $pathinfo = pathinfo($rubricRefHref); $renderingFile = $pathinfo['filename'] . '.php'; $rubric = $rubricDoc->getDocumentComponent(); $rubricStylesheets = $rubric->getStylesheets(); $stylesheets = new StylesheetCollection(); // In any case, include the base QTI Stylesheet. $stylesheets->merge($rubricStylesheets); $rubric->setStylesheets($stylesheets); // -- If the rubricBlock has no id, give it a auto-generated one in order // to be sure that CSS rescoping procedure works fine (it needs at least an id // to target its scoping). if ($rubric->hasId() === false) { // Prepend 'tao' to the generated id because the CSS // ident token must begin by -|[a-zA-Z] $rubric->setId('tao' . uniqid()); } // -- Copy eventual remote resources of the rubricBlock. $this->copyRemoteResources($rubric); $domRendering = $renderingEngine->render($rubric); $mainStringRendering = $markupPostRenderer->render($domRendering); // Prepend stylesheets rendering to the main rendering. $styleRendering = $renderingEngine->getStylesheets(); $mainStringRendering = $styleRendering->ownerDocument->saveXML($styleRendering) . $mainStringRendering; if ($this->useCssScoping()) { foreach ($stylesheets as $rubricStylesheet) { $relPath = trim($this->getExtraPath(), '/'); $relPath = (empty($relPath) ? '' : $relPath . DIRECTORY_SEPARATOR) . $rubricStylesheet->getHref(); $sourceFile = $sourceDir->getFile($relPath); if (!$publicCompiledDocDir->has($relPath)) { try { $data = $sourceFile->read(); $tmpDir = \tao_helpers_File::createTempDir(); $tmpFile = $tmpDir . 'tmp.css'; file_put_contents($tmpFile, $data); $scopedCss = $cssScoper->render($tmpFile, $rubric->getId()); unlink($tmpFile); rmdir($tmpDir); $publicCompiledDocDir->write($relPath, $scopedCss); } catch (\InvalidArgumentException $e) { common_Logger::e('Unable to copy file into public directory: ' . $relPath); } } } } // -- Replace the artificial 'tao://qti-directory' base path with a runtime call to the delivery time base path. $mainStringRendering = str_replace(taoQtiTest_models_classes_QtiTestService::TEST_PLACEHOLDER_BASE_URI, '', $mainStringRendering); if (!$privateCompiledDocDir->has($renderingFile)) { try { $privateCompiledDocDir->write($renderingFile, $mainStringRendering); common_Logger::t("rubricBlockRef '" . $rubricRefHref . "' successfully rendered."); } catch (\InvalidArgumentException $e) { common_Logger::e('Unable to copy file into public directory: ' . $renderingFile); } } // -- Clean up old rubric block and reference the new rubric block template. $privateCompiledDocDir->delete($rubricRefHref); $rubricRef->setHref('./' . $pathinfo['filename'] . '.php'); } } /** * Copy the test resources (e.g. images) that will be availabe at delivery time * in the public compilation directory. * */ protected function copyPublicResources() { $testService = taoQtiTest_models_classes_QtiTestService::singleton(); $testDefinitionDir = $testService->getQtiTestDir($this->getResource()); $publicCompiledDocDir = $this->getPublicDirectory(); $iterator = $testDefinitionDir->getFlyIterator(Directory::ITERATOR_RECURSIVE | Directory::ITERATOR_FILE); foreach ($iterator as $file) { /** @var \oat\oatbox\filesystem\File $file */ $mime = $file->getMimeType(); $pathinfo = pathinfo($file->getBasename()); if (in_array($mime, self::getPublicMimeTypes()) === true && $pathinfo['extension'] !== 'php') { $publicPathFile = $testDefinitionDir->getRelPath($file); try { common_Logger::d('Public ' . $file->getPrefix() . '(' . $mime . ') to ' . $publicPathFile); $publicCompiledDocDir->getFile($publicPathFile)->write($file->readStream()); } catch (FileExistsException $e) { common_Logger::w('File ' . $publicPathFile . ' copied twice to public test folder during compilation'); } } } } /** * Copy all remote resource (absolute URLs to another host) contained in a rubricBlock into a dedicated directory. Remote resources * can be refereced by the following QTI classes/attributes: * * * a:href * * object:data * * img:src * * @param AssessmentTest $assessmentTest An AssessmentTest object. * @throws taoQtiTest_models_classes_QtiTestCompilationFailedException If a remote resource cannot be retrieved. */ protected function copyRemoteResources(RubricBlock $rubricBlock) { $ds = DIRECTORY_SEPARATOR; $tmpDir = tao_helpers_File::createTempDir(); $destPath = trim($this->getExtraPath(), $ds) . $ds . taoQtiTest_models_classes_QtiTestService::TEST_REMOTE_FOLDER . $ds; // Search for all class-attributes in QTI-XML that might reference a remote file. $search = $rubricBlock->getComponentsByClassName(['a', 'object', 'img']); foreach ($search as $component) { switch ($component->getQtiClassName()) { case 'object': $url = $component->getData(); break; case 'img': $url = $component->getSrc(); break; } if (isset($url) && !preg_match('@^' . ROOT_URL . '@', $url) && !Url::isRelative($url)) { $tmpFile = taoItems_helpers_Deployment::retrieveFile($url, $tmpDir); if ($tmpFile !== false) { $pathinfo = pathinfo($tmpFile); $handle = fopen($tmpFile, 'r'); $this->getPublicDirectory()->writeStream($destPath . $pathinfo['basename'], $handle); fclose($handle); unlink($tmpFile); $newUrl = taoQtiTest_models_classes_QtiTestService::TEST_REMOTE_FOLDER . '/' . $pathinfo['basename']; switch ($component->getQtiClassName()) { case 'object': $component->setData($newUrl); break; case 'img': $component->setSrc($newUrl); break; } } else { $msg = "The remote resource referenced by '${url}' could not be retrieved."; throw new taoQtiTest_models_classes_QtiTestCompilationFailedException($msg, $this->getResource(), taoQtiTest_models_classes_QtiTestCompilationFailedException::REMOTE_RESOURCE); } } } } /** * Compile the given $test into PHP source code for maximum performance. The file will be stored * into PRIVATE_DIRECTORY/compact-test.php. * * @param AssessmentTest $test */ protected function compileTest(AssessmentTest $test) { common_Logger::t("Compiling QTI test definition..."); $this->getServiceLocator()->get(CompilationDataService::SERVICE_ID)->writeCompilationData( $this->getPrivateDirectory(), taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_FILENAME, $test ); common_Logger::d("QTI-PHP Test Compilation file saved to stream."); } /** * @param core_kernel_classes_Resource $resource * @throws FileNotFoundException * @throws common_Exception */ protected function compileTestMetadata(core_kernel_classes_Resource $resource) { /** @var ResourceJsonMetadataCompiler $jsonMetadataCompiler */ $jsonMetadataCompiler = $this->getServiceLocator()->get(ResourceJsonMetadataCompiler::SERVICE_ID); $metadataJson = $jsonMetadataCompiler->compile($resource); $this->getPrivateDirectory()->write(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_METADATA_FILENAME, json_encode($metadataJson)); } /** * Compile Adaptive Test Information. * * This method compiles all information required at runtime in terms of Adaptive Testing. * * @param \qtism\data\AssessmentTest $test */ protected function compileAdaptive(AssessmentTest $test) { $catService = $this->getServiceLocator()->get(CatService::SERVICE_ID); $compilationDataService = $this->getServiceLocator()->get(CompilationDataService::SERVICE_ID); $catSectionMap = []; $trail = []; foreach ($test->getTestParts() as $testPart) { foreach ($testPart->getAssessmentSections() as $assessmentSection) { array_push($trail, $assessmentSection); } } $traversed = []; while (count($trail) > 0) { $current = array_pop($trail); if (in_array($current, $traversed, true) === false) { // 1st pass. array_push($trail, $current); foreach ($current->getSectionParts() as $sectionPart) { if ($sectionPart instanceof ExtendedAssessmentSection) { array_push($trail, $sectionPart); } } array_push($traversed, $current); } else { // 2nd pass. $sectionParts = $current->getSectionParts(); $sectionIdentifier = $current->getIdentifier(); $catInfo = $catService->getAdaptiveAssessmentSectionInfo( $test, $this->getPrivateDirectory(), $this->getExtraPath(), $sectionIdentifier ); if ($catInfo !== false) { // QTI Adaptive Section detected. \common_Logger::d("QTI Adaptive Section with identifier '" . $current->getIdentifier() . "' found."); // Deal with AssessmentSection Compiling. $compilationDataService->writeCompilationData( $this->getPrivateDirectory(), "adaptive-assessment-section-${sectionIdentifier}", $current ); foreach ($sectionParts->getKeys() as $sectionPartIdentifier) { $sectionPart = $sectionParts[$sectionPartIdentifier]; if ($sectionPart instanceof ExtendedAssessmentItemRef) { $sectionPartHref = $sectionPart->getHref(); // Deal with AssessmentItemRef Compiling. $compilationDataService->writeCompilationData( $this->getPrivateDirectory(), "adaptive-assessment-item-ref-${sectionPartIdentifier}", $sectionPart ); unset($sectionParts[$sectionPartIdentifier]); } } if (count($sectionParts) === 0) { $placeholderIdentifier = "adaptive-placeholder-${sectionIdentifier}"; // Make the placeholder's href something predictable for later use... $placeholderHref = "x-tao-qti-adaptive://section/${sectionIdentifier}"; $placeholder = new ExtendedAssessmentItemRef($placeholderIdentifier, $placeholderHref); // Tag the item ref in order to make it recognizable as an adaptive placeholder. $placeholder->getCategories()[] = self::ADAPTIVE_PLACEHOLDER_CATEGORY; $sectionParts[] = $placeholder; \common_Logger::d("Adaptive AssessmentItemRef Placeholder '${placeholderIdentifier}' injected in AssessmentSection '${sectionIdentifier}'."); // Ask for section setup to the CAT Engine. $section = $catService->getEngine($catInfo['adaptiveEngineRef'])->setupSection($catInfo['adaptiveSectionIdentifier']); $catSectionMap[$catInfo['qtiSectionIdentifier']] = ['section' => $section, 'endpoint' => $catInfo['adaptiveEngineRef']]; } } } } // Write Adaptive Section Map for runtime usage. $this->getPrivateDirectory()->write(self::ADAPTIVE_SECTION_MAP_FILENAME, json_encode($catSectionMap)); } /** * Compile the $test meta-data into PHP source code for maximum performance. The file is * stored into PRIVATE_DIRECTORY/test-meta.php. * * @param AssessmentTest $test */ protected function compileMeta(AssessmentTest $test) { common_Logger::t("Compiling test metadata..."); $compiledDocDir = $this->getPrivateDirectory(); /** @var CompilationDataService $compilationDataService */ $compilationDataService = $this->getServiceLocator()->get(CompilationDataService::SERVICE_ID); $compilationDataService->writeCompilationMetadata($compiledDocDir, $test); } /** * Compile the test index into JSON file to improve performance of the map build. * The file is stored into PRIVATE_DIRECTORY/test-index.json. */ protected function compileIndex() { $compiledDocDir = $this->getPrivateDirectory(); /** @var $index QtiTestCompilerIndex */ $index = $this->getContext(); if ($index) { $compiledDocDir->write(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_INDEX, $index->serialize()); } } /** * Compile AssessmentItemRef Href Indexes * * This method indexes the value of $assessmentItemRef->href by $assessmentItemRef->identifier for later * usage at delivery time (for fast access). * * @param \qtism\data\AssessmentItemRef $assessmentItemRef */ protected function compileAssessmentItemRefHrefIndex(AssessmentItemRef $assessmentItemRef) { $compiledDocDir = $this->getPrivateDirectory(); $compiledDocDir->getFile(self::buildHrefIndexPath($assessmentItemRef->getIdentifier())) ->write($assessmentItemRef->getHref()); } /** * Get the list of mime types of files that are accepted to be put * into the public compilation directory. * * @return array */ protected static function getPublicMimeTypes() { return self::$publicMimeTypes; } /** * Build Href Index Path * * Builds the Href Index Path from given $identifier. * * @param string $identifier * @return string */ public static function buildHrefIndexPath($identifier) { return taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_HREF_INDEX_FILE_PREFIX . md5($identifier) . taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_HREF_INDEX_FILE_EXTENSION; } protected function addCompilationInfo($key, $info) { if (is_scalar($info)) { $this->compilationInfo[$key] = $info; } } public function getCompilatonInfo() { return $this->compilationInfo; } protected function getTaoQtiTestExtension() { return $this->getServiceLocator()->get(\common_ext_ExtensionsManager::SERVICE_ID)->getExtensionById('taoQtiTest'); } protected function buildCompilationInfo() { $this->addCompilationInfo('tao-version', TAO_VERSION); $this->addCompilationInfo('testqti-version', $this->getTaoQtiTestExtension()->getVersion()); $this->addCompilationInfo('compilation-data-service-implementation', get_class($this->getServiceLocator()->get(CompilationDataService::SERVICE_ID))); $this->getPrivateDirectory()->write( self::COMPILATION_INFO_FILENAME, json_encode($this->getCompilatonInfo()) ); } /** * Set whenever or not the compiler should use client test container * @param boolean $boolean */ public function setClientContainer($boolean) { $this->settingClientContainer = !!$boolean; } /** * Whenever or not we use the Client Test runner * @return boolean */ protected function useClientTestRunner() { return $this->settingClientContainer; } /** * Set whenever or not the compiler should scope rubric block css * @param boolean $boolean */ public function setCssScoping($boolean) { $this->settingCssScope = !!$boolean; } /** * Whenever or not we scope rubric block css * @return boolean */ protected function useCssScoping() { return $this->settingCssScope; } }