Descriptor.java 38.3 KB
Newer Older
K
kohsuke 已提交
1 2 3
/*
 * The MIT License
 * 
4
 * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi
K
kohsuke 已提交
5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
 * 
 * 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.
 */
K
kohsuke 已提交
24 25
package hudson.model;

26
import hudson.DescriptorExtensionList;
27
import hudson.PluginWrapper;
28
import hudson.RelativePath;
K
kohsuke 已提交
29
import hudson.XmlFile;
30
import hudson.BulkChange;
31
import hudson.ExtensionList;
32
import hudson.Util;
33
import hudson.model.listeners.SaveableListener;
34
import hudson.util.FormApply;
K
Kohsuke Kawaguchi 已提交
35
import hudson.util.FormValidation.CheckMethod;
36 37
import hudson.util.ReflectionUtils;
import hudson.util.ReflectionUtils.Parameter;
38
import hudson.views.ListViewColumn;
39
import jenkins.model.Jenkins;
40 41
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
42
import org.kohsuke.stapler.*;
43
import org.kohsuke.stapler.jelly.JellyCompatibleFacet;
44
import org.kohsuke.stapler.lang.Klass;
45
import org.springframework.util.StringUtils;
K
kohsuke 已提交
46
import org.jvnet.tiger_types.Types;
K
kohsuke 已提交
47
import org.apache.commons.io.IOUtils;
K
kohsuke 已提交
48

49
import static hudson.util.QuotedStringTokenizer.*;
K
kohsuke 已提交
50
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
51 52
import javax.servlet.ServletException;
import javax.servlet.RequestDispatcher;
K
kohsuke 已提交
53 54
import java.io.File;
import java.io.IOException;
K
kohsuke 已提交
55
import java.io.InputStream;
56
import java.net.URL;
K
kohsuke 已提交
57
import java.util.ArrayList;
58
import java.util.Collection;
K
kohsuke 已提交
59
import java.util.LinkedHashMap;
60 61
import java.util.List;
import java.util.Map;
K
kohsuke 已提交
62
import java.util.HashMap;
K
kohsuke 已提交
63
import java.util.Locale;
64
import java.util.Arrays;
65
import java.util.Collections;
66
import java.util.concurrent.ConcurrentHashMap;
67 68
import java.util.logging.Level;
import java.util.logging.Logger;
69
import java.lang.reflect.Method;
K
kohsuke 已提交
70
import java.lang.reflect.Modifier;
K
kohsuke 已提交
71 72 73 74
import java.lang.reflect.Type;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.beans.Introspector;
75 76
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
K
kohsuke 已提交
77 78 79 80 81 82

/**
 * Metadata about a configurable instance.
 *
 * <p>
 * {@link Descriptor} is an object that has metadata about a {@link Describable}
83 84 85 86
 * 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}
K
kohsuke 已提交
87
 * combination is used throughout in Hudson to implement a
K
kohsuke 已提交
88 89 90
 * configuration/extensibility mechanism.
 *
 * <p>
91 92 93
 * 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
K
kohsuke 已提交
94
 * information. This instance gets serialized to XML, and this instance
95
 * will be called to render the view page. This is the job
K
kohsuke 已提交
96
 * of {@link Describable} &mdash; each instance represents a specific
97
 * configuration of a view (what projects are in it, regular expression, etc.)
K
kohsuke 已提交
98 99
 *
 * <p>
100 101 102
 * 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
K
kohsuke 已提交
103
 * has a singleton descriptor, and this descriptor helps render
104
 * the configuration form, remember system-wide configuration, and works as a factory.
K
kohsuke 已提交
105 106 107 108
 *
 * <p>
 * {@link Descriptor} also usually have its associated views.
 *
109 110 111 112 113 114 115
 *
 * <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()}.
 *
K
kohsuke 已提交
116 117 118 119 120 121
 * <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. 
 *
K
kohsuke 已提交
122 123 124
 * @author Kohsuke Kawaguchi
 * @see Describable
 */
125
public abstract class Descriptor<T extends Describable<T>> implements Saveable {
K
kohsuke 已提交
126 127 128
    /**
     * The class being described by this descriptor.
     */
129
    public transient final Class<? extends T> clazz;
K
kohsuke 已提交
130

K
Kohsuke Kawaguchi 已提交
131
    private transient final Map<String,CheckMethod> checkMethods = new ConcurrentHashMap<String,CheckMethod>();
132

K
kohsuke 已提交
133
    /**
134
     * Lazily computed list of properties on {@link #clazz} and on the descriptor itself.
K
kohsuke 已提交
135
     */
136
    private transient volatile Map<String, PropertyType> propertyTypes,globalPropertyTypes;
K
kohsuke 已提交
137 138 139 140 141 142 143 144

    /**
     * Represents a readable property on {@link Describable}.
     */
    public static final class PropertyType {
        public final Class clazz;
        public final Type type;
        private volatile Class itemType;
145
        public final String displayName;
K
kohsuke 已提交
146

147
        PropertyType(Class clazz, Type type, String displayName) {
K
kohsuke 已提交
148 149
            this.clazz = clazz;
            this.type = type;
150
            this.displayName = displayName;
K
kohsuke 已提交
151 152 153
        }

        PropertyType(Field f) {
154
            this(f.getType(),f.getGenericType(),f.toString());
K
kohsuke 已提交
155 156 157
        }

        PropertyType(Method getter) {
158
            this(getter.getReturnType(),getter.getGenericReturnType(),getter.toString());
K
kohsuke 已提交
159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
        }

        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() {
193
            return Jenkins.getInstance().getDescriptor(getItemType());
K
kohsuke 已提交
194
        }
195 196

        public Descriptor getItemTypeDescriptorOrDie() {
197
            Class it = getItemType();
198 199 200
            if (it == null) {
                throw new AssertionError(clazz + " is not an array/collection type in " + displayName + ". See https://wiki.jenkins-ci.org/display/JENKINS/My+class+is+missing+descriptor");
            }
201 202 203 204
            Descriptor d = Jenkins.getInstance().getDescriptor(it);
            if (d==null)
                throw new AssertionError(it +" is missing its descriptor in "+displayName+". See https://wiki.jenkins-ci.org/display/JENKINS/My+class+is+missing+descriptor");
            return d;
205
        }
206 207

        /**
208
         * Returns all the descriptors that produce types assignable to the property type.
209 210
         */
        public List<? extends Descriptor> getApplicableDescriptors() {
211
            return Jenkins.getInstance().getDescriptorList(clazz);
212
        }
213 214 215 216 217 218 219

        /**
         * Returns all the descriptors that produce types assignable to the item type for a collection property.
         */
        public List<? extends Descriptor> getApplicableItemDescriptors() {
            return Jenkins.getInstance().getDescriptorList(getItemType());
        }
K
kohsuke 已提交
220 221
    }

222 223 224 225 226
    /**
     * Help file redirect, keyed by the field name to the path.
     *
     * @see #getHelpFile(String) 
     */
K
Kohsuke Kawaguchi 已提交
227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242
    private transient final Map<String,HelpRedirect> helpRedirect = new HashMap<String,HelpRedirect>();

    private static class HelpRedirect {
        private final Class<? extends Describable> owner;
        private final String fieldNameToRedirectTo;

        private HelpRedirect(Class<? extends Describable> owner, String fieldNameToRedirectTo) {
            this.owner = owner;
            this.fieldNameToRedirectTo = fieldNameToRedirectTo;
        }

        private String resolve() {
            // the resolution has to be deferred to avoid ordering issue among descriptor registrations.
            return Jenkins.getInstance().getDescriptor(owner).getHelpFile(fieldNameToRedirectTo);
        }
    }
243

244 245 246 247 248 249
    /**
     *
     * @param clazz
     *      Pass in {@link #self()} to have the descriptor describe itself,
     *      (this hack is needed since derived types can't call "getClass()" to refer to itself.
     */
K
kohsuke 已提交
250
    protected Descriptor(Class<? extends T> clazz) {
251 252
        if (clazz==self())
            clazz = (Class)getClass();
K
kohsuke 已提交
253
        this.clazz = clazz;
254 255 256
        // doing this turns out to be very error prone,
        // as field initializers in derived types will override values.
        // load();
K
kohsuke 已提交
257 258
    }

259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    /**
     * 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?");
        }
280 281 282 283 284 285 286 287 288 289 290 291

        // 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.");
        }

292 293
    }

K
kohsuke 已提交
294 295 296 297 298
    /**
     * Human readable name of this kind of configurable object.
     */
    public abstract String getDisplayName();

299 300 301 302 303 304 305 306
    /**
     * 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.
     *
K
Kohsuke Kawaguchi 已提交
307 308 309
     * <p>
     * To look up {@link Descriptor} from ID, use {@link Jenkins#getDescriptor(String)}.
     *
310 311 312 313 314 315 316 317 318
     * @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();
    }

319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
    /**
     * Unlike {@link #clazz}, return the parameter type 'T', which determines
     * the {@link DescriptorExtensionList} that this goes to.
     *
     * <p>
     * In those situations where subtypes cannot provide the type parameter,
     * this method can be overridden to provide it.
     */
    public Class<T> getT() {
        Type subTyping = Types.getBaseClass(getClass(), Descriptor.class);
        if (!(subTyping instanceof ParameterizedType)) {
            throw new IllegalStateException(getClass()+" doesn't extend Descriptor with a type parameter.");
        }
        return Types.erasure(Types.getTypeArgument(subTyping, 0));
    }

335 336
    /**
     * Gets the URL that this Descriptor is bound to, relative to the nearest {@link DescriptorByNameOwner}.
337
     * Since {@link Jenkins} is a {@link DescriptorByNameOwner}, there's always one such ancestor to any request.
338
     */
339
    public String getDescriptorUrl() {
340
        return "descriptorByName/"+getId();
341 342
    }

K
Kohsuke Kawaguchi 已提交
343 344 345 346 347 348 349 350
    /**
     * Gets the URL that this Descriptor is bound to, relative to the context path.
     * @since 1.406
     */
    public final String getDescriptorFullUrl() {
        return getCurrentDescriptorByNameUrl()+'/'+getDescriptorUrl();
    }

K
Kohsuke Kawaguchi 已提交
351 352 353 354
    /**
     * @since 1.402
     */
    public static String getCurrentDescriptorByNameUrl() {
355
        StaplerRequest req = Stapler.getCurrentRequest();
356 357 358 359 360

        // this override allows RenderOnDemandClosure to preserve the proper value
        Object url = req.getAttribute("currentDescriptorByNameUrl");
        if (url!=null)  return url.toString();

361 362 363 364
        Ancestor a = req.findAncestor(DescriptorByNameOwner.class);
        return a.getUrl();
    }

K
Kohsuke Kawaguchi 已提交
365 366 367 368 369 370 371 372
    /**
     * @deprecated since 1.528
     *      Use {@link #getCheckMethod(String)}
     */
    public String getCheckUrl(String fieldName) {
        return getCheckMethod(fieldName).toCheckUrl();
    }

373 374
    /**
     * If the field "xyz" of a {@link Describable} has the corresponding "doCheckXyz" method,
K
Kohsuke Kawaguchi 已提交
375
     * return the model of the check method.
376
     * <p>
K
kohsuke 已提交
377
     * This method is used to hook up the form validation method to the corresponding HTML input element.
378
     */
K
Kohsuke Kawaguchi 已提交
379 380
    public CheckMethod getCheckMethod(String fieldName) {
        CheckMethod method = checkMethods.get(fieldName);
381
        if(method==null) {
K
Kohsuke Kawaguchi 已提交
382
            method = new CheckMethod(this,fieldName);
383 384 385
            checkMethods.put(fieldName,method);
        }

K
Kohsuke Kawaguchi 已提交
386
        return method;
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
    }

    /**
     * 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
402
        List<String> depends = buildFillDependencies(method, new ArrayList<String>());
403

404 405 406 407 408 409
        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) {
410 411
        for (Parameter p : ReflectionUtils.getParameters(method)) {
            QueryParameter qp = p.annotation(QueryParameter.class);
412 413 414 415 416
            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.
417

418 419 420 421
                RelativePath rp = p.annotation(RelativePath.class);
                if (rp!=null)
                    name = rp.value()+'/'+name;

422 423 424
                depends.add(name);
                continue;
            }
425

426 427 428
            Method m = ReflectionUtils.getPublicMethodNamed(p.type(), "fromStapler");
            if (m!=null)
                buildFillDependencies(m,depends);
429
        }
430
        return depends;
K
kohsuke 已提交
431 432
    }

433 434 435 436 437 438 439 440 441 442 443 444 445
    /**
     * 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));
    }

446 447 448
    /**
     * Used by Jelly to abstract away the handlign of global.jelly vs config.jelly databinding difference.
     */
449
    public @CheckForNull PropertyType getPropertyType(@Nonnull Object instance, @Nonnull String field) {
450 451 452 453
        // in global.jelly, instance==descriptor
        return instance==this ? getGlobalPropertyType(field) : getPropertyType(field);
    }

454
    /**
455
     * Akin to {@link #getPropertyType(Object,String)} but never returns null.
456 457 458 459 460 461 462 463 464 465 466 467 468 469
     * @throws AssertionError in case the field cannot be found
     * @since 1.492
     */
    public @Nonnull PropertyType getPropertyTypeOrDie(@Nonnull Object instance, @Nonnull String field) {
        PropertyType propertyType = getPropertyType(instance, field);
        if (propertyType != null) {
            return propertyType;
        } else if (instance == this) {
            throw new AssertionError(getClass().getName() + " has no property " + field);
        } else {
            throw new AssertionError(clazz.getName() + " has no property " + field);
        }
    }

K
kohsuke 已提交
470 471 472 473
    /**
     * Obtains the property type of the given field of {@link #clazz}
     */
    public PropertyType getPropertyType(String field) {
474 475 476 477
        if(propertyTypes==null)
            propertyTypes = buildPropertyTypes(clazz);
        return propertyTypes.get(field);
    }
K
kohsuke 已提交
478

479 480 481 482 483 484 485 486
    /**
     * 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);
    }
K
kohsuke 已提交
487

488 489 490 491 492 493 494 495 496 497 498 499 500
    /**
     * 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;
501 502
    }

503 504 505
    /**
     * Gets the class name nicely escaped to be usable as a key in the structured form submission.
     */
K
kohsuke 已提交
506
    public final String getJsonSafeClassName() {
507
        return getId().replace('.','-');
508 509
    }

510 511 512 513 514 515 516 517 518
    /**
     * @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)");
    }

K
kohsuke 已提交
519 520 521 522 523 524 525
    /**
     * 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.
     *
526 527 528 529 530 531 532 533
     * <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}.
     *
534 535 536 537 538 539 540 541
     * <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.
     *
K
kohsuke 已提交
542
     * @param req
543
     *      Always non-null (see note above.) This object includes represents the entire submission.
544 545
     * @param formData
     *      The JSON object that captures the configuration data for this {@link Descriptor}.
K
Kohsuke Kawaguchi 已提交
546
     *      See http://wiki.jenkins-ci.org/display/JENKINS/Structured+Form+Submission
547
     *      Always non-null.
K
kohsuke 已提交
548 549 550
     *
     * @throws FormException
     *      Signals a problem in the submitted form.
551
     * @since 1.145
K
kohsuke 已提交
552
     */
553
    public T newInstance(StaplerRequest req, JSONObject formData) throws FormException {
554 555 556
        try {
            Method m = getClass().getMethod("newInstance", StaplerRequest.class);

K
kohsuke 已提交
557
            if(!Modifier.isAbstract(m.getDeclaringClass().getModifiers())) {
558 559
                // this class overrides newInstance(StaplerRequest).
                // maintain the backward compatible behavior
560
                return verifyNewInstance(newInstance(req));
561
            } else {
562 563
                if (req==null) {
                    // yes, req is supposed to be always non-null, but see the note above
564
                    return verifyNewInstance(clazz.newInstance());
565 566
                }

567
                // new behavior as of 1.206
568
                return verifyNewInstance(req.bindJSON(clazz,formData));
569 570 571
            }
        } catch (NoSuchMethodException e) {
            throw new AssertionError(e); // impossible
572
        } catch (InstantiationException e) {
573
            throw new Error("Failed to instantiate "+clazz+" from "+formData,e);
574
        } catch (IllegalAccessException e) {
575 576 577
            throw new Error("Failed to instantiate "+clazz+" from "+formData,e);
        } catch (RuntimeException e) {
            throw new RuntimeException("Failed to instantiate "+clazz+" from "+formData,e);
578
        }
579
    }
K
kohsuke 已提交
580

581 582 583 584 585 586 587 588 589 590 591 592
    /**
     * 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;
    }

593 594 595 596 597 598 599 600 601
    /**
     * 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);
    }

K
kohsuke 已提交
602 603
    /**
     * Returns the resource path to the help screen HTML, if any.
K
kohsuke 已提交
604 605
     *
     * <p>
K
kohsuke 已提交
606 607 608 609 610
     * Starting 1.282, this method uses "convention over configuration" &mdash; 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>
K
kohsuke 已提交
611 612 613 614 615 616
     * 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
617
     *      null to indicate that there's no help.
K
kohsuke 已提交
618 619
     */
    public String getHelpFile() {
620 621 622 623 624 625 626 627 628 629
        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.
     */
630
    public String getHelpFile(final String fieldName) {
631 632 633 634
        return getHelpFile(getKlass(),fieldName);
    }

    public String getHelpFile(Klass<?> clazz, String fieldName) {
K
Kohsuke Kawaguchi 已提交
635 636
        HelpRedirect r = helpRedirect.get(fieldName);
        if (r!=null)    return r.resolve();
637

638
        for (Klass<?> c : clazz.getAncestors()) {
639
            String page = "/descriptor/" + getId() + "/help";
640 641 642 643 644 645 646
            String suffix;
            if(fieldName==null) {
                suffix="";
            } else {
                page += '/'+fieldName;
                suffix='-'+fieldName;
            }
647

648 649 650 651 652 653
            try {
                if(Stapler.getCurrentRequest().getView(c,"help"+suffix)!=null)
                    return page;
            } catch (IOException e) {
                throw new Error(e);
            }
654

655
            if(getStaticHelpUrl(c, suffix) !=null)    return page;
656
        }
657
        return null;
K
kohsuke 已提交
658
    }
659
    
660 661 662
    /**
     * Tells Jenkins that the help file for the field 'fieldName' is defined in the help file for
     * the 'fieldNameToRedirectTo' in the 'owner' class.
O
Olivier Lamy 已提交
663
     * @since 1.425
664 665
     */
    protected void addHelpFileRedirect(String fieldName, Class<? extends Describable> owner, String fieldNameToRedirectTo) {
K
Kohsuke Kawaguchi 已提交
666
        helpRedirect.put(fieldName, new HelpRedirect(owner,fieldNameToRedirectTo));
667 668
    }

K
kohsuke 已提交
669 670 671 672 673 674 675
    /**
     * Checks if the given object is created from this {@link Descriptor}.
     */
    public final boolean isInstance( T instance ) {
        return clazz.isInstance(instance);
    }

K
kohsuke 已提交
676 677 678 679 680 681 682
    /**
     * 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);
    }

683 684 685 686 687
    /**
     * @deprecated
     *      As of 1.239, use {@link #configure(StaplerRequest, JSONObject)}.
     */
    public boolean configure( StaplerRequest req ) throws FormException {
688
        return true;
689 690
    }

K
kohsuke 已提交
691 692 693
    /**
     * Invoked when the global configuration page is submitted.
     *
K
kohsuke 已提交
694
     * Can be overriden to store descriptor-specific information.
K
kohsuke 已提交
695
     *
K
typo  
kohsuke 已提交
696
     * @param json
697
     *      The JSON object that captures the configuration data for this {@link Descriptor}.
K
Kohsuke Kawaguchi 已提交
698
     *      See http://wiki.jenkins-ci.org/display/JENKINS/Structured+Form+Submission
K
kohsuke 已提交
699 700 701
     * @return false
     *      to keep the client in the same config page.
     */
702
    public boolean configure( StaplerRequest req, JSONObject json ) throws FormException {
703
        // compatibility
704
        return configure(req);
K
kohsuke 已提交
705 706
    }

707
    public String getConfigPage() {
708
        return getViewPage(clazz, getPossibleViewNames("config"), "config.jelly");
K
kohsuke 已提交
709 710
    }

711
    public String getGlobalConfigPage() {
712
        return getViewPage(clazz, getPossibleViewNames("global"), null);
713
    }
714
    
715
    private String getViewPage(Class<?> clazz, String pageName, String defaultValue) {
716 717 718 719
        return getViewPage(clazz,Collections.singleton(pageName),defaultValue);
    }

    private String getViewPage(Class<?> clazz, Collection<String> pageNames, String defaultValue) {
720
        while(clazz!=Object.class && clazz!=null) {
721 722 723 724 725
            for (String pageName : pageNames) {
                String name = clazz.getName().replace('.', '/').replace('$', '/') + "/" + pageName;
                if(clazz.getClassLoader().getResource(name)!=null)
                    return '/'+name;
            }
K
kohsuke 已提交
726 727
            clazz = clazz.getSuperclass();
        }
728 729 730 731
        return defaultValue;
    }

    protected final String getViewPage(Class<?> clazz, String pageName) {
K
kohsuke 已提交
732 733 734 735 736
        // 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.
737
        return getViewPage(clazz,pageName,pageName);
K
kohsuke 已提交
738 739
    }

K
Kohsuke Kawaguchi 已提交
740
    protected List<String> getPossibleViewNames(String baseName) {
741
        List<String> names = new ArrayList<String>();
742
        for (Facet f : WebApp.get(Jenkins.getInstance().servletContext).facets) {
743 744
            if (f instanceof JellyCompatibleFacet) {
                JellyCompatibleFacet jcf = (JellyCompatibleFacet) f;
745 746
                for (String ext : jcf.getScriptExtensions())
                    names.add(baseName +ext);
747 748 749 750 751
            }
        }
        return names;
    }

K
kohsuke 已提交
752 753 754 755

    /**
     * Saves the configuration info to the disk.
     */
756 757
    public synchronized void save() {
        if(BulkChange.contains(this))   return;
758 759
        try {
            getConfigFile().write(this);
760
            SaveableListener.fireOnChange(this, getConfigFile());
761 762 763
        } catch (IOException e) {
            LOGGER.log(Level.WARNING, "Failed to save "+getConfigFile(),e);
        }
K
kohsuke 已提交
764 765
    }

766 767
    /**
     * Loads the data from the disk into this object.
K
kohsuke 已提交
768 769 770 771 772
     *
     * <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.)
773
     */
774
    public synchronized void load() {
K
kohsuke 已提交
775 776
        XmlFile file = getConfigFile();
        if(!file.exists())
777
            return;
K
kohsuke 已提交
778 779

        try {
780
            file.unmarshal(this);
K
kohsuke 已提交
781
        } catch (IOException e) {
782
            LOGGER.log(Level.WARNING, "Failed to load "+file, e);
K
kohsuke 已提交
783 784 785
        }
    }

786
    protected XmlFile getConfigFile() {
787
        return new XmlFile(new File(Jenkins.getInstance().getRootDir(),getId()+".xml"));
K
kohsuke 已提交
788 789
    }

790 791 792 793 794 795 796 797 798 799
    /**
     * 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);
    }

K
kohsuke 已提交
800 801 802
    /**
     * Serves <tt>help.html</tt> from the resource of {@link #clazz}.
     */
803 804 805 806
    public void doHelp(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        String path = req.getRestOfPath();
        if(path.contains("..")) throw new ServletException("Illegal path: "+path);

807 808
        path = path.replace('/','-');

809 810 811 812 813 814 815 816
        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()));
        }

817
        for (Klass<?> c= getKlass(); c!=null; c=c.getSuperClass()) {
818
            RequestDispatcher rd = Stapler.getCurrentRequest().getView(c, "help"+path);
819
            if(rd!=null) {// template based help page
820 821 822
                rd.forward(req,rsp);
                return;
            }
823

824 825
            URL url = getStaticHelpUrl(c, path);
            if(url!=null) {
826 827
                // TODO: generalize macro expansion and perhaps even support JEXL
                rsp.setContentType("text/html;charset=UTF-8");
828 829 830 831 832 833 834
                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);
                }
835 836
                return;
            }
K
kohsuke 已提交
837
        }
838
        rsp.sendError(SC_NOT_FOUND);
K
kohsuke 已提交
839 840
    }

841
    private URL getStaticHelpUrl(Klass<?> c, String suffix) {
K
kohsuke 已提交
842
        Locale locale = Stapler.getCurrentRequest().getLocale();
843 844 845 846 847 848 849 850 851 852

        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;
K
kohsuke 已提交
853 854

        // default
855
        return c.getResource(base + ".html");
K
kohsuke 已提交
856 857 858 859 860 861 862 863
    }


//
// static methods
//


K
kohsuke 已提交
864 865 866 867 868 869
    // 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 ) {
870
        return new ArrayList<T>(Arrays.asList(values));
K
kohsuke 已提交
871 872 873
    }

    public static <T extends Describable<T>>
874
    Map<Descriptor<T>,T> toMap(Iterable<T> describables) {
K
kohsuke 已提交
875 876 877 878 879 880 881
        Map<Descriptor<T>,T> m = new LinkedHashMap<Descriptor<T>,T>();
        for (T d : describables) {
            m.put(d.getDescriptor(),d);
        }
        return m;
    }

882 883 884 885 886 887 888 889 890 891 892
    /**
     * Used to build {@link Describable} instance list from &lt;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.
893 894
     * @return
     *      Can be empty but never null.
895 896 897 898 899
     */
    public static <T extends Describable<T>>
    List<T> newInstancesFromHeteroList(StaplerRequest req, JSONObject formData, String key,
                Collection<? extends Descriptor<T>> descriptors) throws FormException {

900 901 902 903 904 905 906 907 908
        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>();

K
Kohsuke Kawaguchi 已提交
909 910 911
        if (formData!=null) {
            for (Object o : JSONArray.fromObject(formData)) {
                JSONObject jo = (JSONObject)o;
912 913 914
                Descriptor<T> d = null;
                String kind = jo.optString("kind", null);
                if (kind != null) {
915
                    d = findById(descriptors, kind);
916 917 918
                }
                if (d == null) {
                  kind = jo.getString("$class");
919 920
                  d = findByDescribableClassName(descriptors, kind);
                  if (d == null) d = findByClassName(descriptors, kind);
921
                }
J
Jesse Glick 已提交
922 923
                if (d != null) {
                    items.add(d.newInstance(req, jo));
924 925
                } else {
                    LOGGER.warning("Received unexpected formData for descriptor " + kind);
J
Jesse Glick 已提交
926
                }
K
Kohsuke Kawaguchi 已提交
927
            }
928 929 930 931 932 933
        }

        return items;
    }

    /**
934
     * Finds a descriptor from a collection by its id.
935
     */
936
    public static @CheckForNull <T extends Descriptor> T findById(Collection<? extends T> list, String id) {
937
        for (T d : list) {
938
            if(d.getId().equals(id))
939 940
                return d;
        }
941 942 943 944 945 946 947 948
        return null;
    }

    /**
     * Finds a descriptor from a collection by its class name.
     * @deprecated Since we introduced {@link Descriptor#getId()}, it is a preferred method of identifying descriptor by a string.
     */
    public static @CheckForNull <T extends Descriptor> T findByClassName(Collection<? extends T> list, String className) {
949
        for (T d : list) {
950
            if(d.getClass().getName().equals(className))
951 952
                return d;
        }
953 954 955
        return null;
    }

956 957 958 959 960 961 962 963 964 965 966
    /**
     * Finds a descriptor from a collection by the class name of the Describable it describes.
     */
    public static @CheckForNull <T extends Descriptor> T findByDescribableClassName(Collection<? extends T> list, String className) {
        for (T d : list) {
            if(d.clazz.getName().equals(className))
                return d;
        }
        return null;
    }

967 968 969 970 971 972 973 974 975 976 977 978
    /**
     * Finds a descriptor from a collection by its class name or ID.
     * @deprecated choose between {@link #findById(java.util.Collection, String)} or {@link #findByClassName(java.util.Collection, String)}
     */
    public static @CheckForNull <T extends Descriptor> T find(Collection<? extends T> list, String string) {
        T d = findByClassName(list, string);
        if (d != null) {
                return d;
        }
        return findById(list, string);
    }

J
Jesse Glick 已提交
979
    public static @CheckForNull Descriptor find(String className) {
980
        return find(ExtensionList.lookup(Descriptor.class),className);
K
kohsuke 已提交
981 982
    }

983
    public static final class FormException extends Exception implements HttpResponse {
K
kohsuke 已提交
984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006
        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;
        }
1007 1008

        public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
1009
            if (FormApply.isApply(req)) {
1010
                FormApply.applyResponse("notificationBar.show(" + quote(getMessage())+ ",notificationBar.ERROR)")
1011 1012 1013 1014 1015
                        .generateResponse(req, rsp, node);
            } else {
                // for now, we can't really use the field name that caused the problem.
                new Failure(getMessage()).generateResponse(req,rsp,node);
            }
1016
        }
K
kohsuke 已提交
1017
    }
1018 1019

    private static final Logger LOGGER = Logger.getLogger(Descriptor.class.getName());
1020

1021 1022 1023 1024 1025 1026 1027
    /**
     * Special type indicating that {@link Descriptor} describes itself.
     * @see Descriptor#Descriptor(Class)
     */
    public static final class Self {}

    protected static Class self() { return Self.class; }
M
mindless 已提交
1028
}