Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
132 / 132 |
|
100.00% |
13 / 13 |
CRAP | |
100.00% |
1 / 1 |
MathWikibaseConnector | |
100.00% |
132 / 132 |
|
100.00% |
13 / 13 |
45 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
25 / 25 |
|
100.00% |
1 / 1 |
1 | |||
loadPropertyId | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
getStatements | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
2 | |||
fetchWikibaseFromId | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
7 | |||
fetchLabelDescription | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
3 | |||
fetchStatements | |
100.00% |
10 / 10 |
|
100.00% |
1 / 1 |
3 | |||
fetchSymbol | |
100.00% |
8 / 8 |
|
100.00% |
1 / 1 |
4 | |||
fetchHasPartSnaks | |
100.00% |
23 / 23 |
|
100.00% |
1 / 1 |
10 | |||
fetchPageUrl | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
4 | |||
isSymbolSnak | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
3 | |||
isFormulaItemSnak | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
buildURL | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getUrlFromSymbol | |
100.00% |
16 / 16 |
|
100.00% |
1 / 1 |
3 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Math; |
4 | |
5 | use DataValues\StringValue; |
6 | use Exception; |
7 | use InvalidArgumentException; |
8 | use MediaWiki\Config\ConfigException; |
9 | use MediaWiki\Config\ServiceOptions; |
10 | use MediaWiki\Languages\LanguageFactory; |
11 | use MediaWiki\Languages\LanguageNameUtils; |
12 | use MediaWiki\Site\Site; |
13 | use Psr\Log\LoggerInterface; |
14 | use Wikibase\Client\RepoLinker; |
15 | use Wikibase\DataModel\Entity\EntityId; |
16 | use Wikibase\DataModel\Entity\EntityIdParser; |
17 | use Wikibase\DataModel\Entity\EntityIdParsingException; |
18 | use Wikibase\DataModel\Entity\EntityIdValue; |
19 | use Wikibase\DataModel\Entity\Item; |
20 | use Wikibase\DataModel\Entity\ItemId; |
21 | use Wikibase\DataModel\Entity\PropertyId; |
22 | use Wikibase\DataModel\Services\Lookup\LabelDescriptionLookup; |
23 | use Wikibase\DataModel\Snak\PropertyValueSnak; |
24 | use Wikibase\DataModel\Snak\Snak; |
25 | use Wikibase\DataModel\Statement\StatementList; |
26 | use Wikibase\Lib\Store\EntityRevisionLookup; |
27 | use Wikibase\Lib\Store\FallbackLabelDescriptionLookupFactory; |
28 | use Wikibase\Lib\Store\RevisionedUnresolvedRedirectException; |
29 | use 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 | */ |
37 | class 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 | } |