Example #1
0
/**
 * Encapsulates the rendering of the list of {@link TopLevelItem}s that {@link Jenkins} owns.
 *
 * <p>This is an extension point in Hudson, allowing different kind of rendering to be added as
 * plugins.
 *
 * <h2>Note for implementors</h2>
 *
 * <ul>
 *   <li>{@link View} subtypes need the <tt>newViewDetail.jelly</tt> page, which is included in the
 *       "new view" page. This page should have some description of what the view is about.
 * </ul>
 *
 * @author Kohsuke Kawaguchi
 * @see ViewDescriptor
 * @see ViewGroup
 */
@ExportedBean
public abstract class View extends AbstractModelObject
    implements AccessControlled, Describable<View>, ExtensionPoint, Saveable {

  /** Container of this view. Set right after the construction and never change thereafter. */
  protected /*final*/ ViewGroup owner;

  /** Name of this view. */
  protected String name;

  /** Message displayed in the view page. */
  protected String description;

  /** System Message displayed. */
  protected String systemmessage;

  /** If true, only show relevant executors */
  protected boolean filterExecutors;

  /** If true, only show relevant queue items */
  protected boolean filterQueue;

  protected transient List<Action> transientActions;

  /**
   * List of {@link ViewProperty}s configured for this view.
   *
   * @since 1.406
   */
  private volatile DescribableList<ViewProperty, ViewPropertyDescriptor> properties =
      new PropertyList(this);

  protected View(String name) {
    this.name = name;
  }

  protected View(String name, ViewGroup owner) {
    this.name = name;
    this.owner = owner;
  }

  /** Gets all the items in this collection in a read-only view. */
  @Exported(name = "jobs")
  public abstract Collection<TopLevelItem> getItems();

  /** Gets the {@link TopLevelItem} of the given name. */
  public TopLevelItem getItem(String name) {
    return getOwnerItemGroup().getItem(name);
  }

  /** Alias for {@link #getItem(String)}. This is the one used in the URL binding. */
  public final TopLevelItem getJob(String name) {
    return getItem(name);
  }

  /** Checks if the job is in this collection. */
  public abstract boolean contains(TopLevelItem item);

  /**
   * Gets the name of all this collection.
   *
   * @see #rename(String)
   */
  @Exported(visibility = 2, name = "name")
  public String getViewName() {
    return name;
  }

  /** Renames this view. */
  public void rename(String newName) throws Failure, FormException {
    if (name.equals(newName)) return; // noop
    checkGoodName(newName);
    if (owner.getView(newName) != null)
      throw new FormException(Messages.Hudson_ViewAlreadyExists(newName), "name");
    String oldName = name;
    name = newName;
    owner.onViewRenamed(this, oldName, newName);
  }

  /** Gets the {@link ViewGroup} that this view belongs to. */
  public ViewGroup getOwner() {
    return owner;
  }

  /** Backward-compatible way of getting {@code getOwner().getItemGroup()} */
  public ItemGroup<? extends TopLevelItem> getOwnerItemGroup() {
    try {
      return _getOwnerItemGroup();
    } catch (AbstractMethodError e) {
      return Hudson.getInstance();
    }
  }

  /**
   * A pointless function to work around what appears to be a HotSpot problem. See JENKINS-5756 and
   * bug 6933067 on BugParade for more details.
   */
  private ItemGroup<? extends TopLevelItem> _getOwnerItemGroup() {
    return owner.getItemGroup();
  }

  public View getOwnerPrimaryView() {
    try {
      return _getOwnerPrimaryView();
    } catch (AbstractMethodError e) {
      return null;
    }
  }

  private View _getOwnerPrimaryView() {
    return owner.getPrimaryView();
  }

  public List<Action> getOwnerViewActions() {
    try {
      return _getOwnerViewActions();
    } catch (AbstractMethodError e) {
      return Hudson.getInstance().getActions();
    }
  }

  private List<Action> _getOwnerViewActions() {
    return owner.getViewActions();
  }

  /** Message displayed in the top page. Can be null. Includes HTML. */
  @Exported
  public String getDescription() {
    return description;
  }

  /** Gets the system message to be displayed at the top of the page on all views */
  @Exported
  public String getSystemMessage() {
    if (owner.getSystemMessage() != null && !owner.getSystemMessage().isEmpty()) {
      this.systemmessage = owner.getSystemMessage();
    }
    return systemmessage;
  }

  /**
   * Gets the view properties configured for this view.
   *
   * @since 1.406
   */
  public DescribableList<ViewProperty, ViewPropertyDescriptor> getProperties() {
    // readResolve was the best place to do this, but for compatibility reasons,
    // this class can no longer have readResolve() (the mechanism itself isn't suitable for class
    // hierarchy)
    // see JENKINS-9431
    //
    // until we have that, putting this logic here.
    synchronized (this) {
      if (properties == null) {
        properties = new PropertyList(this);
      } else {
        properties.setOwner(this);
      }
    }

    return properties;
  }

  /**
   * Returns all the {@link LabelAtomPropertyDescriptor}s that can be potentially configured on this
   * label.
   */
  public List<ViewPropertyDescriptor> getApplicablePropertyDescriptors() {
    List<ViewPropertyDescriptor> r = new ArrayList<ViewPropertyDescriptor>();
    for (ViewPropertyDescriptor pd : ViewProperty.all()) {
      if (pd.isEnabledFor(this)) r.add(pd);
    }
    return r;
  }

  public void save() throws IOException {
    // persistence is a part of the owner
    // due to initialization timing issue, it can be null when this method is called
    if (owner != null) {
      owner.save();
    }
  }

  /**
   * List of all {@link ViewProperty}s exposed primarily for the remoting API.
   *
   * @since 1.406
   */
  @Exported(name = "property", inline = true)
  public List<ViewProperty> getAllProperties() {
    return getProperties().toList();
  }

  public ViewDescriptor getDescriptor() {
    return (ViewDescriptor) Jenkins.getInstance().getDescriptorOrDie(getClass());
  }

  public String getDisplayName() {
    return getViewName();
  }

  public String getNewPronoun() {
    return AlternativeUiTextProvider.get(NEW_PRONOUN, this, Messages.AbstractItem_Pronoun());
  }

  /**
   * By default, return true to render the "Edit view" link on the page. This method is really just
   * for the default "All" view to hide the edit link so that the default Hudson top page remains
   * the same as before 1.316.
   *
   * @since 1.316
   */
  public boolean isEditable() {
    return true;
  }

  /** If true, only show relevant executors */
  public boolean isFilterExecutors() {
    return filterExecutors;
  }

  /** If true, only show relevant queue items */
  public boolean isFilterQueue() {
    return filterQueue;
  }

  /**
   * Gets the {@link Widget}s registered on this object.
   *
   * <p>For now, this just returns the widgets registered to Hudson.
   */
  public List<Widget> getWidgets() {
    return Collections.unmodifiableList(Jenkins.getInstance().getWidgets());
  }

  /**
   * If this view uses &lt;t:projectView> for rendering, this method returns columns to be
   * displayed.
   */
  public Iterable<? extends ListViewColumn> getColumns() {
    return ListViewColumn.createDefaultInitialColumnList();
  }

  /**
   * If this view uses &lt;t:projectView> for rendering, this method returns the indenter used to
   * indent each row.
   */
  public Indenter getIndenter() {
    return null;
  }

  /** If true, this is a view that renders the top page of Hudson. */
  public boolean isDefault() {
    return getOwnerPrimaryView() == this;
  }

  public List<Computer> getComputers() {
    Computer[] computers = Jenkins.getInstance().getComputers();

    if (!isFilterExecutors()) {
      return Arrays.asList(computers);
    }

    List<Computer> result = new ArrayList<Computer>();

    boolean roam = false;
    HashSet<Label> labels = new HashSet<Label>();
    for (Item item : getItems()) {
      if (item instanceof AbstractProject<?, ?>) {
        AbstractProject<?, ?> p = (AbstractProject<?, ?>) item;
        Label l = p.getAssignedLabel();
        if (l != null) {
          labels.add(l);
        } else {
          roam = true;
        }
      }
    }

    for (Computer c : computers) {
      Node n = c.getNode();
      if (n != null) {
        if (roam && n.getMode() == Mode.NORMAL
            || !Collections.disjoint(n.getAssignedLabels(), labels)) {
          result.add(c);
        }
      }
    }

    return result;
  }

  public List<Queue.Item> getQueueItems() {
    if (!isFilterQueue()) {
      return Arrays.asList(Jenkins.getInstance().getQueue().getItems());
    }

    Collection<TopLevelItem> items = getItems();
    List<Queue.Item> result = new ArrayList<Queue.Item>();
    for (Queue.Item qi : Jenkins.getInstance().getQueue().getItems()) {
      if (items.contains(qi.task)) {
        result.add(qi);
      }
    }
    return result;
  }

  /**
   * Returns the path relative to the context root.
   *
   * <p>Doesn't start with '/' but ends with '/' (except returns empty string when this is the
   * default view).
   */
  public String getUrl() {
    return isDefault() ? (owner != null ? owner.getUrl() : "") : getViewUrl();
  }

  /** Same as {@link #getUrl()} except this returns a view/{name} path even for the default view. */
  public String getViewUrl() {
    return (owner != null ? owner.getUrl() : "") + "view/" + Util.rawEncode(getViewName()) + '/';
  }

  public String getSearchUrl() {
    return getUrl();
  }

  /**
   * Returns the transient {@link Action}s associated with the top page.
   *
   * <p>If views don't want to show top-level actions, this method can be overridden to return
   * different objects.
   *
   * @see Jenkins#getActions()
   */
  public List<Action> getActions() {
    List<Action> result = new ArrayList<Action>();
    result.addAll(getOwnerViewActions());
    synchronized (this) {
      if (transientActions == null) {
        transientActions = TransientViewActionFactory.createAllFor(this);
      }
      result.addAll(transientActions);
    }
    return result;
  }

  public Object getDynamic(String token) {
    for (Action a : getActions()) {
      String url = a.getUrlName();
      if (url == null) continue;
      if (a.getUrlName().equals(token)) return a;
    }
    return null;
  }

  /** Gets the absolute URL of this view. */
  @Exported(visibility = 2, name = "url")
  public String getAbsoluteUrl() {
    return Jenkins.getInstance().getRootUrl() + getUrl();
  }

  public Api getApi() {
    return new Api(this);
  }

  /**
   * Returns the page to redirect the user to, after the view is created.
   *
   * <p>The returned string is appended to "/view/foobar/", so for example to direct the user to the
   * top page of the view, return "", etc.
   */
  public String getPostConstructLandingPage() {
    return "configure";
  }

  /** Returns the {@link ACL} for this object. */
  public ACL getACL() {
    return Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
  }

  public void checkPermission(Permission p) {
    getACL().checkPermission(p);
  }

  public boolean hasPermission(Permission p) {
    return getACL().hasPermission(p);
  }

  /**
   * Called when a job name is changed or deleted.
   *
   * <p>If this view contains this job, it should update the view membership so that the renamed job
   * will remain in the view, and the deleted job is removed.
   *
   * @param item The item whose name is being changed.
   * @param oldName Old name of the item. Always non-null.
   * @param newName New name of the item, if the item is renamed. Or null, if the item is removed.
   */
  public abstract void onJobRenamed(Item item, String oldName, String newName);

  @ExportedBean(defaultVisibility = 2)
  public static final class UserInfo implements Comparable<UserInfo> {
    private final User user;
    /** When did this user made a last commit on any of our projects? Can be null. */
    private Calendar lastChange;
    /** Which project did this user commit? Can be null. */
    private AbstractProject project;

    UserInfo(User user, AbstractProject p, Calendar lastChange) {
      this.user = user;
      this.project = p;
      this.lastChange = lastChange;
    }

    @Exported
    public User getUser() {
      return user;
    }

    @Exported
    public Calendar getLastChange() {
      return lastChange;
    }

    @Exported
    public AbstractProject getProject() {
      return project;
    }

    /** Returns a human-readable string representation of when this user was last active. */
    public String getLastChangeTimeString() {
      if (lastChange == null) return "N/A";
      long duration = new GregorianCalendar().getTimeInMillis() - ordinal();
      return Util.getTimeSpanString(duration);
    }

    public String getTimeSortKey() {
      if (lastChange == null) return "-";
      return Util.XS_DATETIME_FORMATTER.format(lastChange.getTime());
    }

    public int compareTo(UserInfo that) {
      long rhs = that.ordinal();
      long lhs = this.ordinal();
      if (rhs > lhs) return 1;
      if (rhs < lhs) return -1;
      return 0;
    }

    private long ordinal() {
      if (lastChange == null) return 0;
      return lastChange.getTimeInMillis();
    }
  }

  /** Does this {@link View} has any associated user information recorded? */
  public final boolean hasPeople() {
    return People.isApplicable(getItems());
  }

  /** Gets the users that show up in the changelog of this job collection. */
  public final People getPeople() {
    return new People(this);
  }

  @ExportedBean
  public static final class People {
    @Exported public final List<UserInfo> users;

    public final Object parent;

    public People(Jenkins parent) {
      this.parent = parent;
      // for Hudson, really load all users
      Map<User, UserInfo> users = getUserInfo(parent.getItems());
      User unknown = User.getUnknown();
      for (User u : User.getAll()) {
        if (u == unknown) continue; // skip the special 'unknown' user
        if (!users.containsKey(u)) users.put(u, new UserInfo(u, null, null));
      }
      this.users = toList(users);
    }

    public People(View parent) {
      this.parent = parent;
      this.users = toList(getUserInfo(parent.getItems()));
    }

    private Map<User, UserInfo> getUserInfo(Collection<? extends Item> items) {
      Map<User, UserInfo> users = new HashMap<User, UserInfo>();
      for (Item item : items) {
        for (Job job : item.getAllJobs()) {
          if (job instanceof AbstractProject) {
            AbstractProject<?, ?> p = (AbstractProject) job;
            for (AbstractBuild<?, ?> build : p.getBuilds()) {
              for (Entry entry : build.getChangeSet()) {
                User user = entry.getAuthor();

                UserInfo info = users.get(user);
                if (info == null) users.put(user, new UserInfo(user, p, build.getTimestamp()));
                else if (info.getLastChange().before(build.getTimestamp())) {
                  info.project = p;
                  info.lastChange = build.getTimestamp();
                }
              }
            }
          }
        }
      }
      return users;
    }

    private List<UserInfo> toList(Map<User, UserInfo> users) {
      ArrayList<UserInfo> list = new ArrayList<UserInfo>();
      list.addAll(users.values());
      Collections.sort(list);
      return Collections.unmodifiableList(list);
    }

    public Api getApi() {
      return new Api(this);
    }

    public static boolean isApplicable(Collection<? extends Item> items) {
      for (Item item : items) {
        for (Job job : item.getAllJobs()) {
          if (job instanceof AbstractProject) {
            AbstractProject<?, ?> p = (AbstractProject) job;
            for (AbstractBuild<?, ?> build : p.getBuilds()) {
              for (Entry entry : build.getChangeSet()) {
                User user = entry.getAuthor();
                if (user != null) return true;
              }
            }
          }
        }
      }
      return false;
    }
  }

  void addDisplayNamesToSearchIndex(SearchIndexBuilder sib, Collection<TopLevelItem> items) {
    for (TopLevelItem item : items) {

      if (LOGGER.isLoggable(Level.FINE)) {
        LOGGER.fine(
            (String.format(
                "Adding url=%s,displayName=%s", item.getSearchUrl(), item.getDisplayName())));
      }
      sib.add(item.getSearchUrl(), item.getDisplayName());
    }
  }

  @Override
  public SearchIndexBuilder makeSearchIndex() {
    SearchIndexBuilder sib = super.makeSearchIndex();
    sib.add(
        new CollectionSearchIndex<TopLevelItem>() { // for jobs in the view
          protected TopLevelItem get(String key) {
            return getItem(key);
          }

          protected Collection<TopLevelItem> all() {
            return getItems();
          }

          @Override
          protected String getName(TopLevelItem o) {
            // return the name instead of the display for suggestion searching
            return o.getName();
          }
        });

    // add the display name for each item in the search index
    addDisplayNamesToSearchIndex(sib, getItems());

    return sib;
  }

  /** Accepts the new description. */
  public synchronized void doSubmitDescription(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    checkPermission(CONFIGURE);

    description = req.getParameter("description");
    save();
    rsp.sendRedirect("."); // go to the top page
  }

  /**
   * Accepts submission from the configuration page.
   *
   * <p>Subtypes should override the {@link #submit(StaplerRequest)} method.
   */
  public final synchronized void doConfigSubmit(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException, FormException {
    checkPermission(CONFIGURE);
    requirePOST();

    submit(req);

    description = Util.nullify(req.getParameter("description"));
    filterExecutors = req.getParameter("filterExecutors") != null;
    filterQueue = req.getParameter("filterQueue") != null;

    rename(req.getParameter("name"));

    getProperties().rebuild(req, req.getSubmittedForm(), getApplicablePropertyDescriptors());

    save();

    rsp.sendRedirect2("../" + name);
  }

  /**
   * Handles the configuration submission.
   *
   * <p>Load view-specific properties here.
   */
  protected abstract void submit(StaplerRequest req)
      throws IOException, ServletException, FormException;

  /** Deletes this view. */
  public synchronized void doDoDelete(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    requirePOST();
    checkPermission(DELETE);

    owner.deleteView(this);

    rsp.sendRedirect2(req.getContextPath() + "/" + owner.getUrl());
  }

  /**
   * Creates a new {@link Item} in this collection.
   *
   * <p>This method should call {@link ModifiableItemGroup#doCreateItem(StaplerRequest,
   * StaplerResponse)} and then add the newly created item to this view.
   *
   * @return null if fails.
   */
  public abstract Item doCreateItem(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException;

  public void doRssAll(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    rss(req, rsp, " all builds", getBuilds());
  }

  public void doRssFailed(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    rss(req, rsp, " failed builds", getBuilds().failureOnly());
  }

  public RunList getBuilds() {
    return new RunList(this);
  }

  public BuildTimelineWidget getTimeline() {
    return new BuildTimelineWidget(getBuilds());
  }

  private void rss(StaplerRequest req, StaplerResponse rsp, String suffix, RunList runs)
      throws IOException, ServletException {
    RSS.forwardToRss(
        getDisplayName() + suffix, getUrl(), runs.newBuilds(), Run.FEED_ADAPTER, req, rsp);
  }

  public void doRssLatest(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    List<Run> lastBuilds = new ArrayList<Run>();
    for (TopLevelItem item : getItems()) {
      if (item instanceof Job) {
        Job job = (Job) item;
        Run lb = job.getLastBuild();
        if (lb != null) lastBuilds.add(lb);
      }
    }
    RSS.forwardToRss(
        getDisplayName() + " last builds only",
        getUrl(),
        lastBuilds,
        Run.FEED_ADAPTER_LATEST,
        req,
        rsp);
  }

  /**
   * A list of available view types.
   *
   * @deprecated as of 1.286 Use {@link #all()} for read access, and use {@link Extension} for
   *     registration.
   */
  public static final DescriptorList<View> LIST = new DescriptorList<View>(View.class);

  /** Returns all the registered {@link ViewDescriptor}s. */
  public static DescriptorExtensionList<View, ViewDescriptor> all() {
    return Jenkins.getInstance().<View, ViewDescriptor>getDescriptorList(View.class);
  }

  public static List<ViewDescriptor> allInstantiable() {
    List<ViewDescriptor> r = new ArrayList<ViewDescriptor>();
    for (ViewDescriptor d : all()) if (d.isInstantiable()) r.add(d);
    return r;
  }

  public static final Comparator<View> SORTER =
      new Comparator<View>() {
        public int compare(View lhs, View rhs) {
          return lhs.getViewName().compareTo(rhs.getViewName());
        }
      };

  public static final PermissionGroup PERMISSIONS =
      new PermissionGroup(View.class, Messages._View_Permissions_Title());
  /** Permission to create new views. */
  public static final Permission CREATE =
      new Permission(
          PERMISSIONS,
          "Create",
          Messages._View_CreatePermission_Description(),
          Permission.CREATE,
          PermissionScope.ITEM_GROUP);

  public static final Permission DELETE =
      new Permission(
          PERMISSIONS,
          "Delete",
          Messages._View_DeletePermission_Description(),
          Permission.DELETE,
          PermissionScope.ITEM_GROUP);
  public static final Permission CONFIGURE =
      new Permission(
          PERMISSIONS,
          "Configure",
          Messages._View_ConfigurePermission_Description(),
          Permission.CONFIGURE,
          PermissionScope.ITEM_GROUP);

  // to simplify access from Jelly
  public static Permission getItemCreatePermission() {
    return Item.CREATE;
  }

  public static View create(StaplerRequest req, StaplerResponse rsp, ViewGroup owner)
      throws FormException, IOException, ServletException {
    String name = req.getParameter("name");
    checkGoodName(name);
    if (owner.getView(name) != null)
      throw new FormException(Messages.Hudson_ViewAlreadyExists(name), "name");

    String mode = req.getParameter("mode");
    if (mode == null || mode.length() == 0)
      throw new FormException(Messages.View_MissingMode(), "mode");

    // create a view
    View v = all().findByName(mode).newInstance(req, req.getSubmittedForm());
    v.owner = owner;

    // redirect to the config screen
    rsp.sendRedirect2(req.getContextPath() + '/' + v.getUrl() + v.getPostConstructLandingPage());

    return v;
  }

  public static class PropertyList extends DescribableList<ViewProperty, ViewPropertyDescriptor> {
    private PropertyList(View owner) {
      super(owner);
    }

    public PropertyList() { // needed for XStream deserialization
    }

    public View getOwner() {
      return (View) owner;
    }

    @Override
    protected void onModified() throws IOException {
      for (ViewProperty p : this) p.setView(getOwner());
    }
  }

  /**
   * "Job" in "New Job". When a view is used in a context that restricts the child type, It might be
   * useful to override this.
   */
  public static final Message<View> NEW_PRONOUN = new Message<View>();

  private static final Logger LOGGER = Logger.getLogger(View.class.getName());
}
Example #2
0
/**
 * Encapsulates the rendering of the list of {@link TopLevelItem}s that {@link Jenkins} owns.
 *
 * <p>This is an extension point in Hudson, allowing different kind of rendering to be added as
 * plugins.
 *
 * <h2>Note for implementers</h2>
 *
 * <ul>
 *   <li>{@link View} subtypes need the <tt>newViewDetail.jelly</tt> page, which is included in the
 *       "new view" page. This page should have some description of what the view is about.
 * </ul>
 *
 * @author Kohsuke Kawaguchi
 * @see ViewDescriptor
 * @see ViewGroup
 */
@ExportedBean
public abstract class View extends AbstractModelObject
    implements AccessControlled,
        Describable<View>,
        ExtensionPoint,
        Saveable,
        ModelObjectWithChildren {

  /** Container of this view. Set right after the construction and never change thereafter. */
  protected /*final*/ ViewGroup owner;

  /** Name of this view. */
  protected String name;

  /** Message displayed in the view page. */
  protected String description;

  /** If true, only show relevant executors */
  protected boolean filterExecutors;

  /** If true, only show relevant queue items */
  protected boolean filterQueue;

  protected transient List<Action> transientActions;

  /**
   * List of {@link ViewProperty}s configured for this view.
   *
   * @since 1.406
   */
  private volatile DescribableList<ViewProperty, ViewPropertyDescriptor> properties =
      new PropertyList(this);

  protected View(String name) {
    this.name = name;
  }

  protected View(String name, ViewGroup owner) {
    this.name = name;
    this.owner = owner;
  }

  /** Gets all the items in this collection in a read-only view. */
  @Exported(name = "jobs")
  public abstract Collection<TopLevelItem> getItems();

  /**
   * Gets all the items recursively contained in this collection in a read-only view.
   *
   * <p>The default implementation recursively adds the items of all contained Views in case this
   * view implements {@link ViewGroup}, which should be enough for most cases.
   *
   * @since 1.520
   */
  public Collection<TopLevelItem> getAllItems() {

    if (this instanceof ViewGroup) {
      final Collection<TopLevelItem> items = new LinkedHashSet<TopLevelItem>(getItems());

      for (View view : ((ViewGroup) this).getViews()) {
        items.addAll(view.getAllItems());
      }
      return Collections.unmodifiableCollection(items);
    } else {
      return getItems();
    }
  }

  /** Gets the {@link TopLevelItem} of the given name. */
  public TopLevelItem getItem(String name) {
    return getOwnerItemGroup().getItem(name);
  }

  /** Alias for {@link #getItem(String)}. This is the one used in the URL binding. */
  public final TopLevelItem getJob(String name) {
    return getItem(name);
  }

  /** Checks if the job is in this collection. */
  public abstract boolean contains(TopLevelItem item);

  /**
   * Gets the name of all this collection.
   *
   * @see #rename(String)
   */
  @Exported(visibility = 2, name = "name")
  public String getViewName() {
    return name;
  }

  /** Renames this view. */
  public void rename(String newName) throws Failure, FormException {
    if (name.equals(newName)) return; // noop
    checkGoodName(newName);
    if (owner.getView(newName) != null)
      throw new FormException(Messages.Hudson_ViewAlreadyExists(newName), "name");
    String oldName = name;
    name = newName;
    owner.onViewRenamed(this, oldName, newName);
  }

  /** Gets the {@link ViewGroup} that this view belongs to. */
  public ViewGroup getOwner() {
    return owner;
  }

  /** Backward-compatible way of getting {@code getOwner().getItemGroup()} */
  public ItemGroup<? extends TopLevelItem> getOwnerItemGroup() {
    try {
      return owner.getItemGroup();
    } catch (AbstractMethodError e) {
      return Jenkins.getInstance();
    }
  }

  public View getOwnerPrimaryView() {
    try {
      return _getOwnerPrimaryView();
    } catch (AbstractMethodError e) {
      return null;
    }
  }

  private View _getOwnerPrimaryView() {
    return owner.getPrimaryView();
  }

  public List<Action> getOwnerViewActions() {
    try {
      return _getOwnerViewActions();
    } catch (AbstractMethodError e) {
      return Jenkins.getInstance().getActions();
    }
  }

  private List<Action> _getOwnerViewActions() {
    return owner.getViewActions();
  }

  /** Message displayed in the top page. Can be null. Includes HTML. */
  @Exported
  public String getDescription() {
    return description;
  }

  /**
   * Gets the view properties configured for this view.
   *
   * @since 1.406
   */
  public DescribableList<ViewProperty, ViewPropertyDescriptor> getProperties() {
    // readResolve was the best place to do this, but for compatibility reasons,
    // this class can no longer have readResolve() (the mechanism itself isn't suitable for class
    // hierarchy)
    // see JENKINS-9431
    //
    // until we have that, putting this logic here.
    synchronized (PropertyList.class) {
      if (properties == null) {
        properties = new PropertyList(this);
      } else {
        properties.setOwner(this);
      }
      return properties;
    }
  }

  /**
   * Returns all the {@link LabelAtomPropertyDescriptor}s that can be potentially configured on this
   * label.
   */
  public List<ViewPropertyDescriptor> getApplicablePropertyDescriptors() {
    List<ViewPropertyDescriptor> r = new ArrayList<ViewPropertyDescriptor>();
    for (ViewPropertyDescriptor pd : ViewProperty.all()) {
      if (pd.isEnabledFor(this)) r.add(pd);
    }
    return r;
  }

  public void save() throws IOException {
    // persistence is a part of the owner
    // due to initialization timing issue, it can be null when this method is called
    if (owner != null) {
      owner.save();
    }
  }

  /**
   * List of all {@link ViewProperty}s exposed primarily for the remoting API.
   *
   * @since 1.406
   */
  @Exported(name = "property", inline = true)
  public List<ViewProperty> getAllProperties() {
    return getProperties().toList();
  }

  public ViewDescriptor getDescriptor() {
    return (ViewDescriptor) Jenkins.getInstance().getDescriptorOrDie(getClass());
  }

  public String getDisplayName() {
    return getViewName();
  }

  public String getNewPronoun() {
    return AlternativeUiTextProvider.get(NEW_PRONOUN, this, Messages.AbstractItem_Pronoun());
  }

  /**
   * By default, return true to render the "Edit view" link on the page. This method is really just
   * for the default "All" view to hide the edit link so that the default Hudson top page remains
   * the same as before 1.316.
   *
   * @since 1.316
   */
  public boolean isEditable() {
    return true;
  }

  /**
   * Enables or disables automatic refreshes of the view. By default, automatic refreshes are
   * enabled.
   *
   * @since 1.557
   */
  public boolean isAutomaticRefreshEnabled() {
    return true;
  }

  /** If true, only show relevant executors */
  public boolean isFilterExecutors() {
    return filterExecutors;
  }

  /** If true, only show relevant queue items */
  public boolean isFilterQueue() {
    return filterQueue;
  }

  /**
   * Gets the {@link Widget}s registered on this object.
   *
   * <p>For now, this just returns the widgets registered to Hudson.
   */
  public List<Widget> getWidgets() {
    return Collections.unmodifiableList(Jenkins.getInstance().getWidgets());
  }

  /**
   * If this view uses &lt;t:projectView> for rendering, this method returns columns to be
   * displayed.
   */
  public Iterable<? extends ListViewColumn> getColumns() {
    return ListViewColumn.createDefaultInitialColumnList();
  }

  /**
   * If this view uses &lt;t:projectView> for rendering, this method returns the indenter used to
   * indent each row.
   */
  public Indenter getIndenter() {
    return null;
  }

  /** If true, this is a view that renders the top page of Hudson. */
  public boolean isDefault() {
    return getOwnerPrimaryView() == this;
  }

  public List<Computer> getComputers() {
    Computer[] computers = Jenkins.getInstance().getComputers();

    if (!isFilterExecutors()) {
      return Arrays.asList(computers);
    }

    List<Computer> result = new ArrayList<Computer>();

    HashSet<Label> labels = new HashSet<Label>();
    for (Item item : getItems()) {
      if (item instanceof AbstractProject<?, ?>) {
        labels.addAll(((AbstractProject<?, ?>) item).getRelevantLabels());
      }
    }

    for (Computer c : computers) {
      if (isRelevant(labels, c)) result.add(c);
    }

    return result;
  }

  private boolean isRelevant(Collection<Label> labels, Computer computer) {
    Node node = computer.getNode();
    if (node == null) return false;
    if (labels.contains(null) && node.getMode() == Mode.NORMAL) return true;

    for (Label l : labels) if (l != null && l.contains(node)) return true;
    return false;
  }

  private List<Queue.Item> filterQueue(List<Queue.Item> base) {
    if (!isFilterQueue()) {
      return base;
    }

    Collection<TopLevelItem> items = getItems();
    List<Queue.Item> result = new ArrayList<Queue.Item>();
    for (Queue.Item qi : base) {
      if (items.contains(qi.task)) {
        result.add(qi);
      } else if (qi.task instanceof AbstractProject<?, ?>) {
        AbstractProject<?, ?> project = (AbstractProject<?, ?>) qi.task;
        if (items.contains(project.getRootProject())) {
          result.add(qi);
        }
      }
    }
    return result;
  }

  public List<Queue.Item> getQueueItems() {
    return filterQueue(Arrays.asList(Jenkins.getInstance().getQueue().getItems()));
  }

  public List<Queue.Item> getApproximateQueueItemsQuickly() {
    return filterQueue(Jenkins.getInstance().getQueue().getApproximateItemsQuickly());
  }

  /**
   * Returns the path relative to the context root.
   *
   * <p>Doesn't start with '/' but ends with '/' (except returns empty string when this is the
   * default view).
   */
  public String getUrl() {
    return isDefault() ? (owner != null ? owner.getUrl() : "") : getViewUrl();
  }

  /** Same as {@link #getUrl()} except this returns a view/{name} path even for the default view. */
  public String getViewUrl() {
    return (owner != null ? owner.getUrl() : "") + "view/" + Util.rawEncode(getViewName()) + '/';
  }

  @Override
  public String toString() {
    return super.toString() + "[" + getViewUrl() + "]";
  }

  public String getSearchUrl() {
    return getUrl();
  }

  /**
   * Returns the transient {@link Action}s associated with the top page.
   *
   * <p>If views don't want to show top-level actions, this method can be overridden to return
   * different objects.
   *
   * @see Jenkins#getActions()
   */
  public List<Action> getActions() {
    List<Action> result = new ArrayList<Action>();
    result.addAll(getOwnerViewActions());
    synchronized (this) {
      if (transientActions == null) {
        updateTransientActions();
      }
      result.addAll(transientActions);
    }
    return result;
  }

  public synchronized void updateTransientActions() {
    transientActions = TransientViewActionFactory.createAllFor(this);
  }

  public Object getDynamic(String token) {
    for (Action a : getActions()) {
      String url = a.getUrlName();
      if (url == null) continue;
      if (a.getUrlName().equals(token)) return a;
    }
    return null;
  }

  /** Gets the absolute URL of this view. */
  @Exported(visibility = 2, name = "url")
  public String getAbsoluteUrl() {
    return Jenkins.getInstance().getRootUrl() + getUrl();
  }

  public Api getApi() {
    return new Api(this);
  }

  /**
   * Returns the page to redirect the user to, after the view is created.
   *
   * <p>The returned string is appended to "/view/foobar/", so for example to direct the user to the
   * top page of the view, return "", etc.
   */
  public String getPostConstructLandingPage() {
    return "configure";
  }

  /** Returns the {@link ACL} for this object. */
  public ACL getACL() {
    return Jenkins.getInstance().getAuthorizationStrategy().getACL(this);
  }

  public void checkPermission(Permission p) {
    getACL().checkPermission(p);
  }

  public boolean hasPermission(Permission p) {
    return getACL().hasPermission(p);
  }

  /**
   * @deprecated Does not work properly with moved jobs. Use {@link ItemListener#onLocationChanged}
   *     instead.
   */
  @Deprecated
  public void onJobRenamed(Item item, String oldName, String newName) {}

  @ExportedBean(defaultVisibility = 2)
  public static final class UserInfo implements Comparable<UserInfo> {
    private final User user;
    /** When did this user made a last commit on any of our projects? Can be null. */
    private Calendar lastChange;
    /** Which project did this user commit? Can be null. */
    private AbstractProject project;

    /** @see UserAvatarResolver */
    String avatar;

    UserInfo(User user, AbstractProject p, Calendar lastChange) {
      this.user = user;
      this.project = p;
      this.lastChange = lastChange;
    }

    @Exported
    public User getUser() {
      return user;
    }

    @Exported
    public Calendar getLastChange() {
      return lastChange;
    }

    @Exported
    public AbstractProject getProject() {
      return project;
    }

    /** Returns a human-readable string representation of when this user was last active. */
    public String getLastChangeTimeString() {
      if (lastChange == null) return "N/A";
      long duration = new GregorianCalendar().getTimeInMillis() - ordinal();
      return Util.getTimeSpanString(duration);
    }

    public String getTimeSortKey() {
      if (lastChange == null) return "-";
      return Util.XS_DATETIME_FORMATTER.format(lastChange.getTime());
    }

    public int compareTo(UserInfo that) {
      long rhs = that.ordinal();
      long lhs = this.ordinal();
      if (rhs > lhs) return 1;
      if (rhs < lhs) return -1;
      return 0;
    }

    private long ordinal() {
      if (lastChange == null) return 0;
      return lastChange.getTimeInMillis();
    }
  }

  /**
   * Does this {@link View} has any associated user information recorded?
   *
   * @deprecated Potentially very expensive call; do not use from Jelly views.
   */
  public boolean hasPeople() {
    return People.isApplicable(getItems());
  }

  /** Gets the users that show up in the changelog of this job collection. */
  public People getPeople() {
    return new People(this);
  }

  /** @since 1.484 */
  public AsynchPeople getAsynchPeople() {
    return new AsynchPeople(this);
  }

  @ExportedBean
  public static final class People {
    @Exported public final List<UserInfo> users;

    public final ModelObject parent;

    public People(Jenkins parent) {
      this.parent = parent;
      // for Hudson, really load all users
      Map<User, UserInfo> users = getUserInfo(parent.getItems());
      User unknown = User.getUnknown();
      for (User u : User.getAll()) {
        if (u == unknown) continue; // skip the special 'unknown' user
        if (!users.containsKey(u)) users.put(u, new UserInfo(u, null, null));
      }
      this.users = toList(users);
    }

    public People(View parent) {
      this.parent = parent;
      this.users = toList(getUserInfo(parent.getItems()));
    }

    private Map<User, UserInfo> getUserInfo(Collection<? extends Item> items) {
      Map<User, UserInfo> users = new HashMap<User, UserInfo>();
      for (Item item : items) {
        for (Job job : item.getAllJobs()) {
          if (job instanceof AbstractProject) {
            AbstractProject<?, ?> p = (AbstractProject) job;
            for (AbstractBuild<?, ?> build : p.getBuilds()) {
              for (Entry entry : build.getChangeSet()) {
                User user = entry.getAuthor();

                UserInfo info = users.get(user);
                if (info == null) users.put(user, new UserInfo(user, p, build.getTimestamp()));
                else if (info.getLastChange().before(build.getTimestamp())) {
                  info.project = p;
                  info.lastChange = build.getTimestamp();
                }
              }
            }
          }
        }
      }
      return users;
    }

    private List<UserInfo> toList(Map<User, UserInfo> users) {
      ArrayList<UserInfo> list = new ArrayList<UserInfo>();
      list.addAll(users.values());
      Collections.sort(list);
      return Collections.unmodifiableList(list);
    }

    public Api getApi() {
      return new Api(this);
    }

    /** @deprecated Potentially very expensive call; do not use from Jelly views. */
    public static boolean isApplicable(Collection<? extends Item> items) {
      for (Item item : items) {
        for (Job job : item.getAllJobs()) {
          if (job instanceof AbstractProject) {
            AbstractProject<?, ?> p = (AbstractProject) job;
            for (AbstractBuild<?, ?> build : p.getBuilds()) {
              for (Entry entry : build.getChangeSet()) {
                User user = entry.getAuthor();
                if (user != null) return true;
              }
            }
          }
        }
      }
      return false;
    }
  }

  /**
   * Variant of {@link People} which can be displayed progressively, since it may be slow.
   *
   * @since 1.484
   */
  public static final class AsynchPeople extends ProgressiveRendering { // JENKINS-15206

    private final Collection<TopLevelItem> items;
    private final User unknown;
    private final Map<User, UserInfo> users = new HashMap<User, UserInfo>();
    private final Set<User> modified = new HashSet<User>();
    private final String iconSize;
    public final ModelObject parent;

    /** @see Jenkins#getAsynchPeople */
    public AsynchPeople(Jenkins parent) {
      this.parent = parent;
      items = parent.getItems();
      unknown = User.getUnknown();
    }

    /** @see View#getAsynchPeople */
    public AsynchPeople(View parent) {
      this.parent = parent;
      items = parent.getItems();
      unknown = null;
    }

    {
      StaplerRequest req = Stapler.getCurrentRequest();
      iconSize =
          req != null
              ? Functions.validateIconSize(Functions.getCookie(req, "iconSize", "32x32"))
              : "32x32";
    }

    @Override
    protected void compute() throws Exception {
      int itemCount = 0;
      for (Item item : items) {
        for (Job<?, ?> job : item.getAllJobs()) {
          if (job instanceof AbstractProject) {
            AbstractProject<?, ?> p = (AbstractProject) job;
            RunList<? extends AbstractBuild<?, ?>> builds = p.getBuilds();
            int buildCount = 0;
            for (AbstractBuild<?, ?> build : builds) {
              if (canceled()) {
                return;
              }
              for (ChangeLogSet.Entry entry : build.getChangeSet()) {
                User user = entry.getAuthor();
                UserInfo info = users.get(user);
                if (info == null) {
                  UserInfo userInfo = new UserInfo(user, p, build.getTimestamp());
                  userInfo.avatar = UserAvatarResolver.resolveOrNull(user, iconSize);
                  synchronized (this) {
                    users.put(user, userInfo);
                    modified.add(user);
                  }
                } else if (info.getLastChange().before(build.getTimestamp())) {
                  synchronized (this) {
                    info.project = p;
                    info.lastChange = build.getTimestamp();
                    modified.add(user);
                  }
                }
              }
              // TODO consider also adding the user of the UserCause when applicable
              buildCount++;
              // TODO this defeats lazy-loading. Should rather do a breadth-first search, as in
              // hudson.plugins.view.dashboard.builds.LatestBuilds
              // (though currently there is no quick implementation of RunMap.size() ~
              // idOnDisk.size(), which would be needed for proper progress)
              progress((itemCount + 1.0 * buildCount / builds.size()) / (items.size() + 1));
            }
          }
        }
        itemCount++;
        progress(1.0 * itemCount / (items.size() + /* handling User.getAll */ 1));
      }
      if (unknown != null) {
        if (canceled()) {
          return;
        }
        for (User u : User.getAll()) { // TODO nice to have a method to iterate these lazily
          if (canceled()) {
            return;
          }
          if (u == unknown) {
            continue;
          }
          if (!users.containsKey(u)) {
            UserInfo userInfo = new UserInfo(u, null, null);
            userInfo.avatar = UserAvatarResolver.resolveOrNull(u, iconSize);
            synchronized (this) {
              users.put(u, userInfo);
              modified.add(u);
            }
          }
        }
      }
    }

    @Override
    protected synchronized JSON data() {
      JSONArray r = new JSONArray();
      for (User u : modified) {
        UserInfo i = users.get(u);
        JSONObject entry =
            new JSONObject()
                .accumulate("id", u.getId())
                .accumulate("fullName", u.getFullName())
                .accumulate("url", u.getUrl())
                .accumulate(
                    "avatar",
                    i.avatar != null
                        ? i.avatar
                        : Stapler.getCurrentRequest().getContextPath()
                            + Functions.getResourcePath()
                            + "/images/"
                            + iconSize
                            + "/user.png")
                .accumulate("timeSortKey", i.getTimeSortKey())
                .accumulate("lastChangeTimeString", i.getLastChangeTimeString());
        AbstractProject<?, ?> p = i.getProject();
        if (p != null) {
          entry
              .accumulate("projectUrl", p.getUrl())
              .accumulate("projectFullDisplayName", p.getFullDisplayName());
        }
        r.add(entry);
      }
      modified.clear();
      return r;
    }

    public Api getApi() {
      return new Api(new People());
    }

    /** JENKINS-16397 workaround */
    @Restricted(NoExternalUse.class)
    @ExportedBean
    public final class People {

      private View.People people;

      @Exported
      public synchronized List<UserInfo> getUsers() {
        if (people == null) {
          people =
              parent instanceof Jenkins
                  ? new View.People((Jenkins) parent)
                  : new View.People((View) parent);
        }
        return people.users;
      }
    }
  }

  void addDisplayNamesToSearchIndex(SearchIndexBuilder sib, Collection<TopLevelItem> items) {
    for (TopLevelItem item : items) {

      if (LOGGER.isLoggable(Level.FINE)) {
        LOGGER.fine(
            (String.format(
                "Adding url=%s,displayName=%s", item.getSearchUrl(), item.getDisplayName())));
      }
      sib.add(item.getSearchUrl(), item.getDisplayName());
    }
  }

  @Override
  public SearchIndexBuilder makeSearchIndex() {
    SearchIndexBuilder sib = super.makeSearchIndex();
    sib.add(
        new CollectionSearchIndex<TopLevelItem>() { // for jobs in the view
          protected TopLevelItem get(String key) {
            return getItem(key);
          }

          protected Collection<TopLevelItem> all() {
            return getItems();
          }

          @Override
          protected String getName(TopLevelItem o) {
            // return the name instead of the display for suggestion searching
            return o.getName();
          }
        });

    // add the display name for each item in the search index
    addDisplayNamesToSearchIndex(sib, getItems());

    return sib;
  }

  /** Accepts the new description. */
  @RequirePOST
  public synchronized void doSubmitDescription(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    checkPermission(CONFIGURE);

    description = req.getParameter("description");
    save();
    rsp.sendRedirect("."); // go to the top page
  }

  /**
   * Accepts submission from the configuration page.
   *
   * <p>Subtypes should override the {@link #submit(StaplerRequest)} method.
   */
  @RequirePOST
  public final synchronized void doConfigSubmit(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException, FormException {
    checkPermission(CONFIGURE);

    submit(req);

    description = Util.nullify(req.getParameter("description"));
    filterExecutors = req.getParameter("filterExecutors") != null;
    filterQueue = req.getParameter("filterQueue") != null;

    rename(req.getParameter("name"));

    getProperties().rebuild(req, req.getSubmittedForm(), getApplicablePropertyDescriptors());
    updateTransientActions();

    save();

    FormApply.success("../" + Util.rawEncode(name)).generateResponse(req, rsp, this);
  }

  /**
   * Handles the configuration submission.
   *
   * <p>Load view-specific properties here.
   */
  protected abstract void submit(StaplerRequest req)
      throws IOException, ServletException, FormException;

  /** Deletes this view. */
  @RequirePOST
  public synchronized void doDoDelete(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    checkPermission(DELETE);

    owner.deleteView(this);

    rsp.sendRedirect2(req.getContextPath() + "/" + owner.getUrl());
  }

  /**
   * Creates a new {@link Item} in this collection.
   *
   * <p>This method should call {@link ModifiableItemGroup#doCreateItem(StaplerRequest,
   * StaplerResponse)} and then add the newly created item to this view.
   *
   * @return null if fails.
   */
  public abstract Item doCreateItem(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException;

  public void doRssAll(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    rss(req, rsp, " all builds", getBuilds());
  }

  public void doRssFailed(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    rss(req, rsp, " failed builds", getBuilds().failureOnly());
  }

  public RunList getBuilds() {
    return new RunList(this);
  }

  public BuildTimelineWidget getTimeline() {
    return new BuildTimelineWidget(getBuilds());
  }

  private void rss(StaplerRequest req, StaplerResponse rsp, String suffix, RunList runs)
      throws IOException, ServletException {
    RSS.forwardToRss(
        getDisplayName() + suffix, getUrl(), runs.newBuilds(), Run.FEED_ADAPTER, req, rsp);
  }

  public void doRssLatest(StaplerRequest req, StaplerResponse rsp)
      throws IOException, ServletException {
    List<Run> lastBuilds = new ArrayList<Run>();
    for (TopLevelItem item : getItems()) {
      if (item instanceof Job) {
        Job job = (Job) item;
        Run lb = job.getLastBuild();
        if (lb != null) lastBuilds.add(lb);
      }
    }
    RSS.forwardToRss(
        getDisplayName() + " last builds only",
        getUrl(),
        lastBuilds,
        Run.FEED_ADAPTER_LATEST,
        req,
        rsp);
  }

  /** Accepts <tt>config.xml</tt> submission, as well as serve it. */
  @WebMethod(name = "config.xml")
  public HttpResponse doConfigDotXml(StaplerRequest req) throws IOException {
    if (req.getMethod().equals("GET")) {
      // read
      checkPermission(READ);
      return new HttpResponse() {
        public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node)
            throws IOException, ServletException {
          rsp.setContentType("application/xml");
          View.this.writeXml(rsp.getOutputStream());
        }
      };
    }
    if (req.getMethod().equals("POST")) {
      // submission
      updateByXml(new StreamSource(req.getReader()));
      return HttpResponses.ok();
    }

    // huh?
    return HttpResponses.error(SC_BAD_REQUEST, "Unexpected request method " + req.getMethod());
  }

  /** @since 1.538 */
  public void writeXml(OutputStream out) throws IOException {
    // pity we don't have a handy way to clone Jenkins.XSTREAM to temp add the omit Field
    XStream2 xStream2 = new XStream2();
    xStream2.omitField(View.class, "owner");
    xStream2.toXMLUTF8(View.this, out);
  }

  /** Updates Job by its XML definition. */
  public void updateByXml(Source source) throws IOException {
    checkPermission(CONFIGURE);
    StringWriter out = new StringWriter();
    try {
      // this allows us to use UTF-8 for storing data,
      // plus it checks any well-formedness issue in the submitted
      // data
      Transformer t = TransformerFactory.newInstance().newTransformer();
      t.transform(source, new StreamResult(out));
      out.close();
    } catch (TransformerException e) {
      throw new IOException("Failed to persist configuration.xml", e);
    }

    // try to reflect the changes by reloading
    InputStream in =
        new BufferedInputStream(new ByteArrayInputStream(out.toString().getBytes("UTF-8")));
    try {
      // Do not allow overwriting view name as it might collide with another
      // view in same ViewGroup and might not satisfy Jenkins.checkGoodName.
      String oldname = name;
      Jenkins.XSTREAM.unmarshal(new XppDriver().createReader(in), this);
      name = oldname;
    } catch (StreamException e) {
      throw new IOException("Unable to read", e);
    } catch (ConversionException e) {
      throw new IOException("Unable to read", e);
    } catch (Error e) { // mostly reflection errors
      throw new IOException("Unable to read", e);
    } finally {
      in.close();
    }
    save();
  }

  public ContextMenu doChildrenContextMenu(StaplerRequest request, StaplerResponse response)
      throws Exception {
    ContextMenu m = new ContextMenu();
    for (TopLevelItem i : getItems()) m.add(i.getShortUrl(), i.getDisplayName());
    return m;
  }

  /**
   * A list of available view types.
   *
   * @deprecated as of 1.286 Use {@link #all()} for read access, and use {@link Extension} for
   *     registration.
   */
  public static final DescriptorList<View> LIST = new DescriptorList<View>(View.class);

  /** Returns all the registered {@link ViewDescriptor}s. */
  public static DescriptorExtensionList<View, ViewDescriptor> all() {
    return Jenkins.getInstance().<View, ViewDescriptor>getDescriptorList(View.class);
  }

  public static List<ViewDescriptor> allInstantiable() {
    List<ViewDescriptor> r = new ArrayList<ViewDescriptor>();
    for (ViewDescriptor d : all()) if (d.isInstantiable()) r.add(d);
    return r;
  }

  public static final Comparator<View> SORTER =
      new Comparator<View>() {
        public int compare(View lhs, View rhs) {
          return lhs.getViewName().compareTo(rhs.getViewName());
        }
      };

  public static final PermissionGroup PERMISSIONS =
      new PermissionGroup(View.class, Messages._View_Permissions_Title());
  /** Permission to create new views. */
  public static final Permission CREATE =
      new Permission(
          PERMISSIONS,
          "Create",
          Messages._View_CreatePermission_Description(),
          Permission.CREATE,
          PermissionScope.ITEM_GROUP);

  public static final Permission DELETE =
      new Permission(
          PERMISSIONS,
          "Delete",
          Messages._View_DeletePermission_Description(),
          Permission.DELETE,
          PermissionScope.ITEM_GROUP);
  public static final Permission CONFIGURE =
      new Permission(
          PERMISSIONS,
          "Configure",
          Messages._View_ConfigurePermission_Description(),
          Permission.CONFIGURE,
          PermissionScope.ITEM_GROUP);
  public static final Permission READ =
      new Permission(
          PERMISSIONS,
          "Read",
          Messages._View_ReadPermission_Description(),
          Permission.READ,
          PermissionScope.ITEM_GROUP);

  // to simplify access from Jelly
  public static Permission getItemCreatePermission() {
    return Item.CREATE;
  }

  public static View create(StaplerRequest req, StaplerResponse rsp, ViewGroup owner)
      throws FormException, IOException, ServletException {
    String requestContentType = req.getContentType();
    if (requestContentType == null) throw new Failure("No Content-Type header set");

    boolean isXmlSubmission =
        requestContentType.startsWith("application/xml")
            || requestContentType.startsWith("text/xml");

    String name = req.getParameter("name");
    checkGoodName(name);
    if (owner.getView(name) != null) throw new Failure(Messages.Hudson_ViewAlreadyExists(name));

    String mode = req.getParameter("mode");
    if (mode == null || mode.length() == 0) {
      if (isXmlSubmission) {
        View v = createViewFromXML(name, req.getInputStream());
        v.owner = owner;
        rsp.setStatus(HttpServletResponse.SC_OK);
        return v;
      } else throw new Failure(Messages.View_MissingMode());
    }

    View v;
    if (mode != null && mode.equals("copy")) {
      v = copy(req, owner, name);
    } else {
      ViewDescriptor descriptor = all().findByName(mode);
      if (descriptor == null) {
        throw new Failure("No view type ‘" + mode + "’ is known");
      }

      // create a view
      v = descriptor.newInstance(req, req.getSubmittedForm());
    }
    v.owner = owner;

    // redirect to the config screen
    rsp.sendRedirect2(req.getContextPath() + '/' + v.getUrl() + v.getPostConstructLandingPage());

    return v;
  }

  private static View copy(StaplerRequest req, ViewGroup owner, String name) throws IOException {
    View v;
    String from = req.getParameter("from");
    View src = src = owner.getView(from);

    if (src == null) {
      if (Util.fixEmpty(from) == null) throw new Failure("Specify which view to copy");
      else throw new Failure("No such view: " + from);
    }
    String xml = Jenkins.XSTREAM.toXML(src);
    v = createViewFromXML(name, new StringInputStream(xml));
    return v;
  }

  /**
   * Instantiate View subtype from XML stream.
   *
   * @param name Alternative name to use or <tt>null</tt> to keep the one in xml.
   */
  public static View createViewFromXML(String name, InputStream xml) throws IOException {
    InputStream in = new BufferedInputStream(xml);
    try {
      View v = (View) Jenkins.XSTREAM.fromXML(in);
      if (name != null) v.name = name;
      checkGoodName(v.name);
      return v;
    } catch (StreamException e) {
      throw new IOException("Unable to read", e);
    } catch (ConversionException e) {
      throw new IOException("Unable to read", e);
    } catch (Error e) { // mostly reflection errors
      throw new IOException("Unable to read", e);
    } finally {
      in.close();
    }
  }

  public static class PropertyList extends DescribableList<ViewProperty, ViewPropertyDescriptor> {
    private PropertyList(View owner) {
      super(owner);
    }

    public PropertyList() { // needed for XStream deserialization
    }

    public View getOwner() {
      return (View) owner;
    }

    @Override
    protected void onModified() throws IOException {
      for (ViewProperty p : this) p.setView(getOwner());
    }
  }

  /**
   * "Job" in "New Job". When a view is used in a context that restricts the child type, It might be
   * useful to override this.
   */
  public static final Message<View> NEW_PRONOUN = new Message<View>();

  private static final Logger LOGGER = Logger.getLogger(View.class.getName());
}