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