diff --git a/jspwiki-main/pom.xml b/jspwiki-main/pom.xml index 8bbc06d4cb..0d4d880535 100644 --- a/jspwiki-main/pom.xml +++ b/jspwiki-main/pom.xml @@ -169,8 +169,8 @@ - org.jvnet.hudson - org.suigeneris.jrcs.diff + io.github.java-diff-utils + java-diff-utils diff --git a/jspwiki-main/src/main/java/org/apache/wiki/diff/ContextualDiffProvider.java b/jspwiki-main/src/main/java/org/apache/wiki/diff/ContextualDiffProvider.java deleted file mode 100644 index 32d96bce18..0000000000 --- a/jspwiki-main/src/main/java/org/apache/wiki/diff/ContextualDiffProvider.java +++ /dev/null @@ -1,417 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -package org.apache.wiki.diff; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.wiki.api.core.Context; -import org.apache.wiki.api.core.Engine; -import org.apache.wiki.api.exceptions.NoRequiredPropertyException; -import org.apache.wiki.util.TextUtil; -import org.suigeneris.jrcs.diff.Diff; -import org.suigeneris.jrcs.diff.DifferentiationFailedException; -import org.suigeneris.jrcs.diff.Revision; -import org.suigeneris.jrcs.diff.RevisionVisitor; -import org.suigeneris.jrcs.diff.delta.AddDelta; -import org.suigeneris.jrcs.diff.delta.ChangeDelta; -import org.suigeneris.jrcs.diff.delta.Chunk; -import org.suigeneris.jrcs.diff.delta.DeleteDelta; -import org.suigeneris.jrcs.diff.delta.Delta; -import org.suigeneris.jrcs.diff.myers.MyersDiff; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Properties; -import java.util.StringTokenizer; -import java.util.stream.Collectors; - - -/** - * A seriously better diff provider, which highlights changes word-by-word using CSS. - * - * Suggested by John Volkar. - */ -public class ContextualDiffProvider implements DiffProvider { - - private static final Logger LOG = LogManager.getLogger( ContextualDiffProvider.class ); - - /** - * A jspwiki.properties value to define how many characters are shown around the change context. - * The current value is {@value}. - */ - public static final String PROP_UNCHANGED_CONTEXT_LIMIT = "jspwiki.contextualDiffProvider.unchangedContextLimit"; - - //TODO all of these publics can become jspwiki.properties entries... - //TODO span title= can be used to get hover info... - - public boolean m_emitChangeNextPreviousHyperlinks = true; - - //Don't use spans here the deletion and insertions are nested in this... - public static String CHANGE_START_HTML = ""; //This could be a image '>' for a start marker - public static String CHANGE_END_HTML = ""; //and an image for an end '<' marker - public static String DIFF_START = "
"; - public static String DIFF_END = "
"; - - // Unfortunately we need to do dumb HTML here for RSS feeds. - - public static String INSERTION_START_HTML = ""; - public static String INSERTION_END_HTML = ""; - public static String DELETION_START_HTML = ""; - public static String DELETION_END_HTML = ""; - private static final String ANCHOR_PRE_INDEX = ""; - private static final String BACK_PRE_INDEX = "<<"; - private static final String FORWARD_PRE_INDEX = ">>"; - public static String ELIDED_HEAD_INDICATOR_HTML = "

..."; - public static String ELIDED_TAIL_INDICATOR_HTML = "...

"; - public static String LINE_BREAK_HTML = "
"; - public static String ALTERNATING_SPACE_HTML = " "; - - // This one, I will make property file based... - private static final int LIMIT_MAX_VALUE = (Integer.MAX_VALUE /2) - 1; - private int m_unchangedContextLimit = LIMIT_MAX_VALUE; - - - /** - * Constructs this provider. - */ - public ContextualDiffProvider() - {} - - /** - * @see org.apache.wiki.api.providers.WikiProvider#getProviderInfo() - * - * {@inheritDoc} - */ - @Override - public String getProviderInfo() - { - return "ContextualDiffProvider"; - } - - /** - * @see org.apache.wiki.api.providers.WikiProvider#initialize(org.apache.wiki.api.core.Engine, java.util.Properties) - * - * {@inheritDoc} - */ - @Override - public void initialize( final Engine engine, final Properties properties) throws NoRequiredPropertyException, IOException { - final String configuredLimit = properties.getProperty( PROP_UNCHANGED_CONTEXT_LIMIT, Integer.toString( LIMIT_MAX_VALUE ) ); - int limit = LIMIT_MAX_VALUE; - try { - limit = Integer.parseInt( configuredLimit ); - } catch( final NumberFormatException e ) { - LOG.warn("Failed to parseInt " + PROP_UNCHANGED_CONTEXT_LIMIT + "=" + configuredLimit + " Will use a huge number as limit.", e ); - } - m_unchangedContextLimit = limit; - } - - - - /** - * Do a colored diff of the two regions. This. is. serious. fun. ;-) - * - * @see org.apache.wiki.diff.DiffProvider#makeDiffHtml(Context, String, String) - * - * {@inheritDoc} - */ - @Override - public synchronized String makeDiffHtml( final Context ctx, final String wikiOld, final String wikiNew ) { - // - // Sequencing handles lineterminator to
and every-other consequtive space to a   - // - final String[] alpha = sequence( TextUtil.replaceEntities( wikiOld ) ); - final String[] beta = sequence( TextUtil.replaceEntities( wikiNew ) ); - - final Revision rev; - try { - rev = Diff.diff( alpha, beta, new MyersDiff() ); - } catch( final DifferentiationFailedException dfe ) { - LOG.error( "Diff generation failed", dfe ); - return "Error while creating version diff."; - } - - final int revSize = rev.size(); - final StringBuffer sb = new StringBuffer(); - - sb.append( DIFF_START ); - - // - // The MyersDiff is a bit dumb by converting a single line multi-word diff into a series - // of Changes. The ChangeMerger pulls them together again... - // - final ChangeMerger cm = new ChangeMerger( sb, alpha, revSize ); - rev.accept( cm ); - cm.shutdown(); - sb.append( DIFF_END ); - return sb.toString(); - } - - /** - * Take the string and create an array from it, split it first on newlines, making - * sure to preserve the newlines in the elements, split each resulting element on - * spaces, preserving the spaces. - * - * All this preseving of newlines and spaces is so the wikitext when diffed will have fidelity - * to it's original form. As a side affect we see edits of purely whilespace. - */ - private String[] sequence( final String wikiText ) { - final String[] linesArray = Diff.stringToArray( wikiText ); - final List< String > list = new ArrayList<>(); - for( final String line : linesArray ) { - - String lastToken = null; - String token; - // StringTokenizer might be discouraged but it still is perfect here... - for( final StringTokenizer st = new StringTokenizer( line, " ", true ); st.hasMoreTokens(); ) { - token = st.nextToken(); - - if( " ".equals( lastToken ) && " ".equals( token ) ) { - token = ALTERNATING_SPACE_HTML; - } - - list.add( token ); - lastToken = token; - } - - list.add( LINE_BREAK_HTML ); // Line Break - } - - return list.toArray( new String[ 0 ] ); - } - - /** - * This helper class does the housekeeping for merging - * our various changes down and also makes sure that the - * whole change process is threadsafe by encapsulating - * all necessary variables. - */ - private final class ChangeMerger implements RevisionVisitor { - private final StringBuffer m_sb; - - /** Keeping score of the original lines to process */ - private final int m_max; - - private int m_index; - - /** Index of the next element to be copied into the output. */ - private int m_firstElem; - - /** Link Anchor counter */ - private int m_count = 1; - - /** State Machine Mode */ - private int m_mode = -1; /* -1: Unset, 0: Add, 1: Del, 2: Change mode */ - - /** Buffer to coalesce the changes together */ - private StringBuffer m_origBuf; - - private StringBuffer m_newBuf; - - /** Reference to the source string array */ - private final String[] m_origStrings; - - private ChangeMerger( final StringBuffer sb, final String[] origStrings, final int max ) { - m_sb = sb; - m_origStrings = origStrings != null ? origStrings.clone() : null; - m_max = max; - - m_origBuf = new StringBuffer(); - m_newBuf = new StringBuffer(); - } - - private void updateState( final Delta delta ) { - m_index++; - final Chunk orig = delta.getOriginal(); - if( orig.first() > m_firstElem ) { - // We "skip" some lines in the output. - // So flush out the last Change, if one exists. - flushChanges(); - - // Allow us to "skip" large swaths of unchanged text, show a "limited" amound of - // unchanged context so the changes are shown in - if( ( orig.first() - m_firstElem ) > 2 * m_unchangedContextLimit ) { - if (m_firstElem > 0) { - final int endIndex = Math.min( m_firstElem + m_unchangedContextLimit, m_origStrings.length -1 ); - - m_sb.append(Arrays.stream(m_origStrings, m_firstElem, endIndex).collect(Collectors.joining("", "", ELIDED_TAIL_INDICATOR_HTML))); - - } - - m_sb.append( ELIDED_HEAD_INDICATOR_HTML ); - - final int startIndex = Math.max(orig.first() - m_unchangedContextLimit, 0); - m_sb.append(Arrays.stream(m_origStrings, startIndex, orig.first()).collect(Collectors.joining())); - - } else { - // No need to skip anything, just output the whole range... - m_sb.append(Arrays.stream(m_origStrings, m_firstElem, orig.first()).collect(Collectors.joining())); - } - } - m_firstElem = orig.last() + 1; - } - - @Override - public void visit( final Revision rev ) { - // GNDN (Goes nowhere, does nothing) - } - - @Override - public void visit( final AddDelta delta ) { - updateState( delta ); - - // We have run Deletes up to now. Flush them out. - if( m_mode == 1 ) { - flushChanges(); - m_mode = -1; - } - // We are in "neutral mode". Start a new Change - if( m_mode == -1 ) { - m_mode = 0; - } - - // We are in "add mode". - if( m_mode == 0 || m_mode == 2 ) { - addNew( delta.getRevised() ); - m_mode = 1; - } - } - - @Override - public void visit( final ChangeDelta delta ) { - updateState( delta ); - - // We are in "neutral mode". A Change might be merged with an add or delete. - if( m_mode == -1 ) { - m_mode = 2; - } - - // Add the Changes to the buffers. - addOrig( delta.getOriginal() ); - addNew( delta.getRevised() ); - } - - @Override - public void visit( final DeleteDelta delta ) { - updateState( delta ); - - // We have run Adds up to now. Flush them out. - if( m_mode == 0 ) { - flushChanges(); - m_mode = -1; - } - // We are in "neutral mode". Start a new Change - if( m_mode == -1 ) { - m_mode = 1; - } - - // We are in "delete mode". - if( m_mode == 1 || m_mode == 2 ) { - addOrig( delta.getOriginal() ); - m_mode = 1; - } - } - - public void shutdown() { - m_index = m_max + 1; // Make sure that no hyperlink gets created - flushChanges(); - - if( m_firstElem < m_origStrings.length ) { - // If there's more than the limit of the orginal left just emit limit and elided... - if( ( m_origStrings.length - m_firstElem ) > m_unchangedContextLimit ) { - final int endIndex = Math.min( m_firstElem + m_unchangedContextLimit, m_origStrings.length -1 ); - m_sb.append(Arrays.stream(m_origStrings, m_firstElem, endIndex).collect(Collectors.joining("", "", ELIDED_TAIL_INDICATOR_HTML))); - - } else { - // emit entire tail of original... - m_sb.append(Arrays.stream(m_origStrings, m_firstElem, m_origStrings.length).collect(Collectors.joining())); - } - } - } - - private void addOrig( final Chunk chunk ) { - if( chunk != null ) { - chunk.toString( m_origBuf ); - } - } - - private void addNew( final Chunk chunk ) { - if( chunk != null ) { - chunk.toString( m_newBuf ); - } - } - - private void flushChanges() { - if( m_newBuf.length() + m_origBuf.length() > 0 ) { - // This is the span element which encapsulates anchor and the change itself - m_sb.append( CHANGE_START_HTML ); - - // Do we want to have a "back link"? - if( m_emitChangeNextPreviousHyperlinks && m_count > 1 ) { - m_sb.append( BACK_PRE_INDEX ); - m_sb.append( m_count - 1 ); - m_sb.append( BACK_POST_INDEX ); - } - - // An anchor for the change. - if (m_emitChangeNextPreviousHyperlinks) { - m_sb.append( ANCHOR_PRE_INDEX ); - m_sb.append( m_count++ ); - m_sb.append( ANCHOR_POST_INDEX ); - } - - // ... has been added - if( m_newBuf.length() > 0 ) { - m_sb.append( INSERTION_START_HTML ); - m_sb.append( m_newBuf ); - m_sb.append( INSERTION_END_HTML ); - } - - // .. has been removed - if( m_origBuf.length() > 0 ) { - m_sb.append( DELETION_START_HTML ); - m_sb.append( m_origBuf ); - m_sb.append( DELETION_END_HTML ); - } - - // Do we want a "forward" link? - if( m_emitChangeNextPreviousHyperlinks && (m_index < m_max) ) { - m_sb.append( FORWARD_PRE_INDEX ); - m_sb.append( m_count ); // Has already been incremented. - m_sb.append( FORWARD_POST_INDEX ); - } - - m_sb.append( CHANGE_END_HTML ); - - // Nuke the buffers. - m_origBuf = new StringBuffer(); - m_newBuf = new StringBuffer(); - } - - // After a flush, everything is reset. - m_mode = -1; - } - } - -} diff --git a/jspwiki-main/src/main/java/org/apache/wiki/diff/DefaultDifferenceManager.java b/jspwiki-main/src/main/java/org/apache/wiki/diff/DefaultDifferenceManager.java index 0c15bb683c..7ad05284ef 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/diff/DefaultDifferenceManager.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/diff/DefaultDifferenceManager.java @@ -55,7 +55,7 @@ public DefaultDifferenceManager( final Engine engine, final Properties props ) { } private void loadProvider( final Properties props ) { - final String providerClassName = props.getProperty( PROP_DIFF_PROVIDER, TraditionalDiffProvider.class.getName() ); + final String providerClassName = props.getProperty( PROP_DIFF_PROVIDER, SvnStyleDiffProvider.class.getName() ); try { m_provider = ClassUtil.buildInstance( "org.apache.wiki.diff", providerClassName ); } catch( final ReflectiveOperationException e ) { diff --git a/jspwiki-main/src/main/java/org/apache/wiki/diff/SvnStyleDiffProvider.java b/jspwiki-main/src/main/java/org/apache/wiki/diff/SvnStyleDiffProvider.java new file mode 100644 index 0000000000..5804b887fc --- /dev/null +++ b/jspwiki-main/src/main/java/org/apache/wiki/diff/SvnStyleDiffProvider.java @@ -0,0 +1,119 @@ +/* + * Copyright 2025 The Apache Software Foundation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.wiki.diff; + +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; +import java.io.IOException; +import java.util.List; +import java.util.Properties; +import org.apache.wiki.api.core.Context; +import org.apache.wiki.api.core.Engine; +import org.apache.wiki.api.exceptions.NoRequiredPropertyException; + +/** + * SVN/Git style diff provider. Uses the DiffLib, ASF 2.0 licensed. + * + * @since 3.0.0 + */ +public class SvnStyleDiffProvider implements DiffProvider { + + public static final String CSS_DIFF_ADDED = ""; + public static final String CSS_DIFF_REMOVED = ""; + public static final String CSS_DIFF_UNCHANGED = ""; + public static final String CSS_DIFF_CLOSE = "\n"; + public static final String CELL_CHANGE = ""; + + @Override + public String makeDiffHtml(Context context, String originalText, String modifiedText) { + + List original = originalText.lines().toList(); + List modified = modifiedText.lines().toList(); + StringBuilder ret = new StringBuilder(); + ret.append("\n"); + + Patch patch = DiffUtils.diff(original, modified); + int lineNumber = 1; + int currentOriginalLine = 0; + int currentModifiedLine = 0; + + for (AbstractDelta delta : patch.getDeltas()) { + int originalPosition = delta.getSource().getPosition(); + int modifiedPosition = delta.getTarget().getPosition(); + + // Output unchanged lines before the delta + while (currentOriginalLine < originalPosition && currentModifiedLine < modifiedPosition) { + ret.append(CSS_DIFF_UNCHANGED); + ret.append(lineNumber).append(CELL_CHANGE); + ret.append(original.get(currentOriginalLine)); + ret.append(CSS_DIFF_CLOSE); + //System.out.println(" " + lineNumber + " " + original.get(currentOriginalLine)); + lineNumber++; + currentOriginalLine++; + currentModifiedLine++; + } + + List originalLines = delta.getSource().getLines(); + List revisedLines = delta.getTarget().getLines(); + + for (String line : originalLines) { + ret.append(CSS_DIFF_REMOVED); + ret.append(lineNumber).append(CELL_CHANGE); + ret.append(line); + ret.append(CSS_DIFF_CLOSE); + //System.out.println("- " + lineNumber + " " + line); + lineNumber++; + currentOriginalLine++; + } + + for (String line : revisedLines) { + ret.append(CSS_DIFF_ADDED); + ret.append(lineNumber).append(CELL_CHANGE); + ret.append(line); + ret.append(CSS_DIFF_CLOSE); + //System.out.println("+ " + lineNumber + " " + line); + lineNumber++; + currentModifiedLine++; + } + } + + // Output any remaining unchanged lines at the end + while (currentOriginalLine < original.size() && currentModifiedLine < modified.size()) { + ret.append(CSS_DIFF_UNCHANGED); + ret.append(lineNumber).append(CELL_CHANGE); + ret.append(original.get(currentOriginalLine)); + ret.append(CSS_DIFF_CLOSE); + + //System.out.println(" " + lineNumber + " " + original.get(currentOriginalLine)); + lineNumber++; + currentOriginalLine++; + currentModifiedLine++; + } + ret.append("
\n"); + return ret.toString(); + } + + @Override + public void initialize(Engine engine, Properties properties) throws NoRequiredPropertyException, IOException { + } + + @Override + public String getProviderInfo() { + return "SvnStyleDiffProvider"; + } + +} diff --git a/jspwiki-main/src/main/java/org/apache/wiki/diff/TraditionalDiffProvider.java b/jspwiki-main/src/main/java/org/apache/wiki/diff/TraditionalDiffProvider.java deleted file mode 100644 index cbe13992ed..0000000000 --- a/jspwiki-main/src/main/java/org/apache/wiki/diff/TraditionalDiffProvider.java +++ /dev/null @@ -1,187 +0,0 @@ -/* - Licensed to the Apache Software Foundation (ASF) under one - or more contributor license agreements. See the NOTICE file - distributed with this work for additional information - regarding copyright ownership. The ASF licenses this file - to you under the Apache License, Version 2.0 (the - "License"); you may not use this file except in compliance - with the License. You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, - software distributed under the License is distributed on an - "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - KIND, either express or implied. See the License for the - specific language governing permissions and limitations - under the License. -*/ - -package org.apache.wiki.diff; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.wiki.api.core.Context; -import org.apache.wiki.api.core.Engine; -import org.apache.wiki.api.exceptions.NoRequiredPropertyException; -import org.apache.wiki.i18n.InternationalizationManager; -import org.apache.wiki.preferences.Preferences; -import org.apache.wiki.util.TextUtil; -import org.suigeneris.jrcs.diff.Diff; -import org.suigeneris.jrcs.diff.DifferentiationFailedException; -import org.suigeneris.jrcs.diff.Revision; -import org.suigeneris.jrcs.diff.RevisionVisitor; -import org.suigeneris.jrcs.diff.delta.AddDelta; -import org.suigeneris.jrcs.diff.delta.ChangeDelta; -import org.suigeneris.jrcs.diff.delta.Chunk; -import org.suigeneris.jrcs.diff.delta.DeleteDelta; -import org.suigeneris.jrcs.diff.myers.MyersDiff; - -import java.io.IOException; -import java.text.ChoiceFormat; -import java.text.Format; -import java.text.MessageFormat; -import java.text.NumberFormat; -import java.util.Properties; -import java.util.ResourceBundle; - - -/** - * This is the JSPWiki 'traditional' diff. It uses an internal diff engine. - */ -public class TraditionalDiffProvider implements DiffProvider { - - private static final Logger LOG = LogManager.getLogger( TraditionalDiffProvider.class ); - private static final String CSS_DIFF_ADDED = ""; - private static final String CSS_DIFF_REMOVED = ""; - private static final String CSS_DIFF_UNCHANGED = ""; - private static final String CSS_DIFF_CLOSE = "" + Diff.NL; - - /** - * Constructs the provider. - */ - public TraditionalDiffProvider() { - } - - /** - * {@inheritDoc} - * @see org.apache.wiki.api.providers.WikiProvider#getProviderInfo() - */ - @Override - public String getProviderInfo() - { - return "TraditionalDiffProvider"; - } - - /** - * {@inheritDoc} - * @see org.apache.wiki.api.providers.WikiProvider#initialize(org.apache.wiki.api.core.Engine, java.util.Properties) - */ - @Override - public void initialize( final Engine engine, final Properties properties ) throws NoRequiredPropertyException, IOException { - } - - /** - * Makes a diff using the BMSI utility package. We use our own diff printer, - * which makes things easier. - * - * @param ctx The WikiContext in which the diff should be made. - * @param p1 The first string - * @param p2 The second string. - * - * @return Full HTML diff. - */ - @Override - public String makeDiffHtml( final Context ctx, final String p1, final String p2 ) { - final String diffResult; - - try { - final String[] first = Diff.stringToArray(TextUtil.replaceEntities(p1)); - final String[] second = Diff.stringToArray(TextUtil.replaceEntities(p2)); - final Revision rev = Diff.diff(first, second, new MyersDiff()); - - if( rev == null || rev.size() == 0 ) { - // No difference - return ""; - } - - final StringBuffer ret = new StringBuffer(rev.size() * 20); // Guessing how big it will become... - - ret.append( "\n" ); - rev.accept( new RevisionPrint( ctx, ret ) ); - ret.append( "
\n" ); - - return ret.toString(); - } catch( final DifferentiationFailedException e ) { - diffResult = "makeDiff failed with DifferentiationFailedException"; - LOG.error( diffResult, e ); - } - - return diffResult; - } - - - private static final class RevisionPrint implements RevisionVisitor { - - private final StringBuffer m_result; - private final Context m_context; - private final ResourceBundle m_rb; - - private RevisionPrint( final Context ctx, final StringBuffer sb ) { - m_result = sb; - m_context = ctx; - m_rb = Preferences.getBundle( ctx, InternationalizationManager.CORE_BUNDLE ); - } - - @Override - public void visit( final Revision rev ) { - // GNDN (Goes nowhere, does nothing) - } - - @Override - public void visit( final AddDelta delta ) { - final Chunk changed = delta.getRevised(); - print( changed, m_rb.getString( "diff.traditional.added" ) ); - changed.toString( m_result, CSS_DIFF_ADDED, CSS_DIFF_CLOSE ); - } - - @Override - public void visit( final ChangeDelta delta ) { - final Chunk changed = delta.getOriginal(); - print(changed, m_rb.getString( "diff.traditional.changed" ) ); - changed.toString( m_result, CSS_DIFF_REMOVED, CSS_DIFF_CLOSE ); - delta.getRevised().toString( m_result, CSS_DIFF_ADDED, CSS_DIFF_CLOSE ); - } - - @Override - public void visit( final DeleteDelta delta ) { - final Chunk changed = delta.getOriginal(); - print( changed, m_rb.getString( "diff.traditional.removed" ) ); - changed.toString( m_result, CSS_DIFF_REMOVED, CSS_DIFF_CLOSE ); - } - - private void print( final Chunk changed, final String type ) { - m_result.append( CSS_DIFF_UNCHANGED ); - - final String[] choiceString = { - m_rb.getString("diff.traditional.oneline"), - m_rb.getString("diff.traditional.lines") - }; - final double[] choiceLimits = { 1, 2 }; - - final MessageFormat fmt = new MessageFormat(""); - fmt.setLocale( Preferences.getLocale(m_context) ); - final ChoiceFormat cfmt = new ChoiceFormat( choiceLimits, choiceString ); - fmt.applyPattern( type ); - final Format[] formats = { NumberFormat.getInstance(), cfmt, NumberFormat.getInstance() }; - fmt.setFormats( formats ); - - final Object[] params = { changed.first() + 1, - changed.size(), - changed.size() }; - m_result.append( fmt.format(params) ); - m_result.append( CSS_DIFF_CLOSE ); - } - } - -} diff --git a/jspwiki-main/src/main/java/org/apache/wiki/filters/SpamFilter.java b/jspwiki-main/src/main/java/org/apache/wiki/filters/SpamFilter.java index 08d11b4a96..78ff1f1273 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/filters/SpamFilter.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/filters/SpamFilter.java @@ -18,6 +18,9 @@ Licensed to the Apache Software Foundation (ASF) under one */ package org.apache.wiki.filters; +import com.github.difflib.DiffUtils; +import com.github.difflib.patch.AbstractDelta; +import com.github.difflib.patch.Patch; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.StopWatch; import org.apache.logging.log4j.LogManager; @@ -46,14 +49,6 @@ Licensed to the Apache Software Foundation (ASF) under one import org.apache.wiki.util.FileUtil; import org.apache.wiki.util.HttpUtil; import org.apache.wiki.util.TextUtil; -import org.suigeneris.jrcs.diff.Diff; -import org.suigeneris.jrcs.diff.DifferentiationFailedException; -import org.suigeneris.jrcs.diff.Revision; -import org.suigeneris.jrcs.diff.delta.AddDelta; -import org.suigeneris.jrcs.diff.delta.ChangeDelta; -import org.suigeneris.jrcs.diff.delta.DeleteDelta; -import org.suigeneris.jrcs.diff.delta.Delta; -import org.suigeneris.jrcs.diff.myers.MyersDiff; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -808,31 +803,48 @@ private static Change getChange( final Context context, final String newText ) { try { final String oldText = engine.getManager( PageManager.class ).getPureText( page.getName(), WikiProvider.LATEST_VERSION ); - final String[] first = Diff.stringToArray( oldText ); - final String[] second = Diff.stringToArray( newText ); - final Revision rev = Diff.diff( first, second, new MyersDiff() ); - - if( rev == null || rev.size() == 0 ) { + Patch patch = DiffUtils.diffInline(oldText, newText); + + + if( patch == null ) { return ch; } - - for( int i = 0; i < rev.size(); i++ ) { - final Delta d = rev.getDelta( i ); + int lineNumber = 1; + int currentOriginalLine = 0; + int currentModifiedLine = 0; + + for (AbstractDelta delta : patch.getDeltas()) { + int originalPosition = delta.getSource().getPosition(); + int modifiedPosition = delta.getTarget().getPosition(); + + // Output unchanged lines before the delta + while (currentOriginalLine < originalPosition && currentModifiedLine < modifiedPosition) { + lineNumber++; + currentOriginalLine++; + currentModifiedLine++; + } - if( d instanceof AddDelta ) { - d.getRevised().toString( change, "", "\r\n" ); - ch.m_adds++; - - } else if( d instanceof ChangeDelta ) { - d.getRevised().toString( change, "", "\r\n" ); - ch.m_adds++; - - } else if( d instanceof DeleteDelta ) { + List originalLines = delta.getSource().getLines(); + List revisedLines = delta.getTarget().getLines(); + + for (String line : originalLines) { + change.append("- " + lineNumber + ": " + line + "\r\n"); ch.m_removals++; + lineNumber++; + currentOriginalLine++; + } + + for (String line : revisedLines) { + change.append("+ " + lineNumber + ": " + line + "\r\n"); + lineNumber++; + currentModifiedLine++; + ch.m_adds++; + } } - } catch( final DifferentiationFailedException e ) { - LOG.error( "Diff failed", e ); + + } catch (final Exception e) { + LOG.error("Diff failed", e); } // Don't forget to include the change note, too diff --git a/jspwiki-main/src/main/resources/ini/jspwiki.properties b/jspwiki-main/src/main/resources/ini/jspwiki.properties index 261b82c37b..d8869738b6 100644 --- a/jspwiki-main/src/main/resources/ini/jspwiki.properties +++ b/jspwiki-main/src/main/resources/ini/jspwiki.properties @@ -190,14 +190,9 @@ jspwiki.attachment.forceDownload= .html .htm .js .pdf .svg .xml # To show differences between page versions, you can define a # difference provider. # The following choices are available: -# * TraditionalDiffProvider - Uses internal (java) diff +# * SvnStyleDiffProvider - Uses internal (java) diff # to create a list of changes and shows it line by # line colored. This is the default -# * ContextualDiffProvider - Uses internal (java) diff -# to create changes inline and shows it on a word by -# word basis using CSS. This is much superior to the -# traditional diff provider, however, it is still quite -# new and not much tested. YMMV. # * ExternalDiffProvider - uses a system diff program (which # can be configured using "jspwiki.diffCommand") to # create a unified (!) diff. @@ -205,7 +200,7 @@ jspwiki.attachment.forceDownload= .html .htm .js .pdf .svg .xml # Example for a diff command: # jspwiki.diffCommand = /usr/bin/diff -u %s1 %s2 # -jspwiki.diffProvider = TraditionalDiffProvider +jspwiki.diffProvider = SvnStyleDiffProvider # # Page references diff --git a/jspwiki-main/src/test/java/org/apache/wiki/diff/ContextualDiffProviderTest.java b/jspwiki-main/src/test/java/org/apache/wiki/diff/SvnStyleDiffProviderTest.java similarity index 51% rename from jspwiki-main/src/test/java/org/apache/wiki/diff/ContextualDiffProviderTest.java rename to jspwiki-main/src/test/java/org/apache/wiki/diff/SvnStyleDiffProviderTest.java index 999b7811b2..9d1d9471d9 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/diff/ContextualDiffProviderTest.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/diff/SvnStyleDiffProviderTest.java @@ -27,48 +27,21 @@ Licensed to the Apache Software Foundation (ASF) under one import java.io.IOException; import java.util.Properties; +import org.apache.commons.lang3.StringUtils; -public class ContextualDiffProviderTest { +public class SvnStyleDiffProviderTest { - /** - * Sets up some shorthand notation for writing test cases. - *

- * The quick |^Brown Fox^-Blue Monster-| jumped over |^the^| moon. - *

- * Get it? - */ - private void specializedNotation( final ContextualDiffProvider diff ) { - diff.CHANGE_END_HTML = "|"; - diff.CHANGE_START_HTML = "|"; - - diff.DELETION_END_HTML = "-"; - diff.DELETION_START_HTML = "-"; - - diff.DIFF_END = ""; - diff.DIFF_START = ""; - - diff.ELIDED_HEAD_INDICATOR_HTML = "..."; - diff.ELIDED_TAIL_INDICATOR_HTML = "..."; - - diff.m_emitChangeNextPreviousHyperlinks = false; - - diff.INSERTION_END_HTML = "^"; - diff.INSERTION_START_HTML = "^"; - - diff.LINE_BREAK_HTML = ""; - diff.ALTERNATING_SPACE_HTML = "_"; - } @Test public void testNoChanges() throws IOException, WikiException { - diffTest( null, "", "", "" ); - diffTest( null, "A", "A", "A" ); - diffTest( null, "A B", "A B", "A B" ); + diffTest( "", "",0); + diffTest( "A", "A",0); + diffTest( "A B", "A B",0); - diffTest( null, " ", " ", " _ _ _" ); - diffTest( null, "A B C", "A B C", "A B _C" ); - diffTest( null, "A B C", "A B C", "A B _ C" ); + diffTest( " ", " ",0); + diffTest( "A B C", "A B C",0); + diffTest( "A B C", "A B C",0); } @Test @@ -79,63 +52,63 @@ public void testSimpleInsertions() throws IOException, WikiException { // when writing tests. // Simple inserts... - diffTest( null, "A C", "A B C", "A |^B ^|C" ); - diffTest( null, "A D", "A B C D", "A |^B C ^|D" ); + diffTest( "A C", "A B C", 1 ); + diffTest( "A D", "A B C D", 1 ); // Simple inserts with spaces... - diffTest( null, "A C", "A B C", "A |^B _^|C" ); - diffTest( null, "A C", "A B C", "A |^B _ ^|C" ); - diffTest( null, "A C", "A B C", "A |^B _ _^|C" ); + diffTest( "A C", "A B C", 1); + diffTest( "A C", "A B C", 1 ); + diffTest( "A C", "A B C", 1 ); // Just inserted spaces... - diffTest( null, "A B", "A B", "A |^_^|B" ); - diffTest( null, "A B", "A B", "A |^_ ^|B" ); - diffTest( null, "A B", "A B", "A |^_ _^|B" ); - diffTest( null, "A B", "A B", "A |^_ _ ^|B" ); + diffTest( "A B", "A B", 1 ); + diffTest( "A B", "A B",1 ); + diffTest( "A B", "A B", 1 ); + diffTest( "A B", "A B", 1 ); } @Test public void testSimpleDeletions() throws IOException, WikiException { // Simple deletes... - diffTest( null, "A B C", "A C", "A |-B -|C" ); - diffTest( null, "A B C D", "A D", "A |-B C -|D" ); + diffTest( "A B C", "A C", 1 ); + diffTest( "A B C D", "A D", 1 ); // Simple deletes with spaces... - diffTest( null, "A B C", "A C", "A |-B _-|C" ); - diffTest( null, "A B C", "A C", "A |-B _ -|C" ); + diffTest( "A B C", "A C", 1 ); + diffTest( "A B C", "A C", 1 ); // Just deleted spaces... - diffTest( null, "A B", "A B", "A |-_-|B" ); - diffTest( null, "A B", "A B", "A |-_ -|B" ); - diffTest( null, "A B", "A B", "A |-_ _-|B" ); + diffTest( "A B", "A B", 1 ); + diffTest( "A B", "A B", 1 ); + diffTest( "A B", "A B", 1 ); } @Test public void testContextLimits() throws IOException, WikiException { // No change - diffTest( "1", "A B C D E F G H I", "A B C D E F G H I", "A..." ); + diffTest( "A B C D E F G H I", "A B C D E F G H I", 0 ); //TODO Hmm, should the diff provider instead return the string, "No Changes"? // Bad property value, should default to huge context limit and return entire string. - diffTest( "foobar", "A B C D E F G H I", "A B C D F G H I", "A B C D |-E -|F G H I" ); + diffTest( "A B C D E F G H I", "A B C D F G H I", 1 ); // One simple deletion, limit context to 2... - diffTest( "2", "A B C D E F G H I", "A B C D F G H I", "...D |-E -|F ..." ); + diffTest( "A B C D E F G H I", "A B C D F G H I", 1); // Deletion of first element, limit context to 2... - diffTest( "2", "A B C D E", "B C D E", "|-A -|B ..." ); + diffTest( "A B C D E", "B C D E", 1); // Deletion of last element, limit context to 2... - diffTest( "2", "A B C D E", "A B C D ", "...D |-E-|" ); + diffTest( "A B C D E", "A B C D ", 1 ); // Two simple deletions, limit context to 2... - diffTest( "2", "A B C D E F G H I J K L M N O P", "A B C E F G H I J K M N O P", "...C |-D -|E ......K |-L -|M ..." ); + diffTest( "A B C D E F G H I J K L M N O P", "A B C E F G H I J K M N O P", 1 ); } @Test public void testMultiples() throws IOException, WikiException { - diffTest( null, "A F", "A B C D E F", "A |^B C D E ^|F" ); - diffTest( null, "A B C D E F", "A F", "A |-B C D E -|F" ); + diffTest( "A F", "A B C D E F", 1 ); + diffTest( "A B C D E F", "A F", 1 ); } @Test @@ -143,25 +116,25 @@ public void testSimpleChanges() throws IOException, WikiException { // *changes* are actually an insert and a delete in the output... //single change - diffTest( null, "A B C", "A b C", "A |^b^-B-| C" ); + diffTest( "A B C", "A b C", 1 ); //non-consequtive changes... - diffTest( null, "A B C D E", "A b C d E", "A |^b^-B-| C |^d^-D-| E" ); + diffTest( "A B C D E", "A b C d E", 1 ); } // FIXME: This test Assertions.fails; must be enabled again asap. - /* + @Test - public void testKnownProblemCases() throws NoRequiredPropertyException, IOException + public void testKnownProblemCases() throws Exception { //These all Assertions.fail... //make two consequtive changes - diffTest(null, "A B C D", "A b c D", "A |^b c^-B C-| D"); + diffTest( "A B C D", "A b c D", 1); //acually returns -> "A |^b^-B-| |^c^-C-| D" //collapse adjacent elements... - diffTest(null, "A B C D", "A BC D", "A |^BC^-B C-| D"); + diffTest( "A B C D", "A BC D", 1); //acually returns -> "A |^BC^-B-| |-C -|D" @@ -169,26 +142,23 @@ public void testKnownProblemCases() throws NoRequiredPropertyException, IOExcept //adjacent edits into one... } - */ + - private void diffTest( final String contextLimit, final String oldText, final String newText, final String expectedDiff ) + private void diffTest( final String oldText, final String newText, int expected ) throws IOException, WikiException { - final ContextualDiffProvider diff = new ContextualDiffProvider(); - - specializedNotation( diff ); + final SvnStyleDiffProvider diff = new SvnStyleDiffProvider(); final Properties props = TestEngine.getTestProperties(); - if( null != contextLimit ) { - props.put( ContextualDiffProvider.PROP_UNCHANGED_CONTEXT_LIMIT, contextLimit ); - } - + diff.initialize( null, props ); final TestEngine engine = new TestEngine( props ); final Context ctx = Wiki.context().create( engine, Wiki.contents().page( engine, "Dummy" ) ); final String actualDiff = diff.makeDiffHtml( ctx, oldText, newText ); - - Assertions.assertEquals( expectedDiff, actualDiff ); + int actuals = StringUtils.countMatches(actualDiff, SvnStyleDiffProvider.CSS_DIFF_ADDED ) + + StringUtils.countMatches(actualDiff, SvnStyleDiffProvider.CSS_DIFF_REMOVED ); + + Assertions.assertEquals( expected*2, actuals , actualDiff); } } diff --git a/pom.xml b/pom.xml index 0dfbd1d807..4d811eb282 100644 --- a/pom.xml +++ b/pom.xml @@ -68,6 +68,7 @@ 2.0.2 2.1.5 6.1.0 + 4.16 0.8.14 0.4.0 2.0.0 @@ -283,6 +284,12 @@ jakarta.servlet.jsp.jstl-api ${jakarta-jstl-api.version} + + + io.github.java-diff-utils + java-diff-utils + ${java-diff-utils-version} + jaxen @@ -428,12 +435,6 @@ ${jdom2.version} - - org.jvnet.hudson - org.suigeneris.jrcs.diff - ${jrcs-diff.version} - - org.slf4j slf4j-api