/*
* The MIT License
*
* Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
* Erik Ramfelt, Seiji Sogabe, Martin Eigenbrodt, Alan Harder
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package hudson.model;
import hudson.Extension;
import hudson.Util;
import hudson.diagnosis.OldDataMonitor;
import hudson.model.Descriptor.FormException;
import hudson.model.listeners.ItemListener;
import hudson.security.ACL;
import hudson.security.ACLContext;
import hudson.util.CaseInsensitiveComparator;
import hudson.util.DescribableList;
import hudson.util.FormValidation;
import hudson.util.HttpResponses;
import hudson.views.ListViewColumn;
import hudson.views.ViewJobFilter;
import java.io.IOException;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;
import javax.annotation.concurrent.GuardedBy;
import javax.servlet.ServletException;
import jenkins.model.Jenkins;
import net.sf.json.JSONObject;
import org.jenkinsci.Symbol;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.DataBoundSetter;
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.
*
* @author Kohsuke Kawaguchi
*/
public class ListView extends View implements DirectlyModifiableView {
/**
* List of job names. This is what gets serialized.
*/
@GuardedBy("this")
/*package*/ /*almost-final*/ SortedSet<String> jobNames = new TreeSet<String>(CaseInsensitiveComparator.INSTANCE);
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;
/**
* Compiled include pattern from the includeRegex string.
*/
private transient Pattern includePattern;
/**
* Filter by enabled/disabled status of jobs.
* Null for no filter, true for enabled-only, false for disabled-only.
*/
private Boolean statusFilter;
@DataBoundConstructor
public ListView(String name) {
super(name);
initColumns();
initJobFilters();
}
public ListView(String name, ViewGroup owner) {
this(name);
this.owner = owner;
}
/**
* Sets the columns of this view.
*/
@DataBoundSetter
public void setColumns(List<ListViewColumn> columns) throws IOException {
this.columns.replaceBy(columns);
}
private Object readResolve() {
if(includeRegex!=null) {
try {
includePattern = Pattern.compile(includeRegex);
} catch (PatternSyntaxException x) {
includeRegex = null;
OldDataMonitor.report(this, Collections.<Throwable>singleton(x));
}
}
if (jobNames == null) {
jobNames = new TreeSet<String>(CaseInsensitiveComparator.INSTANCE);
}
initColumns();
initJobFilters();
return this;
}
protected void initColumns() {
if (columns == null)
columns = new DescribableList<ListViewColumn, Descriptor<ListViewColumn>>(this,ListViewColumn.createDefaultInitialColumnList());
}
protected void initJobFilters() {
if (jobFilters == null)
jobFilters = new DescribableList<ViewJobFilter, Descriptor<ViewJobFilter>>(this);
}
/**
* Used to determine if we want to display the Add button.
*/
public boolean hasJobFilterExtensions() {
return !ViewJobFilter.all().isEmpty();
}
public DescribableList<ViewJobFilter, Descriptor<ViewJobFilter>> getJobFilters() {
return jobFilters;
}
@Override
public DescribableList<ListViewColumn, Descriptor<ListViewColumn>> getColumns() {
return columns;
}
/**
* Returns a read-only view of all {@link Job}s in this view.
*
* <p>
* This method returns a separate copy each time to avoid
* concurrent modification issue.
*/
@Override
public List<TopLevelItem> getItems() {
SortedSet<String> names;
List<TopLevelItem> items = new ArrayList<TopLevelItem>();
synchronized (this) {
names = new TreeSet<String>(jobNames);
}
ItemGroup<? extends TopLevelItem> parent = getOwnerItemGroup();
List<TopLevelItem> parentItems = new ArrayList<TopLevelItem>(parent.getItems());
includeItems(parent, parentItems, names);
Boolean statusFilter = this.statusFilter; // capture the value to isolate us from concurrent update
Iterable<? extends TopLevelItem> candidates;
if (recurse) {
candidates = Items.getAllItems(parent, TopLevelItem.class);
} else {
candidates = parent.getItems();
}
for (TopLevelItem item : candidates) {
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)
items.add(item);
}
// check the filters
Iterable<ViewJobFilter> jobFilters = getJobFilters();
List<TopLevelItem> allItems = new ArrayList<TopLevelItem>(parentItems);
if (recurse) allItems = expand(allItems, new ArrayList<TopLevelItem>());
for (ViewJobFilter jobFilter: jobFilters) {
items = jobFilter.filter(items, allItems, this);
}
// for sanity, trim off duplicates
items = new ArrayList<TopLevelItem>(new LinkedHashSet<TopLevelItem>(items));
return items;
}
private List<TopLevelItem> expand(Collection<TopLevelItem> items, List<TopLevelItem> allItems) {
for (TopLevelItem item : items) {
if (item instanceof ItemGroup) {
ItemGroup<? extends Item> ig = (ItemGroup<? extends Item>) item;
expand(Util.filter(ig.getItems(), TopLevelItem.class), allItems);
}
allItems.add(item);
}
return allItems;
}
@Override
public boolean contains(TopLevelItem item) {
return getItems().contains(item);
}
private void includeItems(ItemGroup<? extends TopLevelItem> root, Collection<? extends Item> parentItems, SortedSet<String> names) {
if (includePattern != null) {
for (Item item : parentItems) {
if (recurse && item instanceof ItemGroup) {
ItemGroup<?> ig = (ItemGroup<?>) item;
includeItems(root, ig.getItems(), names);
}
if (item instanceof TopLevelItem) {
String itemName = item.getRelativeNameFrom(root);
if (includePattern.matcher(itemName).matches()) {
names.add(itemName);
}
}
}
}
}
public synchronized boolean jobNamesContains(TopLevelItem item) {
if (item == null) return false;
return jobNames.contains(item.getRelativeNameFrom(getOwnerItemGroup()));
}
/**
* Adds the given item to this view.
*
* @since 1.389
*/
@Override
public void add(TopLevelItem item) throws IOException {
synchronized (this) {
jobNames.add(item.getRelativeNameFrom(getOwnerItemGroup()));
}
save();
}
/**
* Removes given item from this view.
*
* @since 1.566
*/
@Override
public boolean remove(TopLevelItem item) throws IOException {
synchronized (this) {
String name = item.getRelativeNameFrom(getOwnerItemGroup());
if (!jobNames.remove(name)) return false;
}
save();
return true;
}
public String getIncludeRegex() {
return includeRegex;
}
public boolean isRecurse() {
return recurse;
}
/**
* @since 1.568
*/
public void setRecurse(boolean recurse) {
this.recurse = recurse;
}
/**
* Filter by enabled/disabled status of jobs.
* Null for no filter, true for enabled-only, false for disabled-only.
*/
public Boolean getStatusFilter() {
return statusFilter;
}
@Override
@RequirePOST
public Item doCreateItem(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
ItemGroup<? extends TopLevelItem> ig = getOwnerItemGroup();
if (ig instanceof ModifiableItemGroup) {
TopLevelItem item = ((ModifiableItemGroup<? extends TopLevelItem>)ig).doCreateItem(req, rsp);
if(item!=null) {
synchronized (this) {
jobNames.add(item.getRelativeNameFrom(getOwnerItemGroup()));
}
owner.save();
}
return item;
}
return null;
}
@Override
@RequirePOST
public HttpResponse doAddJobToView(@QueryParameter String name) throws IOException, ServletException {
checkPermission(View.CONFIGURE);
if(name==null)
throw new Failure("Query parameter 'name' is required");
TopLevelItem item = resolveName(name);
if (item == null)
throw new Failure("Query parameter 'name' does not correspond to a known item");
if (contains(item)) return HttpResponses.ok();
add(item);
owner.save();
return HttpResponses.ok();
}
@Override
@RequirePOST
public HttpResponse doRemoveJobFromView(@QueryParameter String name) throws IOException, ServletException {
checkPermission(View.CONFIGURE);
if(name==null)
throw new Failure("Query parameter 'name' is required");
TopLevelItem item = resolveName(name);
if (remove(item))
owner.save();
return HttpResponses.ok();
}
private TopLevelItem resolveName(String name) {
TopLevelItem item = getOwnerItemGroup().getItem(name);
if (item == null) {
name = Items.getCanonicalName(getOwnerItemGroup(), name);
item = Jenkins.getInstance().getItemByFullName(name, TopLevelItem.class);
}
return item;
}
/**
* Handles the configuration submission.
*
* Load view-specific properties here.
*/
@Override
protected void submit(StaplerRequest req) throws ServletException, FormException, IOException {
JSONObject json = req.getSubmittedForm();
synchronized (this) {
recurse = json.optBoolean("recurse", true);
jobNames.clear();
Iterable<? extends TopLevelItem> items;
if (recurse) {
items = Items.getAllItems(getOwnerItemGroup(), TopLevelItem.class);
} else {
items = getOwnerItemGroup().getItems();
}
for (TopLevelItem item : items) {
String relativeNameFrom = item.getRelativeNameFrom(getOwnerItemGroup());
if(req.getParameter(relativeNameFrom)!=null) {
jobNames.add(relativeNameFrom);
}
}
}
setIncludeRegex(req.getParameter("useincluderegex") != null ? req.getParameter("includeRegex") : null);
if (columns == null) {
columns = new DescribableList<ListViewColumn,Descriptor<ListViewColumn>>(this);
}
columns.rebuildHetero(req, json, ListViewColumn.all(), "columns");
if (jobFilters == null) {
jobFilters = new DescribableList<ViewJobFilter,Descriptor<ViewJobFilter>>(this);
}
jobFilters.rebuildHetero(req, json, ViewJobFilter.all(), "jobFilters");
String filter = Util.fixEmpty(req.getParameter("statusFilter"));
statusFilter = filter != null ? "1".equals(filter) : null;
}
/** @since 1.526 */
public void setIncludeRegex(String includeRegex) {
this.includeRegex = Util.nullify(includeRegex);
if (this.includeRegex == null)
this.includePattern = null;
else
this.includePattern = Pattern.compile(includeRegex);
}
@Extension @Symbol("list")
public static class DescriptorImpl extends ViewDescriptor {
@Override
public String getDisplayName() {
return Messages.ListView_DisplayName();
}
/**
* Checks if the include regular expression is valid.
*/
public FormValidation doCheckIncludeRegex( @QueryParameter String value ) throws IOException, ServletException, InterruptedException {
String v = Util.fixEmpty(value);
if (v != null) {
try {
Pattern.compile(v);
} catch (PatternSyntaxException pse) {
return FormValidation.error(pse.getMessage());
}
}
return FormValidation.ok();
}
}
/**
* @deprecated as of 1.391
* Use {@link ListViewColumn#createDefaultInitialColumnList()}
*/
@Deprecated
public static List<ListViewColumn> getDefaultColumns() {
return ListViewColumn.createDefaultInitialColumnList();
}
@Restricted(NoExternalUse.class)
@Extension
public static final class Listener extends ItemListener {
@Override
public void onLocationChanged(final Item item, final String oldFullName, final String newFullName) {
try (ACLContext _ = ACL.as(ACL.SYSTEM)) {
locationChanged(item, oldFullName, newFullName);
}
}
private void locationChanged(Item item, String oldFullName, String newFullName) {
final Jenkins jenkins = Jenkins.getInstance();
for (View view: jenkins.getViews()) {
if (view instanceof ListView) {
renameViewItem(oldFullName, newFullName, jenkins, (ListView) view);
}
}
for (Item g : jenkins.getAllItems()) {
if (g instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) g;
for (View v : vg.getViews()) {
if (v instanceof ListView) {
renameViewItem(oldFullName, newFullName, vg, (ListView) v);
}
}
}
}
}
private void renameViewItem(String oldFullName, String newFullName, ViewGroup vg, ListView lv) {
boolean needsSave;
synchronized (lv) {
Set<String> oldJobNames = new HashSet<String>(lv.jobNames);
lv.jobNames.clear();
for (String oldName : oldJobNames) {
lv.jobNames.add(Items.computeRelativeNamesAfterRenaming(oldFullName, newFullName, oldName, vg.getItemGroup()));
}
needsSave = !oldJobNames.equals(lv.jobNames);
}
if (needsSave) { // do not hold ListView lock at the time
try {
lv.save();
} catch (IOException x) {
Logger.getLogger(ListView.class.getName()).log(Level.WARNING, null, x);
}
}
}
@Override
public void onDeleted(final Item item) {
try (ACLContext _ = ACL.as(ACL.SYSTEM)) {
deleted(item);
}
}
private void deleted(Item item) {
final Jenkins jenkins = Jenkins.getInstance();
for (View view: jenkins.getViews()) {
if (view instanceof ListView) {
deleteViewItem(item, jenkins, (ListView) view);
}
}
for (Item g : jenkins.getAllItems()) {
if (g instanceof ViewGroup) {
ViewGroup vg = (ViewGroup) g;
for (View v : vg.getViews()) {
if (v instanceof ListView) {
deleteViewItem(item, vg, (ListView) v);
}
}
}
}
}
private void deleteViewItem(Item item, ViewGroup vg, ListView lv) {
boolean needsSave;
synchronized (lv) {
needsSave = lv.jobNames.remove(item.getRelativeNameFrom(vg.getItemGroup()));
}
if (needsSave) {
try {
lv.save();
} catch (IOException x) {
Logger.getLogger(ListView.class.getName()).log(Level.WARNING, null, x);
}
}
}
}
}