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