/* * See the NOTICE file distributed with this work for additional * information regarding copyright ownership. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package com.xpn.xwiki.objects; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.dom4j.Element; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.xwiki.model.EntityType; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.DocumentReferenceResolver; import org.xwiki.model.reference.EntityReference; import org.xwiki.model.reference.EntityReferenceResolver; import com.xpn.xwiki.XWikiContext; import com.xpn.xwiki.XWikiException; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.doc.merge.MergeConfiguration; import com.xpn.xwiki.doc.merge.MergeResult; import com.xpn.xwiki.objects.classes.BaseClass; import com.xpn.xwiki.objects.classes.PropertyClass; import com.xpn.xwiki.web.Utils; /** * Base class for representing an element having a collection of properties. For example: * <ul> * <li>an XClass definition (composed of XClass properties)</li> * <li>an XObject definition (composed of XObject properties)</li> * <li>an XWikiStats object (composed of stats properties)</li> * </ul> * * @version $Id: e0f2d63c5b9529b88a2abd231315bc6d20e66d1c $ */ public abstract class BaseCollection<R extends EntityReference> extends BaseElement<R> implements ObjectInterface, Cloneable { private static final Logger LOGGER = LoggerFactory.getLogger(BaseCollection.class); /** * The meaning of this reference fields depends on the element represented. Examples: * <ul> * <li>If this BaseCollection instance represents an XObject then refers to the document where the XObject's XClass * is defined.</li> * <li>If this BaseCollection instance represents an XClass then it's not used.</li> * </ul> */ private EntityReference xClassReference; /** * Cache the XClass reference resolved as an absolute reference for improved performance (so that we don't have to * resolve the relative reference every time getXClassReference() is called. */ private DocumentReference xClassReferenceCache; /** * List of properties (eg XClass properties, XObject properties, etc). */ protected Map<String, Object> fields = new LinkedHashMap<String, Object>(); protected List<Object> fieldsToRemove = new ArrayList<>(); /** * The meaning of this reference fields depends on the element represented. Examples: * <ul> * <li>When the BaseCollection represents an XObject, this number is the position of this XObject in the document * where it's located. The first XObject of a given XClass type is at position 0, and other XObject of the same * XClass type are at position 1, etc.</li> * </ul> */ protected int number; /** * Used to resolve XClass references in the way they are stored externally (database, xml, etc), ie relative or * absolute. */ protected EntityReferenceResolver<String> relativeEntityReferenceResolver; /** * Used to normalize references. */ protected DocumentReferenceResolver<EntityReference> currentReferenceDocumentReferenceResolver; /** * @return the component used to resolve XClass references in the way they are stored externally (database, xml, * etc), ie relative or absolute */ protected EntityReferenceResolver<String> getRelativeEntityReferenceResolver() { if (this.relativeEntityReferenceResolver == null) { this.relativeEntityReferenceResolver = Utils.getComponent(EntityReferenceResolver.TYPE_STRING, "relative"); } return this.relativeEntityReferenceResolver; } /** * @return the component used to normalize references */ protected DocumentReferenceResolver<EntityReference> getCurrentReferenceDocumentReferenceResolver() { if (this.currentReferenceDocumentReferenceResolver == null) { this.currentReferenceDocumentReferenceResolver = Utils.getComponent(DocumentReferenceResolver.TYPE_REFERENCE, "current"); } return this.currentReferenceDocumentReferenceResolver; } public int getNumber() { return this.number; } public void setNumber(int number) { this.number = number; } /** * Marks a field as scheduled for removal when saving this entity. Should only be used internally, use * {@link #removeField(String)} to actually remove a field. * * @param field the field to remove, must belong to this entity * @see #removeField(String) */ public void addPropertyForRemoval(PropertyInterface field) { getFieldsToRemove().add(field); } /** * Get the absolute reference of the XClass. * * @since 2.2M2 */ public DocumentReference getXClassReference() { if (this.xClassReferenceCache == null && getRelativeXClassReference() != null) { this.xClassReferenceCache = getCurrentReferenceDocumentReferenceResolver() .resolve(getRelativeXClassReference(), getDocumentReference()); } return this.xClassReferenceCache; } /** * Get the actual reference to the XClass as stored in this instance. * * @since 4.0M2 */ public EntityReference getRelativeXClassReference() { return this.xClassReference; } /** * Note that this method cannot be removed for now since it's used by Hibernate for saving an XObject. * * @deprecated since 2.2M2 use {@link #getXClassReference()} instead */ @Deprecated public String getClassName() { String xClassAsString; if (getXClassReference() != null) { xClassAsString = getLocalEntityReferenceSerializer().serialize(getXClassReference()); } else { xClassAsString = ""; } return xClassAsString; } /** * Set the reference to the XClass (used for an XObject). * <p> * Note that absolute reference are not supported for xclasses which mean that the wiki part (whatever the wiki is) * of the reference will be systematically removed. * * @param xClassReference the reference to the XClass of this XObject. * @since 2.2.3 */ public void setXClassReference(EntityReference xClassReference) { // Ensure that the reference to the XClass is always relative to the document wiki. EntityReference ref = xClassReference; if (ref != null) { EntityReference wiki = xClassReference.extractReference(EntityType.WIKI); if (wiki != null) { ref = xClassReference.removeParent(wiki); } } this.xClassReference = ref; this.xClassReferenceCache = null; } /** * Note that this method cannot be removed for now since it's used by Hibernate for loading an XObject. * * @deprecated since 2.2.3 use {@link #setXClassReference(EntityReference)} ()} instead */ @Deprecated public void setClassName(String name) { EntityReference xClassReference = null; if (!StringUtils.isEmpty(name)) { // Handle backward compatibility: In the past, for statistics objects we used to use a special class name // of "internal". We now check for a null Class Reference instead wherever we were previously checking for // "internal". if (!"internal".equals(name)) { xClassReference = getRelativeEntityReferenceResolver().resolve(name, EntityType.DOCUMENT); } } setXClassReference(xClassReference); } @Override public PropertyInterface safeget(String name) { return (PropertyInterface) getFields().get(name); } @Override public PropertyInterface get(String name) throws XWikiException { return safeget(name); } @Override public void safeput(String name, PropertyInterface property) { addField(name, property); if (property instanceof BaseProperty) { ((BaseProperty) property).setObject(this); ((BaseProperty) property).setName(name); } } @Override public void put(String name, PropertyInterface property) throws XWikiException { safeput(name, property); } /** * @since 2.2M1 */ @Override public BaseClass getXClass(XWikiContext context) { BaseClass baseClass = null; if ((context == null) || (context.getWiki() == null)) { return baseClass; } DocumentReference classReference = getXClassReference(); if (classReference != null) { try { baseClass = context.getWiki().getXClass(classReference, context); } catch (Exception e) { LOGGER.error("Failed to get class [" + classReference + "]", e); } } return baseClass; } public String getStringValue(String name) { BaseProperty prop = (BaseProperty) safeget(name); if (prop == null || prop.getValue() == null) { return ""; } else { return prop.getValue().toString(); } } public String getLargeStringValue(String name) { return getStringValue(name); } public void setStringValue(String name, String value) { BaseStringProperty property = (BaseStringProperty) safeget(name); if (property == null) { property = new StringProperty(); } property.setName(name); property.setValue(value); safeput(name, property); } public void setLargeStringValue(String name, String value) { BaseStringProperty property = (BaseStringProperty) safeget(name); if (property == null) { property = new LargeStringProperty(); } property.setName(name); property.setValue(value); safeput(name, property); } public int getIntValue(String name) { return getIntValue(name, 0); } public int getIntValue(String name, int default_value) { try { NumberProperty prop = (NumberProperty) safeget(name); if (prop == null) { return default_value; } else { return ((Number) prop.getValue()).intValue(); } } catch (Exception e) { return default_value; } } public void setIntValue(String name, int value) { NumberProperty property = new IntegerProperty(); property.setName(name); property.setValue(value); safeput(name, property); } public long getLongValue(String name) { try { NumberProperty prop = (NumberProperty) safeget(name); if (prop == null) { return 0; } else { return ((Number) prop.getValue()).longValue(); } } catch (Exception e) { return 0; } } public void setLongValue(String name, long value) { NumberProperty property = new LongProperty(); property.setName(name); property.setValue(value); safeput(name, property); } public float getFloatValue(String name) { try { NumberProperty prop = (NumberProperty) safeget(name); if (prop == null) { return 0; } else { return ((Number) prop.getValue()).floatValue(); } } catch (Exception e) { return 0; } } public void setFloatValue(String name, float value) { NumberProperty property = new FloatProperty(); property.setName(name); property.setValue(Float.valueOf(value)); safeput(name, property); } public double getDoubleValue(String name) { try { NumberProperty prop = (NumberProperty) safeget(name); if (prop == null) { return 0; } else { return ((Number) prop.getValue()).doubleValue(); } } catch (Exception e) { return 0; } } public void setDoubleValue(String name, double value) { NumberProperty property = new DoubleProperty(); property.setName(name); property.setValue(Double.valueOf(value)); safeput(name, property); } public Date getDateValue(String name) { try { DateProperty prop = (DateProperty) safeget(name); if (prop == null) { return null; } else { return (Date) prop.getValue(); } } catch (Exception e) { return null; } } public void setDateValue(String name, Date value) { DateProperty property = new DateProperty(); property.setName(name); property.setValue(value); safeput(name, property); } public Set<?> getSetValue(String name) { ListProperty prop = (ListProperty) safeget(name); if (prop == null) { return new HashSet<Object>(); } else { return new HashSet<Object>((Collection<?>) prop.getValue()); } } public void setSetValue(String name, Set<?> value) { ListProperty property = new ListProperty(); property.setValue(value); safeput(name, property); } public List getListValue(String name) { ListProperty prop = (ListProperty) safeget(name); if (prop == null) { return new ArrayList(); } else { return (List) prop.getValue(); } } public void setStringListValue(String name, List value) { ListProperty property = (ListProperty) safeget(name); if (property == null) { property = new StringListProperty(); } property.setValue(value); safeput(name, property); } public void setDBStringListValue(String name, List value) { ListProperty property = (ListProperty) safeget(name); if (property == null) { property = new DBStringListProperty(); } property.setValue(value); safeput(name, property); } // These functions should not be used // but instead our own implementation private Map<String, Object> getFields() { return this.fields; } public void setFields(Map fields) { this.fields = fields; } public PropertyInterface getField(String name) { return (PropertyInterface) this.fields.get(name); } public void addField(String name, PropertyInterface element) { this.fields.put(name, element); if (element instanceof BaseElement) { ((BaseElement) element).setOwnerDocument(getOwnerDocument()); } } public void removeField(String name) { Object field = safeget(name); if (field != null) { this.fields.remove(name); this.fieldsToRemove.add(field); } } public Collection getFieldList() { return this.fields.values(); } public Set<String> getPropertyList() { return this.fields.keySet(); } public Object[] getProperties() { return getFields().values().toArray(); } public String[] getPropertyNames() { return getFields().keySet().toArray(new String[0]); } /** * Return an iterator that will operate on a collection of values (as would be returned by getProperties or * getFieldList) sorted by their name (ElementInterface.getName()). */ public Iterator getSortedIterator() { Iterator it = null; try { // Use getProperties to get the values in list form (rather than as generic collection) List propList = Arrays.asList(getProperties()); // Use the element comparator to sort the properties by name (based on ElementInterface) Collections.sort(propList, new ElementComparator()); // Iterate over the sorted property list it = propList.iterator(); } catch (ClassCastException ccex) { // If sorting by the comparator resulted in a ClassCastException (possible), // iterate over the generic collection of values. it = getFieldList().iterator(); } return it; } @Override public boolean equals(Object coll) { // Same Java object, they sure are equal if (this == coll) { return true; } if (!super.equals(coll)) { return false; } BaseCollection collection = (BaseCollection) coll; if (collection.getXClassReference() == null) { if (getXClassReference() != null) { return false; } } else if (!collection.getXClassReference().equals(getXClassReference())) { return false; } if (getFields().size() != collection.getFields().size()) { return false; } for (Map.Entry<String, Object> entry : getFields().entrySet()) { Object prop = entry.getValue(); Object prop2 = collection.getFields().get(entry.getKey()); if (!prop.equals(prop2)) { return false; } } return true; } @Override public BaseCollection clone() { BaseCollection collection = (BaseCollection) super.clone(); collection.setXClassReference(getRelativeXClassReference()); collection.setNumber(getNumber()); Map fields = getFields(); Map cfields = new HashMap(); for (Object objEntry : fields.entrySet()) { Map.Entry entry = (Map.Entry) objEntry; PropertyInterface prop = (PropertyInterface) ((BaseElement) entry.getValue()).clone(); prop.setObject(collection); cfields.put(entry.getKey(), prop); } collection.setFields(cfields); return collection; } public void merge(BaseObject object) { Iterator itfields = object.getPropertyList().iterator(); while (itfields.hasNext()) { String name = (String) itfields.next(); if (safeget(name) == null) { safeput(name, (PropertyInterface) ((BaseElement) object.safeget(name)).clone()); } } } public List<ObjectDiff> getDiff(Object oldObject, XWikiContext context) { ArrayList<ObjectDiff> difflist = new ArrayList<ObjectDiff>(); BaseCollection oldCollection = (BaseCollection) oldObject; // Iterate over the new properties first, to handle changed and added objects for (Object key : this.getFields().keySet()) { String propertyName = (String) key; BaseProperty newProperty = (BaseProperty) this.getFields().get(propertyName); BaseProperty oldProperty = (BaseProperty) oldCollection.getFields().get(propertyName); BaseClass bclass = getXClass(context); PropertyClass pclass = (PropertyClass) ((bclass == null) ? null : bclass.getField(propertyName)); String propertyType = (pclass == null) ? "" : pclass.getClassType(); if (oldProperty == null) { // The property exist in the new object, but not in the old one if ((newProperty != null) && (!newProperty.toText().equals(""))) { if (pclass != null) { String newPropertyValue = (newProperty.getValue() instanceof String) ? newProperty.toText() : pclass.displayView(propertyName, this, context); difflist.add(new ObjectDiff(getXClassReference(), getNumber(), "", ObjectDiff.ACTION_PROPERTYADDED, propertyName, propertyType, "", newPropertyValue)); } } } else if (!oldProperty.toText().equals(((newProperty == null) ? "" : newProperty.toText()))) { // The property exists in both objects and is different if (pclass != null) { // Put the values as they would be displayed in the interface String newPropertyValue = (newProperty.getValue() instanceof String) ? newProperty.toText() : pclass.displayView(propertyName, this, context); String oldPropertyValue = (oldProperty.getValue() instanceof String) ? oldProperty.toText() : pclass.displayView(propertyName, oldCollection, context); difflist .add(new ObjectDiff(getXClassReference(), getNumber(), "", ObjectDiff.ACTION_PROPERTYCHANGED, propertyName, propertyType, oldPropertyValue, newPropertyValue)); } else { // Cannot get property definition, so use the plain value difflist .add(new ObjectDiff(getXClassReference(), getNumber(), "", ObjectDiff.ACTION_PROPERTYCHANGED, propertyName, propertyType, oldProperty.toText(), newProperty.toText())); } } } // Iterate over the old properties, in case there are some removed properties for (Object key : oldCollection.getFields().keySet()) { String propertyName = (String) key; BaseProperty newProperty = (BaseProperty) this.getFields().get(propertyName); BaseProperty oldProperty = (BaseProperty) oldCollection.getFields().get(propertyName); BaseClass bclass = getXClass(context); PropertyClass pclass = (PropertyClass) ((bclass == null) ? null : bclass.getField(propertyName)); String propertyType = (pclass == null) ? "" : pclass.getClassType(); if (newProperty == null) { // The property exists in the old object, but not in the new one if ((oldProperty != null) && (!oldProperty.toText().equals(""))) { if (pclass != null) { // Put the values as they would be displayed in the interface String oldPropertyValue = (oldProperty.getValue() instanceof String) ? oldProperty.toText() : pclass.displayView(propertyName, oldCollection, context); difflist.add(new ObjectDiff(oldCollection.getXClassReference(), oldCollection.getNumber(), "", ObjectDiff.ACTION_PROPERTYREMOVED, propertyName, propertyType, oldPropertyValue, "")); } else { // Cannot get property definition, so use the plain value difflist.add(new ObjectDiff(oldCollection.getXClassReference(), oldCollection.getNumber(), "", ObjectDiff.ACTION_PROPERTYREMOVED, propertyName, propertyType, oldProperty.toText(), "")); } } } } return difflist; } public List getFieldsToRemove() { return this.fieldsToRemove; } public void setFieldsToRemove(List fieldsToRemove) { this.fieldsToRemove = fieldsToRemove; } @Override public Element toXML() { return super.toXML(); } /** * @deprecated since 9.0RC1, use {@link #toXML()} instead */ @Override @Deprecated public Element toXML(BaseClass bclass) { // Set passed class in the context so that the input event generator finds it XWikiContext xcontext = getXWikiContext(); BaseClass currentBaseClass; DocumentReference classReference; if (bclass != null && xcontext != null) { classReference = bclass.getDocumentReference(); currentBaseClass = xcontext.getBaseClass(bclass.getDocumentReference()); xcontext.addBaseClass(bclass); } else { classReference = null; currentBaseClass = null; } try { return super.toXML(); } finally { if (classReference != null) { if (currentBaseClass != null) { xcontext.addBaseClass(currentBaseClass); } else { xcontext.removeBaseClass(classReference); } } } } /** * Return a XML version of this collection. * <p> * The XML is not formated. to get formatted XML you can use {@link #toXMLString(boolean)} instead. * * @return the XML as a String */ public String toXMLString() { return toXMLString(true); } /** * @since 2.4M2 */ public Map<String, Object> getCustomMappingMap() throws XWikiException { Map<String, Object> map = new HashMap<String, Object>(); for (String name : this.fields.keySet()) { BaseProperty property = (BaseProperty) get(name); map.put(name, property.getCustomMappingValue()); } map.put("id", getId()); return map; } @Override public void setDocumentReference(DocumentReference reference) { super.setDocumentReference(reference); // We force to refresh the XClass reference so that next time it's retrieved again it'll be resolved against // the new document reference. this.xClassReferenceCache = null; } @Override public void merge(ElementInterface previousElement, ElementInterface newElement, MergeConfiguration configuration, XWikiContext context, MergeResult mergeResult) { BaseCollection<R> previousCollection = (BaseCollection<R>) previousElement; BaseCollection<R> newCollection = (BaseCollection<R>) newElement; List<ObjectDiff> classDiff = newCollection.getDiff(previousCollection, context); for (ObjectDiff diff : classDiff) { PropertyInterface propertyResult = getField(diff.getPropName()); PropertyInterface previousProperty = previousCollection.getField(diff.getPropName()); PropertyInterface newProperty = newCollection.getField(diff.getPropName()); if (diff.getAction() == ObjectDiff.ACTION_PROPERTYADDED) { if (propertyResult == null) { // Add if none has been added by user already safeput(diff.getPropName(), configuration.isProvidedVersionsModifiables() ? newProperty : newProperty.clone()); mergeResult.setModified(true); } else if (!propertyResult.equals(newProperty)) { // collision between DB and new: property to add but already exists in the DB mergeResult.getLog().error("Collision found on property [{}]", newProperty.getReference()); } } else if (diff.getAction() == ObjectDiff.ACTION_PROPERTYREMOVED) { if (propertyResult != null) { if (propertyResult.equals(previousProperty)) { // Delete if it's the same as previous one removeField(diff.getPropName()); mergeResult.setModified(true); } else { // collision between DB and new: property to remove but not the same as previous // version mergeResult.getLog().error("Collision found on property [{}]", previousProperty.getReference()); } } else { // Already removed from DB, lets assume the user is prescient mergeResult.getLog().warn("Property [{}] already removed", previousProperty.getReference()); } } else if (diff.getAction() == ObjectDiff.ACTION_PROPERTYCHANGED) { if (propertyResult != null) { if (propertyResult.equals(previousProperty)) { // Let some automatic migration take care of that modification between DB and new safeput(diff.getPropName(), configuration.isProvidedVersionsModifiables() ? newProperty : newProperty.clone()); mergeResult.setModified(true); } else if (!propertyResult.equals(newProperty)) { // Try to apply 3 ways merge on the property mergeField(propertyResult, previousProperty, newProperty, configuration, context, mergeResult); } } else { // collision between DB and new: property to modify but does not exists in DB // Lets assume it's a mistake to fix mergeResult.getLog().warn("Collision found on property [{}]", newProperty.getReference()); safeput(diff.getPropName(), configuration.isProvidedVersionsModifiables() ? newProperty : newProperty.clone()); mergeResult.setModified(true); } } } } protected void mergeField(PropertyInterface currentElement, ElementInterface previousElement, ElementInterface newElement, MergeConfiguration configuration, XWikiContext context, MergeResult mergeResult) { currentElement.merge(previousElement, newElement, configuration, context, mergeResult); } @Override public boolean apply(ElementInterface newElement, boolean clean) { boolean modified = false; BaseCollection<R> newCollection = (BaseCollection<R>) newElement; if (clean) { // Delete fields that don't exist anymore List<String> fieldsToDelete = new ArrayList<String>(this.fields.size()); for (String key : this.fields.keySet()) { if (newCollection.safeget(key) == null) { fieldsToDelete.add(key); } } for (String key : fieldsToDelete) { removeField(key); modified = true; } } // Add new fields and update existing fields for (Map.Entry<String, Object> entry : newCollection.fields.entrySet()) { PropertyInterface field = (PropertyInterface) this.fields.get(entry.getKey()); PropertyInterface newField = (PropertyInterface) entry.getValue(); if (field == null) { // If the field does not exist add it safeput(entry.getKey(), newField); modified = true; } else if (field.getClass() != newField.getClass()) { // If the field is of different type, remove it first removeField(entry.getKey()); safeput(entry.getKey(), newField); modified = true; } else { // Otherwise try to merge the fields modified |= field.apply(newField, clean); } } return modified; } /** * Set the owner document of this base object. * * @param ownerDocument The owner document. * @since 5.3M1 */ @Override public void setOwnerDocument(XWikiDocument ownerDocument) { super.setOwnerDocument(ownerDocument); for (String propertyName : getPropertyList()) { PropertyInterface property = getField(propertyName); if (property instanceof BaseElement) { ((BaseElement) property).setOwnerDocument(ownerDocument); } } } }