diff --git a/actions/class.TestRunner.php b/actions/class.TestRunner.php index 6733dbaa2a..22d5a3ad63 100644 --- a/actions/class.TestRunner.php +++ b/actions/class.TestRunner.php @@ -15,7 +15,7 @@ * 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 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); + * Copyright (c) 2013-2016 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT); * * */ @@ -29,12 +29,15 @@ use qtism\common\enums\BaseType; use qtism\common\enums\Cardinality; use qtism\common\datatypes\String as QtismString; +use qtism\common\datatypes\files\FileManager; use qtism\runtime\storage\binary\BinaryAssessmentTestSeeker; use qtism\runtime\storage\common\AbstractStorage; use qtism\data\SubmissionMode; use qtism\data\NavigationMode; use oat\taoQtiItem\helpers\QtiRunner; use oat\taoQtiTest\models\TestSessionMetaData; +use oat\taoQtiTest\models\StateStorageQtiFileManager; + /** * Runs a QTI Test. * @@ -73,6 +76,13 @@ class taoQtiTest_actions_TestRunner extends tao_actions_ServiceModule { * @var AbstractStorage */ private $storage = null; + + /** + * The service state storage of storage engine. + * + * @var FileManager + */ + private $serviceStateStorage = null; /** * The error that occured during the current request. @@ -221,6 +231,23 @@ protected function getTestMeta() { return $this->testMeta; } + /** + * Return the State storage + * @return StateStorageQtiFileManager + * @throws common_exception_Error + */ + protected function getStateStorageQtiFileManager() + { + if (!$this->serviceStateStorage) { + $this->serviceStateStorage = new StateStorageQtiFileManager( + $this->getServiceCallId(), + \common_session_SessionManager::getSession()->getUserUri() + ); + $this->serviceStateStorage->setServiceLocator($this->getServiceManager()); + } + return $this->serviceStateStorage; + } + /** * Print an error report into the response. * After you have called this method, you must prevent other actions to be processed and must close the response. @@ -257,12 +284,15 @@ protected function beforeAction($notifyError = true) { // Initialize storage and test session. $testResource = new core_kernel_classes_Resource($this->getRequestParameter('QtiTestDefinition')); - + $sessionManager = new taoQtiTest_helpers_SessionManager($resultServer, $testResource); $userUri = common_session_SessionManager::getSession()->getUserUri(); $seeker = new BinaryAssessmentTestSeeker($this->getTestDefinition()); - - $this->setStorage(new taoQtiTest_helpers_TestSessionStorage($sessionManager, $seeker, $userUri)); + + $storage = new taoQtiTest_helpers_TestSessionStorage($sessionManager, $seeker, $userUri); + + $storage->setFileManager($this->getStateStorageQtiFileManager()); + $this->setStorage($storage); $this->retrieveTestSession(); // @TODO: use some storage to get the potential reason of the state (close/suspended) @@ -340,7 +370,7 @@ protected function afterAction($withContext = true) { } common_Logger::i("Persisting QTI Assessment Test Session '${sessionId}'..."); - $this->getStorage()->persist($testSession); + $this->getStorage()->persist($testSession); } /** @@ -713,6 +743,7 @@ public function storeItemVariableSet() } $filler = new taoQtiCommon_helpers_PciVariableFiller($currentItem); + $filler->setFileManager($this->getStateStorageQtiFileManager()); if (is_array($jsonPayload)) { foreach ($jsonPayload as $id => $response) { @@ -860,6 +891,16 @@ protected function handleAssessmentTestSessionException(AssessmentTestSessionExc case AssessmentTestSessionException::ASSESSMENT_ITEM_DURATION_OVERFLOW: $this->onTimeout($e); break; + default: + $msg = "An unexpected error occured in the QTI Test Runner: \n"; + while ($e) { + $msg .= $e->getMessage(); + if (($e = $e->getPrevious()) !== null) { + $msg .= "\nCaused by:\n"; + } + } + common_Logger::e($msg); + break; } } -} \ No newline at end of file +} diff --git a/composer.json b/composer.json index 7353099d21..6cfdb21465 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ }, "minimum-stability": "dev", "require": { - "oat-sa/lib-tao-qti": "dev-master", + "oat-sa/lib-tao-qti": "dev-refactor/qtism-dependencies-to-feature-branch", "oat-sa/oatbox-extension-installer": "dev-master" }, "autoload": { diff --git a/helpers/class.TestSessionStorage.php b/helpers/class.TestSessionStorage.php index afa49c8880..7b97a66def 100644 --- a/helpers/class.TestSessionStorage.php +++ b/helpers/class.TestSessionStorage.php @@ -26,7 +26,8 @@ use qtism\runtime\storage\common\StorageException; use qtism\data\AssessmentTest; use qtism\runtime\tests\AssessmentTestSession; -use qtism\runtime\storage\binary\QtiBinaryStreamAccessFsFile; +use qtism\runtime\storage\binary\QtiBinaryStreamAccess; +use qtism\common\datatypes\files\FileManager; /** * A QtiSm AssessmentTestSession Storage Service implementation for TAO. @@ -52,7 +53,13 @@ class taoQtiTest_helpers_TestSessionStorage extends AbstractQtiBinaryStorage { * @var string */ private $userUri; - + + /** + * File manager to pass to QtiBinaryStreamAccess + * @var FileManager + */ + protected $fileManager; + /** * Create a new TestSessionStorage object. * @@ -159,8 +166,30 @@ public function exists($sessionId) { return $storageService->has($userUri, $sessionId); } - - protected function createBinaryStreamAccess(IStream $stream) { - return new QtiBinaryStreamAccessFsFile($stream); + + /** + * @throws StorageException + * @return FileManager + */ + public function getFileManager() + { + if (!isset($this->fileManager)) { + throw new StorageException('Missing file manager for test session storage.', StorageException::UNKNOWN); + } + return $this->fileManager; + } + + /** + * @param FileManager $fileManager + */ + public function setFileManager(FileManager $fileManager) + { + $this->fileManager = $fileManager; + } + + + protected function createBinaryStreamAccess(IStream $stream) + { + return new QtiBinaryStreamAccess($stream, $this->getFileManager()); } } \ No newline at end of file diff --git a/models/classes/StateStorageQtiFile.php b/models/classes/StateStorageQtiFile.php new file mode 100644 index 0000000000..023719d414 --- /dev/null +++ b/models/classes/StateStorageQtiFile.php @@ -0,0 +1,225 @@ +key = $key; + $this->mimeType = $mimeType; + $this->filename = $filename; + $this->data = $data; + } + + /** + * Get data + * @return string + */ + public function getData() + { + return $this->data; + } + + /** + * Get the mime type + * @return mixed + */ + public function getMimeType() + { + return $this->mimeType; + } + + /** + * Check if filename is set + * @return bool + */ + public function hasFilename() + { + return $this->getFilename() !== ''; + } + + /** + * Get filename + * @return mixed + */ + public function getFilename() + { + return $this->filename; + } + + /** + * Return the stream of file $data + * @todo use PSR7 stream, but not respect File::getStream signature + * @return Resource + */ + public function getStream() + { + if (empty($this->data)) { + return false; + } + + $temp = tmpfile(); + fwrite($temp, $this->getData()); + fseek($temp, 0); + + return $temp; + } + + /** + * Get identifier of the file e.q. $key + * @return string + */ + public function getIdentifier() + { + return $this->key; + } + + /** + * Compare two File by checking filename, mime type & content + * @param mixed $obj + * @return bool + */ + public function equals($obj) + { + if (!$obj instanceof File) { + return false; + } + + if ($this->getFilename() !== $obj->getFilename()) { + return false; + } + + if ($this->getMimeType() !== $obj->getMimeType()) { + return false; + } + + // We have to check the content of the file. + $myStream = $this->getStream(); + $objStream = $obj->getStream(); + + while (feof($myStream) === false && feof($objStream) === false) { + $myChunk = fread($myStream, self::CHUNK_SIZE); + $objChjunk = fread($objStream, self::CHUNK_SIZE); + + if ($myChunk !== $objChjunk) { + @fclose($myStream); + @fclose($objStream); + return false; + } + } + + @fclose($myStream); + @fclose($objStream); + + return true; + } + + /** + * Return base type for file e.q. 9 + * @return int + */ + public function getBaseType() + { + return BaseType::FILE; + } + + /** + * Return cardinality type for file e.q. 0 + * @return int + */ + public function getCardinality() + { + return Cardinality::SINGLE; + } + + /** + * Return filename + * @return string|void + */ + public function __toString() + { + return $this->getIdentifier(); + } + + /** + * Transform current file to binary content + * @return string + */ + public function toBinary() + { + // Filename + $len = strlen($this->filename); + $packedFilename = pack('S', $len) . $this->filename; + + // MIME type. + $len = strlen($this->mimeType); + $packedMimeType = pack('S', $len) . $this->mimeType; + + // Data + return $packedFilename . $packedMimeType . $this->data; + } +} \ No newline at end of file diff --git a/models/classes/StateStorageQtiFileManager.php b/models/classes/StateStorageQtiFileManager.php new file mode 100644 index 0000000000..f0e5ffe806 --- /dev/null +++ b/models/classes/StateStorageQtiFileManager.php @@ -0,0 +1,193 @@ +testId = $testId; + $this->userId = $userId; + } + + /** + * Get state storage, retrieve it if empty + * @return array|object|\tao_models_classes_service_StateStorage + */ + public function getStateStorage() + { + if (empty($this->storageService)) { + $this->storageService = $this->getServiceLocator()->get('tao/stateStorage'); + } + return $this->storageService; + } + + /** + * Set storage service + * @param $storageService + */ + public function setStateStorage($storageService) + { + $this->storageService = $storageService; + } + + /** + * Create a StateStorageQtiFile by storing data in key=>value persistence + * Compact metadata at the begining of data string + * @param $filename + * @param $mimeType + * @param $data + * @return StateStorageQtiFile + */ + protected function create($filename, $mimeType, $data) + { + $key = $this->generateUniqKey($this->testId); + + if ($filename=='') { + $filename = $key; + } + + $stateStorageFile = new StateStorageQtiFile($key, $mimeType, $filename, $data); + $content = $stateStorageFile->toBinary(); + + //State storage update + if (!$this->getStateStorage()->set($this->userId, $key, $content)) { + throw new \RuntimeException('Unable to store file in state storage system'); + } + return $stateStorageFile; + } + + /** + * Return a StateStorageQtiFile from content of file located at $path + * @todo Access file as stream + * @todo Check mime type? + * @param string $path + * @param string $mimeType + * @param string $filename + * @return StateStorageQtiFile + * @throws FileManagerException + */ + public function createFromFile($path, $mimeType, $filename='') + { + try { + if (!is_file($path)) { + throw new \RuntimeException('Unable to find source file at "' . $path . '".'); + } + + if (!is_readable($path)) { + throw new \RuntimeException('Source file "' . $path . '" found but not readable.'); + } + + $pathinfo = pathinfo($path); + $filename = ($filename=='') ? $pathinfo['filename'] . '.' . $pathinfo['extension'] : $filename; + $data = file_get_contents($path); + + return $this->create($filename, $mimeType, $data); + } catch (\RuntimeException $e) { + throw new FileManagerException('An error occured while creating a StateStorageQtiFile object', 0, $e); + } + } + + /** + * Return a StateStorageQtiFile from $data + * @param string $data + * @param string $mimeType + * @param string $filename + * @return StateStorageQtiFile + * @throws FileManagerException + */ + public function createFromData($data, $mimeType, $filename='') + { + try { + return $this->create($filename, $mimeType, $data); + } catch (\RuntimeException $e) { + throw new FileManagerException('An error occured while creating a StateStorageQtiFile object', 0, $e); + } + } + + /** + * Create a StateStorageQtiFile with identifier + * @todo delete reference into test state storage + * @param string $identifier + * @return StateStorageQtiFile + */ + public function retrieve($identifier) + { + return new StateStorageQtiFile($identifier); + } + + /** + * Delete file in key=>value storage + * @param File $file + * @return bool + */ + public function delete(File $file) + { + $key = $file->getIdentifier(); + if (!$this->getStateStorage()->del($this->userId, $key)) { + throw new \RuntimeException('Unable to delete file in state storage system'); + } + return true; + } + + /** + * Generate a key as identifier to track file in state storage + * @param string $prefix + * @return string + */ + protected function generateUniqKey($prefix='') + { + return uniqid($prefix, true); + } +} \ No newline at end of file