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

File R40001XWIKI7540DataMigration.java

 

Coverage histogram

../../../../../../../img/srcFileCovDistChart1.png
82% of files have more coverage

Code metrics

32
110
12
3
444
259
31
0.28
9.17
4
2.58

Classes

Class Line # Actions
R40001XWIKI7540DataMigration 74 30 0% 8 38
0.055%
R40001XWIKI7540DataMigration.GetWorkToBeDoneHibernateCallback 194 23 0% 6 31
0.00%
R40001XWIKI7540DataMigration.DoWorkOnDocumentHibernateCallback 259 57 0% 17 83
0.00%
 

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.migration.hibernate;
21   
22    import java.util.ArrayList;
23    import java.util.Collections;
24    import java.util.Comparator;
25    import java.util.Date;
26    import java.util.HashMap;
27    import java.util.List;
28    import java.util.Map;
29    import java.util.Map.Entry;
30   
31    import javax.inject.Inject;
32    import javax.inject.Named;
33    import javax.inject.Singleton;
34   
35    import org.hibernate.HibernateException;
36    import org.hibernate.Query;
37    import org.hibernate.Session;
38    import org.slf4j.Logger;
39    import org.xwiki.annotation.AnnotationConfiguration;
40    import org.xwiki.component.annotation.Component;
41    import org.xwiki.model.EntityType;
42    import org.xwiki.model.reference.DocumentReference;
43    import org.xwiki.model.reference.EntityReference;
44    import org.xwiki.model.reference.EntityReferenceSerializer;
45    import org.xwiki.model.reference.WikiReference;
46   
47    import com.xpn.xwiki.XWikiContext;
48    import com.xpn.xwiki.XWikiException;
49    import com.xpn.xwiki.objects.BaseObject;
50    import com.xpn.xwiki.objects.BaseProperty;
51    import com.xpn.xwiki.objects.DateProperty;
52    import com.xpn.xwiki.objects.LargeStringProperty;
53    import com.xpn.xwiki.objects.StringListProperty;
54    import com.xpn.xwiki.objects.StringProperty;
55    import com.xpn.xwiki.objects.classes.BaseClass;
56    import com.xpn.xwiki.store.XWikiHibernateBaseStore.HibernateCallback;
57    import com.xpn.xwiki.store.migration.DataMigrationException;
58    import com.xpn.xwiki.store.migration.XWikiDBVersion;
59    import com.xpn.xwiki.store.migration.hibernate.AbstractHibernateDataMigration;
60   
61    /**
62    * Migration for XWIKI7540: Merging Annotations with Comments requires some extra fields to the XWiki.XWikiComments
63    * class. These fields come from AnnotationCode.AnnotationClass and any existing annotations objects (that are using
64    * AnnotationClass) need to be converted to use the updated XWikiComments class instead. Also, all the comments in a
65    * document that was modified by this migration need to be sorted by date and given new object numbers so that the
66    * comments order is not affected.
67    *
68    * @version $Id: 2c74374d94f69c5ce3db6ed1d544282c729bbdb1 $
69    * @since 4.0M2
70    */
71    @Component
72    @Named("R40001XWIKI7540")
73    @Singleton
 
74    public class R40001XWIKI7540DataMigration extends AbstractHibernateDataMigration
75    {
76    /** The comment class reference. */
77    private static final EntityReference XWIKI_COMMENT_CLASS_REFERENCE = new EntityReference("XWikiComments",
78    EntityType.DOCUMENT, new EntityReference("XWiki", EntityType.SPACE));
79   
80    /** The annotation class reference. */
81    private static final EntityReference XWIKI_ANNOTATION_CLASS_REFERENCE = new EntityReference("AnnotationClass",
82    EntityType.DOCUMENT, new EntityReference("AnnotationCode", EntityType.SPACE));
83   
84    /** Everybody logs... sometimes. */
85    @Inject
86    protected Logger logger;
87   
88    /** Used to serialize document references when logging. */
89    @Inject
90    protected EntityReferenceSerializer<String> referenceSerializer;
91   
92    /** Used to determine the current annotation class. */
93    @Inject
94    protected AnnotationConfiguration configuration;
95   
96    /** Holds the work to be done by grouping datedComments by documents. */
97    protected Map<DocumentReference, List<Entry<Date, BaseObject>>> documentToDatedObjectsMap =
98    new HashMap<DocumentReference, List<Entry<Date, BaseObject>>>();
99   
100    /** Holds the work to be done by grouping properties by the objects to which they belong. */
101    protected Map<BaseObject, List<BaseProperty>> objectToPropertiesMap = new HashMap<BaseObject, List<BaseProperty>>();
102   
 
103  0 toggle @Override
104    public String getDescription()
105    {
106  0 return "See http://jira.xwiki.org/browse/XWIKI-7540";
107    }
108   
 
109  3 toggle @Override
110    public XWikiDBVersion getVersion()
111    {
112    // XWiki 4.0, second migration.
113  3 return new XWikiDBVersion(40001);
114    }
115   
116    /**
117    * Check if the migration can be executed by verifying if the current annotation class is the default one and that
118    * the comments class does not have any custom mappings set up.
119    * <p>
120    * Note: We can not do this in {@link #shouldExecute(XWikiDBVersion)} because we need to read the database and we
121    * can not do that until the previous migrations are executed.
122    *
123    * @return true if the migration can be executed, false otherwise.
124    * @throws DataMigrationException if the annotation or comments class can not be properly retrieved
125    */
 
126  0 toggle protected boolean checkAnnotationsAndComments() throws DataMigrationException
127    {
128  0 XWikiContext context = getXWikiContext();
129  0 String resultOfSkippingDatabase = "Comments and anotations will remain separated";
130   
131  0 try {
132  0 EntityReference currentAnnotationClassReference = configuration.getAnnotationClassReference();
133  0 currentAnnotationClassReference =
134    currentAnnotationClassReference.removeParent(new WikiReference(context.getWikiId()));
135  0 if (!XWIKI_ANNOTATION_CLASS_REFERENCE.equals(currentAnnotationClassReference)) {
136  0 logger.warn("Skipping database [{}] because it uses a custom annotation class. "
137    + resultOfSkippingDatabase, context.getWikiId());
138  0 return false;
139    }
140   
141  0 BaseClass commentsClass = context.getWiki().getCommentsClass(context);
142  0 if (commentsClass.hasCustomMapping()) {
143  0 logger.warn("Skipping database [{}] because it uses a custom mapping for comments. "
144    + resultOfSkippingDatabase, context.getWikiId());
145  0 return false;
146    }
147    } catch (Exception e) {
148    // Should not happen
149  0 String message =
150    "Failed to check the current annotation and comments classes for customizations. "
151    + "Migration will not execute";
152  0 logger.error(message, e);
153  0 throw new DataMigrationException(message, e);
154    }
155   
156  0 return true;
157    }
158   
 
159  0 toggle @Override
160    protected void hibernateMigrate() throws DataMigrationException, XWikiException
161    {
162    // Check if the migration can be executed.
163  0 if (!checkAnnotationsAndComments()) {
164  0 return;
165    }
166   
167    // Clear any existing migration data/cache from previously migrated wikis.
168  0 documentToDatedObjectsMap.clear();
169  0 objectToPropertiesMap.clear();
170   
171  0 logger.info("Computing the work to be done.");
172   
173    // 1st step: populate the 2 maps with the work to be done.
174  0 getStore().executeRead(getXWikiContext(), true, new GetWorkToBeDoneHibernateCallback());
175   
176  0 logger.info("There is a total of {} documents to migrate.", documentToDatedObjectsMap.keySet().size());
177   
178    // 2nd step: for each document, delete the old objects and create new (updated) ones. One transaction per
179    // document.
180  0 DoWorkOnDocumentHibernateCallback doWorkOnDocumentHibernateCallback = new DoWorkOnDocumentHibernateCallback();
181  0 for (DocumentReference documentReference : documentToDatedObjectsMap.keySet()) {
182  0 logger.info("Migrating document [{}]", referenceSerializer.serialize(documentReference, (Object[]) null));
183   
184  0 doWorkOnDocumentHibernateCallback.setDocumentReference(documentReference);
185  0 getStore().executeWrite(getXWikiContext(), true, doWorkOnDocumentHibernateCallback);
186    }
187    }
188   
189    /**
190    * Inner class that retrieves the documents, objects and properties that need to be migrated.
191    *
192    * @version $Id: 2c74374d94f69c5ce3db6ed1d544282c729bbdb1 $
193    */
 
194    private class GetWorkToBeDoneHibernateCallback implements HibernateCallback<Object>
195    {
 
196  0 toggle @Override
197    public Object doInHibernate(Session session) throws HibernateException, XWikiException
198    {
199  0 try {
200    // Get all annotation object and comment object with every property that they have. Do this only for
201    // documents that have annotation objects in them, and thus need to be migrated.
202  0 Query getExistingAnnotationsAndCommentsQuery =
203    session.createQuery("SELECT obj, prop FROM BaseObject obj, BaseProperty prop WHERE "
204    + "(obj.className='AnnotationCode.AnnotationClass' OR obj.className='XWiki.XWikiComments') "
205    + "AND prop.id.id=obj.id AND obj.name in "
206    + "(SELECT doc.fullName FROM XWikiDocument doc, BaseObject ann WHERE "
207    + "ann.name=doc.fullName AND ann.className='AnnotationCode.AnnotationClass')");
208  0 List<Object[]> queryResults = (List<Object[]>) getExistingAnnotationsAndCommentsQuery.list();
209   
210  0 preProcessResults(queryResults);
211    } catch (Exception e) {
212  0 throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.ERROR_XWIKI_STORE_MIGRATION,
213    getName() + " failed to read the work to be done.", e);
214    }
215   
216  0 return Boolean.TRUE;
217    }
218   
219    /**
220    * Pre-process the results into data structures that can be easily worked with.
221    *
222    * @param queryResults an array containing multiple [comment, property] arrays
223    * @throws HibernateException if underlying Hibernate operations fail
224    */
 
225  0 toggle private void preProcessResults(List<Object[]> queryResults) throws HibernateException
226    {
227  0 for (Object[] queryResult : queryResults) {
228  0 BaseObject object = (BaseObject) queryResult[0];
229  0 BaseProperty property = (BaseProperty) queryResult[1];
230  0 DocumentReference documentReference = object.getDocumentReference();
231   
232  0 if (property instanceof DateProperty) {
233  0 List<Entry<Date, BaseObject>> datedObjects = documentToDatedObjectsMap.get(documentReference);
234  0 if (datedObjects == null) {
235  0 datedObjects = new ArrayList<Map.Entry<Date, BaseObject>>();
236  0 documentToDatedObjectsMap.put(documentReference, datedObjects);
237    }
238   
239  0 Date date = (Date) ((DateProperty) property).getValue();
240  0 Entry<Date, BaseObject> datedObject = new HashMap.SimpleEntry<Date, BaseObject>(date, object);
241  0 datedObjects.add(datedObject);
242    }
243   
244  0 List<BaseProperty> properties = objectToPropertiesMap.get(object);
245  0 if (properties == null) {
246  0 properties = new ArrayList<BaseProperty>();
247  0 objectToPropertiesMap.put(object, properties);
248    }
249  0 properties.add(property);
250    }
251    }
252    }
253   
254    /**
255    * Inner class that is in charge of migrating each document.
256    *
257    * @version $Id: 2c74374d94f69c5ce3db6ed1d544282c729bbdb1 $
258    */
 
259    private class DoWorkOnDocumentHibernateCallback implements HibernateCallback<Object>
260    {
261    /** @see #setDocumentReference(DocumentReference) */
262    private DocumentReference documentReference;
263   
264    /** @see #getMigratedObject(BaseObject, int) */
265    private Map<BaseObject, BaseObject> oldToNewObjectMap;
266   
267    /** @see #processObjects(Session) */
268    private Map<Integer, Integer> oldToNewCommentNumberMap;
269   
270    /** @param documentReference the document on which to work */
 
271  0 toggle public void setDocumentReference(DocumentReference documentReference)
272    {
273  0 this.documentReference = documentReference;
274  0 this.oldToNewObjectMap = new HashMap<BaseObject, BaseObject>();
275  0 this.oldToNewCommentNumberMap = new HashMap<Integer, Integer>();
276    }
277   
 
278  0 toggle @Override
279    public Object doInHibernate(Session session) throws HibernateException, XWikiException
280    {
281  0 try {
282    // Parse the maps, delete the old objects and properties, and create new (updated) ones that replace
283    // them.
284  0 processObjects(session);
285    } catch (Exception e) {
286  0 throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, XWikiException.ERROR_XWIKI_STORE_MIGRATION,
287    getName() + " failed to do the work for document "
288    + referenceSerializer.serialize(documentReference, (Object[]) null), e);
289    }
290   
291  0 return Boolean.TRUE;
292    }
293   
294    /**
295    * Sort the objects by date and assign new object IDs. For objects that are annotations (using
296    * AnnotationCode.AnnotationClass) convert them to XWiki.XWikiComments.
297    *
298    * @param session the Hibernate Session
299    * @throws HibernateException if underlying Hibernate operations fail
300    */
 
301  0 toggle private void processObjects(Session session) throws HibernateException
302    {
303  0 List<Entry<Date, BaseObject>> datedObjects = documentToDatedObjectsMap.get(documentReference);
304   
305    // Because the changes we need to do are part of the computed object ID, updating the objects and
306    // properties in the session is not possible. Thus, we need to delete from the session all objects and
307    // properties for the current document so that they do not clash IDs with the ones that will replace them
308    // below.
309  0 for (Entry<Date, BaseObject> datedObject : datedObjects) {
310  0 BaseObject object = datedObject.getValue();
311   
312  0 for (BaseProperty property : objectToPropertiesMap.get(object)) {
313  0 session.delete(property);
314    }
315   
316  0 session.delete(object);
317    }
318   
319    // Flush and clear the session to be able to make sure the delete batch is processed before the insertions
320    // batch, avoiding ID collisions in the DB at insert time.
321  0 session.flush();
322  0 session.clear();
323   
324    // Sort the objects by date. The objects were removed from the session but are still available in-memory.
325  0 Collections.sort(datedObjects, new Comparator<Entry<Date, BaseObject>>()
326    {
 
327  0 toggle @Override
328    public int compare(Entry<Date, BaseObject> datedObject1, Entry<Date, BaseObject> datedObject2)
329    {
330  0 return datedObject1.getKey().compareTo(datedObject2.getKey());
331    }
332    });
333   
334    // Reassign object numbers and convert annotations for the current document, based on the previous sorting.
335  0 for (int newObjectNumber = 0; newObjectNumber < datedObjects.size(); newObjectNumber++) {
336  0 BaseObject deletedObject = datedObjects.get(newObjectNumber).getValue();
337   
338  0 BaseObject newComment = getMigratedObject(deletedObject, newObjectNumber);
339   
340    // Only for simple comments, keeps track of converted object numbers. Used when migrating a comment's
341    // "replyto" property.
342  0 if (deletedObject.getRelativeXClassReference().equals(XWIKI_COMMENT_CLASS_REFERENCE)) {
343  0 oldToNewCommentNumberMap.put(deletedObject.getNumber(), newComment.getNumber());
344    }
345   
346    // Remember the corresponding new objects generated in this phase to be used below, when migrating the
347    // properties.
348  0 oldToNewObjectMap.put(deletedObject, newComment);
349   
350  0 session.save(newComment);
351    }
352   
353    // Migrate each of the deleted object's properties and link the new properties to the new objects.
354  0 for (int newObjectNumber = 0; newObjectNumber < datedObjects.size(); newObjectNumber++) {
355  0 BaseObject deletedObject = datedObjects.get(newObjectNumber).getValue();
356   
357    // Use the corresponding new object created above.
358  0 BaseObject newComment = oldToNewObjectMap.get(deletedObject);
359   
360  0 List<BaseProperty> deletedProperties = objectToPropertiesMap.get(deletedObject);
361  0 for (BaseProperty deletedProperty : deletedProperties) {
362  0 BaseProperty newProperty = getMigratedProperty(deletedProperty, newComment);
363   
364  0 session.save(newProperty);
365    }
366    }
367    }
368   
369    /**
370    * @param deletedObject the old object to migrate
371    * @param newObjectNumber the new object ID to assign to the migrated object
372    * @return an in-memory migrated version of the old object
373    */
 
374  0 toggle private BaseObject getMigratedObject(BaseObject deletedObject, int newObjectNumber)
375    {
376    // Clone the deleted object and use the new number.
377  0 BaseObject newObject = deletedObject.clone();
378  0 newObject.setNumber(newObjectNumber);
379   
380    // If the deleted object is an annotation, make sure to use the comments class instead.
381  0 if (deletedObject.getRelativeXClassReference().equals(XWIKI_ANNOTATION_CLASS_REFERENCE)) {
382  0 newObject.setXClassReference(XWIKI_COMMENT_CLASS_REFERENCE);
383    }
384   
385  0 return newObject;
386    }
387   
388    /**
389    * @param deletedProperty the old property to migrate
390    * @param newComment the new comment to which to assign the migrated property
391    * @return an in-memory migrated version of the old property
392    */
 
393  0 toggle private BaseProperty getMigratedProperty(BaseProperty deletedProperty, BaseObject newComment)
394    {
395  0 BaseProperty newProperty = null;
396   
397    // Note: LargeStringProperty instances are a bit special because they share the same table with
398    // StringListProperty and Hibernate gets confused when loading them. The result is that
399    // StringListProperty instances are loaded instead of LargeStringProperty and, since we know our
400    // classes well in this specific migration, we can just create the LargeStringProperty instances
401    // ourselves from the loaded ones. It might be related to http://jira.xwiki.org/browse/XWIKI-4384
402  0 if (deletedProperty instanceof StringListProperty) {
403    // The "author" property was of type User List in AnnotationClass and now it is going to be
404    // String in XWikiComments.
405  0 if ("author".equals(deletedProperty.getName())) {
406  0 newProperty = new StringProperty();
407    } else {
408  0 newProperty = new LargeStringProperty();
409    }
410   
411    // Extract the value as first element in the internal list. If the list has 0 elements, then the value
412    // is null.
413  0 String deletedPropertyValue = null;
414  0 List<String> internalListValue = ((StringListProperty) deletedProperty).getList();
415  0 if (internalListValue.size() != 0) {
416  0 deletedPropertyValue = internalListValue.get(0);
417    }
418   
419  0 newProperty.setValue(deletedPropertyValue);
420  0 newProperty.setName(deletedProperty.getName());
421    } else {
422  0 newProperty = deletedProperty.clone();
423    }
424  0 newProperty.setId(newComment.getId());
425   
426  0 if ("annotation".equals(deletedProperty.getName())) {
427    // If the deleted property was "annotation" (from AnnotationClass), then use the new
428    // property "comment" for (XWikiComments).
429  0 newProperty.setName("comment");
430  0 } else if ("replyto".equals(deletedProperty.getName())) {
431    // XWIKI-7745: We need to handle the fact that the "replyto" property needs to point to the new object
432    // number of the comment it was previously assigned to, since the comment can now have a new number
433    // assigned to it.
434  0 if (deletedProperty.getValue() != null) {
435  0 int oldValue = (Integer) deletedProperty.getValue();
436  0 int newValue = oldToNewCommentNumberMap.get(oldValue);
437   
438  0 newProperty.setValue(newValue);
439    }
440    }
441  0 return newProperty;
442    }
443    }
444    }