1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package org.xwiki.annotation.maintainer

File AbstractAnnotationMaintainer.java

 

Coverage histogram

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

Code metrics

58
140
7
1
475
252
51
0.36
20
7
7.29

Classes

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