1. Project Clover database Sat Feb 2 2019 06:45:20 CET
  2. Package org.xwiki.annotation.maintainer

File AbstractAnnotationMaintainer.java

 

Coverage histogram

../../../../img/srcFileCovDistChart10.png
0% of files have more coverage

Code metrics

58
139
7
1
465
242
51
0.37
19.86
7
7.29

Classes

Class Line # Actions
AbstractAnnotationMaintainer 51 139 0% 51 9
0.955882495.6%
 

Contributing tests

This file is covered by 49 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package org.xwiki.annotation.maintainer;
21   
22    import java.io.StringReader;
23    import java.util.ArrayList;
24    import java.util.Collection;
25    import java.util.List;
26   
27    import javax.inject.Inject;
28    import javax.inject.Named;
29   
30    import org.apache.commons.lang3.StringUtils;
31    import org.xwiki.annotation.Annotation;
32    import org.xwiki.annotation.content.AlteredContent;
33    import org.xwiki.annotation.content.ContentAlterer;
34    import org.xwiki.annotation.io.IOService;
35    import org.xwiki.annotation.io.IOTargetService;
36    import org.xwiki.component.manager.ComponentManager;
37    import org.xwiki.rendering.block.XDOM;
38    import org.xwiki.rendering.parser.Parser;
39    import org.xwiki.rendering.renderer.PrintRenderer;
40    import org.xwiki.rendering.renderer.printer.DefaultWikiPrinter;
41    import org.xwiki.rendering.renderer.printer.WikiPrinter;
42    import org.xwiki.rendering.syntax.Syntax;
43    import org.xwiki.rendering.transformation.TransformationManager;
44   
45    /**
46    * Default implementation for the annotation maintainer.
47    *
48    * @version $Id: 9805dc5e9edcbc8b1467cac5b78ec9274624edc7 $
49    * @since 2.3M1
50    */
 
51    public abstract class AbstractAnnotationMaintainer implements AnnotationMaintainer
52    {
53    /**
54    * Annotations storage service.
55    */
56    @Inject
57    protected IOService ioService;
58   
59    /**
60    * Content storage and manipulation service.
61    */
62    @Inject
63    protected IOTargetService ioContentService;
64   
65    /**
66    * Space stripper content alterer, to be able to map annotations on the content in the same way the rendering
67    * mapping does it.
68    */
69    @Inject
70    @Named("whitespace")
71    protected ContentAlterer spaceStripperContentAlterer;
72   
73    /**
74    * The component manager, used to grab the plain text renderer.
75    */
76    @Inject
77    protected ComponentManager componentManager;
78   
 
79  675 toggle @Override
80    public void updateAnnotations(String target, String previousContent, String currentContent)
81    throws MaintainerServiceException
82    {
83  675 Collection<Annotation> annotations;
84  675 try {
85  675 annotations = ioService.getAnnotations(target);
86   
87  675 if (annotations.size() == 0) {
88    // no annotations, nothing to do
89  626 return;
90    }
91   
92    // store the annotations to save after update
93  49 List<Annotation> toUpdate = new ArrayList<Annotation>();
94   
95    // produce the ptr of the previous and current, wrt to syntax
96  49 String syntaxId = ioContentService.getSourceSyntax(target);
97  49 String renderedPreviousContent = renderPlainText(previousContent, syntaxId);
98  49 String renderedCurrentContent = renderPlainText(currentContent, syntaxId);
99   
100    // create the diffs
101  49 Collection<XDelta> differences =
102    getDiffService().getDifferences(renderedPreviousContent, renderedCurrentContent);
103    // if any differences: note that there can be updates on the content that have no influence on the plain
104    // text space normalized version
105  49 if (differences.size() > 0) {
106    // compute the spaceless version of the renderedPreviousContent to be able to map the annotation on it
107    // (so that matching is done in the same way as for rendering), and then go back to the normalized
108    // version
109  46 AlteredContent spacelessRenderedPreviousContent =
110    spaceStripperContentAlterer.alter(renderedPreviousContent);
111    // recompute properties for all annotations and store the ones to update
112  46 for (Annotation annotation : annotations) {
113  46 boolean wasUpdated = recomputeProperties(annotation, differences, renderedPreviousContent,
114    spacelessRenderedPreviousContent, renderedCurrentContent);
115  46 if (wasUpdated) {
116  35 toUpdate.add(annotation);
117    }
118    }
119    }
120   
121    // finally store the updates
122  49 ioService.updateAnnotations(target, toUpdate);
123    } catch (Exception e) {
124  0 throw new MaintainerServiceException(
125    "An exception occurred while updating annotations for content at " + target, e);
126    }
127    }
128   
129    /**
130    * Helper method to render the plain text version of the passed content.
131    *
132    * @param content the content to render in plain text
133    * @param syntaxId the source syntax of the content to render
134    * @throws Exception if anything goes wrong while rendering the content
135    * @return the normalized plain text rendered content
136    */
 
137  98 toggle private String renderPlainText(String content, String syntaxId) throws Exception
138    {
139  98 PrintRenderer renderer = componentManager.getInstance(PrintRenderer.class, "normalizer-plain/1.0");
140   
141    // parse
142  98 Parser parser = componentManager.getInstance(Parser.class, syntaxId);
143  98 XDOM xdom = parser.parse(new StringReader(content));
144   
145    // run transformations -> although it's going to be at least strange to handle rendered content since there
146    // is no context
147  98 Syntax sourceSyntax = Syntax.valueOf(syntaxId);
148  98 TransformationManager transformationManager = componentManager.getInstance(TransformationManager.class);
149  98 transformationManager.performTransformations(xdom, sourceSyntax);
150   
151    // render
152  98 WikiPrinter printer = new DefaultWikiPrinter();
153  98 renderer.setPrinter(printer);
154   
155  98 xdom.traverse(renderer);
156   
157  98 return printer.toString();
158    }
159   
160    /**
161    * For each annotation, recompute its properties wrt the differences in the document. The annotation mapping will be
162    * done using the spaceless version of the rendered previous content, in order to have synchronization with the
163    * rendering, whereas the annotation diff and update will be done wrt to the normalized spaces version, to produce
164    * human readable versions of the annotation selection and contexts.
165    *
166    * @param annotation the annotation to update properties for
167    * @param differences the differences between {@code renderedPreviousContent} and {@code renderedCurrentContent}
168    * @param renderedPreviousContent the plain text space normalized rendered previous content
169    * @param spacelessPreviousContent the spaceless version of the rendered previous content, to be used to map
170    * annotations on the content in the same way they are done on rendering, that is, spaceless.
171    * @param renderedCurrentContent the plain text space normalized rendered current content
172    * @return the updated state of this annotation, {@code true} if the annotation was updated during property
173    * recompute, {@code false} otherwise
174    */
 
175  46 toggle protected boolean recomputeProperties(Annotation annotation, Collection<XDelta> differences,
176    String renderedPreviousContent, AlteredContent spacelessPreviousContent, String renderedCurrentContent)
177    {
178  46 boolean updated = false;
179   
180    // TODO: do we still want this here? Do we want to try to recover altered annotations?
181  46 if (annotation.getState().equals(AnnotationState.ALTERED)) {
182  0 return updated;
183    }
184   
185  46 String spacelessLeftContext = StringUtils.isEmpty(annotation.getSelectionLeftContext()) ? ""
186    : spaceStripperContentAlterer.alter(annotation.getSelectionLeftContext()).getContent().toString();
187  46 String spacelessRightContext = StringUtils.isEmpty(annotation.getSelectionRightContext()) ? ""
188    : spaceStripperContentAlterer.alter(annotation.getSelectionRightContext()).getContent().toString();
189  46 String spacelessSelection = StringUtils.isEmpty(annotation.getSelection()) ? ""
190    : spaceStripperContentAlterer.alter(annotation.getSelection()).getContent().toString();
191  46 String spacelessContext = spacelessLeftContext + spacelessSelection + spacelessRightContext;
192    // get the positions for the first character in selection and last character in selection (instead of first out)
193    // to protect selection boundaries (spaces are grouped to the left when altered and we don't want extra spaces
194    // in the selection by using first index outside the selection)
195  46 int selectionIndex = spacelessLeftContext.length();
196  46 int lastSelectionIndex = selectionIndex + spacelessSelection.length() - 1;
197   
198    // map spaceless annotation (in context) on the spaceless version of the content
199  46 int cStart = spacelessPreviousContent.getContent().toString().indexOf(spacelessContext);
200   
201  46 if (spacelessContext.length() == 0 || cStart < 0) {
202    // annotation context does not exist or could not be found in the previous rendered content, it must be
203    // somewhere in the generated content or something like that, skip it
204  0 return updated;
205    }
206   
207  46 int cEnd = cStart + spacelessContext.length();
208  46 int sStart = cStart + selectionIndex;
209  46 int sEnd = cStart + lastSelectionIndex;
210   
211    // translate all back to the spaces version
212  46 cStart = spacelessPreviousContent.getInitialOffset(cStart);
213    // -1 +1 here because we're interested in the first character outside the context. To get that, we get last
214    // significant character and we advance one char further
215  46 cEnd = spacelessPreviousContent.getInitialOffset(cEnd - 1) + 1;
216  46 sStart = spacelessPreviousContent.getInitialOffset(sStart);
217    // add one char here so that selection end is outside the selection
218  46 sEnd = spacelessPreviousContent.getInitialOffset(sEnd) + 1;
219   
220    // save initial annotation state, to check how it needs to be updated afterwards
221  46 AnnotationState initialState = annotation.getState();
222   
223    // the context start & selection length after the modification of the content has took place
224  46 int alteredCStart = cStart;
225  46 int alteredSLength = sEnd - sStart;
226   
227  46 for (XDelta diff : differences) {
228  62 int dStart = diff.getOffset();
229  62 int dEnd = diff.getOffset() + diff.getOriginal().length();
230    // 1/ if the diff is before the selection, or ends exactly where selection starts, update the position of
231    // the context, to preserve the selection offset
232  62 if (dEnd <= sStart) {
233  13 alteredCStart += diff.getSignedDelta();
234    }
235    // 2/ diff is inside the selection (and not the previous condition)
236  62 if (dEnd > sStart && dStart >= sStart && dStart < sEnd && dEnd <= sEnd) {
237    // update the selection length
238  24 alteredSLength += diff.getSignedDelta();
239  24 annotation.setState(AnnotationState.UPDATED);
240  24 updated = true;
241    }
242   
243    // 3/ the edit overlaps the annotation selection completely
244  62 if (dStart <= sStart && dEnd >= sEnd) {
245    // mark annotation as altered and drop it
246  2 annotation.setState(AnnotationState.ALTERED);
247  2 updated = true;
248  2 break;
249    }
250   
251    // 4/ the edit overlaps the start of the annotation
252  60 if (dStart < sStart && dEnd > sStart && dEnd <= sEnd) {
253    // shift with the signed delta to the right, assume that the edit took place before the annotation and
254    // keep its size. This way it will be mapped at the position as if the edit would have taken place
255    // before it and will contain the new content at the start of the annotation
256  7 alteredCStart += diff.getSignedDelta();
257  7 annotation.setState(AnnotationState.UPDATED);
258  7 updated = true;
259    }
260   
261    // 5/ the edit overlaps the end of the annotation
262  60 if (dStart < sEnd && dEnd > sEnd) {
263    // nothing, behave as if the edit would have taken place after the annotation
264  7 annotation.setState(AnnotationState.UPDATED);
265  7 updated = true;
266    }
267    }
268   
269  46 if (annotation.getState() != AnnotationState.ALTERED) {
270    // compute the sizes of the contexts to be able to build the annotation contexts
271  44 int cLeftSize = sStart - cStart;
272  44 int cRightSize = cEnd - sEnd;
273   
274    // recompute the annotation context and all
275    // if this annotation was updated first time during this update, set its original selection
276  44 if (annotation.getState() == AnnotationState.UPDATED && initialState == AnnotationState.SAFE) {
277  28 annotation.setOriginalSelection(annotation.getSelection());
278    // FIXME: redundant, but anyway
279  28 updated = true;
280    }
281   
282  44 String originalLeftContext = annotation.getSelectionLeftContext();
283  44 String originalSelection = annotation.getSelection();
284  44 String originalRightContext = annotation.getSelectionRightContext();
285   
286  44 String contextLeft = renderedCurrentContent.substring(alteredCStart, alteredCStart + cLeftSize);
287  44 String selection =
288    renderedCurrentContent.substring(alteredCStart + cLeftSize, alteredCStart + cLeftSize + alteredSLength);
289  44 String contextRight = renderedCurrentContent.substring(alteredCStart + cLeftSize + alteredSLength,
290    alteredCStart + cLeftSize + alteredSLength + cRightSize);
291    // and finally update the context & selection
292  44 annotation.setSelection(selection, contextLeft, contextRight);
293   
294    // make sure annotation stays unique
295  44 ensureUnique(annotation, renderedCurrentContent, alteredCStart, cLeftSize, alteredSLength, cRightSize);
296   
297    // if the annotations selection and/or context have changed during the recompute, set the update flag
298  44 updated = updated || !(selection.equals(originalSelection) && contextLeft.equals(originalLeftContext)
299    && contextRight.equals(originalRightContext));
300    }
301   
302  46 return updated;
303    }
304   
305    /**
306    * Helper function to adjust passed annotation to make sure it is unique in the content.
307    *
308    * @param annotation the annotation to ensure uniqueness for
309    * @param content the content in which the annotation must be unique
310    * @param cStart precomputed position where the annotation starts, passed here for cache reasons
311    * @param cLeftSize precomputed length of the context to the left side of the selection inside the annotation
312    * context, passed here for cache reasons
313    * @param sLength precomputed length of the annotation selection, passed here for cache reasons
314    * @param cRightSize precomputed length of the context to the right side of the selection inside the annotation,
315    * passed here for cache reasons
316    */
 
317  44 toggle private void ensureUnique(Annotation annotation, String content, int cStart, int cLeftSize, int sLength,
318    int cRightSize)
319    {
320    // find out if there is another encounter of the selection text & context than the one at cStart
321  44 List<Integer> occurrences = getOccurrences(content, annotation.getSelectionInContext(), cStart);
322  44 if (occurrences.size() == 0) {
323    // it appears only once, it's done
324  35 return;
325    }
326   
327    // enlarge the context to the left and right with one character, until it is unique
328  9 boolean isUnique = false;
329  9 int cLength = cLeftSize + sLength + cRightSize;
330    // size expansion of the context of the annotation such as it becomes unique
331  9 int expansionLeft = 0;
332  9 int expansionRight = 0;
333    // the characters corresponding to the ends of the expanded context, to compare with all other occurrences and
334    // check if they're unique
335    // TODO: an odd situation can happen by comparing characters: at each expansion position there's another
336    // occurrence that matches, therefore an unique context is never found although it exists
337    // TODO: maybe expansion should be considered by words?
338  9 char charLeft = content.charAt(cStart - expansionLeft);
339  9 char charRight = content.charAt(cStart + cLength + expansionRight - 1);
340  44 while (!isUnique) {
341  35 boolean updated = false;
342    // get the characters at left and right and expand, but only if the positions are valid. If one stops being
343    // valid, only the other direction will be expanded in search of a new context
344  35 if (cStart - expansionLeft - 1 > 0) {
345  33 expansionLeft++;
346  33 charLeft = content.charAt(cStart - expansionLeft);
347  33 updated = true;
348    }
349  35 if (cStart + cLength + expansionRight + 1 <= content.length()) {
350  26 expansionRight++;
351  26 charRight = content.charAt(cStart + cLength + expansionRight - 1);
352  26 updated = true;
353    }
354  35 if (!updated) {
355    // couldn't update the context to the left nor to the right
356  0 break;
357    }
358  35 if (charLeft == ' ' || charRight == ' ') {
359    // don't consider uniqueness from space chars
360  13 continue;
361    }
362    // assume it's unique
363  22 isUnique = true;
364    // and check again all occurrences
365  22 for (int occurence : occurrences) {
366    // get the chars relative to the current occurrence at the respective expansion positions to the right
367    // and left
368  24 Character occurenceCharLeft = getSafeCharacter(content, occurence - expansionLeft);
369  24 Character occurenceCharRight = getSafeCharacter(content, occurence + cLength + expansionRight - 1);
370  24 if ((occurenceCharLeft != null && occurenceCharLeft.charValue() == charLeft)
371    && (occurenceCharRight != null && occurenceCharRight.charValue() == charRight)) {
372  13 isUnique = false;
373  13 break;
374    }
375    }
376    }
377  9 if (isUnique) {
378    // update the context with the new indexes
379    // expand the context to the entire word that it touches (just to make more sense and not depend with only
380    // one letter)
381  9 expansionLeft = expansionLeft + toNextWord(content, cStart - expansionLeft, true);
382  9 expansionRight = expansionRight + toNextWord(content, cStart + cLength + expansionRight, false);
383    // normally selection is not updated here, only the context therefore we don't set original selection
384  9 String contextLeft = content.substring(cStart - expansionLeft, cStart + cLeftSize);
385  9 String selection = content.substring(cStart + cLeftSize, cStart + cLeftSize + sLength);
386  9 String contextRight = content.substring(cStart + cLeftSize + sLength, cStart + cLength + expansionRight);
387   
388  9 annotation.setSelection(selection, contextLeft, contextRight);
389    } else {
390    // left the loop for other reasons: for example couldn't expand context
391    // leave it unchanged there's not much we could do anyway
392    }
393    }
394   
395    /**
396    * Helper function to get all occurrences of {@code pattern} in {@code subject}.
397    *
398    * @param subject the subject of the search
399    * @param pattern the pattern of the search
400    * @param exclude value to exclude from the results set
401    * @return the list of all occurrences of {@code pattern} in {@code subject}
402    */
 
403  44 toggle private List<Integer> getOccurrences(String subject, String pattern, int exclude)
404    {
405  44 List<Integer> indexes = new ArrayList<Integer>();
406  44 int lastIndex = subject.indexOf(pattern);
407  99 while (lastIndex != -1) {
408  55 if (lastIndex != exclude) {
409  11 indexes.add(lastIndex);
410    }
411  55 lastIndex = subject.indexOf(pattern, lastIndex + 1);
412    }
413   
414  44 return indexes;
415    }
416   
417    /**
418    * Helper function to advance to the next word in the subject, until the first space is encountered, starting from
419    * {@code position} and going to the left or to the right, as {@code toLeft} specifies. The returned value is the
420    * length of the offset from position to where the space was found.
421    *
422    * @param subject the string to search for spaces in
423    * @param position the position to start the search from
424    * @param toLeft {@code true} if the search should be done to the left of the string, {@code false} otherwise
425    * @return the offset starting from position, to the left or to the right, until the next word starts (or the
426    * document ends)
427    */
 
428  18 toggle private int toNextWord(String subject, int position, boolean toLeft)
429    {
430  18 int expansion = 1;
431    // advance until the next space is encountered in subject, from position, to the right by default and left if
432    // it's specified otherwise
433  18 boolean isSpaceOrEnd = toLeft ? position - expansion < 0 || subject.charAt(position - expansion) == ' '
434    : position + expansion > subject.length() || subject.charAt(position + expansion - 1) == ' ';
435  61 while (!isSpaceOrEnd) {
436  43 expansion++;
437  43 isSpaceOrEnd = toLeft ? position - expansion < 0 || subject.charAt(position - expansion) == ' '
438    : position + expansion > subject.length() || subject.charAt(position + expansion - 1) == ' ';
439    }
440   
441  18 return expansion - 1;
442    }
443   
444    /**
445    * Helper function to safely get the character at position {@code position} in the passed content, or null
446    * otherwise.
447    *
448    * @param content the content to get the character from
449    * @param position the position to get character at
450    * @return the character at position {@code position} or {@code null} otherwise.
451    */
 
452  48 toggle private Character getSafeCharacter(String content, int position)
453    {
454  48 if (position >= 0 && position < content.length()) {
455  46 return content.charAt(position);
456    } else {
457  2 return null;
458    }
459    }
460   
461    /**
462    * @return the diff service to be used by this maintainer to get the content differences
463    */
464    public abstract DiffService getDiffService();
465    }