package jenkins.model;
import hudson.Functions;
import hudson.Util;
import hudson.model.Action;
import hudson.model.Actionable;
import hudson.model.BallColor;
import hudson.model.Computer;
import hudson.model.Job;
import hudson.model.ModelObject;
import hudson.model.Node;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyException;
import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.XMLOutput;
import org.kohsuke.stapler.HttpResponse;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.WebApp;
import org.kohsuke.stapler.export.Exported;
import org.kohsuke.stapler.export.ExportedBean;
import org.kohsuke.stapler.export.Flavor;
import org.kohsuke.stapler.jelly.JellyClassTearOff;
import org.kohsuke.stapler.jelly.JellyFacet;
import org.xml.sax.helpers.DefaultHandler;
import javax.servlet.ServletException;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* {@link ModelObject} that has context menu in the breadcrumb.
*
* <p>
* When the user is visiting a particular page, all the ancestor objects that has {@link ModelObject}
* appears in the breadcrumbs. Among those which that also implements {@link ModelObjectWithContextMenu}
* shows the drop-down menu for providing quicker access to the actions to those objects.
*
* @author Kohsuke Kawaguchi
* @see ModelObjectWithChildren
*/
public interface ModelObjectWithContextMenu extends ModelObject {
/**
* Generates the context menu.
*
* The typical implementation is {@code return new ContextMenu().from(this,request,response);},
* which implements the default behaviour. See {@link ContextMenu#from(ModelObjectWithContextMenu, StaplerRequest, StaplerResponse)}
* for more details of what it does. This should suit most implementations.
*/
public ContextMenu doContextMenu(StaplerRequest request, StaplerResponse response) throws Exception;
/**
* Data object that represents the context menu.
*
* Via {@link HttpResponse}, this class is capable of converting itself to JSON that <l:breadcrumb/> understands.
*/
@ExportedBean
public class ContextMenu implements HttpResponse {
/**
* The actual contents of the menu.
*/
@Exported(inline=true)
public final List<MenuItem> items = new ArrayList<MenuItem>();
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object o) throws IOException, ServletException {
rsp.serveExposedBean(req,this,Flavor.JSON);
}
public ContextMenu add(String url, String text) {
items.add(new MenuItem(url,null,text));
return this;
}
public ContextMenu addAll(Collection<? extends Action> actions) {
for (Action a : actions)
add(a);
return this;
}
/**
* @see ContextMenuVisibility
*/
public ContextMenu add(Action a) {
if (!Functions.isContextMenuVisible(a)) {
return this;
}
StaplerRequest req = Stapler.getCurrentRequest();
String text = a.getDisplayName();
String base = Functions.getIconFilePath(a);
if (base==null) return this;
String icon = Stapler.getCurrentRequest().getContextPath()+(base.startsWith("images/")?Functions.getResourcePath():"")+'/'+base;
String url = Functions.getActionUrl(req.findAncestor(ModelObject.class).getUrl(),a);
return add(url,icon,text);
}
public ContextMenu add(String url, String icon, String text) {
if (text != null && icon != null && url != null)
items.add(new MenuItem(url,icon,text));
return this;
}
/** @since 1.504 */
public ContextMenu add(String url, String icon, String text, boolean post) {
if (text != null && icon != null && url != null) {
MenuItem item = new MenuItem(url,icon,text);
item.post = post;
items.add(item);
}
return this;
}
/** @since 1.512 */
public ContextMenu add(String url, String icon, String text, boolean post, boolean requiresConfirmation) {
if (text != null && icon != null && url != null) {
MenuItem item = new MenuItem(url,icon,text);
item.post = post;
item.requiresConfirmation = requiresConfirmation;
items.add(item);
}
return this;
}
/**
* Adds a manually constructed {@link MenuItem}
*
* @since 1.513
*/
public ContextMenu add(MenuItem item) {
items.add(item);
return this;
}
/**
* Adds a node
*
* @since 1.513
*/
public ContextMenu add(Node n) {
Computer c = n.toComputer();
return add(new MenuItem()
.withDisplayName(n.getDisplayName())
.withStockIcon((c==null) ? "computer.png" : c.getIcon())
.withContextRelativeUrl(n.getSearchUrl()));
}
/**
* Adds a computer
*
* @since 1.513
*/
public ContextMenu add(Computer c) {
return add(new MenuItem()
.withDisplayName(c.getDisplayName())
.withStockIcon(c.getIcon())
.withContextRelativeUrl(c.getUrl()));
}
/**
* Adds a child item when rendering context menu of its parent.
*
* @since 1.513
*/
public ContextMenu add(Job job) {
return add(new MenuItem()
.withDisplayName(job.getDisplayName())
.withIcon(job.getIconColor())
.withUrl(job.getSearchUrl()));
}
/**
* Default implementation of the context menu generation.
*
* <p>
* This method uses {@code sidepanel.groovy} to run the side panel generation, captures
* the use of <l:task> tags, and then converts those into {@link MenuItem}s. This is
* supposed to make this work with most existing {@link ModelObject}s that follow the standard
* convention.
*
* <p>
* Unconventional {@link ModelObject} implementations that do not use {@code sidepanel.groovy}
* can override {@link ModelObjectWithContextMenu#doContextMenu(StaplerRequest, StaplerResponse)}
* directly to provide alternative semantics.
*/
public ContextMenu from(ModelObjectWithContextMenu self, StaplerRequest request, StaplerResponse response) throws JellyException, IOException {
return from(self,request,response,"sidepanel");
}
public ContextMenu from(ModelObjectWithContextMenu self, StaplerRequest request, StaplerResponse response, String view) throws JellyException, IOException {
WebApp webApp = WebApp.getCurrent();
final Script s = webApp.getMetaClass(self).getTearOff(JellyClassTearOff.class).findScript(view);
if (s!=null) {
JellyFacet facet = webApp.getFacet(JellyFacet.class);
request.setAttribute("taskTags",this); // <l:task> will look for this variable and populate us
request.setAttribute("mode","side-panel");
// run sidepanel but ignore generated HTML
facet.scriptInvoker.invokeScript(request,response,new Script() {
public Script compile() throws JellyException {
return this;
}
public void run(JellyContext context, XMLOutput output) throws JellyTagException {
Functions.initPageVariables(context);
s.run(context,output);
}
},self,new XMLOutput(new DefaultHandler()));
} else
if (self instanceof Actionable) {
// fallback
this.addAll(((Actionable)self).getAllActions());
}
return this;
}
}
/**
* Menu item in {@link ContextMenu}
*/
@ExportedBean
public class MenuItem {
/**
* Target of the link.
*
* This can start with '/', but it must not be a relative URL, since
* you cannot really tell which page this context menu is used.
*/
@Exported
public String url;
/**
* Human readable caption of the menu item. Do not use HTML.
*/
@Exported
public String displayName;
/**
* Optional URL to the icon image. Rendered as 24x24.
*/
@Exported
public String icon;
/**
* True to make a POST request rather than GET.
* @since 1.504
*/
@Exported public boolean post;
/**
* True to require confirmation after a click.
* @since 1.512
*/
@Exported public boolean requiresConfirmation;
/**
* If this is a submenu, definition of subitems.
*/
@Exported(inline=true)
public ContextMenu subMenu;
public MenuItem(String url, String icon, String displayName) {
withUrl(url).withIcon(icon).withDisplayName(displayName);
}
public MenuItem() {
}
public MenuItem withUrl(String url) {
try {
this.url = new URI(Stapler.getCurrentRequest().getRequestURI()).resolve(new URI(url)).toString();
} catch (URISyntaxException x) {
throw new IllegalArgumentException("Bad URI from " + Stapler.getCurrentRequest().getRequestURI() + " vs. " + url, x);
}
return this;
}
/**
* Sets the URL by passing in a URL relative to the context path of Jenkins
*/
public MenuItem withContextRelativeUrl(String url) {
if (!url.startsWith("/")) url = '/'+url;
this.url = Stapler.getCurrentRequest().getContextPath()+url;
return this;
}
public MenuItem withIcon(String icon) {
this.icon = icon;
return this;
}
public MenuItem withIcon(BallColor color) {
return withStockIcon(color.getImage());
}
/**
* Sets the icon from core's stock icon
*
* @param icon
* String like "gear.png" that resolves to 24x24 stock icon in the core
*/
public MenuItem withStockIcon(String icon) {
this.icon = Stapler.getCurrentRequest().getContextPath() + Jenkins.RESOURCE_PATH + "/images/24x24/"+icon;
return this;
}
public MenuItem withDisplayName(String displayName) {
this.displayName = Util.escape(displayName);
return this;
}
public MenuItem withDisplayName(ModelObject o) {
return withDisplayName(o.getDisplayName());
}
}
/**
* Allows an action to decide whether it will be visible in a context menu.
* @since 1.538
*/
interface ContextMenuVisibility extends Action {
/**
* Determines whether to show this action right now.
* Can always return false, for an action which should never be in the context menu;
* or could examine {@link Stapler#getCurrentRequest}.
* @return true to display it, false to hide
* @see ContextMenu#add(Action)
*/
boolean isVisible();
}
}