PageRenderTime 32ms CodeModel.GetById 10ms app.highlight 17ms RepoModel.GetById 1ms app.codeStats 0ms

/hudson-core/src/main/java/hudson/PluginWrapper.java

http://github.com/hudson/hudson
Java | 560 lines | 276 code | 72 blank | 212 comment | 27 complexity | 5490aa76f743686341b01210143654ac MD5 | raw file
  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 */
 25package hudson;
 26
 27import hudson.PluginManager.PluginInstanceStore;
 28import hudson.model.Hudson;
 29import hudson.model.UpdateCenter;
 30import hudson.model.UpdateSite;
 31import hudson.util.VersionNumber;
 32
 33import java.io.File;
 34import java.io.FileOutputStream;
 35import java.io.IOException;
 36import java.io.OutputStream;
 37import java.io.Closeable;
 38import java.net.URL;
 39import java.util.ArrayList;
 40import java.util.List;
 41import java.util.jar.Manifest;
 42import java.util.logging.Logger;
 43import static java.util.logging.Level.WARNING;
 44
 45import org.apache.commons.logging.LogFactory;
 46import org.kohsuke.stapler.HttpResponse;
 47import org.kohsuke.stapler.HttpResponses;
 48
 49import java.util.Enumeration;
 50import java.util.jar.JarFile;
 51
 52/**
 53 * Represents a Hudson plug-in and associated control information
 54 * for Hudson to control {@link Plugin}.
 55 *
 56 * <p>
 57 * A plug-in is packaged into a jar file whose extension is <tt>".hpi"</tt>,
 58 * A plugin needs to have a special manifest entry to identify what it is.
 59 *
 60 * <p>
 61 * At the runtime, a plugin has two distinct state axis.
 62 * <ol>
 63 *  <li>Enabled/Disabled. If enabled, Hudson is going to use it
 64 *      next time Hudson runs. Otherwise the next run will ignore it.
 65 *  <li>Activated/Deactivated. If activated, that means Hudson is using
 66 *      the plugin in this session. Otherwise it's not.
 67 * </ol>
 68 * <p>
 69 * For example, an activated but disabled plugin is still running but the next
 70 * time it won't.
 71 *
 72 * @author Kohsuke Kawaguchi
 73 */
 74public class PluginWrapper implements Comparable<PluginWrapper> {
 75    /**
 76     * {@link PluginManager} to which this belongs to.
 77     */
 78    //TODO: review and check whether we can do it private
 79    public final PluginManager parent;
 80
 81    /**
 82     * Plugin manifest.
 83     * Contains description of the plugin.
 84     */
 85    private final Manifest manifest;
 86
 87    /**
 88     * {@link ClassLoader} for loading classes from this plugin.
 89     * Null if disabled.
 90     */
 91    //TODO: review and check whether we can do it private
 92    public final ClassLoader classLoader;
 93
 94    /**
 95     * Base URL for loading static resources from this plugin.
 96     * Null if disabled. The static resources are mapped under
 97     * <tt>hudson/plugin/SHORTNAME/</tt>.
 98     */
 99    //TODO: review and check whether we can do it private
100    public final URL baseResourceURL;
101
102    /**
103     * Used to control enable/disable setting of the plugin.
104     * If this file exists, plugin will be disabled.
105     */
106    private final File disableFile;
107
108    /**
109     * Used to control the unpacking of the bundled plugin.
110     * If a pin file exists, Hudson assumes that the user wants to pin down a particular version
111     * of a plugin, and will not try to overwrite it. Otherwise, it'll be overwritten
112     * by a bundled copy, to ensure consistency across upgrade/downgrade.
113     * @since 1.325
114     */
115    private final File pinFile;
116
117    /**
118     * Short name of the plugin. The artifact Id of the plugin.
119     * This is also used in the URL within Hudson, so it needs
120     * to remain stable even when the *.hpi file name is changed
121     * (like Maven does.)
122     */
123    private final String shortName;
124
125    /**
126     * True if this plugin is activated for this session.
127     * The snapshot of <tt>disableFile.exists()</tt> as of the start up.
128     */
129    private final boolean active;
130
131    private final List<Dependency> dependencies;
132    private final List<Dependency> optionalDependencies;
133
134    /**
135     * Is this plugin bundled in hudson.war?
136     */
137    /*package*/ boolean isBundled;
138
139    public static final class Dependency {
140        //TODO: review and check whether we can do it private
141        public final String shortName;
142        public final String version;
143        public final boolean optional;
144
145        public Dependency(String s) {
146            int idx = s.indexOf(':');
147            if(idx==-1)
148                throw new IllegalArgumentException("Illegal dependency specifier "+s);
149            this.shortName = s.substring(0,idx);
150            this.version = s.substring(idx+1);
151            
152            boolean isOptional = false;
153            String[] osgiProperties = s.split(";");
154            for (int i = 1; i < osgiProperties.length; i++) {
155                String osgiProperty = osgiProperties[i].trim();
156                if (osgiProperty.equalsIgnoreCase("resolution:=optional")) {
157                    isOptional = true;
158                }
159            }
160            this.optional = isOptional;
161        }
162
163        public String getShortName() {
164            return shortName;
165        }
166
167        public String getVersion() {
168            return version;
169        }
170
171        public boolean isOptional() {
172            return optional;
173        }
174
175        @Override
176        public String toString() {
177            return shortName + " (" + version + ")";
178        }        
179    }
180
181    /**
182     * @param archive
183     *      A .hpi archive file jar file, or a .hpl linked plugin.
184     *  @param manifest
185     *  	The manifest for the plugin
186     *  @param baseResourceURL
187     *  	A URL pointing to the resources for this plugin
188     *  @param classLoader
189     *  	a classloader that loads classes from this plugin and its dependencies
190     *  @param disableFile
191     *  	if this file exists on startup, the plugin will not be activated
192     *  @param dependencies a list of mandatory dependencies
193     *  @param optionalDependencies a list of optional dependencies
194     */
195    public PluginWrapper(PluginManager parent, File archive, Manifest manifest, URL baseResourceURL, 
196			ClassLoader classLoader, File disableFile, 
197			List<Dependency> dependencies, List<Dependency> optionalDependencies) {
198        this.parent = parent;
199		this.manifest = manifest;
200		this.shortName = computeShortName(manifest, archive);
201		this.baseResourceURL = baseResourceURL;
202		this.classLoader = classLoader;
203		this.disableFile = disableFile;
204        this.pinFile = new File(archive.getPath() + ".pinned");
205		this.active = !disableFile.exists();
206		this.dependencies = dependencies;
207		this.optionalDependencies = optionalDependencies;
208    }
209
210    public PluginManager getParent() {
211        return parent;
212    }
213
214    public ClassLoader getClassLoader() {
215        return classLoader;
216    }
217
218    public URL getBaseResourceURL() {
219        return baseResourceURL;
220    }
221
222    /**
223     * Returns the URL of the index page jelly script.
224     */
225    public URL getIndexPage() {
226        // In the current impl dependencies are checked first, so the plugin itself
227        // will add the last entry in the getResources result.
228        URL idx = null;
229        try {
230            Enumeration<URL> en = classLoader.getResources("index.jelly");
231            while (en.hasMoreElements())
232                idx = en.nextElement();
233        } catch (IOException ignore) { }
234        // In case plugin has dependencies but is missing its own index.jelly,
235        // check that result has this plugin's artifactId in it:
236        return idx != null && idx.toString().contains(shortName) ? idx : null;
237    }
238
239    private String computeShortName(Manifest manifest, File archive) {
240        // use the name captured in the manifest, as often plugins
241        // depend on the specific short name in its URLs.
242        String n = manifest.getMainAttributes().getValue("Short-Name");
243        if(n!=null)     return n;
244
245        // maven seems to put this automatically, so good fallback to check.
246        n = manifest.getMainAttributes().getValue("Extension-Name");
247        if(n!=null)     return n;
248
249        // otherwise infer from the file name, since older plugins don't have
250        // this entry.
251        return getBaseName(archive);
252    }
253
254
255    /**
256     * Gets the "abc" portion from "abc.ext".
257     */
258    static String getBaseName(File archive) {
259        String n = archive.getName();
260        int idx = n.lastIndexOf('.');
261        if(idx>=0)
262            n = n.substring(0,idx);
263        return n;
264    }
265
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    public String getShortName() {
279        return shortName;
280    }
281
282    /**
283     * Gets the instance of {@link Plugin} contributed by this plugin.
284     */
285    public Plugin getPlugin() {
286        return Hudson.lookup(PluginInstanceStore.class).store.get(this);
287    }
288
289    /**
290     * Gets the URL that shows more information about this plugin.
291     * @return
292     *      null if this information is unavailable.
293     * @since 1.283
294     */
295    public String getUrl() {
296        // first look for the manifest entry. This is new in maven-hpi-plugin 1.30
297        String url = manifest.getMainAttributes().getValue("Url");
298        if(url!=null)      return url;
299
300        // fallback to update center metadata
301        UpdateSite.Plugin ui = getInfo();
302        if(ui!=null)    return ui.wiki;
303
304        return null;
305    }
306
307    @Override
308    public String toString() {
309        return "Plugin:" + getShortName();
310    }
311
312    /**
313     * Returns a one-line descriptive name of this plugin.
314     */
315    public String getLongName() {
316        String name = manifest.getMainAttributes().getValue("Long-Name");
317        if(name!=null)      return name;
318        return shortName;
319    }
320
321    /**
322     * Returns the version number of this plugin
323     */
324    public String getVersion() {
325        String v = manifest.getMainAttributes().getValue("Plugin-Version");
326        if(v!=null)      return v;
327
328        // plugins generated before maven-hpi-plugin 1.3 should still have this attribute
329        v = manifest.getMainAttributes().getValue("Implementation-Version");
330        if(v!=null)      return v;
331
332        return "???";
333    }
334
335    /**
336     * Returns the version number of this plugin
337     */
338    public VersionNumber getVersionNumber() {
339        return new VersionNumber(getVersion());
340    }
341
342    /**
343     * Returns true if the version of this plugin is older than the given version.
344     */
345    public boolean isOlderThan(VersionNumber v) {
346        try {
347            return getVersionNumber().compareTo(v) < 0;
348        } catch (IllegalArgumentException e) {
349            // if we can't figure out our current version, it probably means it's very old,
350            // since the version information is missing only from the very old plugins 
351            return true;
352        }
353    }
354
355    /**
356     * Terminates the plugin.
357     */
358    public void stop() {
359        LOGGER.info("Stopping "+shortName);
360        try {
361            getPlugin().stop();
362        } catch(Throwable t) {
363            LOGGER.log(WARNING, "Failed to shut down "+shortName, t);
364        }
365        // Work around a bug in commons-logging.
366        // See http://www.szegedi.org/articles/memleak.html
367        LogFactory.release(classLoader);
368    }
369
370    public void releaseClassLoader() {
371        if (classLoader instanceof Closeable)
372            try {
373                ((Closeable) classLoader).close();
374            } catch (IOException e) {
375                LOGGER.log(WARNING, "Failed to shut down classloader",e);
376            }
377    }
378
379    /**
380     * Enables this plugin next time Hudson runs.
381     */
382    public void enable() throws IOException {
383        if(!disableFile.delete())
384            throw new IOException("Failed to delete "+disableFile);
385    }
386
387    /**
388     * Disables this plugin next time Hudson runs.
389     */
390    public void disable() throws IOException {
391        // creates an empty file
392        OutputStream os = new FileOutputStream(disableFile);
393        os.close();
394    }
395
396    /**
397     * Returns true if this plugin is enabled for this session.
398     */
399    public boolean isActive() {
400        return active;
401    }
402
403    public boolean isBundled() {
404        return isBundled;
405    }
406
407    /**
408     * If true, the plugin is going to be activated next time
409     * Hudson runs.
410     */
411    public boolean isEnabled() {
412        return !disableFile.exists();
413    }
414
415    public Manifest getManifest() {
416        return manifest;
417    }
418
419    public void setPlugin(Plugin plugin) {
420        Hudson.lookup(PluginInstanceStore.class).store.put(this,plugin);
421        plugin.wrapper = this;
422    }
423
424    public String getPluginClass() {
425        return manifest.getMainAttributes().getValue("Plugin-Class");
426    }
427
428    /**
429     * Makes sure that all the dependencies exist, and then accept optional dependencies
430     * as real dependencies.
431     *
432     * @throws IOException
433     *             thrown if one or several mandatory dependencies doesn't exists.
434     */
435    /*package*/ void resolvePluginDependencies() throws IOException {
436        List<String> missingDependencies = new ArrayList<String>();
437        // make sure dependencies exist
438        for (Dependency d : dependencies) {
439            if (parent.getPlugin(d.shortName) == null)
440                missingDependencies.add(d.toString());
441        }
442        if (!missingDependencies.isEmpty())
443            throw new IOException("Dependency "+Util.join(missingDependencies, ", ")+" doesn't exist");
444
445        // add the optional dependencies that exists
446        for (Dependency d : optionalDependencies) {
447            if (parent.getPlugin(d.shortName) != null)
448                dependencies.add(d);
449        }
450    }
451
452    /**
453     * If the plugin has {@link #getUpdateInfo() an update},
454     * returns the {@link UpdateSite.Plugin} object.
455     *
456     * @return
457     *      This method may return null &mdash; for example,
458     *      the user may have installed a plugin locally developed.
459     */
460    public UpdateSite.Plugin getUpdateInfo() {
461        UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
462        UpdateSite.Plugin p = uc.getPlugin(getShortName());
463        if(p!=null && p.isNewerThan(getVersion())) return p;
464        return null;
465    }
466    
467    /**
468     * returns the {@link UpdateSite.Plugin} object, or null.
469     */
470    public UpdateSite.Plugin getInfo() {
471        UpdateCenter uc = Hudson.getInstance().getUpdateCenter();
472        return uc.getPlugin(getShortName());
473    }
474
475    /**
476     * Returns true if this plugin has update in the update center.
477     *
478     * <p>
479     * This method is conservative in the sense that if the version number is incomprehensible,
480     * it always returns false.
481     */
482    public boolean hasUpdate() {
483        return getUpdateInfo()!=null;
484    }
485    
486    public boolean isPinned() {
487        return pinFile.exists();
488    }
489
490    /**
491     * Sort by short name.
492     */
493    public int compareTo(PluginWrapper pw) {
494        return shortName.compareToIgnoreCase(pw.shortName);
495    }
496
497    /**
498     * returns true if backup of previous version of plugin exists
499     */
500    public boolean isDowngradable() {
501        return getBackupFile().exists();
502    }
503
504    /**
505     * Where is the backup file?
506     */
507    public File getBackupFile() {
508        return new File(Hudson.getInstance().getRootDir(),"plugins/"+getShortName() + ".bak");
509    }
510
511    /**
512     * returns the version of the backed up plugin,
513     * or null if there's no back up.
514     */
515    public String getBackupVersion() {
516        if (getBackupFile().exists()) {
517            try {
518                JarFile backupPlugin = new JarFile(getBackupFile());
519                return backupPlugin.getManifest().getMainAttributes().getValue("Plugin-Version");
520            } catch (IOException e) {
521                LOGGER.log(WARNING, "Failed to get backup version ", e);
522                return null;
523            }
524        } else {
525            return null;
526        }
527    }
528//
529//
530// Action methods
531//
532//
533    public HttpResponse doMakeEnabled() throws IOException {
534        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
535        enable();
536        return HttpResponses.ok();
537    }
538
539    public HttpResponse doMakeDisabled() throws IOException {
540        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
541        disable();
542        return HttpResponses.ok();
543    }
544
545    public HttpResponse doPin() throws IOException {
546        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
547        new FileOutputStream(pinFile).close();
548        return HttpResponses.ok();
549    }
550
551    public HttpResponse doUnpin() throws IOException {
552        Hudson.getInstance().checkPermission(Hudson.ADMINISTER);
553        pinFile.delete();
554        return HttpResponses.ok();
555    }
556
557
558    private static final Logger LOGGER = Logger.getLogger(PluginWrapper.class.getName());
559
560}