Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 159
0.00% covered (danger)
0.00%
0 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
QuerySerializer
0.00% covered (danger)
0.00%
0 / 159
0.00% covered (danger)
0.00%
0 / 15
2070
0.00% covered (danger)
0.00%
0 / 1
 pretty
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 prefixQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setCriteriaList
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 count
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 property
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 setOptions
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
12
 serialyse
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
12
 buildMatchPatterns
0.00% covered (danger)
0.00%
0 / 32
0.00% covered (danger)
0.00%
0 / 1
20
 buildWhereConditions
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
72
 buildCondition
0.00% covered (danger)
0.00%
0 / 26
0.00% covered (danger)
0.00%
0 / 1
56
 buildPropertyQuery
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 buildLanguagePattern
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
12
 buildReturn
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 buildOrderCondition
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 getMainNode
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
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 */
21
22namespace oat\generis\model\kernel\persistence\starsql\search;
23
24use Laminas\ServiceManager\ServiceLocatorAwareTrait;
25use Laudis\Neo4j\Databags\Statement;
26use oat\generis\model\data\ModelManager;
27use oat\generis\model\kernel\persistence\starsql\search\Command\CommandFactory;
28use oat\generis\model\OntologyRdf;
29use oat\generis\model\OntologyRdfs;
30use oat\search\base\QueryBuilderInterface;
31use oat\search\base\QueryCriterionInterface;
32use oat\search\base\QuerySerialyserInterface;
33use oat\search\helper\SupportedOperatorHelper;
34use oat\search\UsableTrait\DriverSensitiveTrait;
35use oat\search\UsableTrait\OptionsTrait;
36use WikibaseSolutions\CypherDSL\Expressions\Procedures\Procedure;
37use WikibaseSolutions\CypherDSL\Expressions\RawExpression;
38use WikibaseSolutions\CypherDSL\Patterns\Node;
39use WikibaseSolutions\CypherDSL\Query;
40use WikibaseSolutions\CypherDSL\QueryConvertible;
41use WikibaseSolutions\CypherDSL\Types\PropertyTypes\BooleanType;
42
43class QuerySerializer implements QuerySerialyserInterface
44{
45    use DriverSensitiveTrait;
46    use OptionsTrait;
47    use ServiceLocatorAwareTrait;
48
49    protected QueryBuilderInterface $criteriaList;
50
51    protected array $matchPatterns = [];
52
53    protected array $whereConditions = [];
54
55    protected array $returnStatements = [];
56
57    protected QueryConvertible $orderCondition;
58
59    protected array $parameters = [];
60
61    private string $userLanguage = '';
62
63    private string $defaultLanguage = '';
64
65    /**
66     * {@inheritDoc}
67     */
68    public function pretty($pretty)
69    {
70        // As library, we currently use doesn't support such option, we ignore it for now.
71        return $this;
72    }
73
74    public function prefixQuery()
75    {
76        //Do nothing as on this point we don't know if it is a normal, count or specific field query.
77        return $this;
78    }
79
80    /**
81     * {@inheritDoc}
82     */
83    public function setCriteriaList(QueryBuilderInterface $criteriaList)
84    {
85        $this->criteriaList = $criteriaList;
86        $this->setOptions($criteriaList->getOptions());
87
88        return $this;
89    }
90
91    public function count(bool $count = true): self
92    {
93        if ($count) {
94            return (new CountSerializer())
95                ->setServiceLocator($this->getServiceLocator())
96                ->setOptions($this->getOptions())
97                ->setDriverEscaper($this->getDriverEscaper())
98                ->setCriteriaList($this->criteriaList);
99        } else {
100            return $this;
101        }
102    }
103
104    public function property(string $propertyUri, bool $isDistinct = false): self
105    {
106        return (new PropertySerializer($propertyUri, $isDistinct))
107            ->setServiceLocator($this->getServiceLocator())
108            ->setOptions($this->getOptions())
109            ->setDriverEscaper($this->getDriverEscaper())
110            ->setCriteriaList($this->criteriaList);
111    }
112
113    public function setOptions(array $options)
114    {
115        $this->defaultLanguage = !empty($options['defaultLanguage'])
116            ? $options['defaultLanguage']
117            : DEFAULT_LANG;
118
119        $this->userLanguage = !empty($options['language'])
120            ? $options['language']
121            : $this->defaultLanguage;
122
123        return $this;
124    }
125
126    public function serialyse()
127    {
128        $subject = $this->getMainNode();
129
130        $this->buildMatchPatterns($subject);
131        $this->buildWhereConditions($subject);
132        $this->buildReturn($subject);
133        $this->buildOrderCondition($subject);
134
135        $query = Query::new()->match($this->matchPatterns);
136        $query->where($this->whereConditions);
137        $query->returning($this->returnStatements);
138
139        if (isset($this->orderCondition)) {
140            //Can't use dedicated order function as it doesn't support raw expressions
141            $query->raw('ORDER BY', $this->orderCondition->toQuery());
142        }
143
144        if ($this->criteriaList->getLimit() > 0) {
145            $query
146                ->skip((int)$this->criteriaList->getOffset())
147                ->limit((int)$this->criteriaList->getLimit());
148        }
149
150        return Statement::create($query->build(), $this->parameters);
151    }
152
153    protected function buildMatchPatterns(Node $subject): void
154    {
155        $queryOptions = $this->criteriaList->getOptions();
156
157        if (isset($queryOptions['type']) && isset($queryOptions['type']['resource'])) {
158            $mainClass = $queryOptions['type']['resource'];
159            $isRecursive = (bool)$queryOptions['type']['recursive'] ?? false;
160
161            $rdfTypes = array_unique(array_merge(
162                [$mainClass->getUri()],
163                $queryOptions['type']['extraClassUriList'] ?? []
164            ));
165
166            $parentClass = Query::node('Resource')->withVariable(Query::variable('parent'));
167            $parentPath = $subject->relationshipTo($parentClass, OntologyRdf::RDF_TYPE);
168            $parentWhere = $this->buildPropertyQuery(
169                $parentClass->property('uri'),
170                $rdfTypes,
171                SupportedOperatorHelper::IN
172            );
173
174            if ($isRecursive) {
175                $grandParentClass = Query::node('Resource')
176                    ->withVariable(Query::variable('grandParent'));
177                $subClassRelation = Query::relationshipTo()
178                    ->addType(OntologyRdfs::RDFS_SUBCLASSOF)
179                    ->withMinHops(0);
180
181                $parentPath = $parentPath->relationship($subClassRelation, $grandParentClass);
182                $parentWhere = $parentWhere->or(
183                    $this->buildPropertyQuery(
184                        $grandParentClass->property('uri'),
185                        $mainClass,
186                        SupportedOperatorHelper::EQUAL
187                    )
188                );
189            }
190
191            $this->matchPatterns[] = $parentPath;
192            $this->whereConditions[] = $parentWhere;
193        } else {
194            $this->matchPatterns[] = $subject;
195        }
196    }
197
198    protected function buildWhereConditions(Node $subject): void
199    {
200        $whereCondition = null;
201        foreach ($this->criteriaList->getStoredQueries() as $query) {
202            $operationList = $query->getStoredQueryCriteria();
203            $queryCondition = null;
204            /** @var QueryCriterionInterface $operation */
205            foreach ($operationList as $operation) {
206                $mainCondition = $this->buildCondition($operation, $subject);
207                foreach ($operation->getAnd() as $subOperation) {
208                    $subCondition = $this->buildCondition($subOperation, $subject, $operation);
209                    $mainCondition = $mainCondition->and($subCondition);
210                }
211
212                foreach ($operation->getOr() as $subOperation) {
213                    $subCondition = $this->buildCondition($subOperation, $subject, $operation);
214                    $mainCondition = $mainCondition->or($subCondition);
215                }
216
217                $queryCondition = ($queryCondition === null)
218                    ? $mainCondition
219                    : $queryCondition->and($mainCondition);
220            }
221
222            $whereCondition = ($whereCondition === null)
223                ? $queryCondition
224                : $whereCondition->or($queryCondition);
225        }
226
227        if ($whereCondition) {
228            $this->whereConditions[] = $whereCondition;
229        }
230    }
231
232    protected function buildCondition(
233        QueryCriterionInterface $operation,
234        Node $subject,
235        QueryCriterionInterface $parentOperation = null
236    ): BooleanType {
237        $propertyName = $operation->getName();
238
239        if (empty($propertyName) && $parentOperation) {
240            $propertyName = $parentOperation->getName();
241        }
242
243        $propertyName = $propertyName === QueryCriterionInterface::VIRTUAL_URI_FIELD
244            ? 'uri'
245            : $propertyName;
246
247        $property = ModelManager::getModel()->getProperty($propertyName);
248        if ($property->isRelationship()) {
249            $object = Query::node('Resource');
250            $this->matchPatterns[] = $subject->relationshipTo($object, $propertyName);
251
252            $predicate = $object->property('uri');
253            $values = $operation->getValue();
254
255            $fieldCondition = $this->buildPropertyQuery(
256                $predicate,
257                $values,
258                is_array($values) ? SupportedOperatorHelper::IN : SupportedOperatorHelper::EQUAL
259            );
260        } else {
261            $predicate = $subject->property($propertyName);
262            if ($property->isLgDependent()) {
263                $predicate = $this->buildLanguagePattern($predicate);
264            }
265            $fieldCondition = $this->buildPropertyQuery(
266                $predicate,
267                $operation->getValue(),
268                $operation->getOperator()
269            );
270        }
271
272        return $fieldCondition;
273    }
274
275    protected function buildPropertyQuery(
276        $predicate,
277        $values,
278        string $operation
279    ): BooleanType {
280        if ($values instanceof \core_kernel_classes_Resource) {
281            $values = $values->getUri();
282        }
283
284        $command = CommandFactory::createCommand($operation);
285        $condition = $command->buildQuery($predicate, $values);
286
287        $this->parameters = array_merge($this->parameters, $condition->getParameterList());
288        return $condition->getCondition();
289    }
290
291    protected function buildLanguagePattern(QueryConvertible $predicate): RawExpression
292    {
293        if (empty($this->userLanguage) || $this->userLanguage === $this->defaultLanguage) {
294            $resultExpression = Query::rawExpression(
295                sprintf(
296                    "n10s.rdf.getLangValue('%s', %s)",
297                    $this->defaultLanguage,
298                    $predicate->toQuery()
299                )
300            );
301        } else {
302            $resultExpression = Query::rawExpression(
303                sprintf(
304                    "coalesce(n10s.rdf.getLangValue('%s', %s), n10s.rdf.getLangValue('%s', %s))",
305                    $this->userLanguage,
306                    $predicate->toQuery(),
307                    $this->defaultLanguage,
308                    $predicate->toQuery()
309                )
310            );
311        }
312
313        return $resultExpression;
314    }
315
316    protected function buildReturn(Node $subject): void
317    {
318        $this->returnStatements[] = Query::rawExpression(
319            sprintf('DISTINCT %s', $subject->getVariable()->toQuery())
320        );
321    }
322
323    protected function buildOrderCondition(Node $subject): void
324    {
325        $sortCriteria = $this->criteriaList->getSort();
326
327        $sort = [];
328        foreach ($sortCriteria as $field => $order) {
329            $predicate = $subject->property($field);
330
331            $orderProperty = ModelManager::getModel()->getProperty($field);
332            if ($orderProperty->isLgDependent()) {
333                $predicate = $this->buildLanguagePattern($predicate);
334            }
335
336            $sort[] = $predicate->toQuery() . ((strtolower($order) === 'desc') ? ' DESCENDING' : '');
337        }
338
339        if (!empty($sort)) {
340            $this->orderCondition = Query::rawExpression(implode(', ', $sort));
341        }
342    }
343
344    /**
345     * @return Node
346     */
347    protected function getMainNode(): Node
348    {
349        $queryOptions = $this->criteriaList->getOptions();
350
351        if (isset($queryOptions['system_only']) && $queryOptions['system_only']) {
352            $node = Query::node('System');
353        } else {
354            $node = Query::node('Resource');
355        }
356
357        return $node->withVariable(Query::variable('subject'));
358    }
359}