Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 250
0.00% covered (danger)
0.00%
0 / 25
CRAP
0.00% covered (danger)
0.00%
0 / 1
RdsValueCollectionRepository
0.00% covered (danger)
0.00%
0 / 250
0.00% covered (danger)
0.00%
0 / 25
2550
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 isApplicable
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 findAll
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
12
 persist
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
42
 delete
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 count
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 insert
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
6
 verifyListElementsUniqueness
0.00% covered (danger)
0.00%
0 / 52
0.00% covered (danger)
0.00%
0 / 1
12
 enrichQueryWithInitialCondition
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enrichQueryWithOrderById
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 enrichQueryWithSelect
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 enrichQueryWithValueCollectionSearchCondition
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 enrichQueryWithSubject
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 enrichQueryWithExcludedValueUris
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 enrichQueryWithFilterValueUris
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 getPersistence
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 deleteListItemsDependencies
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 insertListItemsDependency
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 enrichQueryWithAllowedValues
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 getParentList
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 isListsDependencyEnabled
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 getFeatureFlagChecker
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDependencyRepository
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getContainer
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getQueryBuilder
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3/**
4 * This program is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU General Public License
6 * as published by the Free Software Foundation; under version 2
7 * of the License (non-upgradable).
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program; if not, write to the Free Software
16 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
17 *
18 * Copyright (c) 2020-2021 (original work) Open Assessment Technologies SA;
19 *
20 * @author Sergei Mikhailov <sergei.mikhailov@taotesting.com>
21 */
22
23declare(strict_types=1);
24
25namespace oat\tao\model\Lists\DataAccess\Repository;
26
27use Doctrine\DBAL\Driver\ResultStatement;
28use Throwable;
29use Doctrine\DBAL\FetchMode;
30use Doctrine\DBAL\Connection;
31use core_kernel_classes_Class;
32use Doctrine\DBAL\Query\QueryBuilder;
33use Psr\Container\ContainerInterface;
34use common_persistence_SqlPersistence;
35use oat\generis\model\OntologyAwareTrait;
36use core_kernel_classes_ContainerCollection;
37use oat\tao\model\Lists\Business\Domain\Value;
38use oat\generis\persistence\PersistenceManager;
39use oat\tao\model\service\InjectionAwareService;
40use oat\tao\model\featureFlag\FeatureFlagChecker;
41use oat\tao\model\Lists\Business\Domain\CollectionType;
42use oat\tao\model\Lists\Business\Domain\ValueCollection;
43use oat\tao\model\featureFlag\FeatureFlagCheckerInterface;
44use oat\oatbox\log\logger\extender\ContextExtenderInterface;
45use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
46use oat\tao\model\Lists\Business\Domain\ValueCollectionSearchRequest;
47use oat\tao\model\Lists\Business\Contract\DependencyRepositoryInterface;
48use oat\tao\model\Lists\Business\Contract\ValueCollectionRepositoryInterface;
49
50class RdsValueCollectionRepository extends InjectionAwareService implements ValueCollectionRepositoryInterface
51{
52    use OntologyAwareTrait;
53
54    public const SERVICE_ID = 'tao/RdsValueCollectionRepository';
55
56    public const TABLE_LIST_ITEMS = 'list_items';
57
58    public const FIELD_ITEM_LABEL = 'label';
59    public const FIELD_ITEM_ID = 'id';
60    public const FIELD_ITEM_URI = 'uri';
61    public const FIELD_ITEM_LIST_URI = 'list_uri';
62
63    public const TABLE_LIST_ITEMS_DEPENDENCIES = 'list_items_dependencies';
64    public const FIELD_LIST_ITEM_ID = 'list_item_id';
65    public const FIELD_LIST_ITEM_FIELD = 'field';
66    public const FIELD_LIST_ITEM_VALUE = 'value';
67
68    /** @var PersistenceManager */
69    private $persistenceManager;
70
71    /** @var string */
72    private $persistenceId;
73
74    /** @var bool */
75    private $isListsDependencyEnabled;
76
77    public function __construct(PersistenceManager $persistenceManager, string $persistenceId)
78    {
79        parent::__construct();
80
81        $this->persistenceManager = $persistenceManager;
82        $this->persistenceId = $persistenceId;
83    }
84
85    public function isApplicable(string $collectionUri): bool
86    {
87        return CollectionType::fromCollectionUri($collectionUri)->equals(CollectionType::remote());
88    }
89
90    public function findAll(ValueCollectionSearchRequest $searchRequest): ValueCollection
91    {
92        $query = $this->getQueryBuilder();
93
94        $this->enrichQueryWithAllowedValues($searchRequest, $query);
95        $this->enrichQueryWithInitialCondition($query);
96        $this->enrichQueryWithSelect($searchRequest, $query);
97        $this->enrichQueryWithValueCollectionSearchCondition($searchRequest, $query);
98        $this->enrichQueryWithSubject($searchRequest, $query);
99        $this->enrichQueryWithExcludedValueUris($searchRequest, $query);
100        $this->enrichQueryWithFilterValueUris($searchRequest, $query);
101        $this->enrichQueryWithOrderById($query);
102
103        $values = [];
104
105        foreach ($query->execute()->fetchAll() as $rawValue) {
106            $value = new Value(
107                (int) $rawValue[self::FIELD_ITEM_ID],
108                $rawValue[self::FIELD_ITEM_URI],
109                $rawValue[self::FIELD_ITEM_LABEL]
110            );
111            $value->setListUri($rawValue[self::FIELD_ITEM_LIST_URI]);
112
113            $values[] = $value;
114        }
115
116        $valueCollectionUri = $searchRequest->hasValueCollectionUri()
117            ? $searchRequest->getValueCollectionUri()
118            : $rawValue[self::FIELD_ITEM_LIST_URI] ?? null;
119
120        return new ValueCollection($valueCollectionUri, ...$values);
121    }
122
123    public function persist(ValueCollection $valueCollection): bool
124    {
125        $this->verifyListElementsUniqueness($valueCollection);
126
127        $platform = $this->getPersistence()->getPlatForm();
128        $platform->beginTransaction();
129
130        try {
131            $this->delete($valueCollection->getUri());
132
133            foreach ($valueCollection as $value) {
134                $this->insert($valueCollection, $value);
135            }
136
137            $platform->commit();
138
139            return true;
140        } catch (ValueConflictException $exception) {
141            throw $exception;
142        } catch (UniqueConstraintViolationException $exception) {
143            throw new ValueConflictException(
144                sprintf(
145                    'List "%s" has duplicated values. (%s)',
146                    $valueCollection->getUri(),
147                    $exception->getMessage()
148                ),
149                __('List "%s" has duplicated values.', $valueCollection->getUri())
150            );
151        } catch (Throwable $exception) {
152            return false;
153        } finally {
154            if (isset($exception)) {
155                $this->logError(
156                    sprintf('List "%s" persistence failed', $valueCollection->getUri()),
157                    [
158                        ContextExtenderInterface::CONTEXT_EXCEPTION => $exception,
159                    ]
160                );
161
162                $platform->rollBack();
163            }
164        }
165    }
166
167    public function delete(string $valueCollectionUri): void
168    {
169        $query = $this->getQueryBuilder();
170
171        $this->deleteListItemsDependencies($query, $valueCollectionUri);
172
173        $query->delete(self::TABLE_LIST_ITEMS)
174            ->where($query->expr()->eq(self::FIELD_ITEM_LIST_URI, ':list_uri'))
175            ->setParameter('list_uri', $valueCollectionUri)
176            ->execute();
177    }
178
179    public function count(ValueCollectionSearchRequest $searchRequest): int
180    {
181        $query = $this->getQueryBuilder();
182
183        $this->enrichQueryWithInitialCondition($query);
184        $this->enrichQueryWithValueCollectionSearchCondition($searchRequest, $query);
185
186        $result = $query->select('count(items.id) AS c')->execute();
187
188        $row = $result->fetch(FetchMode::NUMERIC);
189        return (int) ($row[0] ?? 0);
190    }
191
192    protected function insert(ValueCollection $valueCollection, Value $value): void
193    {
194        $platform = $this->getPersistence()->getPlatForm();
195        $platform->beginTransaction();
196
197        try {
198            $qb = $platform->getQueryBuilder();
199            $qb->insert(self::TABLE_LIST_ITEMS)
200                ->values([
201                    self::FIELD_ITEM_LABEL => ':label',
202                    self::FIELD_ITEM_URI => ':uri',
203                    self::FIELD_ITEM_LIST_URI => ':listUri',
204                ])
205                ->setParameters([
206                    'uri' => $value->getUri(),
207                    'label' => $value->getLabel(),
208                    'listUri' => $valueCollection->getUri(),
209                ])
210                ->execute();
211
212            $this->insertListItemsDependency($qb, $value);
213
214            $platform->commit();
215        } catch (Throwable $exception) {
216            $this->logError(
217                sprintf(
218                    'Cannot persist list element "%s" ("%s") for list "%s". Exception: %s. Message: %s',
219                    $value->getLabel(),
220                    $value->getUri(),
221                    $valueCollection->getUri(),
222                    get_class($exception),
223                    $exception->getMessage()
224                )
225            );
226
227            $platform->rollBack();
228        }
229    }
230
231    private function verifyListElementsUniqueness(ValueCollection $valueCollection): void
232    {
233        if ($valueCollection->hasDuplicates()) {
234            $duplicatedValues = implode('", "', $valueCollection->getDuplicatedValues(5)->getUris());
235            $valueConflictException = new ValueConflictException(
236                sprintf(
237                    'List "%s" has duplicated values: "%s"',
238                    $valueCollection->getUri(),
239                    $duplicatedValues
240                ),
241                __(
242                    'List "%s" has duplicated values: "%s"',
243                    $valueCollection->getUri(),
244                    $duplicatedValues
245                )
246            );
247            $this->logError($valueConflictException->getMessage());
248
249            throw $valueConflictException;
250        }
251
252        $queryBuilder = $this->getQueryBuilder();
253        $expr = $queryBuilder->expr();
254
255        $existingUris = $queryBuilder
256            ->select('items.' . self::FIELD_ITEM_URI)
257            ->from(self::TABLE_LIST_ITEMS, 'items')
258            ->where(
259                $expr->neq(
260                    'items.' . self::FIELD_ITEM_LIST_URI,
261                    ':listUri'
262                )
263            )
264            ->andWhere(
265                $expr->in(
266                    'items.' . self::FIELD_ITEM_URI,
267                    ':uris'
268                )
269            )
270            ->setParameter('listUri', $valueCollection->getUri())
271            ->setParameter('uris', $valueCollection->getUris(), Connection::PARAM_STR_ARRAY)
272            ->setMaxResults(5)
273            ->execute()
274            ->fetchAll(FetchMode::COLUMN);
275
276        if (!empty($existingUris)) {
277            $existingUrisList = implode('", "', $existingUris);
278            $valueConflictException = new ValueConflictException(
279                sprintf(
280                    'List contains elements whose URIs are already defined: "%s"',
281                    $existingUrisList
282                ),
283                __(
284                    'List contains elements whose URIs are already defined: "%s"',
285                    $existingUrisList
286                )
287            );
288            $this->logError($valueConflictException->getMessage());
289
290            throw $valueConflictException;
291        }
292    }
293
294    private function enrichQueryWithInitialCondition(QueryBuilder $query): void
295    {
296        $query->from(self::TABLE_LIST_ITEMS, 'items');
297    }
298
299    private function enrichQueryWithOrderById(QueryBuilder $query): void
300    {
301        $query->addOrderBy('items.' . self::FIELD_ITEM_ID);
302    }
303
304    private function enrichQueryWithSelect(ValueCollectionSearchRequest $searchRequest, QueryBuilder $query): void
305    {
306        $query
307            ->select(
308                'items.' . self::FIELD_ITEM_ID,
309                'items.' . self::FIELD_ITEM_LIST_URI,
310                'items.' . self::FIELD_ITEM_URI,
311                'items.' . self::FIELD_ITEM_LABEL
312            );
313
314        if ($searchRequest->hasLimit()) {
315            $query->setMaxResults($searchRequest->getLimit());
316        }
317    }
318
319    private function enrichQueryWithValueCollectionSearchCondition(
320        ValueCollectionSearchRequest $searchRequest,
321        QueryBuilder $query
322    ): void {
323        if (!$searchRequest->hasValueCollectionUri()) {
324            return;
325        }
326
327        $expressionBuilder = $query->expr();
328
329        $query
330            ->andWhere($expressionBuilder->eq(self::FIELD_ITEM_LIST_URI, ':collection_uri'))
331            ->setParameter('collection_uri', $searchRequest->getValueCollectionUri());
332    }
333
334    private function enrichQueryWithSubject(ValueCollectionSearchRequest $searchRequest, QueryBuilder $query): void
335    {
336        if (!$searchRequest->hasSubject()) {
337            return;
338        }
339
340        $query
341            ->andWhere(
342                $this->getQueryBuilder()->expr()->like(
343                    sprintf('LOWER(%s)', self::FIELD_ITEM_LABEL),
344                    ':label'
345                )
346            )
347            ->setParameter('label', '%' . $searchRequest->getSubject() . '%');
348    }
349
350    private function enrichQueryWithExcludedValueUris(
351        ValueCollectionSearchRequest $searchRequest,
352        QueryBuilder $query
353    ): void {
354        if (!$searchRequest->hasExcluded()) {
355            return;
356        }
357
358        $query
359            ->andWhere(
360                $this->getQueryBuilder()->expr()->notIn(
361                    self::FIELD_ITEM_LABEL,
362                    ':excluded_value_uri'
363                )
364            )
365            ->setParameter('excluded_value_uri', $searchRequest->getExcluded(), Connection::PARAM_STR_ARRAY);
366    }
367
368    private function enrichQueryWithFilterValueUris(
369        ValueCollectionSearchRequest $searchRequest,
370        QueryBuilder $query
371    ): void {
372        if (!$searchRequest->hasUris()) {
373            return;
374        }
375
376        $expressionBuilder = $query->expr();
377
378        $query
379            ->andWhere($expressionBuilder->in(self::FIELD_ITEM_URI, ':uris'))
380            ->setParameter('uris', $searchRequest->getUris(), Connection::PARAM_STR_ARRAY);
381    }
382
383    private function getPersistence(): common_persistence_SqlPersistence
384    {
385        /** @noinspection PhpIncompatibleReturnTypeInspection */
386        return $this->persistenceManager->getPersistenceById($this->persistenceId);
387    }
388
389    private function deleteListItemsDependencies(QueryBuilder $query, string $valueCollectionUri): void
390    {
391        if ($this->isListsDependencyEnabled()) {
392            $ids = $query->from(self::TABLE_LIST_ITEMS, 'items')
393                ->select(self::FIELD_ITEM_ID)
394                ->where($query->expr()->eq('items.' . self::FIELD_ITEM_LIST_URI, ':list_uri'))
395                ->setParameter('list_uri', $valueCollectionUri)
396                ->execute()
397                ->fetchAll(FetchMode::COLUMN);
398
399            if (!empty($ids)) {
400                $query->delete(self::TABLE_LIST_ITEMS_DEPENDENCIES)
401                    ->where($query->expr()->in(self::FIELD_LIST_ITEM_ID, ':list_items_ids'))
402                    ->setParameter('list_items_ids', $ids, Connection::PARAM_STR_ARRAY)
403                    ->execute();
404            }
405        }
406    }
407
408    private function insertListItemsDependency(QueryBuilder $qb, Value $value): void
409    {
410        if ($this->isListsDependencyEnabled() && $value->getDependencyUri() !== null) {
411            $qb->insert(self::TABLE_LIST_ITEMS_DEPENDENCIES)
412                ->values([
413                    self::FIELD_LIST_ITEM_ID => ':list_item_id',
414                    self::FIELD_LIST_ITEM_FIELD => ':field',
415                    self::FIELD_LIST_ITEM_VALUE => ':value',
416                ])
417                ->setParameters([
418                    'list_item_id' => $qb->getConnection()->lastInsertId('list_items_id_seq'),
419                    'field' => 'uri',
420                    'value' => $value->getDependencyUri(),
421                ])
422                ->execute();
423        }
424    }
425
426    private function enrichQueryWithAllowedValues(ValueCollectionSearchRequest $request, QueryBuilder $query): void
427    {
428        if (!$this->isListsDependencyEnabled() || !$request->hasParentListValues()) {
429            return;
430        }
431
432        $parentList = $this->getParentList($request);
433
434        if (!$parentList) {
435            return;
436        }
437
438        $allowedItemIds = $this->getDependencyRepository()->findChildListIds(
439            [
440                'parentListUris' => [$parentList->getUri()],
441                'parentListValues' => $request->getParentListValues(),
442            ]
443        );
444
445        $query
446            ->andWhere(
447                $query->expr()->orX(
448                    $query->expr()->in(self::FIELD_ITEM_ID, ':allowed_item_ids'),
449                    $query->expr()->in(self::FIELD_ITEM_URI, ':allowed_item_uris')
450                )
451            )
452            ->setParameter('allowed_item_ids', $allowedItemIds, Connection::PARAM_STR_ARRAY)
453            ->setParameter('allowed_item_uris', $request->getSelectedValues(), Connection::PARAM_STR_ARRAY);
454    }
455
456    /**
457     * @return core_kernel_classes_Class|core_kernel_classes_ContainerCollection|null
458     */
459    private function getParentList(ValueCollectionSearchRequest $request)
460    {
461        if (!$request->hasPropertyUri()) {
462            return null;
463        }
464
465        $parentProperty = $this->getProperty($request->getPropertyUri())->getDependsOnPropertyCollection()->current();
466
467        return $parentProperty ? $parentProperty->getRange() : null;
468    }
469
470    private function isListsDependencyEnabled(): bool
471    {
472        if (!isset($this->isListsDependencyEnabled)) {
473            $this->isListsDependencyEnabled = $this->getFeatureFlagChecker()->isEnabled(
474                FeatureFlagCheckerInterface::FEATURE_FLAG_LISTS_DEPENDENCY_ENABLED
475            );
476        }
477
478        return $this->isListsDependencyEnabled;
479    }
480
481    private function getFeatureFlagChecker(): FeatureFlagCheckerInterface
482    {
483        return $this->getContainer()->get(FeatureFlagChecker::class);
484    }
485
486    private function getDependencyRepository(): DependencyRepositoryInterface
487    {
488        return $this->getContainer()->get(DependencyRepository::class);
489    }
490
491    private function getContainer(): ContainerInterface
492    {
493        return $this->getServiceLocator()->getContainer();
494    }
495
496    private function getQueryBuilder(): QueryBuilder
497    {
498        return $this->getPersistence()->getPlatForm()->getQueryBuilder();
499    }
500}