提交 a210e489 编写于 作者: V Vincent Latombe

ListView can match now items belonging to nested ItemGroups

This changes introduces a 'recurse' option on ListView that allows to
match using regexp jobs contained inside folders.
The full name of the
job is matched so / can be used as separator.
This allows slice and dice of jobs contained in different folders.
上级 b99a794c
......@@ -55,6 +55,8 @@ Upcoming changes</a>
<!-- Record your changes in the trunk here. -->
<div id="trunk" style="display:none"><!--=TRUNK-BEGIN=-->
<ul class=image>
<li class=rfe>
Views can now include jobs located within folders
<li class=>
</ul>
</div><!--=TRUNK-END=-->
......
......@@ -903,7 +903,7 @@ public class Functions {
ItemGroup ig = i.getParent();
url = i.getShortUrl()+url;
if(ig== Jenkins.getInstance()) {
if(ig== Jenkins.getInstance() || (view != null && ig == view.getOwner())) {
assert i instanceof TopLevelItem;
if(view!=null && view.contains((TopLevelItem)i)) {
// if p and the current page belongs to the same view, then return a relative path
......
......@@ -43,6 +43,8 @@ import hudson.util.AtomicFileWriter;
import hudson.util.IOException2;
import hudson.util.IOUtils;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.apache.tools.ant.taskdefs.Copy;
import org.apache.tools.ant.types.FileSet;
import org.kohsuke.stapler.WebMethod;
......@@ -54,6 +56,7 @@ import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.StringTokenizer;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
......@@ -326,6 +329,21 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
if(n.length()==0) return getDisplayName();
else return n+" \u00BB "+getDisplayName();
}
public String getRelativeDisplayNameFrom(ItemGroup p) {
String relativeName = getRelativeNameFrom(p);
if (relativeName == null) return null;
return relativeName.replace("/", " \u00BB ");
}
/**
* This method only exists to disambiguate getRelativeNameFrom(Itemgroup) and getRelativeNameFrom(Item)
* @param p
* @return
*/
public String getRelativeNameFromGroup(ItemGroup p) {
return getRelativeNameFrom(p);
}
public String getRelativeNameFrom(ItemGroup p) {
// first list up all the parents
......@@ -367,7 +385,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
/**
* Called right after when a {@link Item} is loaded from disk.
* This is an opporunity to do a post load processing.
* This is an opportunity to do a post load processing.
*/
public void onLoad(ItemGroup<? extends Item> parent, String name) throws IOException {
this.parent = parent;
......@@ -407,7 +425,7 @@ public abstract class AbstractItem extends Actionable implements Item, HttpDelet
public String getShortUrl() {
return getParent().getUrlChildPrefix()+'/'+Util.rawEncode(getName())+'/';
}
public String getSearchUrl() {
return getShortUrl();
}
......
......@@ -124,6 +124,14 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
* of the ancestors.
*/
String getFullDisplayName();
/**
* Gets the relative display name to this item from the specified group
*
* @since 1.512
* @return
*/
String getRelativeDisplayNameFrom(ItemGroup g);
/**
* Gets the relative name to this item from the specified group.
......@@ -159,7 +167,7 @@ public interface Item extends PersistenceRoot, SearchableModelObject, AccessCont
* URL that ends with '/'.
*/
String getShortUrl();
/**
* Returns the absolute URL of this item. This relies on the current
* {@link StaplerRequest} to figure out what the host name is,
......
......@@ -235,6 +235,31 @@ public class Items {
public static XmlFile getConfigFile(Item item) {
return getConfigFile(item.getRootDir());
}
/**
* Gets all the {@link Item}s recursively in the {@link ItemGroup} tree
* and filter them by the given type.
*/
public static <T extends Item> List<T> getAllItems(ItemGroup root, Class<T> type) {
List<T> r = new ArrayList<T>();
Stack<ItemGroup> q = new Stack<ItemGroup>();
q.push(root);
while(!q.isEmpty()) {
ItemGroup<?> parent = q.pop();
for (Item i : parent.getItems()) {
if(type.isInstance(i)) {
if (i.hasPermission(Item.READ))
r.add(type.cast(i));
}
if(i instanceof ItemGroup)
q.push((ItemGroup)i);
}
}
return r;
}
/**
* Used to load/save job configuration.
......
......@@ -33,24 +33,29 @@ import hudson.util.FormValidation;
import hudson.util.HttpResponses;
import hudson.views.ListViewColumn;
import hudson.views.ViewJobFilter;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
import javax.annotation.concurrent.GuardedBy;
import javax.servlet.ServletException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SortedSet;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.concurrent.GuardedBy;
import javax.servlet.ServletException;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.QueryParameter;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.interceptor.RequirePOST;
/**
* Displays {@link Job}s in a flat list view.
*
......@@ -63,16 +68,25 @@ public class ListView extends View implements Saveable {
*/
@GuardedBy("this")
/*package*/ final SortedSet<String> jobNames = new TreeSet<String>(CaseInsensitiveComparator.INSTANCE);
@GuardedBy("this")
private transient SortedSet<String> allJobNames;
private DescribableList<ViewJobFilter, Descriptor<ViewJobFilter>> jobFilters;
private DescribableList<ListViewColumn, Descriptor<ListViewColumn>> columns;
/**
* Include regex string.
*/
private String includeRegex;
/**
* Whether to recurse in ItemGroups
*/
private boolean recurse = true;
/**
* Compiled include pattern from the includeRegex string.
*/
......@@ -138,33 +152,31 @@ public class ListView extends View implements Saveable {
*/
public List<TopLevelItem> getItems() {
SortedSet<String> names;
List<TopLevelItem> items = new ArrayList<TopLevelItem>();
SortedSet<String> allNames;
synchronized (this) {
names = new TreeSet<String>(jobNames);
allNames = allJobNames = new TreeSet<String>(CaseInsensitiveComparator.INSTANCE);
}
if (includePattern != null) {
for (Item item : getOwnerItemGroup().getItems()) {
String itemName = item.getName();
if (includePattern.matcher(itemName).matches()) {
names.add(itemName);
}
}
}
ItemGroup<? extends TopLevelItem> parent = getOwnerItemGroup();
includeItems(parent, names);
Boolean statusFilter = this.statusFilter; // capture the value to isolate us from concurrent update
List<TopLevelItem> items = new ArrayList<TopLevelItem>(names.size());
for (TopLevelItem item : getOwnerItemGroup().getItems()) {
if (!names.contains(item.getName())) continue;
for (TopLevelItem item : Items.getAllItems(getOwnerItemGroup(), TopLevelItem.class)) {
if (!names.contains(item.getRelativeNameFrom(getOwnerItemGroup()))) continue;
// Add if no status filter or filter matches enabled/disabled status:
if(statusFilter == null || !(item instanceof AbstractProject)
|| ((AbstractProject)item).isDisabled() ^ statusFilter)
|| ((AbstractProject)item).isDisabled() ^ statusFilter) {
items.add(item);
allNames.add(item.getName());
}
}
// check the filters
Iterable<ViewJobFilter> jobFilters = getJobFilters();
List<TopLevelItem> allItems = new ArrayList<TopLevelItem>(getOwnerItemGroup().getItems());
List<TopLevelItem> allItems = new ArrayList<TopLevelItem>(parent.getItems());
for (ViewJobFilter jobFilter: jobFilters) {
items = jobFilter.filter(items, allItems, this);
}
......@@ -173,9 +185,51 @@ public class ListView extends View implements Saveable {
return items;
}
private TopLevelItem getItem(ItemGroup<? extends TopLevelItem> parent, String name) {
if (!name.contains("/")) { // same namespace
return parent.getItem(name);
}
StringTokenizer stringTokenizer = new StringTokenizer(name, "/");
TopLevelItem leaf = null;
ItemGroup<? extends TopLevelItem> node = parent;
// Navigate down to the item using / as separator
while(stringTokenizer.hasMoreTokens()) {
leaf = node.getItem(stringTokenizer.nextToken());
if (stringTokenizer.hasMoreTokens()) {
if (leaf instanceof ItemGroup) {
node = (ItemGroup<? extends TopLevelItem>)leaf;
} else {
return null;
}
}
}
return leaf;
}
private void includeItems(ItemGroup<? extends TopLevelItem> parent, SortedSet<String> names) {
includeItems(parent, parent, names);
}
private void includeItems(ItemGroup<? extends TopLevelItem> root, ItemGroup<? extends TopLevelItem> parent, SortedSet<String> names) {
if (includePattern != null) {
for (Item item : parent.getItems()) {
if (recurse && item instanceof ItemGroup) {
includeItems(root, (ItemGroup<? extends TopLevelItem>)item, names);
}
String itemName = item.getRelativeNameFrom(root);
if (includePattern.matcher(itemName).matches()) {
names.add(itemName);
}
}
}
}
public synchronized boolean contains(TopLevelItem item) {
return jobNames.contains(item.getName());
if(allJobNames == null) {
getItems();
}
return allJobNames.contains(item.getName());
}
/**
......@@ -193,6 +247,10 @@ public class ListView extends View implements Saveable {
public String getIncludeRegex() {
return includeRegex;
}
public boolean isRecurse() {
return recurse;
}
/**
* Filter by enabled/disabled status of jobs.
......@@ -257,6 +315,7 @@ public class ListView extends View implements Saveable {
*/
@Override
protected void submit(StaplerRequest req) throws ServletException, FormException, IOException {
JSONObject json = req.getSubmittedForm();
synchronized (this) {
jobNames.clear();
for (TopLevelItem item : getOwnerItemGroup().getItems()) {
......@@ -275,16 +334,17 @@ public class ListView extends View implements Saveable {
includeRegex = null;
includePattern = null;
}
recurse = json.optBoolean("recurse", true);
if (columns == null) {
columns = new DescribableList<ListViewColumn,Descriptor<ListViewColumn>>(this);
}
columns.rebuildHetero(req, req.getSubmittedForm(), ListViewColumn.all(), "columns");
columns.rebuildHetero(req, json, ListViewColumn.all(), "columns");
if (jobFilters == null) {
jobFilters = new DescribableList<ViewJobFilter,Descriptor<ViewJobFilter>>(this);
}
jobFilters.rebuildHetero(req, req.getSubmittedForm(), ViewJobFilter.all(), "jobFilters");
jobFilters.rebuildHetero(req, json, ViewJobFilter.all(), "jobFilters");
String filter = Util.fixEmpty(req.getParameter("statusFilter"));
statusFilter = filter != null ? "1".equals(filter) : null;
......
......@@ -1385,24 +1385,7 @@ public class Jenkins extends AbstractCIBase implements ModifiableTopLevelItemGro
* and filter them by the given type.
*/
public <T extends Item> List<T> getAllItems(Class<T> type) {
List<T> r = new ArrayList<T>();
Stack<ItemGroup> q = new Stack<ItemGroup>();
q.push(this);
while(!q.isEmpty()) {
ItemGroup<?> parent = q.pop();
for (Item i : parent.getItems()) {
if(type.isInstance(i)) {
if (i.hasPermission(Item.READ))
r.add(type.cast(i));
}
if(i instanceof ItemGroup)
q.push((ItemGroup)i);
}
}
return r;
return Items.getAllItems(this, type);
}
/**
......
......@@ -40,17 +40,20 @@ THE SOFTWARE.
<f:entry title="${%Jobs}">
<div class="listview-jobs">
<j:forEach var="job" items="${it.ownerItemGroup.items}">
<f:checkbox name="${job.name}" checked="${it.contains(job)}" title="${job.name}" />
<f:checkbox name="${job.getRelativeNameFromGroup(it.ownerItemGroup)}" checked="${it.contains(job)}" title="${job.getRelativeDisplayNameFrom(it.ownerItemGroup)}" />
<br/>
</j:forEach>
</div>
</f:entry>
<f:optionalBlock name="useincluderegex" title="${%Use a regular expression to include jobs into the view}"
checked="${it.includeRegex != null}" help="/help/view-config/includeregex.html">
checked="${it.includeRegex != null}" help="/help/view-config/includeregex.html" inline="true">
<f:entry title="${%Regular expression}">
<f:textbox name="includeRegex" field="includeRegex" />
</f:entry>
<f:entry title="${%Recurse in subfolders}" field="recurse">
<f:checkbox/>
</f:entry>
</f:optionalBlock>
<j:if test="${it.hasJobFilterExtensions()}">
......
......@@ -11,7 +11,7 @@ if (items.isEmpty()) {
}
include(my,"noJob.jelly");
} else {
t.projectView(jobs: items, jobBaseUrl: "", showViewTabs: true, columnExtensions: my.columns, indenter: my.indenter) {
t.projectView(jobs: items, jobBaseUrl: "", showViewTabs: true, columnExtensions: my.columns, indenter: my.indenter, useFullName: true) {
set("views",my.owner.views);
set("currentView",my);
if (my.owner.class == hudson.model.MyViewsProperty.class) {
......
......@@ -25,6 +25,6 @@ THE SOFTWARE.
<?jelly escape-by-default='true'?>
<j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i="jelly:fmt">
<td style="${indenter.getCss(job)}">
<a href="${jobBaseUrl}${job.shortUrl}" class='model-link'> ${useFullName ? job.fullDisplayName : job.displayName}</a>
<a href="${h.getRelativeLinkTo(job)}" class='model-link'> ${useFullName ? job.getRelativeDisplayNameFrom(currentView.owner.itemGroup) : job.displayName}</a>
</td>
</j:jelly>
\ No newline at end of file
......@@ -79,7 +79,7 @@ THE SOFTWARE.
</j:forEach>
<j:forEach var="job" items="${jobs}">
<t:projectViewRow jobBaseUrl="${useFullName ? jobBaseUrl + job.parent.url : jobBaseUrl}"/>
<t:projectViewRow jobBaseUrl="${jobBaseUrl}"/>
</j:forEach>
</table>
<t:iconSize><t:rssBar/></t:iconSize>
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册