Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 363
0.00% covered (danger)
0.00%
0 / 53
CRAP
0.00% covered (danger)
0.00%
0 / 1
QtiRunnerServiceContext
0.00% covered (danger)
0.00%
0 / 363
0.00% covered (danger)
0.00%
0 / 53
11772
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 init
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initCompilationDirectory
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 initTestDefinition
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 initStorage
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 initTestSession
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 retrieveTestMeta
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 retrieveItemIndex
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 setTestSession
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getStorage
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getEventManager
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSessionManager
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getTestDefinition
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getCompilationDirectory
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getTestMeta
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getTestCompilationVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestDefinitionUri
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestCompilationUri
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestExecutionUri
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItemIndex
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getUserUri
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setUserUri
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItemIndexValue
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getCatEngine
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
12
 getTestSession
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getCatSession
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 persistCatSession
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 persistSeenCatItemIds
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
12
 getLastCatItemOutput
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 persistLastCatItemOutput
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getCatSection
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 isAdaptive
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 containsAdaptive
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 selectAdaptiveNextItem
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
42
 getCurrentAssessmentItemRef
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getPreviouslySeenCatItemIds
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getShadowTest
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getCurrentCatItemId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 persistCurrentCatItemId
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getItemPositionInRoute
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 getCurrentPosition
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 getCatAttempts
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 persistCatAttempts
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 canMoveBackward
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
30
 saveAdaptiveResults
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 storeResult
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
72
 convertCatVariables
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
72
 getItemUriFromRefId
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 isSyncingMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSyncingMode
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestTakerFromSessionOrRds
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 getSectionPauseService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCatService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2017-2023 (original work) Open Assessment Technologies SA.
19 */
20
21namespace oat\taoQtiTest\models\runner;
22
23use common_exception_InvalidArgumentType;
24use common_ext_ExtensionException;
25use common_ext_ExtensionsManager;
26use common_session_SessionManager;
27use core_kernel_classes_Resource;
28use oat\libCat\CatEngine;
29use oat\libCat\CatSection;
30use oat\libCat\CatSession;
31use oat\libCat\exception\CatEngineException;
32use oat\libCat\result\AbstractResult;
33use oat\libCat\result\ItemResult;
34use oat\libCat\result\ResultVariable;
35use oat\oatbox\event\EventManager;
36use oat\oatbox\service\ServiceNotFoundException;
37use oat\oatbox\user\User;
38use oat\tao\helpers\UserHelper;
39use oat\taoDelivery\model\execution\DeliveryServerService;
40use oat\taoQtiTest\helpers\TestSessionMemento;
41use oat\taoQtiTest\models\cat\CatEngineNotFoundException;
42use oat\taoQtiTest\models\CompilationDataService;
43use oat\taoQtiTest\models\event\QtiTestChangeEvent;
44use oat\taoQtiTest\models\QtiTestCompilerIndex;
45use oat\taoQtiTest\models\runner\session\TestSession;
46use oat\taoQtiTest\models\cat\CatService;
47use oat\taoQtiTest\models\ExtendedStateService;
48use oat\taoQtiTest\models\SectionPauseService;
49use oat\taoQtiTest\models\event\SelectAdaptiveNextItemEvent;
50use Psr\Container\ContainerInterface;
51use qtism\data\AssessmentTest;
52use qtism\data\AssessmentItemRef;
53use qtism\data\ExtendedAssessmentItemRef;
54use qtism\data\NavigationMode;
55use qtism\runtime\storage\binary\AbstractQtiBinaryStorage;
56use qtism\runtime\storage\binary\BinaryAssessmentTestSeeker;
57use qtism\runtime\storage\common\StorageException;
58use qtism\runtime\tests\AssessmentTestSession;
59use qtism\runtime\tests\AssessmentTestSessionException;
60use qtism\runtime\tests\RouteItem;
61use tao_models_classes_service_FileStorage;
62use tao_models_classes_service_StorageDirectory;
63use taoQtiTest_helpers_SessionManager;
64use taoQtiTest_helpers_TestCompilerUtils;
65use taoQtiTest_helpers_TestRunnerUtils;
66use taoQtiTest_models_classes_QtiTestService;
67use taoResultServer_models_classes_Variable;
68use common_Exception;
69use common_exception_Error;
70use common_exception_NotImplemented;
71use common_Logger;
72use Exception;
73
74/**
75 * Class QtiRunnerServiceContext
76 *
77 * Defines a container to store and to share runner service context of the QTI implementation
78 *
79 * @package oat\taoQtiTest\models
80 *
81 * @author Jean-Sébastien Conan <jean-sebastien.conan@vesperiagroup.com>
82 */
83class QtiRunnerServiceContext extends RunnerServiceContext
84{
85    /**
86     * The session storage
87     */
88    protected ?AbstractQtiBinaryStorage $storage = null;
89
90    protected ?taoQtiTest_helpers_SessionManager $sessionManager = null;
91
92    /**
93     * The assessment test definition
94     */
95    protected ?AssessmentTest $testDefinition = null;
96
97    /**
98     * The path of the compilation directory.
99     *
100     * @var tao_models_classes_service_StorageDirectory[]
101     */
102    protected ?array $compilationDirectory = null;
103
104    /**
105     * The metadata about the test definition being executed.
106     */
107    private ?array $testMeta = null;
108
109    /**
110     * The index of compiled items.
111     */
112    private QtiTestCompilerIndex $itemIndex;
113
114    /**
115     * The URI of the assessment test
116     */
117    protected string $testDefinitionUri;
118
119    /**
120     * The URI of the compiled delivery
121     */
122    protected string $testCompilationUri;
123
124    /**
125     * The URI of the delivery execution
126     */
127    protected string $testExecutionUri;
128
129    /**
130     * Whether we are in synchronization mode
131     */
132    private bool $syncingMode = false;
133
134    private ?string $userUri;
135
136    private ?ContainerInterface $container = null;
137
138    /**
139     * QtiRunnerServiceContext constructor.
140     *
141     * @param string $testDefinitionUri
142     * @param string $testCompilationUri
143     * @param string $testExecutionUri
144     */
145    public function __construct(
146        string $testDefinitionUri,
147        string $testCompilationUri,
148        string $testExecutionUri
149    ) {
150        $this->testDefinitionUri = $testDefinitionUri;
151        $this->testCompilationUri = $testCompilationUri;
152        $this->testExecutionUri = $testExecutionUri;
153    }
154
155    /**
156     * Starts the context
157     */
158    public function init()
159    {
160        $this->retrieveItemIndex();
161    }
162
163    /**
164     * Extracts the path of the compilation directory
165     */
166    protected function initCompilationDirectory()
167    {
168        $fileStorage = tao_models_classes_service_FileStorage::singleton();
169        $directoryIds = explode('|', $this->getTestCompilationUri());
170        $directories = [
171            'private' => $fileStorage->getDirectoryById($directoryIds[0]),
172            'public' => $fileStorage->getDirectoryById($directoryIds[1])
173        ];
174
175        $this->compilationDirectory = $directories;
176    }
177
178    /**
179     * Loads the test definition
180     */
181    protected function initTestDefinition()
182    {
183        $this->testDefinition = \taoQtiTest_helpers_Utils::getTestDefinition($this->getTestCompilationUri());
184    }
185
186    /**
187     * Loads the storage
188     *
189     * @throws ServiceNotFoundException
190     * @throws common_Exception
191     * @throws common_exception_Error
192     * @throws common_ext_ExtensionException
193     */
194    protected function initStorage()
195    {
196        /** @var DeliveryServerService $deliveryServerService */
197        $deliveryServerService = $this->getServiceManager()->get(DeliveryServerService::SERVICE_ID);
198        $resultStore = $deliveryServerService->getResultStoreWrapper($this->getTestExecutionUri());
199        $testResource = new core_kernel_classes_Resource($this->getTestDefinitionUri());
200        $sessionManager = new taoQtiTest_helpers_SessionManager($resultStore, $testResource);
201
202        $seeker = new BinaryAssessmentTestSeeker($this->getTestDefinition());
203        $userUri = $this->getUserUri();
204
205        $config = common_ext_ExtensionsManager::singleton()->getExtensionById('taoQtiTest')->getConfig('testRunner');
206        $storageClassName = $config['test-session-storage'];
207        $this->storage = new $storageClassName($sessionManager, $seeker, $userUri);
208        $this->sessionManager = $sessionManager;
209    }
210
211    /**
212     * Loads the test session
213     *
214     * @throws StorageException
215     * @throws common_exception_Error
216     * @throws common_exception_InvalidArgumentType
217     * @throws common_ext_ExtensionException
218     */
219    protected function initTestSession()
220    {
221        $storage = $this->getStorage();
222        $sessionId = $this->getTestExecutionUri();
223
224        if ($storage->exists($sessionId) === false) {
225            common_Logger::d("Instantiating QTI Assessment Test Session");
226            $this->setTestSession($storage->instantiate($this->getTestDefinition(), $sessionId));
227
228            $testTaker = $this->getTestTakerFromSessionOrRds();
229            taoQtiTest_helpers_TestRunnerUtils::setInitialOutcomes($this->getTestSession(), $testTaker);
230        } else {
231            common_Logger::d("Retrieving QTI Assessment Test Session '${sessionId}'...");
232            $this->setTestSession($storage->retrieve($this->getTestDefinition(), $sessionId));
233        }
234
235        taoQtiTest_helpers_TestRunnerUtils::preserveOutcomes($this->getTestSession());
236    }
237
238    /**
239     * @deprecated
240     */
241    protected function retrieveTestMeta()
242    {
243    }
244
245    /**
246     * Retrieves the index of compiled items.
247     */
248    protected function retrieveItemIndex()
249    {
250        $this->itemIndex = new QtiTestCompilerIndex();
251        try {
252            $directories = $this->getCompilationDirectory();
253            $data = $directories['private']->read(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_INDEX);
254            if ($data) {
255                $this->itemIndex->unserialize($data);
256            }
257        } catch (Exception $e) {
258            common_Logger::d('Ignoring file not found exception for Items Index');
259        }
260    }
261
262    /**
263     * Sets the test session
264     * @param mixed $testSession
265     * @throws common_exception_InvalidArgumentType
266     */
267    public function setTestSession($testSession)
268    {
269        if ($testSession instanceof TestSession) {
270            parent::setTestSession($testSession);
271        } else {
272            throw new common_exception_InvalidArgumentType(
273                'QtiRunnerServiceContext',
274                'setTestSession',
275                0,
276                TestSession::class,
277                $testSession
278            );
279        }
280    }
281
282    /**
283     * Gets the session storage
284     * @return AbstractQtiBinaryStorage
285     * @throws common_exception_Error
286     * @throws common_ext_ExtensionException
287     */
288    public function getStorage()
289    {
290        if (!$this->storage) {
291            $this->initStorage();
292        }
293        return $this->storage;
294    }
295
296    /**
297     * @return EventManager
298     * @throws \Zend\ServiceManager\Exception\ServiceNotFoundException
299     */
300    protected function getEventManager()
301    {
302        return $this->getServiceLocator()->get(EventManager::SERVICE_ID);
303    }
304
305    /**
306     * @return taoQtiTest_helpers_SessionManager
307     * @throws common_exception_Error
308     * @throws common_ext_ExtensionException
309     */
310    public function getSessionManager()
311    {
312        if (null === $this->sessionManager) {
313            $this->initStorage();
314        }
315        return $this->sessionManager;
316    }
317
318    /**
319     * Gets the assessment test definition
320     * @return AssessmentTest
321     */
322    public function getTestDefinition()
323    {
324        if (null === $this->testDefinition) {
325            $this->initTestDefinition();
326        }
327        return $this->testDefinition;
328    }
329
330    /**
331     * Gets the path of the compilation directory
332     * @return tao_models_classes_service_StorageDirectory[]
333     */
334    public function getCompilationDirectory()
335    {
336        if (null === $this->compilationDirectory) {
337            $this->initCompilationDirectory();
338        }
339        return $this->compilationDirectory;
340    }
341
342    /**
343     * Gets the meta data about the test definition being executed.
344     * @return array
345     * @throws common_Exception
346     */
347    public function getTestMeta()
348    {
349        if (!isset($this->testMeta)) {
350            $directories = $this->getCompilationDirectory();
351
352            /** @var CompilationDataService $compilationDataService */
353            $compilationDataService = $this->getServiceLocator()->get(CompilationDataService::SERVICE_ID);
354            $this->testMeta = $compilationDataService->readCompilationMetadata($directories['private']);
355        }
356
357        return $this->testMeta;
358    }
359
360    public function getTestCompilationVersion(): int
361    {
362        return $this->getTestMeta()[taoQtiTest_helpers_TestCompilerUtils::COMPILATION_VERSION] ?? 0;
363    }
364
365    /**
366     * Gets the URI of the assessment test
367     * @return string
368     */
369    public function getTestDefinitionUri()
370    {
371        return $this->testDefinitionUri;
372    }
373
374    /**
375     * Gets the URI of the compiled delivery
376     * @return string
377     */
378    public function getTestCompilationUri()
379    {
380        return $this->testCompilationUri;
381    }
382
383    /**
384     * Gets the URI of the delivery execution
385     * @return string
386     */
387    public function getTestExecutionUri()
388    {
389        return $this->testExecutionUri;
390    }
391
392    /**
393     * Gets info from item index
394     * @param string $id
395     * @return mixed
396     * @throws common_exception_Error
397     */
398    public function getItemIndex($id)
399    {
400        return $this->itemIndex->getItem($id, common_session_SessionManager::getSession()->getInterfaceLanguage());
401    }
402
403
404    /**
405     * @return string
406     * @throws common_exception_Error
407     */
408    public function getUserUri()
409    {
410        if ($this->userUri === null) {
411            $this->userUri = common_session_SessionManager::getSession()->getUserUri();
412        }
413        return $this->userUri;
414    }
415
416    /**
417     * @param string $userUri
418     */
419    public function setUserUri($userUri)
420    {
421        $this->userUri = $userUri;
422    }
423
424    /**
425     * Gets a particular value from item index
426     * @param string $id
427     * @param string $name
428     * @return mixed
429     * @throws common_exception_Error
430     */
431    public function getItemIndexValue($id, $name)
432    {
433        return $this->itemIndex->getItemValue(
434            $id,
435            common_session_SessionManager::getSession()->getInterfaceLanguage(),
436            $name
437        );
438    }
439
440    /**
441     * Get Cat Engine Implementation
442     *
443     * Get the currently configured Cat Engine implementation.
444     *
445     * @param RouteItem|null $routeItem
446     * @return CatEngine
447     *
448     * @throws ServiceNotFoundException
449     * @throws CatEngineNotFoundException
450     * @throws common_exception_Error
451     */
452    public function getCatEngine(RouteItem $routeItem = null)
453    {
454        $compiledDirectory = $this->getCompilationDirectory()['private'];
455        $adaptiveSectionMap = $this->getCatService()->getAdaptiveSectionMap($compiledDirectory);
456        $routeItem = $routeItem ? $routeItem : $this->getTestSession()->getRoute()->current();
457
458        $sectionId = $routeItem->getAssessmentSection()->getIdentifier();
459        $catEngine = false;
460
461        if (isset($adaptiveSectionMap[$sectionId])) {
462            $catEngine = $this->getCatService()->getEngine(
463                $adaptiveSectionMap[$sectionId]['endpoint']
464            );
465        }
466
467        return $catEngine;
468    }
469
470    /**
471     * @return AssessmentTestSession
472     * @throws common_exception_Error
473     */
474    public function getTestSession()
475    {
476        if (!$this->testSession) {
477            $this->initTestSession();
478        }
479        return parent::getTestSession();
480    }
481
482
483    /**
484     * Get the current CAT Session Object.
485     *
486     * @param RouteItem|null $routeItem
487     * @return CatSession|false
488     *
489     * @throws ServiceNotFoundException
490     * @throws common_exception_Error
491     */
492    public function getCatSession(RouteItem $routeItem = null)
493    {
494        return $this->getCatService()->getCatSession(
495            $this->getTestSession(),
496            $this->getCompilationDirectory()['private'],
497            $routeItem
498        );
499    }
500
501    /**
502     * Persist the CAT Session Data.
503     *
504     * Persist the current CAT Session Data in storage.
505     *
506     * @param string $catSession JSON encoded CAT Session data.
507     * @param RouteItem|null $routeItem
508     * @return mixed
509     *
510     * @throws ServiceNotFoundException
511     * @throws common_exception_Error
512     */
513    public function persistCatSession($catSession, RouteItem $routeItem = null)
514    {
515        return $this->getCatService()->persistCatSession(
516            $catSession,
517            $this->getTestSession(),
518            $this->getCompilationDirectory()['private'],
519            $routeItem
520        );
521    }
522
523    /**
524     * Persist seen CAT Item identifiers.
525     *
526     * @param string $seenCatItemId
527     *
528     * @throws ServiceNotFoundException
529     * @throws common_exception_Error
530     */
531    public function persistSeenCatItemIds($seenCatItemId)
532    {
533        $sessionId = $this->getTestSession()->getSessionId();
534        $items = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue(
535            $sessionId,
536            $this->getCatSection()->getSectionId(),
537            'cat-seen-item-ids'
538        );
539
540        if (!$items) {
541            $items = [];
542        } else {
543            $items = json_decode($items);
544        }
545
546        if (!in_array($seenCatItemId, $items)) {
547            $items[] = $seenCatItemId;
548        }
549
550        $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue(
551            $sessionId,
552            $this->getCatSection()->getSectionId(),
553            'cat-seen-item-ids',
554            json_encode($items)
555        );
556    }
557
558    /**
559     * Get Last CAT Item Output.
560     *
561     * Get the last CAT Item Result from memory.
562     */
563    public function getLastCatItemOutput()
564    {
565        $sessionId = $this->getTestSession()->getSessionId();
566
567        $itemOutput = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue(
568            $sessionId,
569            $this->getCatSection()->getSectionId(),
570            'cat-item-output'
571        );
572
573        $output = [];
574
575        if (!is_null($itemOutput)) {
576            $rawData = json_decode($itemOutput, true);
577
578            foreach ($rawData as $result) {
579                /** @var ItemResult $itemResult */
580                $itemResult = ItemResult::restore($result);
581                $output[$itemResult->getItemRefId()] = $itemResult;
582            }
583        }
584
585        return $output;
586    }
587
588    /**
589     * Persist CAT Item Output.
590     *
591     * Persist the last CAT Item Result in memory.
592     *
593     * @throws common_exception_Error
594     * @throws ServiceNotFoundException
595     */
596    public function persistLastCatItemOutput(array $lastCatItemOutput)
597    {
598        $sessionId = $this->getTestSession()->getSessionId();
599
600        $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue(
601            $sessionId,
602            $this->getCatSection()->getSectionId(),
603            'cat-item-output',
604            json_encode($lastCatItemOutput)
605        );
606    }
607
608    /**
609     * Get Current CAT Section.
610     *
611     * Returns the current CatSection object. In case of the current Assessment Section is not adaptive, the method
612     * returns the boolean false value.
613     *
614     * @param RouteItem|null $routeItem
615     * @return CatSection|boolean
616     *
617     * @throws ServiceNotFoundException
618     * @throws common_exception_Error
619     */
620    public function getCatSection(RouteItem $routeItem = null)
621    {
622        return $this->getCatService()->getCatSection(
623            $this->getTestSession(),
624            $this->getCompilationDirectory()['private'],
625            $routeItem
626        );
627    }
628
629    /**
630     * Is the Assessment Test Session Context Adaptive.
631     *
632     * Determines whether the current Assessment Test Session is in an adaptive context.
633     *
634     * @param ?AssessmentItemRef $currentAssessmentItemRef An AssessmentItemRef object to be considered as
635     *                                                    the current assessmentItemRef.
636     * @return boolean
637     *
638     * @throws common_exception_Error
639     * @throws ServiceNotFoundException
640     */
641    public function isAdaptive(AssessmentItemRef $currentAssessmentItemRef = null)
642    {
643        return $this->getCatService()->isAdaptive(
644            $this->getTestSession(),
645            $currentAssessmentItemRef
646        );
647    }
648
649    /**
650     * Contains Adaptive Content.
651     *
652     * Whether the current Assessment Test Session has some adaptive contents.
653     *
654     * @return boolean
655     *
656     * @throws ServiceNotFoundException
657     */
658    public function containsAdaptive()
659    {
660        $adaptiveSectionMap = $this->getCatService()->getAdaptiveSectionMap(
661            $this->getCompilationDirectory()['private']
662        );
663
664        return !empty($adaptiveSectionMap);
665    }
666
667    /**
668     * Select the next Adaptive Item and store the retrieved results from CAT engine
669     *
670     * Ask the CAT Engine for the Next Item to be presented to the candidate, depending on the last
671     * CAT Item ID and last CAT Item Output currently stored.
672     *
673     * This method returns a CAT Item ID in case of the CAT Engine returned one. Otherwise, it returns
674     * null meaning that there is no CAT Item to be presented.
675     *
676     * @return mixed|null
677     *
678     * @throws common_Exception
679     * @throws ServiceNotFoundException
680     */
681    public function selectAdaptiveNextItem()
682    {
683        $lastItemId = $this->getCurrentCatItemId();
684        $lastOutput = $this->getLastCatItemOutput();
685        $catSession = $this->getCatSession();
686
687        $preSelection = $catSession->getTestMap();
688
689        try {
690            if (!$this->syncingMode) {
691                $selection = $catSession->getTestMap(array_values($lastOutput));
692
693                if (!$this->saveAdaptiveResults($catSession)) {
694                    common_Logger::w('Unable to save CatService results.');
695                }
696                $isShadowItem = false;
697            } else {
698                $selection = $catSession->getTestMap();
699                $isShadowItem = true;
700            }
701        } catch (CatEngineException $e) {
702            common_Logger::e('Error during CatEngine processing. ' . $e->getMessage());
703            $selection = $catSession->getTestMap();
704            $isShadowItem = true;
705        }
706
707        $event = new SelectAdaptiveNextItemEvent(
708            $this->getTestSession(),
709            $lastItemId,
710            $preSelection,
711            $selection,
712            $isShadowItem
713        );
714        $this->getServiceManager()->get(EventManager::SERVICE_ID)->trigger($event);
715
716        $this->persistCatSession($catSession);
717        if (is_array($selection) && count($selection) > 0) {
718            common_Logger::d("New CAT item selection is '" . implode(', ', $selection) . "'.");
719            return $selection[0];
720        } else {
721            common_Logger::d('No new CAT item selection.');
722            return null;
723        }
724    }
725
726    /**
727     * Get Current AssessmentItemRef object.
728     *
729     * This method returns the current AssessmentItemRef object depending on the test $context.
730     *
731     * @return ExtendedAssessmentItemRef|false
732     *
733     * @throws ServiceNotFoundException
734     * @throws common_exception_Error
735     */
736    public function getCurrentAssessmentItemRef()
737    {
738        if ($this->isAdaptive()) {
739            return $this->getCatService()->getAssessmentItemRefByIdentifier(
740                $this->getCompilationDirectory()['private'],
741                $this->getCurrentCatItemId()
742            );
743        } else {
744            return $this->getTestSession()->getCurrentAssessmentItemRef();
745        }
746    }
747
748    /**
749     * @return array
750     *
751     * @throws ServiceNotFoundException
752     * @throws common_exception_Error
753     */
754    public function getPreviouslySeenCatItemIds(RouteItem $routeItem = null)
755    {
756        return $this->getCatService()->getPreviouslySeenCatItemIds(
757            $this->getTestSession(),
758            $this->getCompilationDirectory()['private'],
759            $routeItem
760        );
761    }
762
763
764    /**
765     * @return array
766     *
767     * @throws ServiceNotFoundException
768     * @throws common_exception_Error
769     */
770    public function getShadowTest(RouteItem $routeItem = null)
771    {
772        return $this->getCatService()->getShadowTest(
773            $this->getTestSession(),
774            $this->getCompilationDirectory()['private'],
775            $routeItem
776        );
777    }
778
779    /**
780     * @return mixed
781     *
782     * @throws ServiceNotFoundException
783     * @throws common_exception_Error
784     */
785    public function getCurrentCatItemId(RouteItem $routeItem = null)
786    {
787        return $this->getCatService()->getCurrentCatItemId(
788            $this->getTestSession(),
789            $this->getCompilationDirectory()['private'],
790            $routeItem
791        );
792    }
793
794    /**
795     * @return void
796     *
797     * @throws ServiceNotFoundException
798     * @throws common_exception_Error
799     */
800    public function persistCurrentCatItemId($catItemId)
801    {
802        $session = $this->getTestSession();
803        $sessionId = $session->getSessionId();
804        $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue(
805            $sessionId,
806            $this->getCatSection()->getSectionId(),
807            'current-cat-item-id',
808            $catItemId
809        );
810
811        $event = new QtiTestChangeEvent($session, new TestSessionMemento($session));
812        $this->getServiceManager()->propagate($event);
813        $this->getEventManager()->trigger($event);
814    }
815
816    /**
817     * @return int
818     *
819     * @throws ServiceNotFoundException
820     * @throws common_exception_Error
821     */
822    public function getItemPositionInRoute($refId, &$catItemId = '')
823    {
824        $route = $this->getTestSession()->getRoute();
825        $routeCount = $route->count();
826
827        $i = 0;
828        $j = 0;
829
830        while ($i < $routeCount) {
831            $routeItem = $route->getRouteItemAt($i);
832
833            if ($this->isAdaptive($routeItem->getAssessmentItemRef())) {
834                $shadow = $this->getShadowTest($routeItem);
835
836                for ($k = 0; $k < count($shadow); $k++) {
837                    if ($j == $refId) {
838                        $catItemId = $shadow[$k];
839                        break 2;
840                    }
841
842                    $j++;
843                }
844            } else {
845                if ($j == $refId) {
846                    break;
847                }
848
849                $j++;
850            }
851
852            $i++;
853        }
854
855        return $i;
856    }
857
858    /**
859     * Get Real Current Position.
860     *
861     * This method returns the real position of the test taker within
862     * the item flow, by considering CAT sections.
863     *
864     * @return integer A zero-based index.
865     *
866     * @throws common_exception_Error
867     * @throws ServiceNotFoundException
868     */
869    public function getCurrentPosition()
870    {
871        $route = $this->getTestSession()->getRoute();
872        $routeCount = $route->count();
873        $routeItemPosition = $route->getPosition();
874        $currentRouteItem = $route->getRouteItemAt($routeItemPosition);
875
876        $finalPosition = 0;
877
878        for ($i = 0; $i < $routeCount; $i++) {
879            $routeItem = $route->getRouteItemAt($i);
880
881            if ($routeItem !== $currentRouteItem) {
882                if (!$this->isAdaptive($routeItem->getAssessmentItemRef())) {
883                    $finalPosition++;
884                } else {
885                    $finalPosition += count($this->getShadowTest($routeItem));
886                }
887            } else {
888                if ($this->isAdaptive($routeItem->getAssessmentItemRef())) {
889                    $finalPosition += array_search(
890                        $this->getCurrentCatItemId($routeItem),
891                        $this->getShadowTest($routeItem)
892                    );
893                }
894
895                break;
896            }
897        }
898
899        return $finalPosition;
900    }
901
902    /**
903     * @return int|mixed
904     *
905     * @throws ServiceNotFoundException
906     * @throws common_exception_Error
907     */
908    public function getCatAttempts($identifier, RouteItem $routeItem = null)
909    {
910        return $this->getCatService()->getCatAttempts(
911            $this->getTestSession(),
912            $this->getCompilationDirectory()['private'],
913            $identifier,
914            $routeItem
915        );
916    }
917
918    /**
919     * @return void
920     *
921     * @throws ServiceNotFoundException
922     * @throws common_exception_Error
923     */
924    public function persistCatAttempts($identifier, $attempts)
925    {
926        $sessionId = $this->getTestSession()->getSessionId();
927        $sectionId = $this->getCatSection()->getSectionId();
928
929        $catAttempts = $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->getCatValue(
930            $sessionId,
931            $sectionId,
932            'cat-attempts'
933        );
934
935        $catAttempts = ($catAttempts) ? $catAttempts : [];
936        $catAttempts[$identifier] = $attempts;
937
938        $this->getServiceManager()->get(ExtendedStateService::SERVICE_ID)->setCatValue(
939            $sessionId,
940            $sectionId,
941            'cat-attempts',
942            $catAttempts
943        );
944    }
945
946    /**
947     * Can Move Backward
948     *
949     * Whether the Test Taker is able to navigate backward.
950     * This implementation takes the CAT sections into consideration.
951     *
952     * @return boolean
953     * @throws AssessmentTestSessionException
954     *
955     * @throws common_exception_Error
956     * @throws ServiceNotFoundException
957     */
958    public function canMoveBackward()
959    {
960        $moveBack = false;
961        $session = $this->getTestSession();
962        if ($this->isAdaptive()) {
963            $positionInCatSession = array_search(
964                $this->getCurrentCatItemId(),
965                $this->getShadowTest()
966            );
967
968            if ($positionInCatSession === 0) {
969                // First item in cat section.
970                if ($session->getRoute()->getPosition() !== 0) {
971                    $testPart = $session->getPreviousRouteItem()->getTestPart();
972                    $moveBack = $testPart->getNavigationMode() === NavigationMode::NONLINEAR;
973                }
974            } else {
975                $testPart = $session->getRoute()->current()->getTestPart();
976                $moveBack = $testPart->getNavigationMode() === NavigationMode::NONLINEAR;
977            }
978        } else {
979            $moveBack = $session->canMoveBackward();
980
981            // Check also if the sectionPause prevents you from moving backward
982            if ($moveBack) {
983                $moveBack = $this->getSectionPauseService()->canMoveBackward($session);
984            }
985        }
986
987        return $moveBack;
988    }
989
990    /**
991     * Save the Cat service result for tests and items
992     *
993     * @param CatSession $catSession
994     * @return bool
995     *
996     * @throws ServiceNotFoundException
997     */
998    protected function saveAdaptiveResults(CatSession $catSession)
999    {
1000        $testResult = $catSession->getTestResult();
1001        $testResult = empty($testResult) ? [] : [$testResult];
1002        return $this->storeResult(array_merge($testResult, $catSession->getItemResults()));
1003    }
1004
1005    /**
1006     * Store a Cat Result variable
1007     *
1008     * The result has to be an ItemResult and TestResult to embed CAT variables
1009     * After converted them to taoResultServer variables
1010     * Use the runner service to store the variables
1011     *
1012     * @param AbstractResult[] $results
1013     * @return bool
1014     * @throws ServiceNotFoundException
1015     */
1016    protected function storeResult(array $results)
1017    {
1018        /** @var QtiRunnerService $runnerService */
1019        $runnerService = $this->getServiceLocator()->get(QtiRunnerService::SERVICE_ID);
1020
1021        $success = true;
1022        try {
1023            foreach ($results as $result) {
1024                if (!$result instanceof AbstractResult) {
1025                    throw new common_Exception(__FUNCTION__ . ' requires a CAT result to store it.');
1026                }
1027
1028                $variables = $this->convertCatVariables($result->getVariables());
1029                if (empty($variables)) {
1030                    common_Logger::t('No Cat result variables to store.');
1031                    continue;
1032                }
1033
1034                if ($result instanceof ItemResult) {
1035                    $itemId = $result->getItemRefId();
1036                    $itemUri = $this->getItemUriFromRefId($itemId);
1037                } else {
1038                    $itemUri = $itemId = null;
1039                    $sectionId = $this
1040                        ->getTestSession()
1041                        ->getRoute()
1042                        ->current()
1043                        ->getAssessmentSection()
1044                        ->getIdentifier();
1045                    foreach ($variables as $variable) {
1046                        /** @var taoResultServer_models_classes_Variable $variable */
1047                        $variable->setIdentifier($sectionId . '-' . $variable->getIdentifier());
1048                    }
1049                }
1050
1051                if (!$runnerService->storeVariables($this, $itemUri, $variables, $itemId)) {
1052                    $success = false;
1053                }
1054            }
1055        } catch (Exception $e) {
1056            common_Logger::w('An error has occurred during CAT result storing: ' . $e->getMessage());
1057            $success = false;
1058        }
1059
1060        return $success;
1061    }
1062
1063    /**
1064     * Convert CAT variables to taoResultServer variables
1065     *
1066     * Following the variable type, use the Runner service to get the appropriate variable
1067     * The method manage the trace, response and outcome variable
1068     *
1069     * @param array $variables
1070     * @return array
1071     * @throws common_exception_NotImplemented If variable type is not managed
1072     */
1073    protected function convertCatVariables(array $variables)
1074    {
1075        /** @var QtiRunnerService $runnerService */
1076        $runnerService = $this->getServiceLocator()->get(QtiRunnerService::SERVICE_ID);
1077        $convertedVariables = [];
1078
1079        foreach ($variables as $variable) {
1080            switch ($variable->getVariableType()) {
1081                case ResultVariable::TRACE_VARIABLE:
1082                    $getVariableMethod = 'getTraceVariable';
1083                    break;
1084                case ResultVariable::RESPONSE_VARIABLE:
1085                    $getVariableMethod = 'getResponseVariable';
1086                    break;
1087                case ResultVariable::OUTCOME_VARIABLE:
1088                    $getVariableMethod = 'getOutcomeVariable';
1089                    break;
1090                case ResultVariable::TEMPLATE_VARIABLE:
1091                default:
1092                    $getVariableMethod = null;
1093                    break;
1094            }
1095
1096            if (is_null($getVariableMethod)) {
1097                common_Logger::w(
1098                    'Variable of type ' . $variable->getVariableType() . ' is not implemented in ' . __METHOD__
1099                );
1100                throw new common_exception_NotImplemented();
1101            }
1102
1103            $convertedVariables[] = call_user_func_array(
1104                [$runnerService, $getVariableMethod],
1105                [$variable->getId(), $variable->getValue()]
1106            );
1107        }
1108
1109        return $convertedVariables;
1110    }
1111
1112    /**
1113     * Get item uri associated to the given $itemId.
1114     *
1115     * @return string The uri
1116     * @throws ServiceNotFoundException
1117     */
1118    protected function getItemUriFromRefId($itemId)
1119    {
1120        $ref = $this->getCatService()->getAssessmentItemRefByIdentifier(
1121            $this->getCompilationDirectory()['private'],
1122            $itemId
1123        );
1124        return explode('|', $ref->getHref())[0];
1125    }
1126
1127    /**
1128     * Are we in a synchronization mode
1129     * @return bool
1130     */
1131    public function isSyncingMode()
1132    {
1133        return $this->syncingMode;
1134    }
1135
1136    /**
1137     * Set/Unset the synchronization mode
1138     * @param bool $syncing
1139     */
1140    public function setSyncingMode($syncing)
1141    {
1142        $this->syncingMode = (bool) $syncing;
1143    }
1144
1145    /**
1146     * @return User
1147     * @throws common_exception_Error
1148     */
1149    private function getTestTakerFromSessionOrRds()
1150    {
1151        try {
1152            $session = common_session_SessionManager::getSession();
1153        } catch (common_exception_Error $exception) {
1154            $session = null;
1155            common_Logger::w($exception->getMessage());
1156        }
1157
1158        if ($session == null || $session->getUser() == null) {
1159            $testTaker = UserHelper::getUser($this->getUserUri());
1160        } else {
1161            $testTaker = $session->getUser();
1162        }
1163
1164        return $testTaker;
1165    }
1166
1167    /**
1168     * @throws ServiceNotFoundException
1169     */
1170    private function getSectionPauseService(): SectionPauseService
1171    {
1172        return $this->getServiceManager()->get(SectionPauseService::SERVICE_ID);
1173    }
1174
1175    /**
1176     * @throws ServiceNotFoundException
1177     */
1178    private function getCatService(): CatService
1179    {
1180        return $this->getServiceManager()->get(CatService::SERVICE_ID);
1181    }
1182}