Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.12% covered (success)
94.12%
80 / 85
66.67% covered (warning)
66.67%
8 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
ResourceWatcher
94.12% covered (success)
94.12%
80 / 85
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
93.33% covered (success)
93.33%
14 / 15
0.00% covered (danger)
0.00%
0 / 1
4.00
 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        $threshold = $this->getOption(self::OPTION_THRESHOLD);
89
90        if ($updatedAt === null || ($now - $updatedAt) > $threshold) {
91            $this->getLogger()->debug(
92                'triggering index update on resourceUpdated event'
93            );
94
95            $property = $this->getProperty(TaoOntology::PROPERTY_UPDATED_AT);
96            $this->updatedAtCache[$resource->getUri()] = $now;
97            $resource->editPropertyValues($property, $now);
98
99            $taskMessage = __('Adding/updating search index for updated resource');
100            $this->createResourceIndexingTask($resource, $taskMessage);
101        }
102    }
103
104    public function catchDeletedResourceEvent(ResourceDeleted $event): void
105    {
106        try {
107            $this->getSearch()->remove($event->getId());
108
109            if ($this->getFeatureFlagChecker()->isEnabled('FEATURE_FLAG_TRANSLATION_ENABLED')) {
110                $this->getTranslationDeletionService()->deleteByOriginResourceUri($event->getId());
111            }
112        } catch (\Exception $e) {
113            $message = $e->getMessage();
114            $this->getLogger()->error(
115                sprintf("Error delete index document for %s with message %s", $event->getId(), $message)
116            );
117        }
118    }
119
120     /**
121     * @return \core_kernel_classes_Container
122     * @throws \core_kernel_persistence_Exception
123     */
124    public function getUpdatedAt(core_kernel_classes_Resource $resource)
125    {
126        if (isset($this->updatedAtCache[$resource->getUri()])) {
127            $updatedAt = $this->updatedAtCache[$resource->getUri()];
128        } else {
129            $property = $this->getProperty(TaoOntology::PROPERTY_UPDATED_AT);
130            $updatedAt = $resource->getOnePropertyValue($property);
131            if ($updatedAt && $updatedAt instanceof core_kernel_classes_Literal) {
132                $updatedAt = (int) $updatedAt->literal;
133            }
134            $this->updatedAtCache[$resource->getUri()] = $updatedAt;
135        }
136        return $updatedAt;
137    }
138
139    /**
140     * Creates a task in the task queue to index/re-index created/updated resource
141     */
142    private function createResourceIndexingTask(core_kernel_classes_Resource $resource, string $message): void
143    {
144        if ($this->getServiceManager()->get(AdvancedSearchChecker::class)->isEnabled()) {
145            /** @var QueueDispatcherInterface $queueDispatcher */
146            $queueDispatcher = $this->getServiceManager()->get(QueueDispatcherInterface::SERVICE_ID);
147
148            if ($this->hasClassSupport($resource) && !$this->ignoreEditIemClassUpdates()) {
149                $queueDispatcher->createTask(new UpdateClassInIndex(), [$resource->getUri()], $message);
150                return;
151            }
152
153            $rootIndexClass = $this->getResourceIndexType($resource);
154            if ($rootIndexClass) {
155                $isTest = TaoOntology::CLASS_URI_TEST === $rootIndexClass;
156                $queueDispatcher->createTask(
157                    $isTest ? new UpdateTestResourceInIndex() : new UpdateResourceInIndex(),
158                    [$resource->getUri()],
159                    $message
160                );
161            }
162        }
163    }
164
165    /**
166     * This method actually finds a root class that is supported by used tao/IndexUpdater
167     */
168    private function getResourceIndexType(core_kernel_classes_Resource $resource): ?string
169    {
170        $resourceTypeIds = $this->getResourceTypes($resource);
171        $checkedResourceTypes = [OntologyRdfs::RDFS_RESOURCE, TaoOntology::CLASS_URI_OBJECT];
172        $resourceTypeIds = array_diff($resourceTypeIds, [OntologyRdfs::RDFS_RESOURCE, TaoOntology::CLASS_URI_OBJECT]);
173
174        while (!empty($resourceTypeIds)) {
175            $classUri = array_pop($resourceTypeIds);
176            $hasClassSupport = $this->getServiceManager()
177                ->get(IndexUpdaterInterface::SERVICE_ID)
178                ->hasClassSupport(
179                    $classUri
180                );
181
182            if ($hasClassSupport) {
183                return $classUri;
184            }
185
186            $class = $this->getClass($classUri);
187
188            foreach ($class->getParentClasses() as $parent) {
189                if (!in_array($parent->getUri(), $checkedResourceTypes)) {
190                    $resourceTypeIds[] = $parent->getUri();
191                }
192            }
193            $checkedResourceTypes[] = $class->getUri();
194        }
195
196        return null;
197    }
198
199    private function getResourceTypes(core_kernel_classes_Resource $resource): array
200    {
201        return array_map(
202            function (core_kernel_classes_Class $resourceType): string {
203                return $resourceType->getUri();
204            },
205            $resource->getTypes()
206        );
207    }
208
209    private function hasClassSupport(core_kernel_classes_Resource $resource): bool
210    {
211        return $resource instanceof core_kernel_classes_Class;
212    }
213
214    private function ignoreEditIemClassUpdates(): bool
215    {
216        try {
217            $url = parse_url(common_http_Request::currentRequest()->getUrl());
218        } catch (\common_exception_Error $e) {
219            return false;
220        }
221
222        return isset($url['path']) && $url['path'] === '/taoItems/Items/editItemClass';
223    }
224
225    private function getFeatureFlagChecker(): FeatureFlagCheckerInterface
226    {
227        return $this->getServiceManager()->getContainer()->get(FeatureFlagChecker::class);
228    }
229
230    private function getTranslationDeletionService(): TranslationDeletionService
231    {
232        return $this->getServiceManager()->getContainer()->get(TranslationDeletionService::class);
233    }
234
235    private function getSearch(): Search
236    {
237        return $this->getServiceManager()->getContainer()->get(Search::SERVICE_ID);
238    }
239}