View Javadoc

1   package net.sf.statsvn.util;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.io.InputStreamReader;
6   import java.io.StringReader;
7   import java.util.Vector;
8   
9   import net.sf.statcvs.util.LookaheadReader;
10  import net.sf.statsvn.output.SvnConfigurationOptions;
11  
12  /**
13   * Utilities class that manages calls to svn diff.
14   * 
15   * @author Jason Kealey <jkealey@shade.ca>
16   * @author Gunter Mussbacher <gunterm@site.uottawa.ca>
17   * 
18   * @version $Id: SvnDiffUtils.java 352 2008-03-31 16:14:51Z benoitx $
19   */
20  public final class SvnDiffUtils {
21  	private static final int PROPERTY_NAME_LINE = 4;
22  
23  	private static final String PROPERTY_CHANGE = "Property changes on:";
24  
25  	private static final String PROPERTY_NAME = "Name:";
26  
27  	private static final String BINARY_TYPE = "Cannot display: file marked as a binary type.";
28  
29  	private static final String INDEX_MARKER = "Index: ";
30  
31  	/**
32  	 * A utility class (only static methods) should be final and have a private
33  	 * constructor.
34  	 */
35  	private SvnDiffUtils() {
36  	}
37  
38  	/**
39  	 * Calls svn diff for the filename and revisions given. Will use URL
40  	 * invocation, to ensure that we get diffs even for deleted files.
41  	 * 
42  	 * @param oldRevNr
43  	 *            old revision number
44  	 * @param newRevNr
45  	 *            new revision number
46  	 * @param filename
47  	 *            filename.
48  	 * @return the InputStream related to the call. If the error steam is
49  	 *         non-empty, will return the error stream instead of the default
50  	 *         input stream.
51  	 */
52  	private static synchronized ProcessUtils callSvnDiff(final String oldRevNr, final String newRevNr, String filename) throws IOException {
53  		String svnDiffCommand = null;
54  		filename = SvnInfoUtils.relativePathToUrl(filename);
55  		filename = SvnInfoUtils.replace(" ", "%20", filename);
56  		svnDiffCommand = "svn diff --old " + filename + "@" + oldRevNr + "  --new " + filename + "@" + newRevNr + "" + SvnCommandHelper.getAuthString();
57  		SvnConfigurationOptions.getTaskLogger().log(Thread.currentThread().getName() + " FIRING command line:\n[" + svnDiffCommand + "]");
58  		return ProcessUtils.call(svnDiffCommand);
59  	}
60  
61  	/**
62  	 * Calls svn diff on all files for given revision and revision-1.  
63  	 * 
64  	 * @param newRevNr
65  	 *            revision number
66  	 * @return the InputStream related to the call. If the error steam is
67  	 *         non-empty, will return the error stream instead of the default
68  	 *         input stream.
69  	 */
70  	private static synchronized ProcessUtils callSvnDiff(final String newRevNr) throws IOException {
71  		String svnDiffCommand = null;
72  		svnDiffCommand = "svn diff -c " + newRevNr + " " + SvnInfoUtils.getRootUrl() + " " + SvnCommandHelper.getAuthString();
73  		SvnConfigurationOptions.getTaskLogger().log(Thread.currentThread().getName() + " FIRING command line:\n[" + svnDiffCommand + "]");
74  		return ProcessUtils.call(svnDiffCommand);
75  	}
76  
77  	/**
78  	 * Returns line count differences between two revisions of a file.
79  	 * 
80  	 * @param oldRevNr
81  	 *            old revision number
82  	 * @param newRevNr
83  	 *            new revision number
84  	 * @param filename
85  	 *            the filename
86  	 * @return A int[2] array of [lines added, lines removed] is returned.
87  	 * @throws IOException
88  	 *             problem parsing the stream
89  	 * @throws BinaryDiffException
90  	 *             if the error message is due to trying to diff binary files.
91  	 */
92  	public static int[] getLineDiff(final String oldRevNr, final String newRevNr, final String filename) throws IOException, BinaryDiffException {
93  		int[] lineDiff;
94  		ProcessUtils pUtils = null;
95  		try {
96  			pUtils = callSvnDiff(oldRevNr, newRevNr, filename);
97  			final InputStream diffStream = pUtils.getInputStream();
98  
99  			final LookaheadReader diffReader = new LookaheadReader(new InputStreamReader(diffStream));
100 			lineDiff = parseDiff(diffReader);
101 
102 			verifyOutput(pUtils);
103 		} finally {
104 			if (pUtils != null) {
105 				pUtils.close();
106 			}
107 		}
108 
109 		return lineDiff;
110 	}
111 
112 	/**
113 	 * Verifies the process error stream. 
114 	 * @param pUtils the process call
115 	 * @throws IOException problem parsing the stream
116 	 * @throws BinaryDiffException if the error message is due to trying to diff binary files.
117 	 */
118 	private static void verifyOutput(final ProcessUtils pUtils) throws IOException, BinaryDiffException {
119 		if (pUtils.hasErrorOccured()) {
120 			// The binary checking code here might be useless... as it may
121 			// be output on the standard out.
122 			final String msg = pUtils.getErrorMessage();
123 			if (isBinaryErrorMessage(msg)) {
124 				throw new BinaryDiffException();
125 			} else {
126 				throw new IOException(msg);
127 			}
128 		}
129 	}
130 
131 	/**
132 	* Returns line count differences for all files in a particular revision.
133 	* 
134 	* @param newRevNr
135 	*            new revision number
136 	* @return A vector of object[3] array of [filename, int[2](lines added, lines removed), isBinary] is returned.
137 	* @throws IOException
138 	*             problem parsing the stream
139 	* @throws BinaryDiffException
140 	*             if the error message is due to trying to diff binary files.
141 	*/
142 	public static Vector getLineDiff(final String newRevNr) throws IOException, BinaryDiffException {
143 		final Vector answer = new Vector();
144 
145 		ProcessUtils pUtils = null;
146 		try {
147 			pUtils = callSvnDiff(newRevNr);
148 			final InputStream diffStream = pUtils.getInputStream();
149 			final LookaheadReader diffReader = new LookaheadReader(new InputStreamReader(diffStream));
150 			String currFile = null;
151 			StringBuffer sb = new StringBuffer();
152 			while (diffReader.hasNextLine()) {
153 				final String currLine = diffReader.nextLine();
154 
155 				if (currFile == null && currLine.startsWith(INDEX_MARKER)) {
156 					currFile = currLine.substring(INDEX_MARKER.length());
157 				} else if (currFile != null && currLine.startsWith(INDEX_MARKER)) {
158 					appendResults(answer, currFile, sb);
159 					sb = new StringBuffer();
160 					currFile = currLine.substring(INDEX_MARKER.length());
161 				}
162 
163 				sb.append(currLine);
164 				sb.append(System.getProperty("line.separator"));
165 			}
166 
167 			// last file
168 			appendResults(answer, currFile, sb);
169 
170 			verifyOutput(pUtils);
171 		} finally {
172 			if (pUtils != null) {
173 				pUtils.close();
174 			}
175 		}
176 
177 		return answer;
178 	}
179 
180 	/**
181 	 * Append results to answer vector.  
182 	 * @param answer the current answers
183 	 * @param currFile the current file being added. 
184 	 * @param sb the diff for this individual file. 
185 	 * @throws IOException
186 	 *             problem parsing the stream
187 	 * @throws BinaryDiffException
188 	 *             if the error message is due to trying to diff binary files.
189 	 */
190 	private static void appendResults(final Vector answer, final String currFile, final StringBuffer sb) throws IOException {
191 		int[] lineDiff;
192 		Boolean isBinary = Boolean.FALSE;
193 
194 		final LookaheadReader individualDiffReader = new LookaheadReader(new StringReader(sb.toString()));
195 		try {
196 			lineDiff = parseDiff(individualDiffReader);
197 		} catch (final BinaryDiffException e) {
198 			lineDiff = new int[2];
199 			lineDiff[0] = 0;
200 			lineDiff[1] = 0;
201 			isBinary = Boolean.TRUE;
202 
203 		}
204 		final Object[] results = new Object[3];
205 		results[0] = currFile;
206 		results[1] = lineDiff;
207 		results[2] = isBinary;
208 		answer.add(results);
209 	}
210 
211 	/**
212 	 * Returns true if msg is an error message display that the file is binary.
213 	 * 
214 	 * @param msg
215 	 *            the error message given by ProcessUtils.getErrorMessage();
216 	 * @return true if the file is binary
217 	 */
218 	private static boolean isBinaryErrorMessage(final String msg) {
219 		/*
220 		 * Index: junit.jar
221 		 * ===================================================================
222 		 * Cannot display: file marked as a binary type.
223 		 * 
224 		 * svn:mime-type = application/octet-stream
225 		 */
226 		return (msg.indexOf(BINARY_TYPE) >= 0);
227 	}
228 
229 	/**
230 	 * Returns line count differences between two revisions of a file.
231 	 * 
232 	 * @param diffReader
233 	 *            the stream reader for svn diff
234 	 * 
235 	 * @return A int[2] array of [lines added, lines removed] is returned.
236 	 * @throws IOException
237 	 *             problem parsing the stream
238 	 */
239 	private static int[] parseDiff(final LookaheadReader diffReader) throws IOException, BinaryDiffException {
240 		final int[] lineDiff = { -1, -1 };
241 		boolean propertyChange = false;
242 		if (!diffReader.hasNextLine()) {
243 			// diff has no output because we modified properties or the changes
244 			// are auto-generated ($id$ $author$ kind of thing)
245 			// http://svnbook.red-bean.com/nightly/en/svn.advanced.props.html#svn.advanced.props.special.keywords
246 			lineDiff[0] = 0;
247 			lineDiff[1] = 0;
248 		}
249 		while (diffReader.hasNextLine()) {
250 			diffReader.nextLine();
251 			final String currentLine = diffReader.getCurrentLine();
252 
253 			SvnConfigurationOptions.getTaskLogger().log(Thread.currentThread().getName() + " Diff Line: [" + currentLine + "]");
254 
255 			if (currentLine.length() == 0) {
256 				continue;
257 			}
258 			final char firstChar = currentLine.charAt(0);
259 			// very simple algorithm
260 			if (firstChar == '+') {
261 				lineDiff[0]++;
262 			} else if (firstChar == '-') {
263 				lineDiff[1]++;
264 			} else if (currentLine.indexOf(PROPERTY_CHANGE) == 0
265 			        || (currentLine.indexOf(PROPERTY_NAME) == 0 && diffReader.getLineNumber() == PROPERTY_NAME_LINE)) {
266 				propertyChange = true;
267 			} else if (currentLine.indexOf(BINARY_TYPE) == 0) {
268 				throw new BinaryDiffException();
269 			}
270 		}
271 		if (propertyChange && (lineDiff[0] == -1 || lineDiff[1] == -1)) {
272 			lineDiff[0] = 0;
273 			lineDiff[1] = 0;
274 		}
275 
276 		return lineDiff;
277 	}
278 
279 }