Index: core/xwiki-core/src/main/java/com/xpn/xwiki/objects/ListProperty.java =================================================================== --- core/xwiki-core/src/main/java/com/xpn/xwiki/objects/ListProperty.java (revision 23877) +++ core/xwiki-core/src/main/java/com/xpn/xwiki/objects/ListProperty.java (working copy) @@ -145,6 +145,14 @@ return true; } + //The save opperation triggers hibernate to compare the new property to the original + //If the property was gotten by a query like "SELECT prop FROM BaseProperty AS prop WHERE..." + //and the value stored in the db is null, hibernate thinks the original is a BaseProperty, + //thus we may end up comparing a ListProperty to a BaseProperty with value = null. + if (list2 == null && obj.getClass().getName().indexOf(".BaseProperty")!=-1){ + list2 = new ArrayList(); + } + if (list1.size() != list2.size()) { return false; } Index: core/xwiki-core/src/main/java/com/xpn/xwiki/store/XWikiHibernateStore.java =================================================================== --- core/xwiki-core/src/main/java/com/xpn/xwiki/store/XWikiHibernateStore.java (revision 23877) +++ core/xwiki-core/src/main/java/com/xpn/xwiki/store/XWikiHibernateStore.java (working copy) @@ -28,6 +28,8 @@ import java.util.Collections; import java.util.Date; import java.util.HashMap; +import java.util.HashSet; +import java.util.TreeMap; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -620,8 +622,120 @@ saveXWikiDoc(doc, context, true); } - public XWikiDocument loadXWikiDoc(XWikiDocument doc, XWikiContext context) throws XWikiException + /** + * If xwiki.cfg has xwiki.store.hibernate.useExperimentalDocumentLoader=1 + * call {#link @newLoadXWikiDoc(XWikiDocument, XWikiContext)} + * otherwise call {@link #defaultLoadXWikiDoc(XWikiDocument, XWikiContext)} + * + * @param XWikiDocument A mock document which has the fullname set to the name of the document you want to load + * @param XWikiContext + * @return XWikiDocument The same document you sent it (it is a mutator!) with all values filled in from the database. + * @throws XWikiException if there is an error while loading. + */ + public XWikiDocument loadXWikiDoc(XWikiDocument doc, XWikiContext context) + throws XWikiException { + if("1".equals(context.getWiki().Param("xwiki.store.hibernate.useNewDocumentLoader"))){ + return newLoadXWikiDoc(doc, context); + } + return defaultLoadXWikiDoc(doc, context); + } + + /** + * Loads the XWikiDocument then calls {@link loadXWikiObjects(XWikiDocument, XWikiContext)} + * + * @param XWikiDocument A mock document which has the fullname set to the name of the document you want to load + * @param XWikiContext + * @return XWikiDocument The same document you sent it (it is a mutator!) with all values filled in from the database. + * @throws XWikiException if there is an error while loading. + */ + public XWikiDocument newLoadXWikiDoc(XWikiDocument doc, XWikiContext context) + throws XWikiException + { + boolean bTransaction = true; + MonitorPlugin monitor = Util.getMonitorPlugin(context); + try { + // Start monitoring timer + if (monitor != null) { + monitor.startTimer("hibernate"); + } + doc.setStore(this); + checkHibernate(context); + + bTransaction = beginTransaction(bTransaction, context); + Session session = getSession(context); + session.setFlushMode(FlushMode.MANUAL); + + try { + session.load(doc, new Long(doc.getId())); + doc.setDatabase(context.getDatabase()); + doc.setNew(false); + doc.setMostRecent(true); + // Fix for XWIKI-1651 + doc.setDate(new Date(doc.getDate().getTime())); + doc.setCreationDate(new Date(doc.getCreationDate().getTime())); + doc.setContentUpdateDate(new Date(doc.getContentUpdateDate().getTime())); + + // If the syntaxId is not set, default to XWiki Syntax 1.0 + if (StringUtils.isBlank(doc.getSyntaxId())) { + doc.setSyntaxId(XWikiDocument.XWIKI10_SYNTAXID); + } + + } catch (ObjectNotFoundException e) { // No document + doc.setNew(true); + return doc; + } + + // Loading the attachment list + if (doc.hasElement(XWikiDocument.HAS_ATTACHMENTS)) { + loadAttachmentList(doc, context, false); + } + + //get the BaseClass if there is no BaseClass, we store a new BaseClass + //with the name of the document. + BaseClass bclass = new BaseClass(); + bclass.setName(doc.getFullName()); + if (doc.getxWikiClassXML() != null) { + bclass.fromXML(doc.getxWikiClassXML()); + } else if (useClassesTable(false, context)) { + bclass = loadXWikiClass(bclass, context, false); + } + doc.setxWikiClass(bclass); + // Store this class in the context in case of recursive usage of classes + context.addBaseClass(bclass); + + // load the objects and properties. + if (doc.hasElement(XWikiDocument.HAS_OBJECTS)) { + loadXWikiObjects(doc, context, false); + } + + // We need to ensure that the loaded document becomes the original document + doc.setOriginalDocument((XWikiDocument) doc.clone()); + + if(bTransaction){ + endTransaction(context, false, false); + } + } catch (Exception e) { + Object[] args = {doc.getFullName()}; + throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, + XWikiException.ERROR_XWIKI_STORE_HIBERNATE_READING_DOC, + "Exception while reading document {0}", e, args); + }finally{ + if(bTransaction){ + try{ + endTransaction(context, false, false); + }catch(Exception e){} + } + } + // End monitoring timer + if (monitor != null) { + monitor.endTimer("hibernate"); + } + return doc; + } + + public XWikiDocument defaultLoadXWikiDoc(XWikiDocument doc, XWikiContext context) throws XWikiException + { // To change body of implemented methods use Options | File Templates. boolean bTransaction = true; MonitorPlugin monitor = Util.getMonitorPlugin(context); @@ -980,6 +1094,299 @@ } } + /** + * Queries all BaseObjects associated with the given document. + * Puts BaseObjects into Vectors, for each class. + * If custom mapping is turned on, each class is loaded and tested for custom mapping, + * custom mapped objects are then loaded again as value maps, value maps are fed to object's class. + * Then it queries all of the BaseProperties associated with any of those objects. + * Then puts each BaseProperty in to the object it belongs with. + * Finally it assigns objects to doc.xWikiObjects (Mutator) + * + * @param XWikiDocument A mock document which has the fullname set to the name of the document you want to load objects for. + * @param XWikiContext + * @param boolean bTransaction if false, attempt to execute in current transaction. If dynamic custom mapping has to be updated, there is + * no way to do everything in one transaction because a new SessionFactory must be created. If bTransaction==false + * and dynamic custom mapping must be updated, a temporary transaction will be opened and closed in the method. + * @throws XWikiException if there is an error while loading. + */ + public void loadXWikiObjects(XWikiDocument doc, XWikiContext context, boolean bTransaction) + throws XWikiException + { + Map> xWikiObjects = new TreeMap>(); + //place a pointer in the document so we don't have to do it later, we can still modify the map. + doc.setxWikiObjects(xWikiObjects); + //It takes a ridiculously long time to resize a Vector, so we will store the size + //until the end when we will set the sizes of all Vectors and copy in all objects. + Map numberOfObjectsPerClass = new HashMap(); + //must be declared out here for error trapping. + BaseObject thisObject = new BaseObject(); + + try{ + if (bTransaction) { + checkHibernate(context); + bTransaction = beginTransaction(false, context); + } + Session session = getSession(context); + session.setFlushMode(FlushMode.MANUAL); + + //get all objects associated with this document. + Query q = session.createQuery("select obj from BaseObject obj where obj.name = :name order by obj.id"); + q.setText("name", doc.getFullName()); + List objects = q.list(); + if(objects.size() == 0){return;} + + //index through the list of objects and... + List objectIds = new ArrayList(objects.size()); + for(int i=0; i()); + numberOfObjectsPerClass.put(obj.getClassName(), obj.getNumber()+1); + //If the stored number of objects for this class is too small, store a larger number + }else if(numberOfObjectsPerClass.get(obj.getClassName()) <= obj.getNumber()){ + numberOfObjectsPerClass.put(obj.getClassName(), obj.getNumber()+1); + } + } + //Set the sizes of the vectors. + for(String className : xWikiObjects.keySet()){ + xWikiObjects.get(className).setSize(numberOfObjectsPerClass.get(className)); + } + //put the object pointers into the vectors + //we can still manipulate the objects outside though. + for(BaseObject object : objects){ + xWikiObjects.get(object.getClassName()).set(object.getNumber(), object); + } + + //custom mapping stuff + if(context.getWiki().hasCustomMappings()){ + //get the objects' classes + Map classes = new HashMap(); + for(String key : xWikiObjects.keySet()){ + //get the first object of this class so we can determine the class + thisObject = xWikiObjects.get(key).get(0); + //if objects were saved with the first slot empty, it is not our problem. + if(thisObject == null){ + //if every object in the Vector is a null, this line will throw an exception. + for(int a=1; thisObject == null; a++){ + thisObject = xWikiObjects.get(key).get(a); + } + } + //get the class + if (!thisObject.getName().equals(thisObject.getClassName())) { + classes.put(thisObject.getClassName(), thisObject.getxWikiClass(context)); + } else { + //from this document if that is where it resides + classes.put(thisObject.getClassName(), doc.getxWikiClass()); + } + } + + //copy the session, we may need to create a new one and the original will still be called "session" + Session customMappingSession = session; + //have we added any new mappings? this will necessitate starting a new session + boolean newMappings = false; + try{ + //a list of classes which have bad dynamic custom mappings, thiese should not + //block loading of all pages containing objects of the class. + HashSet badlyMappedClasses = new HashSet(); + //dynamic custom mapping stuff + if(context.getWiki().hasDynamicCustomMappings()){ + //inject all custom mappings into configuration first + for(BaseClass thisClass : classes.values()){ + if(thisClass.hasExternalCustomMapping() + && getConfiguration().getClassMapping(thisClass.getName()) == null) + { + if(thisClass.isCustomMappingValid(context)){ + injectCustomMapping(thisClass, context); + newMappings = true; + }else{ + badlyMappedClasses.add(thisClass.getName()); + if(log.isErrorEnabled()){ + log.error("Failed to load object(s) of class "+thisClass.getName()+" due to incorrect dynamic custom mapping."); + } + } + } + } + //if no new mappings were injected, we will continue with the current transaction + if(newMappings){ + setSessionFactory(injectInSessionFactory(getConfiguration())); + //open a temporary session and start a transaction. + customMappingSession = getSessionFactory().openSession(); + customMappingSession.beginTransaction(); + } + } + //this runs whether dynamic custom mapping or not + for(String key : classes.keySet()){ + BaseClass bclass = classes.get(key); + if(bclass.hasCustomMapping() + && !badlyMappedClasses.contains(bclass.getName())) + { + for(BaseObject obj : xWikiObjects.get(key)){ + //load the value map from the database + Object map = customMappingSession.load(bclass.getName(), obj.getId()); + //make the object. + bclass.fromValueMap((Map) map, obj); + } + } + } + }finally{ + if(newMappings){ + customMappingSession.getTransaction().rollback(); + customMappingSession.close(); + } + } + } + //we are all done with custom mapping stuff :) + + + //now get a single list of all properties associated with any one of the above id numbers + q = session.createQuery("select prop from BaseProperty prop where prop.id in (:ids) order by prop.id"); + q.setParameterList("ids", objectIds); + List props = q.list(); + if(props.size() == 0){return;} + +//remove when StringListProperty is mapped to a seperate column------- + q = session.createQuery("select prop.id, prop.classType, prop.value, prop.name from LargeStringProperty prop where prop.id in (:ids)"); + q.setParameterList("ids", objectIds); + List propClassAndValueArrays = q.list(); + HashMap propClassAndValueById = new HashMap(); + for(Object[] propClassAndValueArray : propClassAndValueArrays){ + String[] thisPropClassAndValue = {(String)propClassAndValueArray[1], (String)propClassAndValueArray[2]}; + int id = ((Integer)propClassAndValueArray[0]+(String)propClassAndValueArray[3]).hashCode(); + propClassAndValueById.put(id, thisPropClassAndValue); + } +//-------------------------------------------------------------------- + + //this is one of the slowest ways of sorting unordered data, however + //the data was ordered by the database so it need only be interleaved. + int objectNum = 0; + thisObject = objects.get(0); + int propNum = 0; + BaseProperty thisProp = props.get(0); + //this is to prevent an infinite loop in the event that the db chokes and + //sends us a property which doesn't belong with any of the objects on the list. + BaseProperty objectlessProperty = thisProp; + + while(true){ + //because the data is ordered by id, once we find a property whose id + //doesn't match the id of the current object, we assume we are done adding + //properties to that object and that our property will match the next object. + if(thisProp.getId() == thisObject.getId()){ + +//TODO: Re-map schema so each class is mapped to an individual column. +//This is a hack to make it accept 2 objects mapped to the same db column + if(thisProp.getClassType().equals("com.xpn.xwiki.objects.StringListProperty")){ + int id = (thisProp.getId()+thisProp.getName()).hashCode(); + if(propClassAndValueById.containsKey(id) + && !thisProp.getClassType().equals(propClassAndValueById.get(id)[0])) + { + BaseProperty prop2 = (BaseProperty) Class.forName(propClassAndValueById.get(id)[0]).newInstance(); + prop2.setName(thisProp.getName()); + prop2.setPrettyName(thisProp.getPrettyName()); + prop2.setValue(propClassAndValueById.get(id)[1]); + prop2.setWiki(thisProp.getWiki()); + thisProp = prop2; + } + } +//Remove this when StringListProperty is mapped to it's own column. + + if(thisProp.getValue() == null + && thisProp instanceof BaseStringProperty){ + //Oracle stores "" as null + //Null value strings should be interpreted as "" + thisProp.setValue((Object) ""); + } + + thisProp.setObject(thisObject); + thisObject.safeput(thisProp.getName(), (PropertyInterface) thisProp); + propNum++; + + + //if we run out of properties, we are done. + if(propNum == props.size()){ + + if(log.isDebugEnabled()){ + log.debug("Loading objects from: "+doc.getFullName()); + for(String className : xWikiObjects.keySet()){ + log.debug(" Class: "+className); + for(BaseObject obj: xWikiObjects.get(className)){ + log.debug(" Object: "+obj.getNumber()); + for(Object prop : obj.getProperties()){ + log.debug(" Property: "+((BaseProperty)prop).getName()); + } + } + } + } + return; //this is the end + } + + thisProp = props.get(propNum); + + }else{ + //We are out of properties with matching id, lets move on to the next object. + objectNum++; + + if(objectNum == objects.size()){ + //This should never happen, but if the data it out of order we loop through the + //objects once again (until every property has found a home.) + objectNum = 0; + if(log.isWarnEnabled()){ + log.warn("Object numbers rolled over, this means properties are out of order "+ + "this is very bad for performance and could indicate database corruption. Document: "+ + doc.getFullName()); + } + //we must catch the event of a property which is not associated with any of the objects, otherwise it will + //loop forever trying to find the object to fit this property with. + if(objectlessProperty == thisProp){ + throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, + XWikiException.ERROR_XWIKI_STORE_HIBERNATE_LOADING_OBJECT, + "The property: '"+thisProp.getName()+"' of type: '"+thisProp.getClassType()+"' doesn't belong "+ + "with any of the objects in this document ("+doc.getFullName()+"), yet the db returned it anyway."); + } + objectlessProperty = thisProp; + } + thisObject = objects.get(objectNum); + } + } + + } catch (Exception e) { + Object[] args = {"(failed to get name)", "(failed to get name)", "(failed to get number)"}; + try{args[0] = thisObject.getName(); }catch(Exception f){} + try{args[1] = thisObject.getClass(); }catch(Exception f){} + try{args[2] = Integer.valueOf(thisObject.getNumber()); }catch(Exception f){} + + throw new XWikiException(XWikiException.MODULE_XWIKI_STORE, + XWikiException.ERROR_XWIKI_STORE_HIBERNATE_LOADING_OBJECT, + "Exception while loading object "+args[0]+" of class "+args[1]+" and number "+args[2], e, args); + }finally{ + if(bTransaction){ + try{ + endTransaction(context, false, false); + }catch(Exception e){} + } + } + } + public void loadXWikiObject(BaseObject object, XWikiContext context, boolean bTransaction) throws XWikiException { loadXWikiCollection(object, null, context, bTransaction, false); Index: tools/xwiki-configuration-resources/src/main/resources/xwiki.cfg.vm =================================================================== --- tools/xwiki-configuration-resources/src/main/resources/xwiki.cfg.vm (revision 23877) +++ tools/xwiki-configuration-resources/src/main/resources/xwiki.cfg.vm (working copy) @@ -103,7 +103,11 @@ #-# Add a prefix to all databases names of the wikis in virtual mode and to the wiki name in non virtual mode. # xwiki.db.prefix= +#-# New more scalable document loader. The new loader is faster at loading documents which have lots of objects, +#-# however it is new and complex so it is turned off by default. To turn it on, uncomment the following line and set it to =1 +# xwiki.store.hibernate.useNewDocumentLoader=0 + #--------------------------------------- # Data migrations #