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