/**
   * Recovers the change transactions for the co-change graph from the revision information, i.e.,
   * it assigns 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<Long, Collection<SortedSet<Revision>>> recoverTransactions(
      List<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 (Revision revision : revisionList) {
      // 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<String> userSet = userMap.keySet();
    for (String user : userSet) {
      msgMap = userMap.get(user);

      Set<String> msgSet = msgMap.keySet();
      for (String logmsg : msgSet) {
        timeMap = msgMap.get(logmsg);

        Set<Long> timeSet = timeMap.keySet();

        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.
        for (Long time : timeSet) {
          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 ArrayList<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);
          for (String filename : fileSet) {
            // Detect a time window that is too long.
            if (verbosity.isAtLeast(Verbosity.WARNING) && tmpFilesSeen.contains(filename)) {
              System.err.println(
                  "Transaction-recovery warning: " + "Time window might be to wide ");
              System.err.println("(currently '" + timeWindow + "' milli-seconds). ");
              System.err.println(
                  "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;
  }
  /**
   * Parses the CVS log data and extracts revisions.
   *
   * @return List of revisions.
   */
  private List<Revision> readRevisionList() {
    List<Revision> result = new ArrayList<Revision>();

    String line = "";
    // String relName = "CO-CHANGE";
    String filename = null;
    Long time;
    String user;
    String logmsg;

    int lineno = 1;
    try {
      while ((line = reader.readLine()) != null) {
        // New working file.
        if (line.startsWith("Working file: ")) {
          // Set name of the current working file,
          //   for which we pasre the revisions.
          filename = line.substring(14);
        }

        // New revision.
        if (line.startsWith("date: ")) {
          // Set date, author, and logmsg of the current revision.

          // Parse date. Start right after "date: ".
          time = parseDate(line.substring(6, line.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 = line.indexOf("author: ") + 8;
          int posEnd = line.indexOf(';', posBegin);
          user = line.substring(posBegin, posEnd);

          // Parse logmsg. Start on next line the date/author line.
          logmsg = "";
          ++lineno;
          while (((line = reader.readLine()) != null)
              && !line.startsWith("----")
              && !line.startsWith("====")) {
            if (!line.startsWith("branches: ")) {
              logmsg += line + ReaderDataGraph.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: "******" 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(line);
    }
    return result;
  }