Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
48.62% covered (danger)
48.62%
53 / 109
37.50% covered (danger)
37.50%
3 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
LexemePatcher
48.62% covered (danger)
48.62%
53 / 109
37.50% covered (danger)
37.50%
3 / 8
170.86
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 canPatchEntityType
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 patchEntity
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
 getPatchedItemId
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
6.60
 patchNextFormId
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 patchNextSenseId
12.50% covered (danger)
12.50%
1 / 8
0.00% covered (danger)
0.00%
0 / 1
14.72
 patchForms
51.61% covered (warning)
51.61%
16 / 31
0.00% covered (danger)
0.00%
0 / 1
12.55
 patchSenses
3.33% covered (danger)
3.33%
1 / 30
0.00% covered (danger)
0.00%
0 / 1
51.26
1<?php
2
3namespace Wikibase\Lexeme\Domain\Diff;
4
5use Diff\DiffOp\Diff\Diff;
6use Diff\DiffOp\DiffOpAdd;
7use Diff\DiffOp\DiffOpChange;
8use Diff\DiffOp\DiffOpRemove;
9use Diff\Patcher\PatcherException;
10use InvalidArgumentException;
11use RequestContext;
12use Wikibase\DataModel\Entity\EntityDocument;
13use Wikibase\DataModel\Entity\ItemId;
14use Wikibase\DataModel\Services\Diff\EntityDiff;
15use Wikibase\DataModel\Services\Diff\EntityPatcherStrategy;
16use Wikibase\DataModel\Services\Diff\StatementListPatcher;
17use Wikibase\DataModel\Services\Diff\TermListPatcher;
18use Wikibase\Lexeme\Domain\Model\Lexeme;
19use Wikibase\Lexeme\Domain\Model\LexemePatchAccess;
20use Wikibase\Repo\WikibaseRepo;
21use Wikimedia\Assert\Assert;
22
23/**
24 * @license GPL-2.0-or-later
25 * @author Amir Sarabadani <ladsgroup@gmail.com>
26 * @author Thiemo Kreuz
27 */
28class LexemePatcher implements EntityPatcherStrategy {
29
30    /**
31     * @var TermListPatcher
32     */
33    private $termListPatcher;
34
35    /**
36     * @var StatementListPatcher
37     */
38    private $statementListPatcher;
39
40    /**
41     * @var FormPatcher
42     */
43    private $formPatcher;
44
45    /**
46     * @var SensePatcher
47     */
48    private $sensePatcher;
49
50    public function __construct() {
51        $this->termListPatcher = new TermListPatcher();
52        $this->statementListPatcher = new StatementListPatcher();
53        $this->formPatcher = new FormPatcher();
54        $this->sensePatcher = new SensePatcher();
55    }
56
57    /**
58     * @param string $entityType
59     *
60     * @return bool
61     */
62    public function canPatchEntityType( $entityType ) {
63        return $entityType === Lexeme::ENTITY_TYPE;
64    }
65
66    /**
67     * @param Lexeme $lexeme
68     * @param LexemeDiff $patch
69     *
70     * @throws InvalidArgumentException
71     */
72    public function patchEntity( EntityDocument $lexeme, EntityDiff $patch ) {
73        Assert::parameterType( Lexeme::class, $lexeme, '$lexeme' );
74        Assert::parameterType( LexemeDiff::class, $patch, '$patch' );
75
76        $this->termListPatcher->patchTermList( $lexeme->getLemmas(), $patch->getLemmasDiff() );
77
78        $this->statementListPatcher->patchStatementList(
79            $lexeme->getStatements(),
80            $patch->getClaimsDiff()
81        );
82
83        $itemId = $this->getPatchedItemId( $patch->getLexicalCategoryDiff() );
84        if ( $itemId !== false ) {
85            $lexeme->setLexicalCategory( $itemId );
86        }
87
88        $itemId = $this->getPatchedItemId( $patch->getLanguageDiff() );
89        if ( $itemId !== false ) {
90            $lexeme->setLanguage( $itemId );
91        }
92
93        $this->patchNextFormId( $lexeme, $patch );
94        $this->patchForms( $lexeme, $patch );
95
96        $this->patchNextSenseId( $lexeme, $patch );
97        $this->patchSenses( $lexeme, $patch );
98    }
99
100    /**
101     * @param Diff $patch
102     *
103     * @throws PatcherException
104     * @return ItemId|null|false False in case the diff is valid, but does not contain a change.
105     */
106    private function getPatchedItemId( Diff $patch ) {
107        if ( $patch->isEmpty() ) {
108            return false;
109        }
110
111        $diffOp = $patch['id'];
112
113        switch ( true ) {
114            case $diffOp instanceof DiffOpAdd:
115                return $diffOp->getNewValue();
116
117            case $diffOp instanceof DiffOpChange:
118                return $diffOp->getNewValue();
119
120            case $diffOp instanceof DiffOpRemove:
121                return null;
122        }
123
124        throw new PatcherException( 'Invalid ItemId diff' );
125    }
126
127    private function patchNextFormId( Lexeme $entity, LexemeDiff $patch ) {
128        // FIXME: Why is this a loop? The nextFormId field is not an array!
129        foreach ( $patch->getNextFormIdDiff() as $nextFormIdDiff ) {
130            if ( !( $nextFormIdDiff instanceof DiffOpChange ) ) {
131                throw new PatcherException( 'Invalid forms list diff' );
132            }
133
134            $newNumber = $nextFormIdDiff->getNewValue();
135            if ( $newNumber > $entity->getNextFormId() ) {
136                $entity->patch( static function ( LexemePatchAccess $patchAccess ) use ( $newNumber ) {
137                    $patchAccess->increaseNextFormIdTo( $newNumber );
138                } );
139            }
140        }
141    }
142
143    private function patchNextSenseId( Lexeme $entity, LexemeDiff $patch ) {
144        // FIXME: Same as above
145        foreach ( $patch->getNextSenseIdDiff() as $nextSenseIdDiff ) {
146            if ( !( $nextSenseIdDiff instanceof DiffOpChange ) ) {
147                throw new PatcherException( 'Invalid senses list diff' );
148            }
149
150            $newNumber = $nextSenseIdDiff->getNewValue();
151            if ( $newNumber > $entity->getNextSenseId() ) {
152                $entity->patch( static function ( LexemePatchAccess $patchAccess ) use ( $newNumber ) {
153                    $patchAccess->increaseNextSenseIdTo( $newNumber );
154                } );
155            }
156        }
157    }
158
159    private function patchForms( Lexeme $lexeme, LexemeDiff $patch ) {
160        foreach ( $patch->getFormsDiff() as $formDiff ) {
161            switch ( true ) {
162                case $formDiff instanceof AddFormDiff:
163                    $form = $formDiff->getAddedForm();
164                    $lexeme->patch(
165                        static function ( LexemePatchAccess $patchAccess ) use ( $form ) {
166                            $patchAccess->addForm( $form );
167                        }
168                    );
169                    break;
170
171                case $formDiff instanceof RemoveFormDiff:
172                    $lexeme->removeForm( $formDiff->getRemovedFormId() );
173                    break;
174
175                case $formDiff instanceof ChangeFormDiffOp:
176                    try {
177                        $form = $lexeme->getForm( $formDiff->getFormId() );
178                    } catch ( \OutOfRangeException $e ) {
179                        /**
180                         * This should never happen, but somehow sometimes a request to remove a form ends up here
181                         * for unknown reasons. See T326768.
182                         *
183                         * Log what data we have to hopefully help figure out the problem
184                         */
185                        WikibaseRepo::getLogger()->warning( __METHOD__ . ': Form not found', [
186                            'formId' => $formDiff->getFormId(),
187                            'representationDiff' => $formDiff->getRepresentationDiff()->serialize(),
188                            'grammaticalFeaturesDiff' => $formDiff->getGrammaticalFeaturesDiff()->serialize(),
189                            'statementsDiff' => $formDiff->getStatementsDiff()->serialize(),
190                            'lexemeDiff' => $patch->serialize(),
191                            'existingForms' => implode( ', ', array_map(
192                                fn ( $form ) => $form->getId()->getSerialization(),
193                                $lexeme->getForms()->toArray()
194                            ) ),
195                            'requestPostValues' => RequestContext::getMain()->getRequest()->getPostValues(),
196                        ] );
197
198                        throw $e;
199                    }
200                    $this->formPatcher->patch( $form, $formDiff );
201                    break;
202
203                default:
204                    throw new PatcherException( 'Invalid forms list diff: ' . get_class( $formDiff ) );
205            }
206        }
207    }
208
209    private function patchSenses( Lexeme $lexeme, LexemeDiff $patch ) {
210        foreach ( $patch->getSensesDiff() as $senseDiff ) {
211            switch ( true ) {
212                case $senseDiff instanceof AddSenseDiff:
213                    $sense = $senseDiff->getAddedSense();
214                    $lexeme->patch(
215                        static function ( LexemePatchAccess $patchAccess ) use ( $sense ) {
216                            $patchAccess->addSense( $sense );
217                        }
218                    );
219                    break;
220
221                case $senseDiff instanceof RemoveSenseDiff:
222                    $lexeme->removeSense( $senseDiff->getRemovedSenseId() );
223                    break;
224
225                case $senseDiff instanceof ChangeSenseDiffOp:
226                    try {
227                        $sense = $lexeme->getSense( $senseDiff->getSenseId() );
228                    } catch ( \OutOfRangeException $e ) {
229                        /**
230                         * This should never happen, but somehow sometimes a request to remove a sense ends up here
231                         * for unknown reasons. See T326768 / T284061.
232                         *
233                         * Log what data we have to hopefully help figure out the problem
234                         */
235                        WikibaseRepo::getLogger()->warning( __METHOD__ . ': Sense not found', [
236                            'senseId' => $senseDiff->getSenseId(),
237                            'glossesDiff' => $senseDiff->getGlossesDiff()->serialize(),
238                            'statementsDiff' => $senseDiff->getStatementsDiff()->serialize(),
239                            'lexemeDiff' => $patch->serialize(),
240                            'existingSenses' => implode( ', ', array_map(
241                                fn ( $sense ) => $sense->getId()->getSerialization(),
242                                $lexeme->getSenses()->toArray()
243                            ) ),
244                            'requestPostValues' => RequestContext::getMain()->getRequest()->getPostValues(),
245                        ] );
246
247                        throw $e;
248                    }
249                    $this->sensePatcher->patchEntity( $sense, $senseDiff );
250                    break;
251
252                default:
253                    throw new PatcherException( 'Invalid senses list diff: ' . get_class( $senseDiff ) );
254            }
255        }
256    }
257
258}