Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
23.75% covered (danger)
23.75%
162 / 682
14.49% covered (danger)
14.49%
10 / 69
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResultsService
23.75% covered (danger)
23.75%
162 / 682
14.49% covered (danger)
14.49%
10 / 69
26200.98
0.00% covered (danger)
0.00%
0 / 1
 getCache
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getCacheKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContainerCacheKey
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setCacheValue
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
20
 deleteCacheFor
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
56
 setContainerCacheValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContainerCacheValue
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRootClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setImplementation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getImplementation
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getVariables
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 getVariablesFromObjectResult
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 getDelivery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeliveryItemType
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getItemResultsFromDeliveryResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getTestsFromDeliveryResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItemFromItemResult
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
12
 getVariableFromTest
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getVariableCandidateResponse
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVariableBaseType
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 calculateResponseStatistics
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
90
 getItemInfos
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
 getStructuredVariables
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 structureItemVariables
0.00% covered (danger)
0.00%
0 / 38
0.00% covered (danger)
0.00%
0 / 1
90
 splitByItemAndAttempt
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
56
 splitByItem
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 splitByAttempt
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItemResponseSplitter
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 filterStructuredVariables
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 getItemVariableDataStatsFromDeliveryResult
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
90
 getItemVariableDataFromDeliveryResult
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
132
 sortTimeStamps
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
12
 getVariableDataFromDeliveryResult
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 extractTestVariables
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
5
 getTestTaker
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 deleteResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVariableFile
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 getTestTakerData
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
30
 getReadableImplementation
71.43% covered (warning)
71.43%
5 / 7
0.00% covered (danger)
0.00%
0 / 1
3.21
 getColumnNames
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getResultsByDelivery
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 shouldResultBeSkipped
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getCellsByResults
5.26% covered (danger)
5.26%
4 / 76
0.00% covered (danger)
0.00%
0 / 1
472.79
 collectColumnDataProviderMap
21.43% covered (danger)
21.43%
3 / 14
0.00% covered (danger)
0.00%
0 / 1
17.13
 convertDates
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 filterData
83.33% covered (warning)
83.33%
20 / 24
0.00% covered (danger)
0.00%
0 / 1
20.67
 getDeliveryExecutionService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 countResultByDelivery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getResultsVariables
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVariableColumns
94.44% covered (success)
94.44%
51 / 54
0.00% covered (danger)
0.00%
0 / 1
11.02
 isResultVariable
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
7
 defineTypeColumn
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
 filterCellData
0.00% covered (danger)
0.00%
0 / 17
0.00% covered (danger)
0.00%
0 / 1
56
 findItemLabel
25.00% covered (danger)
25.00%
2 / 8
0.00% covered (danger)
0.00%
0 / 1
6.80
 getItemIndexer
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
2.31
 getTestMetadata
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 loadTestMetadata
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 loadTestMetadataFromFile
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 compileTestMetadata
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 getDecompiledIndexer
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 getResultLanguage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDeliveryByResultId
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getDirectoryIds
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 getPrivateDirectory
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getServiceLocator
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 findResultsByDeliveryAndFilters
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getColumnIdProvider
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColumnLabelProvider
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getItemResultStrategy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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) 2013-2022 Open Assessment Technologies S.A.
19 *
20 *
21 * @access  public
22 * @author  Joel Bout, <joel.bout@tudor.lu>
23 * @package taoOutcomeUi
24 */
25
26namespace oat\taoOutcomeUi\model;
27
28use common_Exception;
29use common_exception_Error;
30use common_exception_NoContent;
31use common_Logger;
32use core_kernel_classes_Resource;
33use League\Flysystem\FileNotFoundException;
34use oat\generis\model\GenerisRdf;
35use oat\generis\model\OntologyRdfs;
36use oat\oatbox\service\ServiceManager;
37use oat\oatbox\user\User;
38use oat\tao\helpers\metadata\ResourceCompiledMetadataHelper;
39use oat\tao\model\metadata\compiler\ResourceJsonMetadataCompiler;
40use oat\tao\model\metadata\compiler\ResourceMetadataCompilerInterface;
41use oat\tao\model\OntologyClassService;
42use oat\taoDelivery\model\execution\DeliveryExecution;
43use oat\taoDelivery\model\execution\DeliveryExecutionService;
44use oat\taoDelivery\model\execution\ServiceProxy;
45use oat\taoDelivery\model\RuntimeService;
46use oat\taoDeliveryRdf\model\DeliveryAssemblyService;
47use oat\taoItems\model\ItemCompilerIndex;
48use oat\taoOutcomeUi\helper\Datatypes;
49use oat\taoOutcomeUi\model\table\ColumnDataProvider\ColumnId\VariableColumnIdProvider;
50use oat\taoOutcomeUi\model\table\ColumnDataProvider\ColumnIdProvider;
51use oat\taoOutcomeUi\model\table\ColumnDataProvider\ColumnLabelProvider;
52use oat\taoOutcomeUi\model\table\ContextTypePropertyColumn;
53use oat\taoOutcomeUi\model\table\GradeColumn;
54use oat\taoOutcomeUi\model\table\ResponseColumn;
55use oat\taoOutcomeUi\model\table\TestCenterColumn;
56use oat\taoOutcomeUi\model\table\TraceVariableColumn;
57use oat\taoOutcomeUi\model\table\VariableColumn;
58use oat\taoOutcomeUi\model\Wrapper\ResultServiceWrapper;
59use oat\taoQtiTest\models\DeliveryItemTypeService;
60use oat\taoQtiTest\models\QtiTestCompilerIndex;
61use oat\taoResultServer\models\classes\NoResultStorage;
62use oat\taoResultServer\models\classes\NoResultStorageException;
63use oat\taoResultServer\models\classes\ResultManagement;
64use oat\taoResultServer\models\classes\ResultServerService;
65use oat\taoResultServer\models\classes\ResultService;
66use oat\taoResultServer\models\Formatter\ItemResponseVariableSplitter;
67use tao_helpers_Date;
68use tao_models_classes_service_StorageDirectory;
69use taoQtiTest_models_classes_QtiTestService;
70use taoResultServer_models_classes_ReadableResultStorage;
71use taoResultServer_models_classes_Variable as Variable;
72use taoTests_models_classes_TestsService as TestsService;
73
74class ResultsService extends OntologyClassService
75{
76    public const SERVICE_ID = 'taoOutcomeUi/OutcomeUiResultService';
77
78    public const VARIABLES_FILTER_LAST_SUBMITTED = 'lastSubmitted';
79    public const VARIABLES_FILTER_FIRST_SUBMITTED = 'firstSubmitted';
80    public const VARIABLES_FILTER_ALL = 'all';
81
82    // Only need to correct formatting trace variables
83    protected const VARIABLES_FILTER_TRACE = 'trace';
84
85    public const PERSISTENCE_CACHE_KEY = 'resultCache';
86
87    public const PERIODS = [self::FILTER_START_FROM, self::FILTER_START_TO, self::FILTER_END_FROM, self::FILTER_END_TO];
88    public const DELIVERY_EXECUTION_STARTED_AT = 'delivery_execution_started_at';
89    public const DELIVERY_EXECUTION_FINISHED_AT = 'delivery_execution_finished_at';
90    public const FILTER_START_FROM = 'startfrom';
91    public const FILTER_START_TO = 'startto';
92    public const FILTER_END_FROM = 'endfrom';
93    public const FILTER_END_TO = 'endto';
94
95    public const OPTION_ALLOW_SQL_EXPORT = 'allow_sql_export';
96    public const OPTION_ALLOW_TRACE_VARIABLES_EXPORT = 'allow_trace_variable_export';
97
98    public const SEPARATOR = ' | ';
99
100    private const WANTED_TYPES = [
101        \taoResultServer_models_classes_ResponseVariable::class,
102        \taoResultServer_models_classes_OutcomeVariable::class,
103        \taoResultServer_models_classes_TraceVariable::class
104    ];
105
106    /** @var taoResultServer_models_classes_ReadableResultStorage */
107    private $implementation;
108
109    /**
110     * Internal cache for item info.
111     *
112     * @var array
113     */
114    private $itemInfoCache = [];
115
116    /**
117     * External cache.
118     *
119     * @var \common_persistence_KvDriver
120     */
121    private $resultCache;
122
123    /** @var array */
124    private $indexerCache = [];
125
126    /** @var array */
127    private $executionCache = [];
128
129    /** @var array */
130    private $testMetadataCache = [];
131    /**
132     * @return \common_persistence_KvDriver|null
133     */
134    public function getCache()
135    {
136        if (is_null($this->resultCache)) {
137            /** @var \common_persistence_Manager $persistenceManager */
138            $persistenceManager = $this->getServiceLocator()->get(\common_persistence_Manager::SERVICE_ID);
139            if ($persistenceManager->hasPersistence(self::PERSISTENCE_CACHE_KEY)) {
140                $this->resultCache = $persistenceManager->getPersistenceById(self::PERSISTENCE_CACHE_KEY);
141            }
142        }
143
144        return $this->resultCache;
145    }
146
147    public function getCacheKey($resultIdentifier, $suffix = '')
148    {
149        return 'resultPageCache:' . $resultIdentifier . ':' . $suffix;
150    }
151
152    protected function getContainerCacheKey($resultIdentifier)
153    {
154        return $this->getCacheKey($resultIdentifier, 'keys');
155    }
156
157    public function setCacheValue($resultIdentifier, $fullKey, $value)
158    {
159        if (is_null($this->getCache())) {
160            return false;
161        }
162
163        $fullKeys = [];
164
165        $containerKey = $this->getContainerCacheKey($resultIdentifier);
166        if ($this->getCache()->exists($containerKey)) {
167            $fullKeys = $this->getContainerCacheValue($containerKey);
168        }
169
170        $fullKeys[] = $fullKey;
171
172        if ($this->getCache()->set($fullKey, $value)) {
173            // let's save the container of the keys as well
174            return $this->setContainerCacheValue($containerKey, $fullKeys);
175        }
176
177        return false;
178    }
179
180    public function deleteCacheFor($resultIdentifier)
181    {
182        if (is_null($this->getCache())) {
183            return false;
184        }
185
186        $containerKey = $this->getContainerCacheKey($resultIdentifier);
187        if (!$this->getCache()->exists($containerKey)) {
188            return false;
189        }
190
191        $fullKeys = $this->getContainerCacheValue($containerKey);
192        $initialCount = count($fullKeys);
193
194        foreach ($fullKeys as $i => $key) {
195            if ($this->getCache()->del($key)) {
196                unset($fullKeys[$i]);
197            }
198        }
199
200        if (empty($fullKeys)) {
201            // delete the whole container
202            return $this->getCache()->del($containerKey);
203        } elseif (count($fullKeys) < $initialCount) {
204            // update the container
205            return $this->setContainerCacheValue($containerKey, $fullKeys);
206        }
207
208        // no cache has been deleted
209        return false;
210    }
211
212    protected function setContainerCacheValue($containerKey, array $fullKeys)
213    {
214        return $this->getCache()->set($containerKey, gzencode(json_encode(array_unique($fullKeys)), 9));
215    }
216
217    protected function getContainerCacheValue($containerKey)
218    {
219        return json_decode(gzdecode($this->getCache()->get($containerKey)), true);
220    }
221
222    /**
223     * (non-PHPdoc)
224     * @see tao_models_classes_ClassService::getRootClass()
225     */
226    public function getRootClass()
227    {
228        return $this->getClass(ResultService::DELIVERY_RESULT_CLASS_URI);
229    }
230
231    public function setImplementation(ResultManagement $implementation)
232    {
233        $this->implementation = $implementation;
234    }
235
236    /**
237     * @return ResultManagement
238     * @throws common_exception_Error
239     */
240    public function getImplementation()
241    {
242        if ($this->implementation == null) {
243            throw new \common_exception_Error('No result storage defined');
244        }
245
246        return $this->implementation;
247    }
248
249    /**
250     * return all variable for that deliveryResults (uri identifiers)
251     *
252     * @access public
253     *
254     * @param string $resultIdentifier
255     * @param boolean $flat a flat array is returned or a structured delvieryResult-ItemResult-Variable
256     *
257     * @return array
258     * @author Joel Bout, <joel.bout@tudor.lu>
259     */
260    public function getVariables($resultIdentifier, $flat = true)
261    {
262        $variables = [];
263        //this service is slow due to the way the data model design
264        //if the delvieryResult related execution is finished, the data is stored in cache.
265
266        $serial = 'deliveryResultVariables:' . $resultIdentifier;
267        //if (common_cache_FileCache::singleton()->has($serial)) {
268        //    $variables = common_cache_FileCache::singleton()->get($serial);
269        //} else {
270        $resultVariables = $this->getImplementation()->getDeliveryVariables($resultIdentifier);
271        foreach ($resultVariables as $resultVariable) {
272            $currentItem = current($resultVariable);
273            $key = isset($currentItem->callIdItem) ? $currentItem->callIdItem : $currentItem->callIdTest;
274            $variables[$key][] = $resultVariable;
275        }
276
277        // impossible to determine state DeliveryExecution::STATE_FINISHIED
278        //    if (false) {
279        //        common_cache_FileCache::singleton()->put($variables, $serial);
280        //    }
281        //}
282        if ($flat) {
283            $returnValue = [];
284            foreach ($variables as $key => $itemResultVariables) {
285                $newKeys = [];
286                $oldKeys = array_keys($itemResultVariables);
287                foreach ($oldKeys as $oldKey) {
288                    $newKeys[] = $key . '_' . $oldKey;
289                }
290                $itemResultVariables = array_combine($newKeys, array_values($itemResultVariables));
291                $returnValue = array_merge($returnValue, $itemResultVariables);
292            }
293        } else {
294            $returnValue = $variables;
295        }
296
297
298        return (array)$returnValue;
299    }
300
301    /**
302     * @param string|array $itemResult
303     * @param array $wantedTypes
304     *
305     * @return array
306     * @throws common_exception_Error
307     */
308    public function getVariablesFromObjectResult($itemResult, $wantedTypes = self::WANTED_TYPES)
309    {
310        $returnedVariables = [];
311        $variables = $this->getImplementation()->getVariables($itemResult);
312
313        foreach ($variables as $itemVariables) {
314            foreach ($itemVariables as $variable) {
315                if (in_array(get_class($variable->variable), $wantedTypes)) {
316                    $returnedVariables[] = [$variable];
317                }
318            }
319        }
320
321        unset($variables);
322
323        return $returnedVariables;
324    }
325
326    /**
327     * Return the corresponding delivery
328     *
329     * @param string $resultIdentifier
330     *
331     * @return core_kernel_classes_Resource delviery
332     * @author Patrick Plichart, <patrick@taotesting.com>
333     */
334    public function getDelivery($resultIdentifier)
335    {
336        return new core_kernel_classes_Resource($this->getImplementation()->getDelivery($resultIdentifier));
337    }
338
339    /**
340     * Ges the type of items contained by the delivery
341     *
342     * @param string $resultIdentifier
343     *
344     * @return string
345     */
346    public function getDeliveryItemType($resultIdentifier)
347    {
348        /** @var DeliveryItemTypeService $deliveryItemTypeService */
349        $deliveryItemTypeService = $this->getServiceLocator()->get(DeliveryItemTypeService::SERVICE_ID);
350
351        return $deliveryItemTypeService->getDeliveryItemType($resultIdentifier);
352    }
353
354    /**
355     * Returns all label of itemResults related to the delvieryResults
356     *
357     * @param string $resultIdentifier
358     *
359     * @return array string uri
360     * */
361    public function getItemResultsFromDeliveryResult($resultIdentifier)
362    {
363        return $this->getImplementation()->getRelatedItemCallIds($resultIdentifier);
364    }
365
366    /**
367     * Returns all label of itemResults related to the delvieryResults
368     *
369     * @param string $resultIdentifier
370     *
371     * @return array string uri
372     * */
373    public function getTestsFromDeliveryResult($resultIdentifier)
374    {
375        return $this->getImplementation()->getRelatedTestCallIds($resultIdentifier);
376    }
377
378    /**
379     *
380     * @param string $itemCallId
381     * @param array $itemVariables already retrieved variables
382     *
383     * @return array|null
384     * @throws \common_exception_NotFound
385     * @throws common_exception_Error
386     */
387    public function getItemFromItemResult($itemCallId, $itemVariables = [])
388    {
389        if (empty($itemVariables)) {
390            $itemVariables = $this->getImplementation()->getVariables($itemCallId);
391        }
392
393        //get the first variable (item are the same in all)
394        $tmpItems = array_shift($itemVariables);
395
396        //get the first object
397        $itemUri = $tmpItems[0]->item;
398
399        if (!$itemUri) {
400            return null;
401        }
402
403        $delivery = $this->getDeliveryByResultId($tmpItems[0]->deliveryResultIdentifier);
404
405        return [
406            'uriResource' => $itemUri,
407            'label' => $this->findItemLabel($delivery, $itemUri),
408        ];
409    }
410
411    /**
412     *
413     * @param string $test
414     *
415     * @return \core_kernel_classes_Resource
416     */
417    public function getVariableFromTest($test)
418    {
419        $returnTest = null;
420        $tests = $this->getImplementation()->getVariables($test);
421
422        //get the first variable (item are the same in all)
423        $tmpTests = array_shift($tests);
424
425        //get the first object
426        if (!is_null($tmpTests[0]->test)) {
427            $returnTest = new core_kernel_classes_Resource($tmpTests[0]->test);
428        }
429
430        return $returnTest;
431    }
432
433    /**
434     *
435     * @param string $variableUri
436     *
437     * @return string
438     *
439     */
440    public function getVariableCandidateResponse($variableUri)
441    {
442        return $this->getImplementation()->getVariableProperty($variableUri, 'candidateResponse');
443    }
444
445    /**
446     *
447     * @param string $variableUri
448     *
449     * @return string
450     */
451    public function getVariableBaseType($variableUri)
452    {
453        return $this->getImplementation()->getVariableProperty($variableUri, 'baseType');
454    }
455
456
457    /**
458     *
459     * @param array $variablesData
460     *
461     * @return array ["nbResponses" => x,"nbCorrectResponses" => y,"nbIncorrectResponses" => z,"nbUnscoredResponses" =>
462     *               a,"data" => $variableData]
463     */
464    public function calculateResponseStatistics($variablesData)
465    {
466        $numberOfResponseVariables = 0;
467        $numberOfCorrectResponseVariables = 0;
468        $numberOfInCorrectResponseVariables = 0;
469        $numberOfUnscoredResponseVariables = 0;
470        foreach ($variablesData as $epoch => $itemVariables) {
471            foreach ($itemVariables as $key => $value) {
472                if ($key == \taoResultServer_models_classes_ResponseVariable::class) {
473                    foreach ($value as $variable) {
474                        $numberOfResponseVariables++;
475                        switch ($variable['isCorrect']) {
476                            case 'correct':
477                                $numberOfCorrectResponseVariables++;
478                                break;
479                            case 'incorrect':
480                                $numberOfInCorrectResponseVariables++;
481                                break;
482                            case 'unscored':
483                                $numberOfUnscoredResponseVariables++;
484                                break;
485                            default:
486                                common_Logger::w('The value ' . $variable['isCorrect'] . ' is not a valid value');
487                                break;
488                        }
489                    }
490                }
491            }
492        }
493        $stats = [
494            "nbResponses" => $numberOfResponseVariables,
495            "nbCorrectResponses" => $numberOfCorrectResponseVariables,
496            "nbIncorrectResponses" => $numberOfInCorrectResponseVariables,
497            "nbUnscoredResponses" => $numberOfUnscoredResponseVariables,
498        ];
499
500        return $stats;
501    }
502
503    /**
504     * @param $itemCallId
505     * @param $itemVariables
506     *
507     * @return array item information ['uri' => xxx, 'label' => yyy]
508     */
509    private function getItemInfos($itemCallId, $itemVariables)
510    {
511        $undefinedStr = __('unknown'); //some data may have not been submitted
512
513        try {
514            common_Logger::d("Retrieving related Item for item call " . $itemCallId . "");
515            $relatedItem = $this->getItemFromItemResult($itemCallId, $itemVariables);
516        } catch (common_Exception $e) {
517            common_Logger::w("The item call '" . $itemCallId . "' is not linked to a valid item. (deleted item ?)");
518            $relatedItem = null;
519        }
520
521        if (isset($relatedItem['uriResource'])) {
522            $itemIdentifier = $relatedItem['uriResource'];
523            // check item info in internal cache
524            if (isset($this->itemInfoCache[$itemIdentifier])) {
525                common_Logger::t("Item info found in internal cache for item " . $itemIdentifier . "");
526
527                return $this->itemInfoCache[$itemIdentifier];
528            }
529        }
530
531        $item['itemModel'] = '---';
532        $item['label'] = $relatedItem['label'] ?? $undefinedStr;
533        $item['uri'] = $relatedItem['uriResource'] ?? $undefinedStr;
534        $item['isLocal'] = strpos($relatedItem['uriResource'] ?? '', LOCAL_NAMESPACE) !== false;
535
536        // storing item info in memory to not hit the db for the same item again and again
537        // when method "getStructuredVariables" are called multiple times in the same request
538        if ($relatedItem) {
539            $this->itemInfoCache[$relatedItem['uriResource']] = $item;
540        }
541
542        return $item;
543    }
544
545
546    /**
547     *  prepare a data set as an associative array, service intended to populate gui controller
548     *
549     * @param string $resultIdentifier
550     * @param string $filter     'lastSubmitted', 'firstSubmitted', 'all'
551     * @param array $wantedTypes ['taoResultServer_models_classes_ResponseVariable',
552     *                           'taoResultServer_models_classes_OutcomeVariable',
553     *                           'taoResultServer_models_classes_TraceVariable']
554     *
555     * @return array
556     * [
557     * 'epoch1' => [
558     * 'label' => Example_0_Introduction,
559     * 'uri' => http://tao.local/mytao.rdf#i1462952280695832,
560     * 'internalIdentifier' => item-1,
561     * 'taoResultServer_models_classes_Variable class name' => [
562     * 'Variable identifier 1' => [
563     * 'uri' => 1,
564     * 'var' => taoResultServer_models_classes_Variable object,
565     * 'isCorrect' => correct
566     * ],
567     * 'Variable identifier 2' => [
568     * 'uri' => 2,
569     * 'var' => taoResultServer_models_classes_Variable object,
570     * 'isCorrect' => unscored
571     * ]
572     * ]
573     * ]
574     * ]
575     *
576     * @throws common_exception_Error
577     */
578    public function getStructuredVariables($resultIdentifier, $filter, $wantedTypes = [])
579    {
580        $itemCallIds = $this->getItemResultsFromDeliveryResult($resultIdentifier);
581
582        // splitting call ids into chunks to perform bulk queries
583        $itemCallIdChunks = array_chunk($itemCallIds, 50);
584
585        $itemVariables = [];
586        foreach ($itemCallIdChunks as $ids) {
587            $itemVariables = array_merge($itemVariables, $this->getVariablesFromObjectResult($ids, $wantedTypes));
588        }
589
590        return $this->structureItemVariables($itemVariables, $filter);
591    }
592
593    public function structureItemVariables($itemVariables, $filter)
594    {
595        usort($itemVariables, function ($a, $b) {
596            $variableA = $a[0]->variable;
597            $variableB = $b[0]->variable;
598            [$usec, $sec] = explode(" ", $variableA->getEpoch());
599            $floata = ((float)$usec + (float)$sec);
600            [$usec, $sec] = explode(" ", $variableB->getEpoch());
601            $floatb = ((float)$usec + (float)$sec);
602
603            if ((floatval($floata) - floatval($floatb)) > 0) {
604                return 1;
605            } elseif ((floatval($floata) - floatval($floatb)) < 0) {
606                return -1;
607            } else {
608                return 0;
609            }
610        });
611
612        $attempts = $this->splitByItemAndAttempt($itemVariables, $filter);
613        $variablesByItem = [];
614        foreach ($attempts as $time => $variables) {
615            $variablesByItem[$time] = [];
616            foreach ($variables as $itemVariable) {
617                $variable = $itemVariable->variable;
618                $itemCallId = $itemVariable->callIdItem;
619                if ($variable->getIdentifier() == 'numAttempts') {
620                    $variablesByItem[$time] = array_merge(
621                        $variablesByItem[$time],
622                        $this->getItemInfos($itemCallId, [[$itemVariable]])
623                    );
624                    $variablesByItem[$time]['attempt'] = $variable->getValue();
625                }
626                $variableDescription = [
627                    'uri' => $itemVariable->uri,
628                    'var' => $variable,
629                ];
630                if (
631                    $variable instanceof \taoResultServer_models_classes_ResponseVariable
632                    && !is_null($variable->getCorrectResponse())
633                ) {
634                    $variableDescription['isCorrect'] = $variable->getCorrectResponse() >= 1 ? 'correct' : 'incorrect';
635                } else {
636                    $variableDescription['isCorrect'] = 'unscored';
637                }
638
639                // some dangerous assumptions about the call Id structure
640                $callIdParts = explode('.', $itemCallId);
641                $variablesByItem[$time]['internalIdentifier'] = $callIdParts[count($callIdParts) - 2];
642                $variablesByItem[$time][get_class($variable)][$variable->getIdentifier()] = $variableDescription;
643            }
644        }
645
646        return $variablesByItem;
647    }
648
649    public function splitByItemAndAttempt($itemVariables, $filter)
650    {
651        $sorted = [];
652        foreach ($this->splitByItem($itemVariables) as $variables) {
653            $itemCallId = current($variables)->callIdItem;
654            $byAttempt = $this->splitByAttempt($variables);
655            switch ($filter) {
656                case self::VARIABLES_FILTER_ALL:
657                    foreach ($byAttempt as $time => $attempt) {
658                        $sorted[$time . $itemCallId] = $attempt;
659                    }
660                    break;
661                case self::VARIABLES_FILTER_FIRST_SUBMITTED:
662                    reset($byAttempt);
663                    $sorted[key($byAttempt) . $itemCallId] = current($byAttempt);
664                    break;
665                case self::VARIABLES_FILTER_LAST_SUBMITTED:
666                    end($byAttempt);
667                    $sorted[key($byAttempt) . $itemCallId] = current($byAttempt);
668                    break;
669                default:
670                    throw new \common_exception_InconsistentData('Unknown Filter ' . $filter);
671            }
672        }
673        ksort($sorted);
674
675        return $sorted;
676    }
677
678    /**
679     * Split item variables by item
680     */
681    public function splitByItem($itemVariables)
682    {
683        $byItem = [];
684        foreach ($itemVariables as $variable) {
685            $itemVariable = $variable[0];
686            if (!is_null($itemVariable->callIdItem)) {
687                if (!isset($byItem[$itemVariable->callIdItem])) {
688                    $byItem[$itemVariable->callIdItem] = [$itemVariable];
689                } else {
690                    $byItem[$itemVariable->callIdItem][] = $itemVariable;
691                }
692            }
693        }
694
695        return $byItem;
696    }
697
698    public function splitByAttempt($itemVariables)
699    {
700        return $this->getItemResponseSplitter()->splitObjByAttempt($itemVariables);
701    }
702
703    private function getItemResponseSplitter(): ItemResponseVariableSplitter
704    {
705        return $this->getServiceLocator()->get(ItemResponseVariableSplitter::class);
706    }
707
708    /**
709     * Filters the complex array structure for variable classes
710     *
711     * @param array $structure as defined by getStructuredVariables()
712     * @param array $filter    classes to keep
713     *
714     * @return array as defined by getStructuredVariables()
715     */
716    public function filterStructuredVariables(array $structure, array $filter)
717    {
718        $all = [
719            \taoResultServer_models_classes_ResponseVariable::class,
720            \taoResultServer_models_classes_OutcomeVariable::class,
721            \taoResultServer_models_classes_TraceVariable::class,
722        ];
723        $toRemove = array_diff($all, $filter);
724        $filtered = $structure;
725        foreach ($filtered as $timestamp => $entry) {
726            foreach ($entry as $key => $value) {
727                if (in_array($key, $toRemove)) {
728                    unset($filtered[$timestamp][$key]);
729                }
730            }
731        }
732
733        return $filtered;
734    }
735
736    /**
737     *
738     * @param $resultIdentifier
739     * @param string $filter 'lastSubmitted', 'firstSubmitted'
740     *
741     * @return array ["nbResponses" => x,"nbCorrectResponses" => y,"nbIncorrectResponses" => z,"nbUnscoredResponses" =>
742     *               a,"data" => $variableData]
743     * @deprecated
744     */
745    public function getItemVariableDataStatsFromDeliveryResult($resultIdentifier, $filter = null)
746    {
747        $numberOfResponseVariables = 0;
748        $numberOfCorrectResponseVariables = 0;
749        $numberOfInCorrectResponseVariables = 0;
750        $numberOfUnscoredResponseVariables = 0;
751        $numberOfOutcomeVariables = 0;
752        $variablesData = $this->getItemVariableDataFromDeliveryResult($resultIdentifier, $filter);
753        foreach ($variablesData as $itemVariables) {
754            foreach ($itemVariables['sortedVars'] as $key => $value) {
755                if ($key == \taoResultServer_models_classes_ResponseVariable::class) {
756                    foreach ($value as $variable) {
757                        $variable = array_shift($variable);
758                        $numberOfResponseVariables++;
759                        switch ($variable['isCorrect']) {
760                            case 'correct':
761                                $numberOfCorrectResponseVariables++;
762                                break;
763                            case 'incorrect':
764                                $numberOfInCorrectResponseVariables++;
765                                break;
766                            case 'unscored':
767                                $numberOfUnscoredResponseVariables++;
768                                break;
769                            default:
770                                common_Logger::w('The value ' . $variable['isCorrect'] . ' is not a valid value');
771                                break;
772                        }
773                    }
774                } else {
775                    $numberOfOutcomeVariables++;
776                }
777            }
778        }
779        $stats = [
780            "nbResponses" => $numberOfResponseVariables,
781            "nbCorrectResponses" => $numberOfCorrectResponseVariables,
782            "nbIncorrectResponses" => $numberOfInCorrectResponseVariables,
783            "nbUnscoredResponses" => $numberOfUnscoredResponseVariables,
784            "data" => $variablesData,
785        ];
786
787        return $stats;
788    }
789
790    /**
791     *  prepare a data set as an associative array, service intended to populate gui controller
792     *
793     * @param string $resultIdentifier
794     * @param string $filter 'lastSubmitted', 'firstSubmitted'
795     *
796     * @return array
797     * @deprecated
798     */
799    public function getItemVariableDataFromDeliveryResult($resultIdentifier, $filter)
800    {
801        $itemCallIds = $this->getItemResultsFromDeliveryResult($resultIdentifier);
802        $variablesByItem = [];
803        foreach ($itemCallIds as $itemCallId) {
804            $itemVariables = $this->getVariablesFromObjectResult($itemCallId);
805
806            $item = $this->getItemInfos($itemCallId, $itemVariables);
807            $itemIdentifier = $item['uri'];
808            $itemLabel = $item['label'];
809            $variablesByItem[$itemIdentifier]['itemModel'] = $item['itemModel'];
810            foreach ($itemVariables as $variable) {
811                //retrieve the type of the variable
812                $variableTemp = $variable[0]->variable;
813                $variableDescription = [];
814                $type = get_class($variableTemp);
815
816
817                $variableIdentifier = $variableTemp->getIdentifier();
818
819                $variableDescription["uri"] = $variable[0]->uri;
820                $variableDescription["var"] = $variableTemp;
821
822                if (
823                    method_exists($variableTemp, 'getCorrectResponse')
824                    && !is_null($variableTemp->getCorrectResponse())
825                ) {
826                    if ($variableTemp->getCorrectResponse() >= 1) {
827                        $variableDescription["isCorrect"] = "correct";
828                    } else {
829                        $variableDescription["isCorrect"] = "incorrect";
830                    }
831                } else {
832                    $variableDescription["isCorrect"] = "unscored";
833                }
834
835                $variablesByItem[$itemIdentifier]['sortedVars'][$type][$variableIdentifier][$variableTemp->getEpoch()]
836                    = $variableDescription;
837                $variablesByItem[$itemIdentifier]['label'] = $itemLabel;
838            }
839        }
840        //sort by epoch and filter
841        foreach ($variablesByItem as $itemIdentifier => $itemVariables) {
842            foreach ($itemVariables['sortedVars'] as $variableType => $variables) {
843                foreach ($variables as $variableIdentifier => $observation) {
844                    uksort(
845                        $variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier],
846                        "self::sortTimeStamps"
847                    );
848
849                    switch ($filter) {
850                        case self::VARIABLES_FILTER_LAST_SUBMITTED:
851                            $variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier] = [
852                                array_pop(
853                                    $variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier]
854                                )
855                            ];
856                            break;
857                        case self::VARIABLES_FILTER_FIRST_SUBMITTED:
858                            $variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier] = [
859                                array_shift(
860                                    $variablesByItem[$itemIdentifier]['sortedVars'][$variableType][$variableIdentifier]
861                                )
862                            ];
863                            break;
864                    }
865                }
866            }
867        }
868
869        return $variablesByItem;
870    }
871
872    /**
873     *
874     * @param string $a epoch
875     * @param string $b epoch
876     *
877     * @return number
878     */
879    public static function sortTimeStamps($a, $b)
880    {
881        [$usec, $sec] = explode(" ", $a);
882        $floata = ((float)$usec + (float)$sec);
883        [$usec, $sec] = explode(" ", $b);
884        $floatb = ((float)$usec + (float)$sec);
885
886        //the callback is expecting an int returned, for the case where the difference is of less than a second
887        //intval(round(floatval($b) - floatval($a),1, PHP_ROUND_HALF_EVEN));
888        if ((floatval($floata) - floatval($floatb)) > 0) {
889            return 1;
890        } elseif ((floatval($floata) - floatval($floatb)) < 0) {
891            return -1;
892        } else {
893            return 0;
894        }
895    }
896
897    /**
898     * return all variables linked to the delviery result and that are not linked to a particular itemResult
899     *
900     * @param string $resultIdentifier
901     * @param array $wantedTypes
902     *
903     * @return array
904     */
905    public function getVariableDataFromDeliveryResult($resultIdentifier, $wantedTypes = self::WANTED_TYPES)
906    {
907        $testCallIds = $this->getTestsFromDeliveryResult($resultIdentifier);
908
909        return $this->extractTestVariables($this->getVariablesFromObjectResult($testCallIds), $wantedTypes);
910    }
911
912    public function extractTestVariables(
913        array $variableObjects,
914        array $wantedTypes,
915        string $filter = self::VARIABLES_FILTER_ALL
916    ) {
917        $variableObjects = array_filter($variableObjects, static function (array $variableObject) use ($wantedTypes) {
918            $variable = current($variableObject);
919
920            return $variable->callIdItem === null && in_array(get_class($variable->variable), $wantedTypes, true);
921        });
922
923        $variableObjects = array_map(static function (array $variableObject) {
924            return current($variableObject)->variable;
925        }, $variableObjects);
926
927        usort($variableObjects, static function (
928            Variable $a,
929            Variable $b
930        ) use ($filter) {
931            if ($filter === self::VARIABLES_FILTER_LAST_SUBMITTED) {
932                return $b->getCreationTime() - $a->getCreationTime();
933            }
934
935            return $a->getCreationTime() - $b->getCreationTime();
936        });
937
938        if (
939            in_array(
940                $filter,
941                [self::VARIABLES_FILTER_FIRST_SUBMITTED, self::VARIABLES_FILTER_LAST_SUBMITTED],
942                true
943            )
944        ) {
945            $uniqueVariableIdentifiers = [];
946
947            $variableObjects = array_filter($variableObjects, static function (
948                Variable $variable
949            ) use (&$uniqueVariableIdentifiers) {
950                if (in_array($variable->getIdentifier(), $uniqueVariableIdentifiers, true)) {
951                    return false;
952                }
953                $uniqueVariableIdentifiers[] = $variable->getIdentifier();
954
955                return true;
956            });
957        }
958
959        return $variableObjects;
960    }
961
962    /**
963     * returns the test taker related to the delivery
964     *
965     * @param string $resultIdentifier
966     *
967     * @return User
968     */
969    public function getTestTaker($resultIdentifier)
970    {
971        $testTaker = $this->getImplementation()->getTestTaker($resultIdentifier);
972        /** @var \tao_models_classes_UserService $userService */
973        $userService = $this->getServiceLocator()->get(\tao_models_classes_UserService::SERVICE_ID);
974        $user = $userService->getUserById($testTaker);
975
976        return $user;
977    }
978
979    /**
980     * Delete a delivery result
981     *
982     * @param string $resultIdentifier
983     *
984     * @return boolean
985     */
986    public function deleteResult($resultIdentifier)
987    {
988        return $this->getImplementation()->deleteResult($resultIdentifier);
989    }
990
991
992    /**
993     * Return the file data associate to a variable
994     *
995     * @param $variableUri
996     *
997     * @return array file data
998     * @throws \core_kernel_persistence_Exception
999     */
1000    public function getVariableFile($variableUri)
1001    {
1002        //distinguish QTI file from other "file" base type
1003        $baseType = $this->getVariableBaseType($variableUri);
1004
1005        switch ($baseType) {
1006            case "file":
1007                $value = $this->getVariableCandidateResponse($variableUri);
1008                common_Logger::i(var_export(strlen($value), true));
1009                $decodedFile = Datatypes::decodeFile($value);
1010                common_Logger::i("FileName:");
1011                common_Logger::i(var_export($decodedFile["name"], true));
1012                common_Logger::i("Mime Type:");
1013                common_Logger::i(var_export($decodedFile["mime"], true));
1014                $file = [
1015                    "data" => $decodedFile["data"],
1016                    "mimetype" => "Content-type: " . $decodedFile["mime"],
1017                    "filename" => $decodedFile["name"]];
1018                break;
1019            default:
1020                $file = [
1021                    "data" => $this->getVariableCandidateResponse($variableUri),
1022                    "mimetype" => "Content-type: text/xml",
1023                    "filename" => "trace.xml"];
1024        }
1025
1026        return $file;
1027    }
1028
1029    /**
1030     * To be reviewed as it implies a dependency towards taoSubjects
1031     *
1032     * @param string $resultIdentifier
1033     *
1034     * @return array test taker properties values
1035     */
1036    public function getTestTakerData($resultIdentifier)
1037    {
1038        $testTaker = $this->gettestTaker($resultIdentifier);
1039        if (get_class($testTaker) == 'core_kernel_classes_Literal') {
1040            return $testTaker;
1041        } elseif (empty($testTaker)) {
1042            return null;
1043        } else {
1044            $arrayOfProperties = [
1045                OntologyRdfs::RDFS_LABEL,
1046                GenerisRdf::PROPERTY_USER_LOGIN,
1047                GenerisRdf::PROPERTY_USER_FIRSTNAME,
1048                GenerisRdf::PROPERTY_USER_LASTNAME,
1049                GenerisRdf::PROPERTY_USER_MAIL,
1050            ];
1051            $propValues = [];
1052            foreach ($arrayOfProperties as $property) {
1053                $values = [];
1054                foreach ($testTaker->getPropertyValues($property) as $value) {
1055                    $values[] = new \core_kernel_classes_Literal($value);
1056                }
1057                $propValues[$property] = $values;
1058            }
1059        }
1060
1061        return $propValues;
1062    }
1063
1064    /**
1065     *
1066     * @param \core_kernel_classes_Resource $delivery
1067     *
1068     * @return taoResultServer_models_classes_ReadableResultStorage
1069     * @throws common_exception_Error
1070     */
1071    public function getReadableImplementation(\core_kernel_classes_Resource $delivery)
1072    {
1073        /** @var ResultServerService $service */
1074        $service = $this->getServiceLocator()->get(ResultServerService::SERVICE_ID);
1075        $resultStorage = $service->getResultStorage($delivery);
1076
1077        /** NoResultStorage it's not readable only writable */
1078        if ($resultStorage instanceof NoResultStorage) {
1079            throw NoResultStorageException::create();
1080        }
1081
1082        if (!$resultStorage instanceof taoResultServer_models_classes_ReadableResultStorage) {
1083            throw new \common_exception_Error('The results storage it is not readable');
1084        }
1085
1086        return $resultStorage;
1087    }
1088
1089    /**
1090     * Get the array of column names indexed by their unique column id.
1091     *
1092     * @param \tao_models_classes_table_Column[] $columns
1093     *
1094     * @return array
1095     */
1096    public function getColumnNames(array $columns)
1097    {
1098        return array_reduce($columns, function ($carry, \tao_models_classes_table_Column $column) {
1099            /** @var ContextTypePropertyColumn|VariableColumn $column */
1100            $columnId = $this->getColumnIdProvider()->provide($column);
1101            $carry[$columnId] = $this->getColumnLabelProvider()->provide($column);
1102
1103            return $carry;
1104        });
1105    }
1106
1107    /**
1108     * @param core_kernel_classes_Resource $delivery
1109     * @param array $storageOptions
1110     * @param array $filters
1111     *
1112     * @throws common_Exception
1113     * @throws common_exception_Error
1114     */
1115    public function getResultsByDelivery(
1116        \core_kernel_classes_Resource $delivery,
1117        array $storageOptions = [],
1118        array $filters = []
1119    ) {
1120        //The list of delivery Results matching the current selection filters
1121        $this->setImplementation($this->getReadableImplementation($delivery));
1122
1123        return $this->findResultsByDeliveryAndFilters($delivery, $filters, $storageOptions);
1124    }
1125
1126    /**
1127     * @param string $result
1128     *
1129     * @return bool
1130     */
1131    protected function shouldResultBeSkipped($result)
1132    {
1133        return false;
1134    }
1135
1136    /**
1137     * @param array $results
1138     * @param                              $columns - columns to be exported
1139     * @param                              $filter 'lastSubmitted' or 'firstSubmitted'
1140     * @param array $filters
1141     * @param int $offset
1142     * @param int $limit
1143     *
1144     * @return array
1145     * @throws common_Exception
1146     * @throws common_exception_Error
1147     */
1148    public function getCellsByResults(
1149        array $results,
1150        $columns,
1151        $filter,
1152        array $filters = [],
1153        $offset = 0,
1154        $limit = null
1155    ) {
1156        $rows = [];
1157        $dataProviderMap = $this->collectColumnDataProviderMap($columns);
1158
1159        if (!array_key_exists($offset, $results)) {
1160            return null;
1161        }
1162
1163        /** @var DeliveryExecution $result */
1164        for ($i = $offset; $i < ($offset + $limit); $i++) {
1165            if (!array_key_exists($i, $results)) {
1166                break;
1167            }
1168            $result = $results[$i];
1169            if ($this->shouldResultBeSkipped($result)) {
1170                continue;
1171            }
1172
1173            // initialize column data providers for single result
1174            foreach ($dataProviderMap as $element) {
1175                $element['instance']->prepare([$result], $element['columns']);
1176            }
1177
1178            $cellData = [];
1179
1180            /** @var ContextTypePropertyColumn|VariableColumn $column */
1181            foreach ($columns as $column) {
1182                $cellKey = $this->getColumnIdProvider()->provide($column);
1183                $cellData[$cellKey] = null;
1184                $values = [];
1185
1186                if ($column instanceof TraceVariableColumn && count($column->getDataProvider()->getCache()) > 0) {
1187                    $cellData[$cellKey] = self::filterCellData(
1188                        $column->getDataProvider()->getValue(new core_kernel_classes_Resource($result), $column),
1189                        self::VARIABLES_FILTER_TRACE
1190                    );
1191
1192                    continue;
1193                } elseif (count($column->getDataProvider()->cache) > 0) {
1194                    // grade or response column values
1195                    $cellData[$cellKey] = self::filterCellData(
1196                        $column->getDataProvider()->getValue(new core_kernel_classes_Resource($result), $column),
1197                        $filter
1198                    );
1199
1200                    continue;
1201                } elseif ($column instanceof ContextTypePropertyColumn) {
1202                    // test taker or delivery property values
1203                    $resource = $column->isTestTakerType()
1204                        ? $this->getTestTaker($result)
1205                        : $this->getDelivery($result);
1206
1207                    $property = $column->getProperty();
1208                    if ($resource instanceof User) {
1209                        $property = $column->getProperty()->getUri();
1210                    }
1211                    $values = $resource->getPropertyValues($property);
1212
1213                    $values = array_map(function ($value) use ($column) {
1214                        if (\common_Utils::isUri($value)) {
1215                            $value = (new core_kernel_classes_Resource($value))->getLabel();
1216                        } else {
1217                            $value = (string)$value;
1218                        }
1219
1220                        if (
1221                            in_array(
1222                                $column->getProperty()->getUri(),
1223                                [DeliveryAssemblyService::PROPERTY_START, DeliveryAssemblyService::PROPERTY_END]
1224                            )
1225                        ) {
1226                            $value = tao_helpers_Date::displayeDate($value, tao_helpers_Date::FORMAT_VERBOSE);
1227                        }
1228
1229                        return $value;
1230                    }, $values);
1231
1232                    // if it's a guest test taker (it has no property values at all), let's display the uri as label
1233                    if (
1234                        $column->isTestTakerType()
1235                        && empty($values)
1236                        && $column->getProperty()->getUri() == OntologyRdfs::RDFS_LABEL
1237                    ) {
1238                        switch (true) {
1239                            case $resource instanceof core_kernel_classes_Resource:
1240                                $values[] = $resource->getUri();
1241                                break;
1242                            case $resource instanceof User:
1243                                $values[] = $resource->getIdentifier();
1244                                break;
1245                            default:
1246                                throw new \Exception('Invalid type of resource property values.');
1247                        }
1248                    }
1249                } elseif ($column instanceof TestCenterColumn) {
1250                    $property = $column->getProperty();
1251                    $testTaker = $this->getTestTaker($result);
1252                    $values = $testTaker->getPropertyValues($property);
1253
1254                    $values = array_map(function ($value) use ($column, $result) {
1255                        $currentDelivery = $this->getDelivery($result);
1256
1257                        return $column->getTestCenterLabel($value, $currentDelivery);
1258                    }, $values);
1259                }
1260
1261                $cellData[$cellKey] = [
1262                    self::filterCellData(implode(self::SEPARATOR, array_filter($values)), $filter)
1263                ];
1264            }
1265            if ($this->filterData($cellData, $filters, $result)) {
1266                $this->convertDates($cellData);
1267                $rows[] = [
1268                    'id' => $result,
1269                    'cell' => $cellData,
1270                ];
1271            }
1272        }
1273
1274        return $rows;
1275    }
1276
1277    /**
1278     * @param $columns
1279     *
1280     * @return array
1281     */
1282    private function collectColumnDataProviderMap($columns)
1283    {
1284        $dataProviderMap = [];
1285        foreach ($columns as $column) {
1286            $dataProvider = $column->getDataProvider();
1287            $found = false;
1288            foreach ($dataProviderMap as $index => $element) {
1289                if ($element['instance'] == $dataProvider) {
1290                    $dataProviderMap[$index]['columns'][] = $column;
1291                    $found = true;
1292                }
1293            }
1294            if (!$found) {
1295                $dataProviderMap[] = [
1296                    'instance' => $dataProvider,
1297                    'columns' => [$column],
1298                ];
1299            }
1300        }
1301
1302        return $dataProviderMap;
1303    }
1304
1305    /**
1306     * @param $data
1307     *
1308     * @throws common_Exception
1309     */
1310    private function convertDates(&$data): void
1311    {
1312        if (array_key_exists(self::DELIVERY_EXECUTION_STARTED_AT, $data)) {
1313            $startDate = current($data[self::DELIVERY_EXECUTION_STARTED_AT]);
1314            $data[self::DELIVERY_EXECUTION_STARTED_AT][0] = $startDate
1315                ? tao_helpers_Date::displayeDate($startDate)
1316                : '';
1317        }
1318
1319        if (array_key_exists(self::DELIVERY_EXECUTION_FINISHED_AT, $data)) {
1320            $endDate = current($data[self::DELIVERY_EXECUTION_FINISHED_AT]);
1321            $data[self::DELIVERY_EXECUTION_FINISHED_AT][0] = $endDate ? tao_helpers_Date::displayeDate($endDate) : '';
1322        }
1323    }
1324
1325    /**
1326     * Check that data is apply to these filter params
1327     *
1328     * @param $row
1329     * @param array $filters
1330     *
1331     * @return bool
1332     */
1333    private function filterData($row, array $filters, string $deliveryExecutionIdentifier)
1334    {
1335        $matched = true;
1336        if (!empty($filters) && !empty(array_intersect(self::PERIODS, array_keys($filters)))) {
1337            if (
1338                !array_key_exists(self::DELIVERY_EXECUTION_STARTED_AT, $row) ||
1339                !array_key_exists(self::DELIVERY_EXECUTION_FINISHED_AT, $row)
1340            ) {
1341                $deliveryExecutionService = $this->getDeliveryExecutionService();
1342                $deliveryExecution = $deliveryExecutionService->getDeliveryExecution($deliveryExecutionIdentifier);
1343                $startDate = $deliveryExecution->getStartTime();
1344                $endDate = $deliveryExecution->getFinishTime();
1345            } else {
1346                $startDate = current($row[self::DELIVERY_EXECUTION_STARTED_AT]);
1347                $endDate = current($row[self::DELIVERY_EXECUTION_FINISHED_AT]);
1348            }
1349
1350            $startTime = $startDate ? tao_helpers_Date::getTimeStamp($startDate) : 0;
1351            $endTime = $endDate ? tao_helpers_Date::getTimeStamp($endDate) : 0;
1352
1353            if (
1354                $matched
1355                && array_key_exists(self::FILTER_START_FROM, $filters) && $filters[self::FILTER_START_FROM]
1356            ) {
1357                $matched = $startTime >= $filters[self::FILTER_START_FROM];
1358            }
1359            if (
1360                $matched
1361                && array_key_exists(self::FILTER_START_TO, $filters) && $filters[self::FILTER_START_TO]
1362            ) {
1363                $matched = $startTime <= $filters[self::FILTER_START_TO];
1364            }
1365            if (
1366                $matched
1367                && array_key_exists(self::FILTER_END_FROM, $filters) && $filters[self::FILTER_END_FROM]
1368            ) {
1369                $matched = $endTime >= $filters[self::FILTER_END_FROM];
1370            }
1371            if ($matched && array_key_exists(self::FILTER_END_TO, $filters) && $filters[self::FILTER_END_TO]) {
1372                $matched = $endTime <= $filters[self::FILTER_END_TO];
1373            }
1374        }
1375
1376        return $matched;
1377    }
1378
1379    private function getDeliveryExecutionService(): DeliveryExecutionService
1380    {
1381        return $this->getServiceLocator()->get(DeliveryExecutionService::SERVICE_ID);
1382    }
1383
1384    /**
1385     * @param array $deliveryUris
1386     *
1387     * @return int
1388     * @throws common_exception_Error
1389     */
1390    public function countResultByDelivery(array $deliveryUris)
1391    {
1392        return $this->getImplementation()->countResultByDelivery($deliveryUris);
1393    }
1394
1395    /**
1396     * @param array|string $resultsIds
1397     *
1398     * @return mixed
1399     * @throws common_exception_Error
1400     */
1401    protected function getResultsVariables($resultsIds)
1402    {
1403        return $this->getImplementation()->getDeliveryVariables($resultsIds);
1404    }
1405
1406    /**
1407     * Retrieve the different variables columns pertainign to the current selection of results
1408     * Implementation note : it nalyses all the data collected to identify the different response variables submitted
1409     * by the items in the context of activities
1410     */
1411    public function getVariableColumns($delivery, $variableClassUri, array $filters = [], array $storageOptions = [])
1412    {
1413        $columns = [];
1414        /** @var ResultServiceWrapper $resultServiceWrapper */
1415        $resultServiceWrapper = $this->getServiceLocator()->get(ResultServiceWrapper::SERVICE_ID);
1416
1417        $this->setImplementation($this->getReadableImplementation($delivery));
1418        //The list of delivery Results matching the current selection filters
1419        $resultsIds = $this->findResultsByDeliveryAndFilters($delivery, $filters, $storageOptions);
1420
1421        $columnsType = $variableClassUri === \taoResultServer_models_classes_OutcomeVariable::class
1422            ? GradeColumn::class
1423            : ResponseColumn::class;
1424        foreach (
1425            array_chunk(
1426                $resultsIds,
1427                $resultServiceWrapper->getOption(ResultServiceWrapper::RESULT_COLUMNS_CHUNK_SIZE_OPTION)
1428            ) as $resultsIdsItem
1429        ) {
1430            $selectedVariables = $this->getResultsVariables($resultsIdsItem);
1431            foreach ($selectedVariables as $variable) {
1432                $refId = null;
1433                $variable = $variable[0];
1434                if ($this->isResultVariable($variable, $variableClassUri)) {
1435                    //variableIdentifier
1436                    $variableIdentifier = $variable->variable->getIdentifier();
1437                    if (!is_null($variable->item)) {
1438                        $uri = $variable->item;
1439                        if (
1440                            !$this->getItemResultStrategy()->isItemEntityBased()
1441                            && $variable->callIdItem !== null
1442                            && strpos($variable->callIdItem, $variable->deliveryResultIdentifier) !== false
1443                        ) {
1444                            [$refId, $occurrence] = explode(
1445                                '.',
1446                                substr($variable->callIdItem, strlen($variable->deliveryResultIdentifier . '.'))
1447                            );
1448                        }
1449                        $contextIdentifierLabel = $this->findItemLabel($delivery, $uri) ?? $uri;
1450                    } else {
1451                        $uri = $variable->test;
1452                        $testData = $this->getTestMetadata($delivery, $variable->test);
1453                        $contextIdentifierLabel = $testData->getLabel() ?? $delivery->getLabel();
1454                    }
1455
1456                    $columnType = $this->defineTypeColumn($variable->variable);
1457
1458                    $column = \tao_models_classes_table_Column::buildColumnFromArray([
1459                        'type' => $columnsType,
1460                        "contextLabel" => $contextIdentifierLabel,
1461                        "contextId" => $uri,
1462                        'refId' => $refId ?? null,
1463                        "variableIdentifier" => $variableIdentifier,
1464                        "columnType" => $columnType
1465                    ]);
1466                    $columns[$this->getColumnIdProvider()->provide($column)] = $column;
1467
1468                    if (
1469                        $variable->variable instanceof \taoResultServer_models_classes_ResponseVariable
1470                        && $variable->variable->getCorrectResponse() !== null
1471                    ) {
1472                        $columnCorrect = \tao_models_classes_table_Column::buildColumnFromArray([
1473                            'type' => $columnsType,
1474                            "contextLabel" => $contextIdentifierLabel,
1475                            "contextId" => $uri,
1476                            'refId' => $refId ?? null,
1477                            "variableIdentifier" => $variableIdentifier . '_is_correct',
1478                            "columnType" => Variable::TYPE_VARIABLE_IDENTIFIER
1479                        ]);
1480                        $columns[$this->getColumnIdProvider()->provide($columnCorrect)] = $columnCorrect;
1481                    }
1482                }
1483            }
1484        }
1485
1486        return array_values(
1487            array_map(fn (\tao_models_classes_table_Column $column) => $column->toArray(), $columns)
1488        );
1489    }
1490
1491    /**
1492     * Check if provided variable is a result variable.
1493     *
1494     * @param $variable
1495     * @param $variableClassUri
1496     *
1497     * @return bool
1498     */
1499    private function isResultVariable($variable, $variableClassUri)
1500    {
1501        $responseVariableClass = \taoResultServer_models_classes_ResponseVariable::class;
1502        $outcomeVariableClass = \taoResultServer_models_classes_OutcomeVariable::class;
1503        $class = isset($variable->class) ? $variable->class : get_class($variable->variable);
1504
1505        return (null != $variable->item || null != $variable->test)
1506            && (
1507                $class == $outcomeVariableClass
1508                && $variableClassUri == $outcomeVariableClass
1509            ) || (
1510                $class == $responseVariableClass
1511                && $variableClassUri == $responseVariableClass
1512            );
1513    }
1514
1515    /**
1516     * @param Variable $variable
1517     * @return string|null
1518     */
1519    private function defineTypeColumn(Variable $variable)
1520    {
1521        $stringColumns = [
1522            'SCORE',
1523            'MAXSCORE',
1524            'numAttempts',
1525            'duration'
1526        ];
1527
1528        if (
1529            in_array($variable->getIdentifier(), $stringColumns)
1530            || (
1531                $variable instanceof \taoResultServer_models_classes_ResponseVariable
1532                && $variable->getCorrectResponse() !== null
1533            )
1534        ) {
1535            return Variable::TYPE_VARIABLE_IDENTIFIER;
1536        }
1537
1538        return $variable->getBaseType();
1539    }
1540
1541    /**
1542     * Sort the list of variables by filters
1543     *
1544     * List of variables contains the response for an interaction.
1545     * Each attempts is an entry in $observationList
1546     *
1547     * 3 allowed filters: firstSubmitted, lastSubmitted, all, trace
1548     *
1549     * @param array $observationsList The list of variable values
1550     * @param string $filterData The filter
1551     * @param string $allDelimiter $delimiter to separate values in "all" filter context
1552     *
1553     * @return array
1554     */
1555    public static function filterCellData($observationsList, $filterData, $allDelimiter = '|')
1556    {
1557        //if the cell content is not an array with multiple entries, do not filter
1558        if (!is_array($observationsList)) {
1559            return $observationsList;
1560        }
1561
1562        // Sort by TimeStamps
1563        uksort($observationsList, "oat\\taoOutcomeUi\\model\\ResultsService::sortTimeStamps");
1564
1565        // Extract the value to make this array flat
1566        $observationsList = array_map(function ($obs) {
1567            return $obs[0];
1568        }, $observationsList);
1569
1570        switch ($filterData) {
1571            case self::VARIABLES_FILTER_LAST_SUBMITTED:
1572                $value = array_pop($observationsList);
1573                break;
1574
1575            case self::VARIABLES_FILTER_FIRST_SUBMITTED:
1576                $value = array_shift($observationsList);
1577                break;
1578
1579            case self::VARIABLES_FILTER_TRACE:
1580                $value = json_encode(array_map(function ($value) {
1581                    return json_decode($value, true);
1582                }, array_values($observationsList)));
1583                break;
1584
1585            case self::VARIABLES_FILTER_ALL:
1586            default:
1587                $value = implode($allDelimiter, $observationsList);
1588                break;
1589        }
1590
1591        return [$value];
1592    }
1593
1594    private function findItemLabel(core_kernel_classes_Resource $delivery, $itemUri)
1595    {
1596        // item label can be found in ItemCompilerIndex for locally published deliveries
1597        try {
1598            $itemIndexer = $this->getItemIndexer($delivery);
1599
1600            return $itemIndexer->getItemValue($itemUri, $this->getResultLanguage(), 'label');
1601        } catch (common_exception_NoContent $e) {
1602        }
1603
1604        // in case of a remote delivery, item label might be found from originating test
1605        // @TODO local test might have been deleted in the meantime, https://oat-sa.atlassian.net/browse/TR-3111
1606        $deliveryAssemblyService = $this->getServiceLocator()->get(DeliveryAssemblyService::class);
1607        $testService = $this->getServiceLocator()->get(TestsService::class);
1608
1609        $test = $deliveryAssemblyService->getOrigin($delivery);
1610        /** @var core_kernel_classes_Resource[] $items */
1611        $items = $testService->getTestItems($test);
1612
1613        return isset($items[$itemUri]) ? $items[$itemUri]->getLabel() : null;
1614    }
1615
1616    /**
1617     * @param $delivery
1618     *
1619     * @return ItemCompilerIndex
1620     * @throws common_exception_Error
1621     */
1622    private function getItemIndexer($delivery)
1623    {
1624        $deliveryUri = $delivery->getUri();
1625        if (!array_key_exists($deliveryUri, $this->indexerCache)) {
1626            $directory = $this->getPrivateDirectory($delivery);
1627            $indexer = $this->getDecompiledIndexer($directory);
1628            $this->indexerCache[$deliveryUri] = $indexer;
1629        }
1630        $indexer = $this->indexerCache[$deliveryUri];
1631
1632        return $indexer;
1633    }
1634
1635    /**
1636     * @param $delivery
1637     * @param $testUri
1638     *
1639     * @return ResourceCompiledMetadataHelper
1640     */
1641    private function getTestMetadata(core_kernel_classes_Resource $delivery, $testUri)
1642    {
1643        if (isset($this->testMetadataCache[$testUri])) {
1644            return $this->testMetadataCache[$testUri];
1645        }
1646
1647        $compiledMetadataHelper = new ResourceCompiledMetadataHelper();
1648
1649        try {
1650            $directory = $this->getPrivateDirectory($delivery);
1651            $testMetadata = $this->loadTestMetadata($directory, $testUri);
1652            if (!empty($testMetadata)) {
1653                $compiledMetadataHelper->unserialize($testMetadata);
1654            }
1655        } catch (\Exception $e) {
1656            \common_Logger::d('Ignoring data not found exception for Test Metadata');
1657        }
1658
1659        $this->testMetadataCache[$testUri] = $compiledMetadataHelper;
1660
1661        return $this->testMetadataCache[$testUri];
1662    }
1663
1664    /**
1665     * Load test metadata from file. For deliveries without compiled file try  to compile test metadata.
1666     *
1667     * @param tao_models_classes_service_StorageDirectory $directory
1668     * @param string $testUri
1669     *
1670     * @return false|string
1671     * @throws \FileNotFoundException
1672     * @throws common_Exception
1673     */
1674    private function loadTestMetadata(tao_models_classes_service_StorageDirectory $directory, $testUri)
1675    {
1676        try {
1677            $testMetadata = $this->loadTestMetadataFromFile($directory);
1678        } catch (FileNotFoundException $e) {
1679            \common_Logger::d('Compiled test metadata file not found. Try to compile a new file.');
1680
1681            $this->compileTestMetadata($directory, $testUri);
1682            $testMetadata = $this->loadTestMetadataFromFile($directory);
1683        }
1684
1685        return $testMetadata;
1686    }
1687
1688    /**
1689     * Get teast metadata from file.
1690     *
1691     * @param tao_models_classes_service_StorageDirectory $directory
1692     *
1693     * @return false|string
1694     */
1695    private function loadTestMetadataFromFile(tao_models_classes_service_StorageDirectory $directory)
1696    {
1697        return $directory->read(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_METADATA_FILENAME);
1698    }
1699
1700    /**
1701     * Compile test metadata and store into file.
1702     * Added for backward compatibility for deliveries without compiled test metadata.
1703     *
1704     * @param tao_models_classes_service_StorageDirectory $directory
1705     * @param $testUri
1706     *
1707     * @throws \FileNotFoundException
1708     * @throws common_Exception
1709     */
1710    private function compileTestMetadata(tao_models_classes_service_StorageDirectory $directory, $testUri)
1711    {
1712        $resource = $this->getResource($testUri);
1713
1714        /** @var ResourceMetadataCompilerInterface $resourceMetadataCompiler */
1715        $resourceMetadataCompiler = $this->getServiceLocator()->get(ResourceJsonMetadataCompiler::SERVICE_ID);
1716        $metadata = $resourceMetadataCompiler->compile($resource);
1717
1718        $directory->write(
1719            taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_METADATA_FILENAME,
1720            json_encode($metadata)
1721        );
1722    }
1723
1724    /**
1725     * @param string $directory
1726     *
1727     * @return QtiTestCompilerIndex
1728     */
1729    private function getDecompiledIndexer(tao_models_classes_service_StorageDirectory $directory)
1730    {
1731        $itemIndex = new QtiTestCompilerIndex();
1732        try {
1733            $data = $directory->read(taoQtiTest_models_classes_QtiTestService::TEST_COMPILED_INDEX);
1734            if ($data) {
1735                $itemIndex->unserialize($data);
1736            }
1737        } catch (\Exception $e) {
1738            \common_Logger::d('Ignoring file not found exception for Items Index');
1739        }
1740
1741        return $itemIndex;
1742    }
1743
1744    /**
1745     * Should be changed if real result language would matter
1746     *
1747     * @return string
1748     */
1749    private function getResultLanguage()
1750    {
1751        return DEFAULT_LANG;
1752    }
1753
1754    /**
1755     * @param $executionUri
1756     *
1757     * @return core_kernel_classes_Resource
1758     * @throws \common_exception_NotFound
1759     */
1760    private function getDeliveryByResultId($executionUri)
1761    {
1762        if (!array_key_exists($executionUri, $this->executionCache)) {
1763            /** @var DeliveryExecution $execution */
1764            $execution = $this->getServiceManager()->get(ServiceProxy::class)->getDeliveryExecution($executionUri);
1765            $delivery = $execution->getDelivery();
1766            $this->executionCache[$executionUri] = $delivery;
1767        }
1768        $delivery = $this->executionCache[$executionUri];
1769
1770        return $delivery;
1771    }
1772
1773    /**
1774     * @param $delivery
1775     *
1776     * @return array
1777     * @throws common_exception_Error
1778     */
1779    private function getDirectoryIds($delivery)
1780    {
1781        $runtime = $this->getServiceLocator()->get(RuntimeService::SERVICE_ID)->getRuntime($delivery);
1782        $inputParameters = \tao_models_classes_service_ServiceCallHelper::getInputValues($runtime, []);
1783        $directoryIds = explode('|', $inputParameters['QtiTestCompilation']);
1784
1785        return $directoryIds;
1786    }
1787
1788    /**
1789     * @param $delivery
1790     *
1791     * @return tao_models_classes_service_StorageDirectory
1792     * @throws common_exception_Error
1793     */
1794    private function getPrivateDirectory($delivery)
1795    {
1796        $directoryIds = $this->getDirectoryIds($delivery);
1797        $fileStorage = \tao_models_classes_service_FileStorage::singleton();
1798
1799        return $fileStorage->getDirectoryById($directoryIds[0]);
1800    }
1801
1802    /**
1803     * @return mixed
1804     */
1805    public function getServiceLocator()
1806    {
1807        if (!$this->serviceLocator) {
1808            $this->setServiceLocator(ServiceManager::getServiceManager());
1809        }
1810
1811        return $this->serviceLocator;
1812    }
1813
1814    /**
1815     * @param core_kernel_classes_Resource $delivery
1816     * @param array $filters
1817     * @param array $storageOptions
1818     *
1819     * @return array
1820     * @throws common_exception_Error
1821     */
1822    protected function findResultsByDeliveryAndFilters($delivery, array $filters = [], array $storageOptions = [])
1823    {
1824        $results = [];
1825        foreach ($this->getImplementation()->getResultByDelivery([$delivery->getUri()], $storageOptions) as $result) {
1826            $results[] = $result['deliveryResultIdentifier'];
1827        }
1828
1829        return $results;
1830    }
1831
1832    private function getColumnIdProvider(): ColumnIdProvider
1833    {
1834        return $this->getServiceLocator()->getContainer()->get(ColumnIdProvider::class);
1835    }
1836
1837    private function getColumnLabelProvider(): ColumnLabelProvider
1838    {
1839        return $this->getServiceLocator()->getContainer()->get(ColumnLabelProvider::class);
1840    }
1841
1842    private function getItemResultStrategy(): ItemResultStrategy
1843    {
1844        return $this->getServiceLocator()->getContainer()->get(ItemResultStrategy::class);
1845    }
1846}