View Javadoc

1   package com.atlassian.plugin.parsers;
2   
3   import com.atlassian.fugue.Option;
4   import com.atlassian.plugin.Application;
5   import com.atlassian.plugin.InstallationMode;
6   import com.atlassian.plugin.ModuleDescriptor;
7   import com.atlassian.plugin.ModuleDescriptorFactory;
8   import com.atlassian.plugin.Plugin;
9   import com.atlassian.plugin.PluginInformation;
10  import com.atlassian.plugin.PluginParseException;
11  import com.atlassian.plugin.PluginPermission;
12  import com.atlassian.plugin.Resources;
13  import com.atlassian.plugin.descriptors.UnloadableModuleDescriptor;
14  import com.atlassian.plugin.descriptors.UnrecognisedModuleDescriptor;
15  import com.atlassian.plugin.impl.UnloadablePluginFactory;
16  import com.atlassian.plugin.util.PluginUtils;
17  import com.google.common.base.Function;
18  import com.google.common.base.Predicate;
19  import com.google.common.collect.ImmutableSet;
20  import com.google.common.collect.Iterables;
21  import org.apache.commons.lang.StringUtils;
22  import org.dom4j.Document;
23  import org.dom4j.DocumentException;
24  import org.dom4j.Element;
25  import org.dom4j.io.SAXReader;
26  import org.slf4j.Logger;
27  import org.slf4j.LoggerFactory;
28  
29  import java.io.InputStream;
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.atlassian.plugin.parsers.XmlDescriptorParserUtils.removeAllNamespaces;
37  import static com.google.common.base.Preconditions.checkNotNull;
38  import static com.google.common.collect.ImmutableList.copyOf;
39  import static com.google.common.collect.Iterables.filter;
40  import static com.google.common.collect.Iterables.transform;
41  
42  /**
43   * Provides access to the descriptor information retrieved from an XML InputStream.
44   * <p/>
45   * Uses the dom4j {@link SAXReader} to parse the XML stream into a document
46   * when the parser is constructed.
47   *
48   * @see XmlDescriptorParserFactory
49   */
50  public class XmlDescriptorParser implements DescriptorParser
51  {
52      private static final Logger log = LoggerFactory.getLogger(XmlDescriptorParser.class);
53  
54      private final Document document;
55      private final Set<Application> applications;
56  
57      /**
58       * Constructs a parser with an already-constructed document
59       * @param source the source document
60       * @param applications the application key to filter modules with, null for all unspecified
61       * @throws PluginParseException if there is a problem reading the descriptor from the XML {@link InputStream}.
62       * @since 2.2.0
63       */
64      public XmlDescriptorParser(final Document source, final Set<Application> applications) throws PluginParseException
65      {
66          this.document = removeAllNamespaces(checkNotNull(source, "XML descriptor source document cannot be null"));
67          this.applications = ImmutableSet.copyOf(checkNotNull(applications));
68      }
69  
70      /**
71       * Constructs a parser with a stream of an XML document for a specific application
72       * @param source The descriptor stream
73       * @param applications the application key to filter modules with, null for all unspecified
74       * @throws PluginParseException if there is a problem reading the descriptor from the XML {@link InputStream}.
75       * @since 2.2.0
76       */
77      public XmlDescriptorParser(final InputStream source, final Set<Application> applications) throws PluginParseException
78      {
79          this(createDocument(checkNotNull(source, "XML descriptor source cannot be null")), applications);
80      }
81  
82      protected static 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, plugin.getInstallationMode()))
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                 final String trimmedPermission = permission.getTextTrim();
282                 if (StringUtils.isNotBlank(trimmedPermission))
283                 {
284                     final String parsedInstallationMode = permission.attributeValue("installation-mode");
285                     final Option<InstallationMode> installationMode = InstallationMode.of(parsedInstallationMode);
286                     if (StringUtils.isNotBlank(parsedInstallationMode) && !installationMode.isDefined())
287                     {
288                         log.warn("The parsed installation mode '{}' for permission '{}' didn't match any of the valid values: {}",
289                                 new Object[]{parsedInstallationMode, trimmedPermission,
290                                         transform(copyOf(InstallationMode.values()), new Function<InstallationMode, String>()
291                                         {
292                                             @Override
293                                             public String apply(InstallationMode im)
294                                             {
295                                                 return im.getKey();
296                                             }
297                                         })});
298                     }
299 
300                     permissions.add(new PluginPermission(trimmedPermission, installationMode));
301                 }
302                 else
303                 {
304                     log.warn("Plugin {} has blank permission.", getKey());
305                 }
306             }
307             pluginInfo.setPermissions(permissions.build());
308         }
309         else if (getPluginsVersion() < Plugin.VERSION_3)
310         {
311             pluginInfo.setPermissions(ImmutableSet.of(PluginPermission.ALL));
312         }
313 
314         return pluginInfo;
315     }
316 
317     public String getKey()
318     {
319         return getPluginElement().attributeValue("key");
320     }
321 
322     public int getPluginsVersion()
323     {
324         String val = getPluginElement().attributeValue("pluginsVersion");
325         if (val == null)
326         {
327             val = getPluginElement().attributeValue("plugins-version");
328         }
329         if (val != null)
330         {
331             try
332             {
333                 return Integer.parseInt(val);
334             }
335             catch (final NumberFormatException e)
336             {
337                 throw new RuntimeException("Could not parse pluginsVersion: " + e.getMessage(), e);
338             }
339         }
340         else
341         {
342             return 1;
343         }
344     }
345 
346     public PluginInformation getPluginInformation()
347     {
348         return createPluginInformation(getDocument().getRootElement().element("plugin-info"));
349     }
350 
351     public boolean isSystemPlugin()
352     {
353         return Boolean.valueOf(getPluginElement().attributeValue("system"));
354     }
355 
356     @SuppressWarnings("unchecked")
357     private List<Element> getElements(Element element, String name)
358     {
359         return element.elements(name);
360     }
361 
362     private static final class ApplicationWithNamePredicate implements Predicate<Application>
363     {
364         private final String name;
365 
366         public ApplicationWithNamePredicate(String name)
367         {
368             this.name = name;
369         }
370 
371         @Override
372         public boolean apply(Application app)
373         {
374             return app.getKey().equals(name); // name might be null
375         }
376     }
377 
378     private static final class ElementWithForApplicationsPredicate implements Predicate<Element>
379     {
380         private final Set<Application> applications;
381 
382         private ElementWithForApplicationsPredicate(Set<Application> applications)
383         {
384             this.applications = checkNotNull(applications);
385         }
386 
387         @Override
388         public boolean apply(final Element el)
389         {
390             final String appName = el.attributeValue("application");
391             return appName == null || Iterables.any(applications, new ApplicationWithNamePredicate(appName));
392         }
393     }
394 }