/* * The MIT License * * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi * * 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.DescriptorExtensionList; import hudson.PluginWrapper; import hudson.RelativePath; import hudson.XmlFile; import hudson.BulkChange; import hudson.ExtensionList; import hudson.Util; import hudson.model.listeners.SaveableListener; import hudson.util.FormApply; import hudson.util.FormValidation.CheckMethod; import hudson.util.ReflectionUtils; import hudson.util.ReflectionUtils.Parameter; import hudson.views.ListViewColumn; import jenkins.model.Jenkins; import net.sf.json.JSONArray; import net.sf.json.JSONObject; import org.kohsuke.stapler.*; import org.kohsuke.stapler.jelly.JellyCompatibleFacet; import org.kohsuke.stapler.lang.Klass; import org.springframework.util.StringUtils; import org.jvnet.tiger_types.Types; import org.apache.commons.io.IOUtils; import static hudson.util.QuotedStringTokenizer.*; import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND; import javax.servlet.ServletException; import javax.servlet.RequestDispatcher; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.net.URL; 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 javax.annotation.CheckForNull; import javax.annotation.Nonnull; /** * Metadata about a configurable instance. * *
* {@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. * *
* 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.) * *
* 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. * *
* {@link Descriptor} also usually have its associated views. * * *
* {@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()}. * *
* 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. * *
* To look up {@link Descriptor} from ID, use {@link Jenkins#getDescriptor(String)}. * * @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(); } /** * Unlike {@link #clazz}, return the parameter type 'T', which determines * the {@link DescriptorExtensionList} that this goes to. * *
* In those situations where subtypes cannot provide the type parameter,
* this method can be overridden to provide it.
*/
public Class
* This method is used to hook up the form validation method to the corresponding HTML input element.
*/
public CheckMethod getCheckMethod(String fieldName) {
CheckMethod method = checkMethods.get(fieldName);
if(method==null) {
method = new CheckMethod(this,fieldName);
checkMethods.put(fieldName,method);
}
return method;
}
/**
* 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
* Hudson only invokes this method when the user wants an instance of T.
* So there's no need to check that in the implementation.
*
*
* Starting 1.206, the default implementation of this method does the following:
*
* ... which performs the databinding on the constructor of {@link #clazz}.
*
*
* 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.jenkins-ci.org/display/JENKINS/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 {@link Klass} object used for the purpose of loading resources from this descriptor.
*
* This hook enables other JVM languages to provide more integrated lookup.
*/
public Klass> getKlass() {
return Klass.java(clazz);
}
/**
* Returns the resource path to the help screen HTML, if any.
*
*
* 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.
*
*
* This value is relative to the context root of Hudson, so normally
* the values are something like "/plugin/emma/help.html" to
* refer to static resource files in a plugin, or "/publisher/EmmaPublisher/abc"
* to refer to Jelly script abc.jelly or a method EmmaPublisher.doAbc().
*
* @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.
*
*
* The help files are assumed to be at "help/FIELDNAME.html" with possible
* locale variations.
*/
public String getHelpFile(final String fieldName) {
return getHelpFile(getKlass(),fieldName);
}
public String getHelpFile(Klass> clazz, String fieldName) {
HelpRedirect r = helpRedirect.get(fieldName);
if (r!=null) return r.resolve();
for (Klass> c : clazz.getAncestors()) {
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);
}
if(getStaticHelpUrl(c, suffix) !=null) return page;
}
return null;
}
/**
* Tells Jenkins 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, new HelpRedirect(owner,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.jenkins-ci.org/display/JENKINS/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, getPossibleViewNames("config"), "config.jelly");
}
public String getGlobalConfigPage() {
return getViewPage(clazz, getPossibleViewNames("global"), null);
}
private String getViewPage(Class> clazz, String pageName, String defaultValue) {
return getViewPage(clazz,Collections.singleton(pageName),defaultValue);
}
private String getViewPage(Class> clazz, Collection
* 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);
}
}
protected XmlFile getConfigFile() {
return new XmlFile(new File(Jenkins.getInstance().getRootDir(),getId()+".xml"));
}
/**
* Returns the plugin in which this descriptor is defined.
*
* @return
* null to indicate that this descriptor came from the core.
*/
protected PluginWrapper getPlugin() {
return Jenkins.getInstance().getPluginManager().whichPlugin(clazz);
}
/**
* Serves help.html 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", Messages.Descriptor_From(
pw.getLongName().replace("Hudson","Jenkins").replace("hudson","jenkins"), pw.getUrl()));
}
for (Klass> c= getKlass(); 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;
}
URL url = getStaticHelpUrl(c, path);
if(url!=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);
}
private URL getStaticHelpUrl(Klass> 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");
}
//
// static methods
//
// to work around warning when creating a generic array type
public static
* req.bindJSON(clazz,formData);
*
*