/* * CCVisu is a tool for visual graph clustering * and general force-directed graph layout. * This file is part of CCVisu. * * Copyright (C) 2005-2007 Dirk Beyer * * CCVisu is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * CCVisu is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with CCVisu; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * Please find the GNU Lesser General Public License in file * license_lgpl.txt or http://www.gnu.org/licenses/lgpl.txt * * Dirk Beyer (firstname.lastname@sfu.ca) * Simon Fraser University (SFU), B.C., Canada */ package ccvisu; import java.io.BufferedReader; import java.util.Calendar; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; import java.util.SortedMap; import java.util.SortedSet; import java.util.TreeMap; import java.util.TreeSet; import java.util.Vector; /***************************************************************** * Reader for CVS log files. * Extracts the co-change graph from the CVS log info. * @version $Revision$; $Date$ * @author Dirk Beyer *****************************************************************/ public class ReaderDataGraphCVS extends ReaderDataGraph { /** Time-window constant for transaction recovery, in milli-seconds.*/ private int timeWindow; private boolean sliding; /** * Constructor. * @param in Stream reader object. * @param timeWindow Time window for transaction recovery, in milli-seconds * (default: 180'000). */ public ReaderDataGraphCVS(BufferedReader in, int timeWindow) { super(in); this.timeWindow = timeWindow; sliding = false; } /** * Constructor. * @param in Stream reader object. * @param timeWindow Time window for transaction recovery, in milli-seconds * (default: 180'000). * @param sliding sliding or fixed time window */ public ReaderDataGraphCVS(BufferedReader in, int timeWindow, boolean sliding) { super(in); this.timeWindow = timeWindow; this.sliding = sliding; } /** * Represents a CVS revision entry (an abstraction of it). * @author Dirk Beyer */ private class Revision implements Comparable { String relName; String filename; Long time; String user; String logmsg; /** The internal number (id) of the change transaction.*/ int transaction; /***************************************************************** * Compares this revision with the specified object for order. * Returns a negative integer, zero, or a positive integer as this object * is less than, equal to, or greater than the specified object.<p> * * @param o Object to be compared for order with this revision. * @return a negative integer, zero, or a positive integer as this object * is less than, equal to, or greater than the specified object. * * @throws ClassCastException if the specified object's type prevents it * from being compared to this Object. *****************************************************************/ public int compareTo(Object o) { Revision rev = (Revision) o; if (rev == null) { // Either o is null or o is not of class Revision. throw new ClassCastException("Object of class Revision required."); } if (this.time.compareTo(rev.time) == 0) { return this.hashCode() - rev.hashCode(); } else { return this.time.compareTo(rev.time); } } /***************************************************************** * Compares the specified object with this revision for equality. * Returns <tt>true</tt> if the specified object is identical with this object. * The method is based on <tt>compareTo</tt> to make the ordering * <i>consistent with equals</i>.<p> * * @param o Object to be compared for equality with this revision. * @return <tt>true</tt> if the specified Object is equal to this revision. *****************************************************************/ public boolean equals(Object o) { Revision rev = (Revision) o; if (rev == null) { // Either o is null or o is not of class Revision. return false; } return (this.compareTo(o) == 0); } }; /***************************************************************** * Reads the edges of a graph in CVS log format * from stream reader <code>in</code>, * and stores them in a list (of <code>GraphEdgeString</code> elements). * @return List of string edges. *****************************************************************/ protected Vector<GraphEdgeString> readEdges() { Vector<GraphEdgeString> result = new Vector<GraphEdgeString>(); Vector<Revision> revisionList = readRevisionList(); SortedMap transMap = recoverTransactions(revisionList); Set timeSet = transMap.keySet(); Iterator timeIt = timeSet.iterator(); while( timeIt.hasNext() ) { Long time = (Long) timeIt.next(); Collection transColl = (Collection) transMap.get(time); Iterator transIt = transColl.iterator(); while( transIt.hasNext() ) { Set revSet = (Set) transIt.next(); Iterator revIt = revSet.iterator(); while( revIt.hasNext() ) { Revision revision = (Revision) revIt.next(); GraphEdgeString edge = new GraphEdgeString(); //relation name edge.relName = revision.relName; // Source vertex. edge.x = Integer.toString(revision.transaction); // Target vertex. edge.y = revision.filename; // Edge weight. edge.w = "1.0"; result.add(edge); // Print revision entry with timestamp and user of the changes to stdout. if (CCVisu.getVerbosityLevel() >= 1) { Calendar cal = Calendar.getInstance(); cal.setTimeInMillis(revision.time.longValue()); System.out.println("REV \t" + Integer.toString(revision.transaction) + "\t" + "\"" + cal.getTime() + "\"" + "\t" + revision.user + "\t" + revision.filename + "\t" + revision.relName); } } } } return result; } /***************************************************************** * Parses the date entry. * @param dateStr The CVS date entry string. * @return Long value of the date, or * <code>null</code> if <code>dateStr</code> * is not a valid date entry. *****************************************************************/ private Long parseDate(String dateStr) { // Delimiter for year/month/day. char delim = '/'; int posEnd = dateStr.indexOf(delim); if (posEnd < 0 || posEnd > 9) { delim = '-'; posEnd = dateStr.indexOf(delim); if (posEnd < 0 || posEnd > 9) { return null; } } int posBegin = 0; int year = Integer.parseInt(dateStr.substring(posBegin, posEnd)); posBegin = posEnd + 1; posEnd = dateStr.indexOf(delim, posBegin); int month = Integer.parseInt(dateStr.substring(posBegin, posEnd)); posBegin = posEnd + 1; posEnd = dateStr.indexOf(' ', posBegin); int day = Integer.parseInt(dateStr.substring(posBegin, posEnd)); posBegin = posEnd + 1; posEnd = dateStr.indexOf(':', posBegin); int hour = Integer.parseInt(dateStr.substring(posBegin, posEnd)); posBegin = posEnd + 1; posEnd = dateStr.indexOf(':', posBegin); int min = Integer.parseInt(dateStr.substring(posBegin, posEnd)); posBegin = posEnd + 1; posEnd = posBegin + 2; int sec =Integer.parseInt(dateStr.substring(posBegin, posEnd)); Calendar cal = Calendar.getInstance(); cal.clear(); // Erase the milli secs. cal.set(year, month-1, day, hour, min, sec); return new Long(cal.getTimeInMillis()); } /***************************************************************** * Parses the CVS log data and extracts revisions. * @return List of revisions. *****************************************************************/ private Vector<Revision> readRevisionList() { Vector<Revision> result = new Vector<Revision>(); String lLine = ""; //String relName = "CO-CHANGE"; String filename = null; Long time; String user; String logmsg; int lineno = 1; try { while ((lLine = in.readLine()) != null) { // New working file. if (lLine.startsWith("Working file: ")) { // Set name of the current working file, // for which we pasre the revisions. filename = lLine.substring(14); } // New revision. if (lLine.startsWith("date: ")) { // Set date, author, and logmsg of the current revision. // Parse date. Start right after "date: ". time = parseDate(lLine.substring(6, lLine.indexOf("author: "))); if (time == null) { System.err.print("Error while reading the CVS date info for file: "); System.err.println(filename + "."); } // Parse author. Start right after "author: ". int posBegin = lLine.indexOf("author: ") + 8; int posEnd = lLine.indexOf(';', posBegin); user = lLine.substring(posBegin, posEnd); // Parse logmsg. Start on next line the date/author line. logmsg = ""; ++lineno; while ( ((lLine = in.readLine()) != null) && !lLine.startsWith("----") && !lLine.startsWith("====") ) { if (!lLine.startsWith("branches: ")) { logmsg += lLine + endl; } ++lineno; } // Create revision and add revision to resulting list. Revision revision = new Revision(); //revision.relName = relName.replace(' ', '_'); // Replace blanks by underline. revision.filename = filename.replace(' ', '_'); // Replace blanks by underline. revision.time = time; revision.user = user; revision.logmsg = logmsg; result.add(revision); //System.out.print("Relation: "+ relName + // " File: " + filename + // " Time: " + time.toString() + // " User: " + user + " LogMsg: " + logmsg); } ++lineno; } // while } catch (Exception e) { System.err.println("Exception while reading the CVS log at line " + lineno + ":"); System.err.println(e); System.err.print("Read line: "); System.err.println(lLine); } return result; } /***************************************************************** * Recovers the change transactions for the co-change graph * from the revision information, i.e., it assignes * the transaction ids for the revisions. * @param revisionList is a list of revisions. * @return Sorted map that maps timestamps to collections of transactions, * where transactions are sets of revisions. *****************************************************************/ private SortedMap recoverTransactions(Vector<Revision> revisionList) { // Step 1: Transform the list of revisions to a sorted data structure. // A map user -> msg-entry. Map<String,Map<String,SortedMap<Long,SortedSet<String>>>> userMap = new HashMap<String,Map<String,SortedMap<Long,SortedSet<String>>>>(); // A map logmsg -> time-entry. Map<String,SortedMap<Long,SortedSet<String>>> msgMap; // A map time -> file. SortedMap<Long,SortedSet<String>> timeMap; // A set of files. SortedSet<String> fileSet; for (int i = 0; i < revisionList.size(); ++i) { Revision revision = revisionList.get(i); // A map logmsg -> time-entry. msgMap = userMap.get(revision.user); if (msgMap == null) { msgMap = new HashMap<String,SortedMap<Long,SortedSet<String>>>(); userMap.put(revision.user, msgMap); } // A map time -> file. timeMap = msgMap.get(revision.logmsg); if (timeMap == null) { timeMap = new TreeMap<Long,SortedSet<String>>(); msgMap.put(revision.logmsg, timeMap); } // A set of files. fileSet = timeMap.get(revision.time); if (fileSet == null) { fileSet = new TreeSet<String>(); timeMap.put(revision.time, fileSet); } // Add file to set. fileSet.add(revision.filename); } // Step 2: Create the result, which is // a map timestamp -> set of transactions (Long -> SortedSet), // where one transaction is a set of revisions. SortedMap<Long,Collection<SortedSet<Revision>>> result = new TreeMap<Long,Collection<SortedSet<Revision>>>(); int transaction = 0; Set userSet = userMap.keySet(); Iterator userIt = userSet.iterator(); while( userIt.hasNext() ) { String user = (String) userIt.next(); msgMap = userMap.get(user); Set msgSet = msgMap.keySet(); Iterator msgIt = msgSet.iterator(); while( msgIt.hasNext() ) { String logmsg = (String) msgIt.next(); timeMap = msgMap.get(logmsg); Set timeSet = timeMap.keySet(); Iterator timeIt = timeSet.iterator(); long firstTime = 0; Set<String> tmpFilesSeen = new TreeSet<String>(); // Detect a time window that is too long. Collection<SortedSet<Revision>> transColl; // Collection of transactions. SortedSet<Revision> revSet = null; // Transaction, i.e., set of revisions. while( timeIt.hasNext() ) { Long time = (Long) timeIt.next(); if (time.longValue() - firstTime > timeWindow) { // Start new transaction. ++transaction; firstTime = time.longValue(); tmpFilesSeen.clear(); // Retrieve (or create new) set of transactions for the timestamp. transColl = result.get(time); if (transColl == null) { transColl = new Vector<SortedSet<Revision>>(); result.put(time, transColl); } // New transaction (set of revisions). revSet = new TreeSet<Revision>(); transColl.add(revSet); } else if(sliding){ // The time window 'slides' with the files. firstTime = time.longValue(); } fileSet = timeMap.get(time); Iterator fileIt = fileSet.iterator(); while( fileIt.hasNext() ) { String filename = (String) fileIt.next(); // Detect a time window that is too long. if (CCVisu.getVerbosityLevel() >= 1 && tmpFilesSeen.contains(filename)) { System.err.println( "Transaction-recovery warning: Time window might be to wide " + endl + "(currently '" + timeWindow + "' milli-seconds). " + endl + "File '" + filename + "' already contained in current transaction." ); } tmpFilesSeen.add(filename); // Create revision and add revision to resulting list. Revision revision = new Revision(); revision.relName = "CO-CHANGE"; revision.filename = filename; revision.time = time; revision.user = user; revision.logmsg = logmsg; revision.transaction = transaction; revSet.add(revision); } } } // } } return result; } };