Descriptor.java 34.9 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.RelativePath;
K
kohsuke 已提交
28
import hudson.XmlFile;
29
import hudson.BulkChange;
30
import hudson.Util;
31
import static hudson.Functions.jsStringEscape;
32
import hudson.model.listeners.SaveableListener;
33 34
import hudson.util.ReflectionUtils;
import hudson.util.ReflectionUtils.Parameter;
35
import hudson.views.ListViewColumn;
36
import jenkins.model.Jenkins;
37 38
import net.sf.json.JSONArray;
import net.sf.json.JSONObject;
39
import org.kohsuke.stapler.*;
40
import org.kohsuke.stapler.jelly.JellyCompatibleFacet;
41
import org.springframework.util.StringUtils;
K
kohsuke 已提交
42
import org.jvnet.tiger_types.Types;
K
kohsuke 已提交
43
import org.apache.commons.io.IOUtils;
K
kohsuke 已提交
44

K
kohsuke 已提交
45
import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
46 47
import javax.servlet.ServletException;
import javax.servlet.RequestDispatcher;
K
kohsuke 已提交
48 49
import java.io.File;
import java.io.IOException;
K
kohsuke 已提交
50
import java.io.InputStream;
K
kohsuke 已提交
51
import java.util.ArrayList;
52
import java.util.Collection;
K
kohsuke 已提交
53
import java.util.LinkedHashMap;
54 55
import java.util.List;
import java.util.Map;
K
kohsuke 已提交
56
import java.util.HashMap;
K
kohsuke 已提交
57
import java.util.Locale;
58
import java.util.Arrays;
59
import java.util.Collections;
60
import java.util.concurrent.ConcurrentHashMap;
61 62
import java.util.logging.Level;
import java.util.logging.Logger;
63
import java.lang.reflect.Method;
K
kohsuke 已提交
64
import java.lang.reflect.Modifier;
K
kohsuke 已提交
65 66 67 68
import java.lang.reflect.Type;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.beans.Introspector;
K
kohsuke 已提交
69 70 71 72 73 74

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

123
    private transient final Map<String,String> checkMethods = new ConcurrentHashMap<String,String>();
124

K
kohsuke 已提交
125
    /**
126
     * Lazily computed list of properties on {@link #clazz} and on the descriptor itself.
K
kohsuke 已提交
127
     */
128
    private transient volatile Map<String, PropertyType> propertyTypes,globalPropertyTypes;
K
kohsuke 已提交
129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182

    /**
     * Represents a readable property on {@link Describable}.
     */
    public static final class PropertyType {
        public final Class clazz;
        public final Type type;
        private volatile Class itemType;

        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() {
183
            return Jenkins.getInstance().getDescriptor(getItemType());
K
kohsuke 已提交
184
        }
185 186

        public Descriptor getItemTypeDescriptorOrDie() {
187
            return Jenkins.getInstance().getDescriptorOrDie(getItemType());
188
        }
189 190 191 192 193

        /**
         * Returns all the descriptors that produce types assignable to the item type.
         */
        public List<? extends Descriptor> getApplicableDescriptors() {
194
            return Jenkins.getInstance().getDescriptorList(clazz);
195
        }
K
kohsuke 已提交
196 197
    }

198 199 200 201 202 203 204
    /**
     * Help file redirect, keyed by the field name to the path.
     *
     * @see #getHelpFile(String) 
     */
    private final Map<String,String> helpRedirect = new HashMap<String, String>();

205 206 207 208 209 210
    /**
     *
     * @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 已提交
211
    protected Descriptor(Class<? extends T> clazz) {
212 213
        if (clazz==self())
            clazz = (Class)getClass();
K
kohsuke 已提交
214
        this.clazz = clazz;
215 216 217
        // doing this turns out to be very error prone,
        // as field initializers in derived types will override values.
        // load();
K
kohsuke 已提交
218 219
    }

220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240
    /**
     * 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?");
        }
241 242 243 244 245 246 247 248 249 250 251 252

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

253 254
    }

K
kohsuke 已提交
255 256 257 258 259
    /**
     * Human readable name of this kind of configurable object.
     */
    public abstract String getDisplayName();

260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276
    /**
     * 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();
    }

277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292
    /**
     * 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));
    }

293 294
    /**
     * Gets the URL that this Descriptor is bound to, relative to the nearest {@link DescriptorByNameOwner}.
295
     * Since {@link Jenkins} is a {@link DescriptorByNameOwner}, there's always one such ancestor to any request.
296
     */
297
    public String getDescriptorUrl() {
298
        return "descriptorByName/"+getId();
299 300
    }

K
Kohsuke Kawaguchi 已提交
301 302 303 304 305 306 307 308
    /**
     * 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 已提交
309 310 311 312
    /**
     * @since 1.402
     */
    public static String getCurrentDescriptorByNameUrl() {
313
        StaplerRequest req = Stapler.getCurrentRequest();
314 315 316 317 318

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

319 320 321 322
        Ancestor a = req.findAncestor(DescriptorByNameOwner.class);
        return a.getUrl();
    }

323 324 325 326
    /**
     * If the field "xyz" of a {@link Describable} has the corresponding "doCheckXyz" method,
     * return the form-field validation string. Otherwise null.
     * <p>
K
kohsuke 已提交
327
     * This method is used to hook up the form validation method to the corresponding HTML input element.
328 329
     */
    public String getCheckUrl(String fieldName) {
330 331 332 333 334 335
        String method = checkMethods.get(fieldName);
        if(method==null) {
            method = calcCheckUrl(fieldName);
            checkMethods.put(fieldName,method);
        }

336 337 338 339 340
        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
341
        return '\'' + jsStringEscape(getCurrentDescriptorByNameUrl()) + "/'+" + method;
342 343 344
    }

    private String calcCheckUrl(String fieldName) {
345 346
        String capitalizedFieldName = StringUtils.capitalize(fieldName);

347
        Method method = ReflectionUtils.getPublicMethodNamed(getClass(),"doCheck"+ capitalizedFieldName);
348

349 350 351
        if(method==null)
            return NONE;

352
        return '\'' + getDescriptorUrl() + "/check" + capitalizedFieldName + '\'' + buildParameterList(method, new StringBuilder()).append(".toString()");
353 354 355 356 357 358
    }

    /**
     * Builds query parameter line by figuring out what should be submitted
     */
    private StringBuilder buildParameterList(Method method, StringBuilder query) {
359 360
        for (Parameter p : ReflectionUtils.getParameters(method)) {
            QueryParameter qp = p.annotation(QueryParameter.class);
361 362 363 364 365 366
            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.

367 368 369 370
                RelativePath rp = p.annotation(RelativePath.class);
                if (rp!=null)
                    name = rp.value()+'/'+name;

K
kohsuke 已提交
371
                if (query.length()==0)  query.append("+qs(this)");
372 373 374

                if (name.equals("value")) {
                    // The special 'value' parameter binds to the the current field
K
kohsuke 已提交
375
                    query.append(".addThis()");
376
                } else {
K
kohsuke 已提交
377
                    query.append(".nearBy('"+name+"')");
378 379
                }
                continue;
380
            }
381

382 383 384 385
            Method m = ReflectionUtils.getPublicMethodNamed(p.type(), "fromStapler");
            if (m!=null)    buildParameterList(m,query);
        }
        return query;
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400
    }

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

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

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

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

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

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

445 446 447 448 449 450 451 452
    /**
     * 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);
    }

K
kohsuke 已提交
453 454 455 456
    /**
     * Obtains the property type of the given field of {@link #clazz}
     */
    public PropertyType getPropertyType(String field) {
457 458 459 460
        if(propertyTypes==null)
            propertyTypes = buildPropertyTypes(clazz);
        return propertyTypes.get(field);
    }
K
kohsuke 已提交
461

462 463 464 465 466 467 468 469
    /**
     * 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 已提交
470

471 472 473 474 475 476 477 478 479 480 481 482 483
    /**
     * 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;
484 485
    }

486 487 488
    /**
     * Gets the class name nicely escaped to be usable as a key in the structured form submission.
     */
K
kohsuke 已提交
489
    public final String getJsonSafeClassName() {
490
        return getId().replace('.','-');
491 492
    }

493 494 495 496 497 498 499 500 501
    /**
     * @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 已提交
502 503 504 505 506 507 508
    /**
     * 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.
     *
509 510 511 512 513 514 515 516
     * <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}.
     *
517 518 519 520 521 522 523 524
     * <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 已提交
525
     * @param req
526
     *      Always non-null (see note above.) This object includes represents the entire submission.
527 528
     * @param formData
     *      The JSON object that captures the configuration data for this {@link Descriptor}.
K
Kohsuke Kawaguchi 已提交
529
     *      See http://wiki.jenkins-ci.org/display/JENKINS/Structured+Form+Submission
530
     *      Always non-null.
K
kohsuke 已提交
531 532 533
     *
     * @throws FormException
     *      Signals a problem in the submitted form.
534
     * @since 1.145
K
kohsuke 已提交
535
     */
536
    public T newInstance(StaplerRequest req, JSONObject formData) throws FormException {
537 538 539
        try {
            Method m = getClass().getMethod("newInstance", StaplerRequest.class);

K
kohsuke 已提交
540
            if(!Modifier.isAbstract(m.getDeclaringClass().getModifiers())) {
541 542
                // this class overrides newInstance(StaplerRequest).
                // maintain the backward compatible behavior
543
                return verifyNewInstance(newInstance(req));
544
            } else {
545 546
                if (req==null) {
                    // yes, req is supposed to be always non-null, but see the note above
547
                    return verifyNewInstance(clazz.newInstance());
548 549
                }

550
                // new behavior as of 1.206
551
                return verifyNewInstance(req.bindJSON(clazz,formData));
552 553 554
            }
        } catch (NoSuchMethodException e) {
            throw new AssertionError(e); // impossible
555
        } catch (InstantiationException e) {
556
            throw new Error("Failed to instantiate "+clazz+" from "+formData,e);
557
        } catch (IllegalAccessException e) {
558 559 560
            throw new Error("Failed to instantiate "+clazz+" from "+formData,e);
        } catch (RuntimeException e) {
            throw new RuntimeException("Failed to instantiate "+clazz+" from "+formData,e);
561
        }
562
    }
K
kohsuke 已提交
563

564 565 566 567 568 569 570 571 572 573 574 575
    /**
     * 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;
    }

K
kohsuke 已提交
576 577
    /**
     * Returns the resource path to the help screen HTML, if any.
K
kohsuke 已提交
578 579
     *
     * <p>
K
kohsuke 已提交
580 581 582 583 584
     * 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 已提交
585 586 587 588 589 590
     * 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
591
     *      null to indicate that there's no help.
K
kohsuke 已提交
592 593
     */
    public String getHelpFile() {
594 595 596 597 598 599 600 601 602 603
        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.
     */
604
    public String getHelpFile(final String fieldName) {
605 606 607
        String v = helpRedirect.get(fieldName);
        if (v!=null)    return v;

608
        for(Class c=clazz; c!=null; c=c.getSuperclass()) {
609
            String page = "/descriptor/" + getId() + "/help";
610 611 612 613 614 615 616
            String suffix;
            if(fieldName==null) {
                suffix="";
            } else {
                page += '/'+fieldName;
                suffix='-'+fieldName;
            }
617

618 619 620 621 622 623
            try {
                if(Stapler.getCurrentRequest().getView(c,"help"+suffix)!=null)
                    return page;
            } catch (IOException e) {
                throw new Error(e);
            }
624

625 626 627 628
            InputStream in = getHelpStream(c,suffix);
            IOUtils.closeQuietly(in);
            if(in!=null)    return page;
        }
629
        return null;
K
kohsuke 已提交
630 631
    }

632 633 634 635 636 637 638 639 640
    /**
     * Tells Jenkins that the help file for the field 'fieldName' is defined in the help file for
     * the 'fieldNameToRedirectTo' in the 'owner' class.
     */
    protected void addHelpFileRedirect(String fieldName, Class<? extends Describable> owner, String fieldNameToRedirectTo) {
        helpRedirect.put(fieldName,
            Jenkins.getInstance().getDescriptor(owner).getHelpFile(fieldNameToRedirectTo));
    }

K
kohsuke 已提交
641 642 643 644 645 646 647
    /**
     * Checks if the given object is created from this {@link Descriptor}.
     */
    public final boolean isInstance( T instance ) {
        return clazz.isInstance(instance);
    }

K
kohsuke 已提交
648 649 650 651 652 653 654
    /**
     * 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);
    }

655 656 657 658 659
    /**
     * @deprecated
     *      As of 1.239, use {@link #configure(StaplerRequest, JSONObject)}.
     */
    public boolean configure( StaplerRequest req ) throws FormException {
660
        return true;
661 662
    }

K
kohsuke 已提交
663 664 665
    /**
     * Invoked when the global configuration page is submitted.
     *
K
kohsuke 已提交
666
     * Can be overriden to store descriptor-specific information.
K
kohsuke 已提交
667
     *
K
typo  
kohsuke 已提交
668
     * @param json
669
     *      The JSON object that captures the configuration data for this {@link Descriptor}.
K
Kohsuke Kawaguchi 已提交
670
     *      See http://wiki.jenkins-ci.org/display/JENKINS/Structured+Form+Submission
K
kohsuke 已提交
671 672 673
     * @return false
     *      to keep the client in the same config page.
     */
674
    public boolean configure( StaplerRequest req, JSONObject json ) throws FormException {
675
        // compatibility
676
        return configure(req);
K
kohsuke 已提交
677 678
    }

679
    public String getConfigPage() {
680
        return getViewPage(clazz, getPossibleViewNames("config"), "config.jelly");
K
kohsuke 已提交
681 682
    }

683
    public String getGlobalConfigPage() {
684
        return getViewPage(clazz, getPossibleViewNames("global"), null);
685
    }
686
    
687
    private String getViewPage(Class<?> clazz, String pageName, String defaultValue) {
688 689 690 691
        return getViewPage(clazz,Collections.singleton(pageName),defaultValue);
    }

    private String getViewPage(Class<?> clazz, Collection<String> pageNames, String defaultValue) {
692
        while(clazz!=Object.class && clazz!=null) {
693 694 695 696 697
            for (String pageName : pageNames) {
                String name = clazz.getName().replace('.', '/').replace('$', '/') + "/" + pageName;
                if(clazz.getClassLoader().getResource(name)!=null)
                    return '/'+name;
            }
K
kohsuke 已提交
698 699
            clazz = clazz.getSuperclass();
        }
700 701 702 703
        return defaultValue;
    }

    protected final String getViewPage(Class<?> clazz, String pageName) {
K
kohsuke 已提交
704 705 706 707 708
        // 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.
709
        return getViewPage(clazz,pageName,pageName);
K
kohsuke 已提交
710 711
    }

712 713
    private List<String> getPossibleViewNames(String baseName) {
        List<String> names = new ArrayList<String>();
714
        for (Facet f : WebApp.get(Jenkins.getInstance().servletContext).facets) {
715 716
            if (f instanceof JellyCompatibleFacet) {
                JellyCompatibleFacet jcf = (JellyCompatibleFacet) f;
717 718
                for (String ext : jcf.getScriptExtensions())
                    names.add(baseName +ext);
719 720 721 722 723
            }
        }
        return names;
    }

K
kohsuke 已提交
724 725 726 727

    /**
     * Saves the configuration info to the disk.
     */
728 729
    public synchronized void save() {
        if(BulkChange.contains(this))   return;
730 731
        try {
            getConfigFile().write(this);
732
            SaveableListener.fireOnChange(this, getConfigFile());
733 734 735
        } catch (IOException e) {
            LOGGER.log(Level.WARNING, "Failed to save "+getConfigFile(),e);
        }
K
kohsuke 已提交
736 737
    }

738 739
    /**
     * Loads the data from the disk into this object.
K
kohsuke 已提交
740 741 742 743 744
     *
     * <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.)
745
     */
746
    public synchronized void load() {
K
kohsuke 已提交
747 748
        XmlFile file = getConfigFile();
        if(!file.exists())
749
            return;
K
kohsuke 已提交
750 751

        try {
752
            file.unmarshal(this);
K
kohsuke 已提交
753
        } catch (IOException e) {
754
            LOGGER.log(Level.WARNING, "Failed to load "+file, e);
K
kohsuke 已提交
755 756 757 758
        }
    }

    private XmlFile getConfigFile() {
759
        return new XmlFile(new File(Jenkins.getInstance().getRootDir(),getId()+".xml"));
K
kohsuke 已提交
760 761
    }

K
kohsuke 已提交
762 763 764
    /**
     * Serves <tt>help.html</tt> from the resource of {@link #clazz}.
     */
765 766 767 768
    public void doHelp(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
        String path = req.getRestOfPath();
        if(path.contains("..")) throw new ServletException("Illegal path: "+path);

769 770
        path = path.replace('/','-');

771 772 773 774 775 776
        for (Class c=clazz; c!=null; c=c.getSuperclass()) {
            RequestDispatcher rd = Stapler.getCurrentRequest().getView(c, "help"+path);
            if(rd!=null) {// Jelly-generated help page
                rd.forward(req,rsp);
                return;
            }
777

778 779 780 781 782 783 784 785 786
            InputStream in = getHelpStream(c,path);
            if(in!=null) {
                // TODO: generalize macro expansion and perhaps even support JEXL
                rsp.setContentType("text/html;charset=UTF-8");
                String literal = IOUtils.toString(in,"UTF-8");
                rsp.getWriter().println(Util.replaceMacro(literal, Collections.singletonMap("rootURL",req.getContextPath())));
                in.close();
                return;
            }
K
kohsuke 已提交
787
        }
788
        rsp.sendError(SC_NOT_FOUND);
K
kohsuke 已提交
789 790
    }

791
    private InputStream getHelpStream(Class c, String suffix) {
K
kohsuke 已提交
792
        Locale locale = Stapler.getCurrentRequest().getLocale();
793
        String base = c.getName().replace('.', '/').replace('$','/') + "/help"+suffix;
K
kohsuke 已提交
794

795 796 797
        ClassLoader cl = c.getClassLoader();
        if(cl==null)    return null;
        
K
kohsuke 已提交
798
        InputStream in;
799
        in = cl.getResourceAsStream(base + '_' + locale.getLanguage() + '_' + locale.getCountry() + '_' + locale.getVariant() + ".html");
K
kohsuke 已提交
800
        if(in!=null)    return in;
801
        in = cl.getResourceAsStream(base + '_' + locale.getLanguage() + '_' + locale.getCountry() + ".html");
K
kohsuke 已提交
802
        if(in!=null)    return in;
803
        in = cl.getResourceAsStream(base + '_' + locale.getLanguage() + ".html");
K
kohsuke 已提交
804 805 806
        if(in!=null)    return in;

        // default
807
        return cl.getResourceAsStream(base+".html");
K
kohsuke 已提交
808 809 810 811 812 813 814 815
    }


//
// static methods
//


K
kohsuke 已提交
816 817 818 819 820 821
    // 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 ) {
822
        return new ArrayList<T>(Arrays.asList(values));
K
kohsuke 已提交
823 824 825
    }

    public static <T extends Describable<T>>
826
    Map<Descriptor<T>,T> toMap(Iterable<T> describables) {
K
kohsuke 已提交
827 828 829 830 831 832 833
        Map<Descriptor<T>,T> m = new LinkedHashMap<Descriptor<T>,T>();
        for (T d : describables) {
            m.put(d.getDescriptor(),d);
        }
        return m;
    }

834 835 836 837 838 839 840 841 842 843 844
    /**
     * 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.
845 846
     * @return
     *      Can be empty but never null.
847 848 849 850 851
     */
    public static <T extends Describable<T>>
    List<T> newInstancesFromHeteroList(StaplerRequest req, JSONObject formData, String key,
                Collection<? extends Descriptor<T>> descriptors) throws FormException {

852 853 854 855 856 857 858 859 860
        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 已提交
861 862 863 864 865 866
        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));
            }
867 868 869 870 871 872
        }

        return items;
    }

    /**
873
     * Finds a descriptor from a collection by its class name.
874
     */
875
    public static <T extends Descriptor> T find(Collection<? extends T> list, String className) {
876
        for (T d : list) {
877
            if(d.getClass().getName().equals(className))
878 879
                return d;
        }
880 881 882 883 884 885
        // Since we introduced Descriptor.getId(), it is a preferred method of identifying descriptor by a string.
        // To make that migration easier without breaking compatibility, let's also match up with the id.
        for (T d : list) {
            if(d.getId().equals(className))
                return d;
        }
886 887 888
        return null;
    }

K
kohsuke 已提交
889
    public static Descriptor find(String className) {
890
        return find(Jenkins.getInstance().getExtensionList(Descriptor.class),className);
K
kohsuke 已提交
891 892
    }

893
    public static final class FormException extends Exception implements HttpResponse {
K
kohsuke 已提交
894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916
        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;
        }
917 918 919 920 921

        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);
        }
K
kohsuke 已提交
922
    }
923 924

    private static final Logger LOGGER = Logger.getLogger(Descriptor.class.getName());
925 926 927 928

    /**
     * Used in {@link #checkMethods} to indicate that there's no check method.
     */
929
    private static final String NONE = "\u0000";
930 931 932 933 934 935 936 937

    /**
     * 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 已提交
938
}