From version < 14.1 >
edited by Marius Dumitru Florea
on 2019/12/11 16:23
To version < 16.1
edited by Marius Dumitru Florea
on 2019/12/12 14:59
<
Change comment: There is no comment for this version

Summary

Details

ExtensionCode.ExtensionClass[0]
Description
... ... @@ -135,25 +135,17 @@
135 135  To compute the changes you need to use the ##XMLDiff## component.
136 136  
137 137  {{code language="java"}}
138 -public interface XMLDiff
139 -{
140 - /**
141 - * Computes the difference between two XML nodes and their descendants. When a value node type (e.g. text,
142 - * attribute, comment) is modified we compute the difference on the text value using the splitter indicated by the
143 - * configuration. Otherwise the difference is expressed at node level, as if two lists of nodes are compared.
144 - * <p>
145 - * The result is a mapping between nodes from the left side and the patches that need to be applied to these nodes
146 - * in order for the left tree to become the right tree. If the root nodes of the left and right trees don't match
147 - * then this change is mapped to the {@code null} key.
148 - *
149 - * @param left the left side of the comparison
150 - * @param right the right side of the comparison
151 - * @param config the configuration
152 - * @return the differences between the two XML nodes
153 - * @throws DiffException if the difference can't be computed
154 - */
155 - Map<Node, Patch<?>> diff(Node left, Node right, XMLDiffConfiguration config) throws DiffException;
156 -}
138 [email protected]
139 +private XMLDiff xmlDiff;
140 +
141 [email protected]
142 +private XMLDiffConfiguration config;
143 +
144 +...
145 +
146 +Document left = parseXML("...");
147 +Document right = parseXML("...");
148 +Map<Node, Patch<?>> patches = this.xmlDiff.diff(left, right, this.config);
157 157  {{/code}}
158 158  
159 159  The default implementation uses the list diff API to compute the changes between child nodes, attributes and text content. The algorithm is quite simple:
... ... @@ -182,33 +182,68 @@
182 182  
183 183  The similarity threshold (0.6 by default) can be changed from the configuration.
184 184  
177 +As indicated in the algorithm described above, you can also change from the configuration the text splitter used for a specific node type. And, of course, you can implement your own splitter component.
178 +
179 +{{code language="java"}}
180 [email protected]
181 [email protected]("myCustomSplitter")
182 +private StringSplitter myCustomSplitter;
183 +
184 [email protected]
185 +private XMLDiffConfiguration config;
186 +
187 +...
188 +
189 +((DefaultXMLDiffConfiguration) this.config).setSplitterForNodeType(Node.TEXT_NODE, this.myCustomSplitter);
190 +{{/code}}
191 +
185 185  == Displaying the Changes ==
186 186  
187 -To compute and display the changes you can use the ##XMLDiffManager## component.
194 +The component responsible for computing and displaying the changes is ##XMLDiffManager##. There's no generic (default) implementation provided at the moment. The provided implementation is dedicated to displaying changes between HTML documents.
188 188  
189 189  {{code language="java"}}
190 -public interface XMLDiffManager
191 -{
192 - /**
193 - * Computes and marks the differences between two XML documents.
194 - *
195 - * @param left the left side of the comparison
196 - * @param right the right side of the comparison
197 - * @param config the configuration
198 - * @return the differences between the two XML documents
199 - * @throws DiffException if the difference can't be computed
200 - */
201 - String diff(String left, String right, XMLDiffConfiguration config) throws DiffException;
202 -}
197 +@Inject
198 +@Named("html/unified")
199 +private XMLDiffManager unifiedHTMLDiffManager;
200 +
201 +@Inject
202 [email protected]Named("html")
203 +private XMLDiffConfiguration config;
204 +
205 +...
206 +
207 +String previousHTML = "...";
208 +String nextHTML = "...";
209 +String diff = this.unifiedHTMLDiffManager.diff(previousHTML, nextHTML, this.config)
203 203  {{/code}}
204 204  
205 205  You can control from the configuration which ##XMLDiffFilter##s are applied on the XML documents before and after computing the changes. You can also implement your own filters, e.g. to remove irrelevant changes, or to ignore parts of the XML documents while computing the changes. The default configuration applies a filter to mark context (unmodified) nodes after the changes are computed.
206 206  
207 -=== HTML Visual Diff ===
214 +{{code language="java"}}
215 [email protected]
216 [email protected]("myCustomFilter")
217 +private XMLDiffFilter myCustomFilter;
208 208  
209 -The ##XMLDiffManager## has an implementation dedicated to computing a visual diff on HTML. Best is to use the provided script service:
219 [email protected]
220 [email protected]("html")
221 +private XMLDiffConfiguration config;
210 210  
223 +...
224 +
225 +this.config.getFilters().add(this.myCustomFilter);
226 +{{/code}}
227 +
228 +== Script Service ==
229 +
230 +Here's how you can compute and display the changes from a Velocity script:
231 +
211 211  {{code language="none"}}
233 +{{velocity}}
234 +{{html clean="false"}}
235 +#set ($discard = $xwiki.ssfx.use('uicomponents/viewers/diff.css', true))
236 +#set ($discard = $xwiki.jsfx.use('uicomponents/viewers/diff.js'))
237 +#set ($previousHTML = '<p>one two three</p>')
238 +#set ($nextHTML = '<p>one 2 three</p>')
212 212  <div class="html-diff">
213 213   #set ($htmlDiff = $services.diff.html.unified($previousHTML, $nextHTML))
214 214   #if ($htmlDiff == '')
... ... @@ -219,6 +219,8 @@
219 219   $htmlDiff
220 220   #end
221 221  </div>
249 +{{/html}}
250 +{{/velocity}}
222 222  {{/code}}
223 223  
224 224  You can configure the diff by passing a third argument:
... ... @@ -225,6 +225,10 @@
225 225  
226 226  {{code language="none"}}
227 227  #set ($config = $services.diff.html.defaultConfiguration)
257 +#set ($discard = $config.setSimilarityThreshold(0.6))
258 +#set ($discard = $config.addFilter('hintOfMyCustomFilter'))
259 +## Change the splitter used for text nodes from 'character' to 'word'.
260 +#set ($discard = $config.setSplitterForNodeType(3, 'word'))
228 228  #set ($htmlDiff = $services.diff.html.unified($previousHTML, $nextHTML, $config))
229 229  {{/code}}
230 230  
cogA set of diff/merge APIs
TypeJAR
Developed by

XWiki Development Team

Rating
Rate!
0 Votes
LicenseGNU Lesser General Public License 2.1
Bundled With

XWiki Standard

Description

Since there is no real standard API for it and that most of the existing library are not very active enough or very tied to various EDIs we decided to write our own API for XWiki ecosystem which would be independent from the actual implementation.

List Diff & Merge

Base API module

Expose generic diff and merge component implementing org.xwiki.commons.diff.DiffManager role.

The main idea of this API is that it's not tied to any type so you have to first transform the datas you want to diff into lists and the diff will be a diff between thoses two lists.

For example when you want to diff text you can choose if you want to to a line based diff, a word based diff, a character based diff, etc.

public interface DiffManager
{
   /**
     * Produce a diff between the two provided versions.
     *
     * @param the type of compared elements
     * @param previous the previous version of the content to compare
     * @param next the next version of the content to compare
     * @param configuration the configuration of the diff behavior
     * @return the result of the diff
     * @throws DiffException error when executing the diff
     */

   <E> DiffResult<E> diff(List<E> previous, List<E> next, DiffConfiguration<E> configuration) throws DiffException;

   /**
     * Execute a 3-way merge on provided versions.
     * If a conflict is detected during the merge, no error is triggered and the returned {@link MergeResult} object
     * always has a result (see {@link MergeResult#getMerged()}): the conflict is automatically fixed with a fallback
     * defined by the {@link MergeConfiguration}.
     *
     * If the {@link MergeConfiguration} instance contains some {@link ConflictDecision}
     * (see {@link MergeConfiguration#setConflictDecisionList(List)}), then those decisions are taken into account
     * to solve the conflict. The decision linked to the conflict is retrieved and applied, unless the decision state
     * is still {@link org.xwiki.diff.ConflictDecision.DecisionType#UNDECIDED}. When a decision is used to solve a
     * conflict, the conflict is not recorded in the {@link MergeResult}.
     *
     * If the decision is {@link org.xwiki.diff.ConflictDecision.DecisionType#UNDECIDED}, or if no decision is available
     * for this conflict, then the fallback version defined in the {@link MergeConfiguration}
     * (see {@link MergeConfiguration#setFallbackOnConflict(MergeConfiguration.Version)}) is used to fix the conflict,
     * but in that case the conflict is recorded in the returned {@link MergeResult}.
     *
     * Finally the configuration parameter accepts a null value: in that case, the fallback version is always the
     * current version.
     *
     * @param the type of compared elements
     * @param commonAncestor the common ancestor of the two versions of the content to compare
     * @param next the next version of the content to compare
     * @param current the current version of the content to compare
     * @param configuration the configuration of the merge behavior
     * @return the result of the merge
     * @throws MergeException error when executing the merge
     */

   <E> MergeResult<E> merge(List<E> commonAncestor, List<E> next, List<E> current, MergeConfiguration<E> configuration)
       throws MergeException;
}

Merge Configuration

The merge configuration class is mainly used to setup the fallback version of the merge in case of conflict. As specified in the javadoc, the merge operation always return a MergeResult which contains a merged value, even if they were conflicts during the operation. The possible fallback versions, are current, next or previous, current being the default version to fallback if nothing is specified. 

Starting with XWiki 11.7RC1 the MergeConfiguration and MergeResult classes get new APIs to retrieve precisely the conflicts information, and to be able to take decisions on a conflict by conflict basis. The decisions specified on the MergeConfiguration to solve a conflict have always the priority over the fallback configuration.

Display API module

This module helps you display a DiffResult obtained with the base API in various formats. Currently there are two diff formats supported.

Inline diff

An inline diff is made of a list of chunks, each marked as added, removed or unmodified. For instance, if changes are computed at word level, you can have this inline diff:

The quicksick brown fox jumpsstumbles over the lazycrazy mad dog.

At character level the diff looks a bit different:

the qusick brown fox

In this case the first chunk is "the ", an unmodified chunk, made of 4 characters and the second chunk is "qu", a removed chunk, made of 2 characters. An inline diff can be displayed either as you've seen above, mixing added and removed chunks in one line, or it can be displayed on two lines, one showing the removed chunks and the other the added chunks:

The quick brown fox jumps over the lazy dog.
The sick brown fox stumbles over the crazy mad dog.

Unified diff

An unified diff consists in a list of blocks, each block grouping changes that are close to each other. The distance between two changes in a block is less than 2 * context size, where context size represents the number of unmodified elements to include before and after a change in order to place that change in context.

If the elements can be split in sub-elements, i.e. if a splitter is provided through the configuration, then the unified diff displays also the changes inside the modified elements using the inline format.

If changes are computed at the line level in a text, i.e. the elements that are compared to produce the diff are lines of text, and a word splitter is provided through configuration then the following is a block from a unified diff:

@@ -81,5 +85,5 @@
 first line of context
 another unmodified line
-this line has been removed
+this line replaced the previous line
 close the block with unmodified lines
 last line of context

Script service

Expose more script oriented APIs of the two previous modules. It also adds helpers like String based APIs etc.

## Compute the differences between two lists of elements (e.g. lines of text).
#set ($diffResult = $services.diff.diff($previous, $next, $configuration))

## Display the differences between two texts in the unified format.
#foreach ($block in $services.diff.display.unified($previous, $next))
  $block
#end

Conflicts Display

Starting with XWiki 11.7RC1 the display diff API also allows to display information about conflicts.
The APIs to build unified diff takes as input a list of conflict elements that have been computed during a merge: some conflict information are then available in the resulting unified diff blocks, and can be used to present conflicts and possible decisions inside an UI presenting unified diff.

XML Diff

This module provides an API to compute and display the changes between 2 XML trees (DOM instances). It was introduced in XWiki 11.6RC1.

Computing the Changes

To compute the changes you need to use the XMLDiff component.

public interface XMLDiff
{
   /**
     * Computes the difference between two XML nodes and their descendants. When a value node type (e.g. text,
     * attribute, comment) is modified we compute the difference on the text value using the splitter indicated by the
     * configuration. Otherwise the difference is expressed at node level, as if two lists of nodes are compared.
     *


     * The result is a mapping between nodes from the left side and the patches that need to be applied to these nodes
     * in order for the left tree to become the right tree. If the root nodes of the left and right trees don't match
     * then this change is mapped to the {@code null} key.
     *
     * @param left the left side of the comparison
     * @param right the right side of the comparison
     * @param config the configuration
     * @return the differences between the two XML nodes
     * @throws DiffException if the difference can't be computed
     */
    Map<Node, Patch> diff(Node left, Node right, XMLDiffConfiguration config) throws DiffException;
}

@Inject
private XMLDiff xmlDiff;

@Inject
private XMLDiffConfiguration config;

...

Document left = parseXML("...");
Document right = parseXML("...");
Map<Node, Patch> patches = this.xmlDiff.diff(left, right, this.config);

The default implementation uses the list diff API to compute the changes between child nodes, attributes and text content. The algorithm is quite simple:

if leftNode and rightNode are "similar"
  if leftNode has value (true for text, comment or attribute nodes)
    if leftNode's value is different than rightNode's value
      compute the changes using the splitter indicated in the configuration
      (character splitter is used for text nodes by default, but there is also a word splitter available)
  else
    if leftNode has attributes (true for elements)
      compute the difference between attributes (added, removed, modified)
    compute the difference between child nodes (lists of nodes), matching nodes that are "very similar"
    compute (recursively) the changes between leftNode's children that are "very similar" to rightNode's children
else
  add change delta

Where "similar" and "very similar" are defined as:

similar = same nodeType, nodeName, localName, namespaceURI, prefix
very similar = similar and the percent of text changes (Levenshtein distance / max length) is less than 60%

The similarity threshold (0.6 by default) can be changed from the configuration.

As indicated in the algorithm described above, you can also change from the configuration the text splitter used for a specific node type. And, of course, you can implement your own splitter component.

@Inject
@Named("myCustomSplitter")
private StringSplitter myCustomSplitter;

@Inject
private XMLDiffConfiguration config;

...

((DefaultXMLDiffConfiguration) this.config).setSplitterForNodeType(Node.TEXT_NODE, this.myCustomSplitter);

Displaying the Changes

To compute and display the changes you can use the XMLDiffManager component.

The component responsible for computing and displaying the changes is XMLDiffManager. There's no generic (default) implementation provided at the moment. The provided implementation is dedicated to displaying changes between HTML documents.

public interface XMLDiffManager
{
   /**
     * Computes and marks the differences between two XML documents.
     *
     * @param left the left side of the comparison
     * @param right the right side of the comparison
     * @param config the configuration
     * @return the differences between the two XML documents
     * @throws DiffException if the difference can't be computed
     */

    String diff(String left, String right, XMLDiffConfiguration config) throws DiffException;
}
@Inject
@Named("html/unified")
private XMLDiffManager unifiedHTMLDiffManager;

@Inject
@Named("html")
private XMLDiffConfiguration config;

...

String previousHTML = "...";
String nextHTML = "...";
String diff = this.unifiedHTMLDiffManager.diff(previousHTML, nextHTML, this.config)

You can control from the configuration which XMLDiffFilters are applied on the XML documents before and after computing the changes. You can also implement your own filters, e.g. to remove irrelevant changes, or to ignore parts of the XML documents while computing the changes. The default configuration applies a filter to mark context (unmodified) nodes after the changes are computed.

HTML Visual Diff

The XMLDiffManager has an implementation dedicated to computing a visual diff on HTML. Best is to use the provided script service:

@Inject
@Named("myCustomFilter")
private XMLDiffFilter myCustomFilter;

@Inject
@Named("html")
private XMLDiffConfiguration config;

...

this.config.getFilters().add(this.myCustomFilter);

Script Service

Here's how you can compute and display the changes from a Velocity script:


  #set ($htmlDiff = $services.diff.html.unified($previousHTML, $nextHTML))
  #if ($htmlDiff == '')
    No changes.
  #elseif ("$!htmlDiff" == '')
    Failed to compute the changes.
  #else
    $htmlDiff
  #end
{{velocity}}
{{html clean="false"}}
#set ($discard = $xwiki.ssfx.use('uicomponents/viewers/diff.css', true))
#set ($discard = $xwiki.jsfx.use('uicomponents/viewers/diff.js'))
#set ($previousHTML = '

one two three

')

#set ($nextHTML = '

one 2 three

')


  #set ($htmlDiff = $services.diff.html.unified($previousHTML, $nextHTML))
  #if ($htmlDiff == '')
    No changes.
  #elseif ("$!htmlDiff" == '')
    Failed to compute the changes.
  #else
    $htmlDiff
  #end

{{/html}}
{{/velocity}}

You can configure the diff by passing a third argument:

#set ($config = $services.diff.html.defaultConfiguration)
#set ($htmlDiff = $services.diff.html.unified($previousHTML, $nextHTML, $config))
#set ($config = $services.diff.html.defaultConfiguration)
#set ($discard = $config.setSimilarityThreshold(0.6))
#set ($discard = $config.addFilter('hintOfMyCustomFilter'))
## Change the splitter used for text nodes from 'character' to 'word'.
#set ($discard = $config.setSplitterForNodeType(3, 'word'))
#set ($htmlDiff = $services.diff.html.unified($previousHTML, $nextHTML, $config))

The default configuration applies a filter that embeds images into the HTML before computing the changes, in order to compare the image data and not the image location.

Get Connected