docs: public PluginEntry[] getCategorizedAvailables() {
          TreeSet<PluginEntry> entries = new TreeSet<>();
          for (Plugin p : getAvailables()) {
              if (p.categories==null || p.categories.length==0)
                  entries.add(new PluginEntry(p, getCategoryDisplayName(null)));
              else
                  for (String c : p.categories)
                      entries.add(new PluginEntry(p, getCategoryDisplayName(c)));
          }
          return entries.toArray(new PluginEntry[0]);
      }
  
      private static String getCategoryDisplayName(String category) {
          if (category==null)
              return Messages.UpdateCenter_PluginCategory_misc();
          try {
              return (String)Messages.class.getMethod(
                      "UpdateCenter_PluginCategory_" + category.replace('-', '_')).invoke(null);
          } catch (Exception ex) {
              return Messages.UpdateCenter_PluginCategory_unrecognized(category);
          }
      }
  
      public List<Plugin> getUpdates() {
          Map<String,Plugin> pluginMap = new LinkedHashMap<>();
          for (UpdateSite site : sites) {
              for (Plugin plugin: site.getUpdates()) {
                  final Plugin existing = pluginMap.get(plugin.name);
                  if (existing == null) {
                      pluginMap.put(plugin.name, plugin);
                  } else if (!existing.version.equals(plugin.version)) {
                      // allow secondary update centers to publish different versions
                      // TODO refactor to consolidate multiple versions of the same plugin within the one row
                      final String altKey = plugin.name + ":" + plugin.version;
                      if (!pluginMap.containsKey(altKey)) {
                          pluginMap.put(altKey, plugin);
                      }
                  }
              }
          }
  
          return new ArrayList<>(pluginMap.values());
      }
  
      /**
       * Ensure that all UpdateSites are up to date, without requiring a user to
       * browse to the instance.
       *
       * @return a list of {@link FormValidation} for each updated Update Site
       * @throws ExecutionException
       * @throws InterruptedException
       * @since 1.501
       *
       */
      public List<FormValidation> updateAllSites() throws InterruptedException, ExecutionException {
          List <Future<FormValidation>> futures = new ArrayList<>();
          for (UpdateSite site : getSites()) {
              Future<FormValidation> future = site.updateDirectly(DownloadService.signatureCheck);
              if (future != null) {
                  futures.add(future);
              }
          }
  
          List<FormValidation> results = new ArrayList<>();
          for (Future<FormValidation> f : futures) {
              results.add(f.get());
          }
          return results;
      }
  
      /**
       * {@link AdministrativeMonitor} that checks if there's Jenkins update.
       */
      @Extension @Symbol("coreUpdate")
      public static final class CoreUpdateMonitor extends AdministrativeMonitor {
  
          @Override
          public String getDisplayName() {
              return Messages.UpdateCenter_CoreUpdateMonitor_DisplayName();
          }
  
          public boolean isActivated() {
              Data data = getData();
              return data!=null && data.hasCoreUpdates();
          }
  
          public Data getData() {
              UpdateSite cs = Jenkins.getInstance().getUpdateCenter().getCoreSource();
              if (cs!=null)   return cs.getData();
              return null;
          }
      }
  
  
      /**
       * Strategy object for controlling the update center's behaviors.
       *
       * <p>
       * Until 1.333, this extension point used to control the configuration of
       * where to get updates (hence the name of this class), but with the introduction
       * of multiple update center sites capability, that functionality is achieved by
       * simply installing another {@link UpdateSite}.
       *
       * <p>
       * See {@link UpdateSite} for how to manipulate them programmatically.
       *
       * @since 1.266
       */
      @SuppressWarnings({"UnusedDeclaration"})
      public static class UpdateCenterConfiguration implements ExtensionPoint {
          /**
           * Creates default update center configuration - uses settings for global update center.
           */
          public UpdateCenterConfiguration() {
          }
  
          /**
           * Check network connectivity by trying to establish a connection to
           * the host in connectionCheckUrl.
           *
           * @param job The connection checker that is invoking this strategy.
           * @param connectionCheckUrl A string containing the URL of a domain
           *          that is assumed to be always available.
           * @throws IOException if a connection can't be established
           */
          public void checkConnection(ConnectionCheckJob job, String connectionCheckUrl) throws IOException {
              testConnection(new URL(connectionCheckUrl));
          }
  
          /**
           * Check connection to update center server.
           *
           * @param job The connection checker that is invoking this strategy.
           * @param updateCenterUrl A sting containing the URL of the update center host.
           * @throws IOException if a connection to the update center server can't be established.
           */
          public void checkUpdateCenter(ConnectionCheckJob job, String updateCenterUrl) throws IOException {
              testConnection(toUpdateCenterCheckUrl(updateCenterUrl));
          }
  
          /**
           * Converts an update center URL into the URL to use for checking its connectivity.
           * @param updateCenterUrl the URL to convert.
           * @return the converted URL.
           * @throws MalformedURLException if the supplied URL is malformed.
           */
          static URL toUpdateCenterCheckUrl(String updateCenterUrl) throws MalformedURLException {
              URL url;
              if (updateCenterUrl.startsWith("http://") || updateCenterUrl.startsWith("https://")) {
                  url = new URL(updateCenterUrl + (updateCenterUrl.indexOf('?') == -1 ? "?uctest" : "&uctest"));
              } else {
                  url = new URL(updateCenterUrl);
              }
              return url;
          }
  
          /**
           * Validate the URL of the resource before downloading it.
           *
           * @param job The download job that is invoking this strategy. This job is
           *          responsible for managing the status of the download and installation.
           * @param src The location of the resource on the network
           * @throws IOException if the validation fails
           */
          public void preValidate(DownloadJob job, URL src) throws IOException {
          }
  
          /**
           * Validate the resource after it has been downloaded, before it is
           * installed. The default implementation does nothing.
           *
           * @param job The download job that is invoking this strategy. This job is
           *          responsible for managing the status of the download and installation.
           * @param src The location of the downloaded resource.
           * @throws IOException if the validation fails.
           */
          public void postValidate(DownloadJob job, File src) throws IOException {
          }
  
          /**
           * Download a plugin or core upgrade in preparation for installing it
           * into its final location. Implementations will normally download the
           * resource into a temporary location and hand off a reference to this
           * location to the install or upgrade strategy to move into the final location.
           *
           * @param job The download job that is invoking this strategy. This job is
           *          responsible for managing the status of the download and installation.
           * @param src The URL to the resource to be downloaded.
           * @return A File object that describes the downloaded resource.
           * @throws IOException if there were problems downloading the resource.
           * @see DownloadJob
           */
          public File download(DownloadJob job, URL src) throws IOException {
              MessageDigest sha1 = null;
              MessageDigest sha256 = null;
              MessageDigest sha512 = null;
              try {
                  // Java spec says SHA-1 and SHA-256 exist, and SHA-512 might not, so one try/catch block should be fine
                  sha1 = MessageDigest.getInstance("SHA-1");
                  sha256 = MessageDigest.getInstance("SHA-256");
                  sha512 = MessageDigest.getInstance("SHA-512");
              } catch (NoSuchAlgorithmException nsa) {
                  LOGGER.log(Level.WARNING, "Failed to instantiate message digest algorithm, may only have weak or no verification of downloaded file", nsa);
              }
  
              URLConnection con = null;
              try {
                  con = connect(job,src);
                  //JENKINS-34174 - set timeout for downloads, may hang indefinitely
                  // particularly noticeable during 2.0 install when downloading
                  // many plugins
                  con.setReadTimeout(PLUGIN_DOWNLOAD_READ_TIMEOUT);
                  
                  int total = con.getContentLength();
                  byte[] buf = new byte[8192];
                  int len;
  
                  File dst = job.getDestination();
                  File tmp = new File(dst.getPath()+".tmp");
  
                  LOGGER.info("Downloading "+job.getName());
                  Thread t = Thread.currentThread();
                  String oldName = t.getName();
                  t.setName(oldName + ": " + src);
                  try (OutputStream _out = Files.newOutputStream(tmp.toPath());
                       OutputStream out =
                               sha1 != null ? new DigestOutputStream(
                                       sha256 != null ? new DigestOutputStream(
                                               sha512 != null ? new DigestOutputStream(_out, sha512) : _out, sha256) : _out, sha1) : _out;
                       InputStream in = con.getInputStream();
                       CountingInputStream cin = new CountingInputStream(in)) {
                      while ((len = cin.read(buf)) >= 0) {
                          out.write(buf,0,len);
                          job.status = job.new Installing(total == -1 ? -1 : cin.getCount() * 100 / total);
                      }
                  } catch (IOException | InvalidPathException e) {
                      throw new IOException("Failed to load "+src+" to "+tmp,e);
                  } finally {
                      t.setName(oldName);
                  }
  
                  if (total!=-1 && total!=tmp.length()) {
                      // don't know exactly how this happens, but report like
                      // http://www.ashlux.com/wordpress/2009/08/14/hudson-and-the-sonar-plugin-fail-maveninstallation-nosuchmethoderror/
                      // indicates that this kind of inconsistency can happen. So let's be defensive
                      throw new IOException("Inconsistent file length: expected "+total+" but only got "+tmp.length());
                  }
  
                  if (sha1 != null) {
                      byte[] digest = sha1.digest();
                      job.computedSHA1 = Base64.encodeBase64String(digest);
                  }
                  if (sha256 != null) {
                      byte[] digest = sha256.digest();
                      job.computedSHA256 = Base64.encodeBase64String(digest);
                  }
                  if (sha512 != null) {
                      byte[] digest = sha512.digest();
                      job.computedSHA512 = Base64.encodeBase64String(digest);
                  }
                  return tmp;
              } catch (IOException e) {
                  // assist troubleshooting in case of e.g. "too many redirects" by printing actual URL
                  String extraMessage = "";
                  if (con != null && con.getURL() != null && !src.toString().equals(con.getURL().toString())) {
                      // Two URLs are considered equal if different hosts resolve to same IP. Prefer to log in case of string inequality,
                      // because who knows how the server responds to different host name in the request header?
                      // Also, since it involved name resolution, it'd be an expensive operation.
                      extraMessage = " (redirected to: " + con.getURL() + ")";
                  }
                  throw new IOException("Failed to download from "+src+extraMessage,e);
              }
          }
  
          /**
           * Connects to the given URL for downloading the binary. Useful for tweaking
           * how the connection gets established.
           */
          protected URLConnection connect(DownloadJob job, URL src) throws IOException {
              return ProxyConfiguration.open(src);
          }
  
          /**
           * Called after a plugin has been downloaded to move it into its final
           * location. The default implementation is a file rename.
           *
           * @param job The install job that is invoking this strategy.
           * @param src The temporary location of the plugin.
           * @param dst The final destination to install the plugin to.
           * @throws IOException if there are problems installing the resource.
           */
          public void install(DownloadJob job, File src, File dst) throws IOException {
              job.replace(dst, src);
          }
  
          /**
           * Called after an upgrade has been downloaded to move it into its final
           * location. The default implementation is a file rename.
           *
           * @param job The upgrade job that is invoking this strategy.
           * @param src The temporary location of the upgrade.
           * @param dst The final destination to install the upgrade to.
           * @throws IOException if there are problems installing the resource.
           */
          public void upgrade(DownloadJob job, File src, File dst) throws IOException {
              job.replace(dst, src);
          }
  
          /**
           * Returns an "always up" server for Internet connectivity testing.
           *
           * @deprecated as of 1.333
           *      With the introduction of multiple update center capability, this information
           *      is now a part of the {@code update-center.json} file. See
           *      {@code http://jenkins-ci.org/update-center.json} as an example.
           */
          @Deprecated
          public String getConnectionCheckUrl() {
              return "http://www.google.com";
          }
  
          /**
           * Returns the URL of the server that hosts the update-center.json
           * file.
           *
           * @deprecated as of 1.333
           *      With the introduction of multiple update center capability, this information
           *      is now moved to {@link UpdateSite}.
           * @return
           *      Absolute URL that ends with '/'.
           */
          @Deprecated
          public String getUpdateCenterUrl() {
              return UPDATE_CENTER_URL;
          }
  
          /**
           * Returns the URL of the server that hosts plugins and core updates.
           *
           * @deprecated as of 1.333
           *      {@code update-center.json} is now signed, so we don't have to further make sure that
           *      we aren't downloading from anywhere unsecure.
           */
          @Deprecated
          public String getPluginRepositoryBaseUrl() {
              return "http://jenkins-ci.org/";
          }
  
  
          private void testConnection(URL url) throws IOException {
              try {
                  URLConnection connection = (URLConnection) ProxyConfiguration.open(url);
  
                  if(connection instanceof HttpURLConnection) {
                      int responseCode = ((HttpURLConnection)connection).getResponseCode();
                      if(HttpURLConnection.HTTP_OK != responseCode) {
                          throw new HttpRetryException("Invalid response code (" + responseCode + ") from URL: " + url, responseCode);
                      }
                  } else {
                      try (InputStream is = connection.getInputStream()) {
                          IOUtils.copy(is, new NullOutputStream());
                      }
                  }
              } catch (SSLHandshakeException e) {
                  if (e.getMessage().contains("PKIX path building failed"))
                     // fix up this crappy error message from JDK
                      throw new IOException("Failed to validate the SSL certificate of "+url,e);
              }
          }
      }
  
      /**
       * Things that {@link UpdateCenter#installerService} executes.
       *
       * This object will have the {@code row.jelly} which renders the job on UI.
       */
      @ExportedBean
      public abstract class UpdateCenterJob implements Runnable {
          /**
           * Unique ID that identifies this job.
           *
           * @see UpdateCenter#getJob(int)
           */
          @Exported
          public final int id = iota.incrementAndGet();
  
          /**
           * Which {@link UpdateSite} does this belong to?
           */
          public final UpdateSite site;
  
          /**
           * Simple correlation ID that can be used to associated a batch of jobs e.g. the
           * installation of a set of plugins.
           */
          private UUID correlationId = null;
  
          /**
           * If this job fails, set to the error.
           */
          protected Throwable error;
  
          protected UpdateCenterJob(UpdateSite site) {
              this.site = site;
          }
  
          public Api getApi() {
              return new Api(this);
          }
  
          public UUID getCorrelationId() {
              return correlationId;
          }
  
          public void setCorrelationId(UUID correlationId) {
              if (this.correlationId != null) {
                  throw new IllegalStateException("Illegal call to set the 'correlationId'. Already set.");
              }
              this.correlationId = correlationId;
          }
  
          /**
           * @deprecated as of 1.326
           *      Use {@link #submit()} instead.
           */
          @Deprecated
          public void schedule() {
              submit();
          }
  
          @Exported
          public String getType() {
              return getClass().getSimpleName();
          }
  
          /**
           * Schedules this job for an execution
           * @return
           *      {@link Future} to keeps track of the status of the execution.
           */
          public Future<UpdateCenterJob> submit() {
              LOGGER.fine("Scheduling "+this+" to installerService");
              // TODO: seems like this access to jobs should be synchronized, no?
              // It might get synch'd accidentally via the addJob method, but that wouldn't be good.
              jobs.add(this);
              return installerService.submit(this,this);
          }
  
          @Exported
          public String getErrorMessage() {
              return error != null ? error.getMessage() : null;
          }
  
          public Throwable getError() {
              return error;
          }
      }
  
      /**
       * Restarts jenkins.
       */
      public class RestartJenkinsJob extends UpdateCenterJob {
           /**
           * Immutable state of this job.
           */
           @Exported(inline=true)
          public volatile RestartJenkinsJobStatus status = new Pending();
  
          /**
           * The name of the user that started this job
           */
          private String authentication;
  
          /**
           * Cancel job
           */
          public synchronized boolean cancel() {
              if (status instanceof Pending) {
                  status = new Canceled();
                  return true;
              }
              return false;
          }
  
          public RestartJenkinsJob(UpdateSite site) {
              super(site);
              this.authentication = Jenkins.getAuthentication().getName();
          }
  
          public synchronized void run() {
              if (!(status instanceof Pending)) {
                  return;
              }
              status = new Running();
              try {
                  // safeRestart records the current authentication for the log, so set it to the managing user
                  try (ACLContext acl = ACL.as(User.get(authentication, false, Collections.emptyMap()))) {
                      Jenkins.getInstance().safeRestart();
                  }
              } catch (RestartNotSupportedException exception) {
                  // ignore if restart is not allowed
                  status = new Failure();
                  error = exception;
              }
          }
  
          @ExportedBean
          public abstract class RestartJenkinsJobStatus {
              @Exported
              public final int id = iota.incrementAndGet();
  
          }
  
          public class Pending extends RestartJenkinsJobStatus {
              @Exported
              public String getType() {
                  return getClass().getSimpleName();
              }
          }
  
          public class Running extends RestartJenkinsJobStatus {
  
          }
  
          public class Failure extends RestartJenkinsJobStatus {
  
          }
  
          public class Canceled extends RestartJenkinsJobStatus {
  
          }
      }
  
      /**
       * Tests the internet connectivity.
       */
      public final class ConnectionCheckJob extends UpdateCenterJob {
          private final Vector<String> statuses= new Vector<String>();
  
          final Map<String, ConnectionStatus> connectionStates = new ConcurrentHashMap<>();
  
          public ConnectionCheckJob(UpdateSite site) {
              super(site);
              connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.PRECHECK);
              connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.PRECHECK);
          }
  
          public void run() {
              connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.UNCHECKED);
              connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.UNCHECKED);
              if (ID_UPLOAD.equals(site.getId())) {
                  return;
              }
              LOGGER.fine("Doing a connectivity check");
              Future<?> internetCheck = null;
              try {
                  final String connectionCheckUrl = site.getConnectionCheckUrl();
                  if (connectionCheckUrl!=null) {
                      connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.CHECKING);
                      statuses.add(Messages.UpdateCenter_Status_CheckingInternet());
                      // Run the internet check in parallel
                      internetCheck = updateService.submit(new Runnable() {
                          @Override
                          public void run() {
                              try {
                                  config.checkConnection(ConnectionCheckJob.this, connectionCheckUrl);
                              } catch (Exception e) {
                                  if(e.getMessage().contains("Connection timed out")) {
                                      // Google can't be down, so this is probably a proxy issue
                                      connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.FAILED);
                                      statuses.add(Messages.UpdateCenter_Status_ConnectionFailed(connectionCheckUrl));
                                      return;
                                  }
                              }
                              connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.OK);
                          }
                      });
                  } else {
                      LOGGER.log(WARNING, "Update site ''{0}'' does not declare the connection check URL. "
                              + "Skipping the network availability check.", site.getId());
                      connectionStates.put(ConnectionStatus.INTERNET, ConnectionStatus.SKIPPED);
                  }
  
                  connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.CHECKING);
                  statuses.add(Messages.UpdateCenter_Status_CheckingJavaNet());
  
                  config.checkUpdateCenter(this, site.getUrl());
  
                  connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.OK);
                  statuses.add(Messages.UpdateCenter_Status_Success());
              } catch (UnknownHostException e) {
                  connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.FAILED);
                  statuses.add(Messages.UpdateCenter_Status_UnknownHostException(e.getMessage()));
                  addStatus(e);
                  error = e;
              } catch (Exception e) {
                  connectionStates.put(ConnectionStatus.UPDATE_SITE, ConnectionStatus.FAILED);
                  statuses.add(Functions.printThrowable(e));
                  error = e;
              }
              
              if(internetCheck != null) {
                  try {
                      // Wait for internet check to complete
                      internetCheck.get();
                  } catch (Exception e) {
                      LOGGER.log(Level.WARNING, "Error completing internet connectivity check: " + e.getMessage(), e);
                  }
              }
          }
  
          private void addStatus(UnknownHostException e) {
              statuses.add("<pre>"+ Functions.xmlEscape(Functions.printThrowable(e))+"</pre>");
          }
  
          public String[] getStatuses() {
              synchronized (statuses) {
                  return statuses.toArray(new String[statuses.size()]);
              }
          }
  
  
      }
      
      /**
       * Enables a required plugin, provides feedback in the update center
       */
      public class EnableJob extends InstallationJob {
          public EnableJob(UpdateSite site, Authentication auth, @Nonnull Plugin plugin, boolean dynamicLoad) {
              super(plugin, site, auth, dynamicLoad);
          }
          
          public Plugin getPlugin() {
              return plugin;
          }
          
          @Override
          public void run() {
              try {
                  PluginWrapper installed = plugin.getInstalled();
                  synchronized(installed) {
                      if (!installed.isEnabled()) {
                          try {
                              installed.enable();
                          } catch (IOException e) {
                              LOGGER.log(Level.SEVERE, "Failed to enable " + plugin.getDisplayName(), e);
                              error = e;
                              status = new Failure(e);
                          }
                          
                          if (dynamicLoad) {
                              try {
                                  // remove the existing, disabled inactive plugin to force a new one to load
                                  pm.dynamicLoad(getDestination(), true);
                              } catch (Exception e) {
                                  LOGGER.log(Level.SEVERE, "Failed to dynamically load " + plugin.getDisplayName(), e);
                                  error = e;
                                  requiresRestart = true;
                                  status = new Failure(e);
                              }
                          } else {
                              requiresRestart = true;
                          }
                      }
                  }
              } catch(Throwable e) {
                  LOGGER.log(Level.SEVERE, "An unexpected error occurred while attempting to enable " + plugin.getDisplayName(), e);
                  error = e;
                  requiresRestart = true;
                  status = new Failure(e);
              }
              if(status instanceof Pending) {
                  status = new Success();
              }
          }
      }
      
      /**
       * A no-op, e.g. this plugin is already installed
       */
      public class NoOpJob extends EnableJob {
          public NoOpJob(UpdateSite site, Authentication auth, @Nonnull Plugin plugin) {
              super(site, auth, plugin, false);
          }
          @Override
          public void run() {
              // do nothing
              status = new Success();
          }
      }
  
      @Restricted(NoExternalUse.class)
      /*package*/ interface WithComputedChecksums {
          String getComputedSHA1();
          String getComputedSHA256();
          String getComputedSHA512();
      }
      
      /**
       * Base class for a job that downloads a file from the Jenkins project.
       */
      public abstract class DownloadJob extends UpdateCenterJob implements WithComputedChecksums {
          /**
           * Immutable object representing the current state of this job.
           */
          @Exported(inline=true)
          public volatile InstallationStatus status = new Pending();
  
          /**
           * Where to download the file from.
           */
          protected abstract URL getURL() throws MalformedURLException;
  
          /**
           * Where to download the file to.
           */
          protected abstract File getDestination();
  
          @Exported
          public abstract String getName();
  
          /**
           * Called when the whole thing went successfully.
           */
          protected abstract void onSuccess();
  
          /**
           * During download, an attempt is made to compute the SHA-1 checksum of the file.
           * This is the base64 encoded SHA-1 checksum.
           *
           * @since 1.641
           */
          @CheckForNull
          public String getComputedSHA1() {
              return computedSHA1;
          }
  
          private String computedSHA1;
  
          /**
           * Base64 encoded SHA-256 checksum of the downloaded file, if it could be computed.
           *
           * @since 2.130
           */
          @CheckForNull
          public String getComputedSHA256() {
              return computedSHA256;
          }
  
          private String computedSHA256;
  
          /**
           * Base64 encoded SHA-512 checksum of the downloaded file, if it could be computed.
           *
           * @since 2.130
           */
          @CheckForNull
          public String getComputedSHA512() {
              return computedSHA512;
          }
  
          private String computedSHA512;
  
          private Authentication authentication;
  
          /**
           * Get the user that initiated this job
           */
          public Authentication getUser() {
              return this.authentication;
          }
  
          protected DownloadJob(UpdateSite site, Authentication authentication) {
              super(site);
              this.authentication = authentication;
          }
  
          public void run() {
              try {
                  LOGGER.info("Starting the installation of "+getName()+" on behalf of "+getUser().getName());
  
                  _run();
  
                  LOGGER.info("Installation successful: "+getName());
                  status = new Success();
                  onSuccess();
              } catch (InstallationStatus e) {
                  status = e;
                  if (status.isSuccess()) onSuccess();
                  requiresRestart |= status.requiresRestart();
              } catch (MissingDependencyException e) {
                  LOGGER.log(Level.SEVERE, "Failed to install {0}: {1}", new Object[] { getName(), e.getMessage() });
                  status = new Failure(e);
                  error = e;
              } catch (Throwable e) {
                  LOGGER.log(Level.SEVERE, "Failed to install "+getName(),e);
                  status = new Failure(e);
                  error = e;
              }
          }
  
          protected void _run() throws IOException, InstallationStatus {
              URL src = getURL();
  
              config.preValidate(this, src);
  
              File dst = getDestination();
              File tmp = config.download(this, src);
  
              config.postValidate(this, tmp);
              config.install(this, tmp, dst);
          }
  
          /**
           * Called when the download is completed to overwrite
           * the old file with the new file.
           */
          protected void replace(File dst, File src) throws IOException {
              File bak = Util.changeExtension(dst,".bak");
              bak.delete();
              dst.renameTo(bak);
              dst.delete(); // any failure up to here is no big deal
              if(!src.renameTo(dst)) {
                  throw new IOException("Failed to rename "+src+" to "+dst);
              }
          }
  
          /**
           * Indicates the status or the result of a plugin installation.
           * <p>
           * Instances of this class is immutable.
           */
          @ExportedBean
          public abstract class InstallationStatus extends Throwable {
              public final int id = iota.incrementAndGet();
              @Exported
              public boolean isSuccess() {
                  return false;
              }
              @Exported
              public final String getType() {
                  return getClass().getSimpleName();
              }
  
              /**
               * Indicates that a restart is needed to complete the tasks.
               */
              public boolean requiresRestart() {
                  return false;
              }
          }
  
          /**
           * Indicates that the installation of a plugin failed.
           */
          public class Failure extends InstallationStatus {
              public final Throwable problem;
  
              public Failure(Throwable problem) {
                  this.problem = problem;
              }
  
              public String getProblemStackTrace() {
                  return Functions.printThrowable(problem);
              }
          }
  
          /**
           * Indicates that the installation was successful but a restart is needed.
           */
          public class SuccessButRequiresRestart extends Success {
              private final Localizable message;
  
              public SuccessButRequiresRestart(Localizable message) {
                  this.message = message;
              }
  
              public String getMessage() {
                  return message.toString();
              }
  
              @Override
              public boolean requiresRestart() {
                  return true;
              }
          }
  
          /**
           * Indicates that the plugin was successfully installed.
           */
          public class Success extends InstallationStatus {
              @Override public boolean isSuccess() {
                  return true;
              }
          }
  
          /**
           * Indicates that the plugin was successfully installed.
           */
          public class Skipped extends InstallationStatus {
              @Override public boolean isSuccess() {
                  return true;
              }
          }
  
          /**
           * Indicates that the plugin is waiting for its turn for installation.
           */
          public class Pending extends InstallationStatus {
          }
  
          /**
           * Installation of a plugin is in progress.
           */
          public class Installing extends InstallationStatus {
              /**
               * % completed download, or -1 if the percentage is not known.
               */
              public final int percentage;
  
              public Installing(int percentage) {
                  this.percentage = percentage;
              }
          }
      }
  
      /**
       * Compare the provided values and return the appropriate {@link VerificationResult}.
       *
       */
      private static VerificationResult verifyChecksums(String expectedDigest, String actualDigest, boolean caseSensitive) {
          if (expectedDigest == null) {
              return VerificationResult.NOT_PROVIDED;
          }
  
          if (actualDigest == null) {
              return VerificationResult.NOT_COMPUTED;
          }
  
          if (caseSensitive ? expectedDigest.equals(actualDigest) : expectedDigest.equalsIgnoreCase(actualDigest)) {
              return VerificationResult.PASS;
          }
  
          return VerificationResult.FAIL;
      }
  
      private enum VerificationResult {
          PASS,
          NOT_PROVIDED,
          NOT_COMPUTED,
          FAIL
      }
  
      /**
       * Throws an {@code IOException} with a message about {@code actual} not matching {@code expected} for {@code file} when using {@code algorithm}.
       */
      private static void throwVerificationFailure(String expected, String actual, File file, String algorithm) throws IOException {
          throw new IOException("Downloaded file " + file.getAbsolutePath() + " does not match expected " + algorithm + ", expected '" + expected + "', actual '" + actual + "'");
      }
  
      /**
       * Implements the checksum verification logic with fallback to weaker algorithm for {@link DownloadJob}.
       * @param job The job downloading the file to check
       * @param entry The metadata entry for the file to check
       * @param file The downloaded file
       * @throws IOException thrown when one of the checks failed, or no checksum could be computed.
       */
      @VisibleForTesting
      @Restricted(NoExternalUse.class)
      /* package */ static void verifyChecksums(WithComputedChecksums job, UpdateSite.Entry entry, File file) throws IOException {
          VerificationResult result512 = verifyChecksums(entry.getSha512(), job.getComputedSHA512(), false);
          switch (result512) {
              case PASS:
                  // this has passed so no reason to check the weaker checksums
                  return;
              case FAIL:
                  throwVerificationFailure(entry.getSha512(), job.getComputedSHA512(), file, "SHA-512");
              case NOT_COMPUTED:
                  LOGGER.log(WARNING, "Attempt to verify a downloaded file (" + file.getName() + ") using SHA-512 failed since it could not be computed. Falling back to weaker algorithms. Update your JRE.");
                  break;
              case NOT_PROVIDED:
                  break;
          }
  
          VerificationResult result256 = verifyChecksums(entry.getSha256(), job.getComputedSHA256(), false);
          switch (result256) {
              case PASS:
                  return;
              case FAIL:
                  throwVerificationFailure(entry.getSha256(), job.getComputedSHA256(), file, "SHA-256");
              case NOT_COMPUTED:
              case NOT_PROVIDED:
                  break;
          }
  
          if (result512 == VerificationResult.NOT_PROVIDED && result256 == VerificationResult.NOT_PROVIDED) {
              LOGGER.log(INFO, "Attempt to verify a downloaded file (" + file.getName() + ") using SHA-512 or SHA-256 failed since your configured update site does not provide either of those checksums. Falling back to SHA-1.");
          }
  
          VerificationResult result1 = verifyChecksums(entry.getSha1(), job.getComputedSHA1(), true);
          switch (result1) {
              case PASS:
                  return;
              case FAIL:
                  throwVerificationFailure(entry.getSha1(), job.getComputedSHA1(), file, "SHA-1");
              case NOT_COMPUTED:
                  throw new IOException("Failed to compute SHA-1 of downloaded file, refusing installation");
              case NOT_PROVIDED:
                  throw new IOException("Unable to confirm integrity of downloaded file, refusing installation");
          }
      }
  
      /**
       * Represents the state of the installation activity of one plugin.
       */
      public class InstallationJob extends DownloadJob {
          /**
           * What plugin are we trying to install?
           */
          @Exported
          public final Plugin plugin;
  
          protected final PluginManager pm = Jenkins.getInstance().getPluginManager();
  
          /**
           * True to load the plugin into this Jenkins, false to wait until restart.
           */
          protected final boolean dynamicLoad;
  
          /**
           * @deprecated as of 1.442
           */
          @Deprecated
          public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth) {
              this(plugin,site,auth,false);
          }
  
          public InstallationJob(Plugin plugin, UpdateSite site, Authentication auth, boolean dynamicLoad) {
              super(site, auth);
              this.plugin = plugin;
              this.dynamicLoad = dynamicLoad;
          }
  
          protected URL getURL() throws MalformedURLException {
              return new URL(plugin.url);
          }
  
          protected File getDestination() {
              File baseDir = pm.rootDir;
              return new File(baseDir, plugin.name + ".jpi");
          }
  
          private File getLegacyDestination() {
              File baseDir = pm.rootDir;
              return new File(baseDir, plugin.name + ".hpi");
          }
  
          public String getName() {
              return plugin.getDisplayName();
          }
  
          @Override
          public void _run() throws IOException, InstallationStatus {
              if (wasInstalled()) {
                  // Do this first so we can avoid duplicate downloads, too
                  // check to see if the plugin is already installed at the same version and skip it
                  LOGGER.info("Skipping duplicate install of: " + plugin.getDisplayName() + "@" + plugin.version);
                  return;
              }
              try {
                  super._run();
  
                  // if this is a bundled plugin, make sure it won't get overwritten
                  PluginWrapper pw = plugin.getInstalled();
                  if (pw!=null && pw.isBundled()) {
                      SecurityContext oldContext = ACL.impersonate(ACL.SYSTEM);
                      try {
                          pw.doPin();
                      } finally {
                          SecurityContextHolder.setContext(oldContext);
                      }
                  }
  
                  if (dynamicLoad) {
                      try {
                          pm.dynamicLoad(getDestination());
                      } catch (RestartRequiredException e) {
                          throw new SuccessButRequiresRestart(e.message);
                      } catch (Exception e) {
                          throw new IOException("Failed to dynamically deploy this plugin",e);
                      }
                  } else {
                      throw new SuccessButRequiresRestart(Messages._UpdateCenter_DownloadButNotActivated());
                  }
              } finally {
                  synchronized(this) {
                      // There may be other threads waiting on completion
                      LOGGER.fine("Install complete for: " + plugin.getDisplayName() + "@" + plugin.version);
                      // some status other than Installing or Downloading needs to be set here
                      // {@link #isAlreadyInstalling()}, it will be overwritten by {@link DownloadJob#run()}
                      status = new Skipped();
                      notifyAll();
                  }
              }
          }
  
          /**
           * Indicates there is another installation job for this plugin
           * @since 2.1
           */
          protected boolean wasInstalled() {
              synchronized(UpdateCenter.this) {
                  for (UpdateCenterJob job : getJobs()) {
                      if (job == this) {
                          // oldest entries first, if we reach this instance,
                          // we need it to continue installing
                          return false;
                      }
                      if (job instanceof InstallationJob) {
                          InstallationJob ij = (InstallationJob)job;
                          if (ij.plugin.equals(plugin) && ij.plugin.version.equals(plugin.version)) {
                              // wait until other install is completed
                              synchronized(ij) {
                                  if(ij.status instanceof Installing || ij.status instanceof Pending) {
                                      try {
                                          LOGGER.fine("Waiting for other plugin install of: " + plugin.getDisplayName() + "@" + plugin.version);
                                          ij.wait();
                                      } catch (InterruptedException e) {
                                          throw new RuntimeException(e);
                                      }
                                  }
                                  // Must check for success, otherwise may have failed installation
                                  if (ij.status instanceof Success) {
                                      return true;
                                  }
                              }
                          }
                      }
                  }
                  return false;
              }
          }
  
          protected void onSuccess() {
              pm.pluginUploaded = true;
          }
  
          @Override
          public String toString() {
              return super.toString()+"[plugin="+plugin.title+"]";
          }
  
          /**
           * Called when the download is completed to overwrite
           * the old file with the new file.
           */
          @Override
          protected void replace(File dst, File src) throws IOException {
              if (!site.getId().equals(ID_UPLOAD)) {
                  verifyChecksums(this, plugin, src);
              }
  
              File bak = Util.changeExtension(dst, ".bak");
              bak.delete();
  
              final File legacy = getLegacyDestination();
              if (legacy.exists()) {
                  if (!legacy.renameTo(bak)) {
                      legacy.delete();
                  }
              }
              if (dst.exists()) {
                  if (!dst.renameTo(bak)) {
                      dst.delete();
                  }
              }
  
              if(!src.renameTo(dst)) {
                  throw new IOException("Failed to rename "+src+" to "+dst);
              }
          }
      }
  
      /**
       * Represents the state of the downgrading activity of plugin.
       */
      public final class PluginDowngradeJob extends DownloadJob {
          /**
           * What plugin are we trying to install?
           */
          public final Plugin plugin;
  
          private final PluginManager pm = Jenkins.getInstance().getPluginManager();
  
          public PluginDowngradeJob(Plugin plugin, UpdateSite site, Authentication auth) {
              super(site, auth);
              this.plugin = plugin;
          }
  
          protected URL getURL() throws MalformedURLException {
              return new URL(plugin.url);
          }
  
          protected File getDestination() {
              File baseDir = pm.rootDir;
              final File legacy = new File(baseDir, plugin.name + ".hpi");
              if(legacy.exists()){
                  return legacy;
              }
              return new File(baseDir, plugin.name + ".jpi");
          }
  
          protected File getBackup() {
              File baseDir = pm.rootDir;
              return new File(baseDir, plugin.name + ".bak");
          }
  
          public String getName() {
              return plugin.getDisplayName();
          }
  
          @Override
          public void run() {
              try {
                  LOGGER.info("Starting the downgrade of "+getName()+" on behalf of "+getUser().getName());
  
                  _run();
  
                  LOGGER.info("Downgrade successful: "+getName());
                  status = new Success();
                  onSuccess();
              } catch (Throwable e) {
                  LOGGER.log(Level.SEVERE, "Failed to downgrade "+getName(),e);
                  status = new Failure(e);
                  error = e;
              }
          }
  
          @Override
          protected void _run() throws IOException {
              File dst = getDestination();
              File backup = getBackup();
  
              config.install(this, backup, dst);
          }
  
          /**
           * Called to overwrite
           * current version with backup file
           */
          @Override
          protected void replace(File dst, File backup) throws IOException {
              dst.delete(); // any failure up to here is no big deal
              if(!backup.renameTo(dst)) {
                  throw new IOException("Failed to rename "+backup+" to "+dst);
              }
          }
  
          protected void onSuccess() {
              pm.pluginUploaded = true;
          }
  
          @Override
          public String toString() {
              return super.toString()+"[plugin="+plugin.title+"]";
          }
      }
  
      /**
       * Represents the state of the upgrade activity of Jenkins core.
       */
      public final class HudsonUpgradeJob extends DownloadJob {
          public HudsonUpgradeJob(UpdateSite site, Authentication auth) {
              super(site, auth);
          }
  
          protected URL getURL() throws MalformedURLException {
              return new URL(site.getData().core.url);
          }
  
          protected File getDestination() {
              return Lifecycle.get().getHudsonWar();
          }
  
          public String getName() {
              return "jenkins.war";
          }
  
          protected void onSuccess() {
              status = new Success();
          }
  
          @Override
          protected void replace(File dst, File src) throws IOException {
              verifyChecksums(this, site.getData().core, src);
              Lifecycle.get().rewriteHudsonWar(src);
          }
      }
  
      public final class HudsonDowngradeJob extends DownloadJob {
          public HudsonDowngradeJob(UpdateSite site, Authentication auth) {
              super(site, auth);
          }
  
          protected URL getURL() throws MalformedURLException {
              return new URL(site.getData().core.url);
          }
  
          protected File getDestination() {
              return Lifecycle.get().getHudsonWar();
          }
  
          public String getName() {
              return "jenkins.war";
          }
          protected void onSuccess() {
              status = new Success();
          }
          @Override
          public void run() {
              try {
                  LOGGER.info("Starting the downgrade of "+getName()+" on behalf of "+getUser().getName());
  
                  _run();
  
                  LOGGER.info("Downgrading successful: "+getName());
                  status = new Success();
                  onSuccess();
              } catch (Throwable e) {
                  LOGGER.log(Level.SEVERE, "Failed to downgrade "+getName(),e);
                  status = new Failure(e);
                  error = e;
              }
          }
  
          @Override
          protected void _run() throws IOException {
  
              File backup = new File(Lifecycle.get().getHudsonWar() + ".bak");
              File dst = getDestination();
  
              config.install(this, backup, dst);
          }
  
          @Override
          protected void replace(File dst, File src) throws IOException {
              Lifecycle.get().rewriteHudsonWar(src);
          }
      }
  
      public static final class PluginEntry implements Comparable<PluginEntry> {
          public Plugin plugin;
          public String category;
          private PluginEntry(Plugin p, String c) { plugin = p; category = c; }
  
          public int compareTo(PluginEntry o) {
              int r = category.compareTo(o.category);
              if (r==0) r = plugin.name.compareToIgnoreCase(o.plugin.name);
              if (r==0) r = new VersionNumber(plugin.version).compareTo(new VersionNumber(o.plugin.version));
              return r;
          }
  
          @Override
          public boolean equals(Object o) {
              if (this == o) {
                  return true;
              }
              if (o == null || getClass() != o.getClass()) {
                  return false;
              }
  
              PluginEntry that = (PluginEntry) o;
  
              if (!category.equals(that.category)) {
                  return false;
              }
              if (!plugin.name.equals(that.plugin.name)) {
                  return false;
              }
              return plugin.version.equals(that.plugin.version);
          }
  
          @Override
          public int hashCode() {
              int result = category.hashCode();
              result = 31 * result + plugin.name.hashCode();
              result = 31 * result + plugin.version.hashCode();
              return result;
          }
      }
  
      /**
       * Adds the update center data retriever to HTML.
       */
      @Extension
      public static class PageDecoratorImpl extends PageDecorator {
      }
  
      /**
       * Initializes the update center.
       *
       * This has to wait until after all plugins load, to let custom UpdateCenterConfiguration take effect first.
       */
      @Initializer(after=PLUGINS_STARTED, fatal=false)
      public static void init(Jenkins h) throws IOException {
          h.getUpdateCenter().load();
      }
  
      @Restricted(NoExternalUse.class)
      public static void updateDefaultSite() {
          final UpdateSite site = Jenkins.getInstance().getUpdateCenter().getSite(UpdateCenter.ID_DEFAULT);
          if (site == null) {
              LOGGER.log(Level.SEVERE, "Upgrading Jenkins. Cannot retrieve the default Update Site ''{0}''. "
                      + "Plugin installation may fail.", UpdateCenter.ID_DEFAULT);
              return;
          }
          try {
              // Need to do the following because the plugin manager will attempt to access
              // $JENKINS_HOME/updates/$ID_DEFAULT.json. Needs to be up to date.
              site.updateDirectlyNow(true);
          } catch (Exception e) {
              LOGGER.log(WARNING, "Upgrading Jenkins. Failed to update the default Update Site '" + UpdateCenter.ID_DEFAULT +
                      "'. Plugin upgrades may fail.", e);
          }
      }
  
      @Override
      @Restricted(NoExternalUse.class)
      public Object getTarget() {
          if (!SKIP_PERMISSION_CHECK) {
              Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
          }
          return this;
      }
  
      /**
       * Escape hatch for StaplerProxy-based access control
       */
      @Restricted(NoExternalUse.class)
      public static /* Script Console modifiable */ boolean SKIP_PERMISSION_CHECK = Boolean.getBoolean(UpdateCenter.class.getName() + ".skipPermissionCheck");
  
  
      /**
       * Sequence number generator.
       */
      private static final AtomicInteger iota = new AtomicInteger();
  
      /**
       * @deprecated as of 1.333
       *      Use {@link UpdateSite#neverUpdate}
       */
      @Deprecated
      public static boolean neverUpdate = SystemProperties.getBoolean(UpdateCenter.class.getName()+".never");
  
      public static final XStream2 XSTREAM = new XStream2();
  
      static {
          XSTREAM.alias("site",UpdateSite.class);
          XSTREAM.alias("sites",PersistedList.class);
      }
  }