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