docs: public List<Future<UpdateCenter.UpdateCenterJob>> prevalidateConfig(InputStream configXml) throws IOException {
          Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
          List<Future<UpdateCenter.UpdateCenterJob>> jobs = new ArrayList<>();
          UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
          // TODO call uc.updateAllSites() when available? perhaps not, since we should not block on network here
          for (Map.Entry<String,VersionNumber> requestedPlugin : parseRequestedPlugins(configXml).entrySet()) {
              PluginWrapper pw = getPlugin(requestedPlugin.getKey());
              if (pw == null) { // install new
                  UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey(), requestedPlugin.getValue());
                  if (toInstall == null) {
                      LOGGER.log(WARNING, "No such plugin {0} to install", requestedPlugin.getKey());
                      continue;
                  }
                  if (new VersionNumber(toInstall.version).compareTo(requestedPlugin.getValue()) < 0) {
                      LOGGER.log(WARNING, "{0} can only be satisfied in @{1}", new Object[] {requestedPlugin, toInstall.version});
                  }
                  if (toInstall.isForNewerHudson()) {
                      LOGGER.log(WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version});
                  }
                  if (toInstall.isForNewerJava()) {
                      LOGGER.log(WARNING, "{0}@{1} was built for a newer Java", new Object[] {toInstall.name, toInstall.version});
                  }
                  jobs.add(toInstall.deploy(true));
              } else if (pw.isOlderThan(requestedPlugin.getValue())) { // upgrade
                  UpdateSite.Plugin toInstall = uc.getPlugin(requestedPlugin.getKey(), requestedPlugin.getValue());
                  if (toInstall == null) {
                      LOGGER.log(WARNING, "No such plugin {0} to upgrade", requestedPlugin.getKey());
                      continue;
                  }
                  if (!pw.isOlderThan(new VersionNumber(toInstall.version))) {
                      LOGGER.log(WARNING, "{0}@{1} is no newer than what we already have", new Object[] {toInstall.name, toInstall.version});
                      continue;
                  }
                  if (new VersionNumber(toInstall.version).compareTo(requestedPlugin.getValue()) < 0) {
                      LOGGER.log(WARNING, "{0} can only be satisfied in @{1}", new Object[] {requestedPlugin, toInstall.version});
                  }
                  if (toInstall.isForNewerHudson()) {
                      LOGGER.log(WARNING, "{0}@{1} was built for a newer Jenkins", new Object[] {toInstall.name, toInstall.version});
                  }
                  if (toInstall.isForNewerJava()) {
                      LOGGER.log(WARNING, "{0}@{1} was built for a newer Java", new Object[] {toInstall.name, toInstall.version});
                  }
                  if (!toInstall.isCompatibleWithInstalledVersion()) {
                      LOGGER.log(WARNING, "{0}@{1} is incompatible with the installed @{2}", new Object[] {toInstall.name, toInstall.version, pw.getVersion()});
                  }
                  jobs.add(toInstall.deploy(true)); // dynamicLoad=true => sure to throw RestartRequiredException, but at least message is nicer
              } // else already good
          }
          return jobs;
      }