001    /*
002     * Copyright (C) 2000-2001 Iowa State University
003     * 
004     * This file is part of mjc, the MultiJava Compiler, and the JML Project.
005     * 
006     * This program is free software; you can redistribute it and/or modify it under
007     * the terms of the GNU General Public License as published by the Free Software
008     * Foundation; either version 2 of the License, or (at your option) any later
009     * version.
010     * 
011     * This program is distributed in the hope that it will be useful, but WITHOUT
012     * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
013     * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
014     * details.
015     * 
016     * You should have received a copy of the GNU General Public License along with
017     * this program; if not, write to the Free Software Foundation, Inc., 59 Temple
018     * Place, Suite 330, Boston, MA 02111-1307 USA
019     * 
020     * $Id: Diff.java,v 1.6 2006/08/05 11:18:50 chalin Exp $
021     */
022    
023    package junitutils;
024    
025    import java.util.StringTokenizer;
026    import java.util.ArrayList;
027    
028    /**
029     * Class for calculating a (somewhat) detailed comparison of two strings.
030     * 
031     * @author Curtis Clifton
032     * @version $Revision: 1.6 $
033     */
034    
035    public class Diff {
036    
037      //---------------------------------------------------------------------
038      // CONSTRUCTORS
039      //---------------------------------------------------------------------
040      /**
041       * Calculate a difference between the given strings.
042       * 
043       * @param oldTextLabel a label for the <code>oldText</code> parameter
044       * @param oldText a value to be compared
045       * @param newTextLabel a label for the <code>newText</code> parameter
046       * @param newText a value to be compared
047       */
048      public Diff(/*@ non_null */ String oldTextLabel, 
049                  /*@ non_null */ String oldText, 
050                  /*@ non_null */ String newTextLabel,
051                  /*@ non_null */ String newText) 
052      {
053        this.oldText = oldText;
054        this.newText = newText;
055        calculate(oldTextLabel, newTextLabel);
056      }
057    
058      //---------------------------------------------------------------------
059      // OPERATIONS
060      //---------------------------------------------------------------------
061    
062      /**
063       * Sets the values of _areDifferent and differences according to a comparison
064       * between oldText and newText
065       * 
066       * @param oldTextLabel a label for the <code>oldText</code> parameter
067       * @param newTextLabel a label for the <code>newText</code> parameter
068       */
069      //@ ensures _areDifferent == areDifferent;
070      private /*@ helper */ void calculate(/*@ non_null */ String oldTextLabel, 
071                                           /*@ non_null */ String newTextLabel) {
072        // Accumulate the diff in differencesSB
073        StringBuffer differencesSB = new StringBuffer(newText.length());
074    
075        calculateDiffs(differencesSB);
076        if (differencesSB.length() > 0) {
077          // Some diffs accumulated, so the strings are different
078          _areDifferent = true;
079    
080          // Prepend a key to the results.
081          differences = NEWLINE + OLD_CH + oldTextLabel + NEWLINE + NEW_CH
082              + newTextLabel + NEWLINE + NEWLINE + differencesSB.toString();
083        } else {
084          // No diffs accumulated, so the strings must be the same
085          _areDifferent = false;
086          differences = "";
087        }
088      }
089    
090      private /*@ helper */ void calculateDiffs(/*@ non_null */ StringBuffer differencesSB) {
091        String[] oldTextLines = splitByLine(oldText);
092        String[] newTextLines = splitByLine(newText);
093    
094        // match by lines
095        int oPos = 0;
096        int nPos = 0;
097        int lastOldMatch = -1;
098        int lastNewMatch = -1;
099        if (oldTextLines.length > 0 && newTextLines.length > 0)
100         while (oPos < oldTextLines.length || nPos < newTextLines.length) {
101          // this is a pretty dumb algorithm that doesn't handle
102          // things like the insertion of a single new line in one
103          // string or grouping
104          if (oPos >= oldTextLines.length) oPos = oldTextLines.length-1;
105          if (nPos >= newTextLines.length) nPos = newTextLines.length-1;
106          boolean matched = false;
107          for (int i = lastOldMatch+1; i<=oPos; ++i) {
108            if (oldTextLines[i].equals(newTextLines[nPos])) {
109              // Got a match
110              for (int j=lastOldMatch+1; j<i; ++j)
111                differencesSB.append((j+1) + OLD_CH + oldTextLines[j] + NEWLINE);
112              for (int j=lastNewMatch+1; j<nPos; ++j)
113                differencesSB.append((j+1) + NEW_CH + newTextLines[j] + NEWLINE);
114              lastOldMatch = i;
115              lastNewMatch = nPos;
116              oPos = i+1;
117              nPos++;
118              matched = true;
119              break;
120            }
121          }
122          if (matched) continue;
123          for (int i = lastNewMatch+1; i<=nPos; ++i) {
124            if (newTextLines[i].equals(oldTextLines[oPos])) {
125              // Got a match
126              for (int j=lastOldMatch+1; j<oPos; ++j)
127                differencesSB.append((j+1) + OLD_CH + oldTextLines[j] + NEWLINE);
128              for (int j=lastNewMatch+1; j<i; ++j)
129                differencesSB.append((j+1) + NEW_CH + newTextLines[j] + NEWLINE);
130              lastOldMatch = oPos;
131              lastNewMatch = i;
132              oPos++;
133              nPos = i + 1;
134              matched = true;
135              break;        }
136          }
137          if (matched) continue;
138          oPos++;
139          nPos++;
140        }
141        // If we reached the end of one array before the other, then this
142        // will print the remainders.
143        for (oPos=lastOldMatch+1; oPos < oldTextLines.length; oPos++) {
144          differencesSB.append((oPos + 1) + OLD_CH + oldTextLines[oPos] + NEWLINE);
145        } // end of for ()
146        for (nPos=lastNewMatch+1; nPos < newTextLines.length; nPos++) {
147          differencesSB.append((nPos + 1) + NEW_CH + newTextLines[nPos] + NEWLINE);
148        } // end of for ()
149      }
150    
151      //@ ensures \nonnullelements(\result);
152      private /*@ helper non_null */ String[] splitByLine(/*@ non_null */ String text) {
153        // thanks to Windows ridiculous two character newlines it is
154        // hard to detect blank lines, so we don't bother trying
155        StringTokenizer toker = new StringTokenizer(text, DELIM, false);
156        ArrayList lines = new ArrayList(oldText.length() / 60);
157        while (toker.hasMoreTokens()) {
158          String tok = toker.nextToken();
159          lines.add(tok);
160        }
161        String[] result = new String[lines.size()];
162        lines.toArray(result);
163        return result;
164      }
165    
166      /**
167       * Returns true if strings on which this was constructed are different.
168       */
169      /*@ public normal_behavior
170        @ ensures \result == areDifferent;
171        @*/
172      public /*@ pure @*/ boolean areDifferent() {
173        return _areDifferent;
174      }
175    
176      /**
177       * Returns the differences between the given strings.
178       *  
179       */
180      //@ private normal_behavior
181      //@ ensures \result == differences;
182      //@ pure
183      public String result() {
184        return differences;
185      }
186    
187      //---------------------------------------------------------------------
188      // PRIVILEGED DATA
189      //---------------------------------------------------------------------
190    
191      /** This is the supplied old text, to be compared against the new text */
192      //@ non_null
193      private String oldText;
194    
195      /** This is the supplied new text, to be compared against the old text */
196      //@ non_null
197      private String newText;
198    
199      /** This is set to true if the oldText and newText are not the same */
200      private boolean _areDifferent;
201    
202      /**
203       * This output String holds the description of the differences between the old
204       * and new text.
205       */
206      /*@ spec_public */ private /*@ non_null */ String differences;
207    
208      //@ public model boolean areDifferent;
209      //@ public represents areDifferent <- !differences.equals("");
210    
211      //@ private invariant _areDifferent == areDifferent;
212    
213      /** This string holds line delimiters */
214      //@ non_null
215      private static final String DELIM = "\n\r\f";
216    
217      /** This is the system new line character */
218      //@ non_null
219      private static final String NEWLINE = System.getProperty("line.separator");
220    
221      /** This string is used to mark lines of old text */
222      //@ non_null
223      private static final String OLD_CH = "< ";
224    
225      /** This string is used to mark lines of new text */
226      //@ non_null
227      private static final String NEW_CH = "> ";
228    }