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