View Javadoc

1   package com.atlassian.plugin.webresource;
2   
3   import static com.atlassian.plugin.util.EfficientStringUtils.endsWith;
4   import static com.google.common.collect.Iterables.filter;
5   
6   import java.util.ArrayList;
7   import java.util.Collections;
8   import java.util.List;
9   import java.util.Map;
10  import java.util.TreeMap;
11  
12  import com.atlassian.plugin.util.PluginUtils;
13  import org.apache.commons.lang.StringUtils;
14  import org.slf4j.Logger;
15  import org.slf4j.LoggerFactory;
16  
17  import com.atlassian.plugin.ModuleDescriptor;
18  import com.atlassian.plugin.Plugin;
19  import com.atlassian.plugin.PluginAccessor;
20  import com.atlassian.plugin.Resources;
21  import com.atlassian.plugin.Resources.TypeFilter;
22  import com.atlassian.plugin.elements.ResourceDescriptor;
23  import com.atlassian.plugin.elements.ResourceLocation;
24  import com.atlassian.plugin.servlet.DownloadableClasspathResource;
25  import com.atlassian.plugin.servlet.DownloadableResource;
26  import com.atlassian.plugin.servlet.DownloadableWebResource;
27  import com.atlassian.plugin.servlet.ForwardableResource;
28  import com.atlassian.plugin.servlet.ServletContextFactory;
29  import com.google.common.collect.Iterables;
30  
31  /**
32   * Default implementation of {@link PluginResourceLocator}.
33   * 
34   * @since 2.2
35   */
36  public class PluginResourceLocatorImpl implements PluginResourceLocator
37  {
38      private static final Logger log = LoggerFactory.getLogger(PluginResourceLocatorImpl.class);
39  
40      public static final String PLUGIN_WEBRESOURCE_BATCHING_OFF = "plugin.webresource.batching.off";
41  
42      private static final String DOWNLOAD_TYPE = "download";
43  
44      final private PluginAccessor pluginAccessor;
45      final private ServletContextFactory servletContextFactory;
46      final private ResourceDependencyResolver dependencyResolver;
47  
48      private static final String RESOURCE_SOURCE_PARAM = "source";
49      private static final String RESOURCE_BATCH_PARAM = "batch";
50  
51      public PluginResourceLocatorImpl(final WebResourceIntegration webResourceIntegration, final ServletContextFactory servletContextFactory)
52      {
53          this(webResourceIntegration, servletContextFactory, new DefaultResourceDependencyResolver(webResourceIntegration, new DefaultResourceBatchingConfiguration()));
54      }
55  
56      public PluginResourceLocatorImpl(final WebResourceIntegration webResourceIntegration, final ServletContextFactory servletContextFactory,
57          final ResourceBatchingConfiguration resourceBatchingConfiguration)
58      {
59          this(webResourceIntegration, servletContextFactory, new DefaultResourceDependencyResolver(webResourceIntegration, resourceBatchingConfiguration));
60      }
61  
62      private PluginResourceLocatorImpl(final WebResourceIntegration webResourceIntegration, final ServletContextFactory servletContextFactory,
63          final ResourceDependencyResolver dependencyResolver)
64      {
65          this.pluginAccessor = webResourceIntegration.getPluginAccessor();
66          this.servletContextFactory = servletContextFactory;
67          this.dependencyResolver = dependencyResolver;
68      }
69  
70      public boolean matches(final String url)
71      {
72          return SuperBatchPluginResource.matches(url) || SuperBatchSubResource.matches(url) || SinglePluginResource.matches(url) || BatchPluginResource.matches(url);
73      }
74  
75      public DownloadableResource getDownloadableResource(final String url, final Map<String, String> queryParams)
76      {
77          try
78          {
79              if (SuperBatchPluginResource.matches(url))
80              {
81                  return locateSuperBatchPluginResource(SuperBatchPluginResource.parse(url, queryParams));
82              }
83              if (SuperBatchSubResource.matches(url))
84              {
85                  return locateSuperBatchSubPluginResource(SuperBatchSubResource.parse(url, queryParams));
86              }
87              if (BatchPluginResource.matches(url))
88              {
89                  return locateBatchPluginResource(BatchPluginResource.parse(url, queryParams));
90              }
91              if (SinglePluginResource.matches(url))
92              {
93                  final SinglePluginResource resource = SinglePluginResource.parse(url);
94                  return locatePluginResource(resource.getModuleCompleteKey(), resource.getResourceName());
95              }
96          }
97          catch (final UrlParseException e)
98          {
99              log.error("Unable to parse URL: " + url, e);
100         }
101         // TODO: It would be better to use Exceptions rather than returning
102         // nulls to indicate an error.
103         return null;
104     }
105 
106     private DownloadableResource locateSuperBatchPluginResource(final SuperBatchPluginResource batchResource)
107     {
108         if (log.isDebugEnabled())
109         {
110             log.debug(batchResource.toString());
111         }
112 
113         for (final String moduleKey : dependencyResolver.getSuperBatchDependencies())
114         {
115             final ModuleDescriptor<?> moduleDescriptor = pluginAccessor.getEnabledPluginModule(moduleKey);
116             if (moduleDescriptor == null)
117             {
118                 log.info("Resource batching configuration refers to plugin that does not exist: " + moduleKey);
119             }
120             else
121             {
122                 if (log.isDebugEnabled())
123                 {
124                     log.debug("searching resources in: " + moduleKey);
125                 }
126 
127                 for (final ResourceDescriptor resourceDescriptor : filter(moduleDescriptor.getResourceDescriptors(), new Resources.TypeFilter(DOWNLOAD_TYPE)))
128                 {
129                     if (isResourceInBatch(resourceDescriptor, batchResource))
130                     {
131                         batchResource.add(locatePluginResource(moduleDescriptor.getCompleteKey(), resourceDescriptor.getName()));
132                     }
133                 }
134             }
135         }
136         return batchResource;
137     }
138 
139     private DownloadableResource locateSuperBatchSubPluginResource(final SuperBatchSubResource superBatchSubResource)
140     {
141         for (final String moduleKey : dependencyResolver.getSuperBatchDependencies())
142         {
143             final DownloadableResource pluginResource = locatePluginResource(moduleKey, superBatchSubResource.getResourceName());
144             if (pluginResource != null)
145             {
146                 return pluginResource;
147             }
148         }
149         log.warn("Could not locate resource in superbatch: " + superBatchSubResource.getResourceName());
150         return superBatchSubResource;
151     }
152 
153     private DownloadableResource locateBatchPluginResource(final BatchPluginResource batchResource)
154     {
155         final ModuleDescriptor<?> moduleDescriptor = pluginAccessor.getEnabledPluginModule(batchResource.getModuleCompleteKey());
156         for (final ResourceDescriptor resourceDescriptor : Iterables.filter(moduleDescriptor.getResourceDescriptors(), new TypeFilter(DOWNLOAD_TYPE)))
157         {
158             if (isResourceInBatch(resourceDescriptor, batchResource))
159             {
160                 batchResource.add(locatePluginResource(moduleDescriptor.getCompleteKey(), resourceDescriptor.getName()));
161             }
162         }
163 
164         // if batch is empty, check if we can locate a plugin resource
165         if (batchResource.isEmpty())
166         {
167             final DownloadableResource resource = locatePluginResource(batchResource.getModuleCompleteKey(), batchResource.getResourceName());
168             if (resource != null)
169             {
170                 return resource;
171             }
172         }
173 
174         return batchResource;
175     }
176 
177     private boolean isResourceInBatch(final ResourceDescriptor resourceDescriptor, final BatchResource batchResource)
178     {
179         if (!descriptorTypeMatchesResourceType(resourceDescriptor, batchResource.getType()))
180         {
181             return false;
182         }
183 
184         if (skipBatch(resourceDescriptor))
185         {
186             return false;
187         }
188 
189         for (final String param : BATCH_PARAMS)
190         {
191             final String batchValue = batchResource.getParams().get(param);
192             final String resourceValue = resourceDescriptor.getParameter(param);
193 
194             if ((batchValue == null) && (resourceValue != null))
195             {
196                 return false;
197             }
198 
199             if ((batchValue != null) && !batchValue.equals(resourceValue))
200             {
201                 return false;
202             }
203         }
204 
205         return true;
206     }
207 
208     private boolean descriptorTypeMatchesResourceType(final ResourceDescriptor resourceDescriptor, final String type)
209     {
210         return endsWith(resourceDescriptor.getName(), ".", type);
211     }
212 
213     private DownloadableResource locatePluginResource(final String moduleCompleteKey, final String resourceName)
214     {
215         DownloadableResource resource;
216 
217         // resource from the module
218         if (moduleCompleteKey.indexOf(":") > -1)
219         {
220             final ModuleDescriptor<?> moduleDescriptor = pluginAccessor.getEnabledPluginModule(moduleCompleteKey);
221             if (moduleDescriptor != null)
222             {
223                 resource = getResourceFromModule(moduleDescriptor, resourceName, "");
224             }
225             else
226             {
227                 log.info("Module not found: " + moduleCompleteKey);
228                 return null;
229             }
230         }
231         else
232         // resource from plugin
233         {
234             resource = getResourceFromPlugin(pluginAccessor.getPlugin(moduleCompleteKey), resourceName, "");
235         }
236 
237         if (resource == null)
238         {
239             resource = getResourceFromPlugin(getPlugin(moduleCompleteKey), resourceName, "");
240         }
241 
242         if (resource == null)
243         {
244             log.info("Unable to find resource for plugin: " + moduleCompleteKey + " and path: " + resourceName);
245             return null;
246         }
247 
248         return resource;
249     }
250 
251     private Plugin getPlugin(final String moduleKey)
252     {
253         if ((moduleKey.indexOf(':') < 0) || (moduleKey.indexOf(':') == moduleKey.length() - 1))
254         {
255             return null;
256         }
257 
258         return pluginAccessor.getPlugin(moduleKey.substring(0, moduleKey.indexOf(':')));
259     }
260 
261     private DownloadableResource getResourceFromModule(final ModuleDescriptor<?> moduleDescriptor, final String resourcePath, final String filePath)
262     {
263         final Plugin plugin = pluginAccessor.getPlugin(moduleDescriptor.getPluginKey());
264         final ResourceLocation resourceLocation = moduleDescriptor.getResourceLocation(DOWNLOAD_TYPE, resourcePath);
265 
266         if (resourceLocation != null)
267         {
268             boolean disableMinification = false;
269             // I think it should always be a WebResourceModuleDescriptor, but
270             // not sure...
271             if (moduleDescriptor instanceof WebResourceModuleDescriptor)
272             {
273                 disableMinification = ((WebResourceModuleDescriptor) moduleDescriptor).isDisableMinification();
274             }
275             return getDownloadablePluginResource(plugin, resourceLocation, moduleDescriptor, filePath, disableMinification);
276         }
277 
278         final String[] nextParts = splitLastPathPart(resourcePath);
279         if (nextParts == null)
280         {
281             return null;
282         }
283 
284         return getResourceFromModule(moduleDescriptor, nextParts[0], nextParts[1] + filePath);
285     }
286 
287     private DownloadableResource getResourceFromPlugin(final Plugin plugin, final String resourcePath, final String filePath)
288     {
289         if (plugin == null)
290         {
291             return null;
292         }
293 
294         final ResourceLocation resourceLocation = plugin.getResourceLocation(DOWNLOAD_TYPE, resourcePath);
295         if (resourceLocation != null)
296         {
297             return getDownloadablePluginResource(plugin, resourceLocation, null, filePath, false);
298         }
299 
300         final String[] nextParts = splitLastPathPart(resourcePath);
301         if (nextParts == null)
302         {
303             return null;
304         }
305 
306         return getResourceFromPlugin(plugin, nextParts[0], nextParts[1] + filePath);
307     }
308 
309     // pacakge protected so we can test it
310     String[] splitLastPathPart(final String resourcePath)
311     {
312         int indexOfSlash = resourcePath.lastIndexOf('/');
313         if (resourcePath.endsWith("/")) // skip over the trailing slash
314         {
315             indexOfSlash = resourcePath.lastIndexOf('/', indexOfSlash - 1);
316         }
317 
318         if (indexOfSlash < 0)
319         {
320             return null;
321         }
322 
323         return new String[] { resourcePath.substring(0, indexOfSlash + 1), resourcePath.substring(indexOfSlash + 1) };
324     }
325 
326     private DownloadableResource getDownloadablePluginResource(final Plugin plugin, final ResourceLocation resourceLocation,
327                                                                ModuleDescriptor descriptor, final String filePath,
328                                                                final boolean disableMinification)
329     {
330         final String sourceParam = resourceLocation.getParameter(RESOURCE_SOURCE_PARAM);
331 
332         // serve by forwarding the request to the location - batching not
333         // supported
334         if ("webContext".equalsIgnoreCase(sourceParam))
335         {
336             return new ForwardableResource(resourceLocation);
337         }
338 
339         DownloadableResource actualResource = null;
340         // serve static resources from the web application - batching supported
341         if ("webContextStatic".equalsIgnoreCase(sourceParam))
342         {
343             actualResource = new DownloadableWebResource(plugin, resourceLocation, filePath, servletContextFactory.getServletContext(), disableMinification);
344         }
345         else
346         {
347             actualResource = new DownloadableClasspathResource(plugin, resourceLocation, filePath);
348         }
349 
350         DownloadableResource result = actualResource;
351         // web resources are able to be transformed during delivery
352         if (descriptor instanceof WebResourceModuleDescriptor)
353         {
354             DownloadableResource lastResource = actualResource;
355             WebResourceModuleDescriptor desc = (WebResourceModuleDescriptor) descriptor;
356             for (WebResourceTransformation list : desc.getTransformations())
357             {
358                 if (list.matches(resourceLocation))
359                 {
360                     lastResource = list.transformDownloadableResource(pluginAccessor, actualResource, resourceLocation, filePath);
361                 }
362             }
363             result = lastResource;
364         }
365         return result;
366     }
367 
368     public List<PluginResource> getPluginResources(final String moduleCompleteKey)
369     {
370         final ModuleDescriptor<?> moduleDescriptor = pluginAccessor.getEnabledPluginModule(moduleCompleteKey);
371         if ((moduleDescriptor == null) || !(moduleDescriptor instanceof WebResourceModuleDescriptor))
372         {
373             log.error("Error loading resource \"" + moduleCompleteKey + "\". Resource is not a Web Resource Module");
374             return Collections.emptyList();
375         }
376 
377         final boolean singleMode = isBatchingOff();
378         final List<PluginResource> resources = new ArrayList<PluginResource>();
379 
380         for (final ResourceDescriptor resourceDescriptor : moduleDescriptor.getResourceDescriptors())
381         {
382             if (singleMode || skipBatch(resourceDescriptor))
383             {
384                 final boolean cache = !"false".equalsIgnoreCase(resourceDescriptor.getParameter("cache"));
385                 resources.add(new SinglePluginResource(resourceDescriptor.getName(), moduleDescriptor.getCompleteKey(), cache, resourceDescriptor.getParameters()));
386             }
387             else
388             {
389                 final BatchPluginResource batchResource = createBatchResource(moduleDescriptor.getCompleteKey(), resourceDescriptor);
390                 if (!resources.contains(batchResource))
391                 {
392                     resources.add(batchResource);
393                 }
394             }
395         }
396         return resources;
397     }
398 
399     /**
400      * @return True if either it is explicitly turned off or in dev mode
401      */
402     Boolean isBatchingOff()
403     {
404         String explicitSetting = System.getProperty(PLUGIN_WEBRESOURCE_BATCHING_OFF);
405         if (explicitSetting != null)
406         {
407             return Boolean.parseBoolean(explicitSetting);
408         }
409         else
410         {
411             return Boolean.parseBoolean(System.getProperty(PluginUtils.ATLASSIAN_DEV_MODE));
412         }
413 
414     }
415 
416     private boolean skipBatch(final ResourceDescriptor resourceDescriptor)
417     {
418         // you can't batch forwarded requests
419         return "false".equalsIgnoreCase(resourceDescriptor.getParameter(RESOURCE_BATCH_PARAM))
420             || "webContext".equalsIgnoreCase(resourceDescriptor.getParameter(RESOURCE_SOURCE_PARAM));
421     }
422 
423     private BatchPluginResource createBatchResource(final String moduleCompleteKey, final ResourceDescriptor resourceDescriptor)
424     {
425         final String name = resourceDescriptor.getName();
426         final String type = name.substring(name.lastIndexOf(".") + 1);
427         final Map<String, String> params = new TreeMap<String, String>();
428         for (final String param : BATCH_PARAMS)
429         {
430             final String value = resourceDescriptor.getParameter(param);
431             if (StringUtils.isNotEmpty(value))
432             {
433                 params.put(param, value);
434             }
435         }
436 
437         return new BatchPluginResource(moduleCompleteKey, type, params);
438     }
439 
440     public String getResourceUrl(final String moduleCompleteKey, final String resourceName)
441     {
442         return new SinglePluginResource(resourceName, moduleCompleteKey, false).getUrl();
443     }
444 }