Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.05% covered (success)
94.05%
79 / 84
66.67% covered (warning)
66.67%
8 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResourceWatcher
94.05% covered (success)
94.05%
79 / 84
66.67% covered (warning)
66.67%
8 / 12
31.20
0.00% covered (danger)
0.00%
0 / 1
 catchCreatedResourceEvent
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 catchUpdatedResourceEvent
92.86% covered (success)
92.86%
13 / 14
0.00% covered (danger)
0.00%
0 / 1
4.01
 catchDeletedResourceEvent
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 getUpdatedAt
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
 createResourceIndexingTask
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 getResourceIndexType
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
5
 getResourceTypes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 hasClassSupport
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 ignoreEditIemClassUpdates
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 getFeatureFlagChecker
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTranslationDeletionService
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getSearch
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) 2017-2022 (original work) Open Assessment Technologies SA;
19 */
20
21namespace oat\tao\model\resources;
22
23use common_http_Request;
24use core_kernel_classes_Class;
25use core_kernel_classes_Literal;
26use core_kernel_classes_Resource;
27use oat\generis\model\data\event\ResourceCreated;
28use oat\generis\model\data\event\ResourceDeleted;
29use oat\generis\model\data\event\ResourceUpdated;
30use oat\generis\model\OntologyAwareTrait;
31use oat\generis\model\OntologyRdfs;
32use oat\oatbox\service\ConfigurableService;
33use oat\tao\model\AdvancedSearch\AdvancedSearchChecker;
34use oat\tao\model\featureFlag\FeatureFlagChecker;
35use oat\tao\model\featureFlag\FeatureFlagCheckerInterface;
36use oat\tao\model\search\index\IndexUpdaterInterface;
37use oat\tao\model\search\Search;
38use oat\tao\model\search\tasks\UpdateClassInIndex;
39use oat\tao\model\search\tasks\UpdateResourceInIndex;
40use oat\tao\model\search\tasks\UpdateTestResourceInIndex;
41use oat\tao\model\TaoOntology;
42use oat\tao\model\taskQueue\QueueDispatcherInterface;
43use oat\tao\model\Translation\Service\TranslationDeletionService;
44
45/**
46 * @author Aleksej Tikhanovich, <aleksej@taotesting.com>
47 */
48class ResourceWatcher extends ConfigurableService
49{
50    use OntologyAwareTrait;
51
52    public const SERVICE_ID = 'tao/ResourceWatcher';
53
54    /** Time in seconds for updatedAt threshold */
55    public const OPTION_THRESHOLD = 'threshold';
56
57    /** @var array */
58    protected $updatedAtCache = [];
59
60    public function catchCreatedResourceEvent(ResourceCreated $event): void
61    {
62        $resource = $event->getResource();
63        $property = $this->getProperty(TaoOntology::PROPERTY_UPDATED_AT);
64        $now = microtime(true);
65        $this->updatedAtCache = [];
66        $this->updatedAtCache[$resource->getUri()] = $now;
67        $resource->editPropertyValues($property, $now);
68
69        $this->getLogger()->debug('triggering index update on resourceCreated event');
70
71        $taskMessage = __('Adding search index for created resource');
72        $this->createResourceIndexingTask($resource, $taskMessage);
73    }
74
75    /**
76     * @throws \core_kernel_persistence_Exception
77     */
78    public function catchUpdatedResourceEvent(ResourceUpdated $event): void
79    {
80        $resource = $event->getResource();
81        $updatedAt = $this->getUpdatedAt($resource);
82
83        if ($updatedAt instanceof core_kernel_classes_Literal) {
84            $updatedAt = (int) $updatedAt->literal;
85        }
86
87        $now = microtime(true);
88
89        if ($updatedAt === null || ((int) $now - (int) $updatedAt) > 0) {
90            $this->getLogger()->debug(
91                'triggering index update on resourceUpdated event'
92            );
93
94            $property = $this->getProperty(TaoOntology::PROPERTY_UPDATED_AT);
95            $this->updatedAtCache[$resource->getUri()] = $now;
96            $resource->editPropertyValues($property, $now);
97
98            $taskMessage = __('Adding/updating search index for updated resource');
99            $this->createResourceIndexingTask($resource, $taskMessage);
100        }
101    }
102
103    public function catchDeletedResourceEvent(ResourceDeleted $event): void
104    {
105        try {
106            $this->getSearch()->remove($event->getId());
107
108            if ($this->getFeatureFlagChecker()->isEnabled('FEATURE_FLAG_TRANSLATION_ENABLED')) {
109                $this->getTranslationDeletionService()->deleteByOriginResourceUri($event->getId());
110            }
111        } catch (\Exception $e) {
112            $message = $e->getMessage();
113            $this->getLogger()->error(
114                sprintf("Error delete index document for %s with message %s", $event->getId(), $message)
115            );
116        }
117    }
118
119    /**
120     * @return \core_kernel_classes_Container
121     * @throws \core_kernel_persistence_Exception
122     */
123    public function getUpdatedAt(core_kernel_classes_Resource $resource)
124    {
125        if (isset($this->updatedAtCache[$resource->getUri()])) {
126            $updatedAt = $this->updatedAtCache[$resource->getUri()];
127        } else {
128            $property = $this->getProperty(TaoOntology::PROPERTY_UPDATED_AT);
129            $updatedAt = $resource->getOnePropertyValue($property);
130            if ($updatedAt && $updatedAt instanceof core_kernel_classes_Literal) {
131                $updatedAt = (int)$updatedAt->literal;
132            }
133            $this->updatedAtCache[$resource->getUri()] = $updatedAt;
134        }
135        return $updatedAt;
136    }
137
138    /**
139     * Creates a task in the task queue to index/re-index created/updated resource
140     */
141    private function createResourceIndexingTask(core_kernel_classes_Resource $resource, string $message): void
142    {
143        if ($this->getServiceManager()->get(AdvancedSearchChecker::class)->isEnabled()) {
144            /** @var QueueDispatcherInterface $queueDispatcher */
145            $queueDispatcher = $this->getServiceManager()->get(QueueDispatcherInterface::SERVICE_ID);
146
147            if ($this->hasClassSupport($resource) && !$this->ignoreEditIemClassUpdates()) {
148                $queueDispatcher->createTask(new UpdateClassInIndex(), [$resource->getUri()], $message);
149                return;
150            }
151
152            $rootIndexClass = $this->getResourceIndexType($resource);
153            if ($rootIndexClass) {
154                $isTest = TaoOntology::CLASS_URI_TEST === $rootIndexClass;
155                $queueDispatcher->createTask(
156                    $isTest ? new UpdateTestResourceInIndex() : new UpdateResourceInIndex(),
157                    [$resource->getUri()],
158                    $message
159                );
160            }
161        }
162    }
163
164    /**
165     * This method actually finds a root class that is supported by used tao/IndexUpdater
166     */
167    private function getResourceIndexType(core_kernel_classes_Resource $resource): ?string
168    {
169        $resourceTypeIds = $this->getResourceTypes($resource);
170        $checkedResourceTypes = [OntologyRdfs::RDFS_RESOURCE, TaoOntology::CLASS_URI_OBJECT];
171        $resourceTypeIds = array_diff($resourceTypeIds, [OntologyRdfs::RDFS_RESOURCE, TaoOntology::CLASS_URI_OBJECT]);
172
173        while (!empty($resourceTypeIds)) {
174            $classUri = array_pop($resourceTypeIds);
175            $hasClassSupport = $this->getServiceManager()
176                ->get(IndexUpdaterInterface::SERVICE_ID)
177                ->hasClassSupport(
178                    $classUri
179                );
180
181            if ($hasClassSupport) {
182                return $classUri;
183            }
184
185            $class = $this->getClass($classUri);
186
187            foreach ($class->getParentClasses() as $parent) {
188                if (!in_array($parent->getUri(), $checkedResourceTypes)) {
189                    $resourceTypeIds[] = $parent->getUri();
190                }
191            }
192            $checkedResourceTypes[] = $class->getUri();
193        }
194
195        return null;
196    }
197
198    private function getResourceTypes(core_kernel_classes_Resource $resource): array
199    {
200        return array_map(
201            function (core_kernel_classes_Class $resourceType): string {
202                return $resourceType->getUri();
203            },
204            $resource->getTypes()
205        );
206    }
207
208    private function hasClassSupport(core_kernel_classes_Resource $resource): bool
209    {
210        return $resource instanceof core_kernel_classes_Class;
211    }
212
213    private function ignoreEditIemClassUpdates(): bool
214    {
215        try {
216            $url = parse_url(common_http_Request::currentRequest()->getUrl());
217        } catch (\common_exception_Error $e) {
218            return false;
219        }
220
221        return isset($url['path']) && $url['path'] === '/taoItems/Items/editItemClass';
222    }
223
224    private function getFeatureFlagChecker(): FeatureFlagCheckerInterface
225    {
226        return $this->getServiceManager()->getContainer()->get(FeatureFlagChecker::class);
227    }
228
229    private function getTranslationDeletionService(): TranslationDeletionService
230    {
231        return $this->getServiceManager()->getContainer()->get(TranslationDeletionService::class);
232    }
233
234    private function getSearch(): Search
235    {
236        return $this->getServiceManager()->getContainer()->get(Search::SERVICE_ID);
237    }
238}