@Override
 public Fingerprint.RangeSet getDownstreamRelationship(AbstractProject that) {
   Fingerprint.RangeSet rs = super.getDownstreamRelationship(that);
   for (List<IvyBuild> builds : getModuleBuilds().values())
     for (IvyBuild b : builds) rs.add(b.getDownstreamRelationship(that));
   return rs;
 }
  /** Called when a module build that corresponds to this module set build has completed. */
  /* package */ void notifyModuleBuild(IvyBuild newBuild) {
    try {
      // update module set build number
      getParent().updateNextBuildNumber();

      // update actions
      Map<IvyModule, List<IvyBuild>> moduleBuilds = getModuleBuilds();

      // actions need to be replaced atomically especially
      // given that two builds might complete simultaneously.
      synchronized (this) {
        boolean modified = false;

        List<Action> actions = getActions();
        Set<Class<? extends AggregatableAction>> individuals =
            new HashSet<Class<? extends AggregatableAction>>();
        for (Action a : actions) {
          if (a instanceof IvyAggregatedReport) {
            IvyAggregatedReport mar = (IvyAggregatedReport) a;
            mar.update(moduleBuilds, newBuild);
            individuals.add(mar.getIndividualActionType());
            modified = true;
          }
        }

        // see if the new build has any new aggregatable action that we
        // haven't seen.
        for (AggregatableAction aa : newBuild.getActions(AggregatableAction.class)) {
          if (individuals.add(aa.getClass())) {
            // new AggregatableAction
            IvyAggregatedReport mar = aa.createAggregatedAction(this, moduleBuilds);
            mar.update(moduleBuilds, newBuild);
            actions.add(mar);
            modified = true;
          }
        }

        if (modified) {
          save();
          getProject().updateTransientActions();
        }
      }

      // symlink to this module build
      String moduleFsName = newBuild.getProject().getModuleName().toFileSystemName();
      Util.createSymlink(
          getRootDir(),
          "../../modules/" + moduleFsName + "/builds/" + newBuild.getId() /*ugly!*/,
          moduleFsName,
          StreamTaskListener.NULL);
    } catch (IOException e) {
      LOGGER.log(Level.WARNING, "Failed to update " + this, e);
    } catch (InterruptedException e) {
      LOGGER.log(Level.WARNING, "Failed to update " + this, e);
    }
  }
  /**
   * Displays the combined status of all modules.
   *
   * <p>More precisely, this picks up the status of this build itself, plus all the latest builds of
   * the modules that belongs to this build.
   */
  @Override
  public Result getResult() {
    Result r = super.getResult();

    for (IvyBuild b : getModuleLastBuilds().values()) {
      Result br = b.getResult();
      if (r == null) r = br;
      else if (br == Result.NOT_BUILT) continue; // UGLY: when computing combined status, ignore the
      // modules that were not built
      else if (br != null) r = r.combine(br);
    }

    return r;
  }
  /** Computes the latest module builds that correspond to this build. */
  public Map<IvyModule, IvyBuild> getModuleLastBuilds() {
    Collection<IvyModule> mods = getParent().getModules();

    // identify the build number range. [start,end)
    IvyModuleSetBuild nb = getNextBuild();
    int end = nb != null ? nb.getNumber() : Integer.MAX_VALUE;

    // preserve the order by using LinkedHashMap
    Map<IvyModule, IvyBuild> r = new LinkedHashMap<IvyModule, IvyBuild>(mods.size());

    for (IvyModule m : mods) {
      IvyBuild b = m.getNearestOldBuild(end - 1);
      if (b != null && b.getNumber() >= getNumber()) r.put(m, b);
    }

    return r;
  }
  /**
   * Finds {@link Action}s from all the module builds that belong to this {@link IvyModuleSetBuild}.
   * One action per one {@link IvyModule}, and newer ones take precedence over older ones.
   */
  public <T extends Action> List<T> findModuleBuildActions(Class<T> action) {
    Collection<IvyModule> mods = getParent().getModules();
    List<T> r = new ArrayList<T>(mods.size());

    // identify the build number range. [start,end)
    IvyModuleSetBuild nb = getNextBuild();
    int end = nb != null ? nb.getNumber() - 1 : Integer.MAX_VALUE;

    for (IvyModule m : mods) {
      IvyBuild b = m.getNearestOldBuild(end);
      while (b != null && b.getNumber() >= number) {
        T a = b.getAction(action);
        if (a != null) {
          r.add(a);
          break;
        }
        b = b.getPreviousBuild();
      }
    }

    return r;
  }
  /**
   * Computes the module builds that correspond to this build.
   *
   * <p>A module may be built multiple times (by the user action), so the value is a list.
   */
  public Map<IvyModule, List<IvyBuild>> getModuleBuilds() {
    Collection<IvyModule> mods = getParent().getModules();

    // identify the build number range. [start,end)
    IvyModuleSetBuild nb = getNextBuild();
    int end = nb != null ? nb.getNumber() : Integer.MAX_VALUE;

    // preserve the order by using LinkedHashMap
    Map<IvyModule, List<IvyBuild>> r = new LinkedHashMap<IvyModule, List<IvyBuild>>(mods.size());

    for (IvyModule m : mods) {
      List<IvyBuild> builds = new ArrayList<IvyBuild>();
      IvyBuild b = m.getNearestBuild(number);
      while (b != null && b.getNumber() < end) {
        builds.add(b);
        b = b.getNextBuild();
      }
      r.put(m, builds);
    }

    return r;
  }