New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FIXED JENKINS-40252] Add an Iterable<T> that returns all items unsorted #2665
Changes from 4 commits
cf0fea0
52ea389
ab849a3
16254cb
6ce8caf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,29 +30,30 @@ | |
import hudson.model.listeners.ItemListener; | ||
import hudson.remoting.Callable; | ||
import hudson.security.ACL; | ||
import hudson.security.ACLContext; | ||
import hudson.security.AccessControlled; | ||
import hudson.triggers.Trigger; | ||
import hudson.util.DescriptorList; | ||
import hudson.util.EditDistance; | ||
import hudson.util.XStream2; | ||
import jenkins.model.Jenkins; | ||
import org.acegisecurity.Authentication; | ||
import org.apache.commons.lang.StringUtils; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.Comparator; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.NoSuchElementException; | ||
import java.util.Stack; | ||
import java.util.StringTokenizer; | ||
import javax.annotation.CheckForNull; | ||
import javax.annotation.Nonnull; | ||
|
||
import jenkins.model.DirectlyModifiableTopLevelItemGroup; | ||
import jenkins.model.Jenkins; | ||
import org.acegisecurity.Authentication; | ||
import org.apache.commons.io.FileUtils; | ||
import org.apache.commons.lang.StringUtils; | ||
|
||
/** | ||
* Convenience methods related to {@link Item}. | ||
|
@@ -79,6 +80,44 @@ public class Items { | |
return false; | ||
} | ||
}; | ||
/** | ||
* A comparator of {@link Item} instances that uses a case-insensitive comparison of {@link Item#getName()}. | ||
* If you are replacing {@link #getAllItems(ItemGroup, Class)} with {@link #allItems(ItemGroup, Class)} and | ||
* need to restore the sort order of a further filtered result, you probably want {@link #BY_FULL_NAME}. | ||
* | ||
* @since FIXME | ||
*/ | ||
public static final Comparator<Item> BY_NAME = new Comparator<Item>() { | ||
@Override public int compare(Item i1, Item i2) { | ||
return name(i1).compareToIgnoreCase(name(i2)); | ||
} | ||
|
||
String name(Item i) { | ||
String n = i.getName(); | ||
if (i instanceof ItemGroup) { | ||
n += '/'; | ||
} | ||
return n; | ||
} | ||
}; | ||
/** | ||
* A comparator of {@link Item} instances that uses a case-insensitive comparison of {@link Item#getFullName()}. | ||
* | ||
* @since FIXME | ||
*/ | ||
public static final Comparator<Item> BY_FULL_NAME = new Comparator<Item>() { | ||
@Override public int compare(Item i1, Item i2) { | ||
return name(i1).compareToIgnoreCase(name(i2)); | ||
} | ||
|
||
String name(Item i) { | ||
String n = i.getFullName(); | ||
if (i instanceof ItemGroup) { | ||
n += '/'; | ||
} | ||
return n; | ||
} | ||
}; | ||
|
||
/** | ||
* Runs a block while making {@link #currentlyUpdatingByXml} be temporarily true. | ||
|
@@ -350,7 +389,12 @@ public static XmlFile getConfigFile(Item item) { | |
|
||
/** | ||
* Gets all the {@link Item}s recursively in the {@link ItemGroup} tree | ||
* and filter them by the given type. | ||
* and filter them by the given type. The returned list will represent a snapshot view of the items present at some | ||
* time during the call. If items are moved during the call, depending on the move, it may be possible for some | ||
* items to escape the snapshot entirely. | ||
* <p> | ||
* If you do not need to iterate all items, or if the order of the items is not required, consider using | ||
* {@link #allItems(ItemGroup, Class)} instead. | ||
* | ||
* @since 1.512 | ||
*/ | ||
|
@@ -361,18 +405,8 @@ public static <T extends Item> List<T> getAllItems(final ItemGroup root, Class<T | |
} | ||
private static <T extends Item> void getAllItems(final ItemGroup root, Class<T> type, List<T> r) { | ||
List<Item> items = new ArrayList<Item>(((ItemGroup<?>) root).getItems()); | ||
Collections.sort(items, new Comparator<Item>() { | ||
@Override public int compare(Item i1, Item i2) { | ||
return name(i1).compareToIgnoreCase(name(i2)); | ||
} | ||
String name(Item i) { | ||
String n = i.getName(); | ||
if (i instanceof ItemGroup) { | ||
n += '/'; | ||
} | ||
return n; | ||
} | ||
}); | ||
// because we add items depth first, we can use the quicker BY_NAME comparison | ||
Collections.sort(items, BY_NAME); | ||
for (Item i : items) { | ||
if (type.isInstance(i)) { | ||
if (i.hasPermission(Item.READ)) { | ||
|
@@ -385,6 +419,41 @@ String name(Item i) { | |
} | ||
} | ||
|
||
/** | ||
* Gets a read-only view of all the {@link Item}s recursively in the {@link ItemGroup} tree visible to | ||
* {@link Jenkins#getAuthentication()} without concern for the order in which items are returned. Each iteration | ||
* of the view will be "live" reflecting the items available between the time the iteration was started and the | ||
* time the iteration was completed, however if items are moved during an iteration - depending on the move - it | ||
* may be possible for such items to escape the entire iteration. | ||
* | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense to mention the iterator is read-inly |
||
* @param root the root. | ||
* @param type the type. | ||
* @param <T> the type. | ||
* @return An {@link Iterable} for all items. | ||
* @since FIXME | ||
*/ | ||
public static <T extends Item> Iterable<T> allItems(ItemGroup root, Class<T> type) { | ||
return allItems(Jenkins.getAuthentication(), root, type); | ||
} | ||
|
||
|
||
/** | ||
* Gets a read-only view all the {@link Item}s recursively in the {@link ItemGroup} tree visible to the supplied | ||
* authentication without concern for the order in which items are returned. Each iteration | ||
* of the view will be "live" reflecting the items available between the time the iteration was started and the | ||
* time the iteration was completed, however if items are moved during an iteration - depending on the move - it | ||
* may be possible for such items to escape the entire iteration. | ||
* | ||
* @param root the root. | ||
* @param type the type. | ||
* @param <T> the type. | ||
* @return An {@link Iterable} for all items. | ||
* @since FIXME | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense to mention the iterator is read-inly There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fixed |
||
*/ | ||
public static <T extends Item> Iterable<T> allItems(Authentication authentication, ItemGroup root, Class<T> type) { | ||
return new AllItemsIterable<>(root, authentication, type); | ||
} | ||
|
||
/** | ||
* Finds an item whose name (when referenced from the specified context) is closest to the given name. | ||
* @param <T> the type of item being considered | ||
|
@@ -395,10 +464,9 @@ String name(Item i) { | |
* @since 1.538 | ||
*/ | ||
public static @CheckForNull <T extends Item> T findNearest(Class<T> type, String name, ItemGroup context) { | ||
List<T> projects = Jenkins.getInstance().getAllItems(type); | ||
String[] names = new String[projects.size()]; | ||
for (int i = 0; i < projects.size(); i++) { | ||
names[i] = projects.get(i).getRelativeNameFrom(context); | ||
List<String> names = new ArrayList<>(); | ||
for (T item: Jenkins.getInstance().allItems(type)) { | ||
names.add(item.getRelativeNameFrom(context)); | ||
} | ||
String nearest = EditDistance.findNearest(name, names); | ||
return Jenkins.getInstance().getItem(nearest, context, type); | ||
|
@@ -440,6 +508,121 @@ public static <I extends AbstractItem & TopLevelItem> I move(I item, DirectlyMod | |
return newItem; | ||
} | ||
|
||
private static class AllItemsIterable<T extends Item> implements Iterable<T> { | ||
|
||
/** | ||
* The authentication we are iterating as. | ||
*/ | ||
private final Authentication authentication; | ||
/** | ||
* The root we are iterating from. | ||
*/ | ||
private final ItemGroup root; | ||
/** | ||
* The type of item we want to return. | ||
*/ | ||
private final Class<T> type; | ||
|
||
private AllItemsIterable(ItemGroup root, Authentication authentication, Class<T> type) { | ||
this.root = root; | ||
this.authentication = authentication; | ||
this.type = type; | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
@Override | ||
public Iterator<T> iterator() { | ||
return new AllItemsIterator(); | ||
} | ||
|
||
private class AllItemsIterator implements Iterator<T> { | ||
|
||
/** | ||
* The stack of {@link ItemGroup}s that we have left to descend into. | ||
*/ | ||
private final Stack<ItemGroup> stack = new Stack<>(); | ||
/** | ||
* The iterator of the current {@link ItemGroup} we are iterating. | ||
*/ | ||
private Iterator<Item> delegate = null; | ||
/** | ||
* The next item. | ||
*/ | ||
private T next = null; | ||
|
||
private AllItemsIterator() { | ||
// put on the stack so that hasNext() is the only place that has to worry about authentication | ||
// alternative would be to impersonate and populate delegate. | ||
stack.push(root); | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
@Override | ||
public void remove() { | ||
throw new UnsupportedOperationException(); | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
@Override | ||
public boolean hasNext() { | ||
if (next != null) { | ||
return true; | ||
} | ||
while (true) { | ||
if (delegate == null || !delegate.hasNext()) { | ||
if (stack.isEmpty()) { | ||
return false; | ||
} | ||
ItemGroup group = stack.pop(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have a concern about the following case:
Maybe I'm reading the code wrong since the test seems to cover this case There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No it will exhaust the current iteration before popping the stack, not strictly breath first There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On the first cycle if D C and B are of type T, you will get D, C and B in any order, then it will pop one of those off the stack and process them, A is done when it starts its children |
||
// group.getItems() is responsible for performing the permission check so we will not repeat it | ||
if (Jenkins.getAuthentication() == authentication) { | ||
delegate = group.getItems().iterator(); | ||
} else { | ||
// slower path because the caller has switched authentication | ||
// we need to keep the original authentication so that allItems() can be used | ||
// like getAllItems() without the cost of building the entire list up front | ||
try (ACLContext ctx = ACL.as(authentication)) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why don't you impersonate once and only once on the top level of Calling the impersonation on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because the usage pattern may be different. With getAllItems() you get the list using current auth, you can then switch auth and do whatever you need... With this, if you change auth after getting the Iterable it will behave the same as getting the List. Without the impersonation, we have an unpredictable iterable. The fast case where you just use the same auth throughout will not impersonate.
Now the side-effect may be legitimate, eg authorise project related... or it may be a bug... But if we don't guard against it then it is not safe to replace getAllItems with allItems. The slow part will be the call to getItems() as that does the permission checks, impersonation around that should be relatively cheap There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not feel it exactly answers my questions, but I agree the permission checks will be more expensive for non-System users. So it's rather NIT There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For 99.9% of use cases, people will not be changing authentication while iterating, and hence
Will be the path taken... which will just be inlined to a call to There are some cases where the authentication used to enumerate the items does not match to the current authentication. In Core there is one example: https://github.com/jenkinsci/jenkins/pull/2665/files#diff-46dd855f38f94f25c68cbdae7e6ba64bL243 the example is correct:
The second half of the branch is to cover this exact case. HTH |
||
delegate = group.getItems().iterator(); | ||
} | ||
} | ||
} | ||
while (delegate.hasNext()) { | ||
Item item = delegate.next(); | ||
if (item instanceof ItemGroup) { | ||
stack.push((ItemGroup) item); | ||
} | ||
if (type.isInstance(item)) { | ||
next = type.cast(item); | ||
return true; | ||
} | ||
} | ||
} | ||
} | ||
|
||
/** | ||
* {@inheritDoc} | ||
*/ | ||
@Override | ||
public T next() { | ||
if (!hasNext()) { | ||
throw new NoSuchElementException(); | ||
} | ||
try { | ||
return next; | ||
} finally { | ||
next = null; | ||
} | ||
} | ||
|
||
} | ||
} | ||
|
||
/** | ||
* Used to load/save job configuration. | ||
* | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't a natural sort be better? IIRC a user recently requested this.
I.e.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can do whatever sorts you want, this is the existing sort that you need to restore the sort order from getAllItems() if you use allItems() to build a filtered sub-list