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