View Javadoc

1   package com.atlassian.plugin.impl;
2   
3   import com.atlassian.fugue.Option;
4   import com.atlassian.plugin.InstallationMode;
5   import com.atlassian.plugin.ModuleDescriptor;
6   import com.atlassian.plugin.Permissions;
7   import com.atlassian.plugin.Plugin;
8   import com.atlassian.plugin.PluginException;
9   import com.atlassian.plugin.PluginInformation;
10  import com.atlassian.plugin.PluginPermission;
11  import com.atlassian.plugin.PluginState;
12  import com.atlassian.plugin.Resourced;
13  import com.atlassian.plugin.Resources;
14  import com.atlassian.plugin.elements.ResourceDescriptor;
15  import com.atlassian.plugin.elements.ResourceLocation;
16  import com.atlassian.plugin.util.VersionStringComparator;
17  import com.atlassian.util.concurrent.CopyOnWriteMap;
18  import com.google.common.base.Function;
19  import com.google.common.base.Predicate;
20  import com.google.common.base.Supplier;
21  import com.google.common.base.Suppliers;
22  import com.google.common.collect.ImmutableSet;
23  import com.google.common.collect.Iterables;
24  import org.apache.commons.lang.StringUtils;
25  import org.slf4j.Logger;
26  import org.slf4j.LoggerFactory;
27  
28  import java.util.ArrayList;
29  import java.util.Collection;
30  import java.util.Collections;
31  import java.util.Date;
32  import java.util.List;
33  import java.util.Map;
34  import java.util.Set;
35  import java.util.concurrent.atomic.AtomicReference;
36  
37  import static com.google.common.base.Suppliers.memoize;
38  
39  /**
40   * Represents the base class for all plugins. Note: This class has a natural ordering that is inconsistent with equals.
41   * <p/>
42   * p1.equals(p2) == true will give p1.compareTo(p2) == 0 but the opposite is not guaranteed because we keep a map of
43   * plugins versus loaders in the DefaultPluginManager .
44   * <p/>
45   * A plugin with the same key and version may well be loaded from multiple loaders (in fact with UPM it's almost
46   * guaranteed) so we CANNOT override equals.
47   */
48  
49  public abstract class AbstractPlugin implements Plugin, Comparable<Plugin>
50  {
51      private final Logger log = LoggerFactory.getLogger(this.getClass());
52  
53      private final Map<String, ModuleDescriptor<?>> modules = CopyOnWriteMap.<String, ModuleDescriptor<?>>builder().stableViews().newLinkedMap();
54      private String name;
55      private String i18nNameKey;
56      private String key;
57      private boolean enabledByDefault = true;
58      private PluginInformation pluginInformation = new PluginInformation();
59      private boolean system;
60      private Resourced resources = Resources.EMPTY_RESOURCES;
61      private int pluginsVersion = 1;
62      private final Date dateLoaded = new Date();
63      private final AtomicReference<PluginState> pluginState = new AtomicReference<PluginState>(PluginState.UNINSTALLED);
64  
65      private final Supplier<Set<String>> permissions;
66  
67      public AbstractPlugin()
68      {
69          permissions = memoize(new Supplier<Set<String>>()
70          {
71              @Override
72              public Set<String> get()
73              {
74                  return getPermissionsInternal();
75              }
76          });
77      }
78  
79      public String getName()
80      {
81          return !StringUtils.isBlank(name) ? name : !StringUtils.isBlank(i18nNameKey) ? "" : getKey();
82      }
83  
84      public void setName(final String name)
85      {
86          this.name = name;
87      }
88  
89      /**
90       * @return the logger used internally
91       */
92      protected Logger getLog()
93      {
94          return log;
95      }
96  
97      public String getI18nNameKey()
98      {
99          return i18nNameKey;
100     }
101 
102     public void setI18nNameKey(final String i18nNameKey)
103     {
104         this.i18nNameKey = i18nNameKey;
105     }
106 
107     public String getKey()
108     {
109         return key;
110     }
111 
112     public void setKey(final String key)
113     {
114         this.key = key;
115     }
116 
117     public void addModuleDescriptor(final ModuleDescriptor<?> moduleDescriptor)
118     {
119         modules.put(moduleDescriptor.getKey(), moduleDescriptor);
120     }
121 
122     protected void removeModuleDescriptor(final String key)
123     {
124         modules.remove(key);
125     }
126 
127     /**
128      * Returns the module descriptors for this plugin
129      *
130      * @return An unmodifiable list of the module descriptors.
131      */
132     public Collection<ModuleDescriptor<?>> getModuleDescriptors()
133     {
134         return modules.values();
135     }
136 
137     public ModuleDescriptor<?> getModuleDescriptor(final String key)
138     {
139         return modules.get(key);
140     }
141 
142     public <T> List<ModuleDescriptor<T>> getModuleDescriptorsByModuleClass(final Class<T> aClass)
143     {
144         final List<ModuleDescriptor<T>> result = new ArrayList<ModuleDescriptor<T>>();
145         for (final ModuleDescriptor<?> moduleDescriptor : modules.values())
146         {
147             final Class<?> moduleClass = moduleDescriptor.getModuleClass();
148             if (moduleClass != null && aClass.isAssignableFrom(moduleClass))
149             {
150                 @SuppressWarnings("unchecked")
151                 final ModuleDescriptor<T> typedModuleDescriptor = (ModuleDescriptor<T>) moduleDescriptor;
152                 result.add(typedModuleDescriptor);
153             }
154         }
155         return result;
156     }
157 
158     public PluginState getPluginState()
159     {
160         return pluginState.get();
161     }
162 
163     protected void setPluginState(final PluginState state)
164     {
165         if (log.isDebugEnabled())
166         {
167             log.debug("Plugin " + getKey() + " going from " + getPluginState() + " to " + state);
168         }
169         pluginState.set(state);
170     }
171 
172     /**
173      * Only sets the plugin state if it is in the expected state.
174      *
175      * @param requiredExistingState The expected state
176      * @param desiredState The desired state
177      * @return True if the set was successful, false if not in the expected state
178      * @since 2.4
179      */
180     protected boolean compareAndSetPluginState(final PluginState requiredExistingState, final PluginState desiredState)
181     {
182         if (log.isDebugEnabled())
183         {
184             log.debug("Plugin {} trying to go from {} to {} but only if in {}",
185                     new Object[] { getKey(), getPluginState(), desiredState, requiredExistingState });
186         }
187         return pluginState.compareAndSet(requiredExistingState, desiredState);
188     }
189 
190     public boolean isEnabledByDefault()
191     {
192         return enabledByDefault && ((pluginInformation == null) || pluginInformation.satisfiesMinJavaVersion());
193     }
194 
195     public void setEnabledByDefault(final boolean enabledByDefault)
196     {
197         this.enabledByDefault = enabledByDefault;
198     }
199 
200     public int getPluginsVersion()
201     {
202         return pluginsVersion;
203     }
204 
205     public void setPluginsVersion(final int pluginsVersion)
206     {
207         this.pluginsVersion = pluginsVersion;
208     }
209 
210     public PluginInformation getPluginInformation()
211     {
212         return pluginInformation;
213     }
214 
215     public void setPluginInformation(final PluginInformation pluginInformation)
216     {
217         this.pluginInformation = pluginInformation;
218     }
219 
220     public void setResources(final Resourced resources)
221     {
222         this.resources = resources != null ? resources : Resources.EMPTY_RESOURCES;
223     }
224 
225     public List<ResourceDescriptor> getResourceDescriptors()
226     {
227         return resources.getResourceDescriptors();
228     }
229 
230     public List<ResourceDescriptor> getResourceDescriptors(final String type)
231     {
232         return resources.getResourceDescriptors(type);
233     }
234 
235     public ResourceLocation getResourceLocation(final String type, final String name)
236     {
237         return resources.getResourceLocation(type, name);
238     }
239 
240     /**
241      * @deprecated
242      */
243     @Deprecated
244     public ResourceDescriptor getResourceDescriptor(final String type, final String name)
245     {
246         return resources.getResourceDescriptor(type, name);
247     }
248 
249     /**
250      * @return true if the plugin has been enabled
251      */
252     @Deprecated
253     public boolean isEnabled()
254     {
255         return getPluginState() == PluginState.ENABLED;
256     }
257 
258     public final void enable()
259     {
260         log.debug("Enabling plugin '{}'", getKey());
261 
262         final PluginState state = pluginState.get();
263         if ((state == PluginState.ENABLED) || (state == PluginState.ENABLING))
264         {
265             log.debug("Plugin '{}' is already enabled, not doing anything.", getKey());
266             return;
267         }
268 
269         try
270         {
271             log.debug("Plugin '{}' is NOT already enabled, actually enabling.", getKey());
272             // not ideal as comparison and set doesn't happen atomically
273             final PluginState desiredState = enableInternal();
274             if ((desiredState != PluginState.ENABLED) && (desiredState != PluginState.ENABLING))
275             {
276                 log.warn("Illegal state transition to {} for plugin '{}' on enable()", desiredState, getKey());
277             }
278             setPluginState(desiredState);
279         }
280         catch (final PluginException ex)
281         {
282             log.warn("Unable to enable plugin '{}'", getKey());
283             log.warn("Because of this exception", ex);
284             throw ex;
285         }
286 
287         log.debug("Enabled plugin '{}'", getKey());
288     }
289 
290     /**
291      * Perform any internal enabling logic. Subclasses should only throw {@link PluginException}.
292      *
293      * @return Either {@link PluginState#ENABLED} or {@link PluginState#ENABLING}
294      * @throws PluginException If the plugin could not be enabled
295      * @since 2.2.0
296      */
297     protected PluginState enableInternal() throws PluginException
298     {
299         return PluginState.ENABLED;
300     }
301 
302     public final void disable()
303     {
304         if (pluginState.get() == PluginState.DISABLED)
305         {
306             return;
307         }
308         if (getLog().isDebugEnabled())
309         {
310             getLog().debug("Disabling plugin '" + getKey() + "'");
311         }
312         try
313         {
314             setPluginState(PluginState.DISABLING);
315             disableInternal();
316             setPluginState(PluginState.DISABLED);
317         }
318         catch (final PluginException ex)
319         {
320             setPluginState(PluginState.ENABLED);
321             log.warn("Unable to disable plugin '" + getKey() + "'", ex);
322             throw ex;
323         }
324         if (getLog().isDebugEnabled())
325         {
326             getLog().debug("Disabled plugin '" + getKey() + "'");
327         }
328     }
329 
330     /**
331      * Perform any internal disabling logic. Subclasses should only throw {@link PluginException}.
332      *
333      * @throws PluginException If the plugin could not be disabled
334      * @since 2.2.0
335      */
336     protected void disableInternal() throws PluginException
337     {
338     }
339 
340     public Set<String> getRequiredPlugins()
341     {
342         return Collections.emptySet();
343     }
344 
345     @Override
346     public final Set<String> getActivePermissions()
347     {
348         return permissions.get();
349     }
350 
351     private Set<String> getPermissionsInternal()
352     {
353         return ImmutableSet.copyOf(Iterables.transform(getPermissionsForCurrentInstallationMode(),
354                 new Function<PluginPermission, String>()
355                 {
356                     @Override
357                     public String apply(PluginPermission p)
358                     {
359                         return p.getName();
360                     }
361                 }));
362     }
363 
364     private Iterable<PluginPermission> getPermissionsForCurrentInstallationMode()
365     {
366         return Iterables.filter(getPluginInformation().getPermissions(),
367                 new Predicate<PluginPermission>()
368                 {
369                     @Override
370                     public boolean apply(PluginPermission p)
371                     {
372                         return isInstallationModeUndefinedOrEqualToCurrentInstallationMode(p.getInstallationMode());
373                     }
374                 });
375     }
376 
377     private boolean isInstallationModeUndefinedOrEqualToCurrentInstallationMode(Option<InstallationMode> installationMode)
378     {
379         return installationMode.fold(Suppliers.ofInstance(Boolean.TRUE),
380                 new Function<InstallationMode, Boolean>()
381                 {
382                     @Override
383                     public Boolean apply(InstallationMode mode)
384                     {
385                         return mode.equals(getInstallationMode());
386                     }
387                 });
388     }
389 
390     @Override
391     public final boolean hasAllPermissions()
392     {
393         return getActivePermissions().contains(Permissions.ALL_PERMISSIONS);
394     }
395 
396     public InstallationMode getInstallationMode()
397     {
398         return InstallationMode.LOCAL;
399     }
400 
401     public void close()
402     {
403         uninstall();
404     }
405 
406     public final void install()
407     {
408         log.debug("Installing plugin '{}'.", getKey());
409 
410         if (pluginState.get() == PluginState.INSTALLED)
411         {
412             log.debug("Plugin '{}' is already installed, not doing anything.", getKey());
413             return;
414         }
415 
416         try
417         {
418             installInternal();
419             setPluginState(PluginState.INSTALLED);
420         }
421         catch (final PluginException ex)
422         {
423             log.warn("Unable to install plugin '" + getKey() + "'.", ex);
424             throw ex;
425         }
426 
427         log.debug("Installed plugin '{}'.", getKey());
428     }
429 
430     /**
431      * Perform any internal installation logic. Subclasses should only throw {@link PluginException}.
432      *
433      * @throws PluginException If the plugin could not be installed
434      * @since 2.2.0
435      */
436     protected void installInternal() throws PluginException
437     {
438         log.debug("Actually installing plugin '{}'.", getKey());
439     }
440 
441     public final void uninstall()
442     {
443         if (pluginState.get() == PluginState.UNINSTALLED)
444         {
445             return;
446         }
447         if (getLog().isDebugEnabled())
448         {
449             getLog().debug("Uninstalling plugin '" + getKey() + "'");
450         }
451         try
452         {
453             uninstallInternal();
454             setPluginState(PluginState.UNINSTALLED);
455         }
456         catch (final PluginException ex)
457         {
458             log.warn("Unable to uninstall plugin '" + getKey() + "'", ex);
459             throw ex;
460         }
461         if (getLog().isDebugEnabled())
462         {
463             getLog().debug("Uninstalled plugin '" + getKey() + "'");
464         }
465     }
466 
467     /**
468      * Perform any internal uninstallation logic. Subclasses should only throw {@link PluginException}.
469      *
470      * @throws PluginException If the plugin could not be uninstalled
471      * @since 2.2.0
472      */
473     protected void uninstallInternal() throws PluginException
474     {
475     }
476 
477     /**
478      * Setter for the enabled state of a plugin. If this is set to false then the plugin will not execute.
479      */
480     @Deprecated
481     public void setEnabled(final boolean enabled)
482     {
483         if (enabled)
484         {
485             enable();
486         }
487         else
488         {
489             disable();
490         }
491     }
492 
493     public boolean isSystemPlugin()
494     {
495         return system;
496     }
497 
498     public boolean containsSystemModule()
499     {
500         for (final ModuleDescriptor<?> moduleDescriptor : modules.values())
501         {
502             if (moduleDescriptor.isSystemModule())
503             {
504                 return true;
505             }
506         }
507         return false;
508     }
509 
510     public void setSystemPlugin(final boolean system)
511     {
512         this.system = system;
513     }
514 
515     public Date getDateLoaded()
516     {
517         return dateLoaded;
518     }
519 
520     @Override
521     public Date getDateInstalled()
522     {
523         return new Date(dateLoaded.getTime());
524     }
525 
526     public boolean isBundledPlugin()
527     {
528         return false;
529     }
530 
531     /**
532      * Compares this Plugin to another Plugin for order. The primary sort field is the key, and the secondary field
533      * is the version number.
534      *
535      * @param otherPlugin The plugin to be compared.
536      * @return a negative integer, zero, or a positive integer as this Plugin is less than, equal to, or greater than
537      *         the specified Plugin.
538      * @see VersionStringComparator
539      * @see Comparable#compareTo
540      */
541     public int compareTo(final Plugin otherPlugin)
542     {
543         if (otherPlugin.getKey() == null)
544         {
545             if (getKey() == null)
546             {
547                 // both null keys - not going to bother checking the version,
548                 // who cares?
549                 return 0;
550             }
551             return 1;
552         }
553         if (getKey() == null)
554         {
555             return -1;
556         }
557 
558         // If the compared plugin doesn't have the same key, the current object
559         // is greater
560         if (!otherPlugin.getKey().equals(getKey()))
561         {
562             return getKey().compareTo(otherPlugin.getKey());
563         }
564 
565         final String thisVersion = cleanVersionString((getPluginInformation() != null ? getPluginInformation().getVersion() : null));
566         final String otherVersion = cleanVersionString((otherPlugin.getPluginInformation() != null ? otherPlugin.getPluginInformation().getVersion() : null));
567 
568         // Valid versions should come after invalid versions because when we
569         // find multiple instances of a plugin, we choose the "latest".
570         if (!VersionStringComparator.isValidVersionString(thisVersion))
571         {
572             if (!VersionStringComparator.isValidVersionString(otherVersion))
573             {
574                 // both invalid
575                 return 0;
576             }
577             return -1;
578         }
579         if (!VersionStringComparator.isValidVersionString(otherVersion))
580         {
581             return 1;
582         }
583 
584         //if they are both equivalent snapshots use timestamps to order them
585         if (VersionStringComparator.isSnapshotVersion(thisVersion) && VersionStringComparator.isSnapshotVersion(otherVersion))
586         {
587             int comparison =  new VersionStringComparator().compare(thisVersion, otherVersion);
588             if (comparison == 0)
589             {
590                 return this.getDateInstalled().compareTo(otherPlugin.getDateInstalled());
591             }
592             else
593             {
594                 return comparison;
595             }
596         }
597 
598         return new VersionStringComparator().compare(thisVersion, otherVersion);
599     }
600 
601     private String cleanVersionString(final String version)
602     {
603         if ((version == null) || version.trim().equals(""))
604         {
605             return "0";
606         }
607         return version.replaceAll(" ", "");
608     }
609 
610     @Override
611     public String toString()
612     {
613         final PluginInformation info = getPluginInformation();
614         return getKey() + ":" + (info == null ? "?" : info.getVersion());
615     }
616 }