View Javadoc

1   package com.atlassian.plugin.webresource;
2   
3   import static com.atlassian.plugin.util.Assertions.notNull;
4   import static com.google.common.collect.ImmutableMap.copyOf;
5   import static com.google.common.collect.Iterables.addAll;
6   import static com.google.common.collect.Iterables.concat;
7   import static com.google.common.collect.Iterables.contains;
8   import static com.google.common.collect.Iterables.transform;
9   
10  import com.atlassian.plugin.ModuleDescriptor;
11  
12  import com.google.common.base.Supplier;
13  import com.google.common.collect.ImmutableSet;
14  import com.google.common.collect.Lists;
15  import com.google.common.collect.Sets;
16  
17  import java.io.IOException;
18  import java.io.StringWriter;
19  import java.io.Writer;
20  import java.util.ArrayList;
21  import java.util.Arrays;
22  import java.util.Collections;
23  import java.util.HashMap;
24  import java.util.HashSet;
25  import java.util.Iterator;
26  import java.util.LinkedList;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.Set;
30  
31  import org.apache.commons.collections.CollectionUtils;
32  
33  /**
34   * A handy super-class that handles most of the resource management.
35   * <p/>
36   * To use this manager, you need to have the following UrlRewriteFilter code:
37   * <pre>
38   * &lt;rule>
39   * &lt;from>^/s/(.*)/_/(.*)&lt;/from>
40   * &lt;run class="com.atlassian.plugin.servlet.ResourceDownloadUtils" method="addCachingHeaders" />
41   * &lt;to type="forward">/$2&lt;/to>
42   * &lt;/rule>
43   * </pre>
44   * <p/>
45   * Sub-classes should implement the abstract methods
46   */
47  public class WebResourceManagerImpl implements WebResourceManager
48  {
49      static final String STATIC_RESOURCE_PREFIX = "s";
50      static final String STATIC_RESOURCE_SUFFIX = "_";
51  
52      static final String REQUEST_CACHE_RESOURCE_KEY = "plugin.webresource.names";
53      static final String REQUEST_CACHE_CONTEXT_KEY = "plugin.webresource.contexts";
54      
55      protected final WebResourceIntegration webResourceIntegration;
56      protected final PluginResourceLocator pluginResourceLocator;
57      private final WebResourceUrlProvider webResourceUrlProvider;
58      protected final ResourceBatchingConfiguration batchingConfiguration;
59      protected final ResourceDependencyResolver dependencyResolver;
60      protected static final List<WebResourceFormatter> webResourceFormatters = Arrays.asList(CssWebResource.FORMATTER, JavascriptWebResource.FORMATTER);
61  
62      private static final boolean IGNORE_SUPERBATCHING = false;
63  
64      public WebResourceManagerImpl(final PluginResourceLocator pluginResourceLocator, final WebResourceIntegration webResourceIntegration, final WebResourceUrlProvider webResourceUrlProvider)
65      {
66          this(pluginResourceLocator, webResourceIntegration, webResourceUrlProvider, new DefaultResourceBatchingConfiguration());
67      }
68  
69      public WebResourceManagerImpl(final PluginResourceLocator pluginResourceLocator, final WebResourceIntegration webResourceIntegration, final WebResourceUrlProvider webResourceUrlProvider, final ResourceBatchingConfiguration batchingConfiguration)
70      {
71          this(pluginResourceLocator, webResourceIntegration, webResourceUrlProvider, batchingConfiguration, new DefaultResourceDependencyResolver(
72              webResourceIntegration, batchingConfiguration));
73      }
74  
75      public WebResourceManagerImpl(final PluginResourceLocator pluginResourceLocator, final WebResourceIntegration webResourceIntegration, final WebResourceUrlProvider webResourceUrlProvider, final ResourceBatchingConfiguration batchingConfiguration, final ResourceDependencyResolver dependencyResolver)
76      {
77          this.pluginResourceLocator = notNull("pluginResourceLocator", pluginResourceLocator);
78          this.webResourceIntegration = notNull("webResourceIntegration", webResourceIntegration);
79          this.webResourceUrlProvider = notNull("webResourceUrlProvider", webResourceUrlProvider);
80          this.batchingConfiguration = notNull("batchingConfiguration", batchingConfiguration);
81          this.dependencyResolver = notNull("dependencyResolver", dependencyResolver);
82      }
83  
84      public void requireResource(final String moduleCompleteKey)
85      {
86          final boolean batchingEnabled = batchingConfiguration.isSuperBatchingEnabled();
87          addAll(getIncludedResourceNames(), toModuleKeys(dependencyResolver.getDependencies(moduleCompleteKey, batchingEnabled)));
88      }
89  
90      public void requireResourcesForContext(final String context)
91      {
92          getIncludedContexts(false).add(context);
93      }
94  
95      public void includeResources(final Iterable<String> moduleCompleteKeys, final Writer writer, final UrlMode urlMode)
96      {
97          Iterable<String> resources = Lists.newArrayList();
98          for (final String moduleCompleteKey : moduleCompleteKeys)
99          {
100             // Include resources from the super batch as we don't include the super batch itself
101             final Iterable<String> dependencies = toModuleKeys(dependencyResolver.getDependencies(moduleCompleteKey, false));
102             resources = concat(resources, dependencies);
103         }
104 
105         // Resolve duplicates
106         resources = ImmutableSet.copyOf(resources);
107         writeResourceTags(getModuleResources(resources, Collections.<String> emptyList(), DefaultWebResourceFilter.INSTANCE), writer, urlMode);
108     }
109 
110     /**
111      * This is the equivalent of of calling {@link #includeResources(Writer, UrlMode, WebResourceFilter)} with
112      * the given url mode and a {@link DefaultWebResourceFilter}.
113      *
114      * @see #includeResources(Writer, UrlMode, WebResourceFilter)
115      */
116     public void includeResources(final Writer writer, final UrlMode urlMode)
117     {
118         includeResources(writer, urlMode, DefaultWebResourceFilter.INSTANCE);
119     }
120 
121     /**
122      * Writes out the resource tags to the previously required resources called via requireResource methods for the
123      * specified url mode and resource filter. Note that this method will clear the list of previously required resources.
124      *
125      * @param writer the writer to write the links to
126      * @param urlMode the url mode to write resource url links in
127      * @param webResourceFilter the resource filter to filter resources on
128      * @since 2.4
129      */
130     public void includeResources(final Writer writer, final UrlMode urlMode, final WebResourceFilter webResourceFilter)
131     {
132         writeIncludedResources(writer, urlMode, webResourceFilter);
133         clear();
134     }
135 
136     /**
137      * This is the equivalent of calling {@link #getRequiredResources(UrlMode, WebResourceFilter)} with the given url
138      * mode and a {@link DefaultWebResourceFilter}.
139      *
140      * @see #getRequiredResources(UrlMode, WebResourceFilter)
141      */
142     public String getRequiredResources(final UrlMode urlMode)
143     {
144         return getRequiredResources(urlMode, DefaultWebResourceFilter.INSTANCE);
145     }
146 
147     /**
148      * Returns a String of the resources tags to the previously required resources called via requireResource methods
149      * for the specified url mode and resource filter. Note that this method will NOT clear the list of previously
150      * required resources.
151      *
152      * @param urlMode the url mode to write out the resource tags
153      * @param filter the web resource filter to filter resources on
154      * @return a String of the resource tags
155      * @since 2.4
156      */
157     public String getRequiredResources(final UrlMode urlMode, final WebResourceFilter filter)
158     {
159         return writeIncludedResources(new StringWriter(), urlMode, filter).toString();
160     }
161 
162     /**
163      * Write all currently included resources to the given writer.
164      */
165     private <W extends Writer> W writeIncludedResources(final W writer, final UrlMode urlMode, final WebResourceFilter filter)
166     {
167         final ContextBatchBuilder builder = new ContextBatchBuilder(pluginResourceLocator, dependencyResolver, batchingConfiguration);
168         final Iterable<PluginResource> resourcesToInclude = concat(getSuperBatchResources(filter), builder.build(getIncludedContexts(false), filter));
169         for (final String skippedResource : builder.getSkippedResources())
170         {
171             requireResource(skippedResource);
172         }
173         final Iterable<PluginResource> moduleResources = getModuleResources(getIncludedResourceNames(), builder.getAllIncludedResources(), filter);
174         return writeResourceTags(concat(resourcesToInclude, moduleResources), writer, urlMode);
175     }
176 
177     /**
178      * Get all super-batch resources that match the given filter. If superbatching is disabled this will just
179      * return the empty list.
180      *
181      * Package private so it can be tested independently.
182      */
183     List<PluginResource> getSuperBatchResources(final WebResourceFilter filter)
184     {
185         if (!batchingConfiguration.isSuperBatchingEnabled())
186         {
187             return Collections.emptyList();
188         }
189 
190         final Iterable<WebResourceModuleDescriptor> superBatchModuleKeys = dependencyResolver.getSuperBatchDependencies();
191         final List<PluginResource> resources = new ArrayList<PluginResource>();
192 
193         // This is necessarily quite complicated. We need distinct superbatch resources for each combination of
194         // resourceFormatter (i.e. separate CSS or JS resources), and also each unique combination of
195         // BATCH_PARAMS (i.e. separate superbatches for print stylesheets, IE only stylesheets, and IE only print
196         // stylesheets if they ever exist in the future)
197         for (final WebResourceFormatter formatter : webResourceFormatters)
198         {
199             final Set<Map<String, String>> alreadyIncluded = new HashSet<Map<String, String>>();
200             for (final WebResourceModuleDescriptor moduleDescriptor : superBatchModuleKeys)
201             {
202                 for (final PluginResource pluginResource : pluginResourceLocator.getPluginResources(moduleDescriptor.getCompleteKey()))
203                 {
204                     if (formatter.matches(pluginResource.getResourceName()) && filter.matches(pluginResource.getResourceName()))
205                     {
206                         final Map<String, String> batchParamsMap = new HashMap<String, String>(PluginResourceLocator.BATCH_PARAMS.length);
207                         for (final String s : PluginResourceLocator.BATCH_PARAMS)
208                         {
209                             batchParamsMap.put(s, pluginResource.getParams().get(s));
210                         }
211 
212                         if (!alreadyIncluded.contains(batchParamsMap))
213                         {
214                             resources.add(SuperBatchPluginResource.createBatchFor(pluginResource));
215                             alreadyIncluded.add(batchParamsMap);
216                         }
217                     }
218                 }
219             }
220         }
221         return resources;
222     }
223 
224     /**
225      * Write the tags for the given set of resources to the writer. Writing will be done in order of
226      * webResourceFormatters so that all CSS resources will be output before Javascript.
227      */
228     private <W extends Writer> W writeResourceTags(final Iterable<PluginResource> resourcesToInclude, final W writer, final UrlMode urlMode)
229     {
230         for (final WebResourceFormatter formatter : webResourceFormatters)
231         {
232             for (final Iterator<PluginResource> iter = resourcesToInclude.iterator(); iter.hasNext();)
233             {
234                 final PluginResource resource = iter.next();
235                 if (formatter.matches(resource.getResourceName()))
236                 {
237                     writeResourceTag(urlMode, resource, formatter, writer);
238                     iter.remove();
239                 }
240             }
241         }
242 
243         for (final PluginResource resource : resourcesToInclude)
244         {
245             writeContentAndSwallowErrors(writer, "<!-- Error loading resource \"", resource.getModuleCompleteKey(),
246                 "\".  No resource formatter matches \"", resource.getResourceName(), "\" -->\n");
247         }
248         return writer;
249     }
250 
251     private void writeResourceTag(final UrlMode urlMode, final PluginResource resource, final WebResourceFormatter formatter, final Writer writer)
252     {
253         final String prefix;
254         if (resource.isCacheSupported())
255         {
256             prefix = webResourceUrlProvider.getStaticResourcePrefix(resource.getVersion(webResourceIntegration), urlMode);
257         }
258         else
259         {
260             prefix = webResourceUrlProvider.getBaseUrl(urlMode);
261         }
262         writeContentAndSwallowErrors(writer, formatter.formatResource(prefix + resource.getUrl(), resource.getParams()));
263     }
264 
265     public void requireResource(final String moduleCompleteKey, final Writer writer, final UrlMode urlMode)
266     {
267         final Iterable<String> allDependentModuleKeys = toModuleKeys(dependencyResolver.getDependencies(moduleCompleteKey, IGNORE_SUPERBATCHING));
268         final Iterable<String> empty = Collections.<String> emptyList();
269         final Iterable<PluginResource> resourcesToInclude = getModuleResources(allDependentModuleKeys, empty, DefaultWebResourceFilter.INSTANCE);
270         writeResourceTags(resourcesToInclude, writer, urlMode);
271     }
272 
273     public String getResourceTags(final String moduleCompleteKey, final UrlMode urlMode)
274     {
275         final StringWriter writer = new StringWriter();
276         requireResource(moduleCompleteKey, writer, urlMode);
277         return writer.toString();
278     }
279     
280     /**
281      * Get the HTML tags necessary to fulfil the requested contexts and excluding the resources from any 'existingContexts'.
282      * <p/>
283      * <strong>Note</strong> that this is only a partial solution towards providing the ability to dynamically add further
284      * resources to a previously rendered page. Partial because it does not allow you to avoid re-requesting any individually
285      * requested resources. So, this has a degree of usefulness in certain situations which is the reason the method is
286      * protected and therefore available in product specific extensions of the {@link WebResourceManager}. You just need to
287      * be clear that as a full solution to these cases is developed this method may not serve as part of it - hence...
288      * <p/>
289      * <center><strong><em>WARNING: This method is likely to be removed in the not too distant future - use with care</em></strong></center> 
290      * 
291      * @param contexts the contexts to retrieve resources for
292      * @param existingContexts the contexts that have been retrieved previously so who's resources are to be excluded
293      * @param filter the filter to apply
294      * @param urlMode how to write the URLs in the returned tags
295      * @return the HTML tags you would include in a page to load the resource contexts required.
296      */
297     protected String getResourceTagsForContexts(final Set<String> contexts, final Set<String> existingContexts, final WebResourceFilter filter, final UrlMode urlMode)
298     {
299         final ContextBatchBuilder builder = new ContextBatchBuilder(pluginResourceLocator, dependencyResolver, batchingConfiguration);
300         Iterable<PluginResource> pluginResources = builder.build(contexts, existingContexts, filter);
301 
302         // If no existingContexts then we will provide the superbatch (on the assumption that it hasn't been served yet).
303         if (CollectionUtils.isEmpty(existingContexts)) 
304         {
305             pluginResources = concat(getSuperBatchResources(filter), pluginResources);
306         }
307         
308         Set<String> includedResources = new HashSet<String>();
309         
310         for (final String skippedResource : builder.getSkippedResources())
311         {
312             Iterable<String> moduleKeys = toModuleKeys(dependencyResolver.getDependencies(skippedResource, batchingConfiguration.isSuperBatchingEnabled()));
313             addAll(includedResources, moduleKeys);
314         }
315 
316         final Iterable<PluginResource> moduleResources = getModuleResources(includedResources, builder.getAllIncludedResources(), filter);
317         return writeResourceTags(concat(pluginResources, moduleResources), new StringWriter(), urlMode).toString();
318     }
319 
320     public String getStaticResourcePrefix(final String resourceCounter, final UrlMode urlMode)
321     {
322         return webResourceUrlProvider.getStaticResourcePrefix(resourceCounter, urlMode);
323     }
324 
325     public String getStaticPluginResource(final String moduleCompleteKey, final String resourceName, final UrlMode urlMode)
326     {
327         return webResourceUrlProvider.getStaticPluginResourceUrl(moduleCompleteKey, resourceName, urlMode);
328     }
329 
330     /**
331      * @return "{base url}/s/{build num}/{system counter}/{plugin version}/_/download/resources/{plugin.key:module.key}/{resource.name}"
332      */
333     public String getStaticPluginResource(final ModuleDescriptor<?> moduleDescriptor, final String resourceName, final UrlMode urlMode)
334     {
335         return webResourceUrlProvider.getStaticPluginResourceUrl(moduleDescriptor, resourceName, urlMode);
336     }
337 
338     public <T> T executeInNewContext(final Supplier<T> nestedExecution)
339     {
340         final Map<String, Object> cache = webResourceIntegration.getRequestCache();
341         final Map<String, Object> storedState = copyOf(cache);
342 
343         // clear the cache, as the nestedExecution must be executed in an empty environment
344         cache.clear();
345         try
346         {
347             return nestedExecution.get();
348         }
349         finally
350         {
351             // restore state, regardless of what happened
352             // we have to clear first to handle the following case:
353             //    nestedExecution wrote to the cache with a key that isn't in storedState. In this case we don't want
354             cache.clear();
355             cache.putAll(storedState);
356         }
357     }
358 
359     //
360     // private helpers
361     //
362 
363     private Iterable<PluginResource> getModuleResources(final Iterable<String> webResourcePluginModuleKeys, final Iterable<String> batchedModules, final WebResourceFilter filter)
364     {
365         final List<PluginResource> includedResources = new LinkedList<PluginResource>(); // use linked list as it gets removed from when writing
366         for (final String moduleKey : webResourcePluginModuleKeys)
367         {
368             if (contains(batchedModules, moduleKey))
369             {
370                 // skip this resource if it is already in a batch
371                 continue;
372             }
373 
374             final List<PluginResource> moduleResources = pluginResourceLocator.getPluginResources(moduleKey);
375             for (final PluginResource moduleResource : moduleResources)
376             {
377                 if (filter.matches(moduleResource.getResourceName()))
378                 {
379                     includedResources.add(moduleResource);
380                 }
381             }
382         }
383         return includedResources;
384     }
385 
386     private void writeContentAndSwallowErrors(final Writer writer, final String... contents)
387     {
388         try
389         {
390             for (final String content : contents)
391             {
392                 writer.write(content);
393             }
394         }
395         catch (final IOException ignore)
396         {}
397     }
398 
399     public Set<String> getIncludedContexts()
400     {
401         return getIncludedContexts(true);
402     }
403     
404     /**
405      * 
406      * @param copy if true then a shallow copy of the contexts Set is returned rather than
407      * the set itself.
408      * @return the included contexts.
409      */
410     private Set<String> getIncludedContexts(boolean copy)
411     {
412         Set<String> cachedSet = getOrCreateFromRequestCache(REQUEST_CACHE_CONTEXT_KEY);
413         if (copy)
414         {
415             return new HashSet<String>(cachedSet);
416         } 
417         else
418         {
419             return cachedSet;
420         }
421     }    
422 
423     private Set<String> getIncludedResourceNames()
424     {
425         return getOrCreateFromRequestCache(REQUEST_CACHE_RESOURCE_KEY);
426     }
427 
428     private Set<String> getOrCreateFromRequestCache(final String key)
429     {
430         final Map<String, Object> cache = webResourceIntegration.getRequestCache();
431         @SuppressWarnings("unchecked")
432         Set<String> set = (Set<String>) cache.get(key);
433         if (set == null)
434         {
435             set = Sets.newLinkedHashSet();
436             cache.put(key, set);
437         }
438         return set;
439     }
440 
441     private void clear()
442     {
443         getIncludedResourceNames().clear();
444         getIncludedContexts(false).clear();
445     }
446 
447     private Iterable<String> toModuleKeys(final Iterable<WebResourceModuleDescriptor> descriptors)
448     {
449         return transform(descriptors, new TransformDescriptorToKey());
450     }
451 
452     //
453     // deprecated impl to be removed in 3.0, only here for backwards compatibility not to be used
454     //
455 
456     @Deprecated
457     public String getRequiredResources()
458     {
459         return getRequiredResources(UrlMode.AUTO);
460     }
461 
462     @Deprecated
463     public String getResourceTags(final String moduleCompleteKey)
464     {
465         return getResourceTags(moduleCompleteKey, UrlMode.AUTO);
466     }
467 
468     @Deprecated
469     public String getStaticPluginResource(final ModuleDescriptor<?> moduleDescriptor, final String resourceName)
470     {
471         return getStaticPluginResource(moduleDescriptor, resourceName, UrlMode.AUTO);
472     }
473 
474     @Deprecated
475     public String getStaticPluginResource(final String moduleCompleteKey, final String resourceName)
476     {
477         return getStaticPluginResource(moduleCompleteKey, resourceName, UrlMode.AUTO);
478     }
479 
480     @Deprecated
481     public String getStaticPluginResourcePrefix(final ModuleDescriptor<?> moduleDescriptor, final String resourceName)
482     {
483         return getStaticPluginResource(moduleDescriptor, resourceName, UrlMode.AUTO);
484     }
485 
486     @Deprecated
487     public String getStaticResourcePrefix()
488     {
489         return webResourceUrlProvider.getStaticResourcePrefix(UrlMode.AUTO);
490     }
491 
492     @Deprecated
493     public String getStaticResourcePrefix(final UrlMode urlMode)
494     {
495         return webResourceUrlProvider.getStaticResourcePrefix(urlMode);
496     }
497 
498     @Deprecated
499     public String getStaticResourcePrefix(final String resourceCounter)
500     {
501         return getStaticResourcePrefix(resourceCounter, UrlMode.AUTO);
502     }
503 
504     @Deprecated
505     public void includeResources(final Writer writer)
506     {
507         includeResources(writer, UrlMode.AUTO);
508     }
509 
510     @Deprecated
511     public void requireResource(final String moduleCompleteKey, final Writer writer)
512     {
513         requireResource(moduleCompleteKey, writer, UrlMode.AUTO);
514     }
515 
516     @Deprecated
517     public void setIncludeMode(final IncludeMode includeMode)
518     {
519         webResourceIntegration.getRequestCache().put("plugin.webresource.mode", includeMode);
520     }
521 }