Completed
Pull Request — master (#1033)
by
unknown
01:46
created

Model::getVocabulary()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 10
rs 9.9332
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Model provides access to the data.
5
 * @property EasyRdf\Graph $graph
6
 * @property GlobalConfig $globalConfig
7
 */
8
class Model
9
{
10
    /** cache for Vocabulary objects */
11
    private $allVocabularies = null;
12
    /** cache for Vocabulary objects */
13
    private $vocabsByGraph = null;
14
    /** cache for Vocabulary objects */
15
    private $vocabsByUriSpace = null;
16
    /** how long to store retrieved URI information in APC cache */
17
    const URI_FETCH_TTL = 86400; // 1 day
18
    private $globalConfig;
19
    private $logger;
20
    private $resolver;
21
22
    /**
23
     * Initializes the Model object
24
     */
25
    public function __construct($config)
26
    {
27
        $this->globalConfig = $config;
28
        $this->initializeLogging();
29
        $this->resolver = new Resolver($this);
30
    }
31
32
    /**
33
     * Returns the GlobalConfig object given to the Model as a constructor parameter.
34
     * @return GlobalConfig
35
     */
36
    public function getConfig() {
37
      return $this->globalConfig;
38
    }
39
40
    /**
41
     * Configures the logging facility
42
     */
43
    private function initializeLogging() {
44
        $this->logger = new \Monolog\Logger('general');
45
        $formatter = new \Monolog\Formatter\LineFormatter("[%datetime%] %level_name% %message%\n");
46
        $formatter->allowInlineLineBreaks(true);
47
        if ($this->getConfig()->getLoggingBrowserConsole()) {
48
            $browserHandler = new \Monolog\Handler\BrowserConsoleHandler(\Monolog\Logger::INFO);
49
            $browserHandler->setFormatter($formatter);
50
            $this->logger->pushHandler($browserHandler);
51
        }
52
        if ($this->getConfig()->getLoggingFilename() !== null) {
53
            $streamHandler = new \Monolog\Handler\StreamHandler($this->getConfig()->getLoggingFilename(), \Monolog\Logger::INFO);
54
            $streamHandler->setFormatter($formatter);
55
            $this->logger->pushHandler($streamHandler);
56
        }
57
        if (!$this->logger->getHandlers()) {
58
            // add a NullHandler to suppress the default Monolog logging to stderr
59
            $nullHandler = new \Monolog\Handler\NullHandler();
60
            $this->logger->pushHandler($nullHandler);
61
        }
62
    }
63
64
    /**
65
     * Return the logging facility
66
     * @return object logger
67
     */
68
    public function getLogger() {
69
        return $this->logger;
70
    }
71
72
    /**
73
     * Return the version of this Skosmos installation, or "unknown" if
74
     * it cannot be determined. The version information is based on Git tags.
75
     * @return string version
76
     */
77
    public function getVersion()
78
    {
79
        $ver = null;
80
        if (file_exists('.git')) {
81
            $ver = shell_exec('git describe --tags --always');
82
        }
83
84
        if ($ver === null) {
85
            return "unknown";
86
        }
87
88
        return $ver;
89
    }
90
91
    /**
92
     * Return all the vocabularies available.
93
     * @param boolean $categories whether you want everything included in a subarray of
94
     * a category.
95
     * @param boolean $shortname set to true if you want the vocabularies sorted by
96
     * their shortnames instead of their titles.
97
     */
98
    public function getVocabularyList($categories = true, $shortname = false)
99
    {
100
        $cats = $this->getVocabularyCategories();
101
        $ret = array();
102
        foreach ($cats as $cat) {
103
            $catlabel = $cat->getTitle();
104
105
            // find all the vocabs in this category
106
            $vocs = array();
107
            foreach ($cat->getVocabularies() as $voc) {
108
                $vocs[$shortname ? $voc->getConfig()->getShortname() : $voc->getConfig()->getTitle()] = $voc;
109
            }
110
            uksort($vocs, 'strcoll');
111
112
            if (sizeof($vocs) > 0 && $categories) {
113
                $ret[$catlabel] = $vocs;
114
            } elseif (sizeof($vocs) > 0) {
115
                $ret = array_merge($ret, $vocs);
116
            }
117
118
        }
119
120
        if (!$categories) {
121
            uksort($ret, 'strcoll');
122
        }
123
124
        return $ret;
125
    }
126
127
    /**
128
     * Return all types (RDFS/OWL classes) present in the specified vocabulary or all vocabularies.
129
     * @return array Array with URIs (string) as key and array of (label, superclassURI) as value
130
     */
131
    public function getTypes($vocid = null, $lang = null)
132
    {
133
        $sparql = (isset($vocid)) ? $this->getVocabulary($vocid)->getSparql() : $this->getDefaultSparql();
134
        $result = $sparql->queryTypes($lang);
135
136
        foreach ($result as $uri => $values) {
137
            if (empty($values)) {
138
                $shorteneduri = EasyRdf\RdfNamespace::shorten($uri);
139
                if ($shorteneduri !== null) {
140
                    $trans = gettext($shorteneduri);
141
                    if ($trans) {
142
                        $result[$uri] = array('label' => $trans);
143
                    }
144
                }
145
            }
146
        }
147
148
        return $result;
149
    }
150
151
    /**
152
     * Return the languages present in the configured vocabularies.
153
     * @return array Array with lang codes (string)
154
     */
155
    public function getLanguages($lang)
156
    {
157
        $vocabs = $this->getVocabularyList(false);
158
        $ret = array();
159
        foreach ($vocabs as $vocab) {
160
            foreach ($vocab->getConfig()->getLanguages() as $langcode) {
161
                $langlit = Punic\Language::getName($langcode, $lang);
162
                $ret[$langlit] = $langcode;
163
            }
164
        }
165
        ksort($ret);
166
        return array_unique($ret);
167
    }
168
169
    /**
170
     * returns a concept's RDF data in downloadable format
171
     * @param string $vocid vocabulary id, or null for global data (from all vocabularies)
172
     * @param string $uri concept URI
173
     * @param string $format the format in which you want to get the result, currently this function supports
174
     * text/turtle, application/rdf+xml and application/json
175
     * @return string RDF data in the requested serialization
176
     */
177
    public function getRDF($vocid, $uri, $format)
178
    {
179
180
        if ($format == 'text/turtle') {
181
            $retform = 'turtle';
182
            $serialiser = new EasyRdf\Serialiser\Turtle();
183
        } elseif ($format == 'application/ld+json' || $format == 'application/json') {
184
            $retform = 'jsonld'; // serve JSON-LD for both JSON-LD and plain JSON requests
185
            $serialiser = new EasyRdf\Serialiser\JsonLd();
186
        } else {
187
            $retform = 'rdfxml';
188
            $serialiser = new EasyRdf\Serialiser\RdfXml();
189
        }
190
191
        if ($vocid !== null) {
192
            $vocab = $this->getVocabulary($vocid);
193
            $sparql = $vocab->getSparql();
194
            $arrayClass = $vocab->getConfig()->getArrayClassURI();
195
            $vocabs = array($vocab);
196
        } else {
197
            $sparql = $this->getDefaultSparql();
198
            $arrayClass = null;
199
            $vocabs = null;
200
        }
201
        $result = $sparql->queryConceptInfoGraph($uri, $arrayClass, $vocabs);
202
203
        if (!$result->isEmpty()) {
204
            return $serialiser->serialise($result, $retform);
205
        }
206
        return "";
207
    }
208
209
    /**
210
     * Makes a SPARQL-query to the endpoint that retrieves concept
211
     * references as it's search results.
212
     * @param ConceptSearchParameters $params an object that contains all the parameters needed for the search
213
     * @return array search results
214
     */
215
    public function searchConcepts($params)
216
    {
217
        // don't even try to search for empty prefix if no other search criteria (group or parent concept) has been set
218
        if (($params->getSearchTerm() === "" || !preg_match('/[^*]/', $params->getSearchTerm())) && !$params->getGroupLimit() && !$params->getParentLimit()) {
219
            return array();
220
        }
221
222
        $vocabs = $params->getVocabs();
223
        $showDeprecated=false;
224
        if (sizeof($vocabs) === 1) { // search within vocabulary
225
            $voc = $vocabs[0];
226
            $sparql = $voc->getSparql();
227
            $showDeprecated=$voc->getConfig()->getShowDeprecated();
228
        } else { // multi-vocabulary or global search
229
            $voc = null;
230
            $sparql = $this->getDefaultSparql();
231
            // @TODO : in a global search showDeprecated will always be false and cannot be set globally
232
        }
233
234
        $results = $sparql->queryConcepts($vocabs, $params->getAdditionalFields(), $params->getUnique(), $params,$showDeprecated);
235
        if ($params->getRest() && $results && $params->getSearchLimit() !== 0) {
236
          $results = array_slice($results, $params->getOffset(), $params->getSearchLimit());
237
        }
238
        $ret = array();
239
240
        foreach ($results as $hit) {
241
            if (sizeof($vocabs) == 1) {
242
                $hitvoc = $voc;
243
                $hit['vocab'] = $vocabs[0]->getId();
244
            } else {
245
                try {
246
                    $hitvoc = $this->getVocabularyByGraph($hit['graph']);
247
                    $hit['vocab'] = $hitvoc->getId();
248
                } catch (Exception $e) {
249
                    trigger_error($e->getMessage(), E_USER_WARNING);
250
                    $hitvoc = null;
251
                    $hit['vocab'] = "???";
252
                }
253
            }
254
            unset($hit['graph']);
255
256
            $hit['voc'] = $hitvoc;
257
258
            if (!$hitvoc->containsURI($hit['uri'])) {
259
                // if uri is a external vocab uri that is included in the current vocab
260
                $realvoc = $this->guessVocabularyFromURI($hit['uri'], $voc !== null ? $voc->getId() : null);
261
                if ($realvoc !== $hitvoc) {
262
                    unset($hit['localname']);
263
                    $hit['exvocab'] = ($realvoc !== null) ? $realvoc->getId() : "???";
264
                }
265
            }
266
267
            $ret[] = $hit;
268
        }
269
270
        return $ret;
271
    }
272
273
    /**
274
     * Function for performing a search for concepts and their data fields.
275
     * @param ConceptSearchParameters $params an object that contains all the parameters needed for the search
276
     * @return array array with keys 'count' and 'results'
277
     */
278
    public function searchConceptsAndInfo($params)
279
    {
280
        $params->setUnique(true);
281
        $allhits = $this->searchConcepts($params);
282
        $count = sizeof($allhits);
283
        $hits = array_slice($allhits, $params->getOffset(), $params->getSearchLimit());
284
285
        $ret = array();
286
        $uris = array();
287
        $vocabs = array();
288
        $uniqueVocabs = array();
289
        foreach ($hits as $hit) {
290
            $uniqueVocabs[$hit['voc']->getId()] = $hit['voc']->getId();
291
            $vocabs[] = $hit['voc'];
292
            $uris[] = $hit['uri'];
293
        }
294
        if (sizeof($uniqueVocabs) == 1) {
295
            $voc = $vocabs[0];
296
            $sparql = $voc->getSparql();
297
            $arrayClass = $voc->getConfig()->getArrayClassURI();
298
        } else {
299
            $arrayClass = null;
300
            $sparql = $this->getDefaultSparql();
301
        }
302
        if (sizeof($uris) > 0) {
303
            $ret = $sparql->queryConceptInfo($uris, $arrayClass, $vocabs, $params->getSearchLang());
304
        }
305
306
        // For marking that the concept has been found through an alternative label, hidden
307
        // label or a label in another language
308
        foreach ($hits as $idx => $hit) {
309
            if (isset($hit['altLabel']) && isset($ret[$idx])) {
310
                $ret[$idx]->setFoundBy($hit['altLabel'], 'alt');
311
            }
312
313
            if (isset($hit['hiddenLabel']) && isset($ret[$idx])) {
314
                $ret[$idx]->setFoundBy($hit['hiddenLabel'], 'hidden');
315
            }
316
317
            if (isset($hit['matchedPrefLabel'])) {
318
                $ret[$idx]->setFoundBy($hit['matchedPrefLabel'], 'lang');
319
            }
320
321
            if ($ret[$idx] && isset($hit['lang'])) {
322
                $ret[$idx]->setContentLang($hit['lang']);
323
            }
324
        }
325
326
        return array('count' => $count, 'results' => $ret);
327
    }
328
329
    /**
330
     * Creates dataobjects from an input array.
331
     * @param string $class the type of class eg. 'Vocabulary'.
332
     * @param array $resarr contains the EasyRdf\Resources.
333
     */
334
    private function createDataObjects($class, $resarr)
335
    {
336
        $ret = array();
337
        foreach ($resarr as $res) {
338
            $ret[] = new $class($this, $res);
339
        }
340
341
        return $ret;
342
    }
343
344
    /**
345
     * Returns the cached vocabularies.
346
     * @return array of Vocabulary dataobjects
347
     */
348
    public function getVocabularies()
349
    {
350
        if ($this->allVocabularies === null) { // initialize cache
351
            $vocs = $this->globalConfig->getGraph()->allOfType('skosmos:Vocabulary');
352
            $this->allVocabularies = $this->createDataObjects("Vocabulary", $vocs);
353
            foreach ($this->allVocabularies as $voc) {
354
                // register vocabulary ids as RDF namespace prefixes
355
                $prefix = preg_replace('/\W+/', '', $voc->getId()); // strip non-word characters
356
                try {
357
                    if ($prefix != '' && EasyRdf\RdfNamespace::get($prefix) === null) // if not already defined
358
                    {
359
                        EasyRdf\RdfNamespace::set($prefix, $voc->getUriSpace());
360
                    }
361
362
                } catch (Exception $e) {
363
                    // not valid as namespace identifier, ignore
364
                }
365
            }
366
        }
367
368
        return $this->allVocabularies;
369
    }
370
371
    /**
372
     * Returns the cached vocabularies from a category.
373
     * @param EasyRdf\Resource $cat the category in question
374
     * @return array of vocabulary dataobjects
375
     */
376
    public function getVocabulariesInCategory($cat)
377
    {
378
        $vocs = $this->globalConfig->getGraph()->resourcesMatching('dc:subject', $cat);
379
        return $this->createDataObjects("Vocabulary", $vocs);
380
    }
381
382
    /**
383
     * Creates dataobjects of all the different vocabulary categories (Health etc.).
384
     * @return array of Dataobjects of the type VocabularyCategory.
385
     */
386
    public function getVocabularyCategories()
387
    {
388
        $cats = $this->globalConfig->getGraph()->allOfType('skos:Concept');
389
        if(empty($cats)) {
390
            return array(new VocabularyCategory($this, null));
391
        }
392
393
        return $this->createDataObjects("VocabularyCategory", $cats);
394
    }
395
396
    /**
397
     * Returns the label defined in config.ttl with the appropriate language.
398
     * @param string $lang language code of returned labels, eg. 'fi'
399
     * @return string the label for vocabulary categories.
400
     */
401
    public function getClassificationLabel($lang)
402
    {
403
        $cats = $this->globalConfig->getGraph()->allOfType('skos:ConceptScheme');
404
        return $cats ? $cats[0]->label($lang) : null;
405
    }
406
407
    /**
408
     * Returns a single cached vocabulary.
409
     * @param string $vocid the vocabulary id eg. 'mesh'.
410
     * @return Vocabulary dataobject
411
     */
412
    public function getVocabulary($vocid): Vocabulary
413
    {
414
        $vocs = $this->getVocabularies();
415
        foreach ($vocs as $voc) {
416
            if ($voc->getId() == $vocid) {
417
                return $voc;
418
            }
419
        }
420
        throw new Exception("Vocabulary id '$vocid' not found in configuration.");
421
    }
422
423
    /**
424
     * Return the vocabulary that is stored in the given graph on the given endpoint.
425
     *
426
     * @param $graph string graph URI
427
     * @param $endpoint string endpoint URL (default SPARQL endpoint if omitted)
428
     * @return Vocabulary vocabulary of this URI, or null if not found
429
     */
430
    public function getVocabularyByGraph($graph, $endpoint = null)
431
    {
432
        if ($endpoint === null) {
433
            $endpoint = $this->getConfig()->getDefaultEndpoint();
434
        }
435
        if ($this->vocabsByGraph === null) { // initialize cache
436
            $this->vocabsByGraph = array();
437
            foreach ($this->getVocabularies() as $voc) {
438
                $key = json_encode(array($voc->getGraph(), $voc->getEndpoint()));
439
                $this->vocabsByGraph[$key] = $voc;
440
            }
441
        }
442
443
        $key = json_encode(array($graph, $endpoint));
444
        if (array_key_exists($key, $this->vocabsByGraph)) {
445
            return $this->vocabsByGraph[$key];
446
        } else {
447
            throw new Exception("no vocabulary found for graph $graph and endpoint $endpoint");
448
        }
449
450
    }
451
452
    /**
453
     * When multiple vocabularies share same URI namespace, return the
454
     * vocabulary in which the URI is actually defined (has a label).
455
     *
456
     * @param Vocabulary[] $vocabs vocabularies to search
457
     * @param string $uri URI to look for
458
     * @param $preferredVocabId string ID of the preferred vocabulary to return if more than one is found
459
     * @return Vocabulary the vocabulary with the URI
460
     */
461
462
    private function disambiguateVocabulary($vocabs, $uri, $preferredVocabId = null)
463
    {
464
        // if there is only one candidate vocabulary, return it
465
        if (sizeof($vocabs) == 1) {
466
            return $vocabs[0];
467
        }
468
469
        // if there are multiple vocabularies and one is the preferred vocabulary, return it
470
        if($preferredVocabId != null) {
471
            foreach ($vocabs as $vocab) {
472
                if($vocab->getId() == $preferredVocabId) {
473
                    try {
474
                        // double check that a label exists in the preferred vocabulary
475
                        if ($vocab->getConceptLabel($uri, null) !== null) {
476
                            return $vocab;
477
                        } else {
478
                            // not found in preferred vocabulary, fall back to next method
479
                            break;
480
                        }
481
                    } catch (EasyRdf\Http\Exception | EasyRdf\Exception | Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
482
                        if ($this->getConfig()->getLogCaughtExceptions()) {
483
                            error_log('Caught exception: ' . $e->getMessage());
484
                        }
485
                        break;
486
                    }
487
                }
488
            }
489
        }
490
491
        // no preferred vocabulary, or it was not found, search in which vocabulary the concept has a label
492
        foreach ($vocabs as $vocab) {
493
            try {
494
                if ($vocab->getConceptLabel($uri, null) !== null) {
495
                    return $vocab;
496
                }
497
            } catch (EasyRdf\Http\Exception | EasyRdf\Exception | Throwable $e) {
0 ignored issues
show
Bug introduced by
The class Throwable does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
498
                if ($this->getConfig()->getLogCaughtExceptions()) {
499
                    error_log('Caught exception: ' . $e->getMessage());
500
                }
501
                break;
502
            }
503
        }
504
505
        // if the URI couldn't be found, fall back to the first vocabulary
506
        return $vocabs[0];
507
    }
508
509
    /**
510
     * Guess which vocabulary a URI originates from, based on the declared
511
     * vocabulary URI spaces.
512
     *
513
     * @param $uri string URI to search
514
     * @param $preferredVocabId string ID of the preferred vocabulary to return if more than one is found
515
     * @return Vocabulary vocabulary of this URI, or null if not found
516
     */
517
    public function guessVocabularyFromURI($uri, $preferredVocabId = null)
518
    {
519
        if ($this->vocabsByUriSpace === null) { // initialize cache
520
            $this->vocabsByUriSpace = array();
521
            foreach ($this->getVocabularies() as $voc) {
522
                $this->vocabsByUriSpace[$voc->getUriSpace()][] = $voc;
523
            }
524
        }
525
526
        // try to guess the URI space and look it up in the cache
527
        $res = new EasyRdf\Resource($uri);
528
        $namespace = substr($uri, 0, -strlen($res->localName()));
529
        if (array_key_exists($namespace, $this->vocabsByUriSpace)) {
530
            $vocabs = $this->vocabsByUriSpace[$namespace];
531
            return $this->disambiguateVocabulary($vocabs, $uri, $preferredVocabId);
532
        }
533
534
        // didn't work, try to match with each URI space separately
535
        foreach ($this->vocabsByUriSpace as $urispace => $vocabs) {
536
            if (strpos($uri, $urispace) === 0) {
537
                return $this->disambiguateVocabulary($vocabs, $uri, $preferredVocabId);
538
            }
539
        }
540
541
        // not found
542
        return null;
543
    }
544
545
    /**
546
     * Get the label for a resource, preferring 1. the given language 2. configured languages 3. any language.
547
     * @param EasyRdf\Resource $res resource whose label to return
548
     * @param string $lang preferred language
549
     * @return EasyRdf\Literal label as an EasyRdf\Literal object, or null if not found
550
     */
551
    public function getResourceLabel($res, $lang)
552
    {
553
        $langs = array_merge(array($lang), array_keys($this->getConfig()->getLanguages()));
554
        foreach ($langs as $l) {
555
            $label = $res->label($l);
556
            if ($label !== null) {
557
                return $label;
558
            }
559
560
        }
561
        return $res->label(); // desperate check for label in any language; will return null if even this fails
562
    }
563
564
    public function getResourceFromUri($uri)
565
    {
566
        // using apc cache for the resource if available
567
        if ($this->globalConfig->getCache()->isAvailable()) {
568
            // @codeCoverageIgnoreStart
569
            $key = 'fetch: ' . $uri;
570
            $resource = $this->globalConfig->getCache()->fetch($key);
571
            if ($resource === null || $resource === false) { // was not found in cache, or previous request failed
572
                $resource = $this->resolver->resolve($uri, $this->getConfig()->getHttpTimeout());
573
                $this->globalConfig->getCache()->store($key, $resource, self::URI_FETCH_TTL);
574
            }
575
            // @codeCoverageIgnoreEnd
576
        } else { // APC not available, parse on every request
577
            $resource = $this->resolver->resolve($uri, $this->getConfig()->getHttpTimeout());
578
        }
579
        return $resource;
580
    }
581
582
    /**
583
     * Returns a SPARQL endpoint object.
584
     * @param string $dialect eg. 'JenaText'.
585
     * @param string $endpoint url address of endpoint
586
     * @param string|null $graph uri for the target graph.
587
     */
588
    public function getSparqlImplementation($dialect, $endpoint, $graph)
589
    {
590
        $classname = $dialect . "Sparql";
591
592
        return new $classname($endpoint, $graph, $this);
593
    }
594
595
    /**
596
     * Returns a SPARQL endpoint object using the default implementation set in the config.ttl.
597
     */
598
    public function getDefaultSparql()
599
    {
600
        return $this->getSparqlImplementation($this->getConfig()->getDefaultSparqlDialect(), $this->getConfig()->getDefaultEndpoint(), '?graph');
601
    }
602
603
}
604