Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 256
0.00% covered (danger)
0.00%
0 / 21
CRAP
0.00% covered (danger)
0.00%
0 / 1
core_kernel_persistence_starsql_Class
0.00% covered (danger)
0.00%
0 / 256
0.00% covered (danger)
0.00%
0 / 21
4032
0.00% covered (danger)
0.00%
0 / 1
 getSubClasses
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
20
 isSubClassOf
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getParentClasses
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 getProperties
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
42
 getInstances
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 setInstance
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setSubClassOf
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 setProperty
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createInstance
0.00% covered (danger)
0.00%
0 / 35
0.00% covered (danger)
0.00%
0 / 1
30
 createSubClass
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
30
 createProperty
0.00% covered (danger)
0.00%
0 / 24
0.00% covered (danger)
0.00%
0 / 1
20
 searchInstances
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 countInstances
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 getInstancesPropertyValues
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 unsetProperty
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 createInstanceWithProperties
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 deleteInstances
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
20
 getFilterQuery
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
6
 addFilters
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
90
 getClassFilter
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
20
 updateUri
0.00% covered (danger)
0.00%
0 / 3
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) 2023 (original work) Open Assessment Technologies SA ;
19 */
20
21declare(strict_types=1);
22
23use oat\generis\model\data\event\ClassPropertyCreatedEvent;
24use oat\generis\model\GenerisRdf;
25use oat\generis\model\kernel\persistence\Filter;
26use oat\generis\model\kernel\uri\UriProvider;
27use oat\generis\model\OntologyRdf;
28use oat\generis\model\OntologyRdfs;
29use oat\oatbox\event\EventManagerAwareTrait;
30use oat\search\helper\SupportedOperatorHelper;
31use oat\search\QueryBuilder;
32use WikibaseSolutions\CypherDSL\Query;
33
34use function WikibaseSolutions\CypherDSL\node;
35use function WikibaseSolutions\CypherDSL\parameter;
36use function WikibaseSolutions\CypherDSL\procedure;
37use function WikibaseSolutions\CypherDSL\query;
38use function WikibaseSolutions\CypherDSL\variable;
39
40class core_kernel_persistence_starsql_Class extends core_kernel_persistence_starsql_Resource implements
41    core_kernel_persistence_ClassInterface
42{
43    use EventManagerAwareTrait;
44
45    public function getSubClasses(core_kernel_classes_Class $resource, $recursive = false)
46    {
47        $uri = $resource->getUri();
48        $relationship = OntologyRdfs::RDFS_SUBCLASSOF;
49        if (!empty($recursive)) {
50            $query = <<<CYPHER
51                MATCH (startNode:Resource {uri: \$uri})
52                MATCH (descendantNode)-[:`{$relationship}`*]->(startNode)
53                RETURN descendantNode.uri
54CYPHER;
55        } else {
56            $query = <<<CYPHER
57                MATCH (startNode:Resource {uri: \$uri})
58                MATCH (descendantNode)-[:`{$relationship}`]->(startNode)
59                RETURN descendantNode.uri
60CYPHER;
61        }
62
63//        \common_Logger::i('getSubClasses(): ' . var_export($query, true));
64        $results = $this->getPersistence()->run($query, ['uri' => $uri]);
65        $returnValue = [];
66        foreach ($results as $result) {
67            $uri = $result->current();
68            if (!$uri) {
69                continue;
70            }
71            $subClass = $this->getModel()->getClass($uri);
72            $returnValue[$subClass->getUri()] = $subClass ;
73        }
74
75        return $returnValue;
76    }
77
78    public function isSubClassOf(core_kernel_classes_Class $resource, core_kernel_classes_Class $parentClass)
79    {
80        // @TODO would it be worth it to check direct relationship of node:IS_SUBCLASS_OF?
81        $parentSubClasses = $parentClass->getSubClasses(true);
82        foreach ($parentSubClasses as $subClass) {
83            if ($subClass->getUri() === $resource->getUri()) {
84                return true;
85            }
86        }
87
88        return false;
89    }
90
91    public function getParentClasses(core_kernel_classes_Class $resource, $recursive = false)
92    {
93        $uri = $resource->getUri();
94        $relationship = OntologyRdfs::RDFS_SUBCLASSOF;
95        if (!empty($recursive)) {
96            $query = <<<CYPHER
97                MATCH (startNode:Resource {uri: \$uri})
98                MATCH (startNode)-[:`{$relationship}`*]->(ancestorNode)
99                RETURN ancestorNode.uri
100CYPHER;
101        } else {
102            $query = <<<CYPHER
103                MATCH (startNode:Resource {uri: \$uri})
104                MATCH (startNode)-[:`{$relationship}`]->(ancestorNode)
105                RETURN ancestorNode.uri
106CYPHER;
107        }
108
109        $results = $this->getPersistence()->run($query, ['uri' => $uri]);
110        $returnValue = [];
111        foreach ($results as $result) {
112            $uri = $result->current();
113            $parentClass = $this->getModel()->getClass($uri);
114            $returnValue[$parentClass->getUri()] = $parentClass ;
115        }
116
117        return $returnValue;
118    }
119
120    public function getProperties(core_kernel_classes_Class $resource, $recursive = false)
121    {
122        $uri = $resource->getUri();
123        $relationship = OntologyRdfs::RDFS_DOMAIN;
124        $query = <<<CYPHER
125                MATCH (startNode:Resource {uri: \$uri})
126                MATCH (descendantNode)-[:`{$relationship}`]->(startNode)
127                RETURN descendantNode.uri
128CYPHER;
129        $results = $this->getPersistence()->run($query, ['uri' => $uri]);
130        $returnValue = [];
131        foreach ($results as $result) {
132            $uri = $result->current();
133            if (!$uri) {
134                continue;
135            }
136            $property = $this->getModel()->getProperty($uri);
137            $returnValue[$property->getUri()] = $property;
138        }
139
140        if ($recursive == true) {
141            $parentClasses = $this->getParentClasses($resource, true);
142            foreach ($parentClasses as $parent) {
143                if ($parent->getUri() != OntologyRdfs::RDFS_CLASS) {
144                    $returnValue = array_merge($returnValue, $parent->getProperties(false));
145                }
146            }
147        }
148
149        return $returnValue;
150    }
151
152    public function getInstances(core_kernel_classes_Class $resource, $recursive = false, $params = [])
153    {
154        $returnValue = [];
155
156        $params = array_merge($params, ['like' => false, 'recursive' => $recursive]);
157
158        $search = $this->getModel()->getSearchInterface();
159        $query = $this->getFilterQuery($search->query(), $resource, [], $params);
160
161        $resultList = $search->getGateway()->search($query);
162        foreach ($resultList as $resource) {
163            $returnValue[$resource->getUri()] = $resource;
164        }
165
166        return $returnValue;
167    }
168
169    /**
170     * @deprecated
171     */
172    public function setInstance(core_kernel_classes_Class $resource, core_kernel_classes_Resource $instance)
173    {
174        throw new common_exception_DeprecatedApiMethod(__METHOD__ . ' is deprecated. ');
175    }
176
177    public function setSubClassOf(core_kernel_classes_Class $resource, core_kernel_classes_Class $iClass): bool
178    {
179        $subClassOf = $this->getModel()->getProperty(OntologyRdfs::RDFS_SUBCLASSOF);
180        $returnValue = $this->setPropertyValue($resource, $subClassOf, $iClass->getUri());
181
182        return (bool) $returnValue;
183    }
184
185    /**
186     * @deprecated
187     */
188    public function setProperty(core_kernel_classes_Class $resource, core_kernel_classes_Property $property)
189    {
190        throw new common_exception_DeprecatedApiMethod(__METHOD__ . ' is deprecated. ');
191    }
192
193    public function createInstance(core_kernel_classes_Class $resource, $label = '', $comment = '', $uri = '')
194    {
195        if ($uri == '') {
196            $subject = $this->getServiceLocator()->get(UriProvider::SERVICE_ID)->provide();
197        } elseif ($uri[0] == '#') { //$uri should start with # and be well formed
198            $modelUri = common_ext_NamespaceManager::singleton()->getLocalNamespace()->getUri();
199            $subject = rtrim($modelUri, '#') . $uri;
200        } else {
201            $subject = $uri;
202        }
203
204        $session = $this->getServiceLocator()->get(\oat\oatbox\session\SessionService::SERVICE_ID)->getCurrentSession();
205        $sessionLanguage = $this->getDataLanguage();
206        $node = node()->addProperty('uri', $uriParameter = parameter())
207            ->addLabel('Resource');
208        if (!empty($label)) {
209            $node->addProperty(OntologyRdfs::RDFS_LABEL, [$label . '@' . $sessionLanguage]);
210        }
211        if (!empty($comment)) {
212            $node->addProperty(OntologyRdfs::RDFS_COMMENT, [$comment . '@' . $sessionLanguage]);
213        }
214
215        $node->addProperty(
216            'http://www.tao.lu/Ontologies/TAO.rdf#UpdatedBy',
217            (string)$session->getUser()->getIdentifier()
218        );
219        $node->addProperty(
220            'http://www.tao.lu/Ontologies/TAO.rdf#UpdatedAt',
221            procedure()::raw('timestamp')
222        );
223
224        $nodeForRelationship = node()->withVariable($variableForRelatedResource = variable());
225        $relatedResource = node('Resource')
226            ->withProperties(['uri' => $relatedUri = parameter()])
227            ->withVariable($variableForRelatedResource);
228        $node = $node->relationshipTo($nodeForRelationship, OntologyRdf::RDF_TYPE);
229
230        $query = query()
231            ->match($relatedResource)
232            ->create($node);
233        $this->getPersistence()->run(
234            $query->build(),
235            [$uriParameter->getParameter() => $subject, $relatedUri->getParameter() => $resource->getUri()]
236        );
237
238        return $this->getModel()->getResource($subject);
239    }
240
241    /**
242     * (non-PHPdoc)
243     * @see core_kernel_persistence_ClassInterface::createSubClass()
244     */
245    public function createSubClass(core_kernel_classes_Class $resource, $label = '', $comment = '', $uri = '')
246    {
247        if (!empty($uri)) {
248            common_Logger::w('Use of parameter uri in ' . __METHOD__ . ' is deprecated');
249        }
250        $uri = empty($uri) ? $this->getServiceLocator()->get(UriProvider::SERVICE_ID)->provide() : $uri;
251        $returnValue = $this->getModel()->getClass($uri);
252        $properties = [
253            OntologyRdfs::RDFS_SUBCLASSOF => $resource,
254        ];
255        if (!empty($label)) {
256            $properties[OntologyRdfs::RDFS_LABEL] = $label;
257        }
258        if (!empty($comment)) {
259            $properties[OntologyRdfs::RDFS_COMMENT] = $comment;
260        }
261
262        $returnValue->setPropertiesValues($properties);
263        return $returnValue;
264    }
265
266    public function createProperty(
267        core_kernel_classes_Class $resource,
268        $label = '',
269        $comment = '',
270        $isLgDependent = false
271    ) {
272        $returnValue = null;
273
274        $propertyClass = $this->getModel()->getClass(OntologyRdf::RDF_PROPERTY);
275        $properties = [
276            OntologyRdfs::RDFS_DOMAIN => $resource->getUri(),
277            GenerisRdf::PROPERTY_IS_LG_DEPENDENT => ((bool)$isLgDependent)
278                ? GenerisRdf::GENERIS_TRUE
279                : GenerisRdf::GENERIS_FALSE,
280        ];
281        if (!empty($label)) {
282            $properties[OntologyRdfs::RDFS_LABEL] = $label;
283        }
284        if (!empty($comment)) {
285            $properties[OntologyRdfs::RDFS_COMMENT] = $comment;
286        }
287        $propertyInstance = $propertyClass->createInstanceWithProperties($properties);
288
289        $returnValue = $this->getModel()->getProperty($propertyInstance->getUri());
290
291        $this->getEventManager()->trigger(
292            new ClassPropertyCreatedEvent(
293                $resource,
294                [
295                    'propertyUri' => $propertyInstance->getUri(),
296                    'propertyLabel' => $propertyInstance->getLabel(),
297                ]
298            )
299        );
300
301        return $returnValue;
302    }
303
304    /**
305     * @deprecated
306     */
307    public function searchInstances(core_kernel_classes_Class $resource, $propertyFilters = [], $options = [])
308    {
309        $returnValue = [];
310
311        $search = $this->getModel()->getSearchInterface();
312        $query = $this->getFilterQuery($search->query(), $resource, $propertyFilters, $options);
313        $resultList = $search->getGateway()->search($query);
314
315        foreach ($resultList as $resource) {
316            $returnValue[$resource->getUri()] = $resource;
317        }
318
319        return $returnValue;
320    }
321
322    public function countInstances(
323        core_kernel_classes_Class $resource,
324        $propertyFilters = [],
325        $options = []
326    ) {
327        $search = $this->getModel()->getSearchInterface();
328        $query = $this->getFilterQuery($search->query(), $resource, $propertyFilters, $options);
329
330        return $search->getGateway()->count($query);
331    }
332
333    public function getInstancesPropertyValues(
334        core_kernel_classes_Class $resource,
335        core_kernel_classes_Property $property,
336        $propertyFilters = [],
337        $options = []
338    ) {
339        $search = $this->getModel()->getSearchInterface();
340        $query = $this->getFilterQuery($search->query(), $resource, $propertyFilters, $options);
341
342        $resultSet = $search->getGateway()->searchTriples($query, $property->getUri(), $options['distinct'] ?? false);
343
344        $valueList = [];
345        /** @var core_kernel_classes_Triple $triple */
346        foreach ($resultSet as $triple) {
347            $valueList[] = common_Utils::toResource($triple->object);
348        }
349
350        return $valueList;
351    }
352
353    /**
354     * @deprecated
355     */
356    public function unsetProperty(core_kernel_classes_Class $resource, core_kernel_classes_Property $property)
357    {
358        throw new common_exception_DeprecatedApiMethod(__METHOD__ . ' is deprecated. ');
359    }
360
361    public function createInstanceWithProperties(core_kernel_classes_Class $type, $properties)
362    {
363        if (isset($properties[OntologyRdf::RDF_TYPE])) {
364            throw new core_kernel_persistence_Exception(
365                'Additional types in createInstanceWithProperties not permitted'
366            );
367        }
368
369        $properties[OntologyRdf::RDF_TYPE] = $type;
370        $returnValue = $this->getModel()->getResource(
371            $this->getServiceLocator()->get(UriProvider::SERVICE_ID)->provide()
372        );
373        $returnValue->setPropertiesValues($properties);
374
375        return $returnValue;
376    }
377
378    public function deleteInstances(core_kernel_classes_Class $resource, $resources, $deleteReference = false)
379    {
380        //TODO: We need to figure out if commented checks below is still correct.
381//        $class = $this->getModel()->getClass($resource->getUri());
382//        if (!$class->exists() || empty($resources)) {
383        if (empty($resources)) {
384            return false;
385        }
386
387        $uris = [];
388        foreach ($resources as $r) {
389            $uri = (($r instanceof core_kernel_classes_Resource) ? $r->getUri() : $r);
390            $uris[] = $uri;
391        }
392
393        $node = Query::node('Resource');
394        $query = Query::new()
395            ->match($node)
396            ->where($node->property('uri')->in($uris))
397            ->delete($node, $deleteReference);
398
399        $this->getPersistence()->run($query->build());
400
401        return true;
402    }
403
404    private function getFilterQuery(
405        QueryBuilder $query,
406        core_kernel_classes_Class $resource,
407        array $propertyFilters = [],
408        array $options = []
409    ): QueryBuilder {
410        $queryOptions = $query->getOptions();
411
412        $queryOptions = array_merge(
413            $queryOptions,
414            $this->getClassFilter($options, $resource, $queryOptions),
415            [
416                'language' => $options['lang'] ?? '',
417            ]
418        );
419
420        $query->setOptions($queryOptions);
421
422        $order = $options['order'] ?? '';
423        if (!empty($order)) {
424            $orderDir = $options['orderdir'] ?? 'ASC';
425            $query->sort([$order => strtolower($orderDir)]);
426        }
427        $query
428            ->setLimit($options['limit'] ?? 0)
429            ->setOffset($options['offset'] ?? 0);
430
431
432        $this->addFilters($query, $propertyFilters, $options);
433
434        return $query;
435    }
436
437    /**
438     * @param QueryBuilder $query
439     * @param array $propertyFilters
440     * @param array $options
441     */
442    private function addFilters(QueryBuilder $query, array $propertyFilters, array $options): void
443    {
444        $isLikeOperator = $options['like'] ?? true;
445        $and = (!isset($options['chaining']) || (strtolower($options['chaining']) === 'and'));
446
447        $criteria = $query->newQuery();
448        foreach ($propertyFilters as $filterProperty => $filterValue) {
449            if ($filterValue instanceof Filter) {
450                $propertyUri = $filterValue->getKey();
451                $operator = $filterValue->getOperator();
452                $mainValue = $filterValue->getValue();
453                $extraValues = $filterValue->getOrConditionValues();
454            } else {
455                $propertyUri = $filterProperty;
456                $operator = $isLikeOperator ? SupportedOperatorHelper::CONTAIN : SupportedOperatorHelper::EQUAL;
457
458                if (is_array($filterValue) && !empty($filterValue)) {
459                    $mainValue = array_shift($filterValue);
460                    $extraValues = $filterValue;
461                } else {
462                    $mainValue = $filterValue;
463                    $extraValues = [];
464                }
465            }
466
467            $criteria->addCriterion(
468                $propertyUri,
469                $operator,
470                $mainValue
471            );
472
473            foreach ($extraValues as $value) {
474                $criteria->addOr($value);
475            }
476
477            if (!$and) {
478                $query->setOr($criteria);
479                $criteria = $query->newQuery();
480            } else {
481                $query->setCriteria($criteria);
482            }
483        }
484    }
485
486    /**
487     * @param array $options
488     * @param core_kernel_classes_Class $resource
489     * @param array $queryOptions
490     *
491     * @return array
492     */
493    private function getClassFilter(array $options, core_kernel_classes_Class $resource, array $queryOptions): array
494    {
495        $rdftypes = [];
496
497        if (isset($options['additionalClasses'])) {
498            foreach ($options['additionalClasses'] as $aC) {
499                $rdftypes[] = ($aC instanceof core_kernel_classes_Resource) ? $aC->getUri() : $aC;
500            }
501        }
502
503        $rdftypes = array_unique($rdftypes);
504
505        $queryOptions['type'] = [
506            'resource' => $resource,
507            'recursive' => $options['recursive'] ?? false,
508            'extraClassUriList' => $rdftypes,
509        ];
510
511        return $queryOptions;
512    }
513
514    public function updateUri(core_kernel_classes_Class $resource, string $newUri): void
515    {
516        $query = <<<CYPHER
517            MATCH (n:Resource {uri: \$original_uri})
518            SET n.uri = \$uri
519CYPHER;
520
521        $this->getPersistence()->run($query, ['original_uri' => $resource->getUri(), 'uri' => $newUri]);
522    }
523}