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