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

File AnnotationGeneratorChainingListener.java

 

Coverage histogram

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

Code metrics

32
78
15
1
379
190
34
0.44
5.2
15
2.27

Classes

Class Line # Actions
AnnotationGeneratorChainingListener 55 78 0% 34 11
0.91291.2%
 

Contributing tests

This file is covered by 124 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.internal.renderer;
21   
22    import java.util.Collection;
23    import java.util.Collections;
24    import java.util.HashMap;
25    import java.util.LinkedList;
26    import java.util.List;
27    import java.util.Map;
28    import java.util.SortedMap;
29    import java.util.TreeMap;
30   
31    import org.apache.commons.lang3.StringUtils;
32    import org.xwiki.annotation.Annotation;
33    import org.xwiki.annotation.content.AlteredContent;
34    import org.xwiki.annotation.content.ContentAlterer;
35    import org.xwiki.annotation.renderer.AnnotationEvent;
36    import org.xwiki.annotation.renderer.AnnotationEvent.AnnotationEventType;
37    import org.xwiki.rendering.listener.Listener;
38    import org.xwiki.rendering.listener.MetaData;
39    import org.xwiki.rendering.listener.QueueListener;
40    import org.xwiki.rendering.listener.chaining.ChainingListener;
41    import org.xwiki.rendering.listener.chaining.ListenerChain;
42    import org.xwiki.rendering.syntax.Syntax;
43   
44    /**
45    * Chaining listener that maps the annotations on the events that it receives, holds the state of annotations on these
46    * events and exposes it to the subsequent listeners in the chain through {@link #getAnnotationEvents()}. It operates by
47    * buffering all events, creating the plain text representation of the listened content, mapping the annotations on this
48    * content and identifying the events in the stream that hold the start and end of the annotations. <br>
49    * FIXME: this should use the PlainTextNormalizingChaininngRenderer to generate the plain text version of the content,
50    * and match with the space normalized selection.
51    *
52    * @version $Id: c25c48768c21bdfe15c9314eb27db2a0d12024a9 $
53    * @since 2.3M1
54    */
 
55    public class AnnotationGeneratorChainingListener extends QueueListener implements ChainingListener
56    {
57    /**
58    * Version number of this class.
59    */
60    private static final long serialVersionUID = -2790330640900288463L;
61   
62    /**
63    * The chain listener from which this listener is part of.
64    */
65    private ListenerChain chain;
66   
67    /**
68    * Buffer to store the plain text version of the content to be rendered, so that annotations are mapped on it.
69    */
70    private StringBuffer plainTextContent = new StringBuffer();
71   
72    /**
73    * Map to store the ranges in the plainTextContent and their corresponding events. The ranges will be stored by
74    * their end index (inclusive) and ordered from smallest to biggest.
75    */
76    private SortedMap<Integer, Event> eventsMapping = new TreeMap<Integer, Event>();
77   
78    /**
79    * Map to store the events whose content has been altered upon append to the plain text representation, along with
80    * the altered content objects to allow translation of offsets back to the original offsets.
81    */
82    private Map<Event, AlteredContent> alteredEventsContent = new HashMap<Event, AlteredContent>();
83   
84    /**
85    * The collection of annotations to generate annotation events for, by default the empty list.
86    */
87    private Collection<Annotation> annotations = Collections.<Annotation> emptyList();
88   
89    /**
90    * Cleaner for the annotation selection text, so that it can be mapped on the content.
91    */
92    private ContentAlterer selectionAlterer;
93   
94    /**
95    * The list of bookmarks where annotation events take place. The map holds a correspondence between the event in the
96    * stream of wiki events and the annotation events that take place inside it, at the specified offset.
97    */
98    private Map<Event, SortedMap<Integer, List<AnnotationEvent>>> bookmarks =
99    new HashMap<Event, SortedMap<Integer, List<AnnotationEvent>>>();
100   
101    /**
102    * Builds an annotation generator listener from the passed link generator in the passed chain.
103    *
104    * @param selectionAlterer cleaner for the annotation selection text, so that it can be mapped on the content
105    * @param listenerChain the chain in which this listener is part of
106    */
 
107  132 toggle public AnnotationGeneratorChainingListener(ContentAlterer selectionAlterer, ListenerChain listenerChain)
108    {
109  132 this.chain = listenerChain;
110  132 this.selectionAlterer = selectionAlterer;
111    }
112   
 
113  1724 toggle @Override
114    public void onWord(String word)
115    {
116    // queue this event
117  1724 super.onWord(word);
118    // put it in the buffer
119  1724 plainTextContent.append(word);
120    // store the mapping of the range to the just added event
121  1724 eventsMapping.put(plainTextContent.length() - 1, getLast());
122    }
123   
 
124  506 toggle @Override
125    public void onSpecialSymbol(char symbol)
126    {
127  506 super.onSpecialSymbol(symbol);
128  506 plainTextContent.append("" + symbol);
129  506 eventsMapping.put(plainTextContent.length() - 1, getLast());
130    }
131   
 
132  30 toggle @Override
133    public void onVerbatim(String content, boolean inline, Map<String, String> parameters)
134    {
135  30 super.onVerbatim(content, inline, parameters);
136  30 handleRawText(content);
137    }
138   
139    /**
140    * Helper function to help handle raw text, such as the raw blocks or the verbatim blocks.
141    *
142    * @param text the raw text to handle
143    */
 
144  38 toggle private void handleRawText(String text)
145    {
146    // normalize the protected string before adding it to the plain text version
147  38 AlteredContent cleanedContent = selectionAlterer.alter(text);
148    // put this event in the mapping only if it has indeed generated something
149  38 String cleanedContentString = cleanedContent.getContent().toString();
150  38 if (!StringUtils.isEmpty(cleanedContentString)) {
151  22 plainTextContent.append(cleanedContentString);
152  22 eventsMapping.put(plainTextContent.length() - 1, getLast());
153    // also store this event in the list of events with altered content
154  22 alteredEventsContent.put(getLast(), cleanedContent);
155    }
156    }
157   
 
158  8 toggle @Override
159    public void onRawText(String text, Syntax syntax)
160    {
161    // Store the raw text as it is ftm. Should handle syntax in the future
162  8 super.onRawText(text, syntax);
163    // Similar approach to verbatim FTM. In the future, syntax specific cleaner could be used for various syntaxes
164    // (which would do the great job for HTML, for example)
165  8 handleRawText(text);
166    }
167   
168    /**
169    * {@inheritDoc}
170    *
171    * @since 3.0M2
172    */
 
173  132 toggle @Override
174    public void endDocument(MetaData metadata)
175    {
176  132 super.endDocument(metadata);
177   
178    // create the bookmarks
179  132 mapAnnotations();
180   
181    // now get the next listener in the chain and consume all events to it
182  132 ChainingListener renderer = chain.getNextListener(getClass());
183   
184    // send the events forward to the next annotation listener
185  132 consumeEvents(renderer);
186    }
187   
188    /**
189    * Helper method to map the annotations on the plainTextContent and identify the events where annotations start and
190    * end.
191    */
 
192  132 toggle private void mapAnnotations()
193    {
194  132 for (Annotation ann : annotations) {
195    // clean it up and its context
196  88 String annotationContext = ann.getSelectionInContext();
197  88 if (StringUtils.isEmpty(annotationContext)) {
198    // cannot find the context of the annotation or the annotation selection cannot be found in the
199    // annotation context, ignore it
200    // TODO: mark it somehow...
201  0 continue;
202    }
203    // build the cleaned version of the annotation by cleaning its left context, selection and right context and
204    // concatenating them together
205  88 String alteredsLeftContext =
206  88 StringUtils.isEmpty(ann.getSelectionLeftContext()) ? "" : selectionAlterer.alter(
207    ann.getSelectionLeftContext()).getContent().toString();
208  88 String alteredRightContext =
209  88 StringUtils.isEmpty(ann.getSelectionRightContext()) ? "" : selectionAlterer.alter(
210    ann.getSelectionRightContext()).getContent().toString();
211  88 String alteredSelection =
212  88 StringUtils.isEmpty(ann.getSelection()) ? "" : selectionAlterer.alter(ann.getSelection()).getContent()
213    .toString();
214  88 String cleanedContext = alteredsLeftContext + alteredSelection + alteredRightContext;
215    // find the annotation with its context in the plain text representation of the content
216  88 int contextIndex = plainTextContent.indexOf(cleanedContext);
217  88 if (contextIndex >= 0) {
218    // find the indexes where annotation starts and ends inside the cleaned context
219  88 int alteredSelectionStartIndex = alteredsLeftContext.length();
220  88 int alteredSelectionEndIndex = alteredSelectionStartIndex + alteredSelection.length() - 1;
221    // get the start and end events for the annotation
222    // annotation starts before char at annotationIndex and ends after char at annotationIndex +
223    // alteredSelection.length() - 1
224  88 Object[] startEvt = getEventAndOffset(contextIndex + alteredSelectionStartIndex, false);
225  88 Object[] endEvt = getEventAndOffset(contextIndex + alteredSelectionEndIndex, true);
226  88 if (startEvt != null & endEvt != null) {
227    // store the bookmarks
228  88 addBookmark((Event) startEvt[0], new AnnotationEvent(AnnotationEventType.START, ann),
229    (Integer) startEvt[1]);
230  88 addBookmark((Event) endEvt[0], new AnnotationEvent(AnnotationEventType.END, ann),
231    (Integer) endEvt[1]);
232    } else {
233    // cannot find the events for the start and / or end of annotation, ignore it
234    // TODO: mark it somehow...
235  0 continue;
236    }
237    } else {
238    // cannot find the context of the annotation or the annotation selection cannot be found in the
239    // annotation context, ignore it
240    // TODO: mark it somehow...
241  0 continue;
242    }
243    }
244    }
245   
246    /**
247    * Helper function to get the event where the passed index falls in, based on the isEnd setting to know if the
248    * offset should be given before the character at the index position or after it.
249    *
250    * @param index the index to get the event for
251    * @param isEnd {@code true} if the index should be considered as an end index, {@code false} otherwise
252    * @return an array of objects to hold the event reference, on the first position, and the offset inside this event
253    * on the second
254    */
 
255  176 toggle private Object[] getEventAndOffset(int index, boolean isEnd)
256    {
257  176 Map.Entry<Integer, Event> previous = null;
258  176 for (Map.Entry<Integer, Event> range : eventsMapping.entrySet()) {
259    // <= because end index is included
260    // if we have reached the first point where the end index is to the left of the index, it means we're in the
261    // first event that contains the index
262  1832 if (index <= range.getKey()) {
263    // get this event
264  176 Event evt = range.getValue();
265    // compute the start index wrt to the end index of the previous event
266  176 int startIndex = 0;
267  176 if (previous != null) {
268  162 startIndex = previous.getKey() + 1;
269    }
270    // compute the offset inside this event wrt the start index of this event
271  176 int offset = index - startIndex;
272   
273    // adjust this offset if the content of this event was altered
274  176 AlteredContent alteredEventContent = alteredEventsContent.get(evt);
275  176 if (alteredEventContent != null) {
276  8 offset = alteredEventContent.getInitialOffset(offset);
277    }
278   
279    // end indexes are specified after the character/position
280  176 if (isEnd) {
281  88 offset += 1;
282    }
283   
284    // return the result
285  176 return new Object[] {evt, offset};
286    }
287    // else advance one more step, storing the previous event
288  1656 previous = range;
289    }
290    // nothing was found, return null. However this shouldn't happen :)
291  0 return null;
292    }
293   
294    /**
295    * Adds an annotation bookmark in this list of bookmarks.
296    *
297    * @param renderingEvent the rendering event where the annotation should be bookmarked
298    * @param offset the offset of the annotation event inside this rendering event
299    * @param annotationEvent the annotation event to bookmark
300    */
 
301  176 toggle protected void addBookmark(Event renderingEvent, AnnotationEvent annotationEvent, int offset)
302    {
303  176 SortedMap<Integer, List<AnnotationEvent>> mappings = bookmarks.get(renderingEvent);
304  176 if (mappings == null) {
305  144 mappings = new TreeMap<Integer, List<AnnotationEvent>>();
306  144 bookmarks.put(renderingEvent, mappings);
307    }
308  176 List<AnnotationEvent> events = mappings.get(offset);
309  176 if (events == null) {
310  173 events = new LinkedList<AnnotationEvent>();
311  173 mappings.put(offset, events);
312    }
313   
314  176 addAnnotationEvent(annotationEvent, events);
315    }
316   
317    /**
318    * Helper function to help add an annotation event to the list of events, and keep the restriction that end events
319    * are stored before start events. Otherwise put, for the same offset, annotations end first and open after.
320    *
321    * @param evt the annotation event to add to the list
322    * @param list the annotation events list to add the event to
323    */
 
324  176 toggle protected void addAnnotationEvent(AnnotationEvent evt, List<AnnotationEvent> list)
325    {
326    // if there is no event in the list, or the event is a start event or there is no start event in the list, just
327    // append the event to the end of the list
328  176 if (list.size() == 0 || evt.getType() == AnnotationEventType.START
329    || list.get(list.size() - 1).getType() == AnnotationEventType.END) {
330  175 list.add(evt);
331    } else {
332    // find the first start event and add before it
333  1 int index = 0;
334  1 for (index = 0; index < list.size() && list.get(index).getType() != AnnotationEventType.START; index++) {
335    // nothing, it will stop at first start event
336    }
337  1 list.add(index, evt);
338    }
339    }
340   
 
341  132 toggle @Override
342    public void consumeEvents(Listener listener)
343    {
344    // same function basically, except that we need to leave the event at the top of the queue when firing so that
345    // we can get the correct state
346  4734 while (!isEmpty()) {
347    // peek the queue
348  4602 Event event = getFirst();
349    // fire the event with the event as the top of the queue still so that we can give the correct bookmarks
350  4602 event.eventType.fireEvent(listener, event.eventParameters);
351    // and remove the event so that we can go to next
352  4602 remove();
353    }
354    }
355   
 
356  0 toggle @Override
357    public ListenerChain getListenerChain()
358    {
359  0 return chain;
360    }
361   
362    /**
363    * Sets the collections of annotations to identify on the listened content and send notifications for.
364    *
365    * @param annotations the collection of annotations to generate events for
366    */
 
367  70 toggle public void setAnnotations(Collection<Annotation> annotations)
368    {
369  70 this.annotations = annotations;
370    }
371   
372    /**
373    * @return the bookmarks where annotation events take place
374    */
 
375  2268 toggle public SortedMap<Integer, List<AnnotationEvent>> getAnnotationEvents()
376    {
377  2268 return bookmarks.get(getFirst());
378    }
379    }