GenericSparql::generateParentListQuery()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 48
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 48
rs 9.7666
c 0
b 0
f 0
cc 1
nc 1
nop 4
1
<?php
2
3
/**
4
 * Generates SPARQL queries and provides access to the SPARQL endpoint.
5
 */
6
class GenericSparql
7
{
8
    /**
9
     * SPARQL endpoint URL
10
     * @property string $endpoint
11
     */
12
    protected $endpoint;
13
    /**
14
     * A SPARQL Client eg. an EasyRDF instance.
15
     * @property EasyRdf\Sparql\Client $client
16
     */
17
    protected $client;
18
    /**
19
     * Graph uri.
20
     * @property object $graph
21
     */
22
    protected $graph;
23
    /**
24
     * A SPARQL query graph part template.
25
     * @property string $graphClause
26
     */
27
    protected $graphClause;
28
    /**
29
     * Model instance.
30
     * @property Model $model
31
     */
32
    protected $model;
33
34
    /**
35
     * Cache used to avoid expensive shorten() calls
36
     * @property array $qnamecache
37
     */
38
    private $qnamecache = array();
39
40
    /**
41
     * Cache used to avoid duplicate SPARQL queries. The cache must be
42
     * static so that all GenericSparql instances have access to the
43
     * same shared cache.
44
     * @property array $querycache
45
     */
46
    private static $querycache = array();
47
48
    /**
49
     * Requires the following three parameters.
50
     * @param string $endpoint SPARQL endpoint address.
51
     * @param string|null $graph Which graph to query: Either an URI, the special value "?graph"
52
     *                           to use the default graph, or NULL to not use a GRAPH clause.
53
     * @param object $model a Model instance.
54
     */
55
    public function __construct($endpoint, $graph, $model)
56
    {
57
        $this->endpoint = $endpoint;
58
        $this->graph = $graph;
59
        $this->model = $model;
60
61
        // create the EasyRDF SPARQL client instance to use
62
        $this->initializeHttpClient();
63
        $this->client = new EasyRdf\Sparql\Client($endpoint);
64
65
        // set graphClause so that it can be used by all queries
66
        if ($this->isDefaultEndpoint()) { // default endpoint; query any graph (and catch it in a variable)
67
            $this->graphClause = "GRAPH $graph";
68
        } elseif ($graph !== null) { // query a specific graph
69
            $this->graphClause = "GRAPH <$graph>";
70
        } else { // query the default graph
71
            $this->graphClause = "";
72
        }
73
74
    }
75
76
    /**
77
     * Returns prefix-definitions for a query
78
     *
79
     * @param string $query
80
     * @return string
81
    */
82
    protected function generateQueryPrefixes($query)
83
    {
84
        // Check for undefined prefixes
85
        $prefixes = '';
86
        foreach (EasyRdf\RdfNamespace::namespaces() as $prefix => $uri) {
87
            if (strpos($query, "{$prefix}:") !== false and
88
                strpos($query, "PREFIX {$prefix}:") === false
89
            ) {
90
                $prefixes .= "PREFIX {$prefix}: <{$uri}>\n";
91
            }
92
        }
93
        return $prefixes;
94
    }
95
96
    /**
97
     * Execute the SPARQL query using the SPARQL client, logging it as well.
98
     * @param string $query SPARQL query to perform
99
     * @return \EasyRdf\Sparql\Result|\EasyRdf\Graph query result
0 ignored issues
show
Bug introduced by
The type EasyRdf\Sparql\Result was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
100
     */
101
    protected function doQuery($query)
102
    {
103
        $queryId = sprintf("%05d", rand(0, 99999));
104
        $logger = $this->model->getLogger();
105
        $logger->info("[qid $queryId] SPARQL query:\n" . $this->generateQueryPrefixes($query) . "\n$query\n");
106
        $starttime = microtime(true);
107
        $result = $this->client->query($query);
108
        $elapsed = intval(round((microtime(true) - $starttime) * 1000));
109
        if (method_exists($result, 'numRows')) {
110
            $numRows = $result->numRows();
111
            $logger->info("[qid $queryId] result: $numRows rows returned in $elapsed ms");
112
        } else { // graph result
113
            $numTriples = $result->countTriples();
114
            $logger->info("[qid $queryId] result: $numTriples triples returned in $elapsed ms");
115
        }
116
        return $result;
117
    }
118
119
120
    /**
121
     * Execute the SPARQL query, if not found in query cache.
122
     * @param string $query SPARQL query to perform
123
     * @return \EasyRdf\Sparql\Result|\EasyRdf\Graph query result
124
     */
125
    protected function query($query)
126
    {
127
        $key = $this->endpoint . " " . $query;
128
        if (!array_key_exists($key, self::$querycache)) {
129
            self::$querycache[$key] = $this->doQuery($query);
130
        }
131
        return self::$querycache[$key];
132
    }
133
134
135
    /**
136
     * Generates FROM clauses for the queries
137
     * @param Vocabulary[]|null $vocabs
138
     * @return string
139
     */
140
    protected function generateFromClause($vocabs = null)
141
    {
142
        $clause = '';
143
        if (!$vocabs) {
144
            return $this->graph !== '?graph' && $this->graph !== null ? "FROM <$this->graph>" : '';
145
        }
146
        $graphs = $this->getVocabGraphs($vocabs);
147
        foreach ($graphs as $graph) {
148
            $clause .= "FROM NAMED <$graph> ";
149
        }
150
        return $clause;
151
    }
152
153
    protected function initializeHttpClient()
154
    {
155
        // configure the HTTP client used by EasyRdf\Sparql\Client
156
        $httpclient = EasyRdf\Http::getDefaultHttpClient();
157
        $httpclient->setConfig(array('timeout' => $this->model->getConfig()->getSparqlTimeout(),
158
                                     'useragent' => 'Skosmos'));
159
160
        // if special cache control (typically no-cache) was requested by the
161
        // client, set the same type of cache control headers also in subsequent
162
        // in the SPARQL requests (this is useful for performance testing)
163
        // @codeCoverageIgnoreStart
164
        $cacheControl = filter_input(INPUT_SERVER, 'HTTP_CACHE_CONTROL', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
0 ignored issues
show
Bug introduced by
The constant FILTER_SANITIZE_FULL_SPECIAL_CHARS was not found. Maybe you did not declare it correctly or list all dependencies?
Loading history...
165
        $pragma = filter_input(INPUT_SERVER, 'HTTP_PRAGMA', FILTER_SANITIZE_FULL_SPECIAL_CHARS);
166
        if ($cacheControl !== null || $pragma !== null) {
167
            $val = $pragma !== null ? $pragma : $cacheControl;
168
            $httpclient->setHeaders('Cache-Control', $val);
169
        }
170
        // @codeCoverageIgnoreEnd
171
172
        EasyRdf\Http::setDefaultHttpClient($httpclient); // actually redundant..
173
    }
174
175
    /**
176
     * Return true if this is the default SPARQL endpoint, used as the facade to query
177
     * all vocabularies.
178
     */
179
180
    protected function isDefaultEndpoint()
181
    {
182
        return !is_null($this->graph) && $this->graph[0] == '?';
183
    }
184
185
    /**
186
     * Returns the graph instance
187
     * @return object EasyRDF graph instance.
188
     */
189
    public function getGraph()
190
    {
191
        return $this->graph;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->graph also could return the type string which is incompatible with the documented return type object.
Loading history...
192
    }
193
194
    /**
195
     * Shorten a URI
196
     * @param string $uri URI to shorten
197
     * @return string shortened URI, or original URI if it cannot be shortened
198
     */
199
    private function shortenUri($uri)
200
    {
201
        if (!array_key_exists($uri, $this->qnamecache)) {
202
            $res = new EasyRdf\Resource($uri);
203
            $qname = $res->shorten(); // returns null on failure
204
            // only URIs in the SKOS namespace are shortened
205
            $this->qnamecache[$uri] = ($qname !== null && strpos($qname, "skos:") === 0) ? $qname : $uri;
206
        }
207
        return $this->qnamecache[$uri];
208
    }
209
210
211
    /**
212
     * Generates the sparql query for retrieving concept and collection counts in a vocabulary.
213
     * @return string sparql query
214
     */
215
    private function generateCountConceptsQuery($array, $group)
216
    {
217
        $fcl = $this->generateFromClause();
218
        $optional = $array ? "(<$array>) " : '';
219
        $optional .= $group ? "(<$group>)" : '';
220
        $query = <<<EOQ
221
      SELECT (COUNT(DISTINCT(?conc)) as ?c) ?type ?typelabel (COUNT(?depr) as ?deprcount) $fcl WHERE {
222
        VALUES (?value) { (skos:Concept) (skos:Collection) $optional }
223
  	    ?type rdfs:subClassOf* ?value
224
        { ?type ^a ?conc .
225
          OPTIONAL { ?conc owl:deprecated ?depr .
226
  		    FILTER (?depr = True)
227
          }
228
        } UNION {SELECT * WHERE {
229
            ?type rdfs:label ?typelabel
230
          }
231
        }
232
      } GROUP BY ?type ?typelabel
233
EOQ;
234
        return $query;
235
    }
236
237
    /**
238
     * Used for transforming the concept count query results.
239
     * @param EasyRdf\Sparql\Result $result query results to be transformed
240
     * @param string $lang language of labels
241
     * @return Array containing the label counts
242
     */
243
    private function transformCountConceptsResults($result, $lang)
244
    {
245
        $ret = array();
246
        foreach ($result as $row) {
247
            if (!isset($row->type)) {
248
                continue;
249
            }
250
            $typeURI = $row->type->getUri();
251
            $ret[$typeURI]['type'] = $typeURI;
252
253
            if (!isset($row->typelabel)) {
254
                $ret[$typeURI]['count'] = $row->c->getValue();
255
                $ret[$typeURI]['deprecatedCount'] = $row->deprcount->getValue();
256
            }
257
258
            if (isset($row->typelabel) && $row->typelabel->getLang() === $lang) {
259
                $ret[$typeURI]['label'] = $row->typelabel->getValue();
260
            }
261
262
        }
263
        return $ret;
264
    }
265
266
    /**
267
     * Used for counting number of concepts and collections in a vocabulary.
268
     * @param string $lang language of labels
269
     * @param string $array the uri of the concept array class, eg. isothes:ThesaurusArray
270
     * @param string $group the uri of the  concept group class, eg. isothes:ConceptGroup
271
     * @return array with number of concepts in this vocabulary per label
272
     */
273
    public function countConcepts($lang = null, $array = null, $group = null)
274
    {
275
        $query = $this->generateCountConceptsQuery($array, $group);
276
        $result = $this->query($query);
277
        return $this->transformCountConceptsResults($result, $lang);
278
    }
279
280
    /**
281
     * @param array $langs Languages to query for
282
     * @param string[] $props property names
283
     * @return string sparql query
284
     */
285
    private function generateCountLangConceptsQuery($langs, $classes, $props)
286
    {
287
        $gcl = $this->graphClause;
288
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
289
290
        $quote_string = function ($val) { return "'$val'"; };
291
        $quoted_values = array_map($quote_string, $langs);
292
        $langFilter = "FILTER(?lang IN (" . implode(',', $quoted_values) . "))";
293
294
        $values = $this->formatValues('?type', $classes, 'uri');
295
        $valuesProp = $this->formatValues('?prop', $props, null);
296
297
        $query = <<<EOQ
298
SELECT ?lang ?prop
299
  (COUNT(?label) as ?count)
300
WHERE {
301
  $gcl {
302
    $values
303
    $valuesProp
304
    ?conc a ?type .
305
    ?conc ?prop ?label .
306
    BIND(LANG(?label) AS ?lang)
307
    $langFilter
308
  }
309
}
310
GROUP BY ?lang ?prop ?type
311
EOQ;
312
        return $query;
313
    }
314
315
    /**
316
     * Transforms the CountLangConcepts results into an array of label counts.
317
     * @param EasyRdf\Sparql\Result $result query results to be transformed
318
     * @param array $langs Languages to query for
319
     * @param string[] $props property names
320
     */
321
    private function transformCountLangConceptsResults($result, $langs, $props)
322
    {
323
        $ret = array();
324
        // set default count to zero; overridden below if query found labels
325
        foreach ($langs as $lang) {
326
            foreach ($props as $prop) {
327
                $ret[$lang][$prop] = 0;
328
            }
329
        }
330
        foreach ($result as $row) {
331
            if (isset($row->lang) && isset($row->prop) && isset($row->count)) {
332
                $ret[$row->lang->getValue()][$row->prop->shorten()] +=
333
                $row->count->getValue();
334
            }
335
336
        }
337
        ksort($ret);
338
        return $ret;
339
    }
340
341
    /**
342
     * Counts the number of concepts in a easyRDF graph with a specific language.
343
     * @param array $langs Languages to query for
344
     * @return Array containing count of concepts for each language and property.
345
     */
346
    public function countLangConcepts($langs, $classes = null)
347
    {
348
        $props = array('skos:prefLabel', 'skos:altLabel', 'skos:hiddenLabel');
349
        $query = $this->generateCountLangConceptsQuery($langs, $classes, $props);
350
        // Count the number of terms in each language
351
        $result = $this->query($query);
352
        return $this->transformCountLangConceptsResults($result, $langs, $props);
353
    }
354
355
    /**
356
     * Formats a VALUES clause (SPARQL 1.1) which states that the variable should be bound to one
357
     * of the constants given.
358
     * @param string $varname variable name, e.g. "?uri"
359
     * @param array $values the values
360
     * @param string $type type of values: "uri", "literal" or null (determines quoting style)
361
     */
362
    protected function formatValues($varname, $values, $type = null)
363
    {
364
        $constants = array();
365
        foreach ($values as $val) {
366
            if ($type == 'uri') {
367
                $val = "<$val>";
368
            }
369
370
            if ($type == 'literal') {
371
                $val = "'$val'";
372
            }
373
374
            $constants[] = "($val)";
375
        }
376
        $values = implode(" ", $constants);
377
378
        return "VALUES ($varname) { $values }";
379
    }
380
381
    /**
382
     * Filters multiple instances of the same vocabulary from the input array.
383
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
384
     * @return \Vocabulary[]
385
     */
386
    private function filterDuplicateVocabs($vocabs)
387
    {
388
        // filtering duplicates
389
        $uniqueVocabs = array();
390
        if ($vocabs !== null && sizeof($vocabs) > 0) {
391
            foreach ($vocabs as $voc) {
392
                $uniqueVocabs[$voc->getId()] = $voc;
393
            }
394
        }
395
396
        return $uniqueVocabs;
397
    }
398
399
    /**
400
     * Generates a sparql query for one or more concept URIs
401
     * @param mixed $uris concept URI (string) or array of URIs
402
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
403
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
404
     * @return string sparql query
405
     */
406
    private function generateConceptInfoQuery($uris, $arrayClass, $vocabs)
407
    {
408
        $gcl = $this->graphClause;
409
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
410
        $values = $this->formatValues('?uri', $uris, 'uri');
411
        $uniqueVocabs = $this->filterDuplicateVocabs($vocabs);
412
        $valuesGraph = empty($vocabs) ? $this->formatValuesGraph($uniqueVocabs) : '';
413
414
        if ($arrayClass === null) {
415
            $construct = $optional = "";
416
        } else {
417
            // add information that can be used to format narrower concepts by
418
            // the array they belong to ("milk by source animal" use case)
419
            $construct = "\n ?x skos:member ?o . ?x skos:prefLabel ?xl . ?x a <$arrayClass> .";
420
            $optional = "\n OPTIONAL {
421
                      ?x skos:member ?o .
422
                      ?x a <$arrayClass> .
423
                      ?x skos:prefLabel ?xl .
424
                      FILTER NOT EXISTS {
425
                        ?x skos:member ?other .
426
                        MINUS { ?other skos:broader ?uri }
427
                      }
428
                    }";
429
        }
430
        $query = <<<EOQ
431
CONSTRUCT {
432
 ?s ?p ?uri .
433
 ?sp ?uri ?op .
434
 ?uri ?p ?o .
435
 ?p rdfs:label ?proplabel .
436
 ?p rdfs:comment ?propcomm .
437
 ?p skos:definition ?propdef .
438
 ?p rdfs:subPropertyOf ?pp .
439
 ?pp rdfs:label ?plabel .
440
 ?o a ?ot .
441
 ?o skos:prefLabel ?opl .
442
 ?o rdfs:label ?ol .
443
 ?o rdf:value ?ov .
444
 ?o skos:notation ?on .
445
 ?o ?oprop ?oval .
446
 ?o ?xlprop ?xlval .
447
 ?dt rdfs:label ?dtlabel .
448
 ?directgroup skos:member ?uri .
449
 ?parent skos:member ?group .
450
 ?group skos:prefLabel ?grouplabel .
451
 ?b1 rdf:first ?item .
452
 ?b1 rdf:rest ?b2 .
453
 ?item a ?it .
454
 ?item skos:prefLabel ?il .
455
 ?group a ?grouptype . $construct
456
} $fcl WHERE {
457
 $values
458
 $gcl {
459
  {
460
    ?s ?p ?uri .
461
    FILTER(!isBlank(?s))
462
    FILTER(?p != skos:inScheme)
463
    FILTER NOT EXISTS { ?s owl:deprecated true . }
464
  }
465
  UNION
466
  { ?sp ?uri ?op . }
467
  UNION
468
  {
469
    ?directgroup skos:member ?uri .
470
    ?group skos:member+ ?uri .
471
    ?group skos:prefLabel ?grouplabel .
472
    ?group a ?grouptype .
473
    OPTIONAL { ?parent skos:member ?group }
474
  }
475
  UNION
476
  {
477
   ?uri ?p ?o .
478
   OPTIONAL {
479
     ?uri skos:notation ?nVal .
480
     FILTER(isLiteral(?nVal))
481
     BIND(datatype(?nVal) AS ?dt)
482
     ?dt rdfs:label ?dtlabel
483
   }
484
   OPTIONAL {
485
     ?o rdf:rest* ?b1 .
486
     ?b1 rdf:first ?item .
487
     ?b1 rdf:rest ?b2 .
488
     OPTIONAL { ?item a ?it . }
489
     OPTIONAL { ?item skos:prefLabel ?il . }
490
   }
491
   OPTIONAL {
492
     { ?p rdfs:label ?proplabel . }
493
     UNION
494
     { ?p rdfs:comment ?propcomm . }
495
     UNION
496
     { ?p skos:definition ?propdef . }
497
     UNION
498
     { ?p rdfs:subPropertyOf ?pp . }
499
   }
500
   OPTIONAL {
501
     { ?o a ?ot . }
502
     UNION
503
     { ?o skos:prefLabel ?opl . }
504
     UNION
505
     { ?o rdfs:label ?ol . }
506
     UNION
507
     { ?o rdf:value ?ov . 
508
       OPTIONAL { ?o ?oprop ?oval . }
509
     }
510
     UNION
511
     { ?o skos:notation ?on . }
512
     UNION
513
     { ?o a skosxl:Label .
514
       ?o ?xlprop ?xlval }
515
   } $optional
516
  }
517
 }
518
}
519
$valuesGraph
520
EOQ;
521
        return $query;
522
    }
523
524
    /**
525
     * Transforms ConceptInfo query results into an array of Concept objects
526
     * @param EasyRdf\Graph $result query results to be transformed
527
     * @param array $uris concept URIs
528
     * @param \Vocabulary[] $vocabs array of Vocabulary object
529
     * @param string|null $clang content language
530
     * @return Concept[] array of Concept objects
531
     */
532
    private function transformConceptInfoResults($result, $uris, $vocabs, $clang)
533
    {
534
        $conceptArray = array();
535
        foreach ($uris as $index => $uri) {
536
            $conc = $result->resource($uri);
537
            if (is_array($vocabs)) {
538
                $vocab = (sizeof($vocabs) == 1) ? $vocabs[0] : $vocabs[$index];
539
            } else {
540
                $vocab = null;
541
            }
542
            $conceptArray[] = new Concept($this->model, $vocab, $conc, $result, $clang);
543
        }
544
        return $conceptArray;
545
    }
546
547
    /**
548
     * Returns information (as a graph) for one or more concept URIs
549
     * @param mixed $uris concept URI (string) or array of URIs
550
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
551
     * @param \Vocabulary[]|null $vocabs vocabularies to target
552
     * @return \EasyRdf\Graph
553
     */
554
    public function queryConceptInfoGraph($uris, $arrayClass = null, $vocabs = array())
555
    {
556
        // if just a single URI is given, put it in an array regardless
557
        if (!is_array($uris)) {
558
            $uris = array($uris);
559
        }
560
561
        $query = $this->generateConceptInfoQuery($uris, $arrayClass, $vocabs);
562
        $result = $this->query($query);
563
        return $result;
564
    }
565
566
    /**
567
     * Returns information (as an array of Concept objects) for one or more concept URIs
568
     * @param mixed $uris concept URI (string) or array of URIs
569
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
570
     * @param \Vocabulary[] $vocabs vocabularies to target
571
     * @param string|null $clang content language
572
     * @return Concept[]
573
     */
574
    public function queryConceptInfo($uris, $arrayClass = null, $vocabs = array(), $clang = null)
575
    {
576
        // if just a single URI is given, put it in an array regardless
577
        if (!is_array($uris)) {
578
            $uris = array($uris);
579
        }
580
        $result = $this->queryConceptInfoGraph($uris, $arrayClass, $vocabs);
581
        if ($result->isEmpty()) {
582
            return [];
583
        }
584
        return $this->transformConceptInfoResults($result, $uris, $vocabs, $clang);
585
    }
586
587
    /**
588
     * Generates the sparql query for queryTypes
589
     * @param string $lang
590
     * @return string sparql query
591
     */
592
    private function generateQueryTypesQuery($lang)
593
    {
594
        $fcl = $this->generateFromClause();
595
        $query = <<<EOQ
596
SELECT DISTINCT ?type ?label ?superclass $fcl
597
WHERE {
598
  {
599
    { BIND( skos:Concept as ?type ) }
600
    UNION
601
    { BIND( skos:Collection as ?type ) }
602
    UNION
603
    { BIND( isothes:ConceptGroup as ?type ) }
604
    UNION
605
    { BIND( isothes:ThesaurusArray as ?type ) }
606
    UNION
607
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Concept . }
608
    UNION
609
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Collection . }
610
  }
611
  OPTIONAL {
612
    ?type rdfs:label ?label .
613
    FILTER(langMatches(lang(?label), '$lang'))
614
  }
615
  OPTIONAL {
616
    ?type rdfs:subClassOf ?superclass .
617
  }
618
  FILTER EXISTS {
619
    ?s a ?type .
620
    ?s skos:prefLabel ?prefLabel .
621
  }
622
}
623
EOQ;
624
        return $query;
625
    }
626
627
    /**
628
     * Transforms the results into an array format.
629
     * @param EasyRdf\Sparql\Result $result
630
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
631
     */
632
    private function transformQueryTypesResults($result)
633
    {
634
        $ret = array();
635
        foreach ($result as $row) {
636
            $type = array();
637
            if (isset($row->label)) {
638
                $type['label'] = $row->label->getValue();
639
            }
640
641
            if (isset($row->superclass)) {
642
                $type['superclass'] = $row->superclass->getUri();
643
            }
644
645
            $ret[$row->type->getURI()] = $type;
646
        }
647
        return $ret;
648
    }
649
650
    /**
651
     * Retrieve information about types from the endpoint
652
     * @param string $lang
653
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
654
     */
655
    public function queryTypes($lang)
656
    {
657
        $query = $this->generateQueryTypesQuery($lang);
658
        $result = $this->query($query);
659
        return $this->transformQueryTypesResults($result);
660
    }
661
662
    /**
663
     * Generates the concept scheme query.
664
     * @param string $conceptscheme concept scheme URI
665
     * @return string sparql query
666
     */
667
    private function generateQueryConceptSchemeQuery($conceptscheme)
668
    {
669
        $fcl = $this->generateFromClause();
670
        $query = <<<EOQ
671
CONSTRUCT {
672
  <$conceptscheme> ?property ?value .
673
} $fcl WHERE {
674
  <$conceptscheme> ?property ?value .
675
  FILTER (?property != skos:hasTopConcept)
676
}
677
EOQ;
678
        return $query;
679
    }
680
681
    /**
682
     * Retrieves conceptScheme information from the endpoint.
683
     * @param string $conceptscheme concept scheme URI
684
     * @return \EasyRdf\Sparql\Result|\EasyRdf\Graph query result graph
685
     */
686
    public function queryConceptScheme($conceptscheme)
687
    {
688
        $query = $this->generateQueryConceptSchemeQuery($conceptscheme);
689
        return $this->query($query);
690
    }
691
692
    /**
693
     * Generates the queryConceptSchemes sparql query.
694
     * @param string $lang language of labels
695
     * @return string sparql query
696
     */
697
    private function generateQueryConceptSchemesQuery($lang)
698
    {
699
        $fcl = $this->generateFromClause();
700
        $query = <<<EOQ
701
SELECT ?cs ?label ?preflabel ?title ?domain ?domainLabel $fcl
702
WHERE {
703
 ?cs a skos:ConceptScheme .
704
 OPTIONAL{
705
    ?cs dcterms:subject ?domain.
706
    ?domain skos:prefLabel ?domainLabel.
707
    FILTER(langMatches(lang(?domainLabel), '$lang'))
708
}
709
 OPTIONAL {
710
   ?cs rdfs:label ?label .
711
   FILTER(langMatches(lang(?label), '$lang'))
712
 }
713
 OPTIONAL {
714
   ?cs skos:prefLabel ?preflabel .
715
   FILTER(langMatches(lang(?preflabel), '$lang'))
716
 }
717
 OPTIONAL {
718
   { ?cs dc11:title ?title }
719
   UNION
720
   { ?cs dc:title ?title }
721
   FILTER(langMatches(lang(?title), '$lang'))
722
 }
723
} 
724
ORDER BY ?cs
725
EOQ;
726
        return $query;
727
    }
728
729
    /**
730
     * Transforms the queryConceptScheme results into an array format.
731
     * @param EasyRdf\Sparql\Result $result
732
     * @return array
733
     */
734
    private function transformQueryConceptSchemesResults($result)
735
    {
736
        $ret = array();
737
        foreach ($result as $row) {
738
            $conceptscheme = array();
739
            if (isset($row->label)) {
740
                $conceptscheme['label'] = $row->label->getValue();
741
            }
742
743
            if (isset($row->preflabel)) {
744
                $conceptscheme['prefLabel'] = $row->preflabel->getValue();
745
            }
746
747
            if (isset($row->title)) {
748
                $conceptscheme['title'] = $row->title->getValue();
749
            }
750
            // add dct:subject and their labels in the result
751
            if (isset($row->domain) && isset($row->domainLabel)) {
752
                $conceptscheme['subject']['uri'] = $row->domain->getURI();
753
                $conceptscheme['subject']['prefLabel'] = $row->domainLabel->getValue();
754
            }
755
756
            $ret[$row->cs->getURI()] = $conceptscheme;
757
        }
758
        return $ret;
759
    }
760
761
    /**
762
     * return a list of skos:ConceptScheme instances in the given graph
763
     * @param string $lang language of labels
764
     * @return array Array with concept scheme URIs (string) as keys and labels (string) as values
765
     */
766
    public function queryConceptSchemes($lang)
767
    {
768
        $query = $this->generateQueryConceptSchemesQuery($lang);
769
        $result = $this->query($query);
770
        return $this->transformQueryConceptSchemesResults($result);
771
    }
772
773
    /**
774
     * Generate a VALUES clause for limiting the targeted graphs.
775
     * @param Vocabulary[]|null $vocabs the vocabularies to target
776
     * @return string[] array of graph URIs
777
     */
778
    protected function getVocabGraphs($vocabs)
779
    {
780
        if ($vocabs === null || sizeof($vocabs) == 0) {
781
            // searching from all vocabularies - limit to known graphs
782
            $vocabs = $this->model->getVocabularies();
783
        }
784
        $graphs = array();
785
        foreach ($vocabs as $voc) {
786
            $graph = $voc->getGraph();
787
            if (!is_null($graph) && !in_array($graph, $graphs)) {
788
                $graphs[] = $graph;
789
            }
790
        }
791
        return $graphs;
792
    }
793
794
    /**
795
     * Generate a VALUES clause for limiting the targeted graphs.
796
     * @param Vocabulary[]|null $vocabs array of Vocabulary objects to target
797
     * @return string VALUES clause, or "" if not necessary to limit
798
     */
799
    protected function formatValuesGraph($vocabs)
800
    {
801
        if (!$this->isDefaultEndpoint()) {
802
            return "";
803
        }
804
        $graphs = $this->getVocabGraphs($vocabs);
805
        return $this->formatValues('?graph', $graphs, 'uri');
806
    }
807
808
    /**
809
     * Generate a FILTER clause for limiting the targeted graphs.
810
     * @param array $vocabs array of Vocabulary objects to target
811
     * @return string FILTER clause, or "" if not necessary to limit
812
     */
813
    protected function formatFilterGraph($vocabs)
814
    {
815
        if (!$this->isDefaultEndpoint()) {
816
            return "";
817
        }
818
        $graphs = $this->getVocabGraphs($vocabs);
819
        $values = array();
820
        foreach ($graphs as $graph) {
821
            $values[] = "<$graph>";
822
        }
823
        if (count($values)) {
824
            return "FILTER (?graph IN (" . implode(',', $values) . "))";
825
        }
826
    }
827
828
    /**
829
     * Formats combined limit and offset clauses for the sparql query
830
     * @param int $limit maximum number of hits to retrieve; 0 for unlimited
831
     * @param int $offset offset of results to retrieve; 0 for beginning of list
832
     * @return string sparql query clauses
833
     */
834
    protected function formatLimitAndOffset($limit, $offset)
835
    {
836
        $limit = ($limit) ? 'LIMIT ' . $limit : '';
837
        $offset = ($offset) ? 'OFFSET ' . $offset : '';
838
        // eliminating whitespace and line changes when the conditions aren't needed.
839
        $limitandoffset = '';
840
        if ($limit && $offset) {
841
            $limitandoffset = "\n" . $limit . "\n" . $offset;
842
        } elseif ($limit) {
843
            $limitandoffset = "\n" . $limit;
844
        } elseif ($offset) {
845
            $limitandoffset = "\n" . $offset;
846
        }
847
848
        return $limitandoffset;
849
    }
850
851
    /**
852
     * Formats a sparql query clause for limiting the search to specific concept types.
853
     * @param array $types limit search to concepts of the given type(s)
854
     * @return string sparql query clause
855
     */
856
    protected function formatTypes($types)
857
    {
858
        $typePatterns = array();
859
        if (!empty($types)) {
860
            foreach ($types as $type) {
861
                $unprefixed = EasyRdf\RdfNamespace::expand($type);
862
                $typePatterns[] = "{ ?s a <$unprefixed> }";
863
            }
864
        }
865
866
        return implode(' UNION ', $typePatterns);
867
    }
868
869
    /**
870
     * @param string $prop property to include in the result eg. 'broader' or 'narrower'
871
     * @return string sparql query clause
872
     */
873
    private function formatPropertyCsvClause($prop)
874
    {
875
        # This expression creates a CSV row containing pairs of (uri,prefLabel) values.
876
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
877
        $clause = <<<EOV
878
(GROUP_CONCAT(DISTINCT CONCAT(
879
 '"', IF(isIRI(?$prop),STR(?$prop),''), '"', ',',
880
 '"', REPLACE(IF(BOUND(?{$prop}lab),?{$prop}lab,''), '"', '""'), '"', ',',
881
 '"', REPLACE(IF(isLiteral(?{$prop}),?{$prop},''), '"', '""'), '"'
882
); separator='\\n') as ?{$prop}s)
883
EOV;
884
        return $clause;
885
    }
886
887
    /**
888
     * @return string sparql query clause
889
     */
890
    private function formatPrefLabelCsvClause()
891
    {
892
        # This expression creates a CSV row containing pairs of (prefLabel, lang) values.
893
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
894
        $clause = <<<EOV
895
(GROUP_CONCAT(DISTINCT CONCAT(
896
 '"', STR(?pref), '"', ',', '"', lang(?pref), '"'
897
); separator='\\n') as ?preflabels)
898
EOV;
899
        return $clause;
900
    }
901
902
    /**
903
     * @param string $lang language code of the returned labels
904
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
905
     * @return array sparql query clause
906
     */
907
    protected function formatExtraFields($lang, $fields)
908
    {
909
        // extra variable expressions to request and extra fields to query for
910
        $ret = array('extravars' => '', 'extrafields' => '');
911
912
        if ($fields === null) {
913
            return $ret;
914
        }
915
916
        if (in_array('prefLabel', $fields)) {
917
            $ret['extravars'] .= $this->formatPreflabelCsvClause();
918
            $ret['extrafields'] .= <<<EOF
919
OPTIONAL {
920
  ?s skos:prefLabel ?pref .
921
}
922
EOF;
923
            // removing the prefLabel from the fields since it has been handled separately
924
            $fields = array_diff($fields, array('prefLabel'));
925
        }
926
927
        foreach ($fields as $field) {
928
            $ret['extravars'] .= $this->formatPropertyCsvClause($field);
929
            $ret['extrafields'] .= <<<EOF
930
OPTIONAL {
931
  ?s skos:$field ?$field .
932
  FILTER(!isLiteral(?$field)||langMatches(lang(?{$field}), '$lang'))
933
  OPTIONAL { ?$field skos:prefLabel ?{$field}lab . FILTER(langMatches(lang(?{$field}lab), '$lang')) }
934
}
935
EOF;
936
        }
937
938
        return $ret;
939
    }
940
941
    /**
942
     * Generate condition for matching labels in SPARQL
943
     * @param string $term search term
944
     * @param string $searchLang language code used for matching labels (null means any language)
945
     * @return string sparql query snippet
946
     */
947
    protected function generateConceptSearchQueryCondition($term, $searchLang)
948
    {
949
        # use appropriate matching function depending on query type: =, strstarts, strends or full regex
950
        if (preg_match('/^[^\*]+$/', $term)) { // exact query
951
            $term = str_replace('\\', '\\\\', $term); // quote slashes
952
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
953
            $filtercond = "LCASE(STR(?match)) = '$term'";
954
        } elseif (preg_match('/^[^\*]+\*$/', $term)) { // prefix query
955
            $term = substr($term, 0, -1); // remove the final asterisk
956
            $term = str_replace('\\', '\\\\', $term); // quote slashes
957
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
958
            $filtercond = "STRSTARTS(LCASE(STR(?match)), '$term')";
959
        } elseif (preg_match('/^\*[^\*]+$/', $term)) { // suffix query
960
            $term = substr($term, 1); // remove the preceding asterisk
961
            $term = str_replace('\\', '\\\\', $term); // quote slashes
962
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
963
            $filtercond = "STRENDS(LCASE(STR(?match)), '$term')";
964
        } else { // too complicated - have to use a regex
965
            # make sure regex metacharacters are not passed through
966
            $term = str_replace('\\', '\\\\', preg_quote($term));
967
            $term = str_replace('\\\\*', '.*', $term); // convert asterisk to regex syntax
968
            $term = str_replace('\'', '\\\'', $term); // ensure single quotes are quoted
969
            $filtercond = "REGEX(STR(?match), '^$term$', 'i')";
970
        }
971
972
        $labelcondMatch = ($searchLang) ? "&& (?prop = skos:notation || LANGMATCHES(lang(?match), ?langParam))" : "";
973
974
        return "?s ?prop ?match . FILTER ($filtercond $labelcondMatch)";
975
    }
976
977
978
    /**
979
     * Inner query for concepts using a search term.
980
     * @param string $term search term
981
     * @param string $lang language code of the returned labels
982
     * @param string $searchLang language code used for matching labels (null means any language)
983
     * @param string[] $props properties to target e.g. array('skos:prefLabel','skos:altLabel')
984
     * @param boolean $unique restrict results to unique concepts (default: false)
985
     * @return string sparql query
986
     */
987
    protected function generateConceptSearchQueryInner($term, $lang, $searchLang, $props, $unique, $filterGraph)
988
    {
989
        $valuesProp = $this->formatValues('?prop', $props);
990
        $textcond = $this->generateConceptSearchQueryCondition($term, $searchLang);
991
992
        $rawterm = str_replace(array('\\', '*', '"'), array('\\\\', '', '\"'), $term);
993
        // graph clause, if necessary
994
        $graphClause = $filterGraph != '' ? 'GRAPH ?graph' : '';
995
996
        // extra conditions for label language, if specified
997
        $labelcondLabel = ($lang) ? "LANGMATCHES(lang(?label), '$lang')" : "lang(?match) = '' || LANGMATCHES(lang(?label), lang(?match))";
998
        // if search language and UI/display language differ, must also consider case where there is no prefLabel in
999
        // the display language; in that case, should use the label with the same language as the matched label
1000
        $labelcondFallback = ($searchLang != $lang) ?
1001
          "OPTIONAL { # in case previous OPTIONAL block gives no labels\n" .
1002
          "?s skos:prefLabel ?label . FILTER (LANGMATCHES(LANG(?label), LANG(?match))) }" : "";
1003
1004
        //  Including the labels if there is no query term given.
1005
        if ($rawterm === '') {
1006
            $labelClause = "?s skos:prefLabel ?label .";
1007
            $labelClause = ($lang) ? $labelClause . " FILTER (LANGMATCHES(LANG(?label), '$lang'))" : $labelClause . "";
1008
            return $labelClause . " BIND(?label AS ?match)";
1009
        }
1010
1011
        /*
1012
         * This query does some tricks to obtain a list of unique concepts.
1013
         * From each match generated by the text index, a string such as
1014
         * "1en@example" is generated, where the first character is a number
1015
         * encoding the property and priority, then comes the language tag and
1016
         * finally the original literal after an @ sign. Of these, the MIN
1017
         * function is used to pick the best match for each concept. Finally,
1018
         * the structure is unpacked to get back the original string. Phew!
1019
         */
1020
        $hitvar = $unique ? '(MIN(?matchstr) AS ?hit)' : '(?matchstr AS ?hit)';
1021
        $hitgroup = $unique ? 'GROUP BY ?s ?label ?notation' : '';
1022
1023
        $langClause = $this->generateLangClause($searchLang);
1024
1025
        $query = <<<EOQ
1026
   SELECT DISTINCT ?s ?label ?notation $hitvar
1027
   WHERE {
1028
    $graphClause {
1029
     { 
1030
     $valuesProp
1031
     VALUES (?prop ?pri ?langParam) { (skos:prefLabel 1 $langClause) (skos:altLabel 3 $langClause) (skos:notation 5 '') (skos:hiddenLabel 7 $langClause)}
1032
     $textcond
1033
     ?s ?prop ?match }
1034
     OPTIONAL {
1035
      ?s skos:prefLabel ?label .
1036
      FILTER ($labelcondLabel)
1037
     } $labelcondFallback
1038
     BIND(IF(langMatches(LANG(?match),'$lang'), ?pri, ?pri+1) AS ?npri)
1039
     BIND(CONCAT(STR(?npri), LANG(?match), '@', STR(?match)) AS ?matchstr)
1040
     OPTIONAL { ?s skos:notation ?notation }
1041
    }
1042
    $filterGraph
1043
   }
1044
   $hitgroup
1045
EOQ;
1046
        return $query;
1047
    }
1048
    /**
1049
    *  This function can be overwritten in other SPARQL dialects for the possibility of handling the different language clauses
1050
     * @param string $lang
1051
     * @return string formatted language clause
1052
     */
1053
    protected function generateLangClause($lang)
1054
    {
1055
        return "'$lang'";
1056
    }
1057
1058
    /**
1059
     * Query for concepts using a search term.
1060
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
1061
     * @param boolean $unique restrict results to unique concepts (default: false)
1062
     * @param boolean $showDeprecated whether to include deprecated concepts in search results (default: false)
1063
     * @param ConceptSearchParameters $params
1064
     * @return string sparql query
1065
     */
1066
    protected function generateConceptSearchQuery($fields, $unique, $params, $showDeprecated = false)
1067
    {
1068
        $vocabs = $params->getVocabs();
1069
        $gcl = $this->graphClause;
1070
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
1071
        $formattedtype = $this->formatTypes($params->getTypeLimit());
1072
        $formattedfields = $this->formatExtraFields($params->getLang(), $fields);
1073
        $extravars = $formattedfields['extravars'];
1074
        $extrafields = $formattedfields['extrafields'];
1075
        $schemes = $params->getSchemeLimit();
1076
1077
        // limit the search to only requested concept schemes
1078
        $schemecond = '';
1079
        if (!empty($schemes)) {
1080
            $conditions = array();
1081
            foreach ($schemes as $scheme) {
1082
                $conditions[] = "{?s skos:inScheme <$scheme>}";
1083
            }
1084
            $schemecond = '{'.implode(" UNION ", $conditions).'}';
1085
        }
1086
        $filterDeprecated = "";
1087
        //show or hide deprecated concepts
1088
        if (!$showDeprecated) {
1089
            $filterDeprecated = "FILTER NOT EXISTS { ?s owl:deprecated true }";
1090
        }
1091
        // extra conditions for parent and group, if specified
1092
        $parentcond = ($params->getParentLimit()) ? "?s skos:broader+ <" . $params->getParentLimit() . "> ." : "";
1093
        $groupcond = ($params->getGroupLimit()) ? "<" . $params->getGroupLimit() . "> skos:member ?s ." : "";
1094
        $pgcond = $parentcond . $groupcond;
1095
1096
        $orderextra = $this->isDefaultEndpoint() ? $this->graph : '';
1097
1098
        # make VALUES clauses
1099
        $props = array('skos:prefLabel', 'skos:altLabel');
1100
1101
        //add notation into searchable data for the vocabularies which have been configured for it
1102
        if ($vocabs) {
1103
            $searchByNotation = false;
1104
            foreach ($vocabs as $vocab) {
1105
                if ($vocab->getConfig()->searchByNotation()) {
1106
                    $searchByNotation = true;
1107
                }
1108
            }
1109
            if ($searchByNotation) {
1110
                $props[] = 'skos:notation';
1111
            }
1112
        }
1113
1114
        if ($params->getHidden()) {
1115
            $props[] = 'skos:hiddenLabel';
1116
        }
1117
        $filterGraph = empty($vocabs) ? $this->formatFilterGraph($vocabs) : '';
1118
1119
        // remove futile asterisks from the search term
1120
        $term = $params->getSearchTerm();
1121
        while (strpos($term, '**') !== false) {
1122
            $term = str_replace('**', '*', $term);
1123
        }
1124
1125
        $labelpriority = <<<EOQ
1126
  FILTER(BOUND(?s))
1127
  BIND(STR(SUBSTR(?hit,1,1)) AS ?pri)
1128
  BIND(IF((SUBSTR(STRBEFORE(?hit, '@'),1) != ?pri), STRLANG(STRAFTER(?hit, '@'), SUBSTR(STRBEFORE(?hit, '@'),2)), STRAFTER(?hit, '@')) AS ?match)
1129
  BIND(IF((?pri = "1" || ?pri = "2") && ?match != ?label, ?match, ?unbound) as ?plabel)
1130
  BIND(IF((?pri = "3" || ?pri = "4"), ?match, ?unbound) as ?alabel)
1131
  BIND(IF((?pri = "7" || ?pri = "8"), ?match, ?unbound) as ?hlabel)
1132
EOQ;
1133
        $innerquery = $this->generateConceptSearchQueryInner($params->getSearchTerm(), $params->getLang(), $params->getSearchLang(), $props, $unique, $filterGraph);
1134
        if ($params->getSearchTerm() === '*' || $params->getSearchTerm() === '') {
1135
            $labelpriority = '';
1136
        }
1137
        $query = <<<EOQ
1138
SELECT DISTINCT ?s ?label ?plabel ?alabel ?hlabel ?graph ?notation (GROUP_CONCAT(DISTINCT STR(?type);separator=' ') as ?types) $extravars 
1139
$fcl
1140
WHERE {
1141
 $gcl {
1142
  {
1143
  $innerquery
1144
  }
1145
  $labelpriority
1146
  $formattedtype
1147
  { $pgcond 
1148
   ?s a ?type .
1149
   $extrafields $schemecond
1150
  }
1151
  $filterDeprecated
1152
 }
1153
 $filterGraph
1154
}
1155
GROUP BY ?s ?match ?label ?plabel ?alabel ?hlabel ?notation ?graph
1156
ORDER BY LCASE(STR(?match)) LANG(?match) $orderextra
1157
EOQ;
1158
        return $query;
1159
    }
1160
1161
    /**
1162
     * Transform a single concept search query results into the skosmos desired return format.
1163
     * @param $row SPARQL query result row
0 ignored issues
show
Bug introduced by
The type SPARQL was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
1164
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1165
     * @return array query result object
1166
     */
1167
    private function transformConceptSearchResult($row, $vocabs, $fields)
1168
    {
1169
        $hit = array();
1170
        $hit['uri'] = $row->s->getUri();
1171
1172
        if (isset($row->graph)) {
1173
            $hit['graph'] = $row->graph->getUri();
1174
        }
1175
1176
        foreach (explode(" ", $row->types->getValue()) as $typeuri) {
1177
            $hit['type'][] = $this->shortenUri($typeuri);
1178
        }
1179
1180
        if (!empty($fields)) {
1181
            foreach ($fields as $prop) {
1182
                $propname = $prop . 's';
1183
                if (isset($row->$propname)) {
1184
                    foreach (explode("\n", $row->$propname->getValue()) as $line) {
1185
                        $rdata = str_getcsv($line, ',', '"', '"');
1186
                        $propvals = array();
1187
                        if ($rdata[0] != '') {
1188
                            $propvals['uri'] = $rdata[0];
1189
                        }
1190
                        if ($rdata[1] != '') {
1191
                            $propvals['prefLabel'] = $rdata[1];
1192
                        }
1193
                        if ($rdata[2] != '') {
1194
                            $propvals = $rdata[2];
1195
                        }
1196
1197
                        $hit['skos:' . $prop][] = $propvals;
1198
                    }
1199
                }
1200
            }
1201
        }
1202
1203
1204
        if (isset($row->preflabels)) {
1205
            foreach (explode("\n", $row->preflabels->getValue()) as $line) {
1206
                $pref = str_getcsv($line, ',', '"', '"');
1207
                $hit['prefLabels'][$pref[1]] = $pref[0];
1208
            }
1209
        }
1210
1211
        foreach ($vocabs as $vocab) { // looping the vocabulary objects and asking these for a localname for the concept.
1212
            $localname = $vocab->getLocalName($hit['uri']);
1213
            if ($localname !== $hit['uri']) { // only passing the result forward if the uri didn't boomerang right back.
1214
                $hit['localname'] = $localname;
1215
                break; // stopping the search when we find one that returns something valid.
1216
            }
1217
        }
1218
1219
        if (isset($row->label)) {
1220
            $hit['prefLabel'] = $row->label->getValue();
1221
        }
1222
1223
        if (isset($row->label)) {
1224
            $hit['lang'] = $row->label->getLang();
1225
        }
1226
1227
        if (isset($row->notation)) {
1228
            $hit['notation'] = $row->notation->getValue();
1229
        }
1230
1231
        if (isset($row->plabel)) {
1232
            $hit['matchedPrefLabel'] = $row->plabel->getValue();
1233
            $hit['lang'] = $row->plabel->getLang();
1234
        } elseif (isset($row->alabel)) {
1235
            $hit['altLabel'] = $row->alabel->getValue();
1236
            $hit['lang'] = $row->alabel->getLang();
1237
        } elseif (isset($row->hlabel)) {
1238
            $hit['hiddenLabel'] = $row->hlabel->getValue();
1239
            $hit['lang'] = $row->hlabel->getLang();
1240
        }
1241
        return $hit;
1242
    }
1243
1244
    /**
1245
     * Transform the concept search query results into the skosmos desired return format.
1246
     * @param EasyRdf\Sparql\Result $results
1247
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1248
     * @return array query result object
1249
     */
1250
    private function transformConceptSearchResults($results, $vocabs, $fields)
1251
    {
1252
        $ret = array();
1253
1254
        foreach ($results as $row) {
1255
            if (!isset($row->s)) {
1256
                // don't break if query returns a single dummy result
1257
                continue;
1258
            }
1259
            $ret[] = $this->transformConceptSearchResult($row, $vocabs, $fields);
1260
        }
1261
        return $ret;
1262
    }
1263
1264
    /**
1265
     * Query for concepts using a search term.
1266
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1267
     * @param array $fields extra fields to include in the result (array of strings or null).
1268
     * @param boolean $unique restrict results to unique concepts
1269
     * @param ConceptSearchParameters $params
1270
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1271
     * @return array query result object
1272
     */
1273
    public function queryConcepts($vocabs, $fields, $unique, $params, $showDeprecated = false)
1274
    {
1275
        $query = $this->generateConceptSearchQuery($fields, $unique, $params, $showDeprecated);
1276
        $results = $this->query($query);
1277
        return $this->transformConceptSearchResults($results, $vocabs, $fields);
1278
    }
1279
1280
    /**
1281
     * Generates sparql query clauses used for creating the alphabetical index.
1282
     * @param string $letter the letter (or special class) to search for
1283
     * @return array of sparql query clause strings
1284
     */
1285
    private function formatFilterConditions($letter, $lang)
1286
    {
1287
        $useRegex = false;
1288
1289
        if ($letter == '*') {
1290
            $letter = '.*';
1291
            $useRegex = true;
1292
        } elseif ($letter == '0-9') {
1293
            $letter = '[0-9].*';
1294
            $useRegex = true;
1295
        } elseif ($letter == '!*') {
1296
            $letter = '[^\\\\p{L}\\\\p{N}].*';
1297
            $useRegex = true;
1298
        }
1299
1300
        # make text query clause
1301
        $lcletter = mb_strtolower($letter, 'UTF-8'); // convert to lower case, UTF-8 safe
1302
        if ($useRegex) {
1303
            $filtercondLabel = $lang ? "regex(str(?label), '^$letter$', 'i') && langMatches(lang(?label), '$lang')" : "regex(str(?label), '^$letter$', 'i')";
1304
            $filtercondALabel = $lang ? "regex(str(?alabel), '^$letter$', 'i') && langMatches(lang(?alabel), '$lang')" : "regex(str(?alabel), '^$letter$', 'i')";
1305
        } else {
1306
            $filtercondLabel = $lang ? "strstarts(lcase(str(?label)), '$lcletter') && langMatches(lang(?label), '$lang')" : "strstarts(lcase(str(?label)), '$lcletter')";
1307
            $filtercondALabel = $lang ? "strstarts(lcase(str(?alabel)), '$lcletter') && langMatches(lang(?alabel), '$lang')" : "strstarts(lcase(str(?alabel)), '$lcletter')";
1308
        }
1309
        return array('filterpref' => $filtercondLabel, 'filteralt' => $filtercondALabel);
1310
    }
1311
1312
    /**
1313
     * Generates the sparql query used for rendering the alphabetical index.
1314
     * @param string $letter the letter (or special class) to search for
1315
     * @param string $lang language of labels
1316
     * @param integer $limit limits the amount of results
1317
     * @param integer $offset offsets the result set
1318
     * @param array|null $classes
1319
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1320
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1321
     * @return string sparql query
1322
     */
1323
    protected function generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated = false, $qualifier = null)
1324
    {
1325
        $gcl = $this->graphClause;
1326
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1327
        $values = $this->formatValues('?type', $classes, 'uri');
1328
        $limitandoffset = $this->formatLimitAndOffset($limit, $offset);
1329
        $conditions = $this->formatFilterConditions($letter, $lang);
1330
        $filtercondLabel = $conditions['filterpref'];
1331
        $filtercondALabel = $conditions['filteralt'];
1332
        $qualifierClause = $qualifier ? "OPTIONAL { ?s <" . $qualifier->getURI() . "> ?qualifier }" : "";
1333
        $filterDeprecated = "";
1334
        if (!$showDeprecated) {
1335
            $filterDeprecated = "FILTER NOT EXISTS { ?s owl:deprecated true }";
1336
        }
1337
        $query = <<<EOQ
1338
SELECT DISTINCT ?s ?label ?alabel ?qualifier
1339
WHERE {
1340
  $gcl {
1341
    {
1342
      ?s skos:prefLabel ?label .
1343
      FILTER (
1344
        $filtercondLabel
1345
      )
1346
    }
1347
    UNION
1348
    {
1349
      {
1350
        ?s skos:altLabel ?alabel .
1351
        FILTER (
1352
          $filtercondALabel
1353
        )
1354
      }
1355
      {
1356
        ?s skos:prefLabel ?label .
1357
        FILTER (langMatches(lang(?label), '$lang'))
1358
      }
1359
    }
1360
    ?s a ?type .
1361
    $qualifierClause
1362
    $filterDeprecated
1363
    $values
1364
  }
1365
}
1366
ORDER BY LCASE(STR(COALESCE(?alabel, ?label))) STR(?s) LCASE(STR(?qualifier)) $limitandoffset
1367
EOQ;
1368
        return $query;
1369
    }
1370
1371
    /**
1372
     * Transforms the alphabetical list query results into an array format.
1373
     * @param EasyRdf\Sparql\Result $results
1374
     * @return array
1375
     */
1376
    private function transformAlphabeticalListResults($results)
1377
    {
1378
        $ret = array();
1379
1380
        foreach ($results as $row) {
1381
            if (!isset($row->s)) {
1382
                continue;
1383
            }
1384
            // don't break if query returns a single dummy result
1385
1386
            $hit = array();
1387
            $hit['uri'] = $row->s->getUri();
1388
1389
            $hit['localname'] = $row->s->localName();
1390
1391
            $hit['prefLabel'] = $row->label->getValue();
1392
            $hit['lang'] = $row->label->getLang();
1393
1394
            if (isset($row->alabel)) {
1395
                $hit['altLabel'] = $row->alabel->getValue();
1396
                $hit['lang'] = $row->alabel->getLang();
1397
            }
1398
1399
            if (isset($row->qualifier)) {
1400
                if ($row->qualifier instanceof EasyRdf\Literal) {
1401
                    $hit['qualifier'] = $row->qualifier->getValue();
1402
                } else {
1403
                    $hit['qualifier'] = $row->qualifier->localName();
1404
                }
1405
            }
1406
1407
            $ret[] = $hit;
1408
        }
1409
1410
        return $ret;
1411
    }
1412
1413
    /**
1414
     * Query for concepts with a term starting with the given letter. Also special classes '0-9' (digits),
1415
     * '*!' (special characters) and '*' (everything) are accepted.
1416
     * @param string $letter the letter (or special class) to search for
1417
     * @param string $lang language of labels
1418
     * @param integer $limit limits the amount of results
1419
     * @param integer $offset offsets the result set
1420
     * @param array $classes
1421
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1422
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1423
     */
1424
    public function queryConceptsAlphabetical($letter, $lang, $limit = null, $offset = null, $classes = null, $showDeprecated = false, $qualifier = null)
1425
    {
1426
        if ($letter === '') {
1427
            return array(); // special case: no letter given, return empty list
1428
        }
1429
        $query = $this->generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated, $qualifier);
1430
        $results = $this->query($query);
1431
        return $this->transformAlphabeticalListResults($results);
1432
    }
1433
1434
    /**
1435
     * Creates the query used for finding out which letters should be displayed in the alphabetical index.
1436
     * Note that we force the datatype of the result variable otherwise Virtuoso does not properly interpret the DISTINCT and we have duplicated results
1437
     * @param string $lang language
1438
     * @return string sparql query
1439
     */
1440
    private function generateFirstCharactersQuery($lang, $classes)
1441
    {
1442
        $gcl = $this->graphClause;
1443
        $classes = (isset($classes) && sizeof($classes) > 0) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1444
        $values = $this->formatValues('?type', $classes, 'uri');
1445
        $query = <<<EOQ
1446
SELECT DISTINCT (ucase(str(substr(?label, 1, 1))) as ?l) WHERE {
1447
  $gcl {
1448
    ?c skos:prefLabel ?label .
1449
    ?c a ?type
1450
    FILTER(langMatches(lang(?label), '$lang'))
1451
    $values
1452
  }
1453
}
1454
EOQ;
1455
        return $query;
1456
    }
1457
1458
    /**
1459
     * Transforms the first characters query results into an array format.
1460
     * @param EasyRdf\Sparql\Result $result
1461
     * @return array
1462
     */
1463
    private function transformFirstCharactersResults($result)
1464
    {
1465
        $ret = array();
1466
        foreach ($result as $row) {
1467
            $ret[] = $row->l->getValue();
1468
        }
1469
        return $ret;
1470
    }
1471
1472
    /**
1473
     * Query for the first characters (letter or otherwise) of the labels in the particular language.
1474
     * @param string $lang language
1475
     * @return array array of characters
1476
     */
1477
    public function queryFirstCharacters($lang, $classes = null)
1478
    {
1479
        $query = $this->generateFirstCharactersQuery($lang, $classes);
1480
        $result = $this->query($query);
1481
        return $this->transformFirstCharactersResults($result);
1482
    }
1483
1484
    /**
1485
     * @param string $uri
1486
     * @param string $lang
1487
     * @return string sparql query string
1488
     */
1489
    private function generateLabelQuery($uri, $lang)
1490
    {
1491
        $fcl = $this->generateFromClause();
1492
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?label), '$lang') )" : "";
1493
        $query = <<<EOQ
1494
SELECT ?label $fcl
1495
WHERE {
1496
  <$uri> a ?type .
1497
  OPTIONAL {
1498
    <$uri> skos:prefLabel ?label .
1499
    $labelcondLabel
1500
  }
1501
  OPTIONAL {
1502
    <$uri> rdfs:label ?label .
1503
    $labelcondLabel
1504
  }
1505
  OPTIONAL {
1506
    <$uri> dc:title ?label .
1507
    $labelcondLabel
1508
  }
1509
  OPTIONAL {
1510
    <$uri> dc11:title ?label .
1511
    $labelcondLabel
1512
  }
1513
}
1514
EOQ;
1515
        return $query;
1516
    }
1517
1518
1519
    /**
1520
     * @param string $uri
1521
     * @param string $lang
1522
     * @return string sparql query string
1523
     */
1524
    private function generateAllLabelsQuery($uri, $lang)
1525
    {
1526
        $fcl = $this->generateFromClause();
1527
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?val), '$lang') )" : "";
1528
        $query = <<<EOQ
1529
SELECT DISTINCT ?prop ?val $fcl
1530
WHERE {
1531
  <$uri> a ?type .
1532
  OPTIONAL {
1533
      <$uri> ?prop ?val .
1534
      $labelcondLabel
1535
  }
1536
  VALUES ?prop { skos:prefLabel skos:altLabel skos:hiddenLabel }
1537
}
1538
EOQ;
1539
        return $query;
1540
    }
1541
1542
    /**
1543
     * Query for a label (skos:prefLabel, rdfs:label, dc:title, dc11:title) of a resource.
1544
     * @param string $uri
1545
     * @param string $lang
1546
     * @return array array of labels (key: lang, val: label), or null if resource doesn't exist
1547
     */
1548
    public function queryLabel($uri, $lang)
1549
    {
1550
        $query = $this->generateLabelQuery($uri, $lang);
1551
        $result = $this->query($query);
1552
        $ret = array();
1553
        foreach ($result as $row) {
1554
            if (!isset($row->label)) {
1555
                // existing concept but no labels
1556
                return array();
1557
            }
1558
            $ret[$row->label->getLang()] = $row->label;
1559
        }
1560
1561
        if (sizeof($ret) > 0) {
1562
            // existing concept, with label(s)
1563
            return $ret;
1564
        } else {
1565
            // nonexistent concept
1566
            return null;
1567
        }
1568
    }
1569
1570
    /**
1571
     * Query for skos:prefLabels, skos:altLabels and skos:hiddenLabels of a resource.
1572
     * @param string $uri
1573
     * @param string $lang
1574
     * @return array array of prefLabels, altLabels and hiddenLabels - or null if resource doesn't exist
1575
     */
1576
    public function queryAllConceptLabels($uri, $lang)
1577
    {
1578
        $query = $this->generateAllLabelsQuery($uri, $lang);
1579
        $result = $this->query($query);
1580
1581
        if ($result->numRows() == 0) {
0 ignored issues
show
Bug introduced by
The method numRows() does not exist on EasyRdf\Graph. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

1581
        if ($result->/** @scrutinizer ignore-call */ numRows() == 0) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
1582
            // nonexistent concept
1583
            return null;
1584
        }
1585
1586
        $ret = array();
1587
        foreach ($result as $row) {
1588
            $labelName = $row->prop->localName();
1589
            if (isset($row->val)) {
1590
                $ret[$labelName][] = $row->val->getValue();
1591
            }
1592
        }
1593
        return $ret;
1594
    }
1595
1596
    /**
1597
     * Generates a SPARQL query to retrieve the super properties of a given property URI.
1598
     * Note this must be executed in the graph where this information is available.
1599
     * @param string $uri
1600
     * @return string sparql query string
1601
     */
1602
    private function generateSubPropertyOfQuery($uri)
1603
    {
1604
        $fcl = $this->generateFromClause();
1605
        $query = <<<EOQ
1606
SELECT ?superProperty $fcl
1607
WHERE {
1608
  <$uri> rdfs:subPropertyOf ?superProperty
1609
}
1610
EOQ;
1611
        return $query;
1612
    }
1613
1614
    /**
1615
     * Query the super properties of a provided property URI.
1616
     * @param string $uri URI of a propertyes
1617
     * @return array array super properties, or null if none exist
1618
     */
1619
    public function querySuperProperties($uri)
1620
    {
1621
        $query = $this->generateSubPropertyOfQuery($uri);
1622
        $result = $this->query($query);
1623
        $ret = array();
1624
        foreach ($result as $row) {
1625
            if (isset($row->superProperty)) {
1626
                $ret[] = $row->superProperty->getUri();
1627
            }
1628
1629
        }
1630
1631
        if (sizeof($ret) > 0) {
1632
            // return result
1633
            return $ret;
1634
        } else {
1635
            // no result, return null
1636
            return null;
1637
        }
1638
    }
1639
1640
1641
    /**
1642
     * Generates a sparql query for queryNotation.
1643
     * @param string $uri
1644
     * @return string sparql query
1645
     */
1646
    private function generateNotationQuery($uri)
1647
    {
1648
        $fcl = $this->generateFromClause();
1649
1650
        $query = <<<EOQ
1651
SELECT * $fcl
1652
WHERE {
1653
  <$uri> skos:notation ?notation .
1654
}
1655
EOQ;
1656
        return $query;
1657
    }
1658
1659
    /**
1660
     * Query for the notation of the concept (skos:notation) of a resource.
1661
     * @param string $uri
1662
     * @return string notation or null if it doesn't exist
1663
     */
1664
    public function queryNotation($uri)
1665
    {
1666
        $query = $this->generateNotationQuery($uri);
1667
        $result = $this->query($query);
1668
        foreach ($result as $row) {
1669
            if (isset($row->notation)) {
1670
                return $row->notation->getValue();
1671
            }
1672
        }
1673
        return null;
1674
    }
1675
1676
    /**
1677
     * Generates a sparql query for queryProperty.
1678
     * @param string $uri
1679
     * @param string $prop the name of the property eg. 'skos:broader'.
1680
     * @param string $lang
1681
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1682
     * @return string sparql query
1683
     */
1684
    private function generatePropertyQuery($uri, $prop, $lang, $anylang)
1685
    {
1686
        $fcl = $this->generateFromClause();
1687
        $anylang = $anylang ? "OPTIONAL { ?object skos:prefLabel ?label }" : "";
1688
1689
        $query = <<<EOQ
1690
SELECT * $fcl
1691
WHERE {
1692
  <$uri> a skos:Concept .
1693
  OPTIONAL {
1694
    <$uri> $prop ?object .
1695
    OPTIONAL {
1696
      ?object skos:prefLabel ?label .
1697
      FILTER (langMatches(lang(?label), "$lang"))
1698
    }
1699
    OPTIONAL {
1700
      ?object skos:prefLabel ?label .
1701
      FILTER (lang(?label) = "")
1702
    }
1703
    $anylang
1704
  }
1705
}
1706
EOQ;
1707
        return $query;
1708
    }
1709
1710
    /**
1711
     * Transforms the sparql query result into an array or null if the concept doesn't exist.
1712
     * @param EasyRdf\Sparql\Result $result
1713
     * @param string $lang
1714
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1715
     */
1716
    private function transformPropertyQueryResults($result, $lang)
1717
    {
1718
        $ret = array();
1719
        foreach ($result as $row) {
1720
            if (!isset($row->object)) {
1721
                return array();
1722
            }
1723
            // existing concept but no properties
1724
            if (isset($row->label)) {
1725
                if ($row->label->getLang() === $lang || array_key_exists($row->object->getUri(), $ret) === false) {
1726
                    $ret[$row->object->getUri()]['label'] = $row->label->getValue();
1727
                }
1728
1729
            } else {
1730
                $ret[$row->object->getUri()]['label'] = null;
1731
            }
1732
        }
1733
        if (sizeof($ret) > 0) {
1734
            return $ret;
1735
        }
1736
        // existing concept, with properties
1737
        else {
1738
            return null;
1739
        }
1740
        // nonexistent concept
1741
    }
1742
1743
    /**
1744
     * Query a single property of a concept.
1745
     * @param string $uri
1746
     * @param string $prop the name of the property eg. 'skos:broader'.
1747
     * @param string $lang
1748
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1749
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1750
     */
1751
    public function queryProperty($uri, $prop, $lang, $anylang = false)
1752
    {
1753
        $uri = is_array($uri) ? $uri[0] : $uri;
0 ignored issues
show
introduced by
The condition is_array($uri) is always false.
Loading history...
1754
        $query = $this->generatePropertyQuery($uri, $prop, $lang, $anylang);
1755
        $result = $this->query($query);
1756
        return $this->transformPropertyQueryResults($result, $lang);
1757
    }
1758
1759
    /**
1760
     * Query a single transitive property of a concept.
1761
     * @param string $uri
1762
     * @param array $props the name of the property eg. 'skos:broader'.
1763
     * @param string $lang
1764
     * @param integer $limit
1765
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1766
     * @return string sparql query
1767
     */
1768
    private function generateTransitivePropertyQuery($uri, $props, $lang, $limit, $anylang)
1769
    {
1770
        $uri = is_array($uri) ? $uri[0] : $uri;
0 ignored issues
show
introduced by
The condition is_array($uri) is always false.
Loading history...
1771
        $fcl = $this->generateFromClause();
1772
        $propertyClause = implode('|', $props);
1773
        $otherlang = $anylang ? "OPTIONAL { ?object skos:prefLabel ?label }" : "";
1774
        // need to do a SPARQL subquery because LIMIT needs to be applied /after/
1775
        // the direct relationships have been collapsed into one string
1776
        $query = <<<EOQ
1777
SELECT * $fcl
1778
WHERE {
1779
  SELECT ?object ?label (GROUP_CONCAT(STR(?dir);separator=' ') as ?direct)
1780
  WHERE {
1781
    <$uri> a skos:Concept .
1782
    OPTIONAL {
1783
      <$uri> $propertyClause* ?object .
1784
      OPTIONAL {
1785
        ?object $propertyClause ?dir .
1786
      }
1787
    }
1788
    OPTIONAL {
1789
      ?object skos:prefLabel ?label .
1790
      FILTER (langMatches(lang(?label), "$lang"))
1791
    }
1792
    $otherlang
1793
  }
1794
  GROUP BY ?object ?label
1795
}
1796
LIMIT $limit
1797
EOQ;
1798
        return $query;
1799
    }
1800
1801
    /**
1802
     * Transforms the sparql query result object into an array.
1803
     * @param EasyRdf\Sparql\Result $result
1804
     * @param string $lang
1805
     * @param string $fallbacklang language to use if label is not available in the preferred language
1806
     * @return array of property values (key: URI, val: label), or null if concept doesn't exist
1807
     */
1808
    private function transformTransitivePropertyResults($result, $lang, $fallbacklang)
1809
    {
1810
        $ret = array();
1811
        foreach ($result as $row) {
1812
            if (!isset($row->object)) {
1813
                return array();
1814
            }
1815
            // existing concept but no properties
1816
            if (isset($row->label)) {
1817
                $val = array('label' => $row->label->getValue());
1818
            } else {
1819
                $val = array('label' => null);
1820
            }
1821
            if (isset($row->direct) && $row->direct->getValue() != '') {
1822
                $val['direct'] = explode(' ', $row->direct->getValue());
1823
            }
1824
            // Preventing labels in a non preferred language overriding the preferred language.
1825
            if (isset($row->label) && $row->label->getLang() === $lang || array_key_exists($row->object->getUri(), $ret) === false) {
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: (IssetNode && $row->labe...tUri(), $ret) === false, Probably Intended Meaning: IssetNode && ($row->labe...Uri(), $ret) === false)
Loading history...
1826
                if (!isset($row->label) || $row->label->getLang() === $lang) {
1827
                    $ret[$row->object->getUri()] = $val;
1828
                } elseif ($row->label->getLang() === $fallbacklang) {
1829
                    $val['label'] .= ' (' . $row->label->getLang() . ')';
1830
                    $ret[$row->object->getUri()] = $val;
1831
                }
1832
            }
1833
        }
1834
1835
        // second iteration of results to find labels for the ones that didn't have one in the preferred languages
1836
        foreach ($result as $row) {
1837
            if (isset($row->object) && array_key_exists($row->object->getUri(), $ret) === false) {
1838
                $val = array('label' => $row->label->getValue());
1839
                if (isset($row->direct) && $row->direct->getValue() != '') {
1840
                    $val['direct'] = explode(' ', $row->direct->getValue());
1841
                }
1842
                $ret[$row->object->getUri()] = $val;
1843
            }
1844
        }
1845
1846
        if (sizeof($ret) > 0) {
1847
            return $ret;
1848
        }
1849
        // existing concept, with properties
1850
        else {
1851
            return null;
1852
        }
1853
        // nonexistent concept
1854
    }
1855
1856
    /**
1857
     * Query a single transitive property of a concept.
1858
     * @param string $uri
1859
     * @param array $props the property/properties.
1860
     * @param string $lang
1861
     * @param string $fallbacklang language to use if label is not available in the preferred language
1862
     * @param integer $limit
1863
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1864
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1865
     */
1866
    public function queryTransitiveProperty($uri, $props, $lang, $limit, $anylang = false, $fallbacklang = '')
1867
    {
1868
        $query = $this->generateTransitivePropertyQuery($uri, $props, $lang, $limit, $anylang);
1869
        $result = $this->query($query);
1870
        return $this->transformTransitivePropertyResults($result, $lang, $fallbacklang);
1871
    }
1872
1873
    /**
1874
     * Generates the query for a concepts skos:narrowers.
1875
     * @param string $uri
1876
     * @param string $lang
1877
     * @param string $fallback
1878
     * @return string sparql query
1879
     */
1880
    private function generateChildQuery($uri, $lang, $fallback, $props)
1881
    {
1882
        $uri = is_array($uri) ? $uri[0] : $uri;
0 ignored issues
show
introduced by
The condition is_array($uri) is always false.
Loading history...
1883
        $fcl = $this->generateFromClause();
1884
        $propertyClause = implode('|', $props);
1885
        $query = <<<EOQ
1886
SELECT ?child ?label ?child ?grandchildren ?notation $fcl WHERE {
1887
  <$uri> a skos:Concept .
1888
  OPTIONAL {
1889
    ?child $propertyClause <$uri> .
1890
    OPTIONAL {
1891
      ?child skos:prefLabel ?label .
1892
      FILTER (langMatches(lang(?label), "$lang"))
1893
    }
1894
    OPTIONAL {
1895
      ?child skos:prefLabel ?label .
1896
      FILTER (langMatches(lang(?label), "$fallback"))
1897
    }
1898
    OPTIONAL { # other language case
1899
      ?child skos:prefLabel ?label .
1900
    }
1901
    OPTIONAL {
1902
      ?child skos:notation ?notation .
1903
    }
1904
    BIND ( EXISTS { ?a $propertyClause ?child . } AS ?grandchildren )
1905
  }
1906
}
1907
EOQ;
1908
        return $query;
1909
    }
1910
1911
    /**
1912
     * Transforms the sparql result object into an array.
1913
     * @param EasyRdf\Sparql\Result $result
1914
     * @param string $lang
1915
     * @return array array of arrays describing each child concept, or null if concept doesn't exist
1916
     */
1917
    private function transformNarrowerResults($result, $lang)
1918
    {
1919
        $ret = array();
1920
        foreach ($result as $row) {
1921
            if (!isset($row->child)) {
1922
                return array();
1923
            }
1924
            // existing concept but no children
1925
1926
            $label = null;
1927
            if (isset($row->label)) {
1928
                if ($row->label->getLang() == $lang || strpos($row->label->getLang(), $lang . "-") == 0) {
1929
                    $label = $row->label->getValue();
1930
                } else {
1931
                    $label = $row->label->getValue() . " (" . $row->label->getLang() . ")";
1932
                }
1933
1934
            }
1935
            $childArray = array(
1936
                'uri' => $row->child->getUri(),
1937
                'prefLabel' => $label,
1938
                'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
1939
            );
1940
            if (isset($row->notation)) {
1941
                $childArray['notation'] = $row->notation->getValue();
1942
            }
1943
1944
            $ret[] = $childArray;
1945
        }
1946
        if (sizeof($ret) > 0) {
1947
            return $ret;
1948
        }
1949
        // existing concept, with children
1950
        else {
1951
            return null;
1952
        }
1953
        // nonexistent concept
1954
    }
1955
1956
    /**
1957
     * Query the narrower concepts of a concept.
1958
     * @param string $uri
1959
     * @param string $lang
1960
     * @param string $fallback
1961
     * @return array array of arrays describing each child concept, or null if concept doesn't exist
1962
     */
1963
    public function queryChildren($uri, $lang, $fallback, $props)
1964
    {
1965
        $query = $this->generateChildQuery($uri, $lang, $fallback, $props);
1966
        $result = $this->query($query);
1967
        return $this->transformNarrowerResults($result, $lang);
1968
    }
1969
1970
    /**
1971
     * Query the top concepts of a vocabulary.
1972
     * @param string $conceptSchemes concept schemes whose top concepts to query for
1973
     * @param string $lang language of labels
1974
     * @param string $fallback language to use if label is not available in the preferred language
1975
     */
1976
    public function queryTopConcepts($conceptSchemes, $lang, $fallback)
1977
    {
1978
        if (!is_array($conceptSchemes)) {
0 ignored issues
show
introduced by
The condition is_array($conceptSchemes) is always false.
Loading history...
1979
            $conceptSchemes = array($conceptSchemes);
1980
        }
1981
1982
        $values = $this->formatValues('?topuri', $conceptSchemes, 'uri');
1983
1984
        $fcl = $this->generateFromClause();
1985
        $query = <<<EOQ
1986
SELECT DISTINCT ?top ?topuri ?label ?notation ?children $fcl WHERE {
1987
  ?top skos:topConceptOf ?topuri .
1988
  OPTIONAL {
1989
    ?top skos:prefLabel ?label .
1990
    FILTER (langMatches(lang(?label), "$lang"))
1991
  }
1992
  OPTIONAL {
1993
    ?top skos:prefLabel ?label .
1994
    FILTER (langMatches(lang(?label), "$fallback"))
1995
  }
1996
  OPTIONAL { # fallback - other language case
1997
    ?top skos:prefLabel ?label .
1998
  }
1999
  OPTIONAL { ?top skos:notation ?notation . }
2000
  BIND ( EXISTS { ?top skos:narrower ?a . } AS ?children )
2001
  $values
2002
}
2003
EOQ;
2004
        $result = $this->query($query);
2005
        $ret = array();
2006
        foreach ($result as $row) {
2007
            if (isset($row->top) && isset($row->label)) {
2008
                $label = $row->label->getValue();
2009
                if ($row->label->getLang() && $row->label->getLang() !== $lang && strpos($row->label->getLang(), $lang . "-") !== 0) {
2010
                    $label .= ' (' . $row->label->getLang() . ')';
2011
                }
2012
                $top = array('uri' => $row->top->getUri(), 'topConceptOf' => $row->topuri->getUri(), 'label' => $label, 'hasChildren' => filter_var($row->children->getValue(), FILTER_VALIDATE_BOOLEAN));
2013
                if (isset($row->notation)) {
2014
                    $top['notation'] = $row->notation->getValue();
2015
                }
2016
2017
                $ret[] = $top;
2018
            }
2019
        }
2020
2021
        return $ret;
2022
    }
2023
2024
    /**
2025
     * Generates a sparql query for finding the hierarchy for a concept.
2026
     * A concept may be a top concept in multiple schemes, returned as a single whitespace-separated literal.
2027
     * @param string $uri concept uri.
2028
     * @param string $lang
2029
     * @param string $fallback language to use if label is not available in the preferred language
2030
     * @return string sparql query
2031
     */
2032
    private function generateParentListQuery($uri, $lang, $fallback, $props)
2033
    {
2034
        $fcl = $this->generateFromClause();
2035
        $propertyClause = implode('|', $props);
2036
        $query = <<<EOQ
2037
SELECT ?broad ?parent ?children ?grandchildren
2038
(SAMPLE(?lab) as ?label) (SAMPLE(?childlab) as ?childlabel) (GROUP_CONCAT(?topcs; separator=" ") as ?tops) 
2039
(SAMPLE(?nota) as ?notation) (SAMPLE(?childnota) as ?childnotation) $fcl
2040
WHERE {
2041
  <$uri> a skos:Concept .
2042
  OPTIONAL {
2043
    <$uri> $propertyClause* ?broad .
2044
    OPTIONAL {
2045
      ?broad skos:prefLabel ?lab .
2046
      FILTER (langMatches(lang(?lab), "$lang"))
2047
    }
2048
    OPTIONAL {
2049
      ?broad skos:prefLabel ?lab .
2050
      FILTER (langMatches(lang(?lab), "$fallback"))
2051
    }
2052
    OPTIONAL { # fallback - other language case
2053
      ?broad skos:prefLabel ?lab .
2054
    }
2055
    OPTIONAL { ?broad skos:notation ?nota . }
2056
    OPTIONAL { ?broad $propertyClause ?parent . }
2057
    OPTIONAL { ?broad skos:narrower ?children .
2058
      OPTIONAL {
2059
        ?children skos:prefLabel ?childlab .
2060
        FILTER (langMatches(lang(?childlab), "$lang"))
2061
      }
2062
      OPTIONAL {
2063
        ?children skos:prefLabel ?childlab .
2064
        FILTER (langMatches(lang(?childlab), "$fallback"))
2065
      }
2066
      OPTIONAL { # fallback - other language case
2067
        ?children skos:prefLabel ?childlab .
2068
      }
2069
      OPTIONAL {
2070
        ?children skos:notation ?childnota .
2071
      }
2072
    }
2073
    BIND ( EXISTS { ?children skos:narrower ?a . } AS ?grandchildren )
2074
    OPTIONAL { ?broad skos:topConceptOf ?topcs . }
2075
  }
2076
}
2077
GROUP BY ?broad ?parent ?member ?children ?grandchildren
2078
EOQ;
2079
        return $query;
2080
    }
2081
2082
    /**
2083
     * Transforms the result into an array.
2084
     * @param EasyRdf\Sparql\Result
2085
     * @param string $lang
2086
     * @return array|null an array for the REST controller to encode.
2087
     */
2088
    private function transformParentListResults($result, $lang)
2089
    {
2090
        $ret = array();
2091
        foreach ($result as $row) {
2092
            if (!isset($row->broad)) {
2093
                // existing concept but no broaders
2094
                return array();
2095
            }
2096
            $uri = $row->broad->getUri();
2097
            if (!isset($ret[$uri])) {
2098
                $ret[$uri] = array('uri' => $uri);
2099
            }
2100
            if (isset($row->exact)) {
2101
                $ret[$uri]['exact'] = $row->exact->getUri();
2102
            }
2103
            if (isset($row->tops)) {
2104
                $topConceptsList = explode(" ", $row->tops->getValue());
2105
                // sort to guarantee an alphabetical ordering of the URI
2106
                sort($topConceptsList);
2107
                $ret[$uri]['tops'] = $topConceptsList;
2108
            }
2109
            if (isset($row->children)) {
2110
                if (!isset($ret[$uri]['narrower'])) {
2111
                    $ret[$uri]['narrower'] = array();
2112
                }
2113
2114
                $label = null;
2115
                if (isset($row->childlabel)) {
2116
                    $label = $row->childlabel->getValue();
2117
                    if ($row->childlabel->getLang() !== $lang && strpos($row->childlabel->getLang(), $lang . "-") !== 0) {
2118
                        $label .= " (" . $row->childlabel->getLang() . ")";
2119
                    }
2120
2121
                }
2122
2123
                $childArr = array(
2124
                    'uri' => $row->children->getUri(),
2125
                    'label' => $label,
2126
                    'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
2127
                );
2128
                if (isset($row->childnotation)) {
2129
                    $childArr['notation'] = $row->childnotation->getValue();
2130
                }
2131
2132
                if (!in_array($childArr, $ret[$uri]['narrower'])) {
2133
                    $ret[$uri]['narrower'][] = $childArr;
2134
                }
2135
2136
            }
2137
            if (isset($row->label)) {
2138
                $preflabel = $row->label->getValue();
2139
                if ($row->label->getLang() && $row->label->getLang() !== $lang && strpos($row->label->getLang(), $lang . "-") !== 0) {
2140
                    $preflabel .= ' (' . $row->label->getLang() . ')';
2141
                }
2142
2143
                $ret[$uri]['prefLabel'] = $preflabel;
2144
            }
2145
            if (isset($row->notation)) {
2146
                $ret[$uri]['notation'] = $row->notation->getValue();
2147
            }
2148
2149
            if (isset($row->parent) && (isset($ret[$uri]['broader']) && !in_array($row->parent->getUri(), $ret[$uri]['broader']))) {
2150
                $ret[$uri]['broader'][] = $row->parent->getUri();
2151
            } elseif (isset($row->parent) && !isset($ret[$uri]['broader'])) {
2152
                $ret[$uri]['broader'][] = $row->parent->getUri();
2153
            }
2154
        }
2155
        if (sizeof($ret) > 0) {
2156
            // existing concept, with children
2157
            return $ret;
2158
        } else {
2159
            // nonexistent concept
2160
            return null;
2161
        }
2162
    }
2163
2164
    /**
2165
     * Query for finding the hierarchy for a concept.
2166
     * @param string $uri concept uri.
2167
     * @param string $lang
2168
     * @param string $fallback language to use if label is not available in the preferred language
2169
     * @param array $props the hierarchy property/properties to use
2170
     * @return an array for the REST controller to encode.
2171
     */
2172
    public function queryParentList($uri, $lang, $fallback, $props)
2173
    {
2174
        $query = $this->generateParentListQuery($uri, $lang, $fallback, $props);
2175
        $result = $this->query($query);
2176
        return $this->transformParentListResults($result, $lang);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->transformP...Results($result, $lang) also could return the type array which is incompatible with the documented return type an.
Loading history...
2177
    }
2178
2179
    /**
2180
     * return a list of concept group instances, sorted by label
2181
     * @param string $groupClass URI of concept group class
2182
     * @param string $lang language of labels to return
2183
     * @return string sparql query
2184
     */
2185
    private function generateConceptGroupsQuery($groupClass, $lang)
2186
    {
2187
        $fcl = $this->generateFromClause();
2188
        $query = <<<EOQ
2189
SELECT ?group (GROUP_CONCAT(DISTINCT STR(?child);separator=' ') as ?children) ?label ?members ?notation $fcl
2190
WHERE {
2191
  ?group a <$groupClass> .
2192
  OPTIONAL { ?group skos:member|isothes:subGroup ?child .
2193
             ?child a <$groupClass> }
2194
  BIND(EXISTS{?group skos:member ?submembers} as ?members)
2195
  OPTIONAL { ?group skos:prefLabel ?label }
2196
  OPTIONAL { ?group rdfs:label ?label }
2197
  FILTER (langMatches(lang(?label), '$lang'))
2198
  OPTIONAL { ?group skos:notation ?notation }
2199
}
2200
GROUP BY ?group ?label ?members ?notation
2201
ORDER BY lcase(?label)
2202
EOQ;
2203
        return $query;
2204
    }
2205
2206
    /**
2207
     * Transforms the sparql query result into an array.
2208
     * @param EasyRdf\Sparql\Result $result
2209
     * @return array
2210
     */
2211
    private function transformConceptGroupsResults($result)
2212
    {
2213
        $ret = array();
2214
        foreach ($result as $row) {
2215
            if (!isset($row->group)) {
2216
                # no groups found, see issue #357
2217
                continue;
2218
            }
2219
            $group = array('uri' => $row->group->getURI());
2220
            if (isset($row->label)) {
2221
                $group['prefLabel'] = $row->label->getValue();
2222
            }
2223
2224
            if (isset($row->children)) {
2225
                $group['childGroups'] = explode(' ', $row->children->getValue());
2226
            }
2227
2228
            if (isset($row->members)) {
2229
                $group['hasMembers'] = $row->members->getValue();
2230
            }
2231
2232
            if (isset($row->notation)) {
2233
                $group['notation'] = $row->notation->getValue();
2234
            }
2235
2236
            $ret[] = $group;
2237
        }
2238
        return $ret;
2239
    }
2240
2241
    /**
2242
     * return a list of concept group instances, sorted by label
2243
     * @param string $groupClass URI of concept group class
2244
     * @param string $lang language of labels to return
2245
     * @return array Result array with group URI as key and group label as value
2246
     */
2247
    public function listConceptGroups($groupClass, $lang)
2248
    {
2249
        $query = $this->generateConceptGroupsQuery($groupClass, $lang);
2250
        $result = $this->query($query);
2251
        return $this->transformConceptGroupsResults($result);
2252
    }
2253
2254
    /**
2255
     * Generates the sparql query for listConceptGroupContents
2256
     * @param string $groupClass URI of concept group class
2257
     * @param string $group URI of the concept group instance
2258
     * @param string $lang language of labels to return
2259
     * @param boolean $showDeprecated whether to include deprecated in the result
2260
     * @return string sparql query
2261
     */
2262
    private function generateConceptGroupContentsQuery($groupClass, $group, $lang, $showDeprecated = false)
2263
    {
2264
        $fcl = $this->generateFromClause();
2265
        $filterDeprecated = "";
2266
        if (!$showDeprecated) {
2267
            $filterDeprecated = "  FILTER NOT EXISTS { ?conc owl:deprecated true }";
2268
        }
2269
        $query = <<<EOQ
2270
SELECT ?conc ?super ?label ?members ?type ?notation $fcl
2271
WHERE {
2272
 <$group> a <$groupClass> .
2273
 { <$group> skos:member ?conc . } UNION { ?conc isothes:superGroup <$group> }
2274
$filterDeprecated
2275
 ?conc a ?type .
2276
 OPTIONAL { ?conc skos:prefLabel ?label .
2277
  FILTER (langMatches(lang(?label), '$lang'))
2278
 }
2279
 OPTIONAL { ?conc skos:prefLabel ?label . }
2280
 OPTIONAL { ?conc skos:notation ?notation }
2281
 BIND(EXISTS{?submembers isothes:superGroup ?conc} as ?super)
2282
 BIND(EXISTS{?conc skos:member ?submembers} as ?members)
2283
} ORDER BY lcase(?label)
2284
EOQ;
2285
        return $query;
2286
    }
2287
2288
    /**
2289
     * Transforms the sparql query result into an array.
2290
     * @param EasyRdf\Sparql\Result $result
2291
     * @param string $lang language of labels to return
2292
     * @return array
2293
     */
2294
    private function transformConceptGroupContentsResults($result, $lang)
2295
    {
2296
        $ret = array();
2297
        $values = array();
2298
        foreach ($result as $row) {
2299
            if (!array_key_exists($row->conc->getURI(), $values)) {
2300
                $values[$row->conc->getURI()] = array(
2301
                    'uri' => $row->conc->getURI(),
2302
                    'isSuper' => $row->super->getValue(),
2303
                    'hasMembers' => $row->members->getValue(),
2304
                    'type' => array($row->type->shorten()),
2305
                );
2306
                if (isset($row->label)) {
2307
                    if ($row->label->getLang() == $lang || strpos($row->label->getLang(), $lang . "-") == 0) {
2308
                        $values[$row->conc->getURI()]['prefLabel'] = $row->label->getValue();
2309
                    } else {
2310
                        $values[$row->conc->getURI()]['prefLabel'] = $row->label->getValue() . " (" . $row->label->getLang() . ")";
2311
                    }
2312
2313
                }
2314
                if (isset($row->notation)) {
2315
                    $values[$row->conc->getURI()]['notation'] = $row->notation->getValue();
2316
                }
2317
2318
            } else {
2319
                $values[$row->conc->getURI()]['type'][] = $row->type->shorten();
2320
            }
2321
        }
2322
2323
        foreach ($values as $val) {
2324
            $ret[] = $val;
2325
        }
2326
2327
        return $ret;
2328
    }
2329
2330
    /**
2331
     * return a list of concepts in a concept group
2332
     * @param string $groupClass URI of concept group class
2333
     * @param string $group URI of the concept group instance
2334
     * @param string $lang language of labels to return
2335
     * @param boolean $showDeprecated whether to include deprecated concepts in search results
2336
     * @return array Result array with concept URI as key and concept label as value
2337
     */
2338
    public function listConceptGroupContents($groupClass, $group, $lang, $showDeprecated = false)
2339
    {
2340
        $query = $this->generateConceptGroupContentsQuery($groupClass, $group, $lang, $showDeprecated);
2341
        $result = $this->query($query);
2342
        return $this->transformConceptGroupContentsResults($result, $lang);
2343
    }
2344
2345
    /**
2346
     * Generates the sparql query for queryChangeList.
2347
     * @param string $prop the property uri pointing to timestamps, eg. 'dc:modified'
2348
     * @param string $lang language of labels to return
2349
     * @param int $offset offset of results to retrieve; 0 for beginning of list
2350
     * @param int $limit maximum number of results to return
2351
     * @param boolean $showDeprecated whether to include deprecated concepts in the change list
2352
     * @return string sparql query
2353
     */
2354
    private function generateChangeListQuery($prop, $lang, $offset, $limit = 200, $showDeprecated = false)
2355
    {
2356
        $fcl = $this->generateFromClause();
2357
        $offset = ($offset) ? 'OFFSET ' . $offset : '';
2358
2359
        //Additional clauses when deprecated concepts need to be included in the results
2360
        $deprecatedOptions = '';
2361
        $deprecatedVars = '';
2362
        if ($showDeprecated) {
2363
            $deprecatedVars = '?replacedBy ?deprecated ?replacingLabel';
2364
            $deprecatedOptions = <<<EOQ
2365
UNION {
2366
    ?concept owl:deprecated True; dc:modified ?date2 .
2367
    BIND(True as ?deprecated)
2368
    BIND(COALESCE(?date2, ?date) AS ?date)
2369
    OPTIONAL {
2370
        ?concept dc:isReplacedBy ?replacedBy .
2371
        OPTIONAL {
2372
            ?replacedBy skos:prefLabel ?replacingLabel .
2373
            FILTER (langMatches(lang(?replacingLabel), '$lang'))
2374
        }
2375
    }
2376
}
2377
EOQ;
2378
        }
2379
2380
        $query = <<<EOQ
2381
SELECT ?concept ?date ?label $deprecatedVars $fcl
2382
WHERE {
2383
    ?concept a skos:Concept ;
2384
    skos:prefLabel ?label .
2385
    FILTER (langMatches(lang(?label), '$lang'))
2386
    {
2387
        ?concept $prop ?date .
2388
        MINUS { ?concept owl:deprecated True . }
2389
    }
2390
    $deprecatedOptions
2391
}
2392
ORDER BY DESC(YEAR(?date)) DESC(MONTH(?date)) LCASE(?label) DESC(?concept)
2393
LIMIT $limit $offset
2394
EOQ;
2395
2396
        return $query;
2397
    }
2398
2399
    /**
2400
     * Transforms the sparql query result into an array.
2401
     * @param EasyRdf\Sparql\Result $result
2402
     * @return array
2403
     */
2404
    private function transformChangeListResults($result)
2405
    {
2406
        $ret = array();
2407
        foreach ($result as $row) {
2408
            $concept = array('uri' => $row->concept->getURI());
2409
            if (isset($row->label)) {
2410
                $concept['prefLabel'] = $row->label->getValue();
2411
            }
2412
2413
            if (isset($row->date)) {
2414
                try {
2415
                    $concept['date'] = $row->date->getValue();
2416
                } catch (Exception $e) {
2417
                    //don't record concepts with malformed dates e.g. 1986-21-00
2418
                    continue;
2419
                }
2420
            }
2421
2422
            if (isset($row->deprecated)) {
2423
                $concept['deprecated'] = $row->deprecated->getValue();
2424
            } else {
2425
                $concept['deprecated'] = false;
2426
            }
2427
            if (isset($row->replacedBy)) {
2428
                $concept['replacedBy'] = $row->replacedBy->getURI();
2429
            }
2430
            if (isset($row->replacingLabel)) {
2431
                $concept['replacingLabel'] = $row->replacingLabel->getValue();
2432
            }
2433
2434
            $ret[] = $concept;
2435
        }
2436
        return $ret;
2437
    }
2438
2439
    /**
2440
     * return a list of recently changed or entirely new concepts
2441
     * @param string $prop the property uri pointing to timestamps, eg. 'dc:modified'
2442
     * @param string $lang language of labels to return
2443
     * @param int $offset offset of results to retrieve; 0 for beginning of list
2444
     * @param int $limit maximum number of results to return
2445
     * @param boolean $showDeprecated whether to include deprecated concepts in the change list
2446
     * @return array Result array
2447
     */
2448
    public function queryChangeList($prop, $lang, $offset, $limit, $showDeprecated = false)
2449
    {
2450
        $query = $this->generateChangeListQuery($prop, $lang, $offset, $limit, $showDeprecated);
2451
2452
        $result = $this->query($query);
2453
        return $this->transformChangeListResults($result);
2454
    }
2455
}
2456