001/*
002 * $Log: DiffPrint.java,v $
003 * Revision 1.10  2009/04/20 18:53:29  stuart
004 * Make print_header public.
005 *
006 * Revision 1.9  2009/01/19 03:05:26  stuart
007 * Fix StackOverflow bug with heuristic on reported by Jimmy Han.
008 *
009 * Revision 1.8  2007/12/20 05:04:21  stuart
010 * Can no longer import from default package.
011 *
012 * Revision 1.7  2007/12/20 04:29:53  stuart
013 * Fix setupOutput permission.  Thanks to Sargon Benjamin
014 *
015 * Revision 1.6  2005/04/27 02:13:40  stuart
016 * Add Str.dup()
017 *
018 * Revision 1.5  2004/01/29 02:35:35  stuart
019 * Test for out of bounds exception in UnifiedPrint.print_hunk.
020 * Add setOutput() to DiffPrint.Base.
021 *
022 * Revision 1.4  2003/04/22  01:50:47  stuart
023 * add Unified format diff
024 *
025 * Revision 1.3  2003/04/22  01:00:32  stuart
026 * added context diff format
027 *
028 * Revision 1.2  2000/03/02  16:59:54  stuart
029 * add GPL
030 *
031 */
032//package bmsi.util;
033package ca.bc.webarts.tools;
034
035import java.io.*;
036import java.util.Vector;
037import java.util.Date;
038//import bmsi.util.Diff;
039import ca.bc.webarts.tools.Diff;
040//import com.objectspace.jgl.predicates.UnaryPredicate;
041
042interface UnaryPredicate {
043  boolean execute(Object obj);
044}
045
046/** A simple framework for printing change lists produced by <code>Diff</code>.
047    @see bmsi.util.Diff
048    @author Stuart D. Gathman
049    Copyright (C) 2000 Business Management Systems, Inc.
050<p>
051    This program is free software; you can redistribute it and/or modify
052    it under the terms of the GNU General Public License as published by
053    the Free Software Foundation; either version 1, or (at your option)
054    any later version.
055<p>
056    This program is distributed in the hope that it will be useful,
057    but WITHOUT ANY WARRANTY; without even the implied warranty of
058    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
059    GNU General Public License for more details.
060<p>
061    You should have received a copy of the GNU General Public License
062    along with this program; if not, write to the Free Software
063    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
064 */
065public class DiffPrint {
066  /** A Base class for printing edit scripts produced by Diff.
067      This class divides the change list into "hunks", and calls
068      <code>print_hunk</code> for each hunk.  Various utility methods
069      are provided as well.
070   */
071  public static abstract class Base {
072    protected PrintWriter outfile;
073    public void setOutput(Writer wtr) {
074      outfile = new PrintWriter(wtr);
075    }
076    protected void setupOutput() {
077      if (outfile == null)
078  outfile = new PrintWriter(new OutputStreamWriter(System.out));
079    }
080    protected Base(Object[] a,Object[] b) {
081      file0 = a;
082      file1 = b;
083    }
084    /** Set to ignore certain kinds of lines when printing
085  an edit script.  For example, ignoring blank lines or comments.
086     */
087    protected UnaryPredicate ignore = null;
088
089    /** Set to the lines of the files being compared.
090     */
091    protected Object[] file0, file1;
092
093    /** Divide SCRIPT into pieces by calling HUNKFUN and
094       print each piece with PRINTFUN.
095       Both functions take one arg, an edit script.
096
097       PRINTFUN takes a subscript which belongs together (with a null
098       link at the end) and prints it.  */
099    public void print_script(Diff.change script) {
100      setupOutput();
101      Diff.change next = script;
102
103      while (next != null)
104  {
105    Diff.change t, end;
106
107    /* Find a set of changes that belong together.  */
108    t = next;
109    end = hunkfun(next);
110
111    /* Disconnect them from the rest of the changes,
112       making them a hunk, and remember the rest for next iteration.  */
113    next = end.link;
114    end.link = null;
115    //if (DEBUG)
116    //  debug_script(t);
117
118    /* Print this hunk.  */
119    print_hunk(t);
120
121    /* Reconnect the script so it will all be freed properly.  */
122    end.link = next;
123  }
124  outfile.flush();
125    }
126
127    /** Called with the tail of the script
128       and returns the last link that belongs together with the start
129       of the tail. */
130
131    protected Diff.change hunkfun(Diff.change hunk) {
132      return hunk;
133    }
134
135    protected int first0, last0, first1, last1, deletes, inserts;
136
137    /** Look at a hunk of edit script and report the range of lines in each file
138      that it applies to.  HUNK is the start of the hunk, which is a chain
139      of `struct change'.  The first and last line numbers of file 0 are stored
140      in *FIRST0 and *LAST0, and likewise for file 1 in *FIRST1 and *LAST1.
141      Note that these are internal line numbers that count from 0.
142
143      If no lines from file 0 are deleted, then FIRST0 is LAST0+1.
144
145      Also set *DELETES nonzero if any lines of file 0 are deleted
146      and set *INSERTS nonzero if any lines of file 1 are inserted.
147      If only ignorable lines are inserted or deleted, both are
148      set to 0.  */
149
150    protected void analyze_hunk(Diff.change hunk) {
151      int f0, l0 = 0, f1, l1 = 0, show_from = 0, show_to = 0;
152      int i;
153      Diff.change next;
154      boolean nontrivial = (ignore == null);
155
156      show_from = show_to = 0;
157
158      f0 = hunk.line0;
159      f1 = hunk.line1;
160
161      for (next = hunk; next != null; next = next.link)
162  {
163    l0 = next.line0 + next.deleted - 1;
164    l1 = next.line1 + next.inserted - 1;
165    show_from += next.deleted;
166    show_to += next.inserted;
167    for (i = next.line0; i <= l0 && ! nontrivial; i++)
168      if (!ignore.execute(file0[i]))
169        nontrivial = true;
170    for (i = next.line1; i <= l1 && ! nontrivial; i++)
171      if (!ignore.execute(file1[i]))
172        nontrivial = true;
173  }
174
175      first0 = f0;
176      last0 = l0;
177      first1 = f1;
178      last1 = l1;
179
180      /* If all inserted or deleted lines are ignorable,
181   tell the caller to ignore this hunk.  */
182
183      if (!nontrivial)
184  show_from = show_to = 0;
185
186      deletes = show_from;
187      inserts = show_to;
188    }
189
190    /** Called to print the script header which identifies the files compared.
191      The default does nothing (except set output to system.out if
192      not otherwise set).  Derived style classes can override to print
193      the files compared in the format for that style.
194     */
195    public void print_header(String filea, String fileb) {
196      setupOutput();
197    }
198
199    protected abstract void print_hunk(Diff.change hunk);
200
201    protected void print_1_line(String pre,Object linbuf) {
202      outfile.println(pre + linbuf.toString());
203    }
204
205    /** Print a pair of line numbers with SEPCHAR, translated for file FILE.
206       If the two numbers are identical, print just one number.
207
208       Args A and B are internal line numbers.
209       We print the translated (real) line numbers.  */
210
211    protected void print_number_range (char sepchar, int a, int b) {
212      /* Note: we can have B < A in the case of a range of no lines.
213   In this case, we should print the line number before the range,
214   which is B.  */
215      if (++b > ++a)
216  outfile.print("" + a + sepchar + b);
217      else
218  outfile.print(b);
219    }
220
221    public static char change_letter(int inserts, int deletes) {
222      if (inserts == 0)
223  return 'd';
224      else if (deletes == 0)
225  return 'a';
226      else
227  return 'c';
228    }
229  }
230
231  /** Print a change list in the standard diff format.
232   */
233  public static class NormalPrint extends Base {
234
235    public NormalPrint(Object[] a,Object[] b) {
236      super(a,b);
237    }
238
239    /** Print a hunk of a normal diff.
240       This is a contiguous portion of a complete edit script,
241       describing changes in consecutive lines.  */
242
243    protected void print_hunk (Diff.change hunk) {
244
245      /* Determine range of line numbers involved in each file.  */
246      analyze_hunk(hunk);
247      if (deletes == 0 && inserts == 0)
248  return;
249
250      /* Print out the line number header for this hunk */
251      print_number_range (',', first0, last0);
252      outfile.print(change_letter(inserts, deletes));
253      print_number_range (',', first1, last1);
254      outfile.println();
255
256      /* Print the lines that the first file has.  */
257      if (deletes != 0)
258  for (int i = first0; i <= last0; i++)
259    print_1_line ("< ", file0[i]);
260
261      if (inserts != 0 && deletes != 0)
262  outfile.println("---");
263
264      /* Print the lines that the second file has.  */
265      if (inserts != 0)
266  for (int i = first1; i <= last1; i++)
267    print_1_line ("> ", file1[i]);
268    }
269  }
270
271  /** Prints an edit script in a format suitable for input to <code>ed</code>.
272      The edit script must be generated with the reverse option to
273      be useful as actual <code>ed</code> input.
274   */
275  public static class EdPrint extends Base {
276
277    public EdPrint(Object[] a,Object[] b) {
278      super(a,b);
279    }
280
281    /** Print a hunk of an ed diff */
282    protected void print_hunk(Diff.change hunk) {
283
284      /* Determine range of line numbers involved in each file.  */
285      analyze_hunk (hunk);
286      if (deletes == 0 && inserts == 0)
287  return;
288
289      /* Print out the line number header for this hunk */
290      print_number_range (',', first0, last0);
291      outfile.println(change_letter(inserts, deletes));
292
293      /* Print new/changed lines from second file, if needed */
294      if (inserts != 0)
295  {
296    boolean inserting = true;
297    for (int i = first1; i <= last1; i++)
298      {
299        /* Resume the insert, if we stopped.  */
300        if (! inserting)
301    outfile.println(i - first1 + first0 + "a");
302        inserting = true;
303
304        /* If the file's line is just a dot, it would confuse `ed'.
305     So output it with a double dot, and set the flag LEADING_DOT
306     so that we will output another ed-command later
307     to change the double dot into a single dot.  */
308
309        if (".".equals(file1[i]))
310    {
311      outfile.println("..");
312      outfile.println(".");
313      /* Now change that double dot to the desired single dot.  */
314      outfile.println(i - first1 + first0 + 1 + "s/^\\.\\././");
315      inserting = false;
316    }
317        else
318    /* Line is not `.', so output it unmodified.  */
319    print_1_line ("", file1[i]);
320      }
321
322    /* End insert mode, if we are still in it.  */
323    if (inserting)
324      outfile.println(".");
325  }
326    }
327  }
328
329  /** Prints an edit script in context diff format.  This and its
330    'unified' variation is used for source code patches.
331   */
332  public static class ContextPrint extends Base {
333
334    protected int context = 3;
335
336    public ContextPrint(Object[] a,Object[] b) {
337      super(a,b);
338    }
339
340    protected void print_context_label (String mark, File inf, String label) {
341      setupOutput();
342      if (label != null)
343  outfile.println(mark + ' ' + label);
344      else if (inf.lastModified() > 0)
345        // FIXME: use DateFormat to get precise format needed.
346  outfile.println(
347    mark + ' ' + inf.getPath() + '\t' + new Date(inf.lastModified())
348  );
349      else
350  /* Don't pretend that standard input is ancient.  */
351  outfile.println(mark + ' ' + inf.getPath());
352    }
353
354    public void print_header(String filea,String fileb) {
355      print_context_label ("***", new File(filea), filea);
356      print_context_label ("---", new File(fileb), fileb);
357    }
358
359    /** If function_regexp defined, search for start of function. */
360    private String find_function(Object[] lines, int start) {
361      return null;
362    }
363
364    protected void print_function(Object[] file,int start) {
365      String function = find_function (file0, first0);
366      if (function != null) {
367  outfile.print(" ");
368  outfile.print(
369    (function.length() < 40) ? function : function.substring(0,40)
370  );
371      }
372    }
373
374    protected void print_hunk(Diff.change hunk) {
375
376      /* Determine range of line numbers involved in each file.  */
377
378      analyze_hunk (hunk);
379
380      if (deletes == 0 && inserts == 0)
381  return;
382
383      /* Include a context's width before and after.  */
384
385      first0 = Math.max(first0 - context, 0);
386      first1 = Math.max(first1 - context, 0);
387      last0 = Math.min(last0 + context, file0.length - 1);
388      last1 = Math.min(last1 + context, file1.length - 1);
389
390
391      outfile.print("***************");
392
393      /* If we looked for and found a function this is part of,
394   include its name in the header of the diff section.  */
395      print_function (file0, first0);
396
397      outfile.println();
398      outfile.print("*** ");
399      print_number_range (',', first0, last0);
400      outfile.println(" ****");
401
402      if (deletes != 0) {
403  Diff.change next = hunk;
404
405  for (int i = first0; i <= last0; i++) {
406    /* Skip past changes that apply (in file 0)
407       only to lines before line I.  */
408
409    while (next != null && next.line0 + next.deleted <= i)
410      next = next.link;
411
412    /* Compute the marking for line I.  */
413
414    String prefix = " ";
415    if (next != null && next.line0 <= i)
416      /* The change NEXT covers this line.
417         If lines were inserted here in file 1, this is "changed".
418         Otherwise it is "deleted".  */
419      prefix = (next.inserted > 0) ? "!" : "-";
420
421    print_1_line (prefix, file0[i]);
422  }
423      }
424
425      outfile.print("--- ");
426      print_number_range (',', first1, last1);
427      outfile.println(" ----");
428
429      if (inserts != 0) {
430  Diff.change next = hunk;
431
432  for (int i = first1; i <= last1; i++) {
433    /* Skip past changes that apply (in file 1)
434       only to lines before line I.  */
435
436    while (next != null && next.line1 + next.inserted <= i)
437      next = next.link;
438
439    /* Compute the marking for line I.  */
440
441    String prefix = " ";
442    if (next != null && next.line1 <= i)
443      /* The change NEXT covers this line.
444         If lines were deleted here in file 0, this is "changed".
445         Otherwise it is "inserted".  */
446      prefix = (next.deleted > 0) ? "!" : "+";
447
448    print_1_line (prefix, file1[i]);
449  }
450      }
451    }
452  }
453
454  /** Prints an edit script in context diff format.  This and its
455    'unified' variation is used for source code patches.
456   */
457  public static class UnifiedPrint extends ContextPrint {
458
459    public UnifiedPrint(Object[] a,Object[] b) {
460      super(a,b);
461    }
462
463    public void print_header(String filea,String fileb) {
464      print_context_label ("---", new File(filea), filea);
465      print_context_label ("+++", new File(fileb), fileb);
466    }
467
468    private void print_number_range (int a, int b) {
469      //translate_range (file, a, b, &trans_a, &trans_b);
470
471      /* Note: we can have B < A in the case of a range of no lines.
472   In this case, we should print the line number before the range,
473   which is B.  */
474      if (b < a)
475  outfile.print(b + ",0");
476      else
477        super.print_number_range(',',a,b);
478    }
479
480    protected void print_hunk(Diff.change hunk) {
481      /* Determine range of line numbers involved in each file.  */
482      analyze_hunk (hunk);
483
484      if (deletes == 0 && inserts == 0)
485  return;
486
487      /* Include a context's width before and after.  */
488
489      first0 = Math.max(first0 - context, 0);
490      first1 = Math.max(first1 - context, 0);
491      last0 = Math.min(last0 + context, file0.length - 1);
492      last1 = Math.min(last1 + context, file1.length - 1);
493
494      outfile.print("@@ -");
495      print_number_range (first0, last0);
496      outfile.print(" +");
497      print_number_range (first1, last1);
498      outfile.print(" @@");
499
500      /* If we looked for and found a function this is part of,
501   include its name in the header of the diff section.  */
502      print_function(file0,first0);
503
504      outfile.println();
505
506      Diff.change next = hunk;
507      int i = first0;
508      int j = first1;
509
510      while (i <= last0 || j <= last1) {
511
512  /* If the line isn't a difference, output the context from file 0. */
513
514  if (next == null || i < next.line0) {
515    if (i < file0.length) {
516      outfile.print(' ');
517      print_1_line ("", file0[i++]);
518    }
519    j++;
520  }
521  else {
522    /* For each difference, first output the deleted part. */
523
524    int k = next.deleted;
525    while (k-- > 0) {
526      outfile.print('-');
527      print_1_line ("", file0[i++]);
528    }
529
530    /* Then output the inserted part. */
531
532    k = next.inserted;
533    while (k-- > 0) {
534      outfile.print('+');
535      print_1_line ("", file1[j++]);
536    }
537
538    /* We're done with this hunk, so on to the next! */
539
540    next = next.link;
541  }
542      }
543    }
544  }
545
546
547  /** Read a text file into an array of String.  This provides basic diff
548     functionality.  A more advanced diff utility will use specialized
549     objects to represent the text lines, with options to, for example,
550     convert sequences of whitespace to a single space for comparison
551     purposes.
552   */
553  static String[] slurp(String file) throws IOException {
554    BufferedReader rdr = new BufferedReader(new FileReader(file));
555    Vector s = new Vector();
556    for (;;) {
557      String line = rdr.readLine();
558      if (line == null) break;
559      s.addElement(line);
560    }
561    String[] a = new String[s.size()];
562    s.copyInto(a);
563    return a;
564  }
565
566  public static void main(String[] argv) throws IOException {
567    String filea = argv[argv.length - 2];
568    String fileb = argv[argv.length - 1];
569    String[] a = slurp(filea);
570    String[] b = slurp(fileb);
571    Diff d = new Diff(a,b);
572    char style = 'n';
573    d.heuristic = false;
574    for (int i = 0; i < argv.length - 2; ++i) {
575      String f = argv[i];
576      if (f.startsWith("-")) {
577        for (int j = 1; j < f.length(); ++j) {
578    switch (f.charAt(j)) {
579    case 'H':   // heuristic on
580      d.heuristic = true; break;
581    case 'e':   // Ed style
582      style = 'e'; break;
583    case 'c':   // Context diff
584      style = 'c'; break;
585    case 'u':
586      style = 'u'; break;
587    }
588  }
589      }
590    }
591    boolean reverse = style == 'e';
592    Diff.change script = d.diff_2(reverse);
593    if (script == null)
594      System.err.println("No differences");
595    else {
596      Base p;
597      switch (style) {
598      case 'e':
599        p = new EdPrint(a,b); break;
600      case 'c':
601        p = new ContextPrint(a,b); break;
602      case 'u':
603        p = new UnifiedPrint(a,b); break;
604      default:
605        p = new NormalPrint(a,b);
606      }
607      p.print_header(filea,fileb);
608      p.print_script(script);
609    }
610  }
611
612}
613