Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
132 / 132
100.00% covered (success)
100.00%
13 / 13
CRAP
100.00% covered (success)
100.00%
1 / 1
MathWikibaseConnector
100.00% covered (success)
100.00%
132 / 132
100.00% covered (success)
100.00%
13 / 13
45
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
1
 loadPropertyId
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getStatements
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 fetchWikibaseFromId
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
7
 fetchLabelDescription
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 fetchStatements
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 fetchSymbol
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 fetchHasPartSnaks
100.00% covered (success)
100.00%
23 / 23
100.00% covered (success)
100.00%
1 / 1
10
 fetchPageUrl
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 isSymbolSnak
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 isFormulaItemSnak
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 buildURL
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUrlFromSymbol
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3namespace MediaWiki\Extension\Math;
4
5use DataValues\StringValue;
6use Exception;
7use InvalidArgumentException;
8use MediaWiki\Config\ConfigException;
9use MediaWiki\Config\ServiceOptions;
10use MediaWiki\Languages\LanguageFactory;
11use MediaWiki\Languages\LanguageNameUtils;
12use MediaWiki\Site\Site;
13use Psr\Log\LoggerInterface;
14use Wikibase\Client\RepoLinker;
15use Wikibase\DataModel\Entity\EntityId;
16use Wikibase\DataModel\Entity\EntityIdParser;
17use Wikibase\DataModel\Entity\EntityIdParsingException;
18use Wikibase\DataModel\Entity\EntityIdValue;
19use Wikibase\DataModel\Entity\Item;
20use Wikibase\DataModel\Entity\ItemId;
21use Wikibase\DataModel\Entity\PropertyId;
22use Wikibase\DataModel\Services\Lookup\LabelDescriptionLookup;
23use Wikibase\DataModel\Snak\PropertyValueSnak;
24use Wikibase\DataModel\Snak\Snak;
25use Wikibase\DataModel\Statement\StatementList;
26use Wikibase\Lib\Store\EntityRevisionLookup;
27use Wikibase\Lib\Store\FallbackLabelDescriptionLookupFactory;
28use Wikibase\Lib\Store\RevisionedUnresolvedRedirectException;
29use Wikibase\Lib\Store\StorageException;
30
31/**
32 * A class that connects with the local instance of wikibase to fetch
33 * information from single items. There is always only one instance of this class.
34 *
35 * @see MathWikibaseConnector::getInstance()    to get an instance of the class
36 */
37class MathWikibaseConnector {
38    /** @var string[] */
39    public const CONSTRUCTOR_OPTIONS = [
40        'MathWikibasePropertyIdHasPart',
41        'MathWikibasePropertyIdDefiningFormula',
42        'MathWikibasePropertyIdInDefiningFormula',
43        'MathWikibasePropertyIdQuantitySymbol',
44        'MathWikibasePropertyIdSymbolRepresents'
45    ];
46
47    /** @var LoggerInterface */
48    private $logger;
49
50    /** @var RepoLinker */
51    private $repoLinker;
52
53    /** @var LanguageFactory */
54    private $languageFactory;
55
56    /** @var LanguageNameUtils */
57    private $languageNameUtils;
58
59    /** @var EntityRevisionLookup */
60    private $entityRevisionLookup;
61
62    /** @var Site */
63    private $site;
64
65    /** @var FallbackLabelDescriptionLookupFactory */
66    private $labelDescriptionLookupFactory;
67
68    /** @var MathFormatter */
69    private $mathFormatter;
70
71    /** @var EntityIdParser */
72    private $idParser;
73
74    /** @var PropertyId|null */
75    private $propertyIdHasPart;
76
77    /** @var PropertyId|null */
78    private $propertyIdDefiningFormula;
79
80    /** @var PropertyId|null */
81    private $propertyIdInDefiningFormula;
82
83    /** @var PropertyId|null */
84    private $propertyIdQuantitySymbol;
85
86    /** @var PropertyId|null */
87    private $propertyIdSymbolRepresents;
88
89    /**
90     * @param ServiceOptions $options
91     * @param RepoLinker $repoLinker
92     * @param LanguageFactory $languageFactory
93     * @param LanguageNameUtils $languageNameUtils
94     * @param EntityRevisionLookup $entityRevisionLookup
95     * @param FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory
96     * @param Site $site
97     * @param EntityIdParser $entityIdParser
98     * @param MathFormatter $mathFormatter
99     * @param LoggerInterface $logger
100     */
101    public function __construct(
102        ServiceOptions $options,
103        RepoLinker $repoLinker,
104        LanguageFactory $languageFactory,
105        LanguageNameUtils $languageNameUtils,
106        EntityRevisionLookup $entityRevisionLookup,
107        FallbackLabelDescriptionLookupFactory $labelDescriptionLookupFactory,
108        Site $site,
109        EntityIdParser $entityIdParser,
110        MathFormatter $mathFormatter,
111        LoggerInterface $logger
112    ) {
113        $options->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
114        $this->repoLinker = $repoLinker;
115        $this->languageFactory = $languageFactory;
116        $this->languageNameUtils = $languageNameUtils;
117        $this->entityRevisionLookup = $entityRevisionLookup;
118        $this->labelDescriptionLookupFactory = $labelDescriptionLookupFactory;
119        $this->site = $site;
120        $this->idParser = $entityIdParser;
121        $this->mathFormatter = $mathFormatter;
122        $this->logger = $logger;
123
124        $this->propertyIdHasPart = $this->loadPropertyId(
125            $options->get( "MathWikibasePropertyIdHasPart" )
126        );
127        $this->propertyIdDefiningFormula = $this->loadPropertyId(
128            $options->get( "MathWikibasePropertyIdDefiningFormula" )
129        );
130        $this->propertyIdInDefiningFormula = $this->loadPropertyId(
131            $options->get( "MathWikibasePropertyIdInDefiningFormula" )
132        );
133        $this->propertyIdQuantitySymbol = $this->loadPropertyId(
134            $options->get( "MathWikibasePropertyIdQuantitySymbol" )
135        );
136        $this->propertyIdSymbolRepresents = $this->loadPropertyId(
137            $options->get( "MathWikibasePropertyIdSymbolRepresents" )
138        );
139    }
140
141    /**
142     * Returns the given PropertyId if available.
143     * @param string $propertyId the string of the Wikibase property
144     * @return EntityId|null the property object or null if unavailable
145     */
146    private function loadPropertyId( string $propertyId ): ?EntityId {
147        try {
148            return $this->idParser->parse( $propertyId );
149        } catch ( ConfigException $e ) {
150            return null;
151        }
152    }
153
154    /**
155     * Returns the inner statements from given statements for a given property ID or an empty list if the given ID
156     * not exists.
157     * @param StatementList $statements
158     * @param PropertyId|null $id
159     * @return StatementList might be empty
160     */
161    private function getStatements( StatementList $statements, ?PropertyId $id ): StatementList {
162        if ( $id === null ) {
163            return new StatementList();
164        }
165        return $statements->getByPropertyId( $id );
166    }
167
168    /**
169     * @param string $qid
170     * @param string $langCode the language to fetch data
171     *          (may fallback if requested language does not exist)
172     *
173     * @return MathWikibaseInfo the object may be empty if no information can be fetched.
174     * @throws InvalidArgumentException if the language code does not exist or the given
175     * id does not exist
176     */
177    public function fetchWikibaseFromId( string $qid, string $langCode ): MathWikibaseInfo {
178        if ( $this->languageNameUtils->isValidCode( $langCode ) ) {
179            $lang = $this->languageFactory->getLanguage( $langCode );
180        } else {
181            throw new InvalidArgumentException( "Invalid language code specified." );
182        }
183
184        $langLookup = $this->labelDescriptionLookupFactory->newLabelDescriptionLookup( $lang );
185        try {
186            $entityId = $this->idParser->parse( $qid ); // exception if the given ID is invalid
187            $entityRevision = $this->entityRevisionLookup->getEntityRevision( $entityId );
188        } catch ( EntityIdParsingException $e ) {
189            throw new InvalidArgumentException( "Invalid Wikibase ID." );
190        } catch ( RevisionedUnresolvedRedirectException | StorageException $e ) {
191            throw new InvalidArgumentException( "Non-existing Wikibase ID." );
192        }
193
194        if ( !$entityId || !$entityRevision ) {
195            throw new InvalidArgumentException( "Non-existing Wikibase ID." );
196        }
197
198        $entity = $entityRevision->getEntity();
199        $output = new MathWikibaseInfo( $entityId, $this->mathFormatter );
200
201        if ( $entity instanceof Item ) {
202            $this->fetchLabelDescription( $output, $langLookup );
203            $this->fetchStatements( $output, $entity, $langLookup );
204            return $output;
205        } else { // we only allow Wikibase items
206            throw new InvalidArgumentException( "The specified Wikibase ID does not represented an item." );
207        }
208    }
209
210    /**
211     * Fetches only label and description from an entity.
212     * @param MathWikibaseInfo $output the entity id of the entity
213     * @param LabelDescriptionLookup $langLookup a lookup handler to fetch right languages
214     * @return MathWikibaseInfo filled up with label and description
215     */
216    private function fetchLabelDescription(
217        MathWikibaseInfo $output,
218        LabelDescriptionLookup $langLookup ) {
219        $label = $langLookup->getLabel( $output->getId() );
220        $desc = $langLookup->getDescription( $output->getId() );
221
222        if ( $label ) {
223            $output->setLabel( $label->getText() );
224        }
225
226        if ( $desc ) {
227            $output->setDescription( $desc->getText() );
228        }
229
230        return $output;
231    }
232
233    /**
234     * Fetches 'has part' statements from a given item element with a defined lookup object for
235     * the languages.
236     * @param MathWikibaseInfo $output the output element
237     * @param Item $item item to fetch statements from
238     * @param LabelDescriptionLookup $langLookup
239     * @return MathWikibaseInfo the updated $output object
240     */
241    private function fetchStatements(
242        MathWikibaseInfo $output,
243        Item $item,
244        LabelDescriptionLookup $langLookup ) {
245        $statements = $item->getStatements();
246        $formulaComponentStatements = $this->getStatements( $statements, $this->propertyIdHasPart );
247        if ( $formulaComponentStatements->isEmpty() ) {
248            $formulaComponentStatements = $this->getStatements( $statements, $this->propertyIdInDefiningFormula );
249        }
250        $this->fetchHasPartSnaks( $output, $formulaComponentStatements, $langLookup );
251
252        $symbolStatement = $this->getStatements( $statements, $this->propertyIdDefiningFormula );
253        if ( $symbolStatement->count() < 1 ) { // if it's not a formula, it might be a symbol
254            $symbolStatement = $this->getStatements( $statements, $this->propertyIdQuantitySymbol );
255        }
256        $this->fetchSymbol( $output, $symbolStatement );
257        return $output;
258    }
259
260    /**
261     * Fetches the symbol or defining formula from a statement list and adds the symbol to the
262     * given info object
263     * @param MathWikibaseInfo $output
264     * @param StatementList $statements
265     * @return MathWikibaseInfo updated object
266     */
267    private function fetchSymbol( MathWikibaseInfo $output, StatementList $statements ) {
268        foreach ( $statements as $statement ) {
269            $snak = $statement->getMainSnak();
270            if ( $snak instanceof PropertyValueSnak && $this->isSymbolSnak( $snak ) ) {
271                $dataVal = $snak->getDataValue();
272                $symbol = new StringValue( $dataVal->getValue() );
273                $output->setSymbol( $symbol );
274                return $output;
275            }
276        }
277
278        return $output;
279    }
280
281    /**
282     * Fetches single snaks from 'has part' statements
283     *
284     * @param MathWikibaseInfo $output
285     * @param StatementList $statements the 'has part' statements
286     * @param LabelDescriptionLookup $langLookup
287     * @return MathWikibaseInfo
288     * @todo refactor this method once Wikibase has a more convenient way to handle snaks
289     */
290    private function fetchHasPartSnaks(
291        MathWikibaseInfo $output,
292        StatementList $statements,
293        LabelDescriptionLookup $langLookup ) {
294        foreach ( $statements as $statement ) {
295            $snaks = $statement->getAllSnaks();
296            $innerInfo = null;
297            $symbol = null;
298
299            foreach ( $snaks as $snak ) {
300                if ( $snak instanceof PropertyValueSnak ) {
301                    if ( $this->isSymbolSnak( $snak ) ) {
302                        $dataVal = $snak->getDataValue();
303                        $symbol = new StringValue( $dataVal->getValue() );
304                    } elseif ( $this->isFormulaItemSnak( $snak ) ) {
305                        $dataVal = $snak->getDataValue();
306                        $entityIdValue = $dataVal->getValue();
307                        if ( $entityIdValue instanceof EntityIdValue ) {
308                            $innerEntityId = $entityIdValue->getEntityId();
309                            $innerInfo = new MathWikibaseInfo( $innerEntityId, $output->getFormatter() );
310                            $this->fetchLabelDescription( $innerInfo, $langLookup );
311                            $url = $this->fetchPageUrl( $innerEntityId );
312                            if ( $url ) {
313                                $innerInfo->setUrl( $url );
314                            }
315                        }
316                    }
317                }
318            }
319
320            if ( $innerInfo && $symbol ) {
321                $innerInfo->setSymbol( $symbol );
322                $output->addHasPartElement( $innerInfo );
323            }
324        }
325
326        return $output;
327    }
328
329    /**
330     * Fetch the page url for a given entity id.
331     * @param EntityId $entityId
332     * @return string|false
333     */
334    private function fetchPageUrl( EntityId $entityId ) {
335        try {
336            $entityRevision = $this->entityRevisionLookup->getEntityRevision( $entityId );
337            $innerEntity = $entityRevision->getEntity();
338            if ( $innerEntity instanceof Item ) {
339                $globalID = $this->site->getGlobalId();
340                if ( $innerEntity->hasLinkToSite( $globalID ) ) {
341                    $siteLink = $innerEntity->getSiteLink( $globalID );
342                    return $this->site->getPageUrl( $siteLink->getPageName() );
343                }
344            }
345        } catch ( StorageException | RevisionedUnresolvedRedirectException $e ) {
346            $this->logger->warning(
347                "Cannot fetch URL for EntityId " . $entityId . ". Reason: " . $e->getMessage()
348            );
349        }
350        return false;
351    }
352
353    /**
354     * @param Snak $snak
355     * @return bool true if the given snak is either a defining formula, a quantity symbol, or a 'in defining formula'
356     */
357    private function isSymbolSnak( Snak $snak ) {
358        return $snak->getPropertyId()->equals( $this->propertyIdQuantitySymbol ) ||
359            $snak->getPropertyId()->equals( $this->propertyIdDefiningFormula ) ||
360            $snak->getPropertyId()->equals( $this->propertyIdInDefiningFormula );
361    }
362
363    /**
364     * @param Snak $snak
365     * @return bool true if the given snak is either the 'has part or parts' or the 'symbol represents' property
366     */
367    private function isFormulaItemSnak( Snak $snak ) {
368        return $snak->getPropertyId()->equals( $this->propertyIdHasPart ) ||
369            $snak->getPropertyId()->equals( $this->propertyIdSymbolRepresents );
370    }
371
372    /**
373     * @param string $qID
374     * @return string
375     */
376    public function buildURL( string $qID ): string {
377        return $this->repoLinker->getEntityUrl( new ItemId( $qID ) );
378    }
379
380    /**
381     * @param string $qid
382     * @param string $langCode
383     * @return array of form ['qid'] = {'url': str, 'title': str}
384     */
385    public function getUrlFromSymbol( string $qid, string $langCode ): array {
386        $resultMap = [];
387        try {
388            $output = $this->fetchWikibaseFromId( $qid, $langCode );
389        } catch ( Exception $e ) {
390            $this->logger->warning(
391                "Cannot fetch QID " . $qid . " from Wikibase. Reason: " . $e->getMessage()
392            );
393            return []; // return empty array if qid or lang code not exists
394        }
395        $parts = $output->getParts();
396        foreach ( $parts as $part ) {
397            $partMap = [];
398            $partMap['url'] = $this->fetchPageUrl(
399                $this->idParser->parse(
400                    $part->getId()->getSerialization() ) ) ?? '';
401            $partMap['title'] = $part->getLabel() ?? '';
402            $resultMap[$part->getSymbol()->getValue()] = $partMap;
403        }
404        return $resultMap;
405    }
406}