Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
21.29% covered (danger)
21.29%
33 / 155
10.00% covered (danger)
10.00%
3 / 30
CRAP
0.00% covered (danger)
0.00%
0 / 1
taoAltResultStorage_models_classes_KeyValueResultStorage
21.29% covered (danger)
21.29%
33 / 155
10.00% covered (danger)
10.00%
3 / 30
1936.42
0.00% covered (danger)
0.00%
0 / 1
 getPersistence
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 storeVariableKeyValue
68.42% covered (warning)
68.42%
13 / 19
0.00% covered (danger)
0.00%
0 / 1
3.28
 spawnResult
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 storeTestVariable
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 storeTestVariables
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 configure
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 storeRelatedTestTaker
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 storeRelatedDelivery
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 storeItemVariable
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 storeItemVariables
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 getVariables
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
5.09
 getDeliveryVariables
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
30
 getVariable
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 getTestTaker
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getDelivery
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 getTestTakerArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDeliveryArray
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAllCallIds
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getAllTestTakerIds
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getAllDeliveryIds
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 subStrPrefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVariableProperty
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
6
 extractResultVariableProperty
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getRelatedItemCallIds
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getRelatedTestCallIds
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 getResultByDelivery
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
 countResultByDelivery
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 deleteResult
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
56
 unserializeVariableValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 serializeVariableValue
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-2017 (original work) Open Assessment Technologies SA (under the project TAO-PRODUCT);
19 */
20
21use oat\oatbox\service\ConfigurableService;
22use oat\taoResultServer\models\classes\ResultDeliveryExecutionDelete;
23use oat\taoResultServer\models\classes\ResultManagement;
24use oat\taoResultServer\models\Entity\ItemVariableStorable;
25use oat\taoResultServer\models\Entity\TestVariableStorable;
26use oat\taoResultServer\models\Entity\VariableStorable;
27use oat\taoResultServer\models\Exceptions\DuplicateVariableException;
28
29/**
30 * Implements tao results storage using the configured persistency "taoAltResultStorage"
31 *
32 * The storage is done on a callId basis (the key). retrieval of all variables pertainign to a callid
33 * is done using get or hget for aparticular variable 0(1)
34 * The jsondata contains all the observations recorderd with the variable data + context
35 *
36 * keyPrefixCallId.$callId =>
37 * (field)variableIdentifier : json data ,
38 * (field)variableIdentifier : json data,
39 * ...
40 * }
41 *
42 * phpcs:disable Squiz.Classes.ValidClassName,PSR1.Classes.ClassDeclaration
43 */
44class taoAltResultStorage_models_classes_KeyValueResultStorage extends ConfigurableService implements
45    taoResultServer_models_classes_WritableResultStorage,
46    ResultManagement
47{
48    use ResultDeliveryExecutionDelete;
49
50    public const SERVICE_ID = 'taoAltResultStorage/KeyValueResultStorage';
51
52    /** result storage persistence identifier */
53    public const OPTION_PERSISTENCE = 'persistence_id';
54
55    // prefixes used for keys
56    // keyPrefixCallId.$callId --> variables
57    public const PREFIX_CALL_ID = 'taoAltResultStorage:callIdVariables';
58    // keyPrefixTestTaker.$deliveryResultIdentifier -->testtaker
59    public const PREFIX_TESTTAKER = 'taoAltResultStorage:resultsTestTaker';
60    // keyPrefixDelivery.$deliveryResultIdentifier -->testtaker
61    public const PREFIX_DELIVERY = 'taoAltResultStorage:resultsDelivery';
62    public const PREFIX_RESULT_ID = 'taoAltResultStorage:id';
63
64    /**
65     * Property separator string.
66     */
67    public const PROPERTY_SEPARATOR = '_prop_';
68
69    /**
70     * @var common_persistence_AdvKeyValuePersistence
71     */
72    private $persistence;
73
74    /**
75     * Initialise the persistence and return it
76     *
77     * @return common_persistence_AdvKeyValuePersistence
78     */
79    private function getPersistence()
80    {
81        if (is_null($this->persistence)) {
82            $perisistenceManager = $this->getServiceLocator()->get(common_persistence_Manager::SERVICE_ID);
83            $this->persistence = $perisistenceManager->getPersistenceById($this->getOption(self::OPTION_PERSISTENCE));
84        }
85
86        return $this->persistence;
87    }
88
89    /**
90     * @param string $callId
91     * @param string $variableIdentifier
92     * @param VariableStorable $data
93     *                               the actual variable-value object,
94     */
95    private function storeVariableKeyValue($callId, $variableIdentifier, $data)
96    {
97        $callId = self::PREFIX_CALL_ID . $callId;
98
99        /*
100         * Seems to be the same complexity, worse if not yet value set for that key to be benchmarked against the
101         * general case only
102         */
103        // Time complexity: O(1)
104        $observed = $this->getPersistence()->hExists($callId, $variableIdentifier);
105        $serializedData = $this->serializeVariableValue([$data]);
106
107        if (!$observed) {
108            // Time complexity: O(1)
109            $this->getPersistence()->hSet($callId, $variableIdentifier, $serializedData);
110        } else {
111            // Time complexity: O(1)
112            $variableObservations = $this->unserializeVariableValue(
113                $this->getPersistence()->hGet($callId, $variableIdentifier)
114            );
115            $unserializedData = $this->unserializeVariableValue($serializedData);
116
117            if (in_array($unserializedData[0], $variableObservations, false)) {
118                throw new DuplicateVariableException(
119                    sprintf('An identical result variable already exists. CallId:%s', $callId)
120                );
121            }
122
123            $variableObservations[] = $data;
124            // Time complexity: O(1)
125            $this->getPersistence()->hSet(
126                $callId,
127                $variableIdentifier,
128                $this->serializeVariableValue($variableObservations)
129            );
130        }
131    }
132
133    /**
134     * Ids must be delegated on key value persistency as we may want to load balance and keep unique identifier
135     */
136    public function spawnResult()
137    {
138        return 'id_' . $this->getPersistence()->incr(self::PREFIX_RESULT_ID);
139    }
140
141    /**
142     * @param type $deliveryResultIdentifier
143     *                                       lis_result_sourcedid
144     * @param type $test
145     *                   ignored
146     * @param taoResultServer_models_classes_Variable $testVariable
147     * @param type $callIdTest
148     *                         ignored
149     */
150    public function storeTestVariable(
151        $deliveryResultIdentifier,
152        $test,
153        taoResultServer_models_classes_Variable $testVariable,
154        $callIdTest
155    ) {
156        if (! ($testVariable->isSetEpoch())) {
157            $testVariable->setEpoch(microtime());
158        }
159
160        $variable = new TestVariableStorable($deliveryResultIdentifier, $test, $testVariable, $callIdTest);
161
162        $this->storeVariableKeyValue($callIdTest, $variable->getIdentifier(), $variable);
163    }
164
165    /**
166     * @param $deliveryResultIdentifier
167     * @param $test
168     * @param array $testVariables
169     * @param $callIdTest
170     */
171    public function storeTestVariables($deliveryResultIdentifier, $test, array $testVariables, $callIdTest)
172    {
173        foreach ($testVariables as $testVariable) {
174            $this->storeTestVariable($deliveryResultIdentifier, $test, $testVariable, $callIdTest);
175        }
176    }
177
178    /*
179     * retrieve specific parameters from the resultserver to configure the storage
180     */
181    /*sic*/
182    public function configure($callOptions = [])
183    {
184    }
185
186    public function storeRelatedTestTaker($deliveryResultIdentifier, $testTakerIdentifier)
187    {
188        $this->getPersistence()->hmSet(self::PREFIX_TESTTAKER . $deliveryResultIdentifier, [
189            'deliveryResultIdentifier' => $deliveryResultIdentifier,
190            'testTakerIdentifier' => $testTakerIdentifier,
191        ]);
192    }
193
194    public function storeRelatedDelivery($deliveryResultIdentifier, $deliveryIdentifier)
195    {
196        $this->getPersistence()->hmSet(self::PREFIX_DELIVERY . $deliveryResultIdentifier, [
197            'deliveryResultIdentifier' => $deliveryResultIdentifier,
198            'deliveryIdentifier' => $deliveryIdentifier,
199        ]);
200    }
201
202    public function storeItemVariable(
203        $deliveryResultIdentifier,
204        $test,
205        $item,
206        taoResultServer_models_classes_Variable $itemVariable,
207        $callIdItem
208    ) {
209        if (! ($itemVariable->isSetEpoch())) {
210            $itemVariable->setEpoch(microtime());
211        }
212
213        $variable = new ItemVariableStorable($deliveryResultIdentifier, $test, $itemVariable, $item, $callIdItem);
214
215        $this->storeVariableKeyValue($callIdItem, $variable->getIdentifier(), $variable);
216    }
217
218    public function storeItemVariables($deliveryResultIdentifier, $test, $item, array $itemVariables, $callIdItem)
219    {
220        foreach ($itemVariables as $itemVariable) {
221            $this->storeItemVariable($deliveryResultIdentifier, $test, $item, $itemVariable, $callIdItem);
222        }
223    }
224
225    /**
226     * @param string|array one or more callIds (item execution identifier)
227     * @param mixed $callId
228     *
229     * @return array keys as variableIdentifier , values is an array of observations ,
230     *               each observation is an object with deliveryResultIdentifier, test,
231     *               taoResultServer_models_classes_Variable variable, callIdTest
232     *               Array
233    (
234    [LtiOutcome] => Array
235        (
236            [0] => stdClass Object
237                (
238                    [deliveryResultIdentifier] => con-777:::rlid-777:::777777
239                    [test] => http://tao26/tao26.rdf#i1402389674744647
240                    [variable] => taoResultServer_models_classes_OutcomeVariable Object
241                        (
242                            [normalMaximum] =>
243                            [normalMinimum] =>
244                            [value] => MC41
245                            [identifier] => LtiOutcome
246                            [cardinality] => single
247                            [baseType] => float
248                            [epoch] => 0.10037600 1402390997
249                        )
250                    [callIdTest] => http://tao26/tao26.rdf#i14023907995907103
251                )
252
253        )
254
255    )
256     */
257    public function getVariables($callId)
258    {
259        $variables = [];
260
261        if (is_array($callId)) {
262            foreach ($callId as $id) {
263                $variables = array_merge($variables, $this->getVariables($id));
264            }
265        } else {
266            $tmpVariables = $this->getPersistence()->hGetAll(self::PREFIX_CALL_ID . $callId);
267
268            foreach ($tmpVariables as $variableIdentifier => $variableObservations) {
269                $observations = $this->unserializeVariableValue($variableObservations);
270
271                foreach ($observations as $key => $observation) {
272                    $observation->variable = unserialize($observation->variable);
273                    $observation->uri .= static::PROPERTY_SEPARATOR . $observation->variable->getIdentifier();
274                }
275
276                $variables[$callId . $variableIdentifier] = $observations;
277            }
278
279            unset($tmpVariables);
280        }
281
282        return $variables;
283    }
284
285    /**
286     * @param string|array $deliveryResultIdentifier
287     *
288     * @return array
289     */
290    public function getDeliveryVariables($deliveryResultIdentifier)
291    {
292        $variables = [];
293
294        if (is_array($deliveryResultIdentifier)) {
295            $deliveryVariables = [];
296
297            foreach ($deliveryResultIdentifier as $id) {
298                $deliveryVariables[] = $this->getDeliveryVariables($id);
299            }
300            $variables = array_merge(...$deliveryVariables);
301        } else {
302            $keys = $this->getPersistence()->keys(self::PREFIX_CALL_ID . $deliveryResultIdentifier . '.*');
303
304            foreach ($keys as $key) {
305                $prefixedVariables = $this->getVariables(str_replace(self::PREFIX_CALL_ID, '', $key));
306
307                foreach ($prefixedVariables as $varId => $variable) {
308                    $variables[$variable[0]->uri . $varId] = $variable;
309                }
310            }
311        }
312
313        return $variables;
314    }
315
316    public function getVariable($callId, $variableIdentifier)
317    {
318        $observations = $this->unserializeVariableValue(
319            $this->getPersistence()->hGet(self::PREFIX_CALL_ID . $callId, $variableIdentifier)
320        );
321
322        foreach ($observations as $key => $observation) {
323            $observation->variable = unserialize($observation->variable);
324            $observations[$key] = $observation;
325        }
326
327        return  $observations;
328    }
329
330    public function getTestTaker($deliveryResultIdentifier)
331    {
332        $testTaker = $this->getTestTakerArray($deliveryResultIdentifier);
333
334        return $testTaker['testTakerIdentifier'];
335    }
336
337    public function getDelivery($deliveryResultIdentifier)
338    {
339        $delivery = $this->getDeliveryArray($deliveryResultIdentifier);
340
341        return $delivery['deliveryIdentifier'];
342    }
343
344    public function getTestTakerArray($deliveryResultIdentifier)
345    {
346        return $this->getPersistence()->hGetAll(self::PREFIX_TESTTAKER . $deliveryResultIdentifier);
347    }
348
349    public function getDeliveryArray($deliveryResultIdentifier)
350    {
351        return $this->getPersistence()->hGetAll(self::PREFIX_DELIVERY . $deliveryResultIdentifier);
352    }
353
354    /**
355     * @return array the list of item executions ids (across all results)
356     *               o(n) do not use real time (postprocessing)
357     */
358
359    public function getAllCallIds()
360    {
361        $keys = $this->getPersistence()->keys(self::PREFIX_CALL_ID . '*');
362        array_walk($keys, 'self::subStrPrefix', self::PREFIX_CALL_ID);
363
364        return $keys;
365    }
366    /**
367     * @return array each element is a two fields array deliveryResultIdentifier, testTakerIdentifier
368     */
369    public function getAllTestTakerIds()
370    {
371        $deliveryResults = [];
372        $keys = $this->getPersistence()->keys(self::PREFIX_TESTTAKER . '*');
373        array_walk($keys, 'self::subStrPrefix', self::PREFIX_TESTTAKER);
374
375        foreach ($keys as $key) {
376            $deliveryResults[$key] = $this->getTestTakerArray($key);
377        }
378
379        return $deliveryResults;
380    }
381    /**
382     * @return array each element is a two fields array deliveryResultIdentifier, deliveryIdentifier
383     */
384    public function getAllDeliveryIds()
385    {
386        $deliveryResults = [];
387        $keys = $this->getPersistence()->keys(self::PREFIX_DELIVERY . '*');
388        array_walk($keys, 'self::subStrPrefix', self::PREFIX_DELIVERY);
389
390        foreach ($keys as $key) {
391            $deliveryResults[$key] = $this->getDeliveryArray($key);
392        }
393
394        return $deliveryResults;
395    }
396
397    /**
398     * helper
399     *
400     * @param mixed $value
401     * @param mixed $key
402     * @param mixed $prefix
403     */
404    private function subStrPrefix(&$value, $key, $prefix)
405    {
406        $value = str_replace($prefix, '', $value);
407    }
408
409    /**
410     * Get only one property from a variable
411     *
412     * @param string $variableId on which we want the property
413     * @param string $property to retrieve
414     *
415     * @return int|string the property retrieved
416     */
417    public function getVariableProperty($variableId, $property)
418    {
419        list($itemUri, $propertyName) = $this->extractResultVariableProperty($variableId);
420        $response = $this->unserializeVariableValue(
421            $this->getPersistence()->hGet(
422                self::PREFIX_CALL_ID . $itemUri,
423                $propertyName
424            )
425        );
426        $variable = unserialize($response[0]->variable);
427
428        $getter = 'get' . ucfirst($property);
429
430        if (method_exists($variable, $getter)) {
431            return $variable->$getter();
432        }
433
434        return '';
435    }
436
437    /**
438     * Returns the variable property key from the absolute variable key.
439     *
440     * @param string $variableId
441     *
442     * @return array
443     */
444    public function extractResultVariableProperty($variableId)
445    {
446        $variableIds = explode('http://', $variableId);
447        $parts = explode(static::PROPERTY_SEPARATOR, $variableIds[2]);
448
449        $itemUri = $variableIds[0] . 'http://' . $parts[0];
450        $propertyName = empty($parts[1]) ? 'RESPONSE' : $parts[1];
451
452        return [
453            $itemUri,
454            $propertyName,
455        ];
456    }
457
458    /**
459     * @todo Only works for QTI Tests, fix this in a more generic way
460     *
461     * (non-PHPdoc)
462     *
463     * @see \oat\taoResultServer\models\classes\ResultManagement::getRelatedItemCallIds()
464     *
465     * @param mixed $deliveryResultIdentifier
466     */
467    public function getRelatedItemCallIds($deliveryResultIdentifier)
468    {
469        $keys = $this->getPersistence()->keys(self::PREFIX_CALL_ID . $deliveryResultIdentifier . '.*');
470        array_walk($keys, 'self::subStrPrefix', self::PREFIX_CALL_ID);
471
472        return $keys;
473    }
474
475    /**
476     * @todo Only works for QTI Tests, fix this in a more generic way
477     *
478     * (non-PHPdoc)
479     *
480     * @see \oat\taoResultServer\models\classes\ResultManagement::getRelatedTestCallIds()
481     *
482     * @param mixed $deliveryResultIdentifier
483     */
484    public function getRelatedTestCallIds($deliveryResultIdentifier)
485    {
486        $keys = $this->getPersistence()->keys(self::PREFIX_CALL_ID . $deliveryResultIdentifier);
487        array_walk($keys, 'self::subStrPrefix', self::PREFIX_CALL_ID);
488
489        return $keys;
490    }
491
492    public function getResultByDelivery($delivery, $options = [])
493    {
494        $returnValue = [];
495        $keys = $this->getPersistence()->keys(self::PREFIX_DELIVERY . '*');
496        array_walk($keys, 'self::subStrPrefix', self::PREFIX_DELIVERY);
497
498        foreach ($keys as $key) {
499            $deliveryExecution = $this->getDelivery($key);
500
501            if (empty($delivery) || in_array($deliveryExecution, $delivery)) {
502                $returnValue[] = [
503                    'deliveryResultIdentifier' => $key,
504                    'testTakerIdentifier' => $this->getTestTaker($key),
505                    'deliveryIdentifier' => $deliveryExecution,
506                ];
507            }
508        }
509
510        return $returnValue;
511    }
512
513    public function countResultByDelivery($delivery)
514    {
515        $count = 0;
516        $keys = $this->getPersistence()->keys(self::PREFIX_DELIVERY . '*');
517        array_walk($keys, 'self::subStrPrefix', self::PREFIX_DELIVERY);
518
519        foreach ($keys as $key) {
520            $deliveryExecution = $this->getDelivery($key);
521
522            if (empty($delivery) || in_array($deliveryExecution, $delivery)) {
523                $count++;
524            }
525        }
526
527        return $count;
528    }
529
530    /**
531     * Remove the result and all the related variables
532     *
533     * @param string $deliveryResultIdentifier The identifier of the delivery execution
534     *
535     * @return boolean if the deletion was successful or not
536     */
537    public function deleteResult($deliveryResultIdentifier)
538    {
539        $return = true;
540
541        foreach ($this->getRelatedTestCallIds($deliveryResultIdentifier) as $key) {
542            $return = $return && $this->getPersistence()->del(self::PREFIX_CALL_ID . $key);
543        }
544
545        foreach ($this->getRelatedItemCallIds($deliveryResultIdentifier) as $key) {
546            $return = $return && $this->getPersistence()->del(self::PREFIX_CALL_ID . $key);
547        }
548        $return = $return && $this->getPersistence()->del(self::PREFIX_DELIVERY . $deliveryResultIdentifier);
549        $return = $return && $this->getPersistence()->del(self::PREFIX_TESTTAKER . $deliveryResultIdentifier);
550
551        return $return;
552    }
553
554    /**
555     * @param $value
556     *
557     * @return mixed
558     */
559    protected function unserializeVariableValue($value)
560    {
561        return json_decode($value);
562    }
563
564    /**
565     * @param $value
566     *
567     * @return string
568     */
569    protected function serializeVariableValue($value)
570    {
571        return json_encode($value);
572    }
573}