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

File DefaultIOService.java

 

Coverage histogram

../../../../../img/srcFileCovDistChart7.png
64% of files have more coverage

Code metrics

40
124
9
1
452
263
49
0.4
13.78
9
5.44

Classes

Class Line # Actions
DefaultIOService 64 124 0% 49 57
0.6705202567.1%
 

Contributing tests

No tests hitting this source file were found.

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.io.internal;
21   
22    import java.util.ArrayList;
23    import java.util.Arrays;
24    import java.util.Collection;
25    import java.util.Collections;
26    import java.util.Date;
27    import java.util.List;
28   
29    import javax.inject.Inject;
30    import javax.inject.Named;
31    import javax.inject.Singleton;
32   
33    import org.codehaus.plexus.util.StringUtils;
34    import org.slf4j.Logger;
35    import org.xwiki.annotation.Annotation;
36    import org.xwiki.annotation.AnnotationConfiguration;
37    import org.xwiki.annotation.io.IOService;
38    import org.xwiki.annotation.io.IOServiceException;
39    import org.xwiki.annotation.maintainer.AnnotationState;
40    import org.xwiki.annotation.reference.TypedStringEntityReferenceResolver;
41    import org.xwiki.component.annotation.Component;
42    import org.xwiki.context.Execution;
43    import org.xwiki.model.EntityType;
44    import org.xwiki.model.reference.EntityReference;
45    import org.xwiki.model.reference.EntityReferenceSerializer;
46   
47    import com.xpn.xwiki.XWikiContext;
48    import com.xpn.xwiki.XWikiException;
49    import com.xpn.xwiki.doc.XWikiDocument;
50    import com.xpn.xwiki.objects.BaseObject;
51    import com.xpn.xwiki.objects.BaseProperty;
52   
53    /**
54    * Default {@link IOService} implementation, based on storing annotations in XWiki Objects in XWiki documents. The
55    * targets manipulated by this implementation are XWiki references, such as xwiki:Space.Page for documents or with an
56    * object and property reference if the target is an object property. Use the reference module to generate the
57    * references passed to this module, so that they can be resolved to XWiki content back by this implementation.
58    *
59    * @version $Id: 19a63d6eda7a463e94720fa83179e2428e4ba390 $
60    * @since 2.3M1
61    */
62    @Component
63    @Singleton
 
64    public class DefaultIOService implements IOService
65    {
66    /**
67    * The execution used to get the deprecated XWikiContext.
68    */
69    @Inject
70    private Execution execution;
71   
72    /**
73    * Entity reference handler to resolve the reference target.
74    */
75    @Inject
76    private TypedStringEntityReferenceResolver referenceResolver;
77   
78    /**
79    * Default entity reference serializer to create document full names.
80    */
81    @Inject
82    private EntityReferenceSerializer<String> serializer;
83   
84    /**
85    * Local entity reference serializer, to create references which are robust to import / export.
86    */
87    @Inject
88    @Named("local")
89    private EntityReferenceSerializer<String> localSerializer;
90   
91    /**
92    * The logger to use for logging.
93    */
94    @Inject
95    private Logger logger;
96   
97    /**
98    * The Annotation Application's configuration.
99    */
100    @Inject
101    private AnnotationConfiguration configuration;
102   
103    /**
104    * {@inheritDoc}
105    * <p>
106    * This implementation saves the added annotation in the document where the target of the annotation is.
107    * </p>
108    *
109    * @see org.xwiki.annotation.io.IOService#addAnnotation(String, org.xwiki.annotation.Annotation)
110    */
 
111  4 toggle @Override
112    public void addAnnotation(String target, Annotation annotation) throws IOServiceException
113    {
114  4 try {
115    // extract the document name from the passed target
116    // by default the fullname is the passed target
117  4 String documentFullName = target;
118  4 EntityReference targetReference = referenceResolver.resolve(target, EntityType.DOCUMENT);
119    // try to get a document reference from the passed target reference
120  4 EntityReference docRef = targetReference.extractReference(EntityType.DOCUMENT);
121  4 if (docRef != null) {
122  4 documentFullName = serializer.serialize(docRef);
123    }
124    // now get the document with that name
125  4 XWikiContext deprecatedContext = getXWikiContext();
126  4 XWikiDocument document = deprecatedContext.getWiki().getDocument(documentFullName, deprecatedContext);
127    // create a new object in this document to hold the annotation
128    // Make sure to use a relative reference when creating the XObject, since we can`t use absolute references
129    // for an object's class. This avoids ugly log warning messages.
130  4 EntityReference annotationClassReference = configuration.getAnnotationClassReference();
131  4 annotationClassReference =
132    annotationClassReference.removeParent(annotationClassReference.extractReference(EntityType.WIKI));
133  4 int id = document.createXObject(annotationClassReference, deprecatedContext);
134  4 BaseObject object = document.getXObject(configuration.getAnnotationClassReference(), id);
135  4 updateObject(object, annotation, deprecatedContext);
136    // and set additional data: author to annotation author, date to now and the annotation target
137  4 object.set(Annotation.DATE_FIELD, new Date(), deprecatedContext);
138    // TODO: maybe we shouldn't trust what we receive from the caller but set the author from the context.
139    // Or the other way around, set the author of the document from the annotations author.
140  4 object.set(Annotation.AUTHOR_FIELD, annotation.getAuthor(), deprecatedContext);
141    // store the target of this annotation, serialized with a local serializer, to be exportable and importable
142    // in a different wiki
143    // TODO: figure out if this is the best idea in terms of target serialization
144    // 1/ the good part is that it is a fixed value that can be searched with a query in all objects in the wiki
145    // 2/ the bad part is that copying a document to another space will not also update its annotation targets
146    // 3/ if annotations are stored in the same document they annotate, the targets are only required for object
147    // fields
148    // ftm don't store the type of the reference since we only need to recognize the field, not to also read it.
149  4 if (targetReference.getType() == EntityType.OBJECT_PROPERTY
150    || targetReference.getType() == EntityType.DOCUMENT) {
151  4 object.set(Annotation.TARGET_FIELD, localSerializer.serialize(targetReference), deprecatedContext);
152    } else {
153  0 object.set(Annotation.TARGET_FIELD, target, deprecatedContext);
154    }
155    // set the author of the document to the current user
156  4 document.setAuthor(deprecatedContext.getUser());
157    // Note: We make sure to only provide a few characters of contextual information in order to control the
158    // size of the comment (we display the first 30 characters).
159  4 deprecatedContext.getWiki().saveDocument(document, "Added annotation on \""
160    + StringUtils.abbreviate(annotation.getSelection(), 30) + "\"", deprecatedContext);
161    } catch (XWikiException e) {
162  0 throw new IOServiceException("An exception message has occurred while saving the annotation", e);
163    }
164    }
165   
166    /**
167    * {@inheritDoc}
168    * <p>
169    * This implementation retrieves all the objects of the annotation class in the document where target points to, and
170    * which have the target set to {@code target}.
171    * </p>
172    *
173    * @see org.xwiki.annotation.io.IOService#getAnnotations(String)
174    */
 
175  9 toggle @Override
176    public Collection<Annotation> getAnnotations(String target) throws IOServiceException
177    {
178  9 try {
179    // parse the target and extract the local reference serialized from it, by the same rules
180  9 EntityReference targetReference = referenceResolver.resolve(target, EntityType.DOCUMENT);
181    // build the target identifier for the annotation
182  9 String localTargetId = target;
183    // and the name of the document where it should be stored
184  9 String docName = target;
185  9 if (targetReference.getType() == EntityType.DOCUMENT
186    || targetReference.getType() == EntityType.OBJECT_PROPERTY) {
187  9 localTargetId = localSerializer.serialize(targetReference);
188  9 docName = serializer.serialize(targetReference.extractReference(EntityType.DOCUMENT));
189    }
190    // get the document
191  9 XWikiContext deprecatedContext = getXWikiContext();
192  9 XWikiDocument document = deprecatedContext.getWiki().getDocument(docName, deprecatedContext);
193    // and the annotation class objects in it
194  9 List<BaseObject> objects = document.getXObjects(configuration.getAnnotationClassReference());
195    // and build a list of Annotation objects
196  9 List<Annotation> result = new ArrayList<Annotation>();
197  9 if (objects == null) {
198  1 return Collections.<Annotation> emptySet();
199    }
200  8 for (BaseObject object : objects) {
201    // if it's not on the required target, ignore it
202  26 if (object == null || !localTargetId.equals(object.getStringValue(Annotation.TARGET_FIELD))) {
203  6 continue;
204    }
205    // use the object number as annotation id
206  20 result.add(loadAnnotationFromObject(object, deprecatedContext));
207    }
208  8 return result;
209    } catch (XWikiException e) {
210  0 throw new IOServiceException("An exception has occurred while loading the annotations", e);
211    }
212    }
213   
 
214  8 toggle @Override
215    public Annotation getAnnotation(String target, String annotationID) throws IOServiceException
216    {
217  8 try {
218  8 if (annotationID == null || target == null) {
219  0 return null;
220    }
221    // parse the target and extract the local reference serialized from it, by the same rules
222  8 EntityReference targetReference = referenceResolver.resolve(target, EntityType.DOCUMENT);
223    // build the target identifier for the annotation
224  8 String localTargetId = target;
225    // and the name of the document where it should be stored
226  8 String docName = target;
227  8 if (targetReference.getType() == EntityType.DOCUMENT
228    || targetReference.getType() == EntityType.OBJECT_PROPERTY) {
229  8 localTargetId = localSerializer.serialize(targetReference);
230  8 docName = serializer.serialize(targetReference.extractReference(EntityType.DOCUMENT));
231    }
232    // get the document
233  8 XWikiContext deprecatedContext = getXWikiContext();
234  8 XWikiDocument document = deprecatedContext.getWiki().getDocument(docName, deprecatedContext);
235    // and the annotation class objects in it
236    // parse the annotation id as object index
237  8 BaseObject object =
238    document.getXObject(configuration.getAnnotationClassReference(),
239    Integer.valueOf(annotationID.toString()));
240  8 if (object == null || !localTargetId.equals(object.getStringValue(Annotation.TARGET_FIELD))) {
241  0 return null;
242    }
243    // use the object number as annotation id
244  8 return loadAnnotationFromObject(object, deprecatedContext);
245    } catch (NumberFormatException e) {
246  0 throw new IOServiceException("Could not parse annotation id " + annotationID, e);
247    } catch (XWikiException e) {
248  0 throw new IOServiceException("An exception has occurred while loading the annotation with id "
249    + annotationID, e);
250    }
251    }
252   
253    /**
254    * {@inheritDoc}
255    * <p>
256    * This implementation deletes the annotation object with the object number indicated by {@code annotationID} from
257    * the document indicated by {@code target}, if its stored target matches the passed target.
258    * </p>
259    *
260    * @see org.xwiki.annotation.io.IOService#removeAnnotation(String, String)
261    */
 
262  4 toggle @Override
263    public void removeAnnotation(String target, String annotationID) throws IOServiceException
264    {
265  4 try {
266  4 if (annotationID == null || target == null) {
267  0 return;
268    }
269   
270  4 EntityReference targetReference = referenceResolver.resolve(target, EntityType.DOCUMENT);
271    // get the target identifier and the document name from the parsed reference
272  4 String localTargetId = target;
273  4 String docName = target;
274  4 if (targetReference.getType() == EntityType.DOCUMENT
275    || targetReference.getType() == EntityType.OBJECT_PROPERTY) {
276  4 localTargetId = localSerializer.serialize(targetReference);
277  4 docName = serializer.serialize(targetReference.extractReference(EntityType.DOCUMENT));
278    }
279    // get the document
280  4 XWikiContext deprecatedContext = getXWikiContext();
281  4 XWikiDocument document = deprecatedContext.getWiki().getDocument(docName, deprecatedContext);
282  4 if (document.isNew()) {
283    // if the document doesn't exist already skip it
284  0 return;
285    }
286    // and the document object on it
287  4 BaseObject annotationObject =
288    document.getXObject(configuration.getAnnotationClassReference(),
289    Integer.valueOf(annotationID.toString()));
290   
291    // if object exists and its target matches the requested target, delete it
292  4 if (annotationObject != null
293    && localTargetId.equals(annotationObject.getStringValue(Annotation.TARGET_FIELD))) {
294  4 document.removeObject(annotationObject);
295  4 document.setAuthor(deprecatedContext.getUser());
296  4 deprecatedContext.getWiki().saveDocument(document, "Deleted annotation " + annotationID,
297    deprecatedContext);
298    }
299    } catch (NumberFormatException e) {
300  0 throw new IOServiceException("An exception has occurred while parsing the annotation id", e);
301    } catch (XWikiException e) {
302  0 throw new IOServiceException("An exception has occurred while removing the annotation", e);
303    }
304    }
305   
306    /**
307    * {@inheritDoc}
308    * <p>
309    * Implementation which gets all the annotation class objects in the document pointed by the target, and matches
310    * their ids against the ids in the passed collection of annotations. If they match, they are updated with the new
311    * data in the annotations in annotation.
312    * </p>
313    *
314    * @see org.xwiki.annotation.io.IOService#updateAnnotations(String, java.util.Collection)
315    */
 
316  0 toggle @Override
317    public void updateAnnotations(String target, Collection<Annotation> annotations) throws IOServiceException
318    {
319  0 try {
320  0 EntityReference targetReference = referenceResolver.resolve(target, EntityType.DOCUMENT);
321    // get the document name from the parsed reference
322  0 String docName = target;
323  0 if (targetReference.getType() == EntityType.DOCUMENT
324    || targetReference.getType() == EntityType.OBJECT_PROPERTY) {
325  0 docName = serializer.serialize(targetReference.extractReference(EntityType.DOCUMENT));
326    }
327    // get the document pointed to by the target
328  0 XWikiContext deprecatedContext = getXWikiContext();
329  0 XWikiDocument document = deprecatedContext.getWiki().getDocument(docName, deprecatedContext);
330  0 List<String> updateNotifs = new ArrayList<String>();
331  0 boolean updated = false;
332  0 for (Annotation annotation : annotations) {
333    // parse annotation id as string. If cannot parse, then ignore annotation, is not valid
334  0 int annId = 0;
335  0 try {
336  0 annId = Integer.parseInt(annotation.getId());
337    } catch (NumberFormatException e) {
338  0 continue;
339    }
340  0 BaseObject object = document.getXObject(configuration.getAnnotationClassReference(), annId);
341  0 if (object == null) {
342  0 continue;
343    }
344  0 updated = updateObject(object, annotation, deprecatedContext) || updated;
345  0 updateNotifs.add(annotation.getId());
346    }
347  0 if (updated) {
348    // set the author of the document to the current user
349  0 document.setAuthor(deprecatedContext.getUser());
350  0 deprecatedContext.getWiki().saveDocument(document, "Updated annotations", deprecatedContext);
351    }
352    } catch (XWikiException e) {
353  0 throw new IOServiceException("An exception has occurred while updating the annotation", e);
354    }
355    }
356   
357    /**
358    * Helper function to load an annotation object from an xwiki object.
359    *
360    * @param object the xwiki object to load an annotation from
361    * @param deprecatedContext XWikiContext to make operations on xwiki data
362    * @return the Annotation instance for the annotation stored in BaseObject
363    */
 
364  28 toggle protected Annotation loadAnnotationFromObject(BaseObject object, XWikiContext deprecatedContext)
365    {
366    // load the annotation with its ID, special handling of the state since it needs deserialization, special
367    // handling of the original selection which shouldn't be set if it's empty
368  28 Annotation annotation = new Annotation(object.getNumber() + "");
369  28 annotation.setState(AnnotationState.valueOf(object.getStringValue(Annotation.STATE_FIELD)));
370  28 String originalSelection = object.getStringValue(Annotation.ORIGINAL_SELECTION_FIELD);
371  28 if (originalSelection != null && originalSelection.length() > 0) {
372  0 annotation.setOriginalSelection(originalSelection);
373    }
374   
375  28 Collection<String> skippedFields =
376    Arrays.asList(new String[] {Annotation.ORIGINAL_SELECTION_FIELD, Annotation.STATE_FIELD});
377    // go through all props and load them in the annotation, except for the ones already loaded
378    // get all the props, filter those that need to be skipped and save the rest
379  28 for (String propName : object.getPropertyNames()) {
380  224 if (!skippedFields.contains(propName)) {
381  196 try {
382  196 annotation.set(propName, ((BaseProperty) object.get(propName)).getValue());
383    } catch (XWikiException e) {
384  0 this.logger.warn("Unable to get property " + propName + " from object " + object.getClassName()
385    + "[" + object.getNumber() + "]. Will not be saved in the annotation.", e);
386    }
387    }
388    }
389  28 return annotation;
390    }
391   
392    /**
393    * Helper function to update object from an annotation.
394    *
395    * @param object the object to update
396    * @param annotation the annotation to marshal in the object
397    * @param deprecatedContext the XWikiContext execute object operations
398    * @return {@code true} if any modification was done on this object, {@code false} otherwise
399    */
 
400  4 toggle protected boolean updateObject(BaseObject object, Annotation annotation, XWikiContext deprecatedContext)
401    {
402  4 boolean updated = false;
403    // TODO: there's an issue here to solve with (custom) types which need to be serialized before saved. Some do,
404    // some don't.... Custom field types in the annotation map should match the types accepted by the object
405    // special handling for state which needs to be string serialized, since the prop in the class is string and the
406    // state is an enum
407  4 updated =
408  4 setIfNotNull(object, Annotation.STATE_FIELD, annotation.getState() == null ? null : annotation.getState()
409    .toString(), deprecatedContext)
410    || updated;
411    // don't reset the state, the date (which will be set now, upon save), and ignore anything that could overwrite
412    // the target. Don't set the author either, will be set by caller, if needed
413  4 Collection<String> skippedFields =
414    Arrays.asList(new String[] {Annotation.STATE_FIELD, Annotation.DATE_FIELD, Annotation.AUTHOR_FIELD,
415    Annotation.TARGET_FIELD});
416    // all fields in the annotation, try to put them in object (I wonder what happens if I can't...)
417  4 for (String propName : annotation.getFieldNames()) {
418  28 if (!skippedFields.contains(propName)) {
419  16 updated = setIfNotNull(object, propName, annotation.get(propName), deprecatedContext) || updated;
420    }
421    }
422   
423  4 return updated;
424    }
425   
426    /**
427    * Helper function to set a field on an object only if the new value is not null. If you wish to reset the value of
428    * a field, pass the empty string for the new value.
429    *
430    * @param object the object to set the value of the field
431    * @param fieldName the name of the field to set
432    * @param newValue the new value to set to the field. It will be ignored if it's {@code null}
433    * @param deprecatedContext the XWikiContext
434    * @return {@code true} if the field was set to newValue, {@code false} otherwise
435    */
 
436  20 toggle protected boolean setIfNotNull(BaseObject object, String fieldName, Object newValue, XWikiContext deprecatedContext)
437    {
438  20 if (newValue != null) {
439  20 object.set(fieldName, newValue, deprecatedContext);
440  20 return true;
441    }
442  0 return false;
443    }
444   
445    /**
446    * @return the deprecated xwiki context used to manipulate xwiki objects
447    */
 
448  25 toggle private XWikiContext getXWikiContext()
449    {
450  25 return (XWikiContext) execution.getContext().getProperty("xwikicontext");
451    }
452    }