View Javadoc

1   package com.atlassian.plugin.parsers;
2   
3   import com.atlassian.plugin.Application;
4   import com.atlassian.plugin.InstallationMode;
5   import com.atlassian.plugin.ModuleDescriptor;
6   import com.atlassian.plugin.ModuleDescriptorFactory;
7   import com.atlassian.plugin.Plugin;
8   import com.atlassian.plugin.PluginInformation;
9   import com.atlassian.plugin.PluginParseException;
10  import com.atlassian.plugin.PluginPermission;
11  import com.atlassian.plugin.Resources;
12  import com.atlassian.plugin.descriptors.UnloadableModuleDescriptor;
13  import com.atlassian.plugin.descriptors.UnrecognisedModuleDescriptor;
14  import com.atlassian.plugin.impl.UnloadablePluginFactory;
15  import com.atlassian.plugin.util.PluginUtils;
16  
17  import com.google.common.base.Predicate;
18  import com.google.common.collect.ImmutableSet;
19  import com.google.common.collect.Iterables;
20  import org.apache.commons.lang.Validate;
21  import org.dom4j.Document;
22  import org.dom4j.DocumentException;
23  import org.dom4j.Element;
24  import org.dom4j.io.SAXReader;
25  import org.slf4j.Logger;
26  import org.slf4j.LoggerFactory;
27  
28  import java.io.InputStream;
29  import java.util.Collections;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Set;
33  
34  import static com.atlassian.plugin.descriptors.UnloadableModuleDescriptorFactory.createUnloadableModuleDescriptor;
35  import static com.atlassian.plugin.descriptors.UnrecognisedModuleDescriptorFactory.createUnrecognisedModuleDescriptor;
36  import static com.google.common.base.Preconditions.checkNotNull;
37  import static com.google.common.collect.Iterables.filter;
38  
39  /**
40   * Provides access to the descriptor information retrieved from an XML InputStream.
41   * <p/>
42   * Uses the dom4j {@link SAXReader} to parse the XML stream into a document
43   * when the parser is constructed.
44   *
45   * @see XmlDescriptorParserFactory
46   */
47  public class XmlDescriptorParser implements DescriptorParser
48  {
49      private static final Logger log = LoggerFactory.getLogger(XmlDescriptorParser.class);
50  
51      private final Document document;
52      private final Set<Application> applications;
53  
54      /**
55       * Constructs a parser with an already-constructed document
56       * @param source the source document
57       * @param applications the application key to filter modules with, null for all unspecified
58       * @throws PluginParseException if there is a problem reading the descriptor from the XML {@link InputStream}.
59       * @since 2.2.0
60       */
61      public XmlDescriptorParser(final Document source, final Application... applications) throws PluginParseException
62      {
63          Validate.notNull(source, "XML descriptor source document cannot be null");
64          this.document = source;
65          this.applications = applications == null ? Collections.<Application>emptySet() : ImmutableSet.copyOf(applications);
66      }
67  
68      /**
69       * Constructs a parser with a stream of an XML document for a specific application
70       * @param source The descriptor stream
71       * @param applications the application key to filter modules with, null for all unspecified
72       * @throws PluginParseException if there is a problem reading the descriptor from the XML {@link InputStream}.
73       * @since 2.2.0
74       */
75      public XmlDescriptorParser(final InputStream source, final Application... applications) throws PluginParseException
76      {
77          Validate.notNull(source, "XML descriptor source cannot be null");
78          this.document = createDocument(source);
79          this.applications = applications == null ? Collections.<Application>emptySet() : ImmutableSet.copyOf(applications);
80      }
81  
82      protected Document createDocument(final InputStream source) throws PluginParseException
83      {
84          final SAXReader reader = new SAXReader();
85          reader.setMergeAdjacentText(true);
86          try
87          {
88              return reader.read(source);
89          }
90          catch (final DocumentException e)
91          {
92              throw new PluginParseException("Cannot parse XML plugin descriptor", e);
93          }
94      }
95  
96      protected Document getDocument()
97      {
98          return document;
99      }
100 
101     public Plugin configurePlugin(final ModuleDescriptorFactory moduleDescriptorFactory, final Plugin plugin) throws PluginParseException
102     {
103         final Element pluginElement = getPluginElement();
104         plugin.setName(pluginElement.attributeValue("name"));
105         plugin.setKey(getKey());
106         plugin.setPluginsVersion(getPluginsVersion());
107         plugin.setSystemPlugin(isSystemPlugin());
108 
109         if (pluginElement.attributeValue("i18n-name-key") != null)
110         {
111             plugin.setI18nNameKey(pluginElement.attributeValue("i18n-name-key"));
112         }
113 
114         if (plugin.getKey().indexOf(":") > 0)
115         {
116             throw new PluginParseException("Plugin keys cannot contain ':'. Key is '" + plugin.getKey() + "'");
117         }
118 
119         if ("disabled".equalsIgnoreCase(pluginElement.attributeValue("state")))
120         {
121             plugin.setEnabledByDefault(false);
122         }
123 
124         plugin.setResources(Resources.fromXml(pluginElement));
125 
126         for (final Iterator i = pluginElement.elementIterator(); i.hasNext(); )
127         {
128             final Element element = (Element) i.next();
129 
130             if ("plugin-info".equalsIgnoreCase(element.getName()))
131             {
132                 plugin.setPluginInformation(createPluginInformation(element));
133             }
134             else if (!"resource".equalsIgnoreCase(element.getName()))
135             {
136                 final ModuleDescriptor<?> moduleDescriptor = createModuleDescriptor(plugin, element, moduleDescriptorFactory);
137 
138                 // If we're not loading the module descriptor, null is returned, so we skip it
139                 if (moduleDescriptor == null)
140                 {
141                     continue;
142                 }
143 
144                 if (plugin.getModuleDescriptor(moduleDescriptor.getKey()) != null)
145                 {
146                     throw new PluginParseException("Found duplicate key '" + moduleDescriptor.getKey() + "' within plugin '" + plugin.getKey() + "'");
147                 }
148 
149                 plugin.addModuleDescriptor(moduleDescriptor);
150 
151                 // If we have any unloadable modules, also create an unloadable plugin, which will make it clear that there was a problem
152                 if (moduleDescriptor instanceof UnloadableModuleDescriptor)
153                 {
154                     log.error("There were errors loading the plugin '" + plugin.getName() + "'. The plugin has been disabled.");
155                     return UnloadablePluginFactory.createUnloadablePlugin(plugin);
156                 }
157             }
158         }
159 
160         return plugin;
161     }
162 
163     private Element getPluginElement()
164     {
165         return document.getRootElement();
166     }
167 
168     protected ModuleDescriptor<?> createModuleDescriptor(final Plugin plugin, final Element element, final ModuleDescriptorFactory moduleDescriptorFactory) throws PluginParseException
169     {
170         final String name = element.getName();
171 
172         // Determine if this module descriptor is applicable for the current application
173         if (!PluginUtils.doesModuleElementApplyToApplication(element, applications))
174         {
175             log.debug("Ignoring module descriptor for this application: " + element.attributeValue("key"));
176             return null;
177         }
178 
179         ModuleDescriptor<?> moduleDescriptorDescriptor;
180 
181         // Try to retrieve the module descriptor
182         try
183         {
184             moduleDescriptorDescriptor = moduleDescriptorFactory.getModuleDescriptor(name);
185         }
186         // When there's a problem loading a module, return an UnrecognisedModuleDescriptor with error
187         catch (final Throwable e)
188         {
189             final UnrecognisedModuleDescriptor descriptor = createUnrecognisedModuleDescriptor(plugin, element, e, moduleDescriptorFactory);
190 
191             log.error("There were problems loading the module '" + name + "' in plugin '" + plugin.getName() + "'. The module has been disabled.");
192             log.error(descriptor.getErrorText(), e);
193 
194             return descriptor;
195         }
196 
197         // When the module descriptor has been excluded, null is returned (PLUG-5)
198         if (moduleDescriptorDescriptor == null)
199         {
200             log.info("The module '" + name + "' in plugin '" + plugin.getName() + "' is in the list of excluded module descriptors, so not enabling.");
201             return null;
202         }
203 
204         // Once we have the module descriptor, create it using the given information
205         try
206         {
207             moduleDescriptorDescriptor.init(plugin, element);
208         }
209         // If it fails, return a dummy module that contains the error
210         catch (final Exception e)
211         {
212             final UnloadableModuleDescriptor descriptor = createUnloadableModuleDescriptor(plugin, element, e, moduleDescriptorFactory);
213 
214             log.error("There were problems loading the module '" + name + "'. The module and its plugin have been disabled.");
215             log.error(descriptor.getErrorText(), e);
216 
217             return descriptor;
218         }
219 
220         return moduleDescriptorDescriptor;
221     }
222 
223     protected PluginInformation createPluginInformation(final Element element)
224     {
225         final PluginInformation pluginInfo = new PluginInformation();
226 
227         if (element.element("description") != null)
228         {
229             pluginInfo.setDescription(element.element("description").getTextTrim());
230             if (element.element("description").attributeValue("key") != null)
231             {
232                 pluginInfo.setDescriptionKey(element.element("description").attributeValue("key"));
233             }
234         }
235 
236         if (element.element("version") != null)
237         {
238             pluginInfo.setVersion(element.element("version").getTextTrim());
239         }
240 
241         if (element.element("vendor") != null)
242         {
243             final Element vendor = element.element("vendor");
244             pluginInfo.setVendorName(vendor.attributeValue("name"));
245             pluginInfo.setVendorUrl(vendor.attributeValue("url"));
246         }
247 
248         // initialize any parameters on the plugin xml definition
249         for (Element param : getElements(element, "param"))
250         {
251             // Retrieve the parameter info => name & text
252             if (param.attribute("name") != null)
253             {
254                 pluginInfo.addParameter(param.attribute("name").getData().toString(), param.getText());
255             }
256         }
257 
258         if (element.element("application-version") != null)
259         {
260             final Element ver = element.element("application-version");
261             if (ver.attribute("max") != null)
262             {
263                 pluginInfo.setMaxVersion(Float.parseFloat(ver.attributeValue("max")));
264             }
265             if (ver.attribute("min") != null)
266             {
267                 pluginInfo.setMinVersion(Float.parseFloat(ver.attributeValue("min")));
268             }
269         }
270 
271         if (element.element("java-version") != null)
272         {
273             pluginInfo.setMinJavaVersion(Float.valueOf(element.element("java-version").attributeValue("min")));
274         }
275 
276         if (element.element("permissions") != null)
277         {
278             ImmutableSet.Builder<PluginPermission> permissions = ImmutableSet.builder();
279             for (Element permission : filter(getElements(element.element("permissions"), "permission"), new ElementWithForApplicationsPredicate(applications)))
280             {
281                 permissions.add(new PluginPermission(permission.getTextTrim(), InstallationMode.of(permission.attributeValue("installation-mode"))));
282             }
283             pluginInfo.setPermissions(permissions.build());
284         }
285         else if (getPluginsVersion() < 3)
286         {
287             pluginInfo.setPermissions(ImmutableSet.of(PluginPermission.ALL));
288         }
289 
290         return pluginInfo;
291     }
292 
293     public String getKey()
294     {
295         return getPluginElement().attributeValue("key");
296     }
297 
298     public int getPluginsVersion()
299     {
300         String val = getPluginElement().attributeValue("pluginsVersion");
301         if (val == null)
302         {
303             val = getPluginElement().attributeValue("plugins-version");
304         }
305         if (val != null)
306         {
307             try
308             {
309                 return Integer.parseInt(val);
310             }
311             catch (final NumberFormatException e)
312             {
313                 throw new RuntimeException("Could not parse pluginsVersion: " + e.getMessage(), e);
314             }
315         }
316         else
317         {
318             return 1;
319         }
320     }
321 
322     public PluginInformation getPluginInformation()
323     {
324         return createPluginInformation(getDocument().getRootElement().element("plugin-info"));
325     }
326 
327     public boolean isSystemPlugin()
328     {
329         return "true".equalsIgnoreCase(getPluginElement().attributeValue("system"));
330     }
331 
332     @SuppressWarnings("unchecked")
333     private List<Element> getElements(Element element, String name)
334     {
335         return element.elements(name);
336     }
337 
338     private static final class ApplicationWithNamePredicate implements Predicate<Application>
339     {
340         private final String name;
341 
342         public ApplicationWithNamePredicate(String name)
343         {
344             this.name = name;
345         }
346 
347         @Override
348         public boolean apply(Application app)
349         {
350             return app.getKey().equals(name); // name might be null
351         }
352     }
353 
354     private static final class ElementWithForApplicationsPredicate implements Predicate<Element>
355     {
356         private final Set<Application> applications;
357 
358         private ElementWithForApplicationsPredicate(Set<Application> applications)
359         {
360             this.applications = checkNotNull(applications);
361         }
362 
363         @Override
364         public boolean apply(final Element el)
365         {
366             final String appName = el.attributeValue("application");
367             return appName == null || Iterables.any(applications, new ApplicationWithNamePredicate(appName));
368         }
369     }
370 }