View Javadoc
1   package com.atlassian.plugin.descriptors;
2   
3   import com.atlassian.plugin.ModuleDescriptor;
4   import com.atlassian.plugin.ModulePermissionException;
5   import com.atlassian.plugin.Permissions;
6   import com.atlassian.plugin.Plugin;
7   import com.atlassian.plugin.PluginParseException;
8   import com.atlassian.plugin.RequirePermission;
9   import com.atlassian.plugin.Resources;
10  import com.atlassian.plugin.StateAware;
11  import com.atlassian.plugin.elements.ResourceDescriptor;
12  import com.atlassian.plugin.elements.ResourceLocation;
13  import com.atlassian.plugin.loaders.LoaderUtils;
14  import com.atlassian.plugin.module.LegacyModuleFactory;
15  import com.atlassian.plugin.module.ModuleFactory;
16  import com.atlassian.plugin.module.PrefixDelegatingModuleFactory;
17  import com.atlassian.plugin.util.ClassUtils;
18  import com.atlassian.plugin.util.JavaVersionUtils;
19  import com.atlassian.plugin.util.validation.ValidationPattern;
20  import com.google.common.collect.ImmutableSet;
21  import com.google.common.collect.Sets;
22  import org.dom4j.Element;
23  import org.slf4j.Logger;
24  import org.slf4j.LoggerFactory;
25  
26  import javax.annotation.Nonnull;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Optional;
30  import java.util.Set;
31  
32  import static com.atlassian.plugin.util.Assertions.notNull;
33  import static com.atlassian.plugin.util.validation.ValidationPattern.createPattern;
34  import static com.atlassian.plugin.util.validation.ValidationPattern.test;
35  import static com.google.common.base.Preconditions.checkNotNull;
36  import static java.lang.Boolean.TRUE;
37  
38  public abstract class AbstractModuleDescriptor<T> implements ModuleDescriptor<T>, StateAware {
39      private static final Logger log = LoggerFactory.getLogger(AbstractModuleDescriptor.class);
40  
41      protected Plugin plugin;
42      protected String key;
43      protected String name;
44      protected String moduleClassName;
45      protected Class<T> moduleClass;
46      private String description;
47      private boolean enabledByDefault = true;
48      private boolean systemModule = false;
49      private Optional<String> scopeKey = Optional.empty();
50  
51      /**
52       * @deprecated since 2.2.0
53       */
54      @Deprecated
55      protected boolean singleton = true;
56      private Map<String, String> params;
57      protected Resources resources = Resources.EMPTY_RESOURCES;
58      private Float minJavaVersion;
59      private String i18nNameKey;
60      private String descriptionKey;
61      private String completeKey;
62      private boolean enabled = false;
63      protected final ModuleFactory moduleFactory;
64      private boolean broken = false;
65  
66      public AbstractModuleDescriptor(final ModuleFactory moduleFactory) {
67          this.moduleFactory = checkNotNull(moduleFactory, "Module creator factory cannot be null");
68      }
69  
70      @Override
71      public void init(@Nonnull final Plugin plugin, @Nonnull final Element element) throws PluginParseException {
72          validate(element);
73  
74          this.plugin = notNull("plugin", plugin);
75          this.key = element.attributeValue("key");
76          this.name = element.attributeValue("name");
77          this.i18nNameKey = element.attributeValue("i18n-name-key");
78          this.completeKey = buildCompleteKey(plugin, key);
79          this.description = element.elementTextTrim("description");
80          this.moduleClassName = element.attributeValue("class");
81          final Element descriptionElement = element.element("description");
82          this.descriptionKey = (descriptionElement != null)
83                  ? descriptionElement.attributeValue("key")
84                  : null;
85          final String scopedAttribute = element.attributeValue("scoped");
86          this.scopeKey = scopedAttribute == null || TRUE.toString().equalsIgnoreCase(scopedAttribute)
87                  ? this.plugin.getScopeKey()
88                  : Optional.<String>empty();
89  
90          params = LoaderUtils.getParams(element);
91  
92          if ("disabled".equalsIgnoreCase(element.attributeValue("state"))) {
93              enabledByDefault = false;
94          }
95  
96          if ("true".equalsIgnoreCase(element.attributeValue("system"))) {
97              systemModule = true;
98          }
99  
100         if (element.element("java-version") != null) {
101             minJavaVersion = Float.valueOf(element.element("java-version").attributeValue("min"));
102         }
103 
104         if ("false".equalsIgnoreCase(element.attributeValue("singleton"))) {
105             singleton = false;
106         } else if ("true".equalsIgnoreCase(element.attributeValue("singleton"))) {
107             singleton = true;
108         } else {
109             singleton = isSingletonByDefault();
110         }
111 
112         resources = Resources.fromXml(element);
113     }
114 
115     /**
116      * This is a method that module should call (in their {@link #init(Plugin, Element) init method implementation} to
117      * check that permissions are correctly set on the plugin. Use the {@link RequirePermission} annotation to add
118      * permissions to a module, and/or {@link #getRequiredPermissions} if you need more dynamic permissions.
119      *
120      * If either all permissions are allowed or this is a system module, we don't bother to check permissions
121      *
122      * @throws ModulePermissionException if the plugin doesn't require the necessary permissions for the module to work.
123      * @see RequirePermission
124      * @see #getRequiredPermissions()
125      * @since 3.0
126      */
127     protected final void checkPermissions() throws ModulePermissionException {
128         if (plugin.hasAllPermissions() || isSystemModule()) {
129             return;
130         }
131 
132         final Sets.SetView<String> difference = Sets.difference(getAllRequiredPermissions(), plugin.getActivePermissions());
133         if (!difference.isEmpty()) {
134             throw new ModulePermissionException(getCompleteKey(), difference.immutableCopy());
135         }
136     }
137 
138     private Set<String> getAllRequiredPermissions() {
139         final ImmutableSet.Builder<String> permissions = ImmutableSet.builder();
140         permissions.addAll(Permissions.getRequiredPermissions(this.getClass()));
141         permissions.addAll(getRequiredPermissions());
142         return permissions.build();
143     }
144 
145     /**
146      * <p>The set of additionally required permissions. This is useful for dynamic permissions depending on module
147      * configuration. One might prefer the {@link RequirePermission} annotation otherwise for 'static' permissions.
148      * <p>Default implementation returns an empty set
149      *
150      * @return the set of additionally required permissions.
151      * @see RequirePermission
152      * @see #checkPermissions()
153      * @since 3.0
154      */
155     protected Set<String> getRequiredPermissions() {
156         return ImmutableSet.of();
157     }
158 
159     /**
160      * Validates the element, collecting the rules from
161      * {@link #provideValidationRules(ValidationPattern)}
162      *
163      * @param element The configuration element
164      * @since 2.2.0
165      */
166     private void validate(final Element element) {
167         notNull("element", element);
168         final ValidationPattern pattern = createPattern();
169         provideValidationRules(pattern);
170         pattern.evaluate(element);
171     }
172 
173     /**
174      * Provides validation rules for the pattern
175      *
176      * @param pattern The validation pattern
177      * @since 2.2.0
178      */
179     protected void provideValidationRules(final ValidationPattern pattern) {
180         pattern.rule(test("@key").withError("The key is required"));
181     }
182 
183     /**
184      * Override this for module descriptors which don't expect to be able to
185      * load a class successfully
186      *
187      * @param plugin
188      * @param element
189      * @deprecated Since 2.1.0, use {@link #loadClass(Plugin, String)} instead
190      */
191     @Deprecated
192     protected void loadClass(final Plugin plugin, final Element element) throws PluginParseException {
193         loadClass(plugin, element.attributeValue("class"));
194     }
195 
196     /**
197      * Loads the module class that this descriptor provides, and will not
198      * necessarily be the implementation class. Override this for module
199      * descriptors whose type cannot be determined via generics.
200      *
201      * @param clazz The module class name to load
202      * @throws IllegalStateException If the module class cannot be determined
203      *                               and the descriptor doesn't define a module type via generics
204      */
205     protected void loadClass(final Plugin plugin, final String clazz) throws PluginParseException {
206         if (moduleClassName != null) {
207             if (moduleFactory instanceof LegacyModuleFactory) // not all plugins have to have a class
208             {
209                 moduleClass = ((LegacyModuleFactory) moduleFactory).getModuleClass(moduleClassName, this);
210             }
211 
212             // This is only here for backwards compatibility with old code that
213             // uses {@link
214             // com.atlassian.plugin.PluginAccessor#getEnabledModulesByClass(Class)}
215             else if (moduleFactory instanceof PrefixDelegatingModuleFactory) {
216                 moduleClass = ((PrefixDelegatingModuleFactory) moduleFactory).guessModuleClass(moduleClassName, this);
217             }
218         }
219         // If this module has no class, then we assume Void
220         else {
221             moduleClass = (Class<T>) Void.class;
222         }
223 
224         // Usually is null when a prefix is used in the class name
225         if (moduleClass == null) {
226             try {
227 
228                 Class moduleTypeClass = null;
229                 try {
230                     moduleTypeClass = ClassUtils.getTypeArguments(AbstractModuleDescriptor.class, getClass()).get(0);
231                 } catch (RuntimeException ex) {
232                     log.debug("Unable to get generic type, usually due to Class.forName() problems", ex);
233                     moduleTypeClass = getModuleReturnClass();
234                 }
235                 moduleClass = moduleTypeClass;
236             } catch (final ClassCastException ex) {
237                 throw new IllegalStateException("The module class must be defined in a concrete instance of " + "ModuleDescriptor and not as another generic type.");
238             }
239 
240             if (moduleClass == null) {
241                 throw new IllegalStateException("The module class cannot be determined, likely because it needs a concrete "
242                         + "module type defined in the generic type it passes to AbstractModuleDescriptor");
243             }
244         }
245     }
246 
247     Class<?> getModuleReturnClass() {
248         try {
249             return getClass().getMethod("getModule").getReturnType();
250         } catch (NoSuchMethodException e) {
251             throw new IllegalStateException("The getModule() method is missing (!) on " + getClass());
252         }
253     }
254 
255     /**
256      * Build the complete key based on the provided plugin and module key. This
257      * method has no side effects.
258      *
259      * @param plugin    The plugin for which the module belongs
260      * @param moduleKey The key for the module
261      * @return A newly constructed complete key, null if the plugin is null
262      */
263     private String buildCompleteKey(final Plugin plugin, final String moduleKey) {
264         if (plugin == null) {
265             return null;
266         }
267 
268         final StringBuffer completeKeyBuffer = new StringBuffer(32);
269         completeKeyBuffer.append(plugin.getKey()).append(":").append(moduleKey);
270         return completeKeyBuffer.toString();
271     }
272 
273     /**
274      * The default implementation disables the module if it's still enabled, and unreference the plugin.
275      * Override this if your plugin needs to clean up when it's been removed,
276      * and call super.destroy(Plugin) before returning.
277      */
278     @Override
279     public void destroy() {
280         destroy(plugin);
281     }
282 
283     @Deprecated
284     @Override
285     public void destroy(final Plugin plugin) {
286         if (enabled) {
287             this.disabled();
288         }
289     }
290 
291     @Override
292     public boolean isEnabledByDefault() {
293         return enabledByDefault && satisfiesMinJavaVersion();
294     }
295 
296     @Override
297     public boolean isSystemModule() {
298         return systemModule;
299     }
300 
301     /**
302      * @deprecated Since 2.2.0
303      */
304     @Deprecated
305     public boolean isSingleton() {
306         return singleton;
307     }
308 
309     /**
310      * @deprecated Since 2.2.0
311      */
312     @Deprecated
313     protected boolean isSingletonByDefault() {
314         return true;
315     }
316 
317     /**
318      * Check that the module class of this descriptor implements a given
319      * interface, or extends a given class.
320      *
321      * @param requiredModuleClazz The class this module's class must implement
322      *                            or extend.
323      * @throws PluginParseException If the module class does not implement or
324      *                              extend the given class.
325      */
326     final protected void assertModuleClassImplements(final Class<T> requiredModuleClazz) throws PluginParseException {
327         if (!enabled) {
328             throw new PluginParseException("Plugin module " + getKey() + " not enabled");
329         }
330         if (!requiredModuleClazz.isAssignableFrom(getModuleClass())) {
331             throw new PluginParseException("Given module class: " + getModuleClass().getName() + " does not implement " + requiredModuleClazz.getName());
332         }
333     }
334 
335     @Override
336     public String getCompleteKey() {
337         return completeKey;
338     }
339 
340     @Override
341     public String getPluginKey() {
342         return getPlugin().getKey();
343     }
344 
345     @Override
346     public String getKey() {
347         return key;
348     }
349 
350     @Override
351     public String getName() {
352         return name;
353     }
354 
355     @Override
356     public Class<T> getModuleClass() {
357         return moduleClass;
358     }
359 
360     @Override
361     public abstract T getModule();
362 
363     @Override
364     public Optional<String> getScopeKey() {
365         return scopeKey;
366     }
367 
368     @Override
369     public String getDescription() {
370         return description;
371     }
372 
373     @Override
374     public Map<String, String> getParams() {
375         return params;
376     }
377 
378     @Override
379     public String getI18nNameKey() {
380         return i18nNameKey;
381     }
382 
383     @Override
384     public String getDescriptionKey() {
385         return descriptionKey;
386     }
387 
388     @Override
389     public List<ResourceDescriptor> getResourceDescriptors() {
390         return resources.getResourceDescriptors();
391     }
392 
393     @Override
394     public List<ResourceDescriptor> getResourceDescriptors(final String type) {
395         return resources.getResourceDescriptors(type);
396     }
397 
398     @Override
399     public ResourceLocation getResourceLocation(final String type, final String name) {
400         return resources.getResourceLocation(type, name);
401     }
402 
403     @Override
404     public ResourceDescriptor getResourceDescriptor(final String type, final String name) {
405         return resources.getResourceDescriptor(type, name);
406     }
407 
408     @Override
409     public Float getMinJavaVersion() {
410         return minJavaVersion;
411     }
412 
413     @Override
414     public boolean satisfiesMinJavaVersion() {
415         if (minJavaVersion != null) {
416             return JavaVersionUtils.satisfiesMinVersion(minJavaVersion);
417         }
418         return true;
419     }
420 
421     /**
422      * Sets the plugin for the ModuleDescriptor
423      *
424      * @param plugin The plugin to set for this descriptor.
425      */
426     public void setPlugin(final Plugin plugin) {
427         this.completeKey = buildCompleteKey(plugin, key);
428         this.plugin = plugin;
429     }
430 
431     /**
432      * @return The plugin this module descriptor is associated with.
433      */
434     @Override
435     public Plugin getPlugin() {
436         return plugin;
437     }
438 
439     @Override
440     public boolean equals(Object obj) {
441         return new ModuleDescriptors.EqualsBuilder().descriptor(this).isEqualTo(obj);
442     }
443 
444     @Override
445     public int hashCode() {
446         return new ModuleDescriptors.HashCodeBuilder().descriptor(this).toHashCode();
447     }
448 
449     @Override
450     public String toString() {
451         return getCompleteKey() + " (" + getDescription() + ")";
452     }
453 
454     /**
455      * Enables the descriptor by loading the module class. Classes overriding
456      * this method MUST call super.enabled() before their own enabling code.
457      *
458      * @since 2.1.0
459      */
460     public void enabled() {
461         loadClass(plugin, moduleClassName);
462         enabled = true;
463         broken = false;
464     }
465 
466     /**
467      * Disables the module descriptor. Classes overriding this method MUST call
468      * super.disabled() after their own disabling code.
469      */
470     public void disabled() {
471         enabled = false;
472         moduleClass = null;
473     }
474 
475     /**
476      * @return The defined module class name
477      * @since 3.0
478      */
479     protected String getModuleClassName() {
480         return moduleClassName;
481     }
482 
483     /**
484      * Get whether this plugin module is enabled.
485      *
486      * @return If this module is enabled
487      * @since 3.1
488      */
489     @Override
490     public boolean isEnabled() {
491         return enabled;
492     }
493 
494     @Override
495     public void setBroken() {
496         broken = true;
497     }
498 
499     @Override
500     public boolean isBroken() {
501         return broken;
502     }
503 }