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

1 /*
2 * The MIT License
3 *
4 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
5 * Yahoo! Inc., Erik Ramfelt, Tom Huybrechts
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE.
24 */
25 package hudson;
26
27 import hudson.PluginManager.PluginInstanceStore;
28 import hudson.model.Api;
29 import hudson.model.ModelObject;
30 import jenkins.YesNoMaybe;
31 import jenkins.model.Jenkins;
32 import hudson.model.UpdateCenter;
33 import hudson.model.UpdateSite;
34 import hudson.util.VersionNumber;
35
36 import java.io.File;
37 import java.io.FileOutputStream;
38 import java.io.IOException;
39 import java.io.OutputStream;
40 import java.io.Closeable;
41 import java.net.URL;
42 import java.util.ArrayList;
43 import java.util.List;
44 import java.util.jar.Manifest;
45 import java.util.logging.Logger;
46 import static java.util.logging.Level.WARNING;
47
48 import org.apache.commons.logging.LogFactory;
49 import org.kohsuke.stapler.HttpResponse;
50 import org.kohsuke.stapler.HttpResponses;
51 import org.kohsuke.stapler.export.Exported;
52 import org.kohsuke.stapler.export.ExportedBean;
53 import org.kohsuke.stapler.interceptor.RequirePOST;
54
55 import java.util.Enumeration;
56 import java.util.jar.JarFile;
57
58 /**
59 * Represents a Jenkins plug-in and associated control information
60 * for Jenkins to control {@link Plugin}.
61 *
62 * <p>
63 * A plug-in is packaged into a jar file whose extension is <tt>".jpi"</tt> (or <tt>".hpi"</tt> for backward compatability),
64 * A plugin needs to have a special manifest entry to identify what it is.
65 *
66 * <p>
67 * At the runtime, a plugin has two distinct state axis.
68 * <ol>
69 * <li>Enabled/Disabled. If enabled, Jenkins is going to use it
70 * next time Jenkins runs. Otherwise the next run will ignore it.
71 * <li>Activated/Deactivated. If activated, that means Jenkins is using
72 * the plugin in this session. Otherwise it's not.
73 * </ol>
74 * <p>
75 * For example, an activated but disabled plugin is still running but the next
76 * time it won't.
77 *
78 * @author Kohsuke Kawaguchi
79 */
80 @ExportedBean
81 public class PluginWrapper implements Comparable<PluginWrapper>, ModelObject {
82 /**
83 * {@link PluginManager} to which this belongs to.
84 */
85 public final PluginManager parent;
86
87 /**
88 * Plugin manifest.
89 * Contains description of the plugin.
90 */
91 private final Manifest manifest;
92
93 /**
94 * {@link ClassLoader} for loading classes from this plugin.
95 * Null if disabled.
96 */
97 public final ClassLoader classLoader;
98
99 /**
100 * Base URL for loading static resources from this plugin.
101 * Null if disabled. The static resources are mapped under
102 * <tt>CONTEXTPATH/plugin/SHORTNAME/</tt>.
103 */
104 public final URL baseResourceURL;
105
106 /**
107 * Used to control enable/disable setting of the plugin.
108 * If this file exists, plugin will be disabled.
109 */
110 private final File disableFile;
111
112 /**
113 * Used to control the unpacking of the bundled plugin.
114 * If a pin file exists, Jenkins assumes that the user wants to pin down a particular version
115 * of a plugin, and will not try to overwrite it. Otherwise, it'll be overwritten
116 * by a bundled copy, to ensure consistency across upgrade/downgrade.
117 * @since 1.325
118 */
119 private final File pinFile;
120
121 /**
122 * A .jpi file, an exploded plugin directory, or a .jpl file.
123 */
124 private final File archive;
125
126 /**
127 * Short name of the plugin. The artifact Id of the plugin.
128 * This is also used in the URL within Jenkins, so it needs
129 * to remain stable even when the *.jpi file name is changed
130 * (like Maven does.)
131 */
132 private final String shortName;
133
134 /**
135 * True if this plugin is activated for this session.
136 * The snapshot of <tt>disableFile.exists()</tt> as of the start up.
137 */
138 private final boolean active;
139
140 private boolean hasCycleDependency = false;
141
142 private final List<Dependency> dependencies;
143 private final List<Dependency> optionalDependencies;
144
145 /**
146 * Is this plugin bundled in jenkins.war?
147 */
148 /*package*/ boolean isBundled;
149
150 @ExportedBean
151 public static final class Dependency {
152 @Exported
153 public final String shortName;
154 @Exported
155 public final String version;
156 @Exported
157 public final boolean optional;
158
159 public Dependency(String s) {
160 int idx = s.indexOf(':');
161 if(idx==-1)
162 throw new IllegalArgumentException("Illegal dependency specifier "+s);
163 this.shortName = s.substring(0,idx);
164 this.version = s.substring(idx+1);
165
166 boolean isOptional = false;
167 String[] osgiProperties = s.split(";");
168 for (int i = 1; i < osgiProperties.length; i++) {
169 String osgiProperty = osgiProperties[i].trim();
170 if (osgiProperty.equalsIgnoreCase("resolution:=optional")) {
171 isOptional = true;
172 }
173 }
174 this.optional = isOptional;
175 }
176
177 @Override
178 public String toString() {
179 return shortName + " (" + version + ")";
180 }
181 }
182
183 /**
184 * @param archive
185 * A .jpi archive file jar file, or a .jpl linked plugin.
186 * @param manifest
187 * The manifest for the plugin
188 * @param baseResourceURL
189 * A URL pointing to the resources for this plugin
190 * @param classLoader
191 * a classloader that loads classes from this plugin and its dependencies
192 * @param disableFile
193 * if this file exists on startup, the plugin will not be activated
194 * @param dependencies a list of mandatory dependencies
195 * @param optionalDependencies a list of optional dependencies
196 */
197 public PluginWrapper(PluginManager parent, File archive, Manifest manifest, URL baseResourceURL,
198 ClassLoader classLoader, File disableFile,
199 List<Dependency> dependencies, List<Dependency> optionalDependencies) {
200 this.parent = parent;
201 this.manifest = manifest;
202 this.shortName = computeShortName(manifest, archive);
203 this.baseResourceURL = baseResourceURL;
204 this.classLoader = classLoader;
205 this.disableFile = disableFile;
206 this.pinFile = new File(archive.getPath() + ".pinned");
207 this.active = !disableFile.exists();
208 this.dependencies = dependencies;
209 this.optionalDependencies = optionalDependencies;
210 this.archive = archive;
211 }
212
213 public String getDisplayName() {
214 return getLongName();
215 }
216
217 public Api getApi() {
218 return new Api(this);
219 }
220
221 /**
222 * Returns the URL of the index page jelly script.
223 */
224 public URL getIndexPage() {
225 // In the current impl dependencies are checked first, so the plugin itself
226 // will add the last entry in the getResources result.
227 URL idx = null;
228 try {
229 Enumeration<URL> en = classLoader.getResources("index.jelly");
230 while (en.hasMoreElements())
231 idx = en.nextElement();
232 } catch (IOException ignore) { }
233 // In case plugin has dependencies but is missing its own index.jelly,
234 // check that result has this plugin's artifactId in it:
235 return idx != null && idx.toString().contains(shortName) ? idx : null;
236 }
237
238 private String computeShortName(Manifest manifest, File archive) {
239 // use the name captured in the manifest, as often plugins
240 // depend on the specific short name in its URLs.
241 String n = manifest.getMainAttributes().getValue("Short-Name");
242 if(n!=null) return n;
243
244 // maven seems to put this automatically, so good fallback to check.
245 n = manifest.getMainAttributes().getValue("Extension-Name");
246 if(n!=null) return n;
247
248 // otherwise infer from the file name, since older plugins don't have
249 // this entry.
250 return getBaseName(archive);
251 }
252
253
254 /**
255 * Gets the "abc" portion from "abc.ext".
256 */
257 static String getBaseName(File archive) {
258 String n = archive.getName();
259 int idx = n.lastIndexOf('.');
260 if(idx>=0)
261 n = n.substring(0,idx);
262 return n;
263 }
264
265 @Exported
266 public List<Dependency> getDependencies() {
267 return dependencies;
268 }
269
270 public List<Dependency> getOptionalDependencies() {
271 return optionalDependencies;
272 }
273
274
275 /**
276 * Returns the short name suitable for URL.
277 */
278 @Exported
279 public String getShortName() {
280 return shortName;
281 }
282
283 /**
284 * Gets the instance of {@link Plugin} contributed by this plugin.
285 */
286 public Plugin getPlugin() {
287 return Jenkins.lookup(PluginInstanceStore.class).store.get(this);
288 }
289
290 /**
291 * Gets the URL that shows more information about this plugin.
292 * @return
293 * null if this information is unavailable.
294 * @since 1.283
295 */
296 @Exported
297 public String getUrl() {
298 // first look for the manifest entry. This is new in maven-hpi-plugin 1.30
299 String url = manifest.getMainAttributes().getValue("Url");
300 if(url!=null) return url;
301
302 // fallback to update center metadata
303 UpdateSite.Plugin ui = getInfo();
304 if(ui!=null) return ui.wiki;
305
306 return null;
307 }
308
309
310
311 @Override
312 public String toString() {
313 return "Plugin:" + getShortName();
314 }
315
316 /**
317 * Returns a one-line descriptive name of this plugin.
318 */
319 @Exported
320 public String getLongName() {
321 String name = manifest.getMainAttributes().getValue("Long-Name");
322 if(name!=null) return name;
323 return shortName;
324 }
325
326 /**
327 * Does this plugin supports dynamic loading?
328 */
329 @Exported
330 public YesNoMaybe supportsDynamicLoad() {
331 String v = manifest.getMainAttributes().getValue("Support-Dynamic-Loading");
332 if (v==null) return YesNoMaybe.MAYBE;
333 return Boolean.parseBoolean(v) ? YesNoMaybe.YES : YesNoMaybe.NO;
334 }
335
336 /**
337 * Returns the version number of this plugin
338 */
339 @Exported
340 public String getVersion() {
341 String v = manifest.getMainAttributes().getValue("Plugin-Version");
342 if(v!=null) return v;
343
344 // plugins generated before maven-hpi-plugin 1.3 should still have this attribute
345 v = manifest.getMainAttributes().getValue("Implementation-Version");
346 if(v!=null) return v;
347
348 return "???";
349 }
350
351 /**
352 * Returns the version number of this plugin
353 */
354 public VersionNumber getVersionNumber() {
355 return new VersionNumber(getVersion());
356 }
357
358 /**
359 * Returns true if the version of this plugin is older than the given version.
360 */
361 public boolean isOlderThan(VersionNumber v) {
362 try {
363 return getVersionNumber().compareTo(v) < 0;
364 } catch (IllegalArgumentException e) {
365 // if we can't figure out our current version, it probably means it's very old,
366 // since the version information is missing only from the very old plugins
367 return true;
368 }
369 }
370
371 /**
372 * Terminates the plugin.
373 */
374 public void stop() {
375 LOGGER.info("Stopping "+shortName);
376 try {
377 getPlugin().stop();
378 } catch(Throwable t) {
379 LOGGER.log(WARNING, "Failed to shut down "+shortName, t);
380 }
381 // Work around a bug in commons-logging.
382 // See http://www.szegedi.org/articles/memleak.html
383 LogFactory.release(classLoader);
384 }
385
386 public void releaseClassLoader() {
387 if (classLoader instanceof Closeable)
388 try {
389 ((Closeable) classLoader).close();
390 } catch (IOException e) {
391 LOGGER.log(WARNING, "Failed to shut down classloader",e);
392 }
393 }
394
395 /**
396 * Enables this plugin next time Jenkins runs.
397 */
398 public void enable() throws IOException {
399 if(!disableFile.delete())
400 throw new IOException("Failed to delete "+disableFile);
401 }
402
403 /**
404 * Disables this plugin next time Jenkins runs.
405 */
406 public void disable() throws IOException {
407 // creates an empty file
408 OutputStream os = new FileOutputStream(disableFile);
409 os.close();
410 }
411
412 /**
413 * Returns true if this plugin is enabled for this session.
414 */
415 @Exported
416 public boolean isActive() {
417 return active && !hasCycleDependency();
418 }
419
420 public boolean hasCycleDependency(){
421 return hasCycleDependency;
422 }
423
424 public void setHasCycleDependency(boolean hasCycle){
425 hasCycleDependency = hasCycle;
426 }
427
428 @Exported
429 public boolean isBundled() {
430 return isBundled;
431 }
432
433 /**
434 * If true, the plugin is going to be activated next time
435 * Jenkins runs.
436 */
437 @Exported
438 public boolean isEnabled() {
439 return !disableFile.exists();
440 }
441
442 public Manifest getManifest() {
443 return manifest;
444 }
445
446 public void setPlugin(Plugin plugin) {
447 Jenkins.lookup(PluginInstanceStore.class).store.put(this,plugin);
448 plugin.wrapper = this;
449 }
450
451 public String getPluginClass() {
452 return manifest.getMainAttributes().getValue("Plugin-Class");
453 }
454
455 public boolean hasLicensesXml() {
456 try {
457 new URL(baseResourceURL,"WEB-INF/licenses.xml").openStream().close();
458 return true;
459 } catch (IOException e) {
460 return false;
461 }
462 }
463
464 /**
465 * Makes sure that all the dependencies exist, and then accept optional dependencies
466 * as real dependencies.
467 *
468 * @throws IOException
469 * thrown if one or several mandatory dependencies doesn't exists.
470 */
471 /*package*/ void resolvePluginDependencies() throws IOException {
472 List<String> missingDependencies = new ArrayList<String>();
473 // make sure dependencies exist
474 for (Dependency d : dependencies) {
475 if (parent.getPlugin(d.shortName) == null)
476 missingDependencies.add(d.toString());
477 }
478 if (!missingDependencies.isEmpty())
479 throw new IOException("Dependency "+Util.join(missingDependencies, ", ")+" doesn't exist");
480
481 // add the optional dependencies that exists
482 for (Dependency d : optionalDependencies) {
483 if (parent.getPlugin(d.shortName) != null)
484 dependencies.add(d);
485 }
486 }
487
488 /**
489 * If the plugin has {@link #getUpdateInfo() an update},
490 * returns the {@link UpdateSite.Plugin} object.
491 *
492 * @return
493 * This method may return null &mdash; for example,
494 * the user may have installed a plugin locally developed.
495 */
496 public UpdateSite.Plugin getUpdateInfo() {
497 UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
498 UpdateSite.Plugin p = uc.getPlugin(getShortName());
499 if(p!=null && p.isNewerThan(getVersion())) return p;
500 return null;
501 }
502
503 /**
504 * returns the {@link UpdateSite.Plugin} object, or null.
505 */
506 public UpdateSite.Plugin getInfo() {
507 UpdateCenter uc = Jenkins.getInstance().getUpdateCenter();
508 return uc.getPlugin(getShortName());
509 }
510
511 /**
512 * Returns true if this plugin has update in the update center.
513 *
514 * <p>
515 * This method is conservative in the sense that if the version number is incomprehensible,
516 * it always returns false.
517 */
518 @Exported
519 public boolean hasUpdate() {
520 return getUpdateInfo()!=null;
521 }
522
523 @Exported
524 public boolean isPinned() {
525 return pinFile.exists();
526 }
527
528 /**
529 * Returns true if this plugin is deleted.
530 *
531 * The plugin continues to function in this session, but in the next session it'll disappear.
532 */
533 @Exported
534 public boolean isDeleted() {
535 return !archive.exists();
536 }
537
538 /**
539 * Sort by short name.
540 */
541 public int compareTo(PluginWrapper pw) {
542 return shortName.compareToIgnoreCase(pw.shortName);
543 }
544
545 /**
546 * returns true if backup of previous version of plugin exists
547 */
548 @Exported
549 public boolean isDowngradable() {
550 return getBackupFile().exists();
551 }
552
553 /**
554 * Where is the backup file?
555 */
556 public File getBackupFile() {
557 return new File(Jenkins.getInstance().getRootDir(),"plugins/"+getShortName() + ".bak");
558 }
559
560 /**
561 * returns the version of the backed up plugin,
562 * or null if there's no back up.
563 */
564 @Exported
565 public String getBackupVersion() {
566 File backup = getBackupFile();
567 if (backup.exists()) {
568 try {
569 JarFile backupPlugin = new JarFile(backup);
570 return backupPlugin.getManifest().getMainAttributes().getValue("Plugin-Version");
571 } catch (IOException e) {
572 LOGGER.log(WARNING, "Failed to get backup version ", e);
573 return null;
574 }
575 } else {
576 return null;
577 }
578 }
579 //
580 //
581 // Action methods
582 //
583 //
584 @RequirePOST
585 public HttpResponse doMakeEnabled() throws IOException {
586 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
587 enable();
588 return HttpResponses.ok();
589 }
590
591 @RequirePOST
592 public HttpResponse doMakeDisabled() throws IOException {
593 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
594 disable();
595 return HttpResponses.ok();
596 }
597
598 @RequirePOST
599 public HttpResponse doPin() throws IOException {
600 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
601 new FileOutputStream(pinFile).close();
602 return HttpResponses.ok();
603 }
604
605 @RequirePOST
606 public HttpResponse doUnpin() throws IOException {
607 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
608 pinFile.delete();
609 return HttpResponses.ok();
610 }
611
612 @RequirePOST
613 public HttpResponse doDoUninstall() throws IOException {
614 Jenkins.getInstance().checkPermission(Jenkins.ADMINISTER);
615 archive.delete();
616 return HttpResponses.redirectViaContextPath("/pluginManager/installed"); // send back to plugin manager
617 }
618
619
620 private static final Logger LOGGER = Logger.getLogger(PluginWrapper.class.getName());
621
622 }
623