/**
* *****************************************************************************
*
* Copyright (c) 2004-2010 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
*
*
******************************************************************************
*/
package hudson.model;
import hudson.RelativePath;
import hudson.XmlFile;
import hudson.BulkChange;
import hudson.Functions;
import hudson.PluginWrapper;
import hudson.Util;
import static hudson.Util.singleQuote;
import hudson.diagnosis.OldDataMonitor;
import hudson.model.listeners.SaveableListener;
import hudson.util.ReflectionUtils;
import hudson.util.ReflectionUtils.Parameter;
import hudson.views.ListViewColumn;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.*;
import org.springframework.util.StringUtils;
import org.jvnet.tiger_types.Types;
import org.apache.commons.io.IOUtils;
import javax.servlet.ServletException;
import javax.servlet.RequestDispatcher;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Locale;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.beans.Introspector;
import java.net.URL;
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
/**
* Metadata about a configurable instance.
*
* <p> {@link Descriptor} is an object that has metadata about a
* {@link Describable} object, and also serves as a factory (in a way this
* relationship is similar to {@link Object}/{@link Class} relationship.
*
* A {@link Descriptor}/{@link Describable} combination is used throughout in
* Hudson to implement a configuration/extensibility mechanism.
*
* <p> Take the list view support as an example, which is implemented in
* {@link ListView} class. Whenever a new view is created, a new
* {@link ListView} instance is created with the configuration information. This
* instance gets serialized to XML, and this instance will be called to render
* the view page. This is the job of {@link Describable} — each instance
* represents a specific configuration of a view (what projects are in it,
* regular expression, etc.)
*
* <p> For Hudson to create such configured {@link ListView} instance, Hudson
* needs another object that captures the metadata of {@link ListView}, and that
* is what a {@link Descriptor} is for. {@link ListView} class has a singleton
* descriptor, and this descriptor helps render the configuration form, remember
* system-wide configuration, and works as a factory.
*
* <p> {@link Descriptor} also usually have its associated views.
*
*
* <h2>Persistence</h2> <p> {@link Descriptor} can persist data just by storing
* them in fields. However, it is the responsibility of the derived type to
* properly invoke {@link #save()} and {@link #load()}.
*
* <h2>Reflection Enhancement</h2> {@link Descriptor} defines addition to the
* standard Java reflection and provides reflective information about its
* corresponding {@link Describable}. These are primarily used by tag libraries
* to keep the Jelly scripts concise.
*
* @author Kohsuke Kawaguchi
* @see Describable
*/
public abstract class Descriptor<T extends Describable<T>> implements Saveable {
/**
* Up to Hudson 1.61 this was used as the primary persistence mechanism.
* Going forward Hudson simply persists all the non-transient fields of
* {@link Descriptor}, just like others, so this is pointless.
*
* @deprecated since 2006-11-16
*/
@Deprecated
private transient Map<String, Object> properties;
/**
* The class being described by this descriptor.
*/
public transient final Class<? extends T> clazz;
private transient final Map<String, String> checkMethods = new ConcurrentHashMap<String, String>();
/**
* Lazily computed list of properties on {@link #clazz} and on the
* descriptor itself.
*/
private transient volatile Map<String, PropertyType> propertyTypes, globalPropertyTypes;
/**
* Help file redirect, keyed by the field name to the path.
*
* @see #getHelpFile(String)
*/
private final Map<String, String> helpRedirect = new HashMap<String, String>();
/**
* Represents a readable property on {@link Describable}.
*/
public static final class PropertyType {
//TODO: review and check whether we can do it private
public final Class clazz;
//TODO: review and check whether we can do it private
public final Type type;
private volatile Class itemType;
public Class getClazz() {
return clazz;
}
public Type getType() {
return type;
}
PropertyType(Class clazz, Type type) {
this.clazz = clazz;
this.type = type;
}
PropertyType(Field f) {
this(f.getType(), f.getGenericType());
}
PropertyType(Method getter) {
this(getter.getReturnType(), getter.getGenericReturnType());
}
public Enum[] getEnumConstants() {
return (Enum[]) clazz.getEnumConstants();
}
/**
* If the property is a collection/array type, what is an item type?
*/
public Class getItemType() {
if (itemType == null) {
itemType = computeItemType();
}
return itemType;
}
private Class computeItemType() {
if (clazz.isArray()) {
return clazz.getComponentType();
}
if (Collection.class.isAssignableFrom(clazz)) {
Type col = Types.getBaseClass(type, Collection.class);
if (col instanceof ParameterizedType) {
return Types.erasure(Types.getTypeArgument(col, 0));
} else {
return Object.class;
}
}
return null;
}
/**
* Returns {@link Descriptor} whose 'clazz' is the same as
* {@link #getItemType() the item type}.
*/
public Descriptor getItemTypeDescriptor() {
return Hudson.getInstance().getDescriptor(getItemType());
}
public Descriptor getItemTypeDescriptorOrDie() {
return Hudson.getInstance().getDescriptorOrDie(getItemType());
}
/**
* Returns all the descriptors that produce types assignable to the item
* type.
*/
public List<? extends Descriptor> getApplicableDescriptors() {
return Hudson.getInstance().getDescriptorList(clazz);
}
}
protected Descriptor(Class<? extends T> clazz) {
this.clazz = clazz;
// doing this turns out to be very error prone,
// as field initializers in derived types will override values.
// load();
}
/**
* Infers the type of the corresponding {@link Describable} from the outer
* class. This version works when you follow the common convention, where a
* descriptor is written as the static nested class of the describable
* class.
*
* @since 1.278
*/
protected Descriptor() {
this.clazz = (Class<T>) getClass().getEnclosingClass();
if (clazz == null) {
throw new AssertionError(getClass() + " doesn't have an outer class. Use the constructor that takes the Class object explicitly.");
}
// detect an type error
Type bt = Types.getBaseClass(getClass(), Descriptor.class);
if (bt instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) bt;
// this 't' is the closest approximation of T of Descriptor<T>.
Class t = Types.erasure(pt.getActualTypeArguments()[0]);
if (!t.isAssignableFrom(clazz)) {
throw new AssertionError("Outer class " + clazz + " of " + getClass() + " is not assignable to " + t + ". Perhaps wrong outer class?");
}
}
// detect a type error. this Descriptor is supposed to be returned from getDescriptor(), so make sure its type match up.
// this prevents a bug like http://www.nabble.com/Creating-a-new-parameter-Type-%3A-Masked-Parameter-td24786554.html
try {
Method getd = clazz.getMethod("getDescriptor");
if (!getd.getReturnType().isAssignableFrom(getClass())) {
throw new AssertionError(getClass() + " must be assignable to " + getd.getReturnType());
}
} catch (NoSuchMethodException e) {
throw new AssertionError(getClass() + " is missing getDescriptor method.");
}
}
/**
* Human readable name of this kind of configurable object.
*/
public abstract String getDisplayName();
/**
* Uniquely identifies this {@link Descriptor} among all the other
* {@link Descriptor}s.
*
* <p> Historically {@link #clazz} is assumed to be unique, so this method
* uses that as the default, but if you are adding {@link Descriptor}s
* programmatically for the same type, you can change this to disambiguate
* them.
*
* @return Stick to valid Java identifier character, plus '.', which had to
* be allowed for historical reasons.
*
* @since 1.391
*/
public String getId() {
return clazz.getName();
}
/**
* Gets the URL that this Descriptor is bound to, relative to the nearest
* {@link DescriptorByNameOwner}. Since {@link Hudson} is a
* {@link DescriptorByNameOwner}, there's always one such ancestor to any
* request.
*/
public String getDescriptorUrl() {
return "descriptorByName/" + getId();
}
private String getCurrentDescriptorByNameUrl() {
StaplerRequest req = Stapler.getCurrentRequest();
Ancestor a = req.findAncestor(DescriptorByNameOwner.class);
return Functions.getAncestorUrl(req, a);
}
/**
* If the field "xyz" of a {@link Describable} has the corresponding
* "doCheckXyz" method, return the form-field validation string. Otherwise
* null. <p> This method is used to hook up the form validation method to
* the corresponding HTML input element.
*/
public String getCheckUrl(String fieldName) {
String method = checkMethods.get(fieldName);
if (method == null) {
method = calcCheckUrl(fieldName);
checkMethods.put(fieldName, method);
}
if (method.equals(NONE)) // == would do, but it makes IDE flag a warning
{
return null;
}
// put this under the right contextual umbrella.
// a is always non-null because we already have Hudson as the sentinel
return singleQuote(getCurrentDescriptorByNameUrl() + '/') + '+' + method;
}
private String calcCheckUrl(String fieldName) {
String capitalizedFieldName = StringUtils.capitalize(fieldName);
Method method = ReflectionUtils.getPublicMethodNamed(getClass(), "doCheck" + capitalizedFieldName);
if (method == null) {
return NONE;
}
return singleQuote(getDescriptorUrl() + "/check" + capitalizedFieldName) + buildParameterList(method, new StringBuilder()).append(".toString()");
}
/**
* Builds query parameter line by figuring out what should be submitted
*/
private StringBuilder buildParameterList(Method method, StringBuilder query) {
for (Parameter p : ReflectionUtils.getParameters(method)) {
QueryParameter qp = p.annotation(QueryParameter.class);
if (qp != null) {
String name = qp.value();
if (name.length() == 0) {
name = p.name();
}
if (name == null || name.length() == 0) {
continue; // unknown parameter name. we'll report the error when the form is submitted.
}
RelativePath rp = p.annotation(RelativePath.class);
if (rp != null) {
name = rp.value() + '/' + name;
}
if (query.length() == 0) {
query.append("+qs(this)");
}
if (name.equals("value")) {
// The special 'value' parameter binds to the the current field
query.append(".addThis()");
} else {
query.append(".nearBy('" + name + "')");
}
continue;
}
Method m = ReflectionUtils.getPublicMethodNamed(p.type(), "fromStapler");
if (m != null) {
buildParameterList(m, query);
}
}
return query;
}
/**
* Computes the list of other form fields that the given field depends on,
* via the doFillXyzItems method, and sets that as the 'fillDependsOn'
* attribute. Also computes the URL of the doFillXyzItems and sets that as
* the 'fillUrl' attribute.
*/
public void calcFillSettings(String field, Map<String, Object> attributes) {
String capitalizedFieldName = StringUtils.capitalize(field);
String methodName = "doFill" + capitalizedFieldName + "Items";
Method method = ReflectionUtils.getPublicMethodNamed(getClass(), methodName);
if (method == null) {
throw new IllegalStateException(String.format("%s doesn't have the %s method for filling a drop-down list", getClass(), methodName));
}
// build query parameter line by figuring out what should be submitted
List<String> depends = buildFillDependencies(method, new ArrayList<String>());
if (!depends.isEmpty()) {
attributes.put("fillDependsOn", Util.join(depends, " "));
}
attributes.put("fillUrl", String.format("%s/%s/fill%sItems", getCurrentDescriptorByNameUrl(), getDescriptorUrl(), capitalizedFieldName));
}
private List<String> buildFillDependencies(Method method, List<String> depends) {
for (Parameter p : ReflectionUtils.getParameters(method)) {
QueryParameter qp = p.annotation(QueryParameter.class);
if (qp != null) {
String name = qp.value();
if (name.length() == 0) {
name = p.name();
}
if (name == null || name.length() == 0) {
continue; // unknown parameter name. we'll report the error when the form is submitted.
}
RelativePath rp = p.annotation(RelativePath.class);
if (rp != null) {
name = rp.value() + '/' + name;
}
depends.add(name);
continue;
}
Method m = ReflectionUtils.getPublicMethodNamed(p.type(), "fromStapler");
if (m != null) {
buildFillDependencies(m, depends);
}
}
return depends;
}
/**
* Computes the auto-completion setting
*/
public void calcAutoCompleteSettings(String field, Map<String, Object> attributes) {
String capitalizedFieldName = StringUtils.capitalize(field);
String methodName = "doAutoComplete" + capitalizedFieldName;
Method method = ReflectionUtils.getPublicMethodNamed(getClass(), methodName);
if (method == null) {
return; // no auto-completion
}
attributes.put("autoCompleteUrl", String.format("%s/%s/autoComplete%s", getCurrentDescriptorByNameUrl(), getDescriptorUrl(), capitalizedFieldName));
}
/**
* Used by Jelly to abstract away the handlign of global.jelly vs
* config.jelly databinding difference.
*/
public PropertyType getPropertyType(Object instance, String field) {
// in global.jelly, instance==descriptor
return instance == this ? getGlobalPropertyType(field) : getPropertyType(field);
}
/**
* Obtains the property type of the given field of {@link #clazz}
*/
public PropertyType getPropertyType(String field) {
if (propertyTypes == null) {
propertyTypes = buildPropertyTypes(clazz);
}
return propertyTypes.get(field);
}
/**
* Obtains the property type of the given field of this descriptor.
*/
public PropertyType getGlobalPropertyType(String field) {
if (globalPropertyTypes == null) {
globalPropertyTypes = buildPropertyTypes(getClass());
}
return globalPropertyTypes.get(field);
}
/**
* Given the class, list up its {@link PropertyType}s from its public
* fields/getters.
*/
private Map<String, PropertyType> buildPropertyTypes(Class<?> clazz) {
Map<String, PropertyType> r = new HashMap<String, PropertyType>();
for (Field f : clazz.getFields()) {
r.put(f.getName(), new PropertyType(f));
}
for (Method m : clazz.getMethods()) {
if (m.getName().startsWith("get")) {
r.put(Introspector.decapitalize(m.getName().substring(3)), new PropertyType(m));
}
}
return r;
}
/**
* Gets the class name nicely escaped to be usable as a key in the
* structured form submission.
*/
public final String getJsonSafeClassName() {
return getId().replace('.', '-');
}
/**
* @deprecated Implement {@link #newInstance(StaplerRequest, JSONObject)}
* method instead. Deprecated as of 1.145.
*/
public T newInstance(StaplerRequest req) throws FormException {
throw new UnsupportedOperationException(getClass() + " should implement newInstance(StaplerRequest,JSONObject)");
}
/**
* Creates a configured instance from the submitted form.
*
* <p> Hudson only invokes this method when the user wants an instance of
* <tt>T</tt>. So there's no need to check that in the implementation.
*
* <p> Starting 1.206, the default implementation of this method does the
* following:
* <pre>
* req.bindJSON(clazz,formData);
* </pre> <p> ... which performs the databinding on the constructor of
* {@link #clazz}.
*
* <p> For some types of {@link Describable}, such as
* {@link ListViewColumn}, this method can be invoked with null request
* object for historical reason. Such design is considered broken, but due
* to the compatibility reasons we cannot fix it. Because of this, the
* default implementation gracefully handles null request, but the contract
* of the method still is "request is always non-null." Extension points
* that need to define the "default instance" semantics should define a
* descriptor subtype and add the no-arg newInstance method.
*
* @param req Always non-null (see note above.) This object includes
* represents the entire submission.
* @param formData The JSON object that captures the configuration data for
* this {@link Descriptor}. See
* http://wiki.hudson-ci.org/display/HUDSON/Structured+Form+Submission
* Always non-null.
*
* @throws FormException Signals a problem in the submitted form.
* @since 1.145
*/
public T newInstance(StaplerRequest req, JSONObject formData) throws FormException {
try {
Method m = getClass().getMethod("newInstance", StaplerRequest.class);
if (!Modifier.isAbstract(m.getDeclaringClass().getModifiers())) {
// this class overrides newInstance(StaplerRequest).
// maintain the backward compatible behavior
return verifyNewInstance(newInstance(req));
} else {
if (req == null) {
// yes, req is supposed to be always non-null, but see the note above
return verifyNewInstance(clazz.newInstance());
}
// new behavior as of 1.206
return verifyNewInstance(req.bindJSON(clazz, formData));
}
} catch (NoSuchMethodException e) {
throw new AssertionError(e); // impossible
} catch (InstantiationException e) {
throw new Error("Failed to instantiate " + clazz + " from " + formData, e);
} catch (IllegalAccessException e) {
throw new Error("Failed to instantiate " + clazz + " from " + formData, e);
} catch (RuntimeException e) {
throw new RuntimeException("Failed to instantiate " + clazz + " from " + formData, e);
}
}
/**
* Look out for a typical error a plugin developer makes. See
* http://hudson.361315.n4.nabble.com/Help-Hint-needed-Post-build-action-doesn-t-stay-activated-td2308833.html
*/
private T verifyNewInstance(T t) {
if (t != null && t.getDescriptor() != this) {
// TODO: should this be a fatal error?
LOGGER.warning("Father of " + t + " and its getDescriptor() points to two different instances. Probably malplaced @Extension. See http://hudson.361315.n4.nabble.com/Help-Hint-needed-Post-build-action-doesn-t-stay-activated-td2308833.html");
}
return t;
}
/**
* Returns the resource path to the help screen HTML, if any.
*
* <p> Starting 1.282, this method uses "convention over configuration"
* — you should just put the "help.html" (and its localized versions,
* if any) in the same directory you put your Jelly view files, and this
* method will automatically does the right thing.
*
* <p> This value is relative to the context root of Hudson, so normally the
* values are something like <tt>"/plugin/emma/help.html"</tt> to refer to
* static resource files in a plugin, or
* <tt>"/publisher/EmmaPublisher/abc"</tt> to refer to Jelly script
* <tt>abc.jelly</tt> or a method <tt>EmmaPublisher.doAbc()</tt>.
*
* @return null to indicate that there's no help.
*/
public String getHelpFile() {
return getHelpFile(null);
}
/**
* Returns the path to the help screen HTML for the given field.
*
* <p>
* The help files are assumed to be at "help/FIELDNAME.html" with possible
* locale variations.
*/
public String getHelpFile(final String fieldName) {
return getHelpFile(clazz, fieldName);
}
public String getHelpFile(Class<?> clazz, String fieldName) {
String v = helpRedirect.get(fieldName);
if (v != null) {
return v;
}
for (Class<?> c : getAncestors(clazz)) {
String page = "/descriptor/" + getId() + "/help";
String suffix;
if (fieldName == null) {
suffix = "";
} else {
page += '/' + fieldName;
suffix = '-' + fieldName;
}
try {
if (Stapler.getCurrentRequest().getView(c, "help" + suffix) != null) {
return page;
}
} catch (IOException e) {
throw new Error(e);
}
InputStream helpStream = getHelpStream(c, suffix);
if (helpStream != null) {
IOUtils.closeQuietly(helpStream);
return page;
}
}
return null;
}
private Iterable<Class<?>> getAncestors(Class clazz) {
List<Class<?>> r = new ArrayList<Class<?>>();
for (; clazz != null; clazz = clazz.getSuperclass()) {
r.add(clazz);
}
return r;
}
private InputStream getHelpStream(Class c, String suffix) {
Locale locale = Stapler.getCurrentRequest().getLocale();
String base = c.getName().replace('.', '/').replace('$', '/') + "/help" + suffix;
ClassLoader cl = c.getClassLoader();
if (cl == null) {
return null;
}
InputStream in;
in = cl.getResourceAsStream(base + '_' + locale.getLanguage() + '_' + locale.getCountry() + '_' + locale.getVariant() + ".html");
if (in != null) {
return in;
}
in = cl.getResourceAsStream(base + '_' + locale.getLanguage() + '_' + locale.getCountry() + ".html");
if (in != null) {
return in;
}
in = cl.getResourceAsStream(base + '_' + locale.getLanguage() + ".html");
if (in != null) {
return in;
}
// default
return cl.getResourceAsStream(base + ".html");
}
private URL getStaticHelpUrl(Class<?> c, String suffix) {
Locale locale = Stapler.getCurrentRequest().getLocale();
String base = "help" + suffix;
URL url;
url = c.getResource(base + '_' + locale.getLanguage() + '_' + locale.getCountry() + '_' + locale.getVariant() + ".html");
if (url != null) {
return url;
}
url = c.getResource(base + '_' + locale.getLanguage() + '_' + locale.getCountry() + ".html");
if (url != null) {
return url;
}
url = c.getResource(base + '_' + locale.getLanguage() + ".html");
if (url != null) {
return url;
}
// default
return c.getResource(base + ".html");
}
/**
* Tells Hudson that the help file for the field 'fieldName' is defined in
* the help file for the 'fieldNameToRedirectTo' in the 'owner' class.
*
* @since 1.425
*/
protected void addHelpFileRedirect(String fieldName, Class<? extends Describable> owner, String fieldNameToRedirectTo) {
helpRedirect.put(fieldName,
Hudson.getInstance().getDescriptor(owner).getHelpFile(fieldNameToRedirectTo));
}
/**
* Checks if the given object is created from this {@link Descriptor}.
*/
public final boolean isInstance(T instance) {
return clazz.isInstance(instance);
}
/**
* Checks if the type represented by this descriptor is a subtype of the
* given type.
*/
public final boolean isSubTypeOf(Class type) {
return type.isAssignableFrom(clazz);
}
/**
* @deprecated As of 1.239, use
* {@link #configure(StaplerRequest, JSONObject)}.
*/
public boolean configure(StaplerRequest req) throws FormException {
return true;
}
/**
* Invoked when the global configuration page is submitted.
*
* Can be overriden to store descriptor-specific information.
*
* @param json The JSON object that captures the configuration data for this
* {@link Descriptor}. See
* http://wiki.hudson-ci.org/display/HUDSON/Structured+Form+Submission
* @return false to keep the client in the same config page.
*/
public boolean configure(StaplerRequest req, JSONObject json) throws FormException {
// compatibility
return configure(req);
}
public String getConfigPage() {
return getViewPage(clazz, "config.jelly");
}
public String getGlobalConfigPage() {
return getViewPage(clazz, "global.jelly", null);
}
private String getViewPage(Class<?> clazz, String pageName, String defaultValue) {
while (clazz != Object.class) {
String name = clazz.getName().replace('.', '/').replace('$', '/') + "/" + pageName;
if (clazz.getClassLoader().getResource(name) != null) {
return '/' + name;
}
clazz = clazz.getSuperclass();
}
return defaultValue;
}
protected final String getViewPage(Class<?> clazz, String pageName) {
// We didn't find the configuration page.
// Either this is non-fatal, in which case it doesn't matter what string we return so long as
// it doesn't exist.
// Or this error is fatal, in which case we want the developer to see what page he's missing.
// so we put the page name.
return getViewPage(clazz, pageName, pageName);
}
/**
* Saves the configuration info to the disk.
*/
public synchronized void save() {
if (BulkChange.contains(this)) {
return;
}
try {
getConfigFile().write(this);
SaveableListener.fireOnChange(this, getConfigFile());
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to save " + getConfigFile(), e);
}
}
/**
* Loads the data from the disk into this object.
*
* <p> The constructor of the derived class must call this method. (If we do
* that in the base class, the derived class won't get a chance to set
* default values.)
*/
public synchronized void load() {
XmlFile file = getConfigFile();
if (!file.exists()) {
return;
}
try {
file.unmarshal(this);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to load " + file, e);
}
}
public XmlFile getConfigFile() {
return new XmlFile(new File(Hudson.getInstance().getRootDir(), getId() + ".xml"));
}
/**
* Serves <tt>help.html</tt> from the resource of {@link #clazz}.
*/
public void doHelp(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
String path = req.getRestOfPath();
if (path.contains("..")) {
throw new ServletException("Illegal path: " + path);
}
path = path.replace('/', '-');
PluginWrapper pw = getPlugin();
if (pw != null) {
rsp.setHeader("X-Plugin-Short-Name", pw.getShortName());
rsp.setHeader("X-Plugin-Long-Name", pw.getLongName());
rsp.setHeader("X-Plugin-From", pw.getUrl());
}
for (Class<?> c = clazz; c != null; c = c.getSuperclass()) {
RequestDispatcher rd = Stapler.getCurrentRequest().getView(c, "help" + path);
if (rd != null) {// template based help page
rd.forward(req, rsp);
return;
}
InputStream in = getHelpStream(c, path);
if (in != null) {
// TODO: generalize macro expansion and perhaps even support JEXL
rsp.setContentType("text/html;charset=UTF-8");
//InputStream in = url.openStream();
try {
String literal = IOUtils.toString(in, "UTF-8");
rsp.getWriter().println(Util.replaceMacro(literal, Collections.singletonMap("rootURL", req.getContextPath())));
} finally {
IOUtils.closeQuietly(in);
}
return;
}
}
rsp.sendError(SC_NOT_FOUND);
}
protected PluginWrapper getPlugin() {
return Hudson.getInstance().getPluginManager().whichPlugin(clazz);
}
//
// static methods
//
// to work around warning when creating a generic array type
public static <T> T[] toArray(T... values) {
return values;
}
public static <T> List<T> toList(T... values) {
return new ArrayList<T>(Arrays.asList(values));
}
public static <T extends Describable<T>> Map<Descriptor<T>, T> toMap(Iterable<T> describables) {
Map<Descriptor<T>, T> m = new LinkedHashMap<Descriptor<T>, T>();
if (null != describables) {
for (T d : describables) {
m.put(d.getDescriptor(), d);
}
}
return m;
}
/**
* Used to build {@link Describable} instance list from <f:hetero-list>
* tag.
*
* @param req Request that represents the form submission.
* @param formData Structured form data that represents the contains data
* for the list of describables.
* @param key The JSON property name for 'formData' that represents the data
* for the list of describables.
* @param descriptors List of descriptors to create instances from.
* @return Can be empty but never null.
*/
public static <T extends Describable<T>> List<T> newInstancesFromHeteroList(StaplerRequest req, JSONObject formData, String key,
Collection<? extends Descriptor<T>> descriptors) throws FormException {
return newInstancesFromHeteroList(req, formData.get(key), descriptors);
}
public static <T extends Describable<T>> List<T> newInstancesFromHeteroList(StaplerRequest req, Object formData,
Collection<? extends Descriptor<T>> descriptors) throws FormException {
List<T> items = new ArrayList<T>();
if (formData != null) {
for (Object o : JSONArray.fromObject(formData)) {
JSONObject jo = (JSONObject) o;
String kind = jo.getString("kind");
items.add(find(descriptors, kind).newInstance(req, jo));
}
}
return items;
}
/**
* Finds a descriptor from a collection by its class name.
*/
public static <T extends Descriptor> T find(Collection<? extends T> list, String className) {
for (T d : list) {
if (d.getClass().getName().equals(className)) {
return d;
}
}
return null;
}
public static Descriptor find(String className) {
return find(Hudson.getInstance().getExtensionList(Descriptor.class), className);
}
public static final class FormException extends Exception implements HttpResponse {
private final String formField;
public FormException(String message, String formField) {
super(message);
this.formField = formField;
}
public FormException(String message, Throwable cause, String formField) {
super(message, cause);
this.formField = formField;
}
public FormException(Throwable cause, String formField) {
super(cause);
this.formField = formField;
}
/**
* Which form field contained an error?
*/
public String getFormField() {
return formField;
}
public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
// for now, we can't really use the field name that caused the problem.
new Failure(getMessage()).generateResponse(req, rsp, node);
}
}
private static final Logger LOGGER = Logger.getLogger(Descriptor.class.getName());
/**
* Used in {@link #checkMethods} to indicate that there's no check method.
*/
private static final String NONE = "\u0000";
private Object readResolve() {
if (properties != null) {
OldDataMonitor.report(this, "1.62");
}
return this;
}
}