/**
   * Takes a collection of StorageFiles and performs the replace functionality described in {@link
   * #replaceOriginal()}. However, all files that are part of the same {@link ShpFiles} are done
   * within a lock so all of the updates for all the Files of a Shapefile can be updated within a
   * single lock.
   *
   * @param storageFiles files to execute the replace functionality.
   * @throws IOException
   */
  static void replaceOriginals(final StorageFile... storageFiles) throws IOException {
    SortedSet<StorageFile> files = new TreeSet<StorageFile>(Arrays.asList(storageFiles));

    ShpFiles currentShpFiles = null;
    for (StorageFile storageFile : files) {
      if (currentShpFiles != storageFile.shpFiles) {
        // there's a new set of files so unlock old and lock new.
        currentShpFiles = storageFile.shpFiles;
      }

      final File storage = storageFile.getFile();

      final URL url = storageFile.getSrcURLForWrite();
      try {
        File dest = toFile(url);

        if (storage.equals(dest)) return;

        if (dest.exists()) {
          if (!dest.delete()) {
            LOGGER.severe(
                "Unable to delete the file: "
                    + dest
                    + " when attempting to replace with temporary copy.");
          }
        }

        if (storage.exists() && !storage.renameTo(dest)) {
          LOGGER.finer(
              "Unable to rename temporary file to the file: "
                  + dest
                  + " when attempting to replace with temporary copy");

          copyFile(storage, url, dest);
          if (!storage.delete()) {
            storage.deleteOnExit();
          }
        }
      } finally {
        if (storage.exists()) {
          storage.delete();
        }
      }
    }
  }
    @Override
    public void run() {
      /*
       * Do not retransmit a message if it has been acknowledged,
       * rejected, canceled or already been retransmitted for the maximum
       * number of times.
       */
      try {
        int failedCount = exchange.getFailedTransmissionCount() + 1;
        exchange.setFailedTransmissionCount(failedCount);

        if (message.isAcknowledged()) {
          LOGGER.finest(
              "Timeout: message already acknowledged, cancel retransmission of " + message);
          return;

        } else if (message.isRejected()) {
          LOGGER.finest("Timeout: message already rejected, cancel retransmission of " + message);
          return;

        } else if (message.isCanceled()) {
          LOGGER.finest("Timeout: canceled (MID=" + message.getMID() + "), do not retransmit");
          return;

        } else if (failedCount <= max_retransmit) {
          LOGGER.finer(
              "Timeout: retransmit message, failed: " + failedCount + ", message: " + message);

          // Trigger MessageObservers
          message.retransmitting();

          // MessageObserver might have canceled
          if (!message.isCanceled()) retransmit();

        } else {
          LOGGER.info(
              "Timeout: retransmission limit reached, exchange failed, message: " + message);
          exchange.setTimedOut();
          message.setTimedOut(true);
        }
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
  /**
   * Resolves all the groups that the user is in.
   *
   * <p>We now use <a
   * href="http://msdn.microsoft.com/en-us/library/windows/desktop/ms680275(v=vs.85).aspx">tokenGroups</a>
   * attribute, which is a computed attribute that lists all the SIDs of the groups that the user is
   * directly/indirectly in. We then use that to retrieve all the groups in one query and resolve
   * their canonical names.
   *
   * @param userDN User's distinguished name.
   * @param context Used for making queries.
   */
  private Set<GrantedAuthority> resolveGroups(String domainDN, String userDN, DirContext context)
      throws NamingException {
    LOGGER.finer("Looking up group of " + userDN);
    Attributes id = context.getAttributes(userDN, new String[] {"tokenGroups", "memberOf", "CN"});
    Attribute tga = id.get("tokenGroups");
    if (tga == null) { // see JENKINS-11644. still trying to figure out when this happens
      LOGGER.warning("Failed to retrieve tokenGroups for " + userDN);
      HashSet<GrantedAuthority> r = new HashSet<GrantedAuthority>();
      r.add(new GrantedAuthorityImpl("unable-to-retrieve-tokenGroups"));
      return r;
    }

    // build up the query to retrieve all the groups
    StringBuilder query = new StringBuilder("(|");
    List<byte[]> sids = new ArrayList<byte[]>();

    NamingEnumeration<?> tokenGroups = tga.getAll();
    while (tokenGroups.hasMore()) {
      byte[] gsid = (byte[]) tokenGroups.next();
      query.append("(objectSid={" + sids.size() + "})");
      sids.add(gsid);
    }
    tokenGroups.close();

    query.append(")");

    Set<GrantedAuthority> groups = new HashSet<GrantedAuthority>();

    NamingEnumeration<SearchResult> renum =
        new LDAPSearchBuilder(context, domainDN)
            .subTreeScope()
            .returns("cn")
            .search(query.toString(), sids.toArray());
    while (renum.hasMore()) {
      Attributes a = renum.next().getAttributes();
      Attribute cn = a.get("cn");
      if (LOGGER.isLoggable(Level.FINE)) LOGGER.fine(userDN + " is a member of " + cn);
      groups.add(new GrantedAuthorityImpl(cn.get().toString()));
    }
    renum.close();

    {
        /*
            stage 2: use memberOf to find groups that aren't picked up by tokenGroups.
            This includes distribution groups
        */
      LOGGER.fine("Stage 2: looking up via memberOf");

      Stack<Attributes> q = new Stack<Attributes>();
      q.push(id);
      while (!q.isEmpty()) {
        Attributes identity = q.pop();
        LOGGER.finer("Looking up group of " + identity);

        Attribute memberOf = identity.get("memberOf");
        if (memberOf == null) continue;

        for (int i = 0; i < memberOf.size(); i++) {
          try {
            Attributes group =
                context.getAttributes(
                    new LdapName(memberOf.get(i).toString()), new String[] {"CN", "memberOf"});
            Attribute cn = group.get("CN");
            if (cn == null) {
              LOGGER.fine("Failed to obtain CN of " + memberOf.get(i));
              continue;
            }
            if (LOGGER.isLoggable(Level.FINE))
              LOGGER.fine(cn.get() + " is a member of " + memberOf.get(i));

            if (groups.add(new GrantedAuthorityImpl(cn.get().toString()))) {
              q.add(group); // recursively look for groups that this group is a member of.
            }
          } catch (NameNotFoundException e) {
            LOGGER.fine("Failed to obtain CN of " + memberOf.get(i));
          }
        }
      }
    }

    return groups;
  }
  /**
   * Notify the commit to this repository.
   *
   * <p>Because this URL is not guarded, we can't really trust the data that's sent to us. But we
   * intentionally don't protect this URL to simplify <tt>post-commit</tt> script set up.
   */
  public void doNotifyCommit(StaplerRequest req, StaplerResponse rsp)
      throws ServletException, IOException {
    requirePOST();

    // compute the affected paths
    Set<String> affectedPath = new HashSet<String>();
    String line;
    BufferedReader r = new BufferedReader(req.getReader());

    try {
      while ((line = r.readLine()) != null) {
        if (LOGGER.isLoggable(FINER)) {
          LOGGER.finer("Reading line: " + line);
        }
        affectedPath.add(line.substring(4));
        if (line.startsWith("svnlook changed --revision ")) {
          String msg =
              "Expecting the output from the svnlook command but instead you just sent me the svnlook invocation command line: "
                  + line;
          LOGGER.warning(msg);
          throw new IllegalArgumentException(msg);
        }
      }
    } finally {
      IOUtils.closeQuietly(r);
    }

    if (LOGGER.isLoggable(FINE))
      LOGGER.fine("Change reported to Subversion repository " + uuid + " on " + affectedPath);
    boolean scmFound = false, triggerFound = false, uuidFound = false, pathFound = false;

    // we can't reliably use req.getParameter() as it can try to parse the payload, which we've
    // already consumed above.
    // servlet container relies on Content-type to decide if it wants to parse the payload or not,
    // and at least
    // in case of Jetty, it doesn't check if the payload is
    QueryParameterMap query = new QueryParameterMap(req);
    String revParam = query.get("rev");
    long rev = -1;
    if (revParam != null) {
      rev = Long.parseLong(revParam);
    } else {
      revParam = req.getHeader("X-Hudson-Subversion-Revision");
      if (revParam != null) {
        rev = Long.parseLong(revParam);
      }
    }

    OUTER:
    for (AbstractProject<?, ?> p : Hudson.getInstance().getItems(AbstractProject.class)) {
      try {
        SCM scm = p.getScm();
        if (scm instanceof SubversionSCM) scmFound = true;
        else continue;

        SCMTrigger trigger = p.getTrigger(SCMTrigger.class);
        if (trigger != null) triggerFound = true;
        else continue;

        SubversionSCM sscm = (SubversionSCM) scm;
        for (ModuleLocation loc : sscm.getLocations()) {
          if (loc.getUUID(p).equals(uuid)) uuidFound = true;
          else continue;

          String m = loc.getSVNURL().getPath();
          String n = loc.getRepositoryRoot(p).getPath();
          if (!m.startsWith(n))
            continue; // repository root should be a subpath of the module path, but be defensive

          String remaining = m.substring(n.length());
          if (remaining.startsWith("/")) remaining = remaining.substring(1);
          String remainingSlash = remaining + '/';

          final RevisionParameterAction[] actions;
          if (rev != -1) {
            SvnInfo info[] = {new SvnInfo(loc.getURL(), rev)};
            RevisionParameterAction action = new RevisionParameterAction(info);
            actions = new RevisionParameterAction[] {action};

          } else {
            actions = new RevisionParameterAction[0];
          }

          for (String path : affectedPath) {
            if (path.equals(remaining) /*for files*/
                || path.startsWith(remainingSlash) /*for dirs*/) {
              // this project is possibly changed. poll now.
              // if any of the data we used was bogus, the trigger will not detect a change
              LOGGER.fine("Scheduling the immediate polling of " + p);
              trigger.run(actions);
              pathFound = true;

              continue OUTER;
            }
          }
        }
      } catch (SVNException e) {
        LOGGER.log(WARNING, "Failed to handle Subversion commit notification", e);
      }
    }

    if (!scmFound) LOGGER.warning("No subversion jobs found");
    else if (!triggerFound) LOGGER.warning("No subversion jobs using SCM polling");
    else if (!uuidFound) LOGGER.warning("No subversion jobs using repository: " + uuid);
    else if (!pathFound) LOGGER.fine("No jobs found matching the modified files");

    rsp.setStatus(SC_OK);
  }