Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
49.64% covered (danger)
49.64%
68 / 137
25.00% covered (danger)
25.00%
3 / 12
CRAP
0.00% covered (danger)
0.00%
0 / 1
IndexDocumentBuilder
49.64% covered (danger)
49.64%
68 / 137
25.00% covered (danger)
25.00%
3 / 12
267.36
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 createDocumentFromResource
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
1
 createDocumentFromArray
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
4
 getTypesForResource
50.00% covered (danger)
50.00%
7 / 14
0.00% covered (danger)
0.00%
0 / 1
8.12
 getTokenizedResourceBody
81.25% covered (warning)
81.25%
13 / 16
0.00% covered (danger)
0.00%
0 / 1
2.03
 getIndexProperties
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 getDynamicProperties
25.00% covered (danger)
25.00%
7 / 28
0.00% covered (danger)
0.00%
0 / 1
27.67
 getAccessProperties
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 getParentClasses
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
5.67
 isRootClass
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 normalizeAndFilterUniqueValues
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 getRawValue
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
156
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-2022 (original work) Open Assessment Technologies SA;
19 */
20
21declare(strict_types=1);
22
23namespace oat\tao\model\search\index\DocumentBuilder;
24
25use common_Exception;
26use common_exception_Error;
27use common_exception_InconsistentData;
28use common_exception_MissingParameter;
29use core_kernel_classes_Container;
30use core_kernel_classes_Property;
31use oat\generis\model\data\Ontology;
32use oat\generis\model\data\permission\PermissionInterface;
33use oat\generis\model\data\permission\ReverseRightLookupInterface;
34use oat\generis\model\OntologyRdfs;
35use oat\tao\model\Lists\Business\Domain\ValueCollectionSearchRequest;
36use oat\tao\model\Lists\Business\Input\ValueCollectionSearchInput;
37use oat\tao\model\Lists\Business\Service\ValueCollectionService;
38use oat\tao\model\Lists\Business\Specification\RemoteListPropertySpecification;
39use oat\tao\model\search\index\IndexDocument;
40use ArrayIterator;
41use core_kernel_classes_Resource as Resource;
42use Iterator;
43use oat\tao\model\search\index\IndexProperty;
44use oat\tao\model\search\index\OntologyIndex;
45use oat\tao\model\search\SearchTokenGenerator;
46use oat\tao\model\TaoOntology;
47use tao_helpers_Uri;
48
49class IndexDocumentBuilder implements IndexDocumentBuilderInterface
50{
51    private Ontology $ontologyService;
52    private SearchTokenGenerator $searchTokenGenerator;
53    private PropertyIndexReferenceFactory $propertyIndexReferenceFactory;
54    private ValueCollectionService $valueCollectionService;
55    private RemoteListPropertySpecification $remoteListPropertySpecification;
56    /** @var PermissionInterface|ReverseRightLookupInterface */
57    private $permissionService;
58
59    public function __construct(
60        Ontology $ontologyService,
61        SearchTokenGenerator $searchTokenGenerator,
62        PropertyIndexReferenceFactory $propertyIndexReferenceFactory,
63        ValueCollectionService $valueCollectionService,
64        RemoteListPropertySpecification $remoteListPropertySpecification,
65        $permissionService
66    ) {
67        $this->ontologyService = $ontologyService;
68        $this->searchTokenGenerator = $searchTokenGenerator;
69        $this->propertyIndexReferenceFactory = $propertyIndexReferenceFactory;
70        $this->valueCollectionService = $valueCollectionService;
71        $this->remoteListPropertySpecification = $remoteListPropertySpecification;
72        $this->permissionService = $permissionService;
73    }
74
75    /** @var array */
76    private $map = [];
77
78    private const ROOT_CLASSES = [
79        TaoOntology::CLASS_URI_ITEM,
80        TaoOntology::CLASS_URI_TEST,
81        TaoOntology::CLASS_URI_ASSEMBLED_DELIVERY,
82        TaoOntology::CLASS_URI_DELIVERY,
83        TaoOntology::CLASS_URI_GROUP,
84        TaoOntology::CLASS_URI_ITEM,
85        TaoOntology::CLASS_URI_RESULT,
86        TaoOntology::CLASS_URI_SUBJECT,
87        TaoOntology::CLASS_URI_TEST,
88    ];
89
90    public function createDocumentFromResource(Resource $resource): IndexDocument
91    {
92        $tokenizationInfo = $this->getTokenizedResourceBody($resource);
93
94        $body = $tokenizationInfo['body'];
95        $indexProperties = $tokenizationInfo['indexProperties'];
96
97        $body['type'] = $this->getTypesForResource($resource);
98
99        return new IndexDocument(
100            $resource->getUri(),
101            $body,
102            $indexProperties,
103            $this->getDynamicProperties($resource->getTypes(), $resource),
104            $this->getAccessProperties($resource)
105        );
106    }
107
108    public function createDocumentFromArray(array $resourceData = []): IndexDocument
109    {
110        if (!isset($resourceData['id'])) {
111            throw new common_exception_MissingParameter('id');
112        }
113
114        if (!isset($resourceData['body'])) {
115            throw new common_exception_MissingParameter('body');
116        }
117
118        $resource = $this->ontologyService->getResource($resourceData['id']);
119
120        if (isset($resourceData['indexProperties'])) {
121            $indexProperties = $resourceData['indexProperties'];
122        } else {
123            $tokenizationInfo = $this->getTokenizedResourceBody($resource);
124            $indexProperties = $tokenizationInfo['indexProperties'];
125        }
126
127        return new IndexDocument(
128            $resourceData['id'],
129            $resourceData['body'],
130            $indexProperties,
131            $this->getDynamicProperties($resource->getTypes(), $resource),
132            $this->getAccessProperties($resource)
133        );
134    }
135
136    /**
137     * @return string[]
138     * @throws common_exception_Error
139     */
140    private function getTypesForResource(Resource $resource): array
141    {
142        $toDo = [];
143        foreach ($resource->getTypes() as $class) {
144            $toDo[] = $class->getUri();
145        }
146
147        $done = [OntologyRdfs::RDFS_RESOURCE, TaoOntology::CLASS_URI_OBJECT];
148        $toDo = array_diff($toDo, $done);
149
150        $classes = [];
151
152        while (!empty($toDo)) {
153            $class = new \core_kernel_classes_Class(array_pop($toDo));
154            $classes[] = $class->getUri();
155
156            foreach ($class->getParentClasses() as $parent) {
157                if (!in_array($parent->getUri(), $done)) {
158                    $toDo[] = $parent->getUri();
159                }
160            }
161
162            $done[] = $class->getUri();
163        }
164
165        return $classes;
166    }
167
168    /**
169     * @throws common_Exception
170     * @throws common_exception_InconsistentData
171     */
172    private function getTokenizedResourceBody(Resource $resource): array
173    {
174        $tokenGenerator = $this->searchTokenGenerator;
175
176        $body = [];
177        $indexProperties = [];
178
179        foreach ($tokenGenerator->generateTokens($resource) as $data) {
180            /** @var OntologyIndex $index */
181            [$index, $strings] = $data;
182            $body[$index->getIdentifier()] = $strings;
183            $indexProperties[$index->getIdentifier()] = $this->getIndexProperties($index);
184        }
185
186        $body['parent_classes'] = $this->getParentClasses($resource->getTypes());
187        $body['location'] = implode('/', array_reverse($body['class'] ?? []));
188        $body['updated_at'] = (string)$resource->getOnePropertyValue(
189            $resource->getProperty(TaoOntology::PROPERTY_UPDATED_AT)
190        );
191
192        return [
193            'body' => $body,
194            'indexProperties' => $indexProperties
195        ];
196    }
197
198    /**
199     * @throws common_Exception
200     */
201    private function getIndexProperties(OntologyIndex $index): IndexProperty
202    {
203        if (!isset($this->map[$index->getIdentifier()])) {
204            $indexProperty = new IndexProperty(
205                $index->getIdentifier(),
206                $index->isFuzzyMatching(),
207                $index->isDefaultSearchable()
208            );
209            $this->map[$index->getIdentifier()] = $indexProperty;
210        }
211
212        return $this->map[$index->getIdentifier()];
213    }
214
215    private function getDynamicProperties(array $classes, Resource $resource): Iterator
216    {
217        $customProperties = [];
218        $customPropertiesCache = [];
219        $propertyIndexReferenceFactory = $this->propertyIndexReferenceFactory;
220
221        foreach ($classes as $class) {
222            $properties = \tao_helpers_form_GenerisFormFactory::getClassProperties(
223                $this->ontologyService->getClass($class)
224            );
225
226            $properties[OntologyRdfs::RDFS_LABEL] = $this->ontologyService->getProperty(OntologyRdfs::RDFS_LABEL);
227
228            foreach ($properties as $property) {
229                $fieldName = $propertyIndexReferenceFactory->create($property);
230
231                if ($fieldName === null) {
232                    continue;
233                }
234
235                $customPropertiesValues = $resource->getPropertyValuesCollection($property);
236                $customProperties[$fieldName][] = array_map(
237                    function (core_kernel_classes_Container $property): string {
238                        return tao_helpers_Uri::encode(
239                            $property instanceof Resource ? $property->getUri() : (string)$property
240                        );
241                    },
242                    $customPropertiesValues->toArray()
243                );
244
245                $customPropertiesCache[$fieldName] = $property;
246            }
247        }
248
249        foreach ($customPropertiesCache as $fieldName => $property) {
250            $rawValue = $this->getRawValue($property, $fieldName, $customProperties[$fieldName]);
251
252            if ($rawValue !== null) {
253                $customProperties[$propertyIndexReferenceFactory->createRaw($property)][] = $rawValue;
254            }
255        }
256
257        $customProperties = $this->normalizeAndFilterUniqueValues($customProperties);
258
259        return new ArrayIterator($customProperties);
260    }
261
262    private function getAccessProperties(Resource $resource): ?Iterator
263    {
264        if (!$this->permissionService instanceof ReverseRightLookupInterface) {
265            return null;
266        }
267
268        $accessRights = $this->permissionService->getResourceAccessData($resource->getUri());
269        $accessRightsURIs = ['read_access' => array_keys($accessRights)];
270
271        return new ArrayIterator($accessRightsURIs);
272    }
273
274    private function getParentClasses(array $types, string $path = ''): string
275    {
276        foreach ($types as $type) {
277            $path = $type->getUri() . $path;
278
279            if (!$this->isRootClass($type->getUri())) {
280                $path = ';' . $path;
281                $path = $this->getParentClasses($type->getParentClasses(), $path);
282            }
283        }
284
285        return $path;
286    }
287
288    private function isRootClass(string $uri): bool
289    {
290        return in_array($uri, self::ROOT_CLASSES);
291    }
292
293    private function normalizeAndFilterUniqueValues(array $customProperties): array
294    {
295        foreach ($customProperties as $fieldName => $value) {
296            $customProperties[$fieldName] = array_unique(array_merge(...(array_values($value))));
297        }
298
299        return array_filter($customProperties);
300    }
301
302    private function getRawValue(core_kernel_classes_Property $property, string $fieldName, array $values): ?array
303    {
304        if (strpos($fieldName, 'HTMLArea') === 0) {
305            $out = [];
306
307            foreach ($values as $value) {
308                $out[] = strip_tags((string)current($value));
309            }
310
311            return $out;
312        }
313
314        if (
315            strpos($fieldName, 'RadioBox') === 0 ||
316            strpos($fieldName, 'ComboBox') === 0 ||
317            strpos($fieldName, 'CheckBox') === 0 ||
318            strpos($fieldName, 'SearchTextBox') === 0 ||
319            strpos($fieldName, 'SearchDropdown') === 0
320        ) {
321            $out = [];
322
323            $request = new ValueCollectionSearchRequest();
324            $request = $this->remoteListPropertySpecification->isSatisfiedBy($property)
325                ? $request->setValueCollectionUri($property->getRange()->getUri())
326                : $request->setPropertyUri($property->getUri());
327
328            $list = $this->valueCollectionService->findAll(new ValueCollectionSearchInput($request));
329
330            foreach ($values as $value) {
331                foreach ($value as $subValue) {
332                    $listValue = $list->extractValueByUri(tao_helpers_Uri::decode((string)$subValue));
333
334                    if ($listValue) {
335                        $out[] = $listValue->getLabel();
336                    }
337                }
338            }
339
340            return [implode(', ', $out)];
341        }
342
343        return null;
344    }
345}