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