Passed
Pull Request — master (#1165)
by
unknown
03:04
created

GenericSparql::transformParentListResults()   F

Complexity

Conditions 24
Paths 7203

Size

Total Lines 74
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 24
eloc 44
nc 7203
nop 2
dl 0
loc 74
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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