Introduce jenkins.model.queue.QueueItem interface (#7926)

* Introduce jenkins.model.queue.QueueItem interface

This formalizes methods used by <t:queue> taglib to do the rendering of queue items.

* Apply suggestions from code review

Co-authored-by: Jesse Glick <jglick@cloudbees.com>

* Implement default Queue.Task methods to simplify Mock item

* Do the right thing automatically if the Queue.Task implements AccessControlled

---------

Co-authored-by: Jesse Glick <jglick@cloudbees.com>
This commit is contained in:
Vincent LatombeandJesse Glick authored and GitHub committed 2023-05-16 09:01:56 +02:00
1 parent 2a4795690c
commit cce017cdec
8 files changed
+170 -57

No files matched your search

+32 -25
View File
@@ -26,6 +26,7 @@
package hudson.model;
import static hudson.init.InitMilestone.JOB_CONFIG_ADAPTED;
import static hudson.model.Item.CANCEL;
import static hudson.util.Iterators.reverse;
import com.google.common.annotations.VisibleForTesting;
@@ -112,6 +113,7 @@ import javax.servlet.http.HttpServletResponse;
import jenkins.model.Jenkins;
import jenkins.model.queue.AsynchronousExecution;
import jenkins.model.queue.CompositeCauseOfBlockage;
import jenkins.model.queue.QueueItem;
import jenkins.security.QueueItemAuthenticator;
import jenkins.security.QueueItemAuthenticatorProvider;
import jenkins.security.stapler.StaplerAccessibleType;
@@ -1929,25 +1931,34 @@ public class Queue extends ResourceController implements Saveable {
* Checks the permission to see if the current user can abort this executable.
* Returns normally from this method if it's OK.
* <p>
* NOTE: If you have implemented {@link AccessControlled} this should just be
* NOTE: If you have implemented {@link AccessControlled} this defaults to
* {@code checkPermission(hudson.model.Item.CANCEL);}
*
* @throws AccessDeniedException if the permission is not granted.
*/
void checkAbortPermission();
default void checkAbortPermission() {
if (this instanceof AccessControlled) {
((AccessControlled) this).checkPermission(CANCEL);
}
}
/**
* Works just like {@link #checkAbortPermission()} except it indicates the status by a return value,
* instead of exception.
* Also used by default for {@link hudson.model.Queue.Item#hasCancelPermission}.
* <p>
* NOTE: If you have implemented {@link AccessControlled} this should just be
* NOTE: If you have implemented {@link AccessControlled} this returns by default
* {@code return hasPermission(hudson.model.Item.CANCEL);}
*
* @return false
* if the user doesn't have the permission.
*/
boolean hasAbortPermission();
default boolean hasAbortPermission() {
if (this instanceof AccessControlled) {
return ((AccessControlled) this).hasPermission(CANCEL);
}
return true;
}
/**
* Returns the URL of this task relative to the context root of the application.
@@ -2117,7 +2128,7 @@ public class Queue extends ResourceController implements Saveable {
* Item in a queue.
*/
@ExportedBean(defaultVisibility = 999)
public abstract static class Item extends Actionable {
public abstract static class Item extends Actionable implements QueueItem {
private final long id;
@@ -2128,6 +2139,7 @@ public class Queue extends ResourceController implements Saveable {
* @since 1.601
*/
@Exported
@Override
public long getId() {
return id;
}
@@ -2147,12 +2159,19 @@ public class Queue extends ResourceController implements Saveable {
* Project to be built.
*/
@Exported
@NonNull
public final Task task;
private /*almost final*/ transient FutureImpl future;
private final long inQueueSince;
@Override
@NonNull
public Task getTask() {
return task;
}
/**
* Build is blocked because another build is in progress,
* required {@link Resource}s are not available, or otherwise blocked
@@ -2173,6 +2192,7 @@ public class Queue extends ResourceController implements Saveable {
* True if the item is starving for an executor for too long.
*/
@Exported
@Override
public boolean isStuck() { return false; }
/**
@@ -2188,6 +2208,7 @@ public class Queue extends ResourceController implements Saveable {
* Returns a human readable presentation of how long this item is already in the queue.
* E.g. something like '3 minutes 40 seconds'
*/
@Override
public String getInQueueForString() {
long duration = System.currentTimeMillis() - this.inQueueSince;
return Util.getTimeSpanString(duration);
@@ -2250,6 +2271,7 @@ public class Queue extends ResourceController implements Saveable {
}
@Restricted(DoNotUse.class) // used from Jelly
@Override
public String getCausesDescription() {
List<Cause> causes = getCauses();
StringBuilder s = new StringBuilder();
@@ -2259,15 +2281,11 @@ public class Queue extends ResourceController implements Saveable {
return s.toString();
}
protected Item(Task task, List<Action> actions, long id, FutureImpl future) {
this.task = task;
this.id = id;
this.future = future;
this.inQueueSince = System.currentTimeMillis();
for (Action action : actions) addAction(action);
protected Item(@NonNull Task task, @NonNull List<Action> actions, long id, FutureImpl future) {
this(task, actions, id, future, System.currentTimeMillis());
}
protected Item(Task task, List<Action> actions, long id, FutureImpl future, long inQueueSince) {
protected Item(@NonNull Task task, @NonNull List<Action> actions, long id, FutureImpl future, long inQueueSince) {
this.task = task;
this.id = id;
this.future = future;
@@ -2297,6 +2315,7 @@ public class Queue extends ResourceController implements Saveable {
* Gets a human-readable status message describing why it's in the queue.
*/
@Exported
@Override
public final String getWhy() {
CauseOfBlockage cob = getCauseOfBlockage();
return cob != null ? cob.getShortDescription() : null;
@@ -2312,6 +2331,7 @@ public class Queue extends ResourceController implements Saveable {
* @return String
*/
@Exported
@Override
public String getParams() {
StringBuilder s = new StringBuilder();
for (ParametersAction pa : getActions(ParametersAction.class)) {
@@ -2322,19 +2342,6 @@ public class Queue extends ResourceController implements Saveable {
return s.toString();
}
/**
* Checks whether a scheduled item may be canceled.
* @return by default, the same as {@link hudson.model.Queue.Task#hasAbortPermission}
*/
public boolean hasCancelPermission() {
return task.hasAbortPermission();
}
@Override
public String getDisplayName() {
return null;
}
@Override
public String getSearchUrl() {
return null;
@@ -24,11 +24,11 @@
package hudson.widgets;
import hudson.model.Queue.Item;
import hudson.model.Queue.Task;
import java.util.LinkedList;
import java.util.List;
import jenkins.model.Jenkins;
import jenkins.model.queue.QueueItem;
import jenkins.widgets.HistoryPageFilter;
/**
@@ -52,17 +52,17 @@ public class BuildHistoryWidget<T> extends HistoryWidget<Task, T> {
/**
* Returns the first queue item if the owner is scheduled for execution in the queue.
*/
public Item getQueuedItem() {
public QueueItem getQueuedItem() {
return Jenkins.get().getQueue().getItem(owner);
}
/**
* Returns the queue item if the owner is scheduled for execution in the queue, in REVERSE ORDER
*/
public List<Item> getQueuedItems() {
LinkedList<Item> list = new LinkedList<>();
for (Item item : Jenkins.get().getQueue().getItems()) {
if (item.task == owner) {
public List<QueueItem> getQueuedItems() {
LinkedList<QueueItem> list = new LinkedList<>();
for (QueueItem item : Jenkins.get().getQueue().getItems()) {
if (item.getTask() == owner) {
list.addFirst(item);
}
}
@@ -0,0 +1,78 @@
package jenkins.model.queue;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.Cause;
import hudson.model.ModelObject;
import hudson.model.Queue;
import hudson.model.Run;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.Beta;
/**
* Interface used by Jelly views to render a queue item through {@code <t:queue>}.
* @since TODO
*/
@Restricted(Beta.class)
public interface QueueItem extends ModelObject {
/**
* @return true if the item is starving for an executor for too long.
*/
boolean isStuck();
/**
* @return The underlying {@link Queue.Task} currently in queue.
*/
@NonNull
Queue.Task getTask();
/**
* Checks whether a scheduled item may be canceled.
* @return by default, the same as {@link Queue.Task#hasAbortPermission}
*/
default boolean hasCancelPermission() {
return getTask().hasAbortPermission();
}
/**
* Unique ID (per controller) that tracks the {@link Queue.Task} as it moves through different stages
* in the queue (each represented by different implementations of {@link QueueItem} and into any subsequent
* {@link Run} instance (see {@link Run#getQueueId()}).
*/
long getId();
/**
* Convenience method that returns a read only view of the {@link Cause}s associated with this item in the queue as a single string.
*/
@NonNull
String getCausesDescription();
/**
* Gets a human-readable status message describing why it's in the queue.
* May return null if there is no cause of blockage.
*/
@CheckForNull
String getWhy();
/**
* Gets a human-readable message about the parameters of this item
*/
@NonNull
String getParams();
/**
* Returns a human readable presentation of how long this item is already in the queue.
* E.g. something like '3 minutes 40 seconds'
*/
@NonNull
String getInQueueForString();
/**
* @return a display name for this queue item; by default, {@link Queue.Task#getFullDisplayName()}
*/
@CheckForNull
@Override
default String getDisplayName() {
return getTask().getFullDisplayName();
}
}
@@ -25,14 +25,14 @@
package jenkins.widgets;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.model.Queue;
import hudson.model.Run;
import jenkins.model.queue.QueueItem;
/**
* Represents an entry used by the {@link HistoryPageFilter}.
*
* <p>
* Wraps {@link Queue.Item} and {@link Run} instances from the build queue, normalizing
* Wraps {@link QueueItem} and {@link Run} instances from the build queue, normalizing
* access to the info required for pagination.
*
*
@@ -55,8 +55,8 @@ public class HistoryPageEntry<T> {
}
protected static long getEntryId(@NonNull Object entry) {
if (entry instanceof Queue.Item) {
return ((Queue.Item) entry).getId();
if (entry instanceof QueueItem) {
return ((QueueItem) entry).getId();
} else if (entry instanceof Run) {
Run run = (Run) entry;
return Long.MIN_VALUE + run.getNumber();
@@ -42,6 +42,7 @@ import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import jenkins.model.queue.QueueItem;
/**
* History page filter.
@@ -55,9 +56,9 @@ public class HistoryPageFilter<T> {
private Long olderThan;
private String searchString;
// Need to use different Lists for Queue.Items and Runs because
// Need to use different Lists for QueueItem and Runs because
// we need access to them separately in the jelly files for rendering.
public final List<HistoryPageEntry<Queue.Item>> queueItems = new ArrayList<>();
public final List<HistoryPageEntry<QueueItem>> queueItems = new ArrayList<>();
public final List<HistoryPageEntry<Run>> runs = new ArrayList<>();
@SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD", justification = "read by Stapler")
@@ -128,7 +129,7 @@ public class HistoryPageFilter<T> {
* @param queueItems The queue items to be added. Queue items do not need to be sorted.
* @since 2.17
*/
public void add(@NonNull Iterable<T> runItems, @NonNull List<Queue.Item> queueItems) {
public void add(@NonNull Iterable<T> runItems, @NonNull List<QueueItem> queueItems) {
sort(queueItems);
addInternal(Iterables.concat(queueItems, runItems));
}
@@ -136,7 +137,7 @@ public class HistoryPageFilter<T> {
/**
* Add items to the History page, internal implementation.
* @param items The items to be added.
* @param <ItemT> The type of items should either be T or Queue.Item.
* @param <ItemT> The type of items should either be T or {@link QueueItem}.
*/
private <ItemT> void addInternal(@NonNull Iterable<ItemT> items) {
// Note that items can be a large lazily evaluated collection,
@@ -240,8 +241,8 @@ public class HistoryPageFilter<T> {
}
private long getNextBuildNumber(@NonNull Object entry) {
if (entry instanceof Queue.Item) {
Queue.Task task = ((Queue.Item) entry).task;
if (entry instanceof QueueItem) {
Queue.Task task = ((QueueItem) entry).getTask();
if (task instanceof Job) {
return ((Job) task).getNextBuildNumber();
}
@@ -253,8 +254,8 @@ public class HistoryPageFilter<T> {
return HistoryPageEntry.getEntryId(entry) + 1;
}
private void addQueueItem(Queue.Item item) {
HistoryPageEntry<Queue.Item> entry = new HistoryPageEntry<>(item);
private void addQueueItem(QueueItem item) {
HistoryPageEntry<QueueItem> entry = new HistoryPageEntry<>(item);
queueItems.add(entry);
updateNewestOldest(entry.getEntryId());
}
@@ -279,8 +280,8 @@ public class HistoryPageFilter<T> {
private boolean add(Object entry) {
// Purposely not calling isFull(). May need to add a greater number of entries
// to the page initially, newerThan then cutting it back down to size using cutLeading()
if (entry instanceof Queue.Item) {
Queue.Item item = (Queue.Item) entry;
if (entry instanceof QueueItem) {
QueueItem item = (QueueItem) entry;
if (searchString != null && !fitsSearchParams(item)) {
return false;
}
@@ -310,7 +311,7 @@ public class HistoryPageFilter<T> {
return Math.max(0, maxEntries - size());
}
private boolean fitsSearchParams(@NonNull Queue.Item item) {
private boolean fitsSearchParams(@NonNull QueueItem item) {
if (fitsSearchString(item.getDisplayName())) {
return true;
} else if (fitsSearchString(item.getId())) {