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