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