File D:\maven-qcr-plugin-testsunitaires\qcr4java\jenkins-master\target\site\qc\core\src\main\java\hudson\model\Descriptor.java.html

1 /*
2 * The MIT License
3 *
4 * Copyright (c) 2004-2011, Sun Microsystems, Inc., Kohsuke Kawaguchi
5 *
6 * Permission is hereby granted, free of charge, to any person obtaining a copy
7 * of this software and associated documentation files (the "Software"), to deal
8 * in the Software without restriction, including without limitation the rights
9 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 * copies of the Software, and to permit persons to whom the Software is
11 * furnished to do so, subject to the following conditions:
12 *
13 * The above copyright notice and this permission notice shall be included in
14 * all copies or substantial portions of the Software.
15 *
16 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 * THE SOFTWARE.
23 */
24 package hudson.model;
25
26 import hudson.DescriptorExtensionList;
27 import hudson.PluginWrapper;
28 import hudson.RelativePath;
29 import hudson.XmlFile;
30 import hudson.BulkChange;
31 import hudson.Util;
32 import hudson.model.listeners.SaveableListener;
33 import hudson.util.FormApply;
34 import hudson.util.ReflectionUtils;
35 import hudson.util.ReflectionUtils.Parameter;
36 import hudson.views.ListViewColumn;
37 import jenkins.model.Jenkins;
38 import net.sf.json.JSONArray;
39 import net.sf.json.JSONObject;
40 import org.kohsuke.stapler.*;
41 import org.kohsuke.stapler.jelly.JellyCompatibleFacet;
42 import org.kohsuke.stapler.lang.Klass;
43 import org.springframework.util.StringUtils;
44 import org.jvnet.tiger_types.Types;
45 import org.apache.commons.io.IOUtils;
46
47 import static hudson.Functions.*;
48 import static hudson.util.QuotedStringTokenizer.*;
49 import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
50 import javax.servlet.ServletException;
51 import javax.servlet.RequestDispatcher;
52 import java.io.File;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.net.URL;
56 import java.util.ArrayList;
57 import java.util.Collection;
58 import java.util.LinkedHashMap;
59 import java.util.List;
60 import java.util.Map;
61 import java.util.HashMap;
62 import java.util.Locale;
63 import java.util.Arrays;
64 import java.util.Collections;
65 import java.util.concurrent.ConcurrentHashMap;
66 import java.util.logging.Level;
67 import java.util.logging.Logger;
68 import java.lang.reflect.Method;
69 import java.lang.reflect.Modifier;
70 import java.lang.reflect.Type;
71 import java.lang.reflect.Field;
72 import java.lang.reflect.ParameterizedType;
73 import java.beans.Introspector;
74 import javax.annotation.CheckForNull;
75 import javax.annotation.Nonnull;
76
77 /**
78 * Metadata about a configurable instance.
79 *
80 * <p>
81 * {@link Descriptor} is an object that has metadata about a {@link Describable}
82 * object, and also serves as a factory (in a way this relationship is similar
83 * to {@link Object}/{@link Class} relationship.
84 *
85 * A {@link Descriptor}/{@link Describable}
86 * combination is used throughout in Hudson to implement a
87 * configuration/extensibility mechanism.
88 *
89 * <p>
90 * Take the list view support as an example, which is implemented
91 * in {@link ListView} class. Whenever a new view is created, a new
92 * {@link ListView} instance is created with the configuration
93 * information. This instance gets serialized to XML, and this instance
94 * will be called to render the view page. This is the job
95 * of {@link Describable} &mdash; each instance represents a specific
96 * configuration of a view (what projects are in it, regular expression, etc.)
97 *
98 * <p>
99 * For Hudson to create such configured {@link ListView} instance, Hudson
100 * needs another object that captures the metadata of {@link ListView},
101 * and that is what a {@link Descriptor} is for. {@link ListView} class
102 * has a singleton descriptor, and this descriptor helps render
103 * the configuration form, remember system-wide configuration, and works as a factory.
104 *
105 * <p>
106 * {@link Descriptor} also usually have its associated views.
107 *
108 *
109 * <h2>Persistence</h2>
110 * <p>
111 * {@link Descriptor} can persist data just by storing them in fields.
112 * However, it is the responsibility of the derived type to properly
113 * invoke {@link #save()} and {@link #load()}.
114 *
115 * <h2>Reflection Enhancement</h2>
116 * {@link Descriptor} defines addition to the standard Java reflection
117 * and provides reflective information about its corresponding {@link Describable}.
118 * These are primarily used by tag libraries to
119 * keep the Jelly scripts concise.
120 *
121 * @author Kohsuke Kawaguchi
122 * @see Describable
123 */
124 public abstract class Descriptor<T extends Describable<T>> implements Saveable {
125 /**
126 * The class being described by this descriptor.
127 */
128 public transient final Class<? extends T> clazz;
129
130 private transient final Map<String,String> checkMethods = new ConcurrentHashMap<String,String>();
131
132 /**
133 * Lazily computed list of properties on {@link #clazz} and on the descriptor itself.
134 */
135 private transient volatile Map<String, PropertyType> propertyTypes,globalPropertyTypes;
136
137 /**
138 * Represents a readable property on {@link Describable}.
139 */
140 public static final class PropertyType {
141 public final Class clazz;
142 public final Type type;
143 private volatile Class itemType;
144 public final String displayName;
145
146 PropertyType(Class clazz, Type type, String displayName) {
147 this.clazz = clazz;
148 this.type = type;
149 this.displayName = displayName;
150 }
151
152 PropertyType(Field f) {
153 this(f.getType(),f.getGenericType(),f.toString());
154 }
155
156 PropertyType(Method getter) {
157 this(getter.getReturnType(),getter.getGenericReturnType(),getter.toString());
158 }
159
160 public Enum[] getEnumConstants() {
161 return (Enum[])clazz.getEnumConstants();
162 }
163
164 /**
165 * If the property is a collection/array type, what is an item type?
166 */
167 public Class getItemType() {
168 if(itemType==null)
169 itemType = computeItemType();
170 return itemType;
171 }
172
173 private Class computeItemType() {
174 if(clazz.isArray()) {
175 return clazz.getComponentType();
176 }
177 if(Collection.class.isAssignableFrom(clazz)) {
178 Type col = Types.getBaseClass(type, Collection.class);
179
180 if (col instanceof ParameterizedType)
181 return Types.erasure(Types.getTypeArgument(col,0));
182 else
183 return Object.class;
184 }
185 return null;
186 }
187
188 /**
189 * Returns {@link Descriptor} whose 'clazz' is the same as {@link #getItemType() the item type}.
190 */
191 public Descriptor getItemTypeDescriptor() {
192 return Jenkins.getInstance().getDescriptor(getItemType());
193 }
194
195 public Descriptor getItemTypeDescriptorOrDie() {
196 Class it = getItemType();
197 if (it == null) {
198 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");
199 }
200 Descriptor d = Jenkins.getInstance().getDescriptor(it);
201 if (d==null)
202 throw new AssertionError(it +" is missing its descriptor in "+displayName+". See https://wiki.jenkins-ci.org/display/JENKINS/My+class+is+missing+descriptor");
203 return d;
204 }
205
206 /**
207 * Returns all the descriptors that produce types assignable to the property type.
208 */
209 public List<? extends Descriptor> getApplicableDescriptors() {
210 return Jenkins.getInstance().getDescriptorList(clazz);
211 }
212
213 /**
214 * Returns all the descriptors that produce types assignable to the item type for a collection property.
215 */
216 public List<? extends Descriptor> getApplicableItemDescriptors() {
217 return Jenkins.getInstance().getDescriptorList(getItemType());
218 }
219 }
220
221 /**
222 * Help file redirect, keyed by the field name to the path.
223 *
224 * @see #getHelpFile(String)
225 */
226 private transient final Map<String,String> helpRedirect = new HashMap<String, String>();
227
228 /**
229 *
230 * @param clazz
231 * Pass in {@link #self()} to have the descriptor describe itself,
232 * (this hack is needed since derived types can't call "getClass()" to refer to itself.
233 */
234 protected Descriptor(Class<? extends T> clazz) {
235 if (clazz==self())
236 clazz = (Class)getClass();
237 this.clazz = clazz;
238 // doing this turns out to be very error prone,
239 // as field initializers in derived types will override values.
240 // load();
241 }
242
243 /**
244 * Infers the type of the corresponding {@link Describable} from the outer class.
245 * This version works when you follow the common convention, where a descriptor
246 * is written as the static nested class of the describable class.
247 *
248 * @since 1.278
249 */
250 protected Descriptor() {
251 this.clazz = (Class<T>)getClass().getEnclosingClass();
252 if(clazz==null)
253 throw new AssertionError(getClass()+" doesn't have an outer class. Use the constructor that takes the Class object explicitly.");
254
255 // detect an type error
256 Type bt = Types.getBaseClass(getClass(), Descriptor.class);
257 if (bt instanceof ParameterizedType) {
258 ParameterizedType pt = (ParameterizedType) bt;
259 // this 't' is the closest approximation of T of Descriptor<T>.
260 Class t = Types.erasure(pt.getActualTypeArguments()[0]);
261 if(!t.isAssignableFrom(clazz))
262 throw new AssertionError("Outer class "+clazz+" of "+getClass()+" is not assignable to "+t+". Perhaps wrong outer class?");
263 }
264
265 // detect a type error. this Descriptor is supposed to be returned from getDescriptor(), so make sure its type match up.
266 // this prevents a bug like http://www.nabble.com/Creating-a-new-parameter-Type-%3A-Masked-Parameter-td24786554.html
267 try {
268 Method getd = clazz.getMethod("getDescriptor");
269 if(!getd.getReturnType().isAssignableFrom(getClass())) {
270 throw new AssertionError(getClass()+" must be assignable to "+getd.getReturnType());
271 }
272 } catch (NoSuchMethodException e) {
273 throw new AssertionError(getClass()+" is missing getDescriptor method.");
274 }
275
276 }
277
278 /**
279 * Human readable name of this kind of configurable object.
280 */
281 public abstract String getDisplayName();
282
283 /**
284 * Uniquely identifies this {@link Descriptor} among all the other {@link Descriptor}s.
285 *
286 * <p>
287 * Historically {@link #clazz} is assumed to be unique, so this method uses that as the default,
288 * but if you are adding {@link Descriptor}s programmatically for the same type, you can change
289 * this to disambiguate them.
290 *
291 * <p>
292 * To look up {@link Descriptor} from ID, use {@link Jenkins#getDescriptor(String)}.
293 *
294 * @return
295 * Stick to valid Java identifier character, plus '.', which had to be allowed for historical reasons.
296 *
297 * @since 1.391
298 */
299 public String getId() {
300 return clazz.getName();
301 }
302
303 /**
304 * Unlike {@link #clazz}, return the parameter type 'T', which determines
305 * the {@link DescriptorExtensionList} that this goes to.
306 *
307 * <p>
308 * In those situations where subtypes cannot provide the type parameter,
309 * this method can be overridden to provide it.
310 */
311 public Class<T> getT() {
312 Type subTyping = Types.getBaseClass(getClass(), Descriptor.class);
313 if (!(subTyping instanceof ParameterizedType)) {
314 throw new IllegalStateException(getClass()+" doesn't extend Descriptor with a type parameter.");
315 }
316 return Types.erasure(Types.getTypeArgument(subTyping, 0));
317 }
318
319 /**
320 * Gets the URL that this Descriptor is bound to, relative to the nearest {@link DescriptorByNameOwner}.
321 * Since {@link Jenkins} is a {@link DescriptorByNameOwner}, there's always one such ancestor to any request.
322 */
323 public String getDescriptorUrl() {
324 return "descriptorByName/"+getId();
325 }
326
327 /**
328 * Gets the URL that this Descriptor is bound to, relative to the context path.
329 * @since 1.406
330 */
331 public final String getDescriptorFullUrl() {
332 return getCurrentDescriptorByNameUrl()+'/'+getDescriptorUrl();
333 }
334
335 /**
336 * @since 1.402
337 */
338 public static String getCurrentDescriptorByNameUrl() {
339 StaplerRequest req = Stapler.getCurrentRequest();
340
341 // this override allows RenderOnDemandClosure to preserve the proper value
342 Object url = req.getAttribute("currentDescriptorByNameUrl");
343 if (url!=null) return url.toString();
344
345 Ancestor a = req.findAncestor(DescriptorByNameOwner.class);
346 return a.getUrl();
347 }
348
349 /**
350 * If the field "xyz" of a {@link Describable} has the corresponding "doCheckXyz" method,
351 * return the form-field validation string. Otherwise null.
352 * <p>
353 * This method is used to hook up the form validation method to the corresponding HTML input element.
354 */
355 public String getCheckUrl(String fieldName) {
356 String method = checkMethods.get(fieldName);
357 if(method==null) {
358 method = calcCheckUrl(fieldName);
359 checkMethods.put(fieldName,method);
360 }
361
362 if (method.equals(NONE)) // == would do, but it makes IDE flag a warning
363 return null;
364
365 // put this under the right contextual umbrella.
366 // a is always non-null because we already have Hudson as the sentinel
367 return '\'' + jsStringEscape(getCurrentDescriptorByNameUrl()) + "/'+" + method;
368 }
369
370 private String calcCheckUrl(String fieldName) {
371 String capitalizedFieldName = StringUtils.capitalize(fieldName);
372
373 Method method = ReflectionUtils.getPublicMethodNamed(getClass(),"doCheck"+ capitalizedFieldName);
374
375 if(method==null)
376 return NONE;
377
378 return '\'' + getDescriptorUrl() + "/check" + capitalizedFieldName + '\'' + buildParameterList(method, new StringBuilder()).append(".toString()");
379 }
380
381 /**
382 * Builds query parameter line by figuring out what should be submitted
383 */
384 private StringBuilder buildParameterList(Method method, StringBuilder query) {
385 for (Parameter p : ReflectionUtils.getParameters(method)) {
386 QueryParameter qp = p.annotation(QueryParameter.class);
387 if (qp!=null) {
388 String name = qp.value();
389 if (name.length()==0) name = p.name();
390 if (name==null || name.length()==0)
391 continue; // unknown parameter name. we'll report the error when the form is submitted.
392
393 RelativePath rp = p.annotation(RelativePath.class);
394 if (rp!=null)
395 name = rp.value()+'/'+name;
396
397 if (query.length()==0) query.append("+qs(this)");
398
399 if (name.equals("value")) {
400 // The special 'value' parameter binds to the the current field
401 query.append(".addThis()");
402 } else {
403 query.append(".nearBy('"+name+"')");
404 }
405 continue;
406 }
407
408 Method m = ReflectionUtils.getPublicMethodNamed(p.type(), "fromStapler");
409 if (m!=null) buildParameterList(m,query);
410 }
411 return query;
412 }
413
414 /**
415 * Computes the list of other form fields that the given field depends on, via the doFillXyzItems method,
416 * and sets that as the 'fillDependsOn' attribute. Also computes the URL of the doFillXyzItems and
417 * sets that as the 'fillUrl' attribute.
418 */
419 public void calcFillSettings(String field, Map<String,Object> attributes) {
420 String capitalizedFieldName = StringUtils.capitalize(field);
421 String methodName = "doFill" + capitalizedFieldName + "Items";
422 Method method = ReflectionUtils.getPublicMethodNamed(getClass(), methodName);
423 if(method==null)
424 throw new IllegalStateException(String.format("%s doesn't have the %s method for filling a drop-down list", getClass(), methodName));
425
426 // build query parameter line by figuring out what should be submitted
427 List<String> depends = buildFillDependencies(method, new ArrayList<String>());
428
429 if (!depends.isEmpty())
430 attributes.put("fillDependsOn",Util.join(depends," "));
431 attributes.put("fillUrl", String.format("%s/%s/fill%sItems", getCurrentDescriptorByNameUrl(), getDescriptorUrl(), capitalizedFieldName));
432 }
433
434 private List<String> buildFillDependencies(Method method, List<String> depends) {
435 for (Parameter p : ReflectionUtils.getParameters(method)) {
436 QueryParameter qp = p.annotation(QueryParameter.class);
437 if (qp!=null) {
438 String name = qp.value();
439 if (name.length()==0) name = p.name();
440 if (name==null || name.length()==0)
441 continue; // unknown parameter name. we'll report the error when the form is submitted.
442
443 RelativePath rp = p.annotation(RelativePath.class);
444 if (rp!=null)
445 name = rp.value()+'/'+name;
446
447 depends.add(name);
448 continue;
449 }
450
451 Method m = ReflectionUtils.getPublicMethodNamed(p.type(), "fromStapler");
452 if (m!=null)
453 buildFillDependencies(m,depends);
454 }
455 return depends;
456 }
457
458 /**
459 * Computes the auto-completion setting
460 */
461 public void calcAutoCompleteSettings(String field, Map<String,Object> attributes) {
462 String capitalizedFieldName = StringUtils.capitalize(field);
463 String methodName = "doAutoComplete" + capitalizedFieldName;
464 Method method = ReflectionUtils.getPublicMethodNamed(getClass(), methodName);
465 if(method==null)
466 return; // no auto-completion
467
468 attributes.put("autoCompleteUrl", String.format("%s/%s/autoComplete%s", getCurrentDescriptorByNameUrl(), getDescriptorUrl(), capitalizedFieldName));
469 }
470
471 /**
472 * Used by Jelly to abstract away the handlign of global.jelly vs config.jelly databinding difference.
473 */
474 public @CheckForNull PropertyType getPropertyType(@Nonnull Object instance, @Nonnull String field) {
475 // in global.jelly, instance==descriptor
476 return instance==this ? getGlobalPropertyType(field) : getPropertyType(field);
477 }
478
479 /**
480 * Akin to {@link #getPropertyType(Object,String) but never returns null.
481 * @throws AssertionError in case the field cannot be found
482 * @since 1.492
483 */
484 public @Nonnull PropertyType getPropertyTypeOrDie(@Nonnull Object instance, @Nonnull String field) {
485 PropertyType propertyType = getPropertyType(instance, field);
486 if (propertyType != null) {
487 return propertyType;
488 } else if (instance == this) {
489 throw new AssertionError(getClass().getName() + " has no property " + field);
490 } else {
491 throw new AssertionError(clazz.getName() + " has no property " + field);
492 }
493 }
494
495 /**
496 * Obtains the property type of the given field of {@link #clazz}
497 */
498 public PropertyType getPropertyType(String field) {
499 if(propertyTypes==null)
500 propertyTypes = buildPropertyTypes(clazz);
501 return propertyTypes.get(field);
502 }
503
504 /**
505 * Obtains the property type of the given field of this descriptor.
506 */
507 public PropertyType getGlobalPropertyType(String field) {
508 if(globalPropertyTypes==null)
509 globalPropertyTypes = buildPropertyTypes(getClass());
510 return globalPropertyTypes.get(field);
511 }
512
513 /**
514 * Given the class, list up its {@link PropertyType}s from its public fields/getters.
515 */
516 private Map<String, PropertyType> buildPropertyTypes(Class<?> clazz) {
517 Map<String, PropertyType> r = new HashMap<String, PropertyType>();
518 for (Field f : clazz.getFields())
519 r.put(f.getName(),new PropertyType(f));
520
521 for (Method m : clazz.getMethods())
522 if(m.getName().startsWith("get"))
523 r.put(Introspector.decapitalize(m.getName().substring(3)),new PropertyType(m));
524
525 return r;
526 }
527
528 /**
529 * Gets the class name nicely escaped to be usable as a key in the structured form submission.
530 */
531 public final String getJsonSafeClassName() {
532 return getId().replace('.','-');
533 }
534
535 /**
536 * @deprecated
537 * Implement {@link #newInstance(StaplerRequest, JSONObject)} method instead.
538 * Deprecated as of 1.145.
539 */
540 public T newInstance(StaplerRequest req) throws FormException {
541 throw new UnsupportedOperationException(getClass()+" should implement newInstance(StaplerRequest,JSONObject)");
542 }
543
544 /**
545 * Creates a configured instance from the submitted form.
546 *
547 * <p>
548 * Hudson only invokes this method when the user wants an instance of <tt>T</tt>.
549 * So there's no need to check that in the implementation.
550 *
551 * <p>
552 * Starting 1.206, the default implementation of this method does the following:
553 * <pre>
554 * req.bindJSON(clazz,formData);
555 * </pre>
556 * <p>
557 * ... which performs the databinding on the constructor of {@link #clazz}.
558 *
559 * <p>
560 * For some types of {@link Describable}, such as {@link ListViewColumn}, this method
561 * can be invoked with null request object for historical reason. Such design is considered
562 * broken, but due to the compatibility reasons we cannot fix it. Because of this, the
563 * default implementation gracefully handles null request, but the contract of the method
564 * still is "request is always non-null." Extension points that need to define the "default instance"
565 * semantics should define a descriptor subtype and add the no-arg newInstance method.
566 *
567 * @param req
568 * Always non-null (see note above.) This object includes represents the entire submission.
569 * @param formData
570 * The JSON object that captures the configuration data for this {@link Descriptor}.
571 * See http://wiki.jenkins-ci.org/display/JENKINS/Structured+Form+Submission
572 * Always non-null.
573 *
574 * @throws FormException
575 * Signals a problem in the submitted form.
576 * @since 1.145
577 */
578 public T newInstance(StaplerRequest req, JSONObject formData) throws FormException {
579 try {
580 Method m = getClass().getMethod("newInstance", StaplerRequest.class);
581
582 if(!Modifier.isAbstract(m.getDeclaringClass().getModifiers())) {
583 // this class overrides newInstance(StaplerRequest).
584 // maintain the backward compatible behavior
585 return verifyNewInstance(newInstance(req));
586 } else {
587 if (req==null) {
588 // yes, req is supposed to be always non-null, but see the note above
589 return verifyNewInstance(clazz.newInstance());
590 }
591
592 // new behavior as of 1.206
593 return verifyNewInstance(req.bindJSON(clazz,formData));
594 }
595 } catch (NoSuchMethodException e) {
596 throw new AssertionError(e); // impossible
597 } catch (InstantiationException e) {
598 throw new Error("Failed to instantiate "+clazz+" from "+formData,e);
599 } catch (IllegalAccessException e) {
600 throw new Error("Failed to instantiate "+clazz+" from "+formData,e);
601 } catch (RuntimeException e) {
602 throw new RuntimeException("Failed to instantiate "+clazz+" from "+formData,e);
603 }
604 }
605
606 /**
607 * Look out for a typical error a plugin developer makes.
608 * See http://hudson.361315.n4.nabble.com/Help-Hint-needed-Post-build-action-doesn-t-stay-activated-td2308833.html
609 */
610 private T verifyNewInstance(T t) {
611 if (t!=null && t.getDescriptor()!=this) {
612 // TODO: should this be a fatal error?
613 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");
614 }
615 return t;
616 }
617
618 /**
619 * Returns the {@link Klass} object used for the purpose of loading resources from this descriptor.
620 *
621 * This hook enables other JVM languages to provide more integrated lookup.
622 */
623 public Klass<?> getKlass() {
624 return Klass.java(clazz);
625 }
626
627 /**
628 * Returns the resource path to the help screen HTML, if any.
629 *
630 * <p>
631 * Starting 1.282, this method uses "convention over configuration" &mdash; you should
632 * just put the "help.html" (and its localized versions, if any) in the same directory
633 * you put your Jelly view files, and this method will automatically does the right thing.
634 *
635 * <p>
636 * This value is relative to the context root of Hudson, so normally
637 * the values are something like <tt>"/plugin/emma/help.html"</tt> to
638 * refer to static resource files in a plugin, or <tt>"/publisher/EmmaPublisher/abc"</tt>
639 * to refer to Jelly script <tt>abc.jelly</tt> or a method <tt>EmmaPublisher.doAbc()</tt>.
640 *
641 * @return
642 * null to indicate that there's no help.
643 */
644 public String getHelpFile() {
645 return getHelpFile(null);
646 }
647
648 /**
649 * Returns the path to the help screen HTML for the given field.
650 *
651 * <p>
652 * The help files are assumed to be at "help/FIELDNAME.html" with possible
653 * locale variations.
654 */
655 public String getHelpFile(final String fieldName) {
656 return getHelpFile(getKlass(),fieldName);
657 }
658
659 public String getHelpFile(Klass<?> clazz, String fieldName) {
660 String v = helpRedirect.get(fieldName);
661 if (v!=null) return v;
662
663 for (Klass<?> c : clazz.getAncestors()) {
664 String page = "/descriptor/" + getId() + "/help";
665 String suffix;
666 if(fieldName==null) {
667 suffix="";
668 } else {
669 page += '/'+fieldName;
670 suffix='-'+fieldName;
671 }
672
673 try {
674 if(Stapler.getCurrentRequest().getView(c,"help"+suffix)!=null)
675 return page;
676 } catch (IOException e) {
677 throw new Error(e);
678 }
679
680 if(getStaticHelpUrl(c, suffix) !=null) return page;
681 }
682 return null;
683 }
684
685 /**
686 * Tells Jenkins that the help file for the field 'fieldName' is defined in the help file for
687 * the 'fieldNameToRedirectTo' in the 'owner' class.
688 * @since 1.425
689 */
690 protected void addHelpFileRedirect(String fieldName, Class<? extends Describable> owner, String fieldNameToRedirectTo) {
691 helpRedirect.put(fieldName,
692 Jenkins.getInstance().getDescriptor(owner).getHelpFile(fieldNameToRedirectTo));
693 }
694
695 /**
696 * Checks if the given object is created from this {@link Descriptor}.
697 */
698 public final boolean isInstance( T instance ) {
699 return clazz.isInstance(instance);
700 }
701
702 /**
703 * Checks if the type represented by this descriptor is a subtype of the given type.
704 */
705 public final boolean isSubTypeOf(Class type) {
706 return type.isAssignableFrom(clazz);
707 }
708
709 /**
710 * @deprecated
711 * As of 1.239, use {@link #configure(StaplerRequest, JSONObject)}.
712 */
713 public boolean configure( StaplerRequest req ) throws FormException {
714 return true;
715 }
716
717 /**
718 * Invoked when the global configuration page is submitted.
719 *
720 * Can be overriden to store descriptor-specific information.
721 *
722 * @param json
723 * The JSON object that captures the configuration data for this {@link Descriptor}.
724 * See http://wiki.jenkins-ci.org/display/JENKINS/Structured+Form+Submission
725 * @return false
726 * to keep the client in the same config page.
727 */
728 public boolean configure( StaplerRequest req, JSONObject json ) throws FormException {
729 // compatibility
730 return configure(req);
731 }
732
733 public String getConfigPage() {
734 return getViewPage(clazz, getPossibleViewNames("config"), "config.jelly");
735 }
736
737 public String getGlobalConfigPage() {
738 return getViewPage(clazz, getPossibleViewNames("global"), null);
739 }
740
741 private String getViewPage(Class<?> clazz, String pageName, String defaultValue) {
742 return getViewPage(clazz,Collections.singleton(pageName),defaultValue);
743 }
744
745 private String getViewPage(Class<?> clazz, Collection<String> pageNames, String defaultValue) {
746 while(clazz!=Object.class && clazz!=null) {
747 for (String pageName : pageNames) {
748 String name = clazz.getName().replace('.', '/').replace('$', '/') + "/" + pageName;
749 if(clazz.getClassLoader().getResource(name)!=null)
750 return '/'+name;
751 }
752 clazz = clazz.getSuperclass();
753 }
754 return defaultValue;
755 }
756
757 protected final String getViewPage(Class<?> clazz, String pageName) {
758 // We didn't find the configuration page.
759 // Either this is non-fatal, in which case it doesn't matter what string we return so long as
760 // it doesn't exist.
761 // Or this error is fatal, in which case we want the developer to see what page he's missing.
762 // so we put the page name.
763 return getViewPage(clazz,pageName,pageName);
764 }
765
766 protected List<String> getPossibleViewNames(String baseName) {
767 List<String> names = new ArrayList<String>();
768 for (Facet f : WebApp.get(Jenkins.getInstance().servletContext).facets) {
769 if (f instanceof JellyCompatibleFacet) {
770 JellyCompatibleFacet jcf = (JellyCompatibleFacet) f;
771 for (String ext : jcf.getScriptExtensions())
772 names.add(baseName +ext);
773 }
774 }
775 return names;
776 }
777
778
779 /**
780 * Saves the configuration info to the disk.
781 */
782 public synchronized void save() {
783 if(BulkChange.contains(this)) return;
784 try {
785 getConfigFile().write(this);
786 SaveableListener.fireOnChange(this, getConfigFile());
787 } catch (IOException e) {
788 LOGGER.log(Level.WARNING, "Failed to save "+getConfigFile(),e);
789 }
790 }
791
792 /**
793 * Loads the data from the disk into this object.
794 *
795 * <p>
796 * The constructor of the derived class must call this method.
797 * (If we do that in the base class, the derived class won't
798 * get a chance to set default values.)
799 */
800 public synchronized void load() {
801 XmlFile file = getConfigFile();
802 if(!file.exists())
803 return;
804
805 try {
806 file.unmarshal(this);
807 } catch (IOException e) {
808 LOGGER.log(Level.WARNING, "Failed to load "+file, e);
809 }
810 }
811
812 protected final XmlFile getConfigFile() {
813 return new XmlFile(new File(Jenkins.getInstance().getRootDir(),getId()+".xml"));
814 }
815
816 /**
817 * Returns the plugin in which this descriptor is defined.
818 *
819 * @return
820 * null to indicate that this descriptor came from the core.
821 */
822 protected PluginWrapper getPlugin() {
823 return Jenkins.getInstance().getPluginManager().whichPlugin(clazz);
824 }
825
826 /**
827 * Serves <tt>help.html</tt> from the resource of {@link #clazz}.
828 */
829 public void doHelp(StaplerRequest req, StaplerResponse rsp) throws IOException, ServletException {
830 String path = req.getRestOfPath();
831 if(path.contains("..")) throw new ServletException("Illegal path: "+path);
832
833 path = path.replace('/','-');
834
835 PluginWrapper pw = getPlugin();
836 if (pw!=null) {
837 rsp.setHeader("X-Plugin-Short-Name",pw.getShortName());
838 rsp.setHeader("X-Plugin-Long-Name",pw.getLongName());
839 rsp.setHeader("X-Plugin-From", Messages.Descriptor_From(
840 pw.getLongName().replace("Hudson","Jenkins").replace("hudson","jenkins"), pw.getUrl()));
841 }
842
843 for (Klass<?> c= getKlass(); c!=null; c=c.getSuperClass()) {
844 RequestDispatcher rd = Stapler.getCurrentRequest().getView(c, "help"+path);
845 if(rd!=null) {// template based help page
846 rd.forward(req,rsp);
847 return;
848 }
849
850 URL url = getStaticHelpUrl(c, path);
851 if(url!=null) {
852 // TODO: generalize macro expansion and perhaps even support JEXL
853 rsp.setContentType("text/html;charset=UTF-8");
854 InputStream in = url.openStream();
855 try {
856 String literal = IOUtils.toString(in,"UTF-8");
857 rsp.getWriter().println(Util.replaceMacro(literal, Collections.singletonMap("rootURL",req.getContextPath())));
858 } finally {
859 IOUtils.closeQuietly(in);
860 }
861 return;
862 }
863 }
864 rsp.sendError(SC_NOT_FOUND);
865 }
866
867 private URL getStaticHelpUrl(Klass<?> c, String suffix) {
868 Locale locale = Stapler.getCurrentRequest().getLocale();
869
870 String base = "help"+suffix;
871
872 URL url;
873 url = c.getResource(base + '_' + locale.getLanguage() + '_' + locale.getCountry() + '_' + locale.getVariant() + ".html");
874 if(url!=null) return url;
875 url = c.getResource(base + '_' + locale.getLanguage() + '_' + locale.getCountry() + ".html");
876 if(url!=null) return url;
877 url = c.getResource(base + '_' + locale.getLanguage() + ".html");
878 if(url!=null) return url;
879
880 // default
881 return c.getResource(base + ".html");
882 }
883
884
885 //
886 // static methods
887 //
888
889
890 // to work around warning when creating a generic array type
891 public static <T> T[] toArray( T... values ) {
892 return values;
893 }
894
895 public static <T> List<T> toList( T... values ) {
896 return new ArrayList<T>(Arrays.asList(values));
897 }
898
899 public static <T extends Describable<T>>
900 Map<Descriptor<T>,T> toMap(Iterable<T> describables) {
901 Map<Descriptor<T>,T> m = new LinkedHashMap<Descriptor<T>,T>();
902 for (T d : describables) {
903 m.put(d.getDescriptor(),d);
904 }
905 return m;
906 }
907
908 /**
909 * Used to build {@link Describable} instance list from &lt;f:hetero-list> tag.
910 *
911 * @param req
912 * Request that represents the form submission.
913 * @param formData
914 * Structured form data that represents the contains data for the list of describables.
915 * @param key
916 * The JSON property name for 'formData' that represents the data for the list of describables.
917 * @param descriptors
918 * List of descriptors to create instances from.
919 * @return
920 * Can be empty but never null.
921 */
922 public static <T extends Describable<T>>
923 List<T> newInstancesFromHeteroList(StaplerRequest req, JSONObject formData, String key,
924 Collection<? extends Descriptor<T>> descriptors) throws FormException {
925
926 return newInstancesFromHeteroList(req,formData.get(key),descriptors);
927 }
928
929 public static <T extends Describable<T>>
930 List<T> newInstancesFromHeteroList(StaplerRequest req, Object formData,
931 Collection<? extends Descriptor<T>> descriptors) throws FormException {
932
933 List<T> items = new ArrayList<T>();
934
935 if (formData!=null) {
936 for (Object o : JSONArray.fromObject(formData)) {
937 JSONObject jo = (JSONObject)o;
938 String kind = jo.getString("kind");
939 Descriptor<T> d = find(descriptors, kind);
940 if (d != null) {
941 items.add(d.newInstance(req, jo));
942 }
943 }
944 }
945
946 return items;
947 }
948
949 /**
950 * Finds a descriptor from a collection by its class name.
951 */
952 public static @CheckForNull <T extends Descriptor> T find(Collection<? extends T> list, String className) {
953 for (T d : list) {
954 if(d.getClass().getName().equals(className))
955 return d;
956 }
957 // Since we introduced Descriptor.getId(), it is a preferred method of identifying descriptor by a string.
958 // To make that migration easier without breaking compatibility, let's also match up with the id.
959 for (T d : list) {
960 if(d.getId().equals(className))
961 return d;
962 }
963 return null;
964 }
965
966 public static @CheckForNull Descriptor find(String className) {
967 return find(Jenkins.getInstance().getExtensionList(Descriptor.class),className);
968 }
969
970 public static final class FormException extends Exception implements HttpResponse {
971 private final String formField;
972
973 public FormException(String message, String formField) {
974 super(message);
975 this.formField = formField;
976 }
977
978 public FormException(String message, Throwable cause, String formField) {
979 super(message, cause);
980 this.formField = formField;
981 }
982
983 public FormException(Throwable cause, String formField) {
984 super(cause);
985 this.formField = formField;
986 }
987
988 /**
989 * Which form field contained an error?
990 */
991 public String getFormField() {
992 return formField;
993 }
994
995 public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException {
996 if (FormApply.isApply(req)) {
997 FormApply.applyResponse("notificationBar.show(" + quote(getMessage())+ ",notificationBar.defaultOptions.ERROR)")
998 .generateResponse(req, rsp, node);
999 } else {
1000 // for now, we can't really use the field name that caused the problem.
1001 new Failure(getMessage()).generateResponse(req,rsp,node);
1002 }
1003 }
1004 }
1005
1006 private static final Logger LOGGER = Logger.getLogger(Descriptor.class.getName());
1007
1008 /**
1009 * Used in {@link #checkMethods} to indicate that there's no check method.
1010 */
1011 private static final String NONE = "\u0000";
1012
1013 /**
1014 * Special type indicating that {@link Descriptor} describes itself.
1015 * @see Descriptor#Descriptor(Class)
1016 */
1017 public static final class Self {}
1018
1019 protected static Class self() { return Self.class; }
1020 }
1021