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