/**
* *****************************************************************************
*
* Copyright (c) 2004-2012 Oracle Corporation.
*
* All rights reserved. This program and the accompanying materials are made
* available under the terms of the Eclipse Public License v1.0 which
* accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*
* Kohsuke Kawaguchi, Winston Prakash, Stephen Connolly, Tom Huybrechts, Alan
* Harder, Romain Seguy
*
*
******************************************************************************
*/
package hudson;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import hudson.console.ConsoleAnnotationDescriptor;
import hudson.console.ConsoleAnnotatorFactory;
import hudson.model.AbstractProject;
import hudson.model.Action;
import hudson.model.BallColor;
import hudson.model.Describable;
import hudson.model.Descriptor;
import hudson.model.DescriptorVisibilityFilter;
import hudson.model.Hudson;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.Items;
import hudson.model.JDK;
import hudson.model.Job;
import hudson.model.JobPropertyDescriptor;
import hudson.model.ModelObject;
import hudson.model.Node;
import hudson.model.PageDecorator;
import hudson.model.ParameterDefinition;
import hudson.model.ParameterDefinition.ParameterDescriptor;
import hudson.model.Project;
import hudson.model.Run;
import hudson.model.TopLevelItem;
import hudson.model.User;
import hudson.model.View;
import hudson.scm.SCM;
import hudson.scm.SCMDescriptor;
import hudson.search.SearchableModelObject;
import hudson.security.AccessControlled;
import hudson.security.AuthorizationStrategy;
import hudson.security.Permission;
import hudson.security.SecurityRealm;
import hudson.security.csrf.CrumbIssuer;
import hudson.slaves.Cloud;
import hudson.slaves.ComputerLauncher;
import hudson.slaves.NodeProperty;
import hudson.slaves.NodePropertyDescriptor;
import hudson.slaves.RetentionStrategy;
import hudson.tasks.BuildStepDescriptor;
import hudson.tasks.BuildWrapper;
import hudson.tasks.BuildWrappers;
import hudson.tasks.Builder;
import hudson.tasks.Publisher;
import hudson.util.Area;
import hudson.util.Iterators;
import hudson.util.Secret;
import hudson.views.MyViewsTabBar;
import hudson.views.ViewsTabBar;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.UnsupportedEncodingException;
import java.lang.management.LockInfo;
import java.lang.management.ManagementFactory;
import java.lang.management.MonitorInfo;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.ConcurrentModificationException;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.SimpleFormatter;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.apache.commons.jelly.JellyContext;
import org.apache.commons.jelly.JellyTagException;
import org.apache.commons.jelly.Script;
import org.apache.commons.jelly.XMLOutput;
import org.apache.commons.jexl.parser.ASTSizeFunction;
import org.apache.commons.jexl.util.Introspector;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.hudson.script.ScriptSupport;
import org.eclipse.hudson.security.HudsonSecurityManager;
import org.eclipse.hudson.security.captcha.CaptchaSupport;
import org.jvnet.tiger_types.Types;
import org.kohsuke.stapler.Ancestor;
import org.kohsuke.stapler.Stapler;
import org.kohsuke.stapler.StaplerProxy;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.StaplerResponse;
import org.kohsuke.stapler.jelly.InternationalizedStringExpression;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
/**
* Utility functions used in views.
*
* <p> An instance of this class is created for each request and made accessible
* from view pages via the variable 'h' (h stands for Hudson.)
*
* @author Kohsuke Kawaguchi
*/
public class Functions {
private static volatile int globalIota = 0;
private int iota;
public Functions() {
iota = globalIota;
// concurrent requests can use the same ID --- we are just trying to
// prevent the same user from seeing the same ID repeatedly.
globalIota += 1000;
}
/**
* Generates an unique ID.
*/
public String generateId() {
return "id" + iota++;
}
public static boolean isModel(Object o) {
return o instanceof ModelObject;
}
public static String xsDate(Calendar cal) {
return Util.XS_DATETIME_FORMATTER.format(cal.getTime());
}
public static String rfc822Date(Calendar cal) {
return Util.RFC822_DATETIME_FORMATTER.format(cal.getTime());
}
/**
* Given {@code c=MyList (extends ArrayList<Foo>), base=List}, compute the
* parameterization of 'base' that's assignable from 'c' (in this case
* {@code List<Foo>}), and return its n-th type parameter (n=0 would return
* {@code Foo}).
*
* <p> This method is useful for doing type arithmetic.
*
* @throws AssertionError if c' is not parameterized.
*/
public static <B> Class getTypeParameter(Class<? extends B> c, Class<B> base, int n) {
Type parameterization = Types.getBaseClass(c, base);
if (parameterization instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) parameterization;
return Types.erasure(Types.getTypeArgument(pt, n));
} else {
throw new AssertionError(c + " doesn't properly parameterize " + base);
}
}
public JDK.DescriptorImpl getJDKDescriptor() {
return Hudson.getInstance().getDescriptorByType(JDK.DescriptorImpl.class);
}
/**
* Prints the integer as a string that represents difference, like "-5",
* "+/-0", "+3".
*/
public static String getDiffString(int i) {
if (i == 0) {
return "\u00B10"; // +/-0
}
String s = Integer.toString(i);
if (i > 0) {
return "+" + s;
} else {
return s;
}
}
/**
* {@link #getDiffString(int)} that doesn't show anything for +/-0
*/
public static String getDiffString2(int i) {
if (i == 0) {
return "";
}
String s = Integer.toString(i);
if (i > 0) {
return "+" + s;
} else {
return s;
}
}
/**
* {@link #getDiffString2(int)} that puts the result into prefix and suffix
* if there's something to print
*/
public static String getDiffString2(String prefix, int i, String suffix) {
if (i == 0) {
return "";
}
String s = Integer.toString(i);
if (i > 0) {
return prefix + "+" + s + suffix;
} else {
return prefix + s + suffix;
}
}
/**
* Adds the proper suffix.
*/
public static String addSuffix(int n, String singular, String plural) {
StringBuilder buf = new StringBuilder();
buf.append(n).append(' ');
if (n == 1) {
buf.append(singular);
} else {
buf.append(plural);
}
return buf.toString();
}
public static RunUrl decompose(StaplerRequest req) {
List<Ancestor> ancestors = req.getAncestors();
// find the first and last Run instances
Ancestor f = null, l = null;
for (Ancestor anc : ancestors) {
if (anc.getObject() instanceof Run) {
if (f == null) {
f = anc;
}
l = anc;
}
}
if (l == null) {
return null; // there was no Run object
}
String head = getAncestorUrl(req, f.getPrev()) + '/';
String base = getAncestorUrl(req, l);
String reqUri = req.getOriginalRequestURI();
// Find "rest" or URI by removing N path components.
// Not using reqUri.substring(f.getUrl().length()) to avoid mismatches due to
// url-encoding or extra slashes. Former may occur in Tomcat (despite the spec saying
// this string is not decoded, Tomcat apparently decodes this string. You see ' '
// instead of '%20', which is what the browser has sent), latter may occur in some
// proxy or URL-rewriting setups where extra slashes are inadvertently added.
String furl = getAncestorUrl(req, f);
int slashCount = 0;
// Count components in ancestor URL
for (int i = furl.indexOf('/'); i >= 0; i = furl.indexOf('/', i + 1)) {
slashCount++;
}
// Remove that many from request URL, ignoring extra slashes
String rest = reqUri.replaceFirst("(?:/+[^/]*){" + slashCount + "}", "");
return new RunUrl((Run) f.getObject(), head, base, rest);
}
/**
* If we know the user's screen resolution, return it. Otherwise null.
*
* @since 1.213
*/
public static Area getScreenResolution() {
Cookie res = Functions.getCookie(Stapler.getCurrentRequest(), "screenResolution");
if (res != null) {
return Area.parse(res.getValue());
}
return null;
}
/**
* URL decomposed for easier computation of relevant URLs.
*
* <p> The decomposed URL will be of the form:
* <pre>
* aaaaaa/524/bbbbb/cccc
* -head-| N |---rest---
* ----- base -----|
* </pre>
*
* <p> The head portion is the part of the URL from the {@link Hudson}
* object to the first {@link Run} subtype. When "next/prev build" is
* chosen, this part remains intact.
*
* <p> The <tt>524</tt> is the path from {@link Job} to {@link Run}.
*
* <p> The <tt>bbb</tt> portion is the path after that till the last
* {@link Run} subtype. The <tt>ccc</tt> portion is the part after that.
*/
public static final class RunUrl {
private final String head, base, rest;
private final Run run;
public RunUrl(Run run, String head, String base, String rest) {
this.run = run;
this.head = head;
this.base = base;
this.rest = rest;
}
public String getBaseUrl() {
return base;
}
/**
* Returns the same page in the next build.
*/
public String getNextBuildUrl() {
return getUrl(run.getNextBuild());
}
/**
* Returns the same page in the previous build.
*/
public String getPreviousBuildUrl() {
return getUrl(run.getPreviousBuild());
}
private String getUrl(Run n) {
if (n == null) {
return null;
} else {
return head + n.getNumber() + rest;
}
}
}
public static Node.Mode[] getNodeModes() {
return Node.Mode.values();
}
public static String getProjectListString(List<Project> projects) {
return Items.toNameList(projects);
}
/**
* @deprecated as of 1.294 JEXL now supports the real ternary operator
* "x?y:z", so this work around is no longer necessary.
*/
public static Object ifThenElse(boolean cond, Object thenValue, Object elseValue) {
return cond ? thenValue : elseValue;
}
public static String appendIfNotNull(String text, String suffix, String nullText) {
return text == null ? nullText : text + suffix;
}
public static Map getSystemProperties() {
return new TreeMap<Object, Object>(System.getProperties());
}
public static Map getEnvVars() {
return new TreeMap<String, String>(EnvVars.masterEnvVars);
}
public static boolean isWindows() {
return File.pathSeparatorChar == ';';
}
public static List<LogRecord> getLogRecords() {
return Hudson.logRecords;
}
public static String printLogRecord(LogRecord r) {
return formatter.format(r);
}
public static Cookie getCookie(HttpServletRequest req, String name) {
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(name)) {
return cookie;
}
}
}
return null;
}
public static String getCookie(HttpServletRequest req, String name, String defaultValue) {
Cookie c = getCookie(req, name);
if (c == null || c.getValue() == null) {
return defaultValue;
}
return c.getValue();
}
/**
* Gets the suffix to use for YUI JavaScript.
*/
public static String getYuiSuffix() {
return DEBUG_YUI ? "debug" : "min";
}
/**
* Set to true if you need to use the debug version of YUI.
*/
public static boolean DEBUG_YUI = Boolean.getBoolean("debug.YUI");
/**
* Creates a sub map by using the given range (both ends inclusive).
*/
public static <V> SortedMap<Integer, V> filter(SortedMap<Integer, V> map, String from, String to) {
if (from == null && to == null) {
return map;
}
if (to == null) {
return map.headMap(Integer.parseInt(from) - 1);
}
if (from == null) {
return map.tailMap(Integer.parseInt(to));
}
return map.subMap(Integer.parseInt(to), Integer.parseInt(from) - 1);
}
private static final SimpleFormatter formatter = new SimpleFormatter();
/**
* Used by <tt>layout.jelly</tt> to control the auto refresh behavior.
*
* @param noAutoRefresh On certain pages, like a page with forms, will have
* annoying interference with auto refresh. On those pages, disable
* auto-refresh.
*/
public static void configureAutoRefresh(HttpServletRequest request, HttpServletResponse response, boolean noAutoRefresh) {
if (noAutoRefresh) {
return;
}
String param = request.getParameter("auto_refresh");
boolean refresh = isAutoRefresh(request);
if (param != null) {
refresh = Boolean.parseBoolean(param);
Cookie c = new Cookie("hudson_auto_refresh", Boolean.toString(refresh));
// Need to set path or it will not stick from e.g. a project page to the dashboard.
// Using request.getContextPath() might work but it seems simpler to just use the hudson_ prefix
// to avoid conflicts with any other web apps that might be on the same machine.
c.setPath("/");
c.setMaxAge(60 * 60 * 24 * 30); // persist it roughly for a month
response.addCookie(c);
}
if (refresh) {
response.addHeader("Refresh", System.getProperty("hudson.Functions.autoRefreshSeconds", "10"));
}
}
public static boolean isAutoRefresh(HttpServletRequest request) {
String param = request.getParameter("auto_refresh");
if (param != null) {
return Boolean.parseBoolean(param);
}
Cookie[] cookies = request.getCookies();
if (cookies == null) {
return false; // when API design messes it up, we all suffer
}
for (Cookie c : cookies) {
if (c.getName().equals("hudson_auto_refresh")) {
return Boolean.parseBoolean(c.getValue());
}
}
return false;
}
/**
* Finds the given object in the ancestor list and returns its URL. This is
* used to determine the "current" URL assigned to the given object, so that
* one can compute relative URLs from it.
*/
public static String getNearestAncestorUrl(StaplerRequest req, Object it) {
List list = req.getAncestors();
for (int i = list.size() - 1; i >= 0; i--) {
Ancestor anc = (Ancestor) list.get(i);
if (anc.getObject() == it) {
String ancUrl = anc.getUrl();
String reqRootPath = Functions.getRequestRootPath(req);
return ancUrl.replaceFirst(req.getContextPath(), reqRootPath);
}
}
return null;
}
public static String getAncestorUrl(StaplerRequest req, Ancestor anc) {
if (anc != null) {
String ancUrl = anc.getUrl();
String reqRootPath = Functions.getRequestRootPath(req);
return ancUrl.replaceFirst(req.getContextPath(), reqRootPath);
}
return "";
}
/**
* Finds the inner-most {@link SearchableModelObject} in scope.
*/
public static String getSearchURL() {
List list = Stapler.getCurrentRequest().getAncestors();
for (int i = list.size() - 1; i >= 0; i--) {
Ancestor anc = (Ancestor) list.get(i);
if (anc.getObject() instanceof SearchableModelObject) {
return anc.getUrl() + "/search/";
}
}
return null;
}
public static String appendSpaceIfNotNull(String n) {
if (n == null) {
return null;
} else {
return n + ' ';
}
}
/**
* One nbsp per 10 pixels in given size, which may be a plain number or
* "NxN" (like an iconSize). Useful in a sortable table heading.
*/
public static String nbspIndent(String size) {
int i = size.indexOf('x');
i = Integer.parseInt(i > 0 ? size.substring(0, i) : size) / 10;
StringBuilder buf = new StringBuilder(30);
for (int j = 0; j < i; j++) {
buf.append(" ");
}
return buf.toString();
}
public static String getWin32ErrorMessage(IOException e) {
return Util.getWin32ErrorMessage(e);
}
public static boolean isMultiline(String s) {
if (s == null) {
return false;
}
return s.indexOf('\r') >= 0 || s.indexOf('\n') >= 0;
}
public static String encode(String s) {
return Util.encode(s);
}
public static String escape(String s) {
return Util.escape(s);
}
public static String xmlEscape(String s) {
return Util.xmlEscape(s);
}
public static String xmlUnescape(String s) {
return s.replace("<", "<").replace(">", ">").replace("&", "&");
}
public static void checkPermission(Permission permission) throws IOException, ServletException {
checkPermission(Hudson.getInstance(), permission);
}
public static void checkPermission(AccessControlled object, Permission permission) throws IOException, ServletException {
if (permission != null) {
object.checkPermission(permission);
}
}
/**
* This version is so that the 'checkPermission' on <tt>layout.jelly</tt>
* degrades gracefully if "it" is not an {@link AccessControlled} object.
* Otherwise it will perform no check and that problem is hard to notice.
*/
public static void checkPermission(Object object, Permission permission) throws IOException, ServletException {
if (permission == null) {
return;
}
if (object instanceof AccessControlled) {
checkPermission((AccessControlled) object, permission);
} else {
List<Ancestor> ancs = Stapler.getCurrentRequest().getAncestors();
for (Ancestor anc : Iterators.reverse(ancs)) {
Object o = anc.getObject();
if (o instanceof AccessControlled) {
checkPermission((AccessControlled) o, permission);
return;
}
}
checkPermission(Hudson.getInstance(), permission);
}
}
/**
* Returns true if the current user has the given permission.
*
* @param permission If null, returns true. This defaulting is convenient in
* making the use of this method terse.
*/
public static boolean hasPermission(Permission permission) throws IOException, ServletException {
return hasPermission(Hudson.getInstance(), permission);
}
/**
* This version is so that the 'hasPermission' can degrade gracefully if
* "it" is not an {@link AccessControlled} object.
*/
public static boolean hasPermission(Object object, Permission permission) throws IOException, ServletException {
if (permission == null) {
return true;
}
if (object instanceof AccessControlled) {
return ((AccessControlled) object).hasPermission(permission);
} else {
List<Ancestor> ancs = Stapler.getCurrentRequest().getAncestors();
for (Ancestor anc : Iterators.reverse(ancs)) {
Object o = anc.getObject();
if (o instanceof AccessControlled) {
return ((AccessControlled) o).hasPermission(permission);
}
}
return Hudson.getInstance().hasPermission(permission);
}
}
public static void adminCheck(StaplerRequest req, StaplerResponse rsp, Object required, Permission permission) throws IOException, ServletException {
// this is legacy --- all views should be eventually converted to
// the permission based model.
if (required != null && !Hudson.adminCheck(req, rsp)) {
// check failed. commit the FORBIDDEN response, then abort.
rsp.setStatus(HttpServletResponse.SC_FORBIDDEN);
rsp.getOutputStream().close();
throw new ServletException("Unauthorized access");
}
// make sure the user owns the necessary permission to access this page.
if (permission != null) {
checkPermission(permission);
}
}
/**
* Infers the hudson installation URL from the given request.
*/
public static String inferHudsonURL(StaplerRequest req) {
String rootUrl = Hudson.getInstance().getRootUrl();
if (rootUrl != null) // prefer the one explicitly configured, to work with load-balancer, frontend, etc.
{
return rootUrl;
}
StringBuilder buf = new StringBuilder();
buf.append(req.getScheme()).append("://");
buf.append(req.getServerName());
if (req.getLocalPort() != 80) {
buf.append(':').append(req.getLocalPort());
}
buf.append(getRequestRootPath(req)).append('/');
return buf.toString();
}
public static List<JobPropertyDescriptor> getJobPropertyDescriptors(Class<? extends Job> clazz) {
return JobPropertyDescriptor.getPropertyDescriptors(clazz);
}
public static List<Descriptor<BuildWrapper>> getBuildWrapperDescriptors(AbstractProject<?, ?> project) {
return BuildWrappers.getFor(project);
}
public static List<Descriptor<SecurityRealm>> getSecurityRealmDescriptors() {
return SecurityRealm.all();
}
public static List<Descriptor<AuthorizationStrategy>> getAuthorizationStrategyDescriptors() {
return AuthorizationStrategy.all();
}
public static List<Descriptor<Builder>> getBuilderDescriptors(AbstractProject<?, ?> project) {
return BuildStepDescriptor.filter(Builder.all(), project.getClass());
}
public static List<Descriptor<Publisher>> getPublisherDescriptors(AbstractProject<?, ?> project) {
return BuildStepDescriptor.filter(Publisher.all(), project.getClass());
}
public static List<SCMDescriptor<?>> getSCMDescriptors(AbstractProject<?, ?> project) {
return SCM._for(project);
}
public static List<Descriptor<ComputerLauncher>> getComputerLauncherDescriptors() {
return Hudson.getInstance().<ComputerLauncher, Descriptor<ComputerLauncher>>getDescriptorList(ComputerLauncher.class);
}
public static List<Descriptor<RetentionStrategy<?>>> getRetentionStrategyDescriptors() {
return RetentionStrategy.all();
}
public static List<ParameterDescriptor> getParameterDescriptors() {
return ParameterDefinition.all();
}
public static List<Descriptor<ViewsTabBar>> getViewsTabBarDescriptors() {
return ViewsTabBar.all();
}
public static List<Descriptor<CaptchaSupport>> getCaptchaSupportDescriptors() {
return CaptchaSupport.all();
}
public static List<Descriptor<MyViewsTabBar>> getMyViewsTabBarDescriptors() {
return MyViewsTabBar.all();
}
public static List<Descriptor<ScriptSupport>> getScriptSupportDescriptors() {
return ScriptSupport.all();
}
public static List<NodePropertyDescriptor> getNodePropertyDescriptors(Class<? extends Node> clazz) {
List<NodePropertyDescriptor> result = new ArrayList<NodePropertyDescriptor>();
Collection<NodePropertyDescriptor> list = (Collection) Hudson.getInstance().getDescriptorList(NodeProperty.class);
for (NodePropertyDescriptor npd : list) {
if (npd.isApplicable(clazz)) {
result.add(npd);
}
}
return result;
}
private static final Set<String> globalConfigIgnoredDescriptors = new HashSet<String>();
/**
* Set of descriptor class names to ignore in the global /configure page.
*
* @since 2.1.0
*/
public static Set<String> getGlobalConfigIgnoredDescriptors() {
return globalConfigIgnoredDescriptors;
}
/**
* Gets all the descriptors sorted by their inheritance tree of
* {@link Describable} so that descriptors of similar types come nearby.
*/
public static Collection<Descriptor> getSortedDescriptorsForGlobalConfig() {
Map<String, Descriptor> r = new TreeMap<String, Descriptor>();
for (Descriptor<?> d : Hudson.getInstance().getExtensionList(Descriptor.class)) {
if (globalConfigIgnoredDescriptors.contains(d.getClass().getName())) {
continue;
}
if (d.getGlobalConfigPage() == null) {
continue;
}
r.put(buildSuperclassHierarchy(d.clazz, new StringBuilder()).toString(), d);
}
return r.values();
}
private static StringBuilder buildSuperclassHierarchy(Class c, StringBuilder buf) {
Class sc = c.getSuperclass();
if (sc != null) {
buildSuperclassHierarchy(sc, buf).append(':');
}
return buf.append(c.getName());
}
public static String getRequestRootPath() {
return getRequestRootPath(null);
}
public static String getRequestRootPath(StaplerRequest req) {
if (req == null) {
req = Stapler.getCurrentRequest();
}
return getHttpRequestRootPath(req);
}
private static RequestRootPathProvider requestRootPathProvider = new DefaultRequestRootPathProvider();
public static void setRequestRootPathProvider(RequestRootPathProvider requestRootPathProvider) {
Functions.requestRootPathProvider = requestRootPathProvider;
}
public static String getHttpRequestRootPath(HttpServletRequest req) {
return requestRootPathProvider.getRootPath(req);
}
/**
* Computes the path to the icon of the given action from the context path.
*/
public static String getIconFilePath(Action a) {
String name = a.getIconFileName();
if (name.startsWith("/")) {
return name.substring(1);
} else {
return "images/24x24/" + name;
}
}
/**
* Works like JSTL build-in size(x) function, but handle null gracefully.
*/
public static int size2(Object o) throws Exception {
if (o == null) {
return 0;
}
return ASTSizeFunction.sizeOf(o, Introspector.getUberspect());
}
/**
* Computes the relative path from the current page to the given item.
*/
public static String getRelativeLinkTo(Item p) {
Map<Object, String> ancestors = new HashMap<Object, String>();
View view = null;
StaplerRequest request = Stapler.getCurrentRequest();
for (Ancestor a : request.getAncestors()) {
ancestors.put(a.getObject(), a.getRelativePath());
if (a.getObject() instanceof View) {
view = (View) a.getObject();
}
}
String path = ancestors.get(p);
if (path != null) {
return path;
}
Item i = p;
String url = "";
while (true) {
ItemGroup ig = i.getParent();
url = i.getShortUrl() + url;
if (ig == Hudson.getInstance()) {
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
return ancestors.get(view) + '/' + url;
} else {
// otherwise return a path from the root Hudson
return getRequestRootPath(request) + '/' + p.getUrl();
}
}
path = ancestors.get(ig);
if (path != null) {
return path + '/' + url;
}
assert ig instanceof Item; // if not, ig must have been the Hudson instance
i = (Item) ig;
}
}
public static Map<Thread, StackTraceElement[]> dumpAllThreads() {
Map<Thread, StackTraceElement[]> sorted = new TreeMap<Thread, StackTraceElement[]>(new ThreadSorter());
sorted.putAll(Thread.getAllStackTraces());
return sorted;
}
public static ThreadInfo[] getThreadInfos() {
ThreadMXBean mbean = ManagementFactory.getThreadMXBean();
return mbean.dumpAllThreads(mbean.isObjectMonitorUsageSupported(), mbean.isSynchronizerUsageSupported());
}
public static ThreadGroupMap sortThreadsAndGetGroupMap(ThreadInfo[] list) {
ThreadGroupMap sorter = new ThreadGroupMap();
Arrays.sort(list, sorter);
return sorter;
}
// Common code for sorting Threads/ThreadInfos by ThreadGroup
private static class ThreadSorterBase {
protected Map<Long, String> map = new HashMap<Long, String>();
private ThreadSorterBase() {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
while (tg.getParent() != null) {
tg = tg.getParent();
}
Thread[] threads = new Thread[tg.activeCount() * 2];
int threadsLen = tg.enumerate(threads, true);
for (int i = 0; i < threadsLen; i++) {
map.put(threads[i].getId(), threads[i].getThreadGroup().getName());
}
}
protected int compare(long idA, long idB) {
String tga = map.get(idA), tgb = map.get(idB);
int result = (tga != null ? -1 : 0) + (tgb != null ? 1 : 0); // Will be non-zero if only one is null
if (result == 0 && tga != null) {
result = tga.compareToIgnoreCase(tgb);
}
return result;
}
}
public static class ThreadGroupMap extends ThreadSorterBase implements Comparator<ThreadInfo> {
/**
* @return ThreadGroup name or null if unknown
*/
public String getThreadGroup(ThreadInfo ti) {
return map.get(ti.getThreadId());
}
@Override
public int compare(ThreadInfo a, ThreadInfo b) {
int result = compare(a.getThreadId(), b.getThreadId());
if (result == 0) {
result = a.getThreadName().compareToIgnoreCase(b.getThreadName());
}
return result;
}
}
private static class ThreadSorter extends ThreadSorterBase implements Comparator<Thread> {
@Override
public int compare(Thread a, Thread b) {
int result = compare(a.getId(), b.getId());
if (result == 0) {
result = a.getName().compareToIgnoreCase(b.getName());
}
return result;
}
}
/**
* Are we running on JRE6 or above?
*/
public static boolean isMustangOrAbove() {
try {
System.console();
return true;
} catch (LinkageError e) {
return false;
}
}
// ThreadInfo.toString() truncates the stack trace by first 8, so needed my own version
public static String dumpThreadInfo(ThreadInfo ti, ThreadGroupMap map) {
String grp = map.getThreadGroup(ti);
StringBuilder sb = new StringBuilder("\"" + ti.getThreadName() + "\""
+ " Id=" + ti.getThreadId() + " Group="
+ (grp != null ? grp : "?") + " "
+ ti.getThreadState());
if (ti.getLockName() != null) {
sb.append(" on " + ti.getLockName());
}
if (ti.getLockOwnerName() != null) {
sb.append(" owned by \"" + ti.getLockOwnerName()
+ "\" Id=" + ti.getLockOwnerId());
}
if (ti.isSuspended()) {
sb.append(" (suspended)");
}
if (ti.isInNative()) {
sb.append(" (in native)");
}
sb.append('\n');
StackTraceElement[] stackTrace = ti.getStackTrace();
for (int i = 0; i < stackTrace.length; i++) {
StackTraceElement ste = stackTrace[i];
sb.append("\tat " + ste.toString());
sb.append('\n');
if (i == 0 && ti.getLockInfo() != null) {
Thread.State ts = ti.getThreadState();
switch (ts) {
case BLOCKED:
sb.append("\t- blocked on " + ti.getLockInfo());
sb.append('\n');
break;
case WAITING:
sb.append("\t- waiting on " + ti.getLockInfo());
sb.append('\n');
break;
case TIMED_WAITING:
sb.append("\t- waiting on " + ti.getLockInfo());
sb.append('\n');
break;
default:
}
}
for (MonitorInfo mi : ti.getLockedMonitors()) {
if (mi.getLockedStackDepth() == i) {
sb.append("\t- locked " + mi);
sb.append('\n');
}
}
}
LockInfo[] locks = ti.getLockedSynchronizers();
if (locks.length > 0) {
sb.append("\n\tNumber of locked synchronizers = " + locks.length);
sb.append('\n');
for (LockInfo li : locks) {
sb.append("\t- " + li);
sb.append('\n');
}
}
sb.append('\n');
return sb.toString();
}
public static <T> Collection<T> emptyList() {
return Collections.emptyList();
}
public static String jsStringEscape(String s) {
StringBuilder buf = new StringBuilder();
for (int i = 0; i < s.length(); i++) {
char ch = s.charAt(i);
switch (ch) {
case '\'':
buf.append("\\'");
break;
case '\\':
buf.append("\\\\");
break;
case '"':
buf.append("\\\"");
break;
default:
buf.append(ch);
}
}
return buf.toString();
}
/**
* Converts "abc" to "Abc".
*/
public static String capitalize(String s) {
if (s == null || s.length() == 0) {
return s;
}
return Character.toUpperCase(s.charAt(0)) + s.substring(1);
}
public static String getVersion() {
return Hudson.VERSION;
}
/**
* Resoruce path prefix.
*/
public static String getResourcePath() {
return Hudson.RESOURCE_PATH;
}
public static String getViewResource(Object it, String path) {
Class clazz = it.getClass();
if (it instanceof Class) {
clazz = (Class) it;
}
if (it instanceof Descriptor) {
clazz = ((Descriptor) it).clazz;
}
StringBuilder buf = new StringBuilder(getRequestRootPath());
buf.append(Hudson.VIEW_RESOURCE_PATH).append('/');
buf.append(clazz.getName().replace('.', '/').replace('$', '/'));
buf.append('/').append(path);
return buf.toString();
}
public static boolean hasView(Object it, String path) throws IOException {
if (it == null) {
return false;
}
return Stapler.getCurrentRequest().getView(it, path) != null;
}
/**
* Can be used to check a checkbox by default. Used from views like
* {@code h.defaultToTrue(scm.useUpdate)}. The expression will evaluate to
* true if scm is null.
*/
public static boolean defaultToTrue(Boolean b) {
if (b == null) {
return true;
}
return b;
}
/**
* If the value exists, return that value. Otherwise return the default
* value. <p> Starting 1.294, JEXL supports the elvis operator "x?:y" that
* supercedes this.
*
* @since 1.150
*/
public static <T> T defaulted(T value, T defaultValue) {
return value != null ? value : defaultValue;
}
public static String printThrowable(Throwable t) {
StringWriter sw = new StringWriter();
t.printStackTrace(new PrintWriter(sw));
return sw.toString();
}
/**
* Counts the number of rows needed for textarea to fit the content. Minimum
* 5 rows.
*/
public static int determineRows(String s) {
if (s == null) {
return 5;
}
return Math.max(5, LINE_END.split(s).length);
}
/**
* Converts the Hudson build status to CruiseControl build status, which is
* either Success, Failure, Exception, or Unknown.
*/
public static String toCCStatus(Item i) {
if (i instanceof Job) {
Job j = (Job) i;
switch (j.getIconColor().noAnime()) {
case ABORTED:
case RED:
case YELLOW:
return "Failure";
case GREEN:
return "Success";
case BLUE:
return "Success";
case DISABLED:
case GREY:
return "Unknown";
}
}
return "Unknown";
}
private static final Pattern LINE_END = Pattern.compile("\r?\n");
/**
* Checks if the current user is anonymous.
*/
public static boolean isAnonymous() {
return HudsonSecurityManager.getAuthentication() instanceof AnonymousAuthenticationToken;
}
/**
* When called from within JEXL expression evaluation, this method returns
* the current {@link JellyContext} used to evaluate the script.
*
* @since 1.164
*/
public static JellyContext getCurrentJellyContext() {
JellyContext context = org.eclipse.hudson.ExpressionFactory2.CURRENT_CONTEXT.get();
assert context != null;
return context;
}
/**
* Evaluate a Jelly script and return output as a String.
*
* @since 1.267
*/
public static String runScript(Script script) throws JellyTagException {
StringWriter out = new StringWriter();
script.run(getCurrentJellyContext(), XMLOutput.createXMLOutput(out));
return out.toString();
}
/**
* Returns a sub-list if the given list is bigger than the specified
* 'maxSize'
*/
public static <T> List<T> subList(List<T> base, int maxSize) {
if (maxSize < base.size()) {
return base.subList(0, maxSize);
} else {
return base;
}
}
/**
* Computes the hyperlink to actions, to handle the situation when the
* {@link Action#getUrlName()} returns absolute URL.
*/
public static String getActionUrl(String itUrl, Action action) {
String urlName = action.getUrlName();
if (urlName == null) {
return null; // to avoid NPE and fail to render the whole page
}
if (SCHEME.matcher(urlName).matches()) {
return urlName; // absolute URL
}
if (urlName.startsWith("/")) {
return getRequestRootPath() + urlName;
} else // relative URL name
{
return getRequestRootPath() + '/' + itUrl + urlName;
}
}
/**
* Escapes the character unsafe for e-mail address. See
* http://en.wikipedia.org/wiki/E-mail_address for the details, but here the
* vocabulary is even more restricted.
*/
public static String toEmailSafeString(String projectName) {
// TODO: escape non-ASCII characters
StringBuilder buf = new StringBuilder(projectName.length());
for (int i = 0; i < projectName.length(); i++) {
char ch = projectName.charAt(i);
if (('a' <= ch && ch <= 'z')
|| ('z' <= ch && ch <= 'Z')
|| ('0' <= ch && ch <= '9')
|| "-_.".indexOf(ch) >= 0) {
buf.append(ch);
} else {
buf.append('_'); // escape
}
}
return projectName;
}
public String getSystemProperty(String key) {
return System.getProperty(key);
}
/**
* Obtains the host name of the Hudson server that clients can use to talk
* back to. <p> This is primarily used in <tt>slave-agent.jnlp.jelly</tt> to
* specify the destination that the slaves talk to.
*/
public String getServerName() {
// Try to infer this from the configured root URL.
// This makes it work correctly when Hudson runs behind a reverse proxy.
String url = Hudson.getInstance().getRootUrl();
try {
if (url != null) {
String host = new URL(url).getHost();
if (host != null) {
return host;
}
}
} catch (MalformedURLException e) {
// fall back to HTTP request
}
return Stapler.getCurrentRequest().getServerName();
}
/**
* Determines the form validation check URL. See textbox.jelly
*/
public String getCheckUrl(String userDefined, Object descriptor, String field) {
if (userDefined != null || field == null) {
return userDefined;
}
if (descriptor instanceof Descriptor) {
Descriptor d = (Descriptor) descriptor;
return d.getCheckUrl(field);
}
return null;
}
/**
* If the given href link is matching the current page, return true.
*
* Used in <tt>task.jelly</tt> to decide if the page should be highlighted.
*/
public boolean hyperlinkMatchesCurrentPage(String href) throws UnsupportedEncodingException {
String url = Stapler.getCurrentRequest().getRequestURL().toString();
if (href == null || href.length() <= 1) {
return ".".equals(href) && url.endsWith("/");
}
url = URLDecoder.decode(url, "UTF-8");
href = URLDecoder.decode(href, "UTF-8");
if (url.endsWith("/")) {
url = url.substring(0, url.length() - 1);
}
if (href.endsWith("/")) {
href = href.substring(0, href.length() - 1);
}
return url.endsWith(href);
}
public <T> List<T> singletonList(T t) {
return Collections.singletonList(t);
}
/**
* Gets all the {@link PageDecorator}s.
*/
public static List<PageDecorator> getPageDecorators() {
// this method may be called to render start up errors, at which point Hudson doesn't exist yet. see HUDSON-3608
if (Hudson.getInstance() == null) {
return Collections.emptyList();
}
return PageDecorator.all();
}
public static List<Descriptor<Cloud>> getCloudDescriptors() {
return Cloud.all();
}
/**
* Prepend a prefix only when there's the specified body.
*/
public String prepend(String prefix, String body) {
if (body != null && body.length() > 0) {
return prefix + body;
}
return body;
}
public static List<Descriptor<CrumbIssuer>> getCrumbIssuerDescriptors() {
return CrumbIssuer.all();
}
public static String getCrumb(StaplerRequest req) {
Hudson h = Hudson.getInstance();
CrumbIssuer issuer = h != null ? h.getCrumbIssuer() : null;
return issuer != null ? issuer.getCrumb(req) : "";
}
public static String getCrumbRequestField() {
Hudson h = Hudson.getInstance();
CrumbIssuer issuer = h != null ? h.getCrumbIssuer() : null;
return issuer != null ? issuer.getDescriptor().getCrumbRequestField() : "";
}
public static Date getCurrentTime() {
return new Date();
}
public static Locale getClientLocale() {
return Stapler.getCurrentRequest().getLocale();
}
public static Locale getServerLocale() {
return Locale.getDefault();
}
/**
* Generate a series of <script> tags to include <tt>script.js</tt> from
* {@link ConsoleAnnotatorFactory}s and
* {@link ConsoleAnnotationDescriptor}s.
*/
public static String generateConsoleAnnotationScriptAndStylesheet() {
String cp = getRequestRootPath();
StringBuilder buf = new StringBuilder();
for (ConsoleAnnotatorFactory f : ConsoleAnnotatorFactory.all()) {
String path = cp + "/extensionList/" + ConsoleAnnotatorFactory.class.getName() + "/" + f.getClass().getName();
if (f.hasScript()) {
buf.append("<script src='" + path + "/script.js'></script>");
}
if (f.hasStylesheet()) {
buf.append("<link rel='stylesheet' type='text/css' href='" + path + "/style.css' />");
}
}
for (ConsoleAnnotationDescriptor d : ConsoleAnnotationDescriptor.all()) {
String path = cp + "/descriptor/" + d.clazz.getName();
if (d.hasScript()) {
buf.append("<script src='" + path + "/script.js'></script>");
}
if (d.hasStylesheet()) {
buf.append("<link rel='stylesheet' type='text/css' href='" + path + "/style.css' />");
}
}
return buf.toString();
}
/**
* Work around for bug 6935026.
*/
public List<String> getLoggerNames() {
while (true) {
try {
List<String> r = new ArrayList<String>();
Enumeration<String> e = LogManager.getLogManager().getLoggerNames();
while (e.hasMoreElements()) {
r.add(e.nextElement());
}
return r;
} catch (ConcurrentModificationException e) {
// retry
}
}
}
/**
* Used by <f:password/> so that we send an encrypted value to the
* client.
*/
public String getPasswordValue(Object o) {
if (o == null) {
return null;
}
if (o instanceof Secret) {
return ((Secret) o).getEncryptedValue();
}
return o.toString();
}
public List filterDescriptors(Object context, Iterable descriptors) {
return DescriptorVisibilityFilter.apply(context, descriptors);
}
private static final Pattern SCHEME = Pattern.compile("[a-z]+://.+");
/**
* Returns true if we are running unit tests.
*/
public static boolean getIsUnitTest() {
return Main.isUnitTest;
}
/**
* Returns {@code true} if the {@link Run#ARTIFACTS} permission is enabled,
* {@code false} otherwise.
*
* <p>When the {@link Run#ARTIFACTS} permission is not turned on using the
* {@code hudson.security.ArtifactsPermission}, this permission must not be
* considered to be set to {@code false} for every user. It must rather be
* like if the permission doesn't exist at all (which means that every user
* has to have an access to the artifacts but the permission can't be
* configured in the security screen). Got it?</p>
*/
public static boolean isArtifactsPermissionEnabled() {
return Boolean.getBoolean("hudson.security.ArtifactsPermission");
}
/**
* Returns true if current user is the author of the job.
*
* @param job job.
* @return returns true if current user is the author of the job.
*/
public static boolean isAuthor(Job job) {
User user = User.current();
return !(user == null || job == null || job.getCreatedBy() == null) && job.getCreatedBy().equals(user.getId());
}
/**
* Resolves the target object for the given object. If the object is a
* StaplerProxy, then return the proxy target.
*
* @since 2.1.0
*/
public static Object resolveStaplerObject(final Object obj) {
if (obj instanceof StaplerProxy) {
return ((StaplerProxy) obj).getTarget();
}
return obj;
}
/**
* Returns item by name from the list.
*
* @param items for filtering.
* @param name the name of the item.
* @return template.
*/
public static <T extends Item> T getItemByName(List<T> items, final String name) {
if (StringUtils.isBlank(name)) {
return null;
}
Iterable<T> templates = Iterables.filter(items, new Predicate<T>() {
@Override
public boolean apply(T item) {
return name.equalsIgnoreCase(item.getName());
}
});
return templates.iterator().hasNext() ? templates.iterator().next() : null;
}
/**
* Returns true if the {@link Item#WIPEOUT} permission is enabled.
*
* By default the "Wipe Out Workspace" action is available on job when user
* has {@link Item#BUILD} permission (if user can trigger builds). If this
* behavior is not acceptable for project you can enable the
* {@code hudson.security.WipeOutPermission} system property. It will add
* "WipeOut" permission checkbox into permission control panel to manage
* "Wipe Out Workspace" action.
*
* @return true if the {@link Item#WIPEOUT} permission is enabled.
*/
public static boolean isWipeOutPermissionEnabled() {
return Boolean.getBoolean("hudson.security.WipeOutPermission");
}
public static boolean disableUpdateCenterSwitch() {
return Boolean.getBoolean("hudson.pluginManager.disableUpdateCenterSwitch");
}
public static Object rawHtml(Object o) {
return InternationalizedStringExpression.rawHtml(o);
}
public static BallColor getJobStatusIcon(String jobName){
TopLevelItem job = Hudson.getInstance().getItem(jobName);
if (job != null && job instanceof Job){
return ((Job)job).getIconColor();
}
return null;
}
public static boolean isBrokenItem(Item item) {
if ( item instanceof hudson.model.BrokenTopLevelItem) {
return true;
}
return false;
}
/**
* Get an attribute stored in a session with out actually creating the session
* @param req
* @param attrName
* @return
* @since 3.2.2
*/
public static Object getSessionAttribute(StaplerRequest req, String attrName){
HttpSession session = req.getSession(false);
if (session != null){
return session.getAttribute(attrName);
}else{
return null;
}
}
}