View Javadoc

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