/* * 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 org.xwiki.extension.xar.internal.job.diff; import java.util.List; import java.util.Map; import javax.inject.Inject; import javax.inject.Singleton; import org.xwiki.component.annotation.Component; import org.xwiki.diff.display.UnifiedDiffBlock; import org.xwiki.extension.xar.job.diff.DocumentUnifiedDiff; import org.xwiki.extension.xar.job.diff.DocumentVersionReference; import org.xwiki.extension.xar.job.diff.EntityUnifiedDiff; import org.xwiki.model.reference.ClassPropertyReference; import org.xwiki.model.reference.DocumentReference; import org.xwiki.model.reference.ObjectReference; import com.xpn.xwiki.doc.XWikiAttachment; import com.xpn.xwiki.doc.XWikiDocument; import com.xpn.xwiki.objects.BaseCollection; import com.xpn.xwiki.objects.BaseObject; import com.xpn.xwiki.objects.BaseObjectReference; import com.xpn.xwiki.objects.BaseProperty; import com.xpn.xwiki.objects.classes.BaseClass; import com.xpn.xwiki.objects.classes.PropertyClass; /** * Computes the differences, in unified format, between two versions of a document. * * @version $Id: ce38ea019c5dfca554fe98b413824cf432533662 $ * @since 7.0RC1 */ @Component(roles = DocumentUnifiedDiffBuilder.class) @Singleton public class DocumentUnifiedDiffBuilder extends AbstractUnifiedDiffBuilder { /** * The reference used to create an empty document when the passed document is null. */ private static final DocumentReference EMPTY_DOCUMENT_REFERENCE = new DocumentReference("wiki", "Space", "EmptyDocument"); @Inject private AttachmentUnifiedDiffBuilder attachmentDiffBuilder; /** * Computes the differences, in unified format, between two versions of a document. A null document represents a * deleted document. * * @param previousDocument the previous document version * @param nextDocument the next document version * @return the differences, in unified format, between the given document versions */ public DocumentUnifiedDiff diff(XWikiDocument previousDocument, XWikiDocument nextDocument) { DocumentUnifiedDiff diff = new DocumentUnifiedDiff(getDocumentVersionReference(previousDocument), getDocumentVersionReference(nextDocument)); if (previousDocument != nextDocument) { XWikiDocument nonNullPreviousDocument = emptyDocumentIfNull(previousDocument); XWikiDocument nonNullNextDocument = emptyDocumentIfNull(nextDocument); addDocumentFieldDiffs(nonNullPreviousDocument, nonNullNextDocument, diff); addAttachmentDiffs(nonNullPreviousDocument, nonNullNextDocument, diff); addObjectDiffs(nonNullPreviousDocument, nonNullNextDocument, diff); addClassPropertyDiffs(nonNullPreviousDocument.getXClass(), nonNullNextDocument.getXClass(), diff); } return diff; } private DocumentVersionReference getDocumentVersionReference(XWikiDocument document) { if (document == null) { return null; } else { DocumentVersionReference documentVersionReference = new DocumentVersionReference(document.getDocumentReferenceWithLocale()); if (documentVersionReference.getVersion() == null) { return new DocumentVersionReference(documentVersionReference, document.getVersion()); } return documentVersionReference; } } private XWikiDocument emptyDocumentIfNull(XWikiDocument document) { if (document == null) { XWikiDocument emptyDocument = new XWikiDocument(EMPTY_DOCUMENT_REFERENCE); emptyDocument.setSyntax(null); return emptyDocument; } else { return document; } } private boolean isNull(XWikiDocument document) { return document.getDocumentReference() == EMPTY_DOCUMENT_REFERENCE; } /** * Computes the document field differences between the given two document versions. Only the fields that the user * can modify from the UI are taken into account. * * @param previousDocument the previous document version * @param nextDocument the next document version * @param documentDiff where to collect the differences */ private void addDocumentFieldDiffs(XWikiDocument previousDocument, XWikiDocument nextDocument, DocumentUnifiedDiff documentDiff) { maybeAddDiff(documentDiff, "title", previousDocument.getTitle(), nextDocument.getTitle()); maybeAddDiff(documentDiff, "parent", previousDocument.getParentReference(), nextDocument.getParentReference()); maybeAddDiff(documentDiff, "hidden", isNull(previousDocument) ? null : previousDocument.isHidden(), isNull(nextDocument) ? null : nextDocument.isHidden()); maybeAddDiff(documentDiff, "defaultLocale", previousDocument.getDefaultLocale(), nextDocument.getDefaultLocale()); maybeAddDiff(documentDiff, "syntax", previousDocument.getSyntax(), nextDocument.getSyntax()); maybeAddDiff(documentDiff, CONTENT, previousDocument.getContent(), nextDocument.getContent()); } private void addAttachmentDiffs(XWikiDocument previousDocument, XWikiDocument nextDocument, DocumentUnifiedDiff documentDiff) { // Check the attachments that have been deleted of modified. for (XWikiAttachment previousAttachment : previousDocument.getAttachmentList()) { XWikiAttachment nextAttachment = nextDocument.getAttachment(previousAttachment.getFilename()); if (previousAttachment != nextAttachment) { this.attachmentDiffBuilder.addAttachmentDiff(previousAttachment, nextAttachment, documentDiff); } } // Check the attachments that have been added. for (XWikiAttachment nextAttachment : nextDocument.getAttachmentList()) { XWikiAttachment previousAttachment = previousDocument.getAttachment(nextAttachment.getFilename()); if (previousAttachment == null) { this.attachmentDiffBuilder.addAttachmentDiff(previousAttachment, nextAttachment, documentDiff); } } } private void addObjectDiffs(XWikiDocument previousDocument, XWikiDocument nextDocument, DocumentUnifiedDiff documentDiff) { // Check the objects that have been deleted of modified. for (List<BaseObject> previousObjects : previousDocument.getXObjects().values()) { for (BaseObject previousObject : previousObjects) { // It can be null when objects are deleted and the document is still in the cache storage. if (previousObject != null) { BaseObject nextObject = nextDocument.getXObject(previousObject.getXClassReference(), previousObject.getNumber()); if (previousObject != nextObject) { addObjectDiff(previousObject, nextObject, documentDiff); } } } } // Check the objects that have been added. for (List<BaseObject> nextObjects : nextDocument.getXObjects().values()) { for (BaseObject nextObject : nextObjects) { // It can be null when objects are deleted and the document is still in the cache storage. if (nextObject != null) { BaseObject previousObject = previousDocument.getXObject(nextObject.getXClassReference(), nextObject.getNumber()); if (previousObject == null) { addObjectDiff(previousObject, nextObject, documentDiff); } } } } } private void addObjectDiff(BaseObject previousObject, BaseObject nextObject, DocumentUnifiedDiff documentDiff) { ObjectReference previousReference = getObjectVersionReference(previousObject, documentDiff.getPreviousReference()); ObjectReference nextReference = getObjectVersionReference(nextObject, documentDiff.getNextReference()); EntityUnifiedDiff<ObjectReference> objectDiff = new EntityUnifiedDiff<>(previousReference, nextReference); addObjectDiff(previousObject == null ? new BaseObject() : previousObject, nextObject == null ? new BaseObject() : nextObject, objectDiff); if (objectDiff.size() > 0) { documentDiff.getObjectDiffs().add(objectDiff); } } private ObjectReference getObjectVersionReference(BaseObject object, DocumentVersionReference documentVersionReference) { return object == null ? null : new BaseObjectReference(object.getXClassReference(), object.getNumber(), documentVersionReference); } private void addObjectDiff(BaseCollection<?> previousObject, BaseCollection<?> nextObject, Map<String, List<UnifiedDiffBlock<String, Character>>> objectDiff) { // Check the properties that have been deleted or modified. for (String propertyName : previousObject.getPropertyList()) { BaseProperty<?> previousProperty = (BaseProperty<?>) previousObject.getField(propertyName); BaseProperty<?> nextProperty = (BaseProperty<?>) nextObject.getField(propertyName); if (previousProperty != nextProperty) { addObjectPropertyDiff(previousProperty, nextProperty, objectDiff); } } // Check the properties that have been added. for (String propertyName : nextObject.getPropertyList()) { BaseProperty<?> previousProperty = (BaseProperty<?>) previousObject.getField(propertyName); BaseProperty<?> nextProperty = (BaseProperty<?>) nextObject.getField(propertyName); if (previousProperty == null) { addObjectPropertyDiff(previousProperty, nextProperty, objectDiff); } } } private void addObjectPropertyDiff(BaseProperty<?> previousProperty, BaseProperty<?> nextProperty, Map<String, List<UnifiedDiffBlock<String, Character>>> objectDiff) { String key = previousProperty == null ? nextProperty.getName() : previousProperty.getName(); Object previousValue = previousProperty == null ? null : previousProperty.getValue(); Object nextValue = nextProperty == null ? null : nextProperty.getValue(); if (maybeAddDiff(objectDiff, key, previousValue, nextValue) && (isPrivateProperty(previousProperty) || isPrivateProperty(nextProperty))) { // Empty the differences if the property is private. objectDiff.get(key).clear(); } } private boolean isPrivateProperty(BaseProperty<?> property) { BaseCollection<?> object = property == null ? null : property.getObject(); if (object != null) { BaseClass xclass = object.getXClass(this.xcontextProvider.get()); if (xclass != null) { PropertyClass propertyClass = (PropertyClass) xclass.get(property.getName()); String propertyType = propertyClass == null ? null : propertyClass.getClassType(); return "Password".equals(propertyType) || "Email".equals(propertyClass); } } return false; } private void addClassPropertyDiffs(BaseClass previousClass, BaseClass nextClass, DocumentUnifiedDiff documentDiff) { // Check the properties that have been deleted or modified. for (String propertyName : previousClass.getPropertyList()) { PropertyClass previousProperty = (PropertyClass) previousClass.get(propertyName); PropertyClass nextProperty = (PropertyClass) nextClass.get(propertyName); addClassPropertyDiff(previousProperty, nextProperty, documentDiff); } // Check the properties that have been added. for (String propertyName : nextClass.getPropertyList()) { PropertyClass previousProperty = (PropertyClass) previousClass.get(propertyName); PropertyClass nextProperty = (PropertyClass) nextClass.get(propertyName); if (previousProperty == null) { addClassPropertyDiff(previousProperty, nextProperty, documentDiff); } } } private void addClassPropertyDiff(PropertyClass previousProperty, PropertyClass nextProperty, DocumentUnifiedDiff documentDiff) { ClassPropertyReference previousReference = getClassPropertyVersionReference(previousProperty, documentDiff.getPreviousReference()); ClassPropertyReference nextReference = getClassPropertyVersionReference(nextProperty, documentDiff.getNextReference()); EntityUnifiedDiff<ClassPropertyReference> classPropertyDiff = new EntityUnifiedDiff<>(previousReference, nextReference); // Catch a property type change. maybeAddDiff(classPropertyDiff, "type", previousProperty == null ? null : previousProperty.getClassType(), nextProperty == null ? null : nextProperty.getClassType()); addObjectDiff(previousProperty == null ? new PropertyClass() : previousProperty, nextProperty == null ? new PropertyClass() : nextProperty, classPropertyDiff); // The property name is already specified by the previous / next reference. classPropertyDiff.remove("name"); // This meta property is not used (there's no UI to change it). classPropertyDiff.remove("unmodifiable"); if (classPropertyDiff.size() > 0) { documentDiff.getClassPropertyDiffs().add(classPropertyDiff); } } private ClassPropertyReference getClassPropertyVersionReference(PropertyClass property, DocumentVersionReference documentVersionReference) { return property == null ? null : new ClassPropertyReference(property.getName(), documentVersionReference); } }