
hudson.model.Descriptor Maven / Gradle / Ivy
package hudson.model;
import hudson.XmlFile;
import hudson.BulkChange;
import hudson.util.CopyOnWriteList;
import hudson.scm.CVSSCM;
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
import org.kohsuke.stapler.StaplerRequest;
import org.kohsuke.stapler.Stapler;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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;
/**
* 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.
*
*
* For example, Take the CVS support as an example, which is implemented
* in {@link CVSSCM} class. Whenever a job is configured with CVS, a new
* {@link CVSSCM} instance is created with the per-job configuration
* information. This instance gets serialized to XML, and this instance
* will be called to perform CVS operations for that job. This is the job
* of {@link Describable} — each instance represents a specific
* configuration of the CVS support (branch, CVSROOT, etc.)
*
*
* For Hudson to create such configured {@link CVSSCM} instance, Hudson
* needs another object that captures the metadata of {@link CVSSCM},
* and that is what a {@link Descriptor} is for. {@link CVSSCM} class
* has a singleton descriptor, and this descriptor helps render
* the configuration form, remember system-wide configuration (such as
* where cvs.exe is), and works as a factory.
*
*
* {@link Descriptor} also usually have its associated views.
*
*
*
Persistence
*
* {@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()}.
*
* @author Kohsuke Kawaguchi
* @see Describable
*/
public abstract class Descriptor> 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
*/
@Deprecated
private transient Map properties;
/**
* The class being described by this descriptor.
*/
public transient final Class extends T> clazz;
private transient final Map checkMethods = new ConcurrentHashMap();
protected Descriptor(Class extends T> clazz) {
this.clazz = clazz;
ALL.add(this);
// doing this turns out to be very error prone,
// as field initializers in derived types will override values.
// load();
}
/**
* Human readable name of this kind of configurable object.
*/
public abstract String getDisplayName();
/**
* If the field "xyz" of a {@link Describable} has the corresponding "doCheckXyz" method,
* return the form-field validation string. Otherwise null.
*
* This method is used to hook up the form validation method to
*/
public String getCheckUrl(String fieldName) {
String capitalizedFieldName = StringUtils.capitalize(fieldName);
Method method = checkMethods.get(fieldName);
if(method==null) {
method = NONE;
String methodName = "doCheck"+ capitalizedFieldName;
for( Method m : getClass().getMethods() ) {
if(m.getName().equals(methodName)) {
method = m;
break;
}
}
checkMethods.put(fieldName,method);
}
if(method==NONE)
return null;
return '\''+Stapler.getCurrentRequest().getContextPath()+"/descriptor/"+clazz.getName()+"/check"+capitalizedFieldName+"?value='+encode(this.value)";
}
/**
* Gets the class name nicely escaped to be usable as a key in the structured form submission.
*/
public String getJsonSafeClassName() {
return clazz.getName().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.
*
*
* 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:
*
* req.bindJSON(clazz,formData);
*
*
* ... which performs the databinding on the constructor of {@link #clazz}.
*
* @param req
* Always non-null. This object includes represents the entire submisison.
* @param formData
* The JSON object that captures the configuration data for this {@link Descriptor}.
* See http://hudson.gotdns.com/wiki/display/HUDSON/Structured+Form+Submission
*
* @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 newInstance(req);
} else {
// new behavior as of 1.206
return req.bindJSON(clazz,formData);
}
} catch (NoSuchMethodException e) {
throw new AssertionError(e); // impossible
}
}
/**
* Returns the resource path to the help screen HTML, if any.
*
*
* 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 null;
}
/**
* Checks if the given object is created from this {@link Descriptor}.
*/
public final boolean isInstance( T instance ) {
return clazz.isInstance(instance);
}
/**
* @deprecated
* As of 1.64. Use {@link #configure(StaplerRequest)}.
*/
@Deprecated
public boolean configure( HttpServletRequest req ) throws FormException {
return true;
}
/**
* @deprecated
* As of 1.239, use {@link #configure(StaplerRequest, JSONObject)}.
*/
public boolean configure( StaplerRequest req ) throws FormException {
// compatibility
return configure( (HttpServletRequest) req );
}
/**
* 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://hudson.gotdns.com/wiki/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");
}
protected final String getViewPage(Class> clazz, String pageName) {
while(clazz!=Object.class) {
String name = clazz.getName().replace('.', '/').replace('$', '/') + "/" + pageName;
if(clazz.getClassLoader().getResource(name)!=null)
return '/'+name;
clazz = clazz.getSuperclass();
}
return "none";
}
/**
* Saves the configuration info to the disk.
*/
public synchronized void save() {
if(BulkChange.contains(this)) return;
try {
getConfigFile().write(this);
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to save "+getConfigFile(),e);
}
}
/**
* Loads the data from the disk into this object.
*
*
* 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 {
Object o = file.unmarshal(this);
if(o instanceof Map) {
// legacy format
@SuppressWarnings("unchecked")
Map _o = (Map) o;
convert(_o);
save(); // convert to the new format
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Failed to load "+file, e);
}
}
/**
* {@link Descriptor}s that has existed <= 1.61 needs to
* be able to read in the old configuration in a property bag
* and reflect that into the new layout.
*/
protected void convert(Map oldPropertyBag) {
}
private XmlFile getConfigFile() {
return new XmlFile(new File(Hudson.getInstance().getRootDir(),clazz.getName()+".xml"));
}
// to work around warning when creating a generic array type
public static T[] toArray( T... values ) {
return values;
}
public static List toList( T... values ) {
final ArrayList r = new ArrayList();
for (T v : values)
r.add(v);
return r;
}
public static >
Map,T> toMap(Iterable describables) {
Map,T> m = new LinkedHashMap,T>();
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 >
List newInstancesFromHeteroList(StaplerRequest req, JSONObject formData, String key,
Collection extends Descriptor> descriptors) throws FormException {
List items = new ArrayList();
if(!formData.has(key)) return items;
JSONArray a = JSONArray.fromObject(formData.get(key));
for (Object o : a) {
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 find(Collection extends T> list, String className) {
for (T d : list) {
if(d.getClass().getName().equals(className))
return d;
}
return null;
}
public static final class FormException extends Exception {
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;
}
}
private static final Logger LOGGER = Logger.getLogger(Descriptor.class.getName());
/**
* All the live instances of {@link Descriptor}.
* {@link Descriptor}s are all supposed to have the singleton semantics, so
* this shouldn't cause a memory leak.
*/
public static final CopyOnWriteList ALL = new CopyOnWriteList();
/**
* Used in {@link #checkMethods} to indicate that there's no check method.
*/
private static final Method NONE;
static {
try {
NONE = Object.class.getMethod("toString");
} catch (NoSuchMethodException e) {
throw new AssertionError();
}
}
}