001/*
002 * (C) Copyright 2012 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     ataillefer
016 */
017package org.nuxeo.ecm.diff.service.impl;
018
019import java.io.IOException;
020import java.util.List;
021
022import org.apache.commons.logging.Log;
023import org.apache.commons.logging.LogFactory;
024import org.custommonkey.xmlunit.DetailedDiff;
025import org.custommonkey.xmlunit.Diff;
026import org.custommonkey.xmlunit.Difference;
027import org.custommonkey.xmlunit.ElementNameAndAttributeQualifier;
028import org.custommonkey.xmlunit.NodeDetail;
029import org.custommonkey.xmlunit.XMLUnit;
030import org.nuxeo.ecm.core.api.CoreSession;
031import org.nuxeo.ecm.core.api.DocumentModel;
032import org.nuxeo.ecm.core.api.NuxeoException;
033import org.nuxeo.ecm.core.io.DocumentXMLExporter;
034import org.nuxeo.ecm.diff.model.DocumentDiff;
035import org.nuxeo.ecm.diff.model.impl.DocumentDiffImpl;
036import org.nuxeo.ecm.diff.service.DocumentDiffService;
037import org.nuxeo.runtime.api.Framework;
038import org.xml.sax.InputSource;
039import org.xml.sax.SAXException;
040
041/**
042 * Implementation of DocumentDiffService.
043 * <p>
044 * The diff is made by exporting the documents to XML, then using the Diff feature provided by XMLUnit to get the
045 * differences between the XML exports.
046 *
047 * @author <a href="mailto:ataillefer@nuxeo.com">Antoine Taillefer</a>
048 */
049public class DocumentDiffServiceImpl implements DocumentDiffService {
050
051    private static final long serialVersionUID = 9023621903602108068L;
052
053    private static final Log LOGGER = LogFactory.getLog(DocumentDiffServiceImpl.class);
054
055    /**
056     * {@inheritDoc}
057     */
058    public DocumentDiff diff(CoreSession session, DocumentModel leftDoc, DocumentModel rightDoc) {
059
060        // Input sources to hold XML exports
061        InputSource leftDocXMLInputSource = new InputSource();
062        InputSource rightDocXMLInputSource = new InputSource();
063
064        // Export leftDoc and rightDoc to XML
065        exportXML(session, leftDoc, rightDoc, leftDocXMLInputSource, rightDocXMLInputSource);
066
067        // Process the XML diff
068        DetailedDiff detailedDiff = diffXML(leftDocXMLInputSource, rightDocXMLInputSource);
069
070        // Fill in the DocumentDiff object using the result of the detailed diff
071        DocumentDiff docDiff = computeDocDiff(detailedDiff);
072
073        return docDiff;
074    }
075
076    /**
077     * {@inheritDoc}
078     */
079    public DocumentDiff diff(String leftXML, String rightXML) {
080
081        // Process the XML diff
082        DetailedDiff detailedDiff = diffXML(leftXML, rightXML);
083
084        // Fill in the DocumentDiff object using the result of the detailed diff
085        DocumentDiff docDiff = computeDocDiff(detailedDiff);
086
087        return docDiff;
088    }
089
090    /**
091     * {@inheritDoc}
092     */
093    public void configureXMLUnit() {
094
095        XMLUnit.setIgnoreWhitespace(true);
096        XMLUnit.setIgnoreDiffBetweenTextAndCDATA(true);
097        XMLUnit.setCompareUnmatched(false);
098    }
099
100    /**
101     * {@inheritDoc}
102     */
103    public void configureDiff(Diff diff) {
104
105        diff.overrideDifferenceListener(new IgnoreStructuralDifferenceListener());
106        diff.overrideElementQualifier(new ElementNameAndAttributeQualifier());
107    }
108
109    /**
110     * Exports leftDoc and rightDoc to XML.
111     *
112     * @param session the session
113     * @param leftDoc the left doc
114     * @param rightDoc the right doc
115     * @param leftDocXMLInputSource the left doc XML input source
116     * @param rightDocXMLInputSource the right doc XML input source
117     */
118    protected final void exportXML(CoreSession session, DocumentModel leftDoc, DocumentModel rightDoc,
119            InputSource leftDocXMLInputSource, InputSource rightDocXMLInputSource) {
120
121        DocumentXMLExporter docXMLExporter = getDocumentXMLExporter();
122
123        leftDocXMLInputSource.setByteStream(docXMLExporter.exportXML(leftDoc, session));
124        rightDocXMLInputSource.setByteStream(docXMLExporter.exportXML(rightDoc, session));
125    }
126
127    /**
128     * Gets the document XML exporter service.
129     *
130     * @return the document XML exporter
131     */
132    protected final DocumentXMLExporter getDocumentXMLExporter() {
133        return Framework.getService(DocumentXMLExporter.class);
134    }
135
136    /**
137     * Processes the XML diff using the XMLUnit Diff feature.
138     *
139     * @param leftDocXMLInputSource the left doc XML input source
140     * @param rightDocXMLInputSource the right doc XML input source
141     * @return the detailed diff
142     */
143    protected final DetailedDiff diffXML(InputSource leftDocXMLInputSource, InputSource rightDocXMLInputSource)
144            {
145
146        DetailedDiff detailedDiff;
147        try {
148            // Configure XMLUnit
149            configureXMLUnit();
150            // Build diff
151            Diff diff = new Diff(leftDocXMLInputSource, rightDocXMLInputSource);
152            // Configure diff
153            configureDiff(diff);
154            // Build detailed diff
155            detailedDiff = new DetailedDiff(diff);
156        } catch (SAXException | IOException e) {
157            throw new NuxeoException("Error while trying to make a detailed diff between two documents.", e);
158        }
159        return detailedDiff;
160    }
161
162    /**
163     * Processes the XML diff using the XMLUnit Diff feature.
164     *
165     * @param leftXML the left xml
166     * @param rightXML the right xml
167     * @return the detailed diff
168     */
169    protected final DetailedDiff diffXML(String leftXML, String rightXML) {
170
171        DetailedDiff detailedDiff;
172        try {
173            // Configure XMLUnit
174            configureXMLUnit();
175            // Build diff
176            Diff diff = new Diff(leftXML, rightXML);
177            // Configure diff
178            configureDiff(diff);
179            // Build detailed diff
180            detailedDiff = new DetailedDiff(diff);
181        } catch (SAXException | IOException e) {
182            throw new NuxeoException("Error while trying to make a detailed diff between two XML strings.", e);
183        }
184        return detailedDiff;
185    }
186
187    /**
188     * Computes the doc diff.
189     *
190     * @param detailedDiff the detailed diff
191     * @return the document diff
192     */
193    @SuppressWarnings("unchecked")
194    protected final DocumentDiff computeDocDiff(DetailedDiff detailedDiff) {
195
196        // Document diff object
197        DocumentDiff docDiff = new DocumentDiffImpl();
198
199        // Iterate on differences
200        List<Difference> differences = detailedDiff.getAllDifferences();
201        LOGGER.debug(String.format("Found %d differences.", differences.size()));
202
203        int fieldDifferenceCount = 0;
204        for (Difference difference : differences) {
205
206            // Control node <=> left doc node
207            NodeDetail controlNodeDetail = difference.getControlNodeDetail();
208            // Test node <=> right doc node
209            NodeDetail testNodeDetail = difference.getTestNodeDetail();
210
211            if (controlNodeDetail != null && testNodeDetail != null) {
212
213                boolean fieldDiffFound = FieldDiffHelper.computeFieldDiff(docDiff, controlNodeDetail, testNodeDetail,
214                        fieldDifferenceCount, difference);
215                if (fieldDiffFound) {
216                    fieldDifferenceCount++;
217                }
218            }
219        }
220        LOGGER.debug(String.format("Found %d field differences.", fieldDifferenceCount));
221
222        return docDiff;
223    }
224
225}