tao-test/app/taoQtiTest/models/classes/class.QtiTestCompiler.php

1101 lines
43 KiB
PHP

<?php
/**
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License
* as published by the Free Software Foundation; under version 2
* of the License (non-upgradable).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*
* Copyright (c) 2013-2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
*
*/
use oat\taoQtiTest\models\runner\RunnerService;
use qtism\runtime\rendering\markup\xhtml\XhtmlRenderingEngine;
use qtism\data\storage\xml\XmlStorageException;
use qtism\runtime\rendering\markup\MarkupPostRenderer;
use qtism\runtime\rendering\css\CssScoper;
use qtism\data\QtiComponentIterator;
use qtism\data\storage\xml\XmlDocument;
use qtism\data\storage\xml\XmlCompactDocument;
use qtism\data\AssessmentTest;
use qtism\data\ExtendedAssessmentSection;
use qtism\data\ExtendedAssessmentItemRef;
use qtism\data\AssessmentItemRef;
use qtism\data\content\RubricBlock;
use qtism\data\content\StylesheetCollection;
use qtism\common\utils\Url;
use oat\taoQtiItem\model\qti\Service;
use League\Flysystem\FileExistsException;
use oat\oatbox\filesystem\Directory;
use oat\taoQtiTest\models\TestCategoryRulesService;
use oat\taoQtiTest\models\QtiTestCompilerIndex;
use oat\taoQtiTest\models\cat\CatService;
use oat\taoQtiTest\models\CompilationDataService;
use oat\taoQtiItem\model\QtiJsonItemCompiler;
use oat\taoDelivery\model\container\delivery\DeliveryContainerRegistry;
use oat\taoDelivery\model\container\delivery\ContainerProvider;
use oat\tao\model\metadata\compiler\ResourceJsonMetadataCompiler;
/**
* A Test Compiler implementation that compiles a QTI Test and related QTI Items.
*
* @author Jérôme Bogaerts <jerome@taotesting.com>
* @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, '<?php echo $' . taoQtiTest_models_classes_QtiTestService::TEST_BASE_PATH_NAME . '; ?>', $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;
}
}