Passed
Pull Request — master (#1284)
by Osma
03:36
created

transformConceptGroupContentsResults()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 33
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 21
nc 16
nop 2
dl 0
loc 33
rs 8.4444
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Generates SPARQL queries and provides access to the SPARQL endpoint.
5
 */
6
class GenericSparql {
7
    /**
8
     * A SPARQL Client eg. an EasyRDF instance.
9
     * @property EasyRdf\Sparql\Client $client
10
     */
11
    protected $client;
12
    /**
13
     * Graph uri.
14
     * @property object $graph
15
     */
16
    protected $graph;
17
    /**
18
     * A SPARQL query graph part template.
19
     * @property string $graphClause
20
     */
21
    protected $graphClause;
22
    /**
23
     * Model instance.
24
     * @property Model $model
25
     */
26
    protected $model;
27
28
    /**
29
     * Cache used to avoid expensive shorten() calls
30
     * @property array $qnamecache
31
     */
32
    private $qnamecache = array();
33
34
    /**
35
     * Requires the following three parameters.
36
     * @param string $endpoint SPARQL endpoint address.
37
     * @param string|null $graph Which graph to query: Either an URI, the special value "?graph"
38
     *                           to use the default graph, or NULL to not use a GRAPH clause.
39
     * @param object $model a Model instance.
40
     */
41
    public function __construct($endpoint, $graph, $model) {
42
        $this->graph = $graph;
43
        $this->model = $model;
44
45
        // create the EasyRDF SPARQL client instance to use
46
        $this->initializeHttpClient();
47
        $this->client = new EasyRdf\Sparql\Client($endpoint);
48
49
        // set graphClause so that it can be used by all queries
50
        if ($this->isDefaultEndpoint()) // default endpoint; query any graph (and catch it in a variable)
51
        {
52
            $this->graphClause = "GRAPH $graph";
53
        } elseif ($graph !== null) // query a specific graph
54
        {
55
            $this->graphClause = "GRAPH <$graph>";
56
        } else // query the default graph
57
        {
58
            $this->graphClause = "";
59
        }
60
61
    }
62
63
    /**
64
     * Returns prefix-definitions for a query
65
     *
66
     * @param string $query
67
     * @return string
68
    */
69
    protected function generateQueryPrefixes($query)
70
    {
71
        // Check for undefined prefixes
72
        $prefixes = '';
73
        foreach (EasyRdf\RdfNamespace::namespaces() as $prefix => $uri) {
74
            if (strpos($query, "{$prefix}:") !== false and
75
                strpos($query, "PREFIX {$prefix}:") === false
76
            ) {
77
                $prefixes .= "PREFIX {$prefix}: <{$uri}>\n";
78
            }
79
        }
80
        return $prefixes;
81
    }
82
83
    /**
84
     * Execute the SPARQL query using the SPARQL client, logging it as well.
85
     * @param string $query SPARQL query to perform
86
     * @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...
87
     */
88
    protected function query($query) {
89
        $queryId = sprintf("%05d", rand(0, 99999));
90
        $logger = $this->model->getLogger();
91
        $logger->info("[qid $queryId] SPARQL query:\n" . $this->generateQueryPrefixes($query) . "\n$query\n");
92
        $starttime = microtime(true);
93
        $result = $this->client->query($query);
94
        $elapsed = intval(round((microtime(true) - $starttime) * 1000));
95
        if(method_exists($result, 'numRows')) {
96
            $numRows = $result->numRows();
97
            $logger->info("[qid $queryId] result: $numRows rows returned in $elapsed ms");
98
        } else { // graph result
99
            $numTriples = $result->countTriples();
100
            $logger->info("[qid $queryId] result: $numTriples triples returned in $elapsed ms");
101
        }
102
        return $result;
103
    }
104
105
106
    /**
107
     * Generates FROM clauses for the queries
108
     * @param Vocabulary[]|null $vocabs
109
     * @return string
110
     */
111
    protected function generateFromClause($vocabs=null) {
112
        $clause = '';
113
        if (!$vocabs) {
114
            return $this->graph !== '?graph' && $this->graph !== NULL ? "FROM <$this->graph>" : '';
115
        }
116
        $graphs = $this->getVocabGraphs($vocabs);
117
        foreach ($graphs as $graph) {
118
            $clause .= "FROM NAMED <$graph> ";
119
        }
120
        return $clause;
121
    }
122
123
    protected function initializeHttpClient() {
124
        // configure the HTTP client used by EasyRdf\Sparql\Client
125
        $httpclient = EasyRdf\Http::getDefaultHttpClient();
126
        $httpclient->setConfig(array('timeout' => $this->model->getConfig()->getSparqlTimeout()));
127
128
        // if special cache control (typically no-cache) was requested by the
129
        // client, set the same type of cache control headers also in subsequent
130
        // in the SPARQL requests (this is useful for performance testing)
131
        // @codeCoverageIgnoreStart
132
        $cacheControl = filter_input(INPUT_SERVER, 'HTTP_CACHE_CONTROL', FILTER_SANITIZE_STRING);
133
        $pragma = filter_input(INPUT_SERVER, 'HTTP_PRAGMA', FILTER_SANITIZE_STRING);
134
        if ($cacheControl !== null || $pragma !== null) {
135
            $val = $pragma !== null ? $pragma : $cacheControl;
136
            $httpclient->setHeaders('Cache-Control', $val);
137
        }
138
        // @codeCoverageIgnoreEnd
139
140
        EasyRdf\Http::setDefaultHttpClient($httpclient); // actually redundant..
141
    }
142
143
    /**
144
     * Return true if this is the default SPARQL endpoint, used as the facade to query
145
     * all vocabularies.
146
     */
147
148
    protected function isDefaultEndpoint() {
149
        return !is_null($this->graph) && $this->graph[0] == '?';
150
    }
151
152
    /**
153
     * Returns the graph instance
154
     * @return object EasyRDF graph instance.
155
     */
156
    public function getGraph() {
157
        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...
158
    }
159
160
    /**
161
     * Shorten a URI
162
     * @param string $uri URI to shorten
163
     * @return string shortened URI, or original URI if it cannot be shortened
164
     */
165
    private function shortenUri($uri) {
166
        if (!array_key_exists($uri, $this->qnamecache)) {
167
            $res = new EasyRdf\Resource($uri);
168
            $qname = $res->shorten(); // returns null on failure
169
            // only URIs in the SKOS namespace are shortened
170
            $this->qnamecache[$uri] = ($qname !== null && strpos($qname, "skos:") === 0) ? $qname : $uri;
171
        }
172
        return $this->qnamecache[$uri];
173
    }
174
175
176
    /**
177
     * Generates the sparql query for retrieving concept and collection counts in a vocabulary.
178
     * @return string sparql query
179
     */
180
    private function generateCountConceptsQuery($array, $group) {
181
        $fcl = $this->generateFromClause();
182
        $optional = $array ? "(<$array>) " : '';
183
        $optional .= $group ? "(<$group>)" : '';
184
        $query = <<<EOQ
185
      SELECT (COUNT(DISTINCT(?conc)) as ?c) ?type ?typelabel (COUNT(?depr) as ?deprcount) $fcl WHERE {
186
        VALUES (?value) { (skos:Concept) (skos:Collection) $optional }
187
  	    ?type rdfs:subClassOf* ?value
188
        { ?type ^a ?conc .
189
          OPTIONAL { ?conc owl:deprecated ?depr .
190
  		    FILTER (?depr = True)
191
          }
192
        } UNION {SELECT * WHERE {
193
            ?type rdfs:label ?typelabel
194
          }
195
        }
196
      } GROUP BY ?type ?typelabel
197
EOQ;
198
        return $query;
199
    }
200
201
    /**
202
     * Used for transforming the concept count query results.
203
     * @param EasyRdf\Sparql\Result $result query results to be transformed
204
     * @param string $lang language of labels
205
     * @return Array containing the label counts
206
     */
207
    private function transformCountConceptsResults($result, $lang) {
208
        $ret = array();
209
        foreach ($result as $row) {
210
            if (!isset($row->type)) {
211
                continue;
212
            }
213
            $typeURI = $row->type->getUri();
214
            $ret[$typeURI]['type'] = $typeURI;
215
216
            if (!isset($row->typelabel)) {
217
                $ret[$typeURI]['count'] = $row->c->getValue();
218
                $ret[$typeURI]['deprecatedCount'] = $row->deprcount->getValue();
219
            }
220
221
            if (isset($row->typelabel) && $row->typelabel->getLang() === $lang) {
222
                $ret[$typeURI]['label'] = $row->typelabel->getValue();
223
            }
224
225
        }
226
        return $ret;
227
    }
228
229
    /**
230
     * Used for counting number of concepts and collections in a vocabulary.
231
     * @param string $lang language of labels
232
     * @param string $array the uri of the concept array class, eg. isothes:ThesaurusArray
233
     * @param string $group the uri of the  concept group class, eg. isothes:ConceptGroup
234
     * @return array with number of concepts in this vocabulary per label
235
     */
236
    public function countConcepts($lang = null, $array = null, $group = null) {
237
        $query = $this->generateCountConceptsQuery($array, $group);
238
        $result = $this->query($query);
239
        return $this->transformCountConceptsResults($result, $lang);
240
    }
241
242
    /**
243
     * @param array $langs Languages to query for
244
     * @param string[] $props property names
245
     * @return string sparql query
246
     */
247
    private function generateCountLangConceptsQuery($langs, $classes, $props) {
248
        $gcl = $this->graphClause;
249
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
250
251
	$quote_string = function($val) { return "'$val'"; };
252
	$quoted_values = array_map($quote_string, $langs);
253
	$langFilter = "FILTER(?lang IN (" . implode(',', $quoted_values) . "))";
254
255
        $values = $this->formatValues('?type', $classes, 'uri');
256
        $valuesProp = $this->formatValues('?prop', $props, null);
257
258
        $query = <<<EOQ
259
SELECT ?lang ?prop
260
  (COUNT(?label) as ?count)
261
WHERE {
262
  $gcl {
263
    $values
264
    $valuesProp
265
    ?conc a ?type .
266
    ?conc ?prop ?label .
267
    BIND(LANG(?label) AS ?lang)
268
    $langFilter
269
  }
270
}
271
GROUP BY ?lang ?prop ?type
272
EOQ;
273
        return $query;
274
    }
275
276
    /**
277
     * Transforms the CountLangConcepts results into an array of label counts.
278
     * @param EasyRdf\Sparql\Result $result query results to be transformed
279
     * @param array $langs Languages to query for
280
     * @param string[] $props property names
281
     */
282
    private function transformCountLangConceptsResults($result, $langs, $props) {
283
        $ret = array();
284
        // set default count to zero; overridden below if query found labels
285
        foreach ($langs as $lang) {
286
            foreach ($props as $prop) {
287
                $ret[$lang][$prop] = 0;
288
            }
289
        }
290
        foreach ($result as $row) {
291
            if (isset($row->lang) && isset($row->prop) && isset($row->count)) {
292
                $ret[$row->lang->getValue()][$row->prop->shorten()] +=
293
                $row->count->getValue();
294
            }
295
296
        }
297
        ksort($ret);
298
        return $ret;
299
    }
300
301
    /**
302
     * Counts the number of concepts in a easyRDF graph with a specific language.
303
     * @param array $langs Languages to query for
304
     * @return Array containing count of concepts for each language and property.
305
     */
306
    public function countLangConcepts($langs, $classes = null) {
307
        $props = array('skos:prefLabel', 'skos:altLabel', 'skos:hiddenLabel');
308
        $query = $this->generateCountLangConceptsQuery($langs, $classes, $props);
309
        // Count the number of terms in each language
310
        $result = $this->query($query);
311
        return $this->transformCountLangConceptsResults($result, $langs, $props);
312
    }
313
314
    /**
315
     * Formats a VALUES clause (SPARQL 1.1) which states that the variable should be bound to one
316
     * of the constants given.
317
     * @param string $varname variable name, e.g. "?uri"
318
     * @param array $values the values
319
     * @param string $type type of values: "uri", "literal" or null (determines quoting style)
320
     */
321
    protected function formatValues($varname, $values, $type = null) {
322
        $constants = array();
323
        foreach ($values as $val) {
324
            if ($type == 'uri') {
325
                $val = "<$val>";
326
            }
327
328
            if ($type == 'literal') {
329
                $val = "'$val'";
330
            }
331
332
            $constants[] = "($val)";
333
        }
334
        $values = implode(" ", $constants);
335
336
        return "VALUES ($varname) { $values }";
337
    }
338
339
    /**
340
     * Filters multiple instances of the same vocabulary from the input array.
341
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
342
     * @return \Vocabulary[]
343
     */
344
    private function filterDuplicateVocabs($vocabs) {
345
        // filtering duplicates
346
        $uniqueVocabs = array();
347
        if ($vocabs !== null && sizeof($vocabs) > 0) {
348
            foreach ($vocabs as $voc) {
349
                $uniqueVocabs[$voc->getId()] = $voc;
350
            }
351
        }
352
353
        return $uniqueVocabs;
354
    }
355
356
    /**
357
     * Generates a sparql query for one or more concept URIs
358
     * @param mixed $uris concept URI (string) or array of URIs
359
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
360
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
361
     * @return string sparql query
362
     */
363
    private function generateConceptInfoQuery($uris, $arrayClass, $vocabs) {
364
        $gcl = $this->graphClause;
365
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
366
        $values = $this->formatValues('?uri', $uris, 'uri');
367
        $uniqueVocabs = $this->filterDuplicateVocabs($vocabs);
368
        $valuesGraph = empty($vocabs) ? $this->formatValuesGraph($uniqueVocabs) : '';
369
370
        if ($arrayClass === null) {
371
            $construct = $optional = "";
372
        } else {
373
            // add information that can be used to format narrower concepts by
374
            // the array they belong to ("milk by source animal" use case)
375
            $construct = "\n ?x skos:member ?o . ?x skos:prefLabel ?xl . ?x a <$arrayClass> .";
376
            $optional = "\n OPTIONAL {
377
                      ?x skos:member ?o .
378
                      ?x a <$arrayClass> .
379
                      ?x skos:prefLabel ?xl .
380
                      FILTER NOT EXISTS {
381
                        ?x skos:member ?other .
382
                        MINUS { ?other skos:broader ?uri }
383
                      }
384
                    }";
385
        }
386
        $query = <<<EOQ
387
CONSTRUCT {
388
 ?s ?p ?uri .
389
 ?sp ?uri ?op .
390
 ?uri ?p ?o .
391
 ?p rdfs:label ?proplabel .
392
 ?p rdfs:comment ?propcomm .
393
 ?p skos:definition ?propdef .
394
 ?p rdfs:subPropertyOf ?pp .
395
 ?pp rdfs:label ?plabel .
396
 ?o a ?ot .
397
 ?o skos:prefLabel ?opl .
398
 ?o rdfs:label ?ol .
399
 ?o rdf:value ?ov .
400
 ?o skos:notation ?on .
401
 ?o ?oprop ?oval .
402
 ?o ?xlprop ?xlval .
403
 ?dt rdfs:label ?dtlabel .
404
 ?directgroup skos:member ?uri .
405
 ?parent skos:member ?group .
406
 ?group skos:prefLabel ?grouplabel .
407
 ?b1 rdf:first ?item .
408
 ?b1 rdf:rest ?b2 .
409
 ?item a ?it .
410
 ?item skos:prefLabel ?il .
411
 ?group a ?grouptype . $construct
412
} $fcl WHERE {
413
 $values
414
 $gcl {
415
  {
416
    ?s ?p ?uri .
417
    FILTER(!isBlank(?s))
418
    FILTER(?p != skos:inScheme)
419
    FILTER NOT EXISTS { ?s owl:deprecated true . }
420
  }
421
  UNION
422
  { ?sp ?uri ?op . }
423
  UNION
424
  {
425
    ?directgroup skos:member ?uri .
426
    ?group skos:member+ ?uri .
427
    ?group skos:prefLabel ?grouplabel .
428
    ?group a ?grouptype .
429
    OPTIONAL { ?parent skos:member ?group }
430
  }
431
  UNION
432
  {
433
   ?uri ?p ?o .
434
   OPTIONAL {
435
     ?uri skos:notation ?nVal .
436
     FILTER(isLiteral(?nVal))
437
     BIND(datatype(?nVal) AS ?dt)
438
     ?dt rdfs:label ?dtlabel
439
   }
440
   OPTIONAL {
441
     ?o rdf:rest* ?b1 .
442
     ?b1 rdf:first ?item .
443
     ?b1 rdf:rest ?b2 .
444
     OPTIONAL { ?item a ?it . }
445
     OPTIONAL { ?item skos:prefLabel ?il . }
446
   }
447
   OPTIONAL {
448
     { ?p rdfs:label ?proplabel . }
449
     UNION
450
     { ?p rdfs:comment ?propcomm . }
451
     UNION
452
     { ?p skos:definition ?propdef . }
453
     UNION
454
     { ?p rdfs:subPropertyOf ?pp . }
455
   }
456
   OPTIONAL {
457
     { ?o a ?ot . }
458
     UNION
459
     { ?o skos:prefLabel ?opl . }
460
     UNION
461
     { ?o rdfs:label ?ol . }
462
     UNION
463
     { ?o rdf:value ?ov . 
464
       OPTIONAL { ?o ?oprop ?oval . }
465
     }
466
     UNION
467
     { ?o skos:notation ?on . }
468
     UNION
469
     { ?o a skosxl:Label .
470
       ?o ?xlprop ?xlval }
471
   } $optional
472
  }
473
 }
474
}
475
$valuesGraph
476
EOQ;
477
        return $query;
478
    }
479
480
    /**
481
     * Transforms ConceptInfo query results into an array of Concept objects
482
     * @param EasyRdf\Graph $result query results to be transformed
483
     * @param array $uris concept URIs
484
     * @param \Vocabulary[] $vocabs array of Vocabulary object
485
     * @param string|null $clang content language
486
     * @return Concept[] array of Concept objects
487
     */
488
    private function transformConceptInfoResults($result, $uris, $vocabs, $clang) {
489
        $conceptArray = array();
490
        foreach ($uris as $index => $uri) {
491
            $conc = $result->resource($uri);
492
            if (is_array($vocabs)) {
493
                $vocab = (sizeof($vocabs) == 1) ? $vocabs[0] : $vocabs[$index];
494
            } else {
495
                $vocab = null;
496
            }
497
            $conceptArray[] = new Concept($this->model, $vocab, $conc, $result, $clang);
498
        }
499
        return $conceptArray;
500
    }
501
502
    /**
503
     * Returns information (as a graph) for one or more concept URIs
504
     * @param mixed $uris concept URI (string) or array of URIs
505
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
506
     * @param \Vocabulary[]|null $vocabs vocabularies to target
507
     * @return \EasyRdf\Graph
508
     */
509
    public function queryConceptInfoGraph($uris, $arrayClass = null, $vocabs = array()) {
510
        // if just a single URI is given, put it in an array regardless
511
        if (!is_array($uris)) {
512
            $uris = array($uris);
513
        }
514
515
        $query = $this->generateConceptInfoQuery($uris, $arrayClass, $vocabs);
516
        $result = $this->query($query);
517
        return $result;
518
    }
519
520
    /**
521
     * Returns information (as an array of Concept objects) for one or more concept URIs
522
     * @param mixed $uris concept URI (string) or array of URIs
523
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
524
     * @param \Vocabulary[] $vocabs vocabularies to target
525
     * @param string|null $clang content language
526
     * @return Concept[]
527
     */
528
    public function queryConceptInfo($uris, $arrayClass = null, $vocabs = array(), $clang = null) {
529
        // if just a single URI is given, put it in an array regardless
530
        if (!is_array($uris)) {
531
            $uris = array($uris);
532
        }
533
        $result = $this->queryConceptInfoGraph($uris, $arrayClass, $vocabs);
534
        if ($result->isEmpty()) {
535
            return [];
536
        }
537
        return $this->transformConceptInfoResults($result, $uris, $vocabs, $clang);
538
    }
539
540
    /**
541
     * Generates the sparql query for queryTypes
542
     * @param string $lang
543
     * @return string sparql query
544
     */
545
    private function generateQueryTypesQuery($lang) {
546
        $fcl = $this->generateFromClause();
547
        $query = <<<EOQ
548
SELECT DISTINCT ?type ?label ?superclass $fcl
549
WHERE {
550
  {
551
    { BIND( skos:Concept as ?type ) }
552
    UNION
553
    { BIND( skos:Collection as ?type ) }
554
    UNION
555
    { BIND( isothes:ConceptGroup as ?type ) }
556
    UNION
557
    { BIND( isothes:ThesaurusArray as ?type ) }
558
    UNION
559
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Concept . }
560
    UNION
561
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Collection . }
562
  }
563
  OPTIONAL {
564
    ?type rdfs:label ?label .
565
    FILTER(langMatches(lang(?label), '$lang'))
566
  }
567
  OPTIONAL {
568
    ?type rdfs:subClassOf ?superclass .
569
  }
570
  FILTER EXISTS {
571
    ?s a ?type .
572
    ?s skos:prefLabel ?prefLabel .
573
  }
574
}
575
EOQ;
576
        return $query;
577
    }
578
579
    /**
580
     * Transforms the results into an array format.
581
     * @param EasyRdf\Sparql\Result $result
582
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
583
     */
584
    private function transformQueryTypesResults($result) {
585
        $ret = array();
586
        foreach ($result as $row) {
587
            $type = array();
588
            if (isset($row->label)) {
589
                $type['label'] = $row->label->getValue();
590
            }
591
592
            if (isset($row->superclass)) {
593
                $type['superclass'] = $row->superclass->getUri();
594
            }
595
596
            $ret[$row->type->getURI()] = $type;
597
        }
598
        return $ret;
599
    }
600
601
    /**
602
     * Retrieve information about types from the endpoint
603
     * @param string $lang
604
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
605
     */
606
    public function queryTypes($lang) {
607
        $query = $this->generateQueryTypesQuery($lang);
608
        $result = $this->query($query);
609
        return $this->transformQueryTypesResults($result);
610
    }
611
612
    /**
613
     * Generates the concept scheme query.
614
     * @param string $conceptscheme concept scheme URI
615
     * @return string sparql query
616
     */
617
    private function generateQueryConceptSchemeQuery($conceptscheme) {
618
        $fcl = $this->generateFromClause();
619
        $query = <<<EOQ
620
CONSTRUCT {
621
  <$conceptscheme> ?property ?value .
622
} $fcl WHERE {
623
  <$conceptscheme> ?property ?value .
624
  FILTER (?property != skos:hasTopConcept)
625
}
626
EOQ;
627
        return $query;
628
    }
629
630
    /**
631
     * Retrieves conceptScheme information from the endpoint.
632
     * @param string $conceptscheme concept scheme URI
633
     * @return \EasyRdf\Sparql\Result|\EasyRdf\Graph query result graph
634
     */
635
    public function queryConceptScheme($conceptscheme) {
636
        $query = $this->generateQueryConceptSchemeQuery($conceptscheme);
637
        return $this->query($query);
638
    }
639
640
    /**
641
     * Generates the queryConceptSchemes sparql query.
642
     * @param string $lang language of labels
643
     * @return string sparql query
644
     */
645
    private function generateQueryConceptSchemesQuery($lang) {
646
        $fcl = $this->generateFromClause();
647
        $query = <<<EOQ
648
SELECT ?cs ?label ?preflabel ?title ?domain ?domainLabel $fcl
649
WHERE {
650
 ?cs a skos:ConceptScheme .
651
 OPTIONAL{
652
    ?cs dcterms:subject ?domain.
653
    ?domain skos:prefLabel ?domainLabel.
654
    FILTER(langMatches(lang(?domainLabel), '$lang'))
655
}
656
 OPTIONAL {
657
   ?cs rdfs:label ?label .
658
   FILTER(langMatches(lang(?label), '$lang'))
659
 }
660
 OPTIONAL {
661
   ?cs skos:prefLabel ?preflabel .
662
   FILTER(langMatches(lang(?preflabel), '$lang'))
663
 }
664
 OPTIONAL {
665
   { ?cs dc11:title ?title }
666
   UNION
667
   { ?cs dc:title ?title }
668
   FILTER(langMatches(lang(?title), '$lang'))
669
 }
670
} 
671
ORDER BY ?cs
672
EOQ;
673
        return $query;
674
    }
675
676
    /**
677
     * Transforms the queryConceptScheme results into an array format.
678
     * @param EasyRdf\Sparql\Result $result
679
     * @return array
680
     */
681
    private function transformQueryConceptSchemesResults($result) {
682
        $ret = array();
683
        foreach ($result as $row) {
684
            $conceptscheme = array();
685
            if (isset($row->label)) {
686
                $conceptscheme['label'] = $row->label->getValue();
687
            }
688
689
            if (isset($row->preflabel)) {
690
                $conceptscheme['prefLabel'] = $row->preflabel->getValue();
691
            }
692
693
            if (isset($row->title)) {
694
                $conceptscheme['title'] = $row->title->getValue();
695
            }
696
            // add dct:subject and their labels in the result
697
            if(isset($row->domain) && isset($row->domainLabel)){
698
                $conceptscheme['subject']['uri']=$row->domain->getURI();
699
                $conceptscheme['subject']['prefLabel']=$row->domainLabel->getValue();
700
            }
701
702
            $ret[$row->cs->getURI()] = $conceptscheme;
703
        }
704
        return $ret;
705
    }
706
707
    /**
708
     * return a list of skos:ConceptScheme instances in the given graph
709
     * @param string $lang language of labels
710
     * @return array Array with concept scheme URIs (string) as keys and labels (string) as values
711
     */
712
    public function queryConceptSchemes($lang) {
713
        $query = $this->generateQueryConceptSchemesQuery($lang);
714
        $result = $this->query($query);
715
        return $this->transformQueryConceptSchemesResults($result);
716
    }
717
718
    /**
719
     * Generate a VALUES clause for limiting the targeted graphs.
720
     * @param Vocabulary[]|null $vocabs the vocabularies to target
721
     * @return string[] array of graph URIs
722
     */
723
    protected function getVocabGraphs($vocabs) {
724
        if ($vocabs === null || sizeof($vocabs) == 0) {
725
            // searching from all vocabularies - limit to known graphs
726
            $vocabs = $this->model->getVocabularies();
727
        }
728
        $graphs = array();
729
        foreach ($vocabs as $voc) {
730
            $graph = $voc->getGraph();
731
            if (!is_null($graph) && !in_array($graph, $graphs)) {
732
                $graphs[] = $graph;
733
            }
734
        }
735
        return $graphs;
736
    }
737
738
    /**
739
     * Generate a VALUES clause for limiting the targeted graphs.
740
     * @param Vocabulary[]|null $vocabs array of Vocabulary objects to target
741
     * @return string VALUES clause, or "" if not necessary to limit
742
     */
743
    protected function formatValuesGraph($vocabs) {
744
        if (!$this->isDefaultEndpoint()) {
745
            return "";
746
        }
747
        $graphs = $this->getVocabGraphs($vocabs);
748
        return $this->formatValues('?graph', $graphs, 'uri');
749
    }
750
751
    /**
752
     * Generate a FILTER clause for limiting the targeted graphs.
753
     * @param array $vocabs array of Vocabulary objects to target
754
     * @return string FILTER clause, or "" if not necessary to limit
755
     */
756
    protected function formatFilterGraph($vocabs) {
757
        if (!$this->isDefaultEndpoint()) {
758
            return "";
759
        }
760
        $graphs = $this->getVocabGraphs($vocabs);
761
        $values = array();
762
        foreach ($graphs as $graph) {
763
          $values[] = "<$graph>";
764
        }
765
        if (count($values)) {
766
          return "FILTER (?graph IN (" . implode(',', $values) . "))";
767
        }
768
    }
769
770
    /**
771
     * Formats combined limit and offset clauses for the sparql query
772
     * @param int $limit maximum number of hits to retrieve; 0 for unlimited
773
     * @param int $offset offset of results to retrieve; 0 for beginning of list
774
     * @return string sparql query clauses
775
     */
776
    protected function formatLimitAndOffset($limit, $offset) {
777
        $limit = ($limit) ? 'LIMIT ' . $limit : '';
778
        $offset = ($offset) ? 'OFFSET ' . $offset : '';
779
        // eliminating whitespace and line changes when the conditions aren't needed.
780
        $limitandoffset = '';
781
        if ($limit && $offset) {
782
            $limitandoffset = "\n" . $limit . "\n" . $offset;
783
        } elseif ($limit) {
784
            $limitandoffset = "\n" . $limit;
785
        } elseif ($offset) {
786
            $limitandoffset = "\n" . $offset;
787
        }
788
789
        return $limitandoffset;
790
    }
791
792
    /**
793
     * Formats a sparql query clause for limiting the search to specific concept types.
794
     * @param array $types limit search to concepts of the given type(s)
795
     * @return string sparql query clause
796
     */
797
    protected function formatTypes($types) {
798
        $typePatterns = array();
799
        if (!empty($types)) {
800
            foreach ($types as $type) {
801
                $unprefixed = EasyRdf\RdfNamespace::expand($type);
802
                $typePatterns[] = "{ ?s a <$unprefixed> }";
803
            }
804
        }
805
806
        return implode(' UNION ', $typePatterns);
807
    }
808
809
    /**
810
     * @param string $prop property to include in the result eg. 'broader' or 'narrower'
811
     * @return string sparql query clause
812
     */
813
    private function formatPropertyCsvClause($prop) {
814
        # This expression creates a CSV row containing pairs of (uri,prefLabel) values.
815
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
816
        $clause = <<<EOV
817
(GROUP_CONCAT(DISTINCT CONCAT(
818
 '"', IF(isIRI(?$prop),STR(?$prop),''), '"', ',',
819
 '"', REPLACE(IF(BOUND(?{$prop}lab),?{$prop}lab,''), '"', '""'), '"', ',',
820
 '"', REPLACE(IF(isLiteral(?{$prop}),?{$prop},''), '"', '""'), '"'
821
); separator='\\n') as ?{$prop}s)
822
EOV;
823
        return $clause;
824
    }
825
826
    /**
827
     * @return string sparql query clause
828
     */
829
    private function formatPrefLabelCsvClause() {
830
        # This expression creates a CSV row containing pairs of (prefLabel, lang) values.
831
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
832
        $clause = <<<EOV
833
(GROUP_CONCAT(DISTINCT CONCAT(
834
 '"', STR(?pref), '"', ',', '"', lang(?pref), '"'
835
); separator='\\n') as ?preflabels)
836
EOV;
837
        return $clause;
838
    }
839
840
    /**
841
     * @param string $lang language code of the returned labels
842
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
843
     * @return array sparql query clause
844
     */
845
    protected function formatExtraFields($lang, $fields) {
846
        // extra variable expressions to request and extra fields to query for
847
        $ret = array('extravars' => '', 'extrafields' => '');
848
849
        if ($fields === null) {
850
            return $ret;
851
        }
852
853
        if (in_array('prefLabel', $fields)) {
854
            $ret['extravars'] .= $this->formatPreflabelCsvClause();
855
            $ret['extrafields'] .= <<<EOF
856
OPTIONAL {
857
  ?s skos:prefLabel ?pref .
858
}
859
EOF;
860
            // removing the prefLabel from the fields since it has been handled separately
861
            $fields = array_diff($fields, array('prefLabel'));
862
        }
863
864
        foreach ($fields as $field) {
865
            $ret['extravars'] .= $this->formatPropertyCsvClause($field);
866
            $ret['extrafields'] .= <<<EOF
867
OPTIONAL {
868
  ?s skos:$field ?$field .
869
  FILTER(!isLiteral(?$field)||langMatches(lang(?{$field}), '$lang'))
870
  OPTIONAL { ?$field skos:prefLabel ?{$field}lab . FILTER(langMatches(lang(?{$field}lab), '$lang')) }
871
}
872
EOF;
873
        }
874
875
        return $ret;
876
    }
877
878
    /**
879
     * Generate condition for matching labels in SPARQL
880
     * @param string $term search term
881
     * @param string $searchLang language code used for matching labels (null means any language)
882
     * @return string sparql query snippet
883
     */
884
    protected function generateConceptSearchQueryCondition($term, $searchLang)
885
    {
886
        # use appropriate matching function depending on query type: =, strstarts, strends or full regex
887
        if (preg_match('/^[^\*]+$/', $term)) { // exact query
888
            $term = str_replace('\\', '\\\\', $term); // quote slashes
889
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
890
            $filtercond = "LCASE(STR(?match)) = '$term'";
891
        } elseif (preg_match('/^[^\*]+\*$/', $term)) { // prefix query
892
            $term = substr($term, 0, -1); // remove the final asterisk
893
            $term = str_replace('\\', '\\\\', $term); // quote slashes
894
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
895
            $filtercond = "STRSTARTS(LCASE(STR(?match)), '$term')";
896
        } elseif (preg_match('/^\*[^\*]+$/', $term)) { // suffix query
897
            $term = substr($term, 1); // remove the preceding asterisk
898
            $term = str_replace('\\', '\\\\', $term); // quote slashes
899
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
900
            $filtercond = "STRENDS(LCASE(STR(?match)), '$term')";
901
        } else { // too complicated - have to use a regex
902
            # make sure regex metacharacters are not passed through
903
            $term = str_replace('\\', '\\\\', preg_quote($term));
904
            $term = str_replace('\\\\*', '.*', $term); // convert asterisk to regex syntax
905
            $term = str_replace('\'', '\\\'', $term); // ensure single quotes are quoted
906
            $filtercond = "REGEX(STR(?match), '^$term$', 'i')";
907
        }
908
909
        $labelcondMatch = ($searchLang) ? "&& (?prop = skos:notation || LANGMATCHES(lang(?match), ?langParam))" : "";
910
911
        return "?s ?prop ?match . FILTER ($filtercond $labelcondMatch)";
912
    }
913
914
915
    /**
916
     * Inner query for concepts using a search term.
917
     * @param string $term search term
918
     * @param string $lang language code of the returned labels
919
     * @param string $searchLang language code used for matching labels (null means any language)
920
     * @param string[] $props properties to target e.g. array('skos:prefLabel','skos:altLabel')
921
     * @param boolean $unique restrict results to unique concepts (default: false)
922
     * @return string sparql query
923
     */
924
    protected function generateConceptSearchQueryInner($term, $lang, $searchLang, $props, $unique, $filterGraph)
925
    {
926
        $valuesProp = $this->formatValues('?prop', $props);
927
        $textcond = $this->generateConceptSearchQueryCondition($term, $searchLang);
928
929
        $rawterm = str_replace(array('\\', '*', '"'), array('\\\\', '', '\"'), $term);
930
        // graph clause, if necessary
931
        $graphClause = $filterGraph != '' ? 'GRAPH ?graph' : '';
932
933
        // extra conditions for label language, if specified
934
        $labelcondLabel = ($lang) ? "LANGMATCHES(lang(?label), '$lang')" : "lang(?match) = '' || LANGMATCHES(lang(?label), lang(?match))";
935
        // if search language and UI/display language differ, must also consider case where there is no prefLabel in
936
        // the display language; in that case, should use the label with the same language as the matched label
937
        $labelcondFallback = ($searchLang != $lang) ?
938
          "OPTIONAL { # in case previous OPTIONAL block gives no labels\n" .
939
          "?s skos:prefLabel ?label . FILTER (LANGMATCHES(LANG(?label), LANG(?match))) }" : "";
940
941
        //  Including the labels if there is no query term given.
942
        if ($rawterm === '') {
943
          $labelClause = "?s skos:prefLabel ?label .";
944
          $labelClause = ($lang) ? $labelClause . " FILTER (LANGMATCHES(LANG(?label), '$lang'))" : $labelClause . "";
945
          return $labelClause . " BIND(?label AS ?match)";
946
        }
947
948
        /*
949
         * This query does some tricks to obtain a list of unique concepts.
950
         * From each match generated by the text index, a string such as
951
         * "1en@example" is generated, where the first character is a number
952
         * encoding the property and priority, then comes the language tag and
953
         * finally the original literal after an @ sign. Of these, the MIN
954
         * function is used to pick the best match for each concept. Finally,
955
         * the structure is unpacked to get back the original string. Phew!
956
         */
957
        $hitvar = $unique ? '(MIN(?matchstr) AS ?hit)' : '(?matchstr AS ?hit)';
958
        $hitgroup = $unique ? 'GROUP BY ?s ?label ?notation' : '';
959
960
        $langClause = $this->generateLangClause($searchLang);
961
962
        $query = <<<EOQ
963
   SELECT DISTINCT ?s ?label ?notation $hitvar
964
   WHERE {
965
    $graphClause {
966
     { 
967
     $valuesProp
968
     VALUES (?prop ?pri ?langParam) { (skos:prefLabel 1 $langClause) (skos:altLabel 3 $langClause) (skos:notation 5 '') (skos:hiddenLabel 7 $langClause)}
969
     $textcond
970
     ?s ?prop ?match }
971
     OPTIONAL {
972
      ?s skos:prefLabel ?label .
973
      FILTER ($labelcondLabel)
974
     } $labelcondFallback
975
     BIND(IF(langMatches(LANG(?match),'$lang'), ?pri, ?pri+1) AS ?npri)
976
     BIND(CONCAT(STR(?npri), LANG(?match), '@', STR(?match)) AS ?matchstr)
977
     OPTIONAL { ?s skos:notation ?notation }
978
    }
979
    $filterGraph
980
   }
981
   $hitgroup
982
EOQ;
983
984
        return $query;
985
    }
986
    /**
987
    *  This function can be overwritten in other SPARQL dialects for the possibility of handling the different language clauses
988
     * @param string $lang
989
     * @return string formatted language clause
990
     */
991
    protected function generateLangClause($lang) {
992
        return "'$lang'";
993
    }
994
995
    /**
996
     * Query for concepts using a search term.
997
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
998
     * @param boolean $unique restrict results to unique concepts (default: false)
999
     * @param boolean $showDeprecated whether to include deprecated concepts in search results (default: false)
1000
     * @param ConceptSearchParameters $params
1001
     * @return string sparql query
1002
     */
1003
    protected function generateConceptSearchQuery($fields, $unique, $params, $showDeprecated = false) {
1004
        $vocabs = $params->getVocabs();
1005
        $gcl = $this->graphClause;
1006
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
1007
        $formattedtype = $this->formatTypes($params->getTypeLimit());
1008
        $formattedfields = $this->formatExtraFields($params->getLang(), $fields);
1009
        $extravars = $formattedfields['extravars'];
1010
        $extrafields = $formattedfields['extrafields'];
1011
        $schemes = $params->getSchemeLimit();
1012
1013
        // limit the search to only requested concept schemes
1014
        $schemecond = '';
1015
        if (!empty($schemes)) {
1016
            $conditions = array();
1017
            foreach($schemes as $scheme) {
1018
                $conditions[] = "{?s skos:inScheme <$scheme>}";
1019
            }
1020
            $schemecond = '{'.implode(" UNION ",$conditions).'}';
1021
        }
1022
        $filterDeprecated="";
1023
        //show or hide deprecated concepts
1024
        if(!$showDeprecated){
1025
            $filterDeprecated="FILTER NOT EXISTS { ?s owl:deprecated true }";
1026
        }
1027
        // extra conditions for parent and group, if specified
1028
        $parentcond = ($params->getParentLimit()) ? "?s skos:broader+ <" . $params->getParentLimit() . "> ." : "";
1029
        $groupcond = ($params->getGroupLimit()) ? "<" . $params->getGroupLimit() . "> skos:member ?s ." : "";
1030
        $pgcond = $parentcond . $groupcond;
1031
1032
        $orderextra = $this->isDefaultEndpoint() ? $this->graph : '';
1033
1034
        # make VALUES clauses
1035
        $props = array('skos:prefLabel', 'skos:altLabel');
1036
1037
        //add notation into searchable data for the vocabularies which have been configured for it
1038
        if ($vocabs) {
1039
            $searchByNotation = false;
1040
            foreach ($vocabs as $vocab) {
1041
                if ($vocab->getConfig()->searchByNotation()) {
1042
                    $searchByNotation = true;
1043
                }
1044
            }
1045
            if ($searchByNotation) {
1046
                $props[] = 'skos:notation';
1047
            }
1048
        }
1049
1050
        if ($params->getHidden()) {
1051
            $props[] = 'skos:hiddenLabel';
1052
        }
1053
        $filterGraph = empty($vocabs) ? $this->formatFilterGraph($vocabs) : '';
1054
1055
        // remove futile asterisks from the search term
1056
        $term = $params->getSearchTerm();
1057
        while (strpos($term, '**') !== false) {
1058
            $term = str_replace('**', '*', $term);
1059
        }
1060
1061
        $labelpriority = <<<EOQ
1062
  FILTER(BOUND(?s))
1063
  BIND(STR(SUBSTR(?hit,1,1)) AS ?pri)
1064
  BIND(IF((SUBSTR(STRBEFORE(?hit, '@'),1) != ?pri), STRLANG(STRAFTER(?hit, '@'), SUBSTR(STRBEFORE(?hit, '@'),2)), STRAFTER(?hit, '@')) AS ?match)
1065
  BIND(IF((?pri = "1" || ?pri = "2") && ?match != ?label, ?match, ?unbound) as ?plabel)
1066
  BIND(IF((?pri = "3" || ?pri = "4"), ?match, ?unbound) as ?alabel)
1067
  BIND(IF((?pri = "7" || ?pri = "8"), ?match, ?unbound) as ?hlabel)
1068
EOQ;
1069
        $innerquery = $this->generateConceptSearchQueryInner($params->getSearchTerm(), $params->getLang(), $params->getSearchLang(), $props, $unique, $filterGraph);
1070
        if ($params->getSearchTerm() === '*' || $params->getSearchTerm() === '') {
1071
          $labelpriority = '';
1072
        }
1073
        $query = <<<EOQ
1074
SELECT DISTINCT ?s ?label ?plabel ?alabel ?hlabel ?graph ?notation (GROUP_CONCAT(DISTINCT STR(?type);separator=' ') as ?types) $extravars 
1075
$fcl
1076
WHERE {
1077
 $gcl {
1078
  {
1079
  $innerquery
1080
  }
1081
  $labelpriority
1082
  $formattedtype
1083
  { $pgcond 
1084
   ?s a ?type .
1085
   $extrafields $schemecond
1086
  }
1087
  $filterDeprecated
1088
 }
1089
 $filterGraph
1090
}
1091
GROUP BY ?s ?match ?label ?plabel ?alabel ?hlabel ?notation ?graph
1092
ORDER BY LCASE(STR(?match)) LANG(?match) $orderextra
1093
EOQ;
1094
        return $query;
1095
    }
1096
1097
    /**
1098
     * Transform a single concept search query results into the skosmos desired return format.
1099
     * @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...
1100
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1101
     * @return array query result object
1102
     */
1103
    private function transformConceptSearchResult($row, $vocabs, $fields)
1104
    {
1105
        $hit = array();
1106
        $hit['uri'] = $row->s->getUri();
1107
1108
        if (isset($row->graph)) {
1109
            $hit['graph'] = $row->graph->getUri();
1110
        }
1111
1112
        foreach (explode(" ", $row->types->getValue()) as $typeuri) {
1113
            $hit['type'][] = $this->shortenUri($typeuri);
1114
        }
1115
1116
        if(!empty($fields)) {
1117
            foreach ($fields as $prop) {
1118
                $propname = $prop . 's';
1119
                if (isset($row->$propname)) {
1120
                    foreach (explode("\n", $row->$propname->getValue()) as $line) {
1121
                        $rdata = str_getcsv($line, ',', '"', '"');
1122
                        $propvals = array();
1123
                        if ($rdata[0] != '') {
1124
                            $propvals['uri'] = $rdata[0];
1125
                        }
1126
                        if ($rdata[1] != '') {
1127
                            $propvals['prefLabel'] = $rdata[1];
1128
                        }
1129
                        if ($rdata[2] != '') {
1130
                            $propvals = $rdata[2];
1131
                        }
1132
1133
                        $hit['skos:' . $prop][] = $propvals;
1134
                    }
1135
                }
1136
            }
1137
        }
1138
1139
1140
        if (isset($row->preflabels)) {
1141
            foreach (explode("\n", $row->preflabels->getValue()) as $line) {
1142
                $pref = str_getcsv($line, ',', '"', '"');
1143
                $hit['prefLabels'][$pref[1]] = $pref[0];
1144
            }
1145
        }
1146
1147
        foreach ($vocabs as $vocab) { // looping the vocabulary objects and asking these for a localname for the concept.
1148
            $localname = $vocab->getLocalName($hit['uri']);
1149
            if ($localname !== $hit['uri']) { // only passing the result forward if the uri didn't boomerang right back.
1150
                $hit['localname'] = $localname;
1151
                break; // stopping the search when we find one that returns something valid.
1152
            }
1153
        }
1154
1155
        if (isset($row->label)) {
1156
            $hit['prefLabel'] = $row->label->getValue();
1157
        }
1158
1159
        if (isset($row->label)) {
1160
            $hit['lang'] = $row->label->getLang();
1161
        }
1162
1163
        if (isset($row->notation)) {
1164
            $hit['notation'] = $row->notation->getValue();
1165
        }
1166
1167
        if (isset($row->plabel)) {
1168
            $hit['matchedPrefLabel'] = $row->plabel->getValue();
1169
            $hit['lang'] = $row->plabel->getLang();
1170
        } elseif (isset($row->alabel)) {
1171
            $hit['altLabel'] = $row->alabel->getValue();
1172
            $hit['lang'] = $row->alabel->getLang();
1173
        } elseif (isset($row->hlabel)) {
1174
            $hit['hiddenLabel'] = $row->hlabel->getValue();
1175
            $hit['lang'] = $row->hlabel->getLang();
1176
        }
1177
        return $hit;
1178
    }
1179
1180
    /**
1181
     * Transform the concept search query results into the skosmos desired return format.
1182
     * @param EasyRdf\Sparql\Result $results
1183
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1184
     * @return array query result object
1185
     */
1186
    private function transformConceptSearchResults($results, $vocabs, $fields) {
1187
        $ret = array();
1188
1189
        foreach ($results as $row) {
1190
            if (!isset($row->s)) {
1191
                // don't break if query returns a single dummy result
1192
                continue;
1193
            }
1194
            $ret[] = $this->transformConceptSearchResult($row, $vocabs, $fields);
1195
        }
1196
        return $ret;
1197
    }
1198
1199
    /**
1200
     * Query for concepts using a search term.
1201
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1202
     * @param array $fields extra fields to include in the result (array of strings or null).
1203
     * @param boolean $unique restrict results to unique concepts
1204
     * @param ConceptSearchParameters $params
1205
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1206
     * @return array query result object
1207
     */
1208
    public function queryConcepts($vocabs, $fields, $unique, $params, $showDeprecated = false) {
1209
        $query = $this->generateConceptSearchQuery($fields, $unique, $params,$showDeprecated);
1210
        $results = $this->query($query);
1211
        return $this->transformConceptSearchResults($results, $vocabs, $fields);
1212
    }
1213
1214
    /**
1215
     * Generates sparql query clauses used for creating the alphabetical index.
1216
     * @param string $letter the letter (or special class) to search for
1217
     * @return array of sparql query clause strings
1218
     */
1219
    private function formatFilterConditions($letter, $lang) {
1220
        $useRegex = false;
1221
1222
        if ($letter == '*') {
1223
            $letter = '.*';
1224
            $useRegex = true;
1225
        } elseif ($letter == '0-9') {
1226
            $letter = '[0-9].*';
1227
            $useRegex = true;
1228
        } elseif ($letter == '!*') {
1229
            $letter = '[^\\\\p{L}\\\\p{N}].*';
1230
            $useRegex = true;
1231
        }
1232
1233
        # make text query clause
1234
        $lcletter = mb_strtolower($letter, 'UTF-8'); // convert to lower case, UTF-8 safe
1235
        if ($useRegex) {
1236
            $filtercondLabel = $lang ? "regex(str(?label), '^$letter$', 'i') && langMatches(lang(?label), '$lang')" : "regex(str(?label), '^$letter$', 'i')";
1237
            $filtercondALabel = $lang ? "regex(str(?alabel), '^$letter$', 'i') && langMatches(lang(?alabel), '$lang')" : "regex(str(?alabel), '^$letter$', 'i')";
1238
        } else {
1239
            $filtercondLabel = $lang ? "strstarts(lcase(str(?label)), '$lcletter') && langMatches(lang(?label), '$lang')" : "strstarts(lcase(str(?label)), '$lcletter')";
1240
            $filtercondALabel = $lang ? "strstarts(lcase(str(?alabel)), '$lcletter') && langMatches(lang(?alabel), '$lang')" : "strstarts(lcase(str(?alabel)), '$lcletter')";
1241
        }
1242
        return array('filterpref' => $filtercondLabel, 'filteralt' => $filtercondALabel);
1243
    }
1244
1245
    /**
1246
     * Generates the sparql query used for rendering the alphabetical index.
1247
     * @param string $letter the letter (or special class) to search for
1248
     * @param string $lang language of labels
1249
     * @param integer $limit limits the amount of results
1250
     * @param integer $offset offsets the result set
1251
     * @param array|null $classes
1252
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1253
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1254
     * @return string sparql query
1255
     */
1256
    protected function generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated = false, $qualifier = null) {
1257
        $gcl = $this->graphClause;
1258
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1259
        $values = $this->formatValues('?type', $classes, 'uri');
1260
        $limitandoffset = $this->formatLimitAndOffset($limit, $offset);
1261
        $conditions = $this->formatFilterConditions($letter, $lang);
1262
        $filtercondLabel = $conditions['filterpref'];
1263
        $filtercondALabel = $conditions['filteralt'];
1264
        $qualifierClause = $qualifier ? "OPTIONAL { ?s <" . $qualifier->getURI() . "> ?qualifier }" : "";
1265
        $filterDeprecated="";
1266
        if(!$showDeprecated){
1267
            $filterDeprecated="FILTER NOT EXISTS { ?s owl:deprecated true }";
1268
        }
1269
        $query = <<<EOQ
1270
SELECT DISTINCT ?s ?label ?alabel ?qualifier
1271
WHERE {
1272
  $gcl {
1273
    {
1274
      ?s skos:prefLabel ?label .
1275
      FILTER (
1276
        $filtercondLabel
1277
      )
1278
    }
1279
    UNION
1280
    {
1281
      {
1282
        ?s skos:altLabel ?alabel .
1283
        FILTER (
1284
          $filtercondALabel
1285
        )
1286
      }
1287
      {
1288
        ?s skos:prefLabel ?label .
1289
        FILTER (langMatches(lang(?label), '$lang'))
1290
      }
1291
    }
1292
    ?s a ?type .
1293
    $qualifierClause
1294
    $filterDeprecated
1295
    $values
1296
  }
1297
}
1298
ORDER BY LCASE(STR(COALESCE(?alabel, ?label))) STR(?s) LCASE(STR(?qualifier)) $limitandoffset
1299
EOQ;
1300
        return $query;
1301
    }
1302
1303
    /**
1304
     * Transforms the alphabetical list query results into an array format.
1305
     * @param EasyRdf\Sparql\Result $results
1306
     * @return array
1307
     */
1308
    private function transformAlphabeticalListResults($results) {
1309
        $ret = array();
1310
1311
        foreach ($results as $row) {
1312
            if (!isset($row->s)) {
1313
                continue;
1314
            }
1315
            // don't break if query returns a single dummy result
1316
1317
            $hit = array();
1318
            $hit['uri'] = $row->s->getUri();
1319
1320
            $hit['localname'] = $row->s->localName();
1321
1322
            $hit['prefLabel'] = $row->label->getValue();
1323
            $hit['lang'] = $row->label->getLang();
1324
1325
            if (isset($row->alabel)) {
1326
                $hit['altLabel'] = $row->alabel->getValue();
1327
                $hit['lang'] = $row->alabel->getLang();
1328
            }
1329
1330
            if (isset($row->qualifier)) {
1331
                if ($row->qualifier instanceof EasyRdf\Literal) {
1332
                    $hit['qualifier'] = $row->qualifier->getValue();
1333
                }
1334
                else {
1335
                    $hit['qualifier'] = $row->qualifier->localName();
1336
                }
1337
            }
1338
1339
            $ret[] = $hit;
1340
        }
1341
1342
        return $ret;
1343
    }
1344
1345
    /**
1346
     * Query for concepts with a term starting with the given letter. Also special classes '0-9' (digits),
1347
     * '*!' (special characters) and '*' (everything) are accepted.
1348
     * @param string $letter the letter (or special class) to search for
1349
     * @param string $lang language of labels
1350
     * @param integer $limit limits the amount of results
1351
     * @param integer $offset offsets the result set
1352
     * @param array $classes
1353
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1354
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1355
     */
1356
    public function queryConceptsAlphabetical($letter, $lang, $limit = null, $offset = null, $classes = null, $showDeprecated = false, $qualifier = null) {
1357
        if ($letter === '') {
1358
            return array(); // special case: no letter given, return empty list
1359
        }
1360
        $query = $this->generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated, $qualifier);
1361
        $results = $this->query($query);
1362
        return $this->transformAlphabeticalListResults($results);
1363
    }
1364
1365
    /**
1366
     * Creates the query used for finding out which letters should be displayed in the alphabetical index.
1367
     * Note that we force the datatype of the result variable otherwise Virtuoso does not properly interpret the DISTINCT and we have duplicated results
1368
     * @param string $lang language
1369
     * @return string sparql query
1370
     */
1371
    private function generateFirstCharactersQuery($lang, $classes) {
1372
        $gcl = $this->graphClause;
1373
        $classes = (isset($classes) && sizeof($classes) > 0) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1374
        $values = $this->formatValues('?type', $classes, 'uri');
1375
        $query = <<<EOQ
1376
SELECT DISTINCT (ucase(str(substr(?label, 1, 1))) as ?l) WHERE {
1377
  $gcl {
1378
    ?c skos:prefLabel ?label .
1379
    ?c a ?type
1380
    FILTER(langMatches(lang(?label), '$lang'))
1381
    $values
1382
  }
1383
}
1384
EOQ;
1385
        return $query;
1386
    }
1387
1388
    /**
1389
     * Transforms the first characters query results into an array format.
1390
     * @param EasyRdf\Sparql\Result $result
1391
     * @return array
1392
     */
1393
    private function transformFirstCharactersResults($result) {
1394
        $ret = array();
1395
        foreach ($result as $row) {
1396
            $ret[] = $row->l->getValue();
1397
        }
1398
        return $ret;
1399
    }
1400
1401
    /**
1402
     * Query for the first characters (letter or otherwise) of the labels in the particular language.
1403
     * @param string $lang language
1404
     * @return array array of characters
1405
     */
1406
    public function queryFirstCharacters($lang, $classes = null) {
1407
        $query = $this->generateFirstCharactersQuery($lang, $classes);
1408
        $result = $this->query($query);
1409
        return $this->transformFirstCharactersResults($result);
1410
    }
1411
1412
    /**
1413
     * @param string $uri
1414
     * @param string $lang
1415
     * @return string sparql query string
1416
     */
1417
    private function generateLabelQuery($uri, $lang) {
1418
        $fcl = $this->generateFromClause();
1419
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?label), '$lang') )" : "";
1420
        $query = <<<EOQ
1421
SELECT ?label $fcl
1422
WHERE {
1423
  <$uri> a ?type .
1424
  OPTIONAL {
1425
    <$uri> skos:prefLabel ?label .
1426
    $labelcondLabel
1427
  }
1428
  OPTIONAL {
1429
    <$uri> rdfs:label ?label .
1430
    $labelcondLabel
1431
  }
1432
  OPTIONAL {
1433
    <$uri> dc:title ?label .
1434
    $labelcondLabel
1435
  }
1436
  OPTIONAL {
1437
    <$uri> dc11:title ?label .
1438
    $labelcondLabel
1439
  }
1440
}
1441
EOQ;
1442
        return $query;
1443
    }
1444
1445
1446
    /**
1447
     * @param string $uri
1448
     * @param string $lang
1449
     * @return string sparql query string
1450
     */
1451
    private function generateAllLabelsQuery($uri, $lang) {
1452
        $fcl = $this->generateFromClause();
1453
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?val), '$lang') )" : "";
1454
        $query = <<<EOQ
1455
SELECT DISTINCT ?prop ?val $fcl
1456
WHERE {
1457
  <$uri> a ?type .
1458
  OPTIONAL {
1459
      <$uri> ?prop ?val .
1460
      $labelcondLabel
1461
  }
1462
  VALUES ?prop { skos:prefLabel skos:altLabel skos:hiddenLabel }
1463
}
1464
EOQ;
1465
        return $query;
1466
    }
1467
1468
    /**
1469
     * Query for a label (skos:prefLabel, rdfs:label, dc:title, dc11:title) of a resource.
1470
     * @param string $uri
1471
     * @param string $lang
1472
     * @return array array of labels (key: lang, val: label), or null if resource doesn't exist
1473
     */
1474
    public function queryLabel($uri, $lang) {
1475
        $query = $this->generateLabelQuery($uri, $lang);
1476
        $result = $this->query($query);
1477
        $ret = array();
1478
        foreach ($result as $row) {
1479
            if (!isset($row->label)) {
1480
                // existing concept but no labels
1481
                return array();
1482
            }
1483
            $ret[$row->label->getLang()] = $row->label;
1484
        }
1485
1486
        if (sizeof($ret) > 0) {
1487
            // existing concept, with label(s)
1488
            return $ret;
1489
        } else {
1490
            // nonexistent concept
1491
            return null;
1492
        }
1493
    }
1494
1495
    /**
1496
     * Query for skos:prefLabels, skos:altLabels and skos:hiddenLabels of a resource.
1497
     * @param string $uri
1498
     * @param string $lang
1499
     * @return array array of prefLabels, altLabels and hiddenLabels - or null if resource doesn't exist
1500
     */
1501
    public function queryAllConceptLabels($uri, $lang) {
1502
        $query = $this->generateAllLabelsQuery($uri, $lang);
1503
        $result = $this->query($query);
1504
1505
        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

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