Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
QtiTimeStoragePackedFormat
0.00% covered (danger)
0.00%
0 / 77
0.00% covered (danger)
0.00%
0 / 7
756
0.00% covered (danger)
0.00%
0 / 1
 getFormat
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getVersion
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getEpoch
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 packTimeLine
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
42
 unpackTimeLine
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
 encode
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 decode
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
72
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2017 (original work) Open Assessment Technologies SA ;
19 *
20 */
21
22namespace oat\taoQtiTest\models\runner\time\storageFormat;
23
24use oat\taoQtiTest\models\runner\time\AdjustmentMap;
25use oat\taoQtiTest\models\runner\time\QtiTimeLine;
26use oat\taoTests\models\runner\time\TimeLine;
27use oat\taoTests\models\runner\time\TimePoint;
28use oat\taoTests\models\runner\time\TimerAdjustmentMapInterface;
29
30/**
31 * Class QtiTimeStoragePackedFormat
32 *
33 * Encode/decode QtiTimer using JSON format.
34 * Apply a compression on the QtiTimeLine entries to reduce the weight of the encoded data.
35 *
36 * Applied format:
37 *
38 * ```
39 * {
40 *      // For each tags set an index that points to each linked timestamps.
41 *      // Each index value is the offset of the timestamp in the "points" array.
42 *      "index": {
43 *          "tag1": [0, 1, 2, 6, ...],
44 *          "tag2": [0, 2, 4, ...],
45 *          ...
46 *          "tagN": [100, 101, 110, 111, ...],
47 *      },
48 *
49 *      // A list of tags that applies for all timestamps.
50 *      "tags": [
51 *          "global-tag-1",
52 *          "global-tag-2",
53 *          ...
54 *          "global-tag-N",
55 *      ],
56 *
57 *      // A flat list of timestamps, their tags are moved into the index map.
58 *      // Each timestamp is reduced by a reference value to minimize the final size.
59 *      "points": [
60 *          [target, type, timestamp - todayTimestamp],
61 *          [target, type, timestamp - todayTimestamp],
62 *          ...
63 *          [target, type, timestamp - todayTimestamp]
64 *      ],
65 *
66 *      // The arbitrary reference value that has been used to reduce the timestamps.
67 *      // Usually it is the timestamp of today morning at 00:00.
68 *      // (could be improved to be based on the assessment start time)
69 *      "epoch": todayTimestamp
70 * }
71 * ```
72 *
73 * @package oat\taoQtiTest\models\runner\time\storageFormat
74 * @author Jean-Sébastien Conan <jean-sebastien@taotesting.com>
75 */
76class QtiTimeStoragePackedFormat extends QtiTimeStorageJsonFormat
77{
78    use QtiTimeStorageObjectDecodingTrait;
79
80    /**
81     * A storage key added to the sored data set to keep format type
82     */
83    public const STORAGE_KEY_FORMAT = 'format';
84
85    /**
86     * A storage key added to the sored data set to keep version info
87     */
88    public const STORAGE_KEY_VERSION = 'version';
89
90    /**
91     * The storage key for the TimeLine index in the packed format
92     */
93    public const STORAGE_KEY_TIMELINE_INDEX = 'index';
94
95    /**
96     * The storage key for the TimeLine tags in the packed format
97     */
98    public const STORAGE_KEY_TIMELINE_TAGS = 'tags';
99
100    /**
101     * The storage key for the TimeLine points in the packed format
102     */
103    public const STORAGE_KEY_TIMELINE_POINTS = 'points';
104
105    /**
106     * The storage key for the timestamp reference in the packed format
107     */
108    public const STORAGE_KEY_TIMELINE_EPOCH = 'epoch';
109
110    /**
111     * The type of format managed by this class
112     */
113    public const STORAGE_FORMAT = 'pack';
114
115    /**
116     * The version of the format. Could be useful in case of changes to take care of legacy.
117     */
118    public const STORAGE_VERSION = 1;
119
120    /**
121     * The type of format applied by this class
122     * @return string
123     */
124    public function getFormat()
125    {
126        return static::STORAGE_FORMAT;
127    }
128
129    /**
130     * The version of the packing format applied
131     * @return int
132     */
133    public function getVersion()
134    {
135        return static::STORAGE_VERSION;
136    }
137
138    /**
139     * The reference value used to compress the timestamps
140     * @return int
141     */
142    public function getEpoch()
143    {
144        // align the reference value with the timestamp of today morning.
145        $today = time();
146        return $today - $today % 86400;
147    }
148
149    /**
150     * Packs a TimeLine in order to reduce the storage footprint
151     * @param TimeLine $timeLine
152     * @return array
153     */
154    protected function packTimeLine(&$timeLine)
155    {
156        $epoch = $this->getEpoch();
157        $data = [
158            self::STORAGE_KEY_TIMELINE_INDEX => [],
159            self::STORAGE_KEY_TIMELINE_TAGS => [],
160            self::STORAGE_KEY_TIMELINE_POINTS => [],
161            self::STORAGE_KEY_TIMELINE_EPOCH => $epoch,
162        ];
163
164        // Will split tags from the list of TimePoint, and put them into a dedicated index.
165        // The other TimePoint info are put in a simple array with predictable order, this way:
166        // [target, type, timestamp].
167        // To save more space a reference value is removed from each timestamp.
168        $index = 0;
169        foreach ($timeLine->getPoints() as &$point) {
170            /** @var TimePoint $point */
171            $data[self::STORAGE_KEY_TIMELINE_POINTS][$index] = [
172                $point->getTarget(),
173                $point->getType(),
174                round($point->getTimestamp() - $epoch, 6),
175            ];
176
177            foreach ($point->getTags() as &$tag) {
178                $data[self::STORAGE_KEY_TIMELINE_INDEX][$tag][] = $index;
179            }
180
181            $index++;
182        }
183
184        // try to reduce the size of the index by simplifying those that target all TimePoint
185        if ($index) {
186            foreach ($data[self::STORAGE_KEY_TIMELINE_INDEX] as $tag => &$list) {
187                if (count($list) == $index) {
188                    unset($data[self::STORAGE_KEY_TIMELINE_INDEX][$tag]);
189                    $data[self::STORAGE_KEY_TIMELINE_TAGS][] = $tag;
190                }
191            }
192        } else {
193            $data = [];
194        }
195
196        return $data;
197    }
198
199    /**
200     * Unpack a dataset to a workable TimeLine
201     * @param array $data
202     * @return TimeLine
203     */
204    protected function unpackTimeLine(&$data)
205    {
206        $timeLine = new QtiTimeLine();
207
208        // the stored data can be packed or not
209        if (isset($data[self::STORAGE_KEY_TIMELINE_POINTS])) {
210            // get the reference value used to compress the timestamps
211            $epoch = 0;
212            if (isset($data[self::STORAGE_KEY_TIMELINE_EPOCH])) {
213                $epoch = $data[self::STORAGE_KEY_TIMELINE_EPOCH];
214            }
215
216            // rebuild the TimeLine from the list of stored TimePoint
217            $tags = $data[self::STORAGE_KEY_TIMELINE_TAGS];
218            foreach ($data[self::STORAGE_KEY_TIMELINE_POINTS] as &$dataPoint) {
219                $point = new TimePoint($tags, $dataPoint[2] + $epoch, $dataPoint[1], $dataPoint[0]);
220                $timeLine->add($point);
221            }
222
223            // reassign the tags from the stored index
224            $points = $timeLine->getPoints();
225            foreach ($data[self::STORAGE_KEY_TIMELINE_INDEX] as $tag => &$list) {
226                foreach ($list as $index) {
227                    $points[$index]->addTag($tag);
228                }
229            }
230        } else {
231            $timeLine->fromArray($data);
232        }
233
234        return $timeLine;
235    }
236
237    /**
238     * Encode a dataset with the managed format.
239     * @param mixed $data
240     * @return string
241     */
242    public function encode($data)
243    {
244        if (is_array($data)) {
245            $encodedData = [
246                self::STORAGE_KEY_FORMAT => $this->getFormat(),
247                self::STORAGE_KEY_VERSION => $this->getVersion(),
248            ];
249
250            foreach ($data as $key => &$value) {
251                if ($value instanceof TimeLine) {
252                    $encodedData[$key] = $this->packTimeLine($value);
253                } else {
254                    $encodedData[$key] = &$value;
255                }
256            }
257
258            return json_encode($encodedData);
259        }
260
261        return json_encode($data);
262    }
263
264    /**
265     * Decode a string encoded with the managed format.
266     * @param string $data
267     * @return mixed
268     */
269    public function decode($data)
270    {
271        $decodedData = json_decode($data, true);
272
273        // fallback for old storage that uses PHP serialize format
274        if (is_null($decodedData) && $data) {
275            $decodedData = unserialize($data);
276        }
277
278        if (is_array($decodedData)) {
279            if (isset($decodedData[self::STORAGE_KEY_FORMAT])) {
280                if ($decodedData[self::STORAGE_KEY_FORMAT] != $this->getFormat()) {
281                    \common_Logger::w(
282                        sprintf(
283                            'QtiTimeStorage: wrong decoder applied! (Expected: %s, Applied: %s)',
284                            $decodedData[self::STORAGE_KEY_FORMAT],
285                            $this->getFormat()
286                        )
287                    );
288                }
289
290                if (
291                    array_key_exists(self::STORAGE_KEY_TIME_LINE, $decodedData)
292                    && !$decodedData[self::STORAGE_KEY_TIME_LINE] instanceof TimeLine
293                ) {
294                    $decodedData[self::STORAGE_KEY_TIME_LINE] = $this->unpackTimeLine(
295                        $decodedData[self::STORAGE_KEY_TIME_LINE]
296                    );
297                }
298
299                unset($decodedData[self::STORAGE_KEY_FORMAT]);
300                unset($decodedData[self::STORAGE_KEY_VERSION]);
301            } else {
302                $decodedData = $this->decodeTimeline($decodedData);
303            }
304            $decodedData = $this->decodeAdjustmentMap($decodedData);
305        }
306
307        return $decodedData;
308    }
309}