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.descriptors.UnloadableModuleDescriptor;
12  import com.atlassian.plugin.impl.UnloadablePluginFactory;
13  import com.atlassian.security.xml.SecureXmlParserFactory;
14  import com.google.common.collect.ImmutableSet;
15  import com.google.common.collect.Iterables;
16  import org.apache.commons.lang3.StringUtils;
17  import org.dom4j.Document;
18  import org.dom4j.Element;
19  import org.dom4j.Node;
20  import org.dom4j.io.DOMReader;
21  import org.dom4j.io.SAXReader;
22  import org.slf4j.Logger;
23  import org.slf4j.LoggerFactory;
24  import org.xml.sax.InputSource;
25  import org.xml.sax.SAXException;
26  
27  import javax.xml.parsers.DocumentBuilder;
28  import java.io.IOException;
29  import java.io.InputStream;
30  import java.util.Iterator;
31  import java.util.Map;
32  import java.util.Optional;
33  import java.util.Set;
34  
35  import static com.atlassian.plugin.descriptors.UnloadableModuleDescriptorFactory.createUnloadableModuleDescriptor;
36  import static com.atlassian.plugin.parsers.XmlDescriptorParserUtils.newModuleDescriptor;
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.transform;
40  
41  /**
42   * Provides access to the descriptor information retrieved from an XML InputStream.
43   * <p>
44   * Uses the dom4j {@link SAXReader} to parse the XML stream into a document
45   * when the parser is constructed.
46   *
47   * @see XmlDescriptorParserFactory
48   */
49  public class XmlDescriptorParser implements DescriptorParser {
50      private static final Logger log = LoggerFactory.getLogger(XmlDescriptorParser.class);
51  
52      private final PluginDescriptorReader descriptorReader;
53  
54      /**
55       * Constructs a parser with an already-constructed document
56       *
57       * @param source       the source document
58       * @param applications the application key to filter modules with, null for all unspecified
59       * @throws PluginParseException if there is a problem reading the descriptor from the XML {@link InputStream}.
60       * @since 2.2.0
61       */
62      public XmlDescriptorParser(final Document source, final Set<Application> applications) throws PluginParseException {
63          this.descriptorReader = new PluginDescriptorReader(checkNotNull(source, "XML descriptor source document cannot be null"), checkNotNull(applications));
64      }
65  
66      /**
67       * Constructs a parser with a stream of an XML document for a specific application
68       *
69       * @param source       The descriptor stream
70       * @param applications the application key to filter modules with, null for all unspecified
71       * @throws PluginParseException if there is a problem reading the descriptor from the XML {@link InputStream}.
72       * @since 2.2.0
73       */
74      public XmlDescriptorParser(final InputStream source, final Set<Application> applications) throws PluginParseException {
75          this(createDocument(checkNotNull(source, "XML descriptor source cannot be null")), applications);
76      }
77  
78      /**
79       * Constructs a parser with a stream of an XML document for a specific application
80       *
81       * @param source              The descriptor stream
82       * @param supplementalSources a collection of streams containing supplemental ModuleDescriptors
83       * @param applications        the application key to filter modules with, null for all unspecified
84       * @throws PluginParseException if there is a problem reading the descriptor from the XML {@link InputStream}.
85       * @since 3.2.16
86       */
87      public XmlDescriptorParser(final InputStream source, final Iterable<InputStream> supplementalSources, final Set<Application> applications) throws PluginParseException {
88          checkNotNull(source, "XML descriptor source cannot be null");
89          checkNotNull(supplementalSources, "Supplemental XML descriptors cannot be null");
90          Document mainDescriptor = createDocument(source);
91          final Iterable<Document> supplementalDocs = Iterables.transform(supplementalSources, XmlDescriptorParser::createDocument);
92          mainDescriptor = mergeDocuments(mainDescriptor, supplementalDocs);
93          descriptorReader = new PluginDescriptorReader(mainDescriptor, checkNotNull(applications));
94      }
95  
96      protected static Document createDocument(final InputStream source) throws PluginParseException {
97  
98          final DocumentBuilder documentBuilder = SecureXmlParserFactory.newNamespaceAwareDocumentBuilder();
99          try {
100             org.w3c.dom.Document document = documentBuilder.parse(new InputSource(source));
101             document.normalize();
102             return (new DOMReader()).read(document);
103         } catch (final IOException | SAXException e) {
104             throw new PluginParseException("Cannot parse XML plugin descriptor", e);
105         }
106     }
107 
108     protected static Document mergeDocuments(final Document mainDocument, final Iterable<Document> supplementalDocuments) {
109         Element mainRootElement = mainDocument.getRootElement();
110         for (Document supplementalDocument : supplementalDocuments) {
111             Element supplementaryRoot = supplementalDocument.getRootElement();
112             for (Iterator<Node> iter = supplementaryRoot.content().iterator(); iter.hasNext(); ) {
113                 Node node = iter.next();
114                 iter.remove();
115                 mainRootElement.add(node);
116             }
117         }
118         return mainDocument;
119     }
120 
121     protected Document getDocument() {
122         return descriptorReader.getDescriptor();
123     }
124 
125     public Plugin configurePlugin(final ModuleDescriptorFactory moduleDescriptorFactory, final Plugin plugin) throws PluginParseException {
126         plugin.setName(descriptorReader.getPluginName());
127         plugin.setKey(getKey());
128         plugin.setPluginsVersion(getPluginsVersion());
129         plugin.setSystemPlugin(isSystemPlugin());
130         plugin.setI18nNameKey(descriptorReader.getI18nPluginNameKey().orElseGet(plugin::getI18nNameKey));
131 
132         if (plugin.getKey().indexOf(":") > 0) {
133             throw new PluginParseException("Plugin keys cannot contain ':'. Key is '" + plugin.getKey() + "'");
134         }
135 
136         plugin.setEnabledByDefault(descriptorReader.isEnabledByDefault());
137         plugin.setResources(descriptorReader.getResources());
138         plugin.setPluginInformation(createPluginInformation());
139 
140         for (Element module : descriptorReader.getModules(plugin.getInstallationMode())) {
141             final ModuleDescriptor<?> moduleDescriptor = createModuleDescriptor(plugin, module, moduleDescriptorFactory);
142 
143             // If we're not loading the module descriptor, null is returned, so we skip it
144             if (moduleDescriptor == null) {
145                 continue;
146             }
147 
148             if (plugin.getModuleDescriptor(moduleDescriptor.getKey()) != null) {
149                 throw new PluginParseException("Found duplicate key '" + moduleDescriptor.getKey() + "' within plugin '" + plugin.getKey() + "'");
150             }
151 
152             plugin.addModuleDescriptor(moduleDescriptor);
153 
154             // If we have any unloadable modules, also create an unloadable plugin, which will make it clear that there was a problem
155             if (moduleDescriptor instanceof UnloadableModuleDescriptor) {
156                 log.error("There were errors loading the plugin '" + plugin.getName() + "'. The plugin has been disabled.");
157                 return UnloadablePluginFactory.createUnloadablePlugin(plugin);
158             }
159         }
160         return plugin;
161     }
162 
163     @Override
164     public ModuleDescriptor<?> addModule(final ModuleDescriptorFactory moduleDescriptorFactory, final Plugin plugin, final Element module) {
165         return XmlDescriptorParserUtils.addModule(moduleDescriptorFactory, plugin, module);
166     }
167 
168     protected ModuleDescriptor<?> createModuleDescriptor(final Plugin plugin, final Element element, final ModuleDescriptorFactory moduleDescriptorFactory) throws PluginParseException {
169         final String name = element.getName();
170 
171         final ModuleDescriptor<?> moduleDescriptor = newModuleDescriptor(plugin, element, moduleDescriptorFactory);
172 
173         // When the module descriptor has been excluded, null is returned (PLUG-5)
174         if (moduleDescriptor == null) {
175             log.info("The module '{}' in plugin '{}' is in the list of excluded module descriptors, so not enabling.", name, plugin.getName());
176             return null;
177         }
178 
179         // Once we have the module descriptor, create it using the given information
180         try {
181             moduleDescriptor.init(plugin, element);
182         }
183         // If it fails, return a dummy module that contains the error
184         catch (final Exception e) {
185             final UnloadableModuleDescriptor descriptor = createUnloadableModuleDescriptor(plugin, element, e, moduleDescriptorFactory);
186 
187             log.error("There were problems loading the module '{}'. The module and its plugin have been disabled.", name);
188             log.error(descriptor.getErrorText(), e);
189 
190             return descriptor;
191         }
192 
193         return moduleDescriptor;
194     }
195 
196     protected PluginInformation createPluginInformation() {
197         final PluginInformationReader pluginInformationReader = descriptorReader.getPluginInformationReader();
198 
199         final PluginInformation pluginInfo = new PluginInformation();
200         pluginInfo.setDescription(pluginInformationReader.getDescription().orElseGet(pluginInfo::getDescription));
201         pluginInfo.setDescriptionKey(pluginInformationReader.getDescriptionKey().orElseGet(pluginInfo::getDescriptionKey));
202         pluginInfo.setVersion(pluginInformationReader.getVersion().orElseGet(pluginInfo::getVersion));
203         pluginInfo.setVendorName(pluginInformationReader.getVendorName().orElseGet(pluginInfo::getVendorName));
204         pluginInfo.setVendorUrl(pluginInformationReader.getVendorUrl().orElseGet(pluginInfo::getVendorUrl));
205         pluginInfo.setScopeKey(pluginInformationReader.getScopeKey());
206 
207         for (Map.Entry<String, String> param : pluginInformationReader.getParameters().entrySet()) {
208             pluginInfo.addParameter(param.getKey(), param.getValue());
209         }
210         pluginInfo.setMinJavaVersion(pluginInformationReader.getMinJavaVersion().orElseGet(pluginInfo::getMinJavaVersion));
211         pluginInfo.setStartup(pluginInformationReader.getStartup().orElseGet(pluginInfo::getStartup));
212         pluginInfo.setModuleScanFolders(pluginInformationReader.getModuleScanFolders());
213 
214         final Map<String, Optional<String>> readPermissions = pluginInformationReader.getPermissions();
215         if (pluginInformationReader.hasAllPermissions()) {
216             pluginInfo.setPermissions(ImmutableSet.of(PluginPermission.ALL));
217         } else {
218             final ImmutableSet.Builder<PluginPermission> permissions = ImmutableSet.builder();
219             for (Map.Entry<String, Optional<String>> permission : readPermissions.entrySet()) {
220                 final String permissionKey = permission.getKey();
221                 final Optional<String> readInstallationMode = permission.getValue();
222                 final Optional<InstallationMode> installationMode = readInstallationMode.flatMap(InstallationMode::of);
223                 if (StringUtils.isNotBlank(readInstallationMode.orElse(null)) && !installationMode.isPresent()) {
224                     log.warn("The parsed installation mode '{}' for permission '{}' didn't match any of the valid values: {}",
225                             readInstallationMode, permission.getKey(),
226                             transform(copyOf(InstallationMode.values()), InstallationMode::getKey));
227                 }
228 
229                 permissions.add(new PluginPermission(permissionKey, installationMode.orElse(null)));
230             }
231             pluginInfo.setPermissions(permissions.build());
232         }
233 
234         return pluginInfo;
235     }
236 
237     public String getKey() {
238         return descriptorReader.getPluginKey();
239     }
240 
241     public int getPluginsVersion() {
242         return descriptorReader.getPluginsVersion();
243     }
244 
245     public PluginInformation getPluginInformation() {
246         return createPluginInformation();
247     }
248 
249     public boolean isSystemPlugin() {
250         return descriptorReader.isSystemPlugin();
251     }
252 }