001 package org.hackystat.sensor.ant.svn; 002 003 import java.io.BufferedReader; 004 import java.io.ByteArrayInputStream; 005 import java.io.ByteArrayOutputStream; 006 import java.io.InputStreamReader; 007 import java.util.ArrayList; 008 import java.util.List; 009 import java.util.Locale; 010 import java.util.Map; 011 import java.util.TreeMap; 012 013 import org.hackystat.sensor.ant.vcs.GenericDiffCounter; 014 import org.hackystat.sensor.ant.vcs.GenericSizeCounter; 015 import org.tmatesoft.svn.core.SVNNodeKind; 016 import org.tmatesoft.svn.core.SVNProperties; 017 import org.tmatesoft.svn.core.io.SVNRepository; 018 019 /** 020 * A record tracing the change of one file or one directory in an SVN commit. 021 * Note that the path may indication either a directory or a file. During a 022 * commit a path name can change, such as in the case of renaming. 023 * <p> 024 * If an item is newly created in the commit, then fromPath is null. If an item 025 * is deleted, then toPath is null. 026 * 027 * @author (Cedric) Qin ZHANG 028 * @version $Id$ 029 */ 030 public class CommitRecordEntry { 031 032 private SVNRepository svnRepository; 033 private String fromPath, toPath; 034 private long fromRevision, toRevision; 035 036 private boolean statisticsComputed = false; 037 private boolean isFile = false; // valid only if statisticsComputed=true 038 private boolean isTextFile = false; // valid only if isFile=true 039 private int linesAdded, linesDeleted, totalLines; // valid only if 040 041 // isTextFile=true 042 043 /** 044 * Create this instance. One of fromPath and toPath can be null, but not both. 045 * 046 * @param svnRepository SVN repository. 047 * @param fromPath The from path. 048 * @param fromRevision The from revision. 049 * @param toPath The to path. 050 * @param toRevision The to revision. 051 * 052 * @throws Exception If both fromPath and toPath are null. 053 */ 054 CommitRecordEntry(SVNRepository svnRepository, String fromPath, long fromRevision, 055 String toPath, long toRevision) throws Exception { 056 this.svnRepository = svnRepository; 057 this.fromPath = fromPath; 058 this.fromRevision = fromRevision; 059 this.toPath = toPath; 060 this.toRevision = toRevision; 061 if (this.fromPath == null && this.toPath == null) { 062 throw new Exception("FromPath and ToPath cannot both be null."); 063 } 064 } 065 066 /** 067 * Gets the from path. 068 * 069 * @return The from path. 070 */ 071 public String getFromPath() { 072 return this.fromPath; 073 } 074 075 /** 076 * Gets the to path. 077 * 078 * @return The to path. 079 */ 080 public String getToPath() { 081 return this.toPath; 082 } 083 084 /** 085 * Gets the from revision. 086 * 087 * @return The from revision. 088 */ 089 public long getFromRevision() { 090 return this.fromRevision; 091 } 092 093 /** 094 * Checks whether this entry represents a file or not. Note that a false 095 * return value does not necessary mean this entry represents a directory. 096 * 097 * @return True if this commit entry represents a file. 098 * 099 * @throws Exception If there is any error. 100 */ 101 public boolean isFile() throws Exception { 102 this.computeStatistics(); 103 return this.isFile; 104 } 105 106 /** 107 * Checks whether this entry represents a text file. 108 * 109 * @return True if this commit entry represents a text file. False if either 110 * this commit entry represents a binary file, or it's not a file at all. 111 * 112 * @throws Exception If there is any error. 113 */ 114 public boolean isTextFile() throws Exception { 115 this.computeStatistics(); 116 return this.isTextFile; 117 } 118 119 /** 120 * Gets the to revision. 121 * 122 * @return The to revision. 123 */ 124 public long getToRevision() { 125 return this.toRevision; 126 } 127 128 /** 129 * Gets the number of lines added. 130 * 131 * @return The number of lines added. 132 * 133 * @throws Exception If this entry does not represent a text file, or if there 134 * is any other error. 135 */ 136 public int getLinesAdded() throws Exception { 137 this.computeStatistics(); 138 if (this.isTextFile) { 139 return this.linesAdded; 140 } 141 else { 142 throw new Exception("This is not a text file."); 143 } 144 } 145 146 /** 147 * Gets the number of lines deleted. 148 * 149 * @return The number of lines deleted. 150 * 151 * @throws Exception If this entry does not represent text a file, or if there 152 * is any other error. 153 */ 154 public int getLinesDeleted() throws Exception { 155 this.computeStatistics(); 156 if (this.isTextFile) { 157 return this.linesDeleted; 158 } 159 else { 160 throw new Exception("This is not a text file."); 161 } 162 } 163 164 /** 165 * Gets the total number of lines for the toRevision. 166 * 167 * @return The total number of lines. 168 * 169 * @throws Exception If this entry does not represent a text file, or if there 170 * is any other error. 171 */ 172 public int getTotalLines() throws Exception { 173 this.computeStatistics(); 174 if (this.isTextFile) { 175 return this.totalLines; 176 } 177 else { 178 throw new Exception("This is not a text file."); 179 } 180 } 181 182 /** 183 * Computes file metrics. Note that if the file is a binary file, either an 184 * exception will be thrown or the computed value is invalid. The exact 185 * behavior depends on the underlying metrics computation engine. 186 * 187 * @throws Exception If this entry does not represent a file, or if there is 188 * any other error. 189 */ 190 private void computeStatistics() throws Exception { 191 192 if (!this.statisticsComputed) { 193 boolean useToPath = (this.toPath != null); 194 195 // check isFile 196 SVNNodeKind nodeKind = useToPath ? this.svnRepository.checkPath(this.toPath, 197 this.toRevision) : this.svnRepository.checkPath(this.fromPath, this.fromRevision); 198 this.isFile = (nodeKind == SVNNodeKind.FILE); 199 // TODO: this is a hack 200 // It seems that each call of checkPath method will open a new socket 201 // connection, 202 // If this method repeatedly called, OS will return a "address already in 203 // use" exception. 204 // This is underlying JavaSVN issue, the only thing I can do is to force 205 // this 206 // thread to sleep for some time. 207 Thread.sleep(50); 208 209 if (this.isFile) { 210 // check isTextFile 211 TreeMap<String, String> properties = new TreeMap<String, String>(); 212 if (useToPath) { 213 this.getVersionedProperties(this.toPath, this.toRevision, properties); 214 } 215 else { 216 this.getVersionedProperties(this.fromPath, this.fromRevision, properties); 217 } 218 String svnFileMineType = properties.get("svn:mime-type"); 219 this.isTextFile = (svnFileMineType == null || svnFileMineType.toLowerCase(Locale.ENGLISH) 220 .startsWith("/text")); 221 222 if (this.isTextFile) { 223 byte[] content = useToPath ? this.getVersionedContent(this.toPath, this.toRevision, 224 null) : this.getVersionedContent(this.fromPath, this.fromRevision, null); 225 // compute diff 226 byte[] fromContent = useToPath ? null : content; 227 byte[] toContent = useToPath ? content : null; 228 229 if (fromContent == null && this.fromPath != null) { 230 fromContent = this.getVersionedContent(this.fromPath, this.fromRevision, null); 231 } 232 233 if (toContent == null && this.toPath != null) { 234 toContent = this.getVersionedContent(this.toPath, this.toRevision, null); 235 } 236 237 String[] fromStrings = fromContent == null ? null : this 238 .byteArrayToStringArray(fromContent); 239 String[] toStrings = toContent == null ? null : this 240 .byteArrayToStringArray(toContent); 241 242 if (fromStrings == null) { 243 this.totalLines = new GenericSizeCounter(toStrings).getNumOfTotalLines(); 244 this.linesAdded = this.totalLines; 245 this.linesDeleted = 0; 246 } 247 else if (toStrings == null) { 248 this.totalLines = 0; 249 this.linesAdded = 0; 250 this.linesDeleted = new GenericSizeCounter(fromStrings).getNumOfTotalLines(); 251 } 252 else { 253 this.totalLines = new GenericSizeCounter(toStrings).getNumOfTotalLines(); 254 GenericDiffCounter diff = new GenericDiffCounter(fromStrings, toStrings); 255 this.linesAdded = diff.getLinesAdded(); 256 this.linesDeleted = diff.getLinesDeleted(); 257 } 258 259 } // this.isTextFile 260 } // this.isFile 261 262 this.statisticsComputed = true; 263 } // this.isStatisticsComputed 264 } 265 266 /** 267 * Gets the content for a file at the specified revision. 268 * @param filePath The file path. 269 * @param revision The revision number. 270 * @param properties A map to receive SVN properties associated with the file. 271 * Note that this is an output parameter, it's advised that you use an empty 272 * map. Null is a valid value. 273 * 274 * @return The content as a byte array. 275 * @throws Exception If there is any error. 276 */ 277 private byte[] getVersionedContent(String filePath, long revision, 278 Map<String, String> properties) throws Exception { 279 ByteArrayOutputStream output = new ByteArrayOutputStream(4096); 280 SVNProperties svnProps = SVNProperties.wrap(properties); 281 this.svnRepository.getFile(filePath, revision, svnProps, output); 282 output.flush(); 283 return output.toByteArray(); 284 } 285 286 /** 287 * Gets the properties for a file at the specified revision. 288 * @param filePath The file path. 289 * @param revision The revision number. 290 * @param properties A map to receive SVN properties associated with the file. 291 * Note that this is an output parameter, it's advised that you use an empty 292 * map. Null is a valid value. 293 * @throws Exception If there is any error. 294 */ 295 private void getVersionedProperties(String filePath, long revision, 296 Map<String, String> properties) throws Exception { 297 SVNProperties svnProps = SVNProperties.wrap(properties); 298 this.svnRepository.getFile(filePath, revision, svnProps, null); 299 } 300 301 /** 302 * Converts a byte array holding text into a string array with each string 303 * represeting one line. TODO: check shits like unicode, text locale, and line 304 * break with \r, \n or both! 305 * 306 * @param byteArray A byte array holding text. 307 * 308 * @return The converted string array. Note that if you pass in a byte array 309 * containing binary contents, then the return value is undefined. 310 * 311 * @throws Exception If there is any error. 312 */ 313 private String[] byteArrayToStringArray(byte[] byteArray) throws Exception { 314 List<String> strings = new ArrayList<String>(16); 315 BufferedReader reader = new BufferedReader(new InputStreamReader(new ByteArrayInputStream( 316 byteArray))); 317 String str = reader.readLine(); 318 while (str != null) { 319 strings.add(str); 320 str = reader.readLine(); 321 } 322 // convert to vanilla string array 323 int size = strings.size(); 324 String[] strs = new String[size]; 325 for (int i = 0; i < size; i++) { 326 strs[i] = strings.get(i); 327 } 328 return strs; 329 } 330 331 /** 332 * Gets a string representation of this instance. 333 * 334 * @return The string representation. 335 */ 336 @Override 337 public String toString() { 338 StringBuffer buff = new StringBuffer(64); 339 buff.append("CommitRecordEntry ("); 340 buff.append(this.fromPath).append('[').append(this.fromRevision).append("] ==> "); 341 buff.append(this.toPath).append('[').append(this.toRevision).append("]) "); 342 return buff.toString(); 343 } 344 }