Completed
Push — master ( 751599...c13dee )
by Osma
02:10 queued 10s
created

Model::fetchResourceFromUri()   A

Complexity

Conditions 2
Paths 6

Size

Total Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 6
nop 1
dl 0
loc 14
rs 9.7998
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 ther 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 View Code Duplication
        if (sizeof($vocabs) === 1) { // search within vocabulary
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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 View Code Duplication
        if (sizeof($uniqueVocabs) == 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
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
        $label = $cats ? $cats[0]->label($lang) : null;
405
406
        return $label;
407
    }
408
409
    /**
410
     * Returns a single cached vocabulary.
411
     * @param string $vocid the vocabulary id eg. 'mesh'.
412
     * @return Vocabulary dataobject
413
     */
414
    public function getVocabulary($vocid): Vocabulary
415
    {
416
        $vocs = $this->getVocabularies();
417
        foreach ($vocs as $voc) {
418
            if ($voc->getId() == $vocid) {
419
                return $voc;
420
            }
421
        }
422
        throw new Exception("Vocabulary id '$vocid' not found in configuration.");
423
    }
424
425
    /**
426
     * Return the vocabulary that is stored in the given graph on the given endpoint.
427
     *
428
     * @param $graph string graph URI
429
     * @param $endpoint string endpoint URL (default SPARQL endpoint if omitted)
430
     * @return Vocabulary vocabulary of this URI, or null if not found
431
     */
432
    public function getVocabularyByGraph($graph, $endpoint = null)
433
    {
434
        if ($endpoint === null) {
435
            $endpoint = $this->getConfig()->getDefaultEndpoint();
436
        }
437
        if ($this->vocabsByGraph === null) { // initialize cache
438
            $this->vocabsByGraph = array();
439
            foreach ($this->getVocabularies() as $voc) {
440
                $key = json_encode(array($voc->getGraph(), $voc->getEndpoint()));
441
                $this->vocabsByGraph[$key] = $voc;
442
            }
443
        }
444
445
        $key = json_encode(array($graph, $endpoint));
446
        if (array_key_exists($key, $this->vocabsByGraph)) {
447
            return $this->vocabsByGraph[$key];
448
        } else {
449
            throw new Exception("no vocabulary found for graph $graph and endpoint $endpoint");
450
        }
451
452
    }
453
454
    /**
455
     * When multiple vocabularies share same URI namespace, return the
456
     * vocabulary in which the URI is actually defined (has a label).
457
     *
458
     * @param Vocabulary[] $vocabs vocabularies to search
459
     * @param string $uri URI to look for
460
     * @param $preferredVocabId string ID of the preferred vocabulary to return if more than one is found
461
     * @return Vocabulary the vocabulary with the URI
462
     */
463
464
    private function disambiguateVocabulary($vocabs, $uri, $preferredVocabId = null)
465
    {
466
        // if there is only one candidate vocabulary, return it
467
        if (sizeof($vocabs) == 1) {
468
            return $vocabs[0];
469
        }
470
471
        // if there are multiple vocabularies and one is the preferred vocabulary, return it
472
        if($preferredVocabId != null) {
473
            foreach ($vocabs as $vocab) {
474
                if($vocab->getId() == $preferredVocabId) {
475
                    // double check that a label exists in the preferred vocabulary
476
                    if ($vocab->getConceptLabel($uri, null) !== null) {
477
                        return $vocab;
478
                    } else {
479
                        // not found in preferred vocabulary, fall back to next method
480
                        break;
481
                    }
482
                }
483
            }
484
        }
485
486
        // no preferred vocabulary, or it was not found, search in which vocabulary the concept has a label
487
        foreach ($vocabs as $vocab) {
488
            if ($vocab->getConceptLabel($uri, null) !== null)
489
                return $vocab;
490
        }
491
492
        // if the URI couldn't be found, fall back to the first vocabulary
493
        return $vocabs[0];
494
    }
495
496
    /**
497
     * Guess which vocabulary a URI originates from, based on the declared
498
     * vocabulary URI spaces.
499
     *
500
     * @param $uri string URI to search
501
     * @param $preferredVocabId string ID of the preferred vocabulary to return if more than one is found
502
     * @return Vocabulary vocabulary of this URI, or null if not found
503
     */
504
    public function guessVocabularyFromURI($uri, $preferredVocabId = null)
505
    {
506
        if ($this->vocabsByUriSpace === null) { // initialize cache
507
            $this->vocabsByUriSpace = array();
508
            foreach ($this->getVocabularies() as $voc) {
509
                $this->vocabsByUriSpace[$voc->getUriSpace()][] = $voc;
510
            }
511
        }
512
513
        // try to guess the URI space and look it up in the cache
514
        $res = new EasyRdf\Resource($uri);
515
        $namespace = substr($uri, 0, -strlen($res->localName()));
516
        if (array_key_exists($namespace, $this->vocabsByUriSpace)) {
517
            $vocabs = $this->vocabsByUriSpace[$namespace];
518
            return $this->disambiguateVocabulary($vocabs, $uri, $preferredVocabId);
519
        }
520
521
        // didn't work, try to match with each URI space separately
522
        foreach ($this->vocabsByUriSpace as $urispace => $vocabs) {
523
            if (strpos($uri, $urispace) === 0) {
524
                return $this->disambiguateVocabulary($vocabs, $uri, $preferredVocabId);
525
            }
526
        }
527
528
        // not found
529
        return null;
530
    }
531
532
    /**
533
     * Get the label for a resource, preferring 1. the given language 2. configured languages 3. any language.
534
     * @param EasyRdf\Resource $res resource whose label to return
535
     * @param string $lang preferred language
536
     * @return EasyRdf\Literal label as an EasyRdf\Literal object, or null if not found
537
     */
538
    public function getResourceLabel($res, $lang)
539
    {
540
        $langs = array_merge(array($lang), array_keys($this->getConfig()->getLanguages()));
541
        foreach ($langs as $l) {
542
            $label = $res->label($l);
543
            if ($label !== null) {
544
                return $label;
545
            }
546
547
        }
548
        return $res->label(); // desperate check for label in any language; will return null if even this fails
549
    }
550
551
    public function getResourceFromUri($uri)
552
    {
553
        // using apc cache for the resource if available
554
        if ($this->globalConfig->getCache()->isAvailable()) {
555
            // @codeCoverageIgnoreStart
556
            $key = 'fetch: ' . $uri;
557
            $resource = $this->globalConfig->getCache()->fetch($key);
558
            if ($resource === null || $resource === false) { // was not found in cache, or previous request failed
559
                $resource = $this->resolver->resolve($uri, $this->getConfig()->getHttpTimeout());
560
                $this->globalConfig->getCache()->store($key, $resource, self::URI_FETCH_TTL);
561
            }
562
            // @codeCoverageIgnoreEnd
563
        } else { // APC not available, parse on every request
564
            $resource = $this->resolver->resolve($uri, $this->getConfig()->getHttpTimeout());
565
        }
566
        return $resource;
567
    }
568
569
    /**
570
     * Returns a SPARQL endpoint object.
571
     * @param string $dialect eg. 'JenaText'.
572
     * @param string $endpoint url address of endpoint
573
     * @param string $graph uri for the target graph.
574
     */
575
    public function getSparqlImplementation($dialect, $endpoint, $graph)
576
    {
577
        $classname = $dialect . "Sparql";
578
579
        return new $classname($endpoint, $graph, $this);
580
    }
581
582
    /**
583
     * Returns a SPARQL endpoint object using the default implementation set in the config.ttl.
584
     */
585
    public function getDefaultSparql()
586
    {
587
        return $this->getSparqlImplementation($this->getConfig()->getDefaultSparqlDialect(), $this->getConfig()->getDefaultEndpoint(), '?graph');
588
    }
589
590
}
591