Passed
Pull Request — master (#1165)
by
unknown
04:13 queued 17s
created

GenericSparql   F

Complexity

Total Complexity 320

Size/Duplication

Total Lines 2301
Duplicated Lines 0 %

Importance

Changes 13
Bugs 4 Features 1
Metric Value
wmc 320
eloc 908
c 13
b 4
f 1
dl 0
loc 2301
rs 1.692

How to fix   Complexity   

Complex Class

Complex classes like GenericSparql often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use GenericSparql, and based on these observations, apply Extract Interface, too.

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
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;
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
            $this->qnamecache[$uri] = ($qname !== null) ? $qname : $uri;
170
        }
171
        return $this->qnamecache[$uri];
172
    }
173
174
175
    /**
176
     * Generates the sparql query for retrieving concept and collection counts in a vocabulary.
177
     * @return string sparql query
178
     */
179
    private function generateCountConceptsQuery($array, $group) {
180
        $fcl = $this->generateFromClause();
181
        $optional = $array ? "(<$array>) " : '';
182
        $optional .= $group ? "(<$group>)" : '';
183
        $query = <<<EOQ
184
      SELECT (COUNT(DISTINCT(?conc)) as ?c) ?type ?typelabel (COUNT(?depr) as ?deprcount) $fcl WHERE {
185
        VALUES (?value) { (skos:Concept) (skos:Collection) $optional }
186
  	    ?type rdfs:subClassOf* ?value
187
        { ?type ^a ?conc .
188
          OPTIONAL { ?conc owl:deprecated ?depr .
189
  		    FILTER (?depr = True)
190
          }
191
        } UNION {SELECT * WHERE {
192
            ?type rdfs:label ?typelabel
193
          }
194
        }
195
      } GROUP BY ?type ?typelabel
196
EOQ;
197
        return $query;
198
    }
199
200
    /**
201
     * Used for transforming the concept count query results.
202
     * @param EasyRdf\Sparql\Result $result query results to be transformed
203
     * @param string $lang language of labels
204
     * @return Array containing the label counts
205
     */
206
    private function transformCountConceptsResults($result, $lang) {
207
        $ret = array();
208
        foreach ($result as $row) {
209
            if (!isset($row->type)) {
210
                continue;
211
            }
212
            $typeURI = $row->type->getUri();
213
            $ret[$typeURI]['type'] = $typeURI;
214
215
            if (!isset($row->typelabel)) {
216
                $ret[$typeURI]['count'] = $row->c->getValue();
217
                $ret[$typeURI]['deprecatedCount'] = $row->deprcount->getValue();
218
            }
219
220
            if (isset($row->typelabel) && $row->typelabel->getLang() === $lang) {
221
                $ret[$typeURI]['label'] = $row->typelabel->getValue();
222
            }
223
224
        }
225
        return $ret;
226
    }
227
228
    /**
229
     * Used for counting number of concepts and collections in a vocabulary.
230
     * @param string $lang language of labels
231
     * @param string $array the uri of the concept array class, eg. isothes:ThesaurusArray
232
     * @param string $group the uri of the  concept group class, eg. isothes:ConceptGroup
233
     * @return array with number of concepts in this vocabulary per label
234
     */
235
    public function countConcepts($lang = null, $array = null, $group = null) {
236
        $query = $this->generateCountConceptsQuery($array, $group);
237
        $result = $this->query($query);
238
        return $this->transformCountConceptsResults($result, $lang);
239
    }
240
241
    /**
242
     * @param array $langs Languages to query for
243
     * @param string[] $props property names
244
     * @return string sparql query
245
     */
246
    private function generateCountLangConceptsQuery($langs, $classes, $props) {
247
        $gcl = $this->graphClause;
248
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
249
250
	$quote_string = function($val) { return "'$val'"; };
251
	$quoted_values = array_map($quote_string, $langs);
252
	$langFilter = "FILTER(?lang IN (" . implode(',', $quoted_values) . "))";
253
254
        $values = $this->formatValues('?type', $classes, 'uri');
255
        $valuesProp = $this->formatValues('?prop', $props, null);
256
257
        $query = <<<EOQ
258
SELECT ?lang ?prop
259
  (COUNT(?label) as ?count)
260
WHERE {
261
  $gcl {
262
    $values
263
    $valuesProp
264
    ?conc a ?type .
265
    ?conc ?prop ?label .
266
    BIND(LANG(?label) AS ?lang)
267
    $langFilter
268
  }
269
}
270
GROUP BY ?lang ?prop ?type
271
EOQ;
272
        return $query;
273
    }
274
275
    /**
276
     * Transforms the CountLangConcepts results into an array of label counts.
277
     * @param EasyRdf\Sparql\Result $result query results to be transformed
278
     * @param array $langs Languages to query for
279
     * @param string[] $props property names
280
     */
281
    private function transformCountLangConceptsResults($result, $langs, $props) {
282
        $ret = array();
283
        // set default count to zero; overridden below if query found labels
284
        foreach ($langs as $lang) {
285
            foreach ($props as $prop) {
286
                $ret[$lang][$prop] = 0;
287
            }
288
        }
289
        foreach ($result as $row) {
290
            if (isset($row->lang) && isset($row->prop) && isset($row->count)) {
291
                $ret[$row->lang->getValue()][$row->prop->shorten()] +=
292
                $row->count->getValue();
293
            }
294
295
        }
296
        ksort($ret);
297
        return $ret;
298
    }
299
300
    /**
301
     * Counts the number of concepts in a easyRDF graph with a specific language.
302
     * @param array $langs Languages to query for
303
     * @return Array containing count of concepts for each language and property.
304
     */
305
    public function countLangConcepts($langs, $classes = null) {
306
        $props = array('skos:prefLabel', 'skos:altLabel', 'skos:hiddenLabel');
307
        $query = $this->generateCountLangConceptsQuery($langs, $classes, $props);
308
        // Count the number of terms in each language
309
        $result = $this->query($query);
310
        return $this->transformCountLangConceptsResults($result, $langs, $props);
311
    }
312
313
    /**
314
     * Formats a VALUES clause (SPARQL 1.1) which states that the variable should be bound to one
315
     * of the constants given.
316
     * @param string $varname variable name, e.g. "?uri"
317
     * @param array $values the values
318
     * @param string $type type of values: "uri", "literal" or null (determines quoting style)
319
     */
320
    protected function formatValues($varname, $values, $type = null) {
321
        $constants = array();
322
        foreach ($values as $val) {
323
            if ($type == 'uri') {
324
                $val = "<$val>";
325
            }
326
327
            if ($type == 'literal') {
328
                $val = "'$val'";
329
            }
330
331
            $constants[] = "($val)";
332
        }
333
        $values = implode(" ", $constants);
334
335
        return "VALUES ($varname) { $values }";
336
    }
337
338
    /**
339
     * Filters multiple instances of the same vocabulary from the input array.
340
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
341
     * @return \Vocabulary[]
342
     */
343
    private function filterDuplicateVocabs($vocabs) {
344
        // filtering duplicates
345
        $uniqueVocabs = array();
346
        if ($vocabs !== null && sizeof($vocabs) > 0) {
347
            foreach ($vocabs as $voc) {
348
                $uniqueVocabs[$voc->getId()] = $voc;
349
            }
350
        }
351
352
        return $uniqueVocabs;
353
    }
354
355
    /**
356
     * Generates a sparql query for one or more concept URIs
357
     * @param mixed $uris concept URI (string) or array of URIs
358
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
359
     * @param \Vocabulary[]|null $vocabs array of Vocabulary objects
360
     * @return string sparql query
361
     */
362
    private function generateConceptInfoQuery($uris, $arrayClass, $vocabs) {
363
        $gcl = $this->graphClause;
364
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
365
        $values = $this->formatValues('?uri', $uris, 'uri');
366
        $uniqueVocabs = $this->filterDuplicateVocabs($vocabs);
367
        $valuesGraph = empty($vocabs) ? $this->formatValuesGraph($uniqueVocabs) : '';
368
369
        if ($arrayClass === null) {
370
            $construct = $optional = "";
371
        } else {
372
            // add information that can be used to format narrower concepts by
373
            // the array they belong to ("milk by source animal" use case)
374
            $construct = "\n ?x skos:member ?o . ?x skos:prefLabel ?xl . ?x a <$arrayClass> .";
375
            $optional = "\n OPTIONAL {
376
                      ?x skos:member ?o .
377
                      ?x a <$arrayClass> .
378
                      ?x skos:prefLabel ?xl .
379
                      FILTER NOT EXISTS {
380
                        ?x skos:member ?other .
381
                        MINUS { ?other skos:broader ?uri }
382
                      }
383
                    }";
384
        }
385
        $query = <<<EOQ
386
CONSTRUCT {
387
 ?s ?p ?uri .
388
 ?sp ?uri ?op .
389
 ?uri ?p ?o .
390
 ?p rdfs:label ?proplabel .
391
 ?p rdfs:comment ?propcomm .
392
 ?p skos:definition ?propdef .
393
 ?p rdfs:subPropertyOf ?pp .
394
 ?pp rdfs:label ?plabel .
395
 ?o a ?ot .
396
 ?o skos:prefLabel ?opl .
397
 ?o rdfs:label ?ol .
398
 ?o rdf:value ?ov .
399
 ?o skos:notation ?on .
400
 ?o ?oprop ?oval .
401
 ?o ?xlprop ?xlval .
402
 ?directgroup skos:member ?uri .
403
 ?parent skos:member ?group .
404
 ?group skos:prefLabel ?grouplabel .
405
 ?b1 rdf:first ?item .
406
 ?b1 rdf:rest ?b2 .
407
 ?item a ?it .
408
 ?item skos:prefLabel ?il .
409
 ?group a ?grouptype . $construct
410
} $fcl WHERE {
411
 $values
412
 $gcl {
413
  {
414
    ?s ?p ?uri .
415
    FILTER(!isBlank(?s))
416
    FILTER(?p != skos:inScheme)
417
    FILTER NOT EXISTS { ?s owl:deprecated true . }
418
  }
419
  UNION
420
  { ?sp ?uri ?op . }
421
  UNION
422
  {
423
    ?directgroup skos:member ?uri .
424
    ?group skos:member+ ?uri .
425
    ?group skos:prefLabel ?grouplabel .
426
    ?group a ?grouptype .
427
    OPTIONAL { ?parent skos:member ?group }
428
  }
429
  UNION
430
  {
431
   ?uri ?p ?o .
432
   OPTIONAL {
433
     ?o rdf:rest* ?b1 .
434
     ?b1 rdf:first ?item .
435
     ?b1 rdf:rest ?b2 .
436
     OPTIONAL { ?item a ?it . }
437
     OPTIONAL { ?item skos:prefLabel ?il . }
438
   }
439
   OPTIONAL {
440
     { ?p rdfs:label ?proplabel . }
441
     UNION
442
     { ?p rdfs:comment ?propcomm . }
443
     UNION
444
     { ?p skos:definition ?propdef . }
445
     UNION
446
     { ?p rdfs:subPropertyOf ?pp . }
447
   }
448
   OPTIONAL {
449
     { ?o a ?ot . }
450
     UNION
451
     { ?o skos:prefLabel ?opl . }
452
     UNION
453
     { ?o rdfs:label ?ol . }
454
     UNION
455
     { ?o rdf:value ?ov . 
456
       OPTIONAL { ?o ?oprop ?oval . }
457
     }
458
     UNION
459
     { ?o skos:notation ?on . }
460
     UNION
461
     { ?o a skosxl:Label .
462
       ?o ?xlprop ?xlval }
463
   } $optional
464
  }
465
 }
466
}
467
$valuesGraph
468
EOQ;
469
        return $query;
470
    }
471
472
    /**
473
     * Transforms ConceptInfo query results into an array of Concept objects
474
     * @param EasyRdf\Graph $result query results to be transformed
475
     * @param array $uris concept URIs
476
     * @param \Vocabulary[] $vocabs array of Vocabulary object
477
     * @param string|null $clang content language
478
     * @return Concept[] array of Concept objects
479
     */
480
    private function transformConceptInfoResults($result, $uris, $vocabs, $clang) {
481
        $conceptArray = array();
482
        foreach ($uris as $index => $uri) {
483
            $conc = $result->resource($uri);
484
            if (is_array($vocabs)) {
485
                $vocab = (sizeof($vocabs) == 1) ? $vocabs[0] : $vocabs[$index];
486
            } else {
487
                $vocab = null;
488
            }
489
            $conceptArray[] = new Concept($this->model, $vocab, $conc, $result, $clang);
490
        }
491
        return $conceptArray;
492
    }
493
494
    /**
495
     * Returns information (as a graph) for one or more concept URIs
496
     * @param mixed $uris concept URI (string) or array of URIs
497
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
498
     * @param \Vocabulary[]|null $vocabs vocabularies to target
499
     * @return \EasyRdf\Graph
500
     */
501
    public function queryConceptInfoGraph($uris, $arrayClass = null, $vocabs = array()) {
502
        // if just a single URI is given, put it in an array regardless
503
        if (!is_array($uris)) {
504
            $uris = array($uris);
505
        }
506
507
        $query = $this->generateConceptInfoQuery($uris, $arrayClass, $vocabs);
508
        $result = $this->query($query);
509
        return $result;
510
    }
511
512
    /**
513
     * Returns information (as an array of Concept objects) for one or more concept URIs
514
     * @param mixed $uris concept URI (string) or array of URIs
515
     * @param string|null $arrayClass the URI for thesaurus array class, or null if not used
516
     * @param \Vocabulary[] $vocabs vocabularies to target
517
     * @param string|null $clang content language
518
     * @return Concept[]
519
     */
520
    public function queryConceptInfo($uris, $arrayClass = null, $vocabs = array(), $clang = null) {
521
        // if just a single URI is given, put it in an array regardless
522
        if (!is_array($uris)) {
523
            $uris = array($uris);
524
        }
525
        $result = $this->queryConceptInfoGraph($uris, $arrayClass, $vocabs);
526
        if ($result->isEmpty()) {
527
            return [];
528
        }
529
        return $this->transformConceptInfoResults($result, $uris, $vocabs, $clang);
530
    }
531
532
    /**
533
     * Generates the sparql query for queryTypes
534
     * @param string $lang
535
     * @return string sparql query
536
     */
537
    private function generateQueryTypesQuery($lang) {
538
        $fcl = $this->generateFromClause();
539
        $query = <<<EOQ
540
SELECT DISTINCT ?type ?label ?superclass $fcl
541
WHERE {
542
  {
543
    { BIND( skos:Concept as ?type ) }
544
    UNION
545
    { BIND( skos:Collection as ?type ) }
546
    UNION
547
    { BIND( isothes:ConceptGroup as ?type ) }
548
    UNION
549
    { BIND( isothes:ThesaurusArray as ?type ) }
550
    UNION
551
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Concept . }
552
    UNION
553
    { ?type rdfs:subClassOf/rdfs:subClassOf* skos:Collection . }
554
  }
555
  OPTIONAL {
556
    ?type rdfs:label ?label .
557
    FILTER(langMatches(lang(?label), '$lang'))
558
  }
559
  OPTIONAL {
560
    ?type rdfs:subClassOf ?superclass .
561
  }
562
  FILTER EXISTS {
563
    ?s a ?type .
564
    ?s skos:prefLabel ?prefLabel .
565
  }
566
}
567
EOQ;
568
        return $query;
569
    }
570
571
    /**
572
     * Transforms the results into an array format.
573
     * @param EasyRdf\Sparql\Result $result
574
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
575
     */
576
    private function transformQueryTypesResults($result) {
577
        $ret = array();
578
        foreach ($result as $row) {
579
            $type = array();
580
            if (isset($row->label)) {
581
                $type['label'] = $row->label->getValue();
582
            }
583
584
            if (isset($row->superclass)) {
585
                $type['superclass'] = $row->superclass->getUri();
586
            }
587
588
            $ret[$row->type->getURI()] = $type;
589
        }
590
        return $ret;
591
    }
592
593
    /**
594
     * Retrieve information about types from the endpoint
595
     * @param string $lang
596
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
597
     */
598
    public function queryTypes($lang) {
599
        $query = $this->generateQueryTypesQuery($lang);
600
        $result = $this->query($query);
601
        return $this->transformQueryTypesResults($result);
602
    }
603
604
    /**
605
     * Generates the concept scheme query.
606
     * @param string $conceptscheme concept scheme URI
607
     * @return string sparql query
608
     */
609
    private function generateQueryConceptSchemeQuery($conceptscheme) {
610
        $fcl = $this->generateFromClause();
611
        $query = <<<EOQ
612
CONSTRUCT {
613
  <$conceptscheme> ?property ?value .
614
} $fcl WHERE {
615
  <$conceptscheme> ?property ?value .
616
  FILTER (?property != skos:hasTopConcept)
617
}
618
EOQ;
619
        return $query;
620
    }
621
622
    /**
623
     * Retrieves conceptScheme information from the endpoint.
624
     * @param string $conceptscheme concept scheme URI
625
     * @return \EasyRdf\Sparql\Result|\EasyRdf\Graph query result graph
626
     */
627
    public function queryConceptScheme($conceptscheme) {
628
        $query = $this->generateQueryConceptSchemeQuery($conceptscheme);
629
        return $this->query($query);
630
    }
631
632
    /**
633
     * Generates the queryConceptSchemes sparql query.
634
     * @param string $lang language of labels
635
     * @return string sparql query
636
     */
637
    private function generateQueryConceptSchemesQuery($lang) {
638
        $fcl = $this->generateFromClause();
639
        $query = <<<EOQ
640
SELECT ?cs ?label ?preflabel ?title ?domain ?domainLabel $fcl
641
WHERE {
642
 ?cs a skos:ConceptScheme .
643
 OPTIONAL{
644
    ?cs dcterms:subject ?domain.
645
    ?domain skos:prefLabel ?domainLabel.
646
    FILTER(langMatches(lang(?domainLabel), '$lang'))
647
}
648
 OPTIONAL {
649
   ?cs rdfs:label ?label .
650
   FILTER(langMatches(lang(?label), '$lang'))
651
 }
652
 OPTIONAL {
653
   ?cs skos:prefLabel ?preflabel .
654
   FILTER(langMatches(lang(?preflabel), '$lang'))
655
 }
656
 OPTIONAL {
657
   { ?cs dc11:title ?title }
658
   UNION
659
   { ?cs dc:title ?title }
660
   FILTER(langMatches(lang(?title), '$lang'))
661
 }
662
} 
663
ORDER BY ?cs
664
EOQ;
665
        return $query;
666
    }
667
668
    /**
669
     * Transforms the queryConceptScheme results into an array format.
670
     * @param EasyRdf\Sparql\Result $result
671
     * @return array
672
     */
673
    private function transformQueryConceptSchemesResults($result) {
674
        $ret = array();
675
        foreach ($result as $row) {
676
            $conceptscheme = array();
677
            if (isset($row->label)) {
678
                $conceptscheme['label'] = $row->label->getValue();
679
            }
680
681
            if (isset($row->preflabel)) {
682
                $conceptscheme['prefLabel'] = $row->preflabel->getValue();
683
            }
684
685
            if (isset($row->title)) {
686
                $conceptscheme['title'] = $row->title->getValue();
687
            }
688
            // add dct:subject and their labels in the result
689
            if(isset($row->domain) && isset($row->domainLabel)){
690
                $conceptscheme['subject']['uri']=$row->domain->getURI();
691
                $conceptscheme['subject']['prefLabel']=$row->domainLabel->getValue();
692
            }
693
694
            $ret[$row->cs->getURI()] = $conceptscheme;
695
        }
696
        return $ret;
697
    }
698
699
    /**
700
     * return a list of skos:ConceptScheme instances in the given graph
701
     * @param string $lang language of labels
702
     * @return array Array with concept scheme URIs (string) as keys and labels (string) as values
703
     */
704
    public function queryConceptSchemes($lang) {
705
        $query = $this->generateQueryConceptSchemesQuery($lang);
706
        $result = $this->query($query);
707
        return $this->transformQueryConceptSchemesResults($result);
708
    }
709
710
    /**
711
     * Generate a VALUES clause for limiting the targeted graphs.
712
     * @param Vocabulary[]|null $vocabs the vocabularies to target
713
     * @return string[] array of graph URIs
714
     */
715
    protected function getVocabGraphs($vocabs) {
716
        if ($vocabs === null || sizeof($vocabs) == 0) {
717
            // searching from all vocabularies - limit to known graphs
718
            $vocabs = $this->model->getVocabularies();
719
        }
720
        $graphs = array();
721
        foreach ($vocabs as $voc) {
722
            $graph = $voc->getGraph();
723
            if (!is_null($graph) && !in_array($graph, $graphs)) {
724
                $graphs[] = $graph;
725
            }
726
        }
727
        return $graphs;
728
    }
729
730
    /**
731
     * Generate a VALUES clause for limiting the targeted graphs.
732
     * @param Vocabulary[]|null $vocabs array of Vocabulary objects to target
733
     * @return string VALUES clause, or "" if not necessary to limit
734
     */
735
    protected function formatValuesGraph($vocabs) {
736
        if (!$this->isDefaultEndpoint()) {
737
            return "";
738
        }
739
        $graphs = $this->getVocabGraphs($vocabs);
740
        return $this->formatValues('?graph', $graphs, 'uri');
741
    }
742
743
    /**
744
     * Generate a FILTER clause for limiting the targeted graphs.
745
     * @param array $vocabs array of Vocabulary objects to target
746
     * @return string FILTER clause, or "" if not necessary to limit
747
     */
748
    protected function formatFilterGraph($vocabs) {
749
        if (!$this->isDefaultEndpoint()) {
750
            return "";
751
        }
752
        $graphs = $this->getVocabGraphs($vocabs);
753
        $values = array();
754
        foreach ($graphs as $graph) {
755
          $values[] = "<$graph>";
756
        }
757
        if (count($values)) {
758
          return "FILTER (?graph IN (" . implode(',', $values) . "))";
759
        }
760
    }
761
762
    /**
763
     * Formats combined limit and offset clauses for the sparql query
764
     * @param int $limit maximum number of hits to retrieve; 0 for unlimited
765
     * @param int $offset offset of results to retrieve; 0 for beginning of list
766
     * @return string sparql query clauses
767
     */
768
    protected function formatLimitAndOffset($limit, $offset) {
769
        $limit = ($limit) ? 'LIMIT ' . $limit : '';
770
        $offset = ($offset) ? 'OFFSET ' . $offset : '';
771
        // eliminating whitespace and line changes when the conditions aren't needed.
772
        $limitandoffset = '';
773
        if ($limit && $offset) {
774
            $limitandoffset = "\n" . $limit . "\n" . $offset;
775
        } elseif ($limit) {
776
            $limitandoffset = "\n" . $limit;
777
        } elseif ($offset) {
778
            $limitandoffset = "\n" . $offset;
779
        }
780
781
        return $limitandoffset;
782
    }
783
784
    /**
785
     * Formats a sparql query clause for limiting the search to specific concept types.
786
     * @param array $types limit search to concepts of the given type(s)
787
     * @return string sparql query clause
788
     */
789
    protected function formatTypes($types) {
790
        $typePatterns = array();
791
        if (!empty($types)) {
792
            foreach ($types as $type) {
793
                $unprefixed = EasyRdf\RdfNamespace::expand($type);
794
                $typePatterns[] = "{ ?s a <$unprefixed> }";
795
            }
796
        }
797
798
        return implode(' UNION ', $typePatterns);
799
    }
800
801
    /**
802
     * @param string $prop property to include in the result eg. 'broader' or 'narrower'
803
     * @return string sparql query clause
804
     */
805
    private function formatPropertyCsvClause($prop) {
806
        # This expression creates a CSV row containing pairs of (uri,prefLabel) values.
807
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
808
        $clause = <<<EOV
809
(GROUP_CONCAT(DISTINCT CONCAT(
810
 '"', IF(isIRI(?$prop),STR(?$prop),''), '"', ',',
811
 '"', REPLACE(IF(BOUND(?{$prop}lab),?{$prop}lab,''), '"', '""'), '"', ',',
812
 '"', REPLACE(IF(isLiteral(?{$prop}),?{$prop},''), '"', '""'), '"'
813
); separator='\\n') as ?{$prop}s)
814
EOV;
815
        return $clause;
816
    }
817
818
    /**
819
     * @return string sparql query clause
820
     */
821
    private function formatPrefLabelCsvClause() {
822
        # This expression creates a CSV row containing pairs of (prefLabel, lang) values.
823
        # The REPLACE is performed for quotes (" -> "") so they don't break the CSV format.
824
        $clause = <<<EOV
825
(GROUP_CONCAT(DISTINCT CONCAT(
826
 '"', STR(?pref), '"', ',', '"', lang(?pref), '"'
827
); separator='\\n') as ?preflabels)
828
EOV;
829
        return $clause;
830
    }
831
832
    /**
833
     * @param string $lang language code of the returned labels
834
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
835
     * @return array sparql query clause
836
     */
837
    protected function formatExtraFields($lang, $fields) {
838
        // extra variable expressions to request and extra fields to query for
839
        $ret = array('extravars' => '', 'extrafields' => '');
840
841
        if ($fields === null) {
842
            return $ret;
843
        }
844
845
        if (in_array('prefLabel', $fields)) {
846
            $ret['extravars'] .= $this->formatPreflabelCsvClause();
847
            $ret['extrafields'] .= <<<EOF
848
OPTIONAL {
849
  ?s skos:prefLabel ?pref .
850
}
851
EOF;
852
            // removing the prefLabel from the fields since it has been handled separately
853
            $fields = array_diff($fields, array('prefLabel'));
854
        }
855
856
        foreach ($fields as $field) {
857
            $ret['extravars'] .= $this->formatPropertyCsvClause($field);
858
            $ret['extrafields'] .= <<<EOF
859
OPTIONAL {
860
  ?s skos:$field ?$field .
861
  FILTER(!isLiteral(?$field)||langMatches(lang(?{$field}), '$lang'))
862
  OPTIONAL { ?$field skos:prefLabel ?{$field}lab . FILTER(langMatches(lang(?{$field}lab), '$lang')) }
863
}
864
EOF;
865
        }
866
867
        return $ret;
868
    }
869
870
    /**
871
     * Generate condition for matching labels in SPARQL
872
     * @param string $term search term
873
     * @param string $searchLang language code used for matching labels (null means any language)
874
     * @return string sparql query snippet
875
     */
876
    protected function generateConceptSearchQueryCondition($term, $searchLang)
877
    {
878
        # use appropriate matching function depending on query type: =, strstarts, strends or full regex
879
        if (preg_match('/^[^\*]+$/', $term)) { // exact query
880
            $term = str_replace('\\', '\\\\', $term); // quote slashes
881
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
882
            $filtercond = "LCASE(STR(?match)) = '$term'";
883
        } elseif (preg_match('/^[^\*]+\*$/', $term)) { // prefix query
884
            $term = substr($term, 0, -1); // remove the final asterisk
885
            $term = str_replace('\\', '\\\\', $term); // quote slashes
886
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
887
            $filtercond = "STRSTARTS(LCASE(STR(?match)), '$term')";
888
        } elseif (preg_match('/^\*[^\*]+$/', $term)) { // suffix query
889
            $term = substr($term, 1); // remove the preceding asterisk
890
            $term = str_replace('\\', '\\\\', $term); // quote slashes
891
            $term = str_replace('\'', '\\\'', mb_strtolower($term, 'UTF-8')); // make lowercase and escape single quotes
892
            $filtercond = "STRENDS(LCASE(STR(?match)), '$term')";
893
        } else { // too complicated - have to use a regex
894
            # make sure regex metacharacters are not passed through
895
            $term = str_replace('\\', '\\\\', preg_quote($term));
896
            $term = str_replace('\\\\*', '.*', $term); // convert asterisk to regex syntax
897
            $term = str_replace('\'', '\\\'', $term); // ensure single quotes are quoted
898
            $filtercond = "REGEX(STR(?match), '^$term$', 'i')";
899
        }
900
901
        $labelcondMatch = ($searchLang) ? "&& (?prop = skos:notation || LANGMATCHES(lang(?match), ?langParam))" : "";
902
903
        return "?s ?prop ?match . FILTER ($filtercond $labelcondMatch)";
904
    }
905
906
907
    /**
908
     * Inner query for concepts using a search term.
909
     * @param string $term search term
910
     * @param string $lang language code of the returned labels
911
     * @param string $searchLang language code used for matching labels (null means any language)
912
     * @param string[] $props properties to target e.g. array('skos:prefLabel','skos:altLabel')
913
     * @param boolean $unique restrict results to unique concepts (default: false)
914
     * @return string sparql query
915
     */
916
    protected function generateConceptSearchQueryInner($term, $lang, $searchLang, $props, $unique, $filterGraph)
917
    {
918
        $valuesProp = $this->formatValues('?prop', $props);
919
        $textcond = $this->generateConceptSearchQueryCondition($term, $searchLang);
920
921
        $rawterm = str_replace(array('\\', '*', '"'), array('\\\\', '', '\"'), $term);
922
        // graph clause, if necessary
923
        $graphClause = $filterGraph != '' ? 'GRAPH ?graph' : '';
924
925
        // extra conditions for label language, if specified
926
        $labelcondLabel = ($lang) ? "LANGMATCHES(lang(?label), '$lang')" : "lang(?match) = '' || LANGMATCHES(lang(?label), lang(?match))";
927
        // if search language and UI/display language differ, must also consider case where there is no prefLabel in
928
        // the display language; in that case, should use the label with the same language as the matched label
929
        $labelcondFallback = ($searchLang != $lang) ?
930
          "OPTIONAL { # in case previous OPTIONAL block gives no labels\n" .
931
          "?s skos:prefLabel ?label . FILTER (LANGMATCHES(LANG(?label), LANG(?match))) }" : "";
932
933
        //  Including the labels if there is no query term given.
934
        if ($rawterm === '') {
935
          $labelClause = "?s skos:prefLabel ?label .";
936
          $labelClause = ($lang) ? $labelClause . " FILTER (LANGMATCHES(LANG(?label), '$lang'))" : $labelClause . "";
937
          return $labelClause . " BIND(?label AS ?match)";
938
        }
939
940
        /*
941
         * This query does some tricks to obtain a list of unique concepts.
942
         * From each match generated by the text index, a string such as
943
         * "1en@example" is generated, where the first character is a number
944
         * encoding the property and priority, then comes the language tag and
945
         * finally the original literal after an @ sign. Of these, the MIN
946
         * function is used to pick the best match for each concept. Finally,
947
         * the structure is unpacked to get back the original string. Phew!
948
         */
949
        $hitvar = $unique ? '(MIN(?matchstr) AS ?hit)' : '(?matchstr AS ?hit)';
950
        $hitgroup = $unique ? 'GROUP BY ?s ?label ?notation' : '';
951
952
        $langClause = $this->generateLangClause($searchLang);
953
954
        $query = <<<EOQ
955
   SELECT DISTINCT ?s ?label ?notation $hitvar
956
   WHERE {
957
    $graphClause {
958
     { 
959
     $valuesProp
960
     VALUES (?prop ?pri ?langParam) { (skos:prefLabel 1 $langClause) (skos:altLabel 3 $langClause) (skos:notation 5 '') (skos:hiddenLabel 7 $langClause)}
961
     $textcond
962
     ?s ?prop ?match }
963
     OPTIONAL {
964
      ?s skos:prefLabel ?label .
965
      FILTER ($labelcondLabel)
966
     } $labelcondFallback
967
     BIND(IF(langMatches(LANG(?match),'$lang'), ?pri, ?pri+1) AS ?npri)
968
     BIND(CONCAT(STR(?npri), LANG(?match), '@', STR(?match)) AS ?matchstr)
969
     OPTIONAL { ?s skos:notation ?notation }
970
    }
971
    $filterGraph
972
   }
973
   $hitgroup
974
EOQ;
975
976
        return $query;
977
    }
978
    /**
979
    *  This function can be overwritten in other SPARQL dialects for the possibility of handling the different language clauses
980
     * @param string $lang
981
     * @return string formatted language clause
982
     */
983
    protected function generateLangClause($lang) {
984
        return "'$lang'";
985
    }
986
987
    /**
988
     * Query for concepts using a search term.
989
     * @param array|null $fields extra fields to include in the result (array of strings). (default: null = none)
990
     * @param boolean $unique restrict results to unique concepts (default: false)
991
     * @param boolean $showDeprecated whether to include deprecated concepts in search results (default: false)
992
     * @param ConceptSearchParameters $params
993
     * @return string sparql query
994
     */
995
    protected function generateConceptSearchQuery($fields, $unique, $params, $showDeprecated = false) {
996
        $vocabs = $params->getVocabs();
997
        $gcl = $this->graphClause;
998
        $fcl = empty($vocabs) ? '' : $this->generateFromClause($vocabs);
999
        $formattedtype = $this->formatTypes($params->getTypeLimit());
1000
        $formattedfields = $this->formatExtraFields($params->getLang(), $fields);
1001
        $extravars = $formattedfields['extravars'];
1002
        $extrafields = $formattedfields['extrafields'];
1003
        $schemes = $params->getSchemeLimit();
1004
1005
        // limit the search to only requested concept schemes
1006
        $schemecond = '';
1007
        if (!empty($schemes)) {
1008
            $conditions = array();
1009
            foreach($schemes as $scheme) {
1010
                $conditions[] = "{?s skos:inScheme <$scheme>}";
1011
            }
1012
            $schemecond = '{'.implode(" UNION ",$conditions).'}';
1013
        }
1014
        $filterDeprecated="";
1015
        //show or hide deprecated concepts
1016
        if(!$showDeprecated){
1017
            $filterDeprecated="FILTER NOT EXISTS { ?s owl:deprecated true }";
1018
        }
1019
        // extra conditions for parent and group, if specified
1020
        $parentcond = ($params->getParentLimit()) ? "?s skos:broader+ <" . $params->getParentLimit() . "> ." : "";
1021
        $groupcond = ($params->getGroupLimit()) ? "<" . $params->getGroupLimit() . "> skos:member ?s ." : "";
1022
        $pgcond = $parentcond . $groupcond;
1023
1024
        $orderextra = $this->isDefaultEndpoint() ? $this->graph : '';
1025
1026
        # make VALUES clauses
1027
        $props = array('skos:prefLabel', 'skos:altLabel');
1028
1029
        //add notation into searchable data for the vocabularies which have been configured for it
1030
        if ($vocabs) {
1031
            $searchByNotation = false;
1032
            foreach ($vocabs as $vocab) {
1033
                if ($vocab->getConfig()->searchByNotation()) {
1034
                    $searchByNotation = true;
1035
                }
1036
            }
1037
            if ($searchByNotation) {
1038
                $props[] = 'skos:notation';
1039
            }
1040
        }
1041
1042
        if ($params->getHidden()) {
1043
            $props[] = 'skos:hiddenLabel';
1044
        }
1045
        $filterGraph = empty($vocabs) ? $this->formatFilterGraph($vocabs) : '';
1046
1047
        // remove futile asterisks from the search term
1048
        $term = $params->getSearchTerm();
1049
        while (strpos($term, '**') !== false) {
1050
            $term = str_replace('**', '*', $term);
1051
        }
1052
1053
        $labelpriority = <<<EOQ
1054
  FILTER(BOUND(?s))
1055
  BIND(STR(SUBSTR(?hit,1,1)) AS ?pri)
1056
  BIND(IF((SUBSTR(STRBEFORE(?hit, '@'),1) != ?pri), STRLANG(STRAFTER(?hit, '@'), SUBSTR(STRBEFORE(?hit, '@'),2)), STRAFTER(?hit, '@')) AS ?match)
1057
  BIND(IF((?pri = "1" || ?pri = "2") && ?match != ?label, ?match, ?unbound) as ?plabel)
1058
  BIND(IF((?pri = "3" || ?pri = "4"), ?match, ?unbound) as ?alabel)
1059
  BIND(IF((?pri = "7" || ?pri = "8"), ?match, ?unbound) as ?hlabel)
1060
EOQ;
1061
        $innerquery = $this->generateConceptSearchQueryInner($params->getSearchTerm(), $params->getLang(), $params->getSearchLang(), $props, $unique, $filterGraph);
1062
        if ($params->getSearchTerm() === '*' || $params->getSearchTerm() === '') {
1063
          $labelpriority = '';
1064
        }
1065
        $query = <<<EOQ
1066
SELECT DISTINCT ?s ?label ?plabel ?alabel ?hlabel ?graph ?notation (GROUP_CONCAT(DISTINCT STR(?type);separator=' ') as ?types) $extravars 
1067
$fcl
1068
WHERE {
1069
 $gcl {
1070
  {
1071
  $innerquery
1072
  }
1073
  $labelpriority
1074
  $formattedtype
1075
  { $pgcond 
1076
   ?s a ?type .
1077
   $extrafields $schemecond
1078
  }
1079
  $filterDeprecated
1080
 }
1081
 $filterGraph
1082
}
1083
GROUP BY ?s ?match ?label ?plabel ?alabel ?hlabel ?notation ?graph
1084
ORDER BY LCASE(STR(?match)) LANG(?match) $orderextra
1085
EOQ;
1086
        return $query;
1087
    }
1088
1089
    /**
1090
     * Transform a single concept search query results into the skosmos desired return format.
1091
     * @param $row SPARQL query result row
1092
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1093
     * @return array query result object
1094
     */
1095
    private function transformConceptSearchResult($row, $vocabs, $fields)
1096
    {
1097
        $hit = array();
1098
        $hit['uri'] = $row->s->getUri();
1099
1100
        if (isset($row->graph)) {
1101
            $hit['graph'] = $row->graph->getUri();
1102
        }
1103
1104
        foreach (explode(" ", $row->types->getValue()) as $typeuri) {
1105
            $hit['type'][] = $this->shortenUri($typeuri);
1106
        }
1107
1108
        if(!empty($fields)) {
1109
            foreach ($fields as $prop) {
1110
                $propname = $prop . 's';
1111
                if (isset($row->$propname)) {
1112
                    foreach (explode("\n", $row->$propname->getValue()) as $line) {
1113
                        $rdata = str_getcsv($line, ',', '"', '"');
1114
                        $propvals = array();
1115
                        if ($rdata[0] != '') {
1116
                            $propvals['uri'] = $rdata[0];
1117
                        }
1118
                        if ($rdata[1] != '') {
1119
                            $propvals['prefLabel'] = $rdata[1];
1120
                        }
1121
                        if ($rdata[2] != '') {
1122
                            $propvals = $rdata[2];
1123
                        }
1124
1125
                        $hit['skos:' . $prop][] = $propvals;
1126
                    }
1127
                }
1128
            }
1129
        }
1130
1131
1132
        if (isset($row->preflabels)) {
1133
            foreach (explode("\n", $row->preflabels->getValue()) as $line) {
1134
                $pref = str_getcsv($line, ',', '"', '"');
1135
                $hit['prefLabels'][$pref[1]] = $pref[0];
1136
            }
1137
        }
1138
1139
        foreach ($vocabs as $vocab) { // looping the vocabulary objects and asking these for a localname for the concept.
1140
            $localname = $vocab->getLocalName($hit['uri']);
1141
            if ($localname !== $hit['uri']) { // only passing the result forward if the uri didn't boomerang right back.
1142
                $hit['localname'] = $localname;
1143
                break; // stopping the search when we find one that returns something valid.
1144
            }
1145
        }
1146
1147
        if (isset($row->label)) {
1148
            $hit['prefLabel'] = $row->label->getValue();
1149
        }
1150
1151
        if (isset($row->label)) {
1152
            $hit['lang'] = $row->label->getLang();
1153
        }
1154
1155
        if (isset($row->notation)) {
1156
            $hit['notation'] = $row->notation->getValue();
1157
        }
1158
1159
        if (isset($row->plabel)) {
1160
            $hit['matchedPrefLabel'] = $row->plabel->getValue();
1161
            $hit['lang'] = $row->plabel->getLang();
1162
        } elseif (isset($row->alabel)) {
1163
            $hit['altLabel'] = $row->alabel->getValue();
1164
            $hit['lang'] = $row->alabel->getLang();
1165
        } elseif (isset($row->hlabel)) {
1166
            $hit['hiddenLabel'] = $row->hlabel->getValue();
1167
            $hit['lang'] = $row->hlabel->getLang();
1168
        }
1169
        return $hit;
1170
    }
1171
1172
    /**
1173
     * Transform the concept search query results into the skosmos desired return format.
1174
     * @param EasyRdf\Sparql\Result $results
1175
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1176
     * @return array query result object
1177
     */
1178
    private function transformConceptSearchResults($results, $vocabs, $fields) {
1179
        $ret = array();
1180
1181
        foreach ($results as $row) {
1182
            if (!isset($row->s)) {
1183
                // don't break if query returns a single dummy result
1184
                continue;
1185
            }
1186
            $ret[] = $this->transformConceptSearchResult($row, $vocabs, $fields);
1187
        }
1188
        return $ret;
1189
    }
1190
1191
    /**
1192
     * Query for concepts using a search term.
1193
     * @param array $vocabs array of Vocabulary objects to search; empty for global search
1194
     * @param array $fields extra fields to include in the result (array of strings). (default: null = none)
1195
     * @param boolean $unique restrict results to unique concepts (default: false)
1196
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1197
     * @param ConceptSearchParameters $params
1198
     * @return array query result object
1199
     */
1200
    public function queryConcepts($vocabs, $fields = null, $unique = false, $params, $showDeprecated = false) {
1201
        $query = $this->generateConceptSearchQuery($fields, $unique, $params,$showDeprecated);
1202
        $results = $this->query($query);
1203
        return $this->transformConceptSearchResults($results, $vocabs, $fields);
1204
    }
1205
1206
    /**
1207
     * Generates sparql query clauses used for creating the alphabetical index.
1208
     * @param string $letter the letter (or special class) to search for
1209
     * @return array of sparql query clause strings
1210
     */
1211
    private function formatFilterConditions($letter, $lang) {
1212
        $useRegex = false;
1213
1214
        if ($letter == '*') {
1215
            $letter = '.*';
1216
            $useRegex = true;
1217
        } elseif ($letter == '0-9') {
1218
            $letter = '[0-9].*';
1219
            $useRegex = true;
1220
        } elseif ($letter == '!*') {
1221
            $letter = '[^\\\\p{L}\\\\p{N}].*';
1222
            $useRegex = true;
1223
        }
1224
1225
        # make text query clause
1226
        $lcletter = mb_strtolower($letter, 'UTF-8'); // convert to lower case, UTF-8 safe
1227
        if ($useRegex) {
1228
            $filtercondLabel = $lang ? "regex(str(?label), '^$letter$', 'i') && langMatches(lang(?label), '$lang')" : "regex(str(?label), '^$letter$', 'i')";
1229
            $filtercondALabel = $lang ? "regex(str(?alabel), '^$letter$', 'i') && langMatches(lang(?alabel), '$lang')" : "regex(str(?alabel), '^$letter$', 'i')";
1230
        } else {
1231
            $filtercondLabel = $lang ? "strstarts(lcase(str(?label)), '$lcletter') && langMatches(lang(?label), '$lang')" : "strstarts(lcase(str(?label)), '$lcletter')";
1232
            $filtercondALabel = $lang ? "strstarts(lcase(str(?alabel)), '$lcletter') && langMatches(lang(?alabel), '$lang')" : "strstarts(lcase(str(?alabel)), '$lcletter')";
1233
        }
1234
        return array('filterpref' => $filtercondLabel, 'filteralt' => $filtercondALabel);
1235
    }
1236
1237
    /**
1238
     * Generates the sparql query used for rendering the alphabetical index.
1239
     * @param string $letter the letter (or special class) to search for
1240
     * @param string $lang language of labels
1241
     * @param integer $limit limits the amount of results
1242
     * @param integer $offset offsets the result set
1243
     * @param array|null $classes
1244
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1245
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1246
     * @return string sparql query
1247
     */
1248
    protected function generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated = false, $qualifier = null) {
1249
        $gcl = $this->graphClause;
1250
        $classes = ($classes) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1251
        $values = $this->formatValues('?type', $classes, 'uri');
1252
        $limitandoffset = $this->formatLimitAndOffset($limit, $offset);
1253
        $conditions = $this->formatFilterConditions($letter, $lang);
1254
        $filtercondLabel = $conditions['filterpref'];
1255
        $filtercondALabel = $conditions['filteralt'];
1256
        $qualifierClause = $qualifier ? "OPTIONAL { ?s <" . $qualifier->getURI() . "> ?qualifier }" : "";
1257
        $filterDeprecated="";
1258
        if(!$showDeprecated){
1259
            $filterDeprecated="FILTER NOT EXISTS { ?s owl:deprecated true }";
1260
        }
1261
        $query = <<<EOQ
1262
SELECT DISTINCT ?s ?label ?alabel ?qualifier
1263
WHERE {
1264
  $gcl {
1265
    {
1266
      ?s skos:prefLabel ?label .
1267
      FILTER (
1268
        $filtercondLabel
1269
      )
1270
    }
1271
    UNION
1272
    {
1273
      {
1274
        ?s skos:altLabel ?alabel .
1275
        FILTER (
1276
          $filtercondALabel
1277
        )
1278
      }
1279
      {
1280
        ?s skos:prefLabel ?label .
1281
        FILTER (langMatches(lang(?label), '$lang'))
1282
      }
1283
    }
1284
    ?s a ?type .
1285
    $qualifierClause
1286
    $filterDeprecated
1287
    $values
1288
  }
1289
}
1290
ORDER BY LCASE(STR(COALESCE(?alabel, ?label))) STR(?s) LCASE(STR(?qualifier)) $limitandoffset
1291
EOQ;
1292
        return $query;
1293
    }
1294
1295
    /**
1296
     * Transforms the alphabetical list query results into an array format.
1297
     * @param EasyRdf\Sparql\Result $results
1298
     * @return array
1299
     */
1300
    private function transformAlphabeticalListResults($results) {
1301
        $ret = array();
1302
1303
        foreach ($results as $row) {
1304
            if (!isset($row->s)) {
1305
                continue;
1306
            }
1307
            // don't break if query returns a single dummy result
1308
1309
            $hit = array();
1310
            $hit['uri'] = $row->s->getUri();
1311
1312
            $hit['localname'] = $row->s->localName();
1313
1314
            $hit['prefLabel'] = $row->label->getValue();
1315
            $hit['lang'] = $row->label->getLang();
1316
1317
            if (isset($row->alabel)) {
1318
                $hit['altLabel'] = $row->alabel->getValue();
1319
                $hit['lang'] = $row->alabel->getLang();
1320
            }
1321
1322
            if (isset($row->qualifier)) {
1323
                if ($row->qualifier instanceof EasyRdf\Literal) {
1324
                    $hit['qualifier'] = $row->qualifier->getValue();
1325
                }
1326
                else {
1327
                    $hit['qualifier'] = $row->qualifier->localName();
1328
                }
1329
            }
1330
1331
            $ret[] = $hit;
1332
        }
1333
1334
        return $ret;
1335
    }
1336
1337
    /**
1338
     * Query for concepts with a term starting with the given letter. Also special classes '0-9' (digits),
1339
     * '*!' (special characters) and '*' (everything) are accepted.
1340
     * @param string $letter the letter (or special class) to search for
1341
     * @param string $lang language of labels
1342
     * @param integer $limit limits the amount of results
1343
     * @param integer $offset offsets the result set
1344
     * @param array $classes
1345
     * @param boolean $showDeprecated whether to include deprecated concepts in the result (default: false)
1346
     * @param \EasyRdf\Resource|null $qualifier alphabetical list qualifier resource or null (default: null)
1347
     */
1348
    public function queryConceptsAlphabetical($letter, $lang, $limit = null, $offset = null, $classes = null, $showDeprecated = false, $qualifier = null) {
1349
        if ($letter === '') {
1350
            return array(); // special case: no letter given, return empty list
1351
        }
1352
        $query = $this->generateAlphabeticalListQuery($letter, $lang, $limit, $offset, $classes, $showDeprecated, $qualifier);
1353
        $results = $this->query($query);
1354
        return $this->transformAlphabeticalListResults($results);
1355
    }
1356
1357
    /**
1358
     * Creates the query used for finding out which letters should be displayed in the alphabetical index.
1359
     * Note that we force the datatype of the result variable otherwise Virtuoso does not properly interpret the DISTINCT and we have duplicated results
1360
     * @param string $lang language
1361
     * @return string sparql query
1362
     */
1363
    private function generateFirstCharactersQuery($lang, $classes) {
1364
        $gcl = $this->graphClause;
1365
        $classes = (isset($classes) && sizeof($classes) > 0) ? $classes : array('http://www.w3.org/2004/02/skos/core#Concept');
1366
        $values = $this->formatValues('?type', $classes, 'uri');
1367
        $query = <<<EOQ
1368
SELECT DISTINCT (ucase(str(substr(?label, 1, 1))) as ?l) WHERE {
1369
  $gcl {
1370
    ?c skos:prefLabel ?label .
1371
    ?c a ?type
1372
    FILTER(langMatches(lang(?label), '$lang'))
1373
    $values
1374
  }
1375
}
1376
EOQ;
1377
        return $query;
1378
    }
1379
1380
    /**
1381
     * Transforms the first characters query results into an array format.
1382
     * @param EasyRdf\Sparql\Result $result
1383
     * @return array
1384
     */
1385
    private function transformFirstCharactersResults($result) {
1386
        $ret = array();
1387
        foreach ($result as $row) {
1388
            $ret[] = $row->l->getValue();
1389
        }
1390
        return $ret;
1391
    }
1392
1393
    /**
1394
     * Query for the first characters (letter or otherwise) of the labels in the particular language.
1395
     * @param string $lang language
1396
     * @return array array of characters
1397
     */
1398
    public function queryFirstCharacters($lang, $classes = null) {
1399
        $query = $this->generateFirstCharactersQuery($lang, $classes);
1400
        $result = $this->query($query);
1401
        return $this->transformFirstCharactersResults($result);
1402
    }
1403
1404
    /**
1405
     * @param string $uri
1406
     * @param string $lang
1407
     * @return string sparql query string
1408
     */
1409
    private function generateLabelQuery($uri, $lang) {
1410
        $fcl = $this->generateFromClause();
1411
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?label), '$lang') )" : "";
1412
        $query = <<<EOQ
1413
SELECT ?label $fcl
1414
WHERE {
1415
  <$uri> a ?type .
1416
  OPTIONAL {
1417
    <$uri> skos:prefLabel ?label .
1418
    $labelcondLabel
1419
  }
1420
  OPTIONAL {
1421
    <$uri> rdfs:label ?label .
1422
    $labelcondLabel
1423
  }
1424
  OPTIONAL {
1425
    <$uri> dc:title ?label .
1426
    $labelcondLabel
1427
  }
1428
  OPTIONAL {
1429
    <$uri> dc11:title ?label .
1430
    $labelcondLabel
1431
  }
1432
}
1433
EOQ;
1434
        return $query;
1435
    }
1436
1437
1438
    /**
1439
     * @param string $uri
1440
     * @param string $lang
1441
     * @return string sparql query string
1442
     */
1443
    private function generateAllLabelsQuery($uri, $lang) {
1444
        $fcl = $this->generateFromClause();
1445
        $labelcondLabel = ($lang) ? "FILTER( langMatches(lang(?val), '$lang') )" : "";
1446
        $query = <<<EOQ
1447
SELECT DISTINCT ?prop ?val $fcl
1448
WHERE {
1449
  <$uri> a ?type .
1450
  OPTIONAL {
1451
      <$uri> ?prop ?val .
1452
      $labelcondLabel
1453
  }
1454
  VALUES ?prop { skos:prefLabel skos:altLabel skos:hiddenLabel }
1455
}
1456
EOQ;
1457
        return $query;
1458
    }
1459
1460
    /**
1461
     * Query for a label (skos:prefLabel, rdfs:label, dc:title, dc11:title) of a resource.
1462
     * @param string $uri
1463
     * @param string $lang
1464
     * @return array array of labels (key: lang, val: label), or null if resource doesn't exist
1465
     */
1466
    public function queryLabel($uri, $lang) {
1467
        $query = $this->generateLabelQuery($uri, $lang);
1468
        $result = $this->query($query);
1469
        $ret = array();
1470
        foreach ($result as $row) {
1471
            if (!isset($row->label)) {
1472
                // existing concept but no labels
1473
                return array();
1474
            }
1475
            $ret[$row->label->getLang()] = $row->label;
1476
        }
1477
1478
        if (sizeof($ret) > 0) {
1479
            // existing concept, with label(s)
1480
            return $ret;
1481
        } else {
1482
            // nonexistent concept
1483
            return null;
1484
        }
1485
    }
1486
1487
    /**
1488
     * Query for skos:prefLabels, skos:altLabels and skos:hiddenLabels of a resource.
1489
     * @param string $uri
1490
     * @param string $lang
1491
     * @return array array of prefLabels, altLabels and hiddenLabels - or null if resource doesn't exist
1492
     */
1493
    public function queryAllConceptLabels($uri, $lang) {
1494
        $query = $this->generateAllLabelsQuery($uri, $lang);
1495
        $result = $this->query($query);
1496
1497
        if ($result->numRows() == 0) {
1498
            // nonexistent concept
1499
            return null;
1500
        }
1501
1502
        $ret = array();
1503
        foreach ($result as $row) {
1504
            $labelName = $row->prop->localName();
1505
            if (isset($row->val)) {
1506
                $ret[$labelName][] = $row->val->getValue();
1507
            }
1508
        }
1509
        return $ret;
1510
    }
1511
1512
    /**
1513
     * Generates a SPARQL query to retrieve the super properties of a given property URI.
1514
     * Note this must be executed in the graph where this information is available.
1515
     * @param string $uri
1516
     * @return string sparql query string
1517
     */
1518
    private function generateSubPropertyOfQuery($uri) {
1519
        $fcl = $this->generateFromClause();
1520
        $query = <<<EOQ
1521
SELECT ?superProperty $fcl
1522
WHERE {
1523
  <$uri> rdfs:subPropertyOf ?superProperty
1524
}
1525
EOQ;
1526
        return $query;
1527
    }
1528
1529
    /**
1530
     * Query the super properties of a provided property URI.
1531
     * @param string $uri URI of a propertyes
1532
     * @return array array super properties, or null if none exist
1533
     */
1534
    public function querySuperProperties($uri) {
1535
        $query = $this->generateSubPropertyOfQuery($uri);
1536
        $result = $this->query($query);
1537
        $ret = array();
1538
        foreach ($result as $row) {
1539
            if (isset($row->superProperty)) {
1540
                $ret[] = $row->superProperty->getUri();
1541
            }
1542
1543
        }
1544
1545
        if (sizeof($ret) > 0) {
1546
            // return result
1547
            return $ret;
1548
        } else {
1549
            // no result, return null
1550
            return null;
1551
        }
1552
    }
1553
1554
1555
    /**
1556
     * Generates a sparql query for queryNotation.
1557
     * @param string $uri
1558
     * @return string sparql query
1559
     */
1560
    private function generateNotationQuery($uri) {
1561
        $fcl = $this->generateFromClause();
1562
1563
        $query = <<<EOQ
1564
SELECT * $fcl
1565
WHERE {
1566
  <$uri> skos:notation ?notation .
1567
}
1568
EOQ;
1569
        return $query;
1570
    }
1571
1572
    /**
1573
     * Query for the notation of the concept (skos:notation) of a resource.
1574
     * @param string $uri
1575
     * @return string notation or null if it doesn't exist
1576
     */
1577
    public function queryNotation($uri) {
1578
        $query = $this->generateNotationQuery($uri);
1579
        $result = $this->query($query);
1580
        foreach ($result as $row) {
1581
            if (isset($row->notation)) {
1582
                return $row->notation->getValue();
1583
            }
1584
        }
1585
        return null;
1586
    }
1587
1588
    /**
1589
     * Generates a sparql query for queryProperty.
1590
     * @param string $uri
1591
     * @param string $prop the name of the property eg. 'skos:broader'.
1592
     * @param string $lang
1593
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1594
     * @return string sparql query
1595
     */
1596
    private function generatePropertyQuery($uri, $prop, $lang, $anylang) {
1597
        $fcl = $this->generateFromClause();
1598
        $anylang = $anylang ? "OPTIONAL { ?object skos:prefLabel ?label }" : "";
1599
1600
        $query = <<<EOQ
1601
SELECT * $fcl
1602
WHERE {
1603
  <$uri> a skos:Concept .
1604
  OPTIONAL {
1605
    <$uri> $prop ?object .
1606
    OPTIONAL {
1607
      ?object skos:prefLabel ?label .
1608
      FILTER (langMatches(lang(?label), "$lang"))
1609
    }
1610
    OPTIONAL {
1611
      ?object skos:prefLabel ?label .
1612
      FILTER (lang(?label) = "")
1613
    }
1614
    $anylang
1615
  }
1616
}
1617
EOQ;
1618
        return $query;
1619
    }
1620
1621
    /**
1622
     * Transforms the sparql query result into an array or null if the concept doesn't exist.
1623
     * @param EasyRdf\Sparql\Result $result
1624
     * @param string $lang
1625
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1626
     */
1627
    private function transformPropertyQueryResults($result, $lang) {
1628
        $ret = array();
1629
        foreach ($result as $row) {
1630
            if (!isset($row->object)) {
1631
                return array();
1632
            }
1633
            // existing concept but no properties
1634
            if (isset($row->label)) {
1635
                if ($row->label->getLang() === $lang || array_key_exists($row->object->getUri(), $ret) === false) {
1636
                    $ret[$row->object->getUri()]['label'] = $row->label->getValue();
1637
                }
1638
1639
            } else {
1640
                $ret[$row->object->getUri()]['label'] = null;
1641
            }
1642
        }
1643
        if (sizeof($ret) > 0) {
1644
            return $ret;
1645
        }
1646
        // existing concept, with properties
1647
        else {
1648
            return null;
1649
        }
1650
        // nonexistent concept
1651
    }
1652
1653
    /**
1654
     * Query a single property of a concept.
1655
     * @param string $uri
1656
     * @param string $prop the name of the property eg. 'skos:broader'.
1657
     * @param string $lang
1658
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1659
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1660
     */
1661
    public function queryProperty($uri, $prop, $lang, $anylang = false) {
1662
        $uri = is_array($uri) ? $uri[0] : $uri;
1663
        $query = $this->generatePropertyQuery($uri, $prop, $lang, $anylang);
1664
        $result = $this->query($query);
1665
        return $this->transformPropertyQueryResults($result, $lang);
1666
    }
1667
1668
    /**
1669
     * Query a single transitive property of a concept.
1670
     * @param string $uri
1671
     * @param array $props the name of the property eg. 'skos:broader'.
1672
     * @param string $lang
1673
     * @param integer $limit
1674
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1675
     * @return string sparql query
1676
     */
1677
    private function generateTransitivePropertyQuery($uri, $props, $lang, $limit, $anylang) {
1678
        $uri = is_array($uri) ? $uri[0] : $uri;
1679
        $fcl = $this->generateFromClause();
1680
        $propertyClause = implode('|', $props);
1681
        $otherlang = $anylang ? "OPTIONAL { ?object skos:prefLabel ?label }" : "";
1682
        // need to do a SPARQL subquery because LIMIT needs to be applied /after/
1683
        // the direct relationships have been collapsed into one string
1684
        $query = <<<EOQ
1685
SELECT * $fcl
1686
WHERE {
1687
  SELECT ?object ?label (GROUP_CONCAT(STR(?dir);separator=' ') as ?direct)
1688
  WHERE {
1689
    <$uri> a skos:Concept .
1690
    OPTIONAL {
1691
      <$uri> $propertyClause* ?object .
1692
      OPTIONAL {
1693
        ?object $propertyClause ?dir .
1694
      }
1695
    }
1696
    OPTIONAL {
1697
      ?object skos:prefLabel ?label .
1698
      FILTER (langMatches(lang(?label), "$lang"))
1699
    }
1700
    $otherlang
1701
  }
1702
  GROUP BY ?object ?label
1703
}
1704
LIMIT $limit
1705
EOQ;
1706
        return $query;
1707
    }
1708
1709
    /**
1710
     * Transforms the sparql query result object into an array.
1711
     * @param EasyRdf\Sparql\Result $result
1712
     * @param string $lang
1713
     * @param string $fallbacklang language to use if label is not available in the preferred language
1714
     * @return array of property values (key: URI, val: label), or null if concept doesn't exist
1715
     */
1716
    private function transformTransitivePropertyResults($result, $lang, $fallbacklang) {
1717
        $ret = array();
1718
        foreach ($result as $row) {
1719
            if (!isset($row->object)) {
1720
                return array();
1721
            }
1722
            // existing concept but no properties
1723
            if (isset($row->label)) {
1724
                $val = array('label' => $row->label->getValue());
1725
            } else {
1726
                $val = array('label' => null);
1727
            }
1728
            if (isset($row->direct) && $row->direct->getValue() != '') {
1729
                $val['direct'] = explode(' ', $row->direct->getValue());
1730
            }
1731
            // Preventing labels in a non preferred language overriding the preferred language.
1732
            if (isset($row->label) && $row->label->getLang() === $lang || array_key_exists($row->object->getUri(), $ret) === false) {
1733
                if (!isset($row->label) || $row->label->getLang() === $lang) {
1734
                    $ret[$row->object->getUri()] = $val;
1735
                } elseif ($row->label->getLang() === $fallbacklang) {
1736
                    $val['label'] .= ' (' . $row->label->getLang() . ')';
1737
                    $ret[$row->object->getUri()] = $val;
1738
                }
1739
            }
1740
        }
1741
1742
        // second iteration of results to find labels for the ones that didn't have one in the preferred languages
1743
        foreach ($result as $row) {
1744
            if (isset($row->object) && array_key_exists($row->object->getUri(), $ret) === false) {
1745
                $val = array('label' => $row->label->getValue());
1746
                if (isset($row->direct) && $row->direct->getValue() != '') {
1747
                    $val['direct'] = explode(' ', $row->direct->getValue());
1748
                }
1749
                $ret[$row->object->getUri()] = $val;
1750
            }
1751
        }
1752
1753
        if (sizeof($ret) > 0) {
1754
            return $ret;
1755
        }
1756
        // existing concept, with properties
1757
        else {
1758
            return null;
1759
        }
1760
        // nonexistent concept
1761
    }
1762
1763
    /**
1764
     * Query a single transitive property of a concept.
1765
     * @param string $uri
1766
     * @param array $props the property/properties.
1767
     * @param string $lang
1768
     * @param string $fallbacklang language to use if label is not available in the preferred language
1769
     * @param integer $limit
1770
     * @param boolean $anylang if you want a label even when it isn't available in the language you requested.
1771
     * @return array array of property values (key: URI, val: label), or null if concept doesn't exist
1772
     */
1773
    public function queryTransitiveProperty($uri, $props, $lang, $limit, $anylang = false, $fallbacklang = '') {
1774
        $query = $this->generateTransitivePropertyQuery($uri, $props, $lang, $limit, $anylang);
1775
        $result = $this->query($query);
1776
        return $this->transformTransitivePropertyResults($result, $lang, $fallbacklang);
1777
    }
1778
1779
    /**
1780
     * Generates the query for a concepts skos:narrowers.
1781
     * @param string $uri
1782
     * @param string $lang
1783
     * @param string $fallback
1784
     * @return string sparql query
1785
     */
1786
    private function generateChildQuery($uri, $lang, $fallback, $props) {
1787
        $uri = is_array($uri) ? $uri[0] : $uri;
1788
        $fcl = $this->generateFromClause();
1789
        $propertyClause = implode('|', $props);
1790
        $query = <<<EOQ
1791
SELECT ?child ?label ?child ?grandchildren ?notation $fcl WHERE {
1792
  <$uri> a skos:Concept .
1793
  OPTIONAL {
1794
    ?child $propertyClause <$uri> .
1795
    OPTIONAL {
1796
      ?child skos:prefLabel ?label .
1797
      FILTER (langMatches(lang(?label), "$lang"))
1798
    }
1799
    OPTIONAL {
1800
      ?child skos:prefLabel ?label .
1801
      FILTER (langMatches(lang(?label), "$fallback"))
1802
    }
1803
    OPTIONAL { # other language case
1804
      ?child skos:prefLabel ?label .
1805
    }
1806
    OPTIONAL {
1807
      ?child skos:notation ?notation .
1808
    }
1809
    BIND ( EXISTS { ?a $propertyClause ?child . } AS ?grandchildren )
1810
  }
1811
}
1812
EOQ;
1813
        return $query;
1814
    }
1815
1816
    /**
1817
     * Transforms the sparql result object into an array.
1818
     * @param EasyRdf\Sparql\Result $result
1819
     * @param string $lang
1820
     * @return array array of arrays describing each child concept, or null if concept doesn't exist
1821
     */
1822
    private function transformNarrowerResults($result, $lang) {
1823
        $ret = array();
1824
        foreach ($result as $row) {
1825
            if (!isset($row->child)) {
1826
                return array();
1827
            }
1828
            // existing concept but no children
1829
1830
            $label = null;
1831
            if (isset($row->label)) {
1832
                if ($row->label->getLang() == $lang || strpos($row->label->getLang(), $lang . "-") == 0) {
1833
                    $label = $row->label->getValue();
1834
                } else {
1835
                    $label = $row->label->getValue() . " (" . $row->label->getLang() . ")";
1836
                }
1837
1838
            }
1839
            $childArray = array(
1840
                'uri' => $row->child->getUri(),
1841
                'prefLabel' => $label,
1842
                'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
1843
            );
1844
            if (isset($row->notation)) {
1845
                $childArray['notation'] = $row->notation->getValue();
1846
            }
1847
1848
            $ret[] = $childArray;
1849
        }
1850
        if (sizeof($ret) > 0) {
1851
            return $ret;
1852
        }
1853
        // existing concept, with children
1854
        else {
1855
            return null;
1856
        }
1857
        // nonexistent concept
1858
    }
1859
1860
    /**
1861
     * Query the narrower concepts of a concept.
1862
     * @param string $uri
1863
     * @param string $lang
1864
     * @param string $fallback
1865
     * @return array array of arrays describing each child concept, or null if concept doesn't exist
1866
     */
1867
    public function queryChildren($uri, $lang, $fallback, $props) {
1868
        $query = $this->generateChildQuery($uri, $lang, $fallback, $props);
1869
        $result = $this->query($query);
1870
        return $this->transformNarrowerResults($result, $lang);
1871
    }
1872
1873
    /**
1874
     * Query the top concepts of a vocabulary.
1875
     * @param string $conceptSchemes concept schemes whose top concepts to query for
1876
     * @param string $lang language of labels
1877
     * @param string $fallback language to use if label is not available in the preferred language
1878
     */
1879
    public function queryTopConcepts($conceptSchemes, $lang, $fallback) {
1880
        if (!is_array($conceptSchemes)) {
1881
            $conceptSchemes = array($conceptSchemes);
1882
        }
1883
1884
        $values = $this->formatValues('?topuri', $conceptSchemes, 'uri');
1885
1886
        $fcl = $this->generateFromClause();
1887
        $query = <<<EOQ
1888
SELECT DISTINCT ?top ?topuri ?label ?notation ?children $fcl WHERE {
1889
  ?top skos:topConceptOf ?topuri .
1890
  OPTIONAL {
1891
    ?top skos:prefLabel ?label .
1892
    FILTER (langMatches(lang(?label), "$lang"))
1893
  }
1894
  OPTIONAL {
1895
    ?top skos:prefLabel ?label .
1896
    FILTER (langMatches(lang(?label), "$fallback"))
1897
  }
1898
  OPTIONAL { # fallback - other language case
1899
    ?top skos:prefLabel ?label .
1900
  }
1901
  OPTIONAL { ?top skos:notation ?notation . }
1902
  BIND ( EXISTS { ?top skos:narrower ?a . } AS ?children )
1903
  $values
1904
}
1905
EOQ;
1906
        $result = $this->query($query);
1907
        $ret = array();
1908
        foreach ($result as $row) {
1909
            if (isset($row->top) && isset($row->label)) {
1910
                $label = $row->label->getValue();
1911
                if ($row->label->getLang() && $row->label->getLang() !== $lang && strpos($row->label->getLang(), $lang . "-") !== 0) {
1912
                    $label .= ' (' . $row->label->getLang() . ')';
1913
                }
1914
                $top = array('uri' => $row->top->getUri(), 'topConceptOf' => $row->topuri->getUri(), 'label' => $label, 'hasChildren' => filter_var($row->children->getValue(), FILTER_VALIDATE_BOOLEAN));
1915
                if (isset($row->notation)) {
1916
                    $top['notation'] = $row->notation->getValue();
1917
                }
1918
1919
                $ret[] = $top;
1920
            }
1921
        }
1922
1923
        return $ret;
1924
    }
1925
1926
    /**
1927
     * Generates a sparql query for finding the hierarchy for a concept.
1928
	 * A concept may be a top concept in multiple schemes, returned as a single whitespace-separated literal.
1929
     * @param string $uri concept uri.
1930
     * @param string $lang
1931
     * @param string $fallback language to use if label is not available in the preferred language
1932
     * @return string sparql query
1933
     */
1934
    private function generateParentListQuery($uri, $lang, $fallback, $props) {
1935
        $fcl = $this->generateFromClause();
1936
        $propertyClause = implode('|', $props);
1937
        $query = <<<EOQ
1938
SELECT ?broad ?parent ?children ?grandchildren
1939
(SAMPLE(?lab) as ?label) (SAMPLE(?childlab) as ?childlabel) (GROUP_CONCAT(?topcs; separator=" ") as ?tops) 
1940
(SAMPLE(?nota) as ?notation) (SAMPLE(?childnota) as ?childnotation) $fcl
1941
WHERE {
1942
  <$uri> a skos:Concept .
1943
  OPTIONAL {
1944
    <$uri> $propertyClause* ?broad .
1945
    OPTIONAL {
1946
      ?broad skos:prefLabel ?lab .
1947
      FILTER (langMatches(lang(?lab), "$lang"))
1948
    }
1949
    OPTIONAL {
1950
      ?broad skos:prefLabel ?lab .
1951
      FILTER (langMatches(lang(?lab), "$fallback"))
1952
    }
1953
    OPTIONAL { # fallback - other language case
1954
      ?broad skos:prefLabel ?lab .
1955
    }
1956
    OPTIONAL { ?broad skos:notation ?nota . }
1957
    OPTIONAL { ?broad $propertyClause ?parent . }
1958
    OPTIONAL { ?broad skos:narrower ?children .
1959
      OPTIONAL {
1960
        ?children skos:prefLabel ?childlab .
1961
        FILTER (langMatches(lang(?childlab), "$lang"))
1962
      }
1963
      OPTIONAL {
1964
        ?children skos:prefLabel ?childlab .
1965
        FILTER (langMatches(lang(?childlab), "$fallback"))
1966
      }
1967
      OPTIONAL { # fallback - other language case
1968
        ?children skos:prefLabel ?childlab .
1969
      }
1970
      OPTIONAL {
1971
        ?children skos:notation ?childnota .
1972
      }
1973
    }
1974
    BIND ( EXISTS { ?children skos:narrower ?a . } AS ?grandchildren )
1975
    OPTIONAL { ?broad skos:topConceptOf ?topcs . }
1976
  }
1977
}
1978
GROUP BY ?broad ?parent ?member ?children ?grandchildren
1979
EOQ;
1980
        return $query;
1981
    }
1982
1983
    /**
1984
     * Transforms the result into an array.
1985
     * @param EasyRdf\Sparql\Result
1986
     * @param string $lang
1987
     * @return array|null an array for the REST controller to encode.
1988
     */
1989
    private function transformParentListResults($result, $lang)
1990
    {
1991
        $ret = array();
1992
        foreach ($result as $row) {
1993
            if (!isset($row->broad)) {
1994
                // existing concept but no broaders
1995
                return array();
1996
            }
1997
            $uri = $row->broad->getUri();
1998
            if (!isset($ret[$uri])) {
1999
                $ret[$uri] = array('uri' => $uri);
2000
            }
2001
            if (isset($row->exact)) {
2002
                $ret[$uri]['exact'] = $row->exact->getUri();
2003
            }
2004
            if (isset($row->tops)) {
2005
               $topConceptsList=explode(" ", $row->tops->getValue());
2006
               // sort to guarantee an alphabetical ordering of the URI
2007
               sort($topConceptsList);
2008
               $ret[$uri]['tops'] = $topConceptsList;
2009
            }
2010
            if (isset($row->children)) {
2011
                if (!isset($ret[$uri]['narrower'])) {
2012
                    $ret[$uri]['narrower'] = array();
2013
                }
2014
2015
                $label = null;
2016
                if (isset($row->childlabel)) {
2017
                    $label = $row->childlabel->getValue();
2018
                    if ($row->childlabel->getLang() !== $lang && strpos($row->childlabel->getLang(), $lang . "-") !== 0) {
2019
                        $label .= " (" . $row->childlabel->getLang() . ")";
2020
                    }
2021
2022
                }
2023
2024
                $childArr = array(
2025
                    'uri' => $row->children->getUri(),
2026
                    'label' => $label,
2027
                    'hasChildren' => filter_var($row->grandchildren->getValue(), FILTER_VALIDATE_BOOLEAN),
2028
                );
2029
                if (isset($row->childnotation)) {
2030
                    $childArr['notation'] = $row->childnotation->getValue();
2031
                }
2032
2033
                if (!in_array($childArr, $ret[$uri]['narrower'])) {
2034
                    $ret[$uri]['narrower'][] = $childArr;
2035
                }
2036
2037
            }
2038
            if (isset($row->label)) {
2039
                $preflabel = $row->label->getValue();
2040
                if ($row->label->getLang() && $row->label->getLang() !== $lang && strpos($row->label->getLang(), $lang . "-") !== 0) {
2041
                    $preflabel .= ' (' . $row->label->getLang() . ')';
2042
                }
2043
2044
                $ret[$uri]['prefLabel'] = $preflabel;
2045
            }
2046
            if (isset($row->notation)) {
2047
                $ret[$uri]['notation'] = $row->notation->getValue();
2048
            }
2049
2050
            if (isset($row->parent) && (isset($ret[$uri]['broader']) && !in_array($row->parent->getUri(), $ret[$uri]['broader']))) {
2051
                $ret[$uri]['broader'][] = $row->parent->getUri();
2052
            } elseif (isset($row->parent) && !isset($ret[$uri]['broader'])) {
2053
                $ret[$uri]['broader'][] = $row->parent->getUri();
2054
            }
2055
        }
2056
        if (sizeof($ret) > 0) {
2057
            // existing concept, with children
2058
            return $ret;
2059
        }
2060
        else {
2061
            // nonexistent concept
2062
            return null;
2063
        }
2064
    }
2065
2066
    /**
2067
     * Query for finding the hierarchy for a concept.
2068
     * @param string $uri concept uri.
2069
     * @param string $lang
2070
     * @param string $fallback language to use if label is not available in the preferred language
2071
     * @param array $props the hierarchy property/properties to use
2072
     * @return an array for the REST controller to encode.
2073
     */
2074
    public function queryParentList($uri, $lang, $fallback, $props) {
2075
        $query = $this->generateParentListQuery($uri, $lang, $fallback, $props);
2076
        $result = $this->query($query);
2077
        return $this->transformParentListResults($result, $lang);
2078
    }
2079
2080
    /**
2081
     * return a list of concept group instances, sorted by label
2082
     * @param string $groupClass URI of concept group class
2083
     * @param string $lang language of labels to return
2084
     * @return string sparql query
2085
     */
2086
    private function generateConceptGroupsQuery($groupClass, $lang) {
2087
        $fcl = $this->generateFromClause();
2088
        $query = <<<EOQ
2089
SELECT ?group (GROUP_CONCAT(DISTINCT STR(?child);separator=' ') as ?children) ?label ?members ?notation $fcl
2090
WHERE {
2091
  ?group a <$groupClass> .
2092
  OPTIONAL { ?group skos:member|isothes:subGroup ?child .
2093
             ?child a <$groupClass> }
2094
  BIND(EXISTS{?group skos:member ?submembers} as ?members)
2095
  OPTIONAL { ?group skos:prefLabel ?label }
2096
  OPTIONAL { ?group rdfs:label ?label }
2097
  FILTER (langMatches(lang(?label), '$lang'))
2098
  OPTIONAL { ?group skos:notation ?notation }
2099
}
2100
GROUP BY ?group ?label ?members ?notation
2101
ORDER BY lcase(?label)
2102
EOQ;
2103
        return $query;
2104
    }
2105
2106
    /**
2107
     * Transforms the sparql query result into an array.
2108
     * @param EasyRdf\Sparql\Result $result
2109
     * @return array
2110
     */
2111
    private function transformConceptGroupsResults($result) {
2112
        $ret = array();
2113
        foreach ($result as $row) {
2114
            if (!isset($row->group)) {
2115
                # no groups found, see issue #357
2116
                continue;
2117
            }
2118
            $group = array('uri' => $row->group->getURI());
2119
            if (isset($row->label)) {
2120
                $group['prefLabel'] = $row->label->getValue();
2121
            }
2122
2123
            if (isset($row->children)) {
2124
                $group['childGroups'] = explode(' ', $row->children->getValue());
2125
            }
2126
2127
            if (isset($row->members)) {
2128
                $group['hasMembers'] = $row->members->getValue();
2129
            }
2130
2131
            if (isset($row->notation)) {
2132
                $group['notation'] = $row->notation->getValue();
2133
            }
2134
2135
            $ret[] = $group;
2136
        }
2137
        return $ret;
2138
    }
2139
2140
    /**
2141
     * return a list of concept group instances, sorted by label
2142
     * @param string $groupClass URI of concept group class
2143
     * @param string $lang language of labels to return
2144
     * @return array Result array with group URI as key and group label as value
2145
     */
2146
    public function listConceptGroups($groupClass, $lang) {
2147
        $query = $this->generateConceptGroupsQuery($groupClass, $lang);
2148
        $result = $this->query($query);
2149
        return $this->transformConceptGroupsResults($result);
2150
    }
2151
2152
    /**
2153
     * Generates the sparql query for listConceptGroupContents
2154
     * @param string $groupClass URI of concept group class
2155
     * @param string $group URI of the concept group instance
2156
     * @param string $lang language of labels to return
2157
     * @param boolean $showDeprecated whether to include deprecated in the result
2158
     * @return string sparql query
2159
     */
2160
    private function generateConceptGroupContentsQuery($groupClass, $group, $lang, $showDeprecated = false) {
2161
        $fcl = $this->generateFromClause();
2162
        $filterDeprecated="";
2163
        if(!$showDeprecated){
2164
            $filterDeprecated="  FILTER NOT EXISTS { ?conc owl:deprecated true }";
2165
        }
2166
        $query = <<<EOQ
2167
SELECT ?conc ?super ?label ?members ?type ?notation $fcl
2168
WHERE {
2169
 <$group> a <$groupClass> .
2170
 { <$group> skos:member ?conc . } UNION { ?conc isothes:superGroup <$group> }
2171
$filterDeprecated
2172
 ?conc a ?type .
2173
 OPTIONAL { ?conc skos:prefLabel ?label .
2174
  FILTER (langMatches(lang(?label), '$lang'))
2175
 }
2176
 OPTIONAL { ?conc skos:prefLabel ?label . }
2177
 OPTIONAL { ?conc skos:notation ?notation }
2178
 BIND(EXISTS{?submembers isothes:superGroup ?conc} as ?super)
2179
 BIND(EXISTS{?conc skos:member ?submembers} as ?members)
2180
} ORDER BY lcase(?label)
2181
EOQ;
2182
        return $query;
2183
    }
2184
2185
    /**
2186
     * Transforms the sparql query result into an array.
2187
     * @param EasyRdf\Sparql\Result $result
2188
     * @param string $lang language of labels to return
2189
     * @return array
2190
     */
2191
    private function transformConceptGroupContentsResults($result, $lang) {
2192
        $ret = array();
2193
        $values = array();
2194
        foreach ($result as $row) {
2195
            if (!array_key_exists($row->conc->getURI(), $values)) {
2196
                $values[$row->conc->getURI()] = array(
2197
                    'uri' => $row->conc->getURI(),
2198
                    'isSuper' => $row->super->getValue(),
2199
                    'hasMembers' => $row->members->getValue(),
2200
                    'type' => array($row->type->shorten()),
2201
                );
2202
                if (isset($row->label)) {
2203
                    if ($row->label->getLang() == $lang || strpos($row->label->getLang(), $lang . "-") == 0) {
2204
                        $values[$row->conc->getURI()]['prefLabel'] = $row->label->getValue();
2205
                    } else {
2206
                        $values[$row->conc->getURI()]['prefLabel'] = $row->label->getValue() . " (" . $row->label->getLang() . ")";
2207
                    }
2208
2209
                }
2210
                if (isset($row->notation)) {
2211
                    $values[$row->conc->getURI()]['notation'] = $row->notation->getValue();
2212
                }
2213
2214
            } else {
2215
                $values[$row->conc->getURI()]['type'][] = $row->type->shorten();
2216
            }
2217
        }
2218
2219
        foreach ($values as $val) {
2220
            $ret[] = $val;
2221
        }
2222
2223
        return $ret;
2224
    }
2225
2226
    /**
2227
     * return a list of concepts in a concept group
2228
     * @param string $groupClass URI of concept group class
2229
     * @param string $group URI of the concept group instance
2230
     * @param string $lang language of labels to return
2231
     * @param boolean $showDeprecated whether to include deprecated concepts in search results
2232
     * @return array Result array with concept URI as key and concept label as value
2233
     */
2234
    public function listConceptGroupContents($groupClass, $group, $lang,$showDeprecated = false) {
2235
        $query = $this->generateConceptGroupContentsQuery($groupClass, $group, $lang,$showDeprecated);
2236
        $result = $this->query($query);
2237
        return $this->transformConceptGroupContentsResults($result, $lang);
2238
    }
2239
2240
    /**
2241
     * Generates the sparql query for queryChangeList.
2242
     * @param string $prop the property uri pointing to timestamps, eg. 'dc:modified'
2243
     * @param string $lang language of labels to return
2244
     * @param int $offset offset of results to retrieve; 0 for beginning of list
2245
     * @param int $limit maximum number of results to return
2246
     * @param boolean $showDeprecated whether to include deprecated concepts in the change list
2247
     * @return string sparql query
2248
     */
2249
    private function generateChangeListQuery($prop, $lang, $offset, $limit=200, $showDeprecated=false) {
2250
        $fcl = $this->generateFromClause();
2251
        $offset = ($offset) ? 'OFFSET ' . $offset : '';
2252
2253
        //Additional clauses when deprecated concepts need to be included in the results
2254
        $deprecatedOptions = '';
2255
        $deprecatedVars = '';
2256
        if ($showDeprecated) {
2257
            $deprecatedVars = '?replacedBy ?deprecated ?replacingLabel';
2258
            $deprecatedOptions =
2259
            'UNION {'.
2260
                '?concept dc:isReplacedBy ?replacedBy ; dc:modified ?date2 .'.
2261
                'BIND(COALESCE(?date2, ?date) AS ?date)'.
2262
                'OPTIONAL { ?replacedBy skos:prefLabel ?replacingLabel .'.
2263
                    'FILTER (langMatches(lang(?replacingLabel), \''.$lang.'\')) }}'.
2264
                'OPTIONAL { ?concept owl:deprecated ?deprecated . }';
2265
        }
2266
2267
        $query = <<<EOQ
2268
SELECT ?concept ?date ?label $deprecatedVars $fcl
2269
WHERE {
2270
    ?concept a skos:Concept ;
2271
    skos:prefLabel ?label .
2272
    FILTER (langMatches(lang(?label), '$lang'))
2273
    {
2274
        ?concept $prop ?date .
2275
        MINUS { ?concept owl:deprecated True . }
2276
    }
2277
    $deprecatedOptions
2278
}
2279
ORDER BY DESC(YEAR(?date)) DESC(MONTH(?date)) LCASE(?label) DESC(?concept)
2280
LIMIT $limit $offset
2281
EOQ;
2282
2283
        return $query;
2284
    }
2285
2286
    /**
2287
     * Transforms the sparql query result into an array.
2288
     * @param EasyRdf\Sparql\Result $result
2289
     * @return array
2290
     */
2291
    private function transformChangeListResults($result) {
2292
        $ret = array();
2293
        foreach ($result as $row) {
2294
            $concept = array('uri' => $row->concept->getURI());
2295
            if (isset($row->label)) {
2296
                $concept['prefLabel'] = $row->label->getValue();
2297
            }
2298
2299
            if (isset($row->date)) {
2300
                try {
2301
                    $concept['date'] = $row->date->getValue();
2302
                } catch (Exception $e) {
2303
                    //don't record concepts with malformed dates e.g. 1986-21-00
2304
                    continue;
2305
                }
2306
            }
2307
2308
            if (isset($row->replacedBy)) {
2309
                $concept['replacedBy'] = $row->replacedBy->getURI();
2310
            }
2311
            if (isset($row->replacingLabel)) {
2312
                $concept['replacingLabel'] = $row->replacingLabel->getValue();
2313
            }
2314
2315
            $ret[] = $concept;
2316
        }
2317
        return $ret;
2318
    }
2319
2320
    /**
2321
     * return a list of recently changed or entirely new concepts
2322
     * @param string $prop the property uri pointing to timestamps, eg. 'dc:modified'
2323
     * @param string $lang language of labels to return
2324
     * @param int $offset offset of results to retrieve; 0 for beginning of list
2325
     * @param int $limit maximum number of results to return
2326
     * @param boolean $showDeprecated whether to include deprecated concepts in the change list
2327
     * @return array Result array
2328
     */
2329
    public function queryChangeList($prop, $lang, $offset, $limit, $showDeprecated=false) {
2330
        $query = $this->generateChangeListQuery($prop, $lang, $offset, $limit, $showDeprecated);
2331
2332
        $result = $this->query($query);
2333
        return $this->transformChangeListResults($result);
2334
    }
2335
}
2336