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.Iterables;
15  import com.google.common.collect.Lists;
16  import com.google.common.collect.Sets;
17  
18  import java.io.IOException;
19  import java.io.StringWriter;
20  import java.io.Writer;
21  import java.util.ArrayList;
22  import java.util.Arrays;
23  import java.util.Collections;
24  import java.util.HashMap;
25  import java.util.HashSet;
26  import java.util.Iterator;
27  import java.util.LinkedHashSet;
28  import java.util.LinkedList;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.Set;
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().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(new ArrayList<String>(getIncludedContexts()), 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      * Create the HTML tags necessary for requesting the supplied contexts <strong>in addition</strong> to any
282      * contexts already requested of the WebResourceManager (using {@link #requireResourcesForContext(String)}).
283      * <p/>
284      * This is intended to be useful in the case of storing in a page render the requests that will need to
285      * be made if you want to dynamically add additional contexts after the initial render. This doesn't form
286      * a complete solution to dynamically adding resources to a page which is the reason this method is not
287      * part of the public API.
288      *  
289      * @param contexts the additional contexts to create tags for
290      * @return the HTML tags required to request the additional context resources
291      */
292     protected String getResourceTagsForAdditionalContexts(final List<String> contexts)
293     {
294         return getResourceTagsForAdditionalContexts(contexts, UrlMode.AUTO, DefaultWebResourceFilter.INSTANCE);
295     }
296     
297     protected String getResourceTagsForAdditionalContexts(final List<String> contexts, final UrlMode urlMode, final WebResourceFilter filter)
298     {
299         final ContextBatchBuilder builder = new ContextBatchBuilder(pluginResourceLocator, dependencyResolver, batchingConfiguration);
300         final Iterable<PluginResource> resourcesToInclude = builder.build(contexts, new ArrayList<String>(getIncludedContexts()), filter);
301         
302         final boolean batchingEnabled = batchingConfiguration.isSuperBatchingEnabled();
303         Set<String> moduleKeys = new HashSet<String>();
304 
305         for (final String skippedResource : builder.getSkippedResources())
306         {
307             Iterables.addAll(moduleKeys, toModuleKeys(dependencyResolver.getDependencies(skippedResource, batchingEnabled)));
308         }
309         
310         final Iterable<PluginResource> moduleResources = getModuleResources(moduleKeys, builder.getAllIncludedResources(), filter);
311         return writeResourceTags(concat(resourcesToInclude, moduleResources), new StringWriter(), urlMode).toString();        
312     }
313 
314     public String getStaticResourcePrefix(final String resourceCounter, final UrlMode urlMode)
315     {
316         return webResourceUrlProvider.getStaticResourcePrefix(resourceCounter, urlMode);
317     }
318 
319     public String getStaticPluginResource(final String moduleCompleteKey, final String resourceName, final UrlMode urlMode)
320     {
321         return webResourceUrlProvider.getStaticPluginResourceUrl(moduleCompleteKey, resourceName, urlMode);
322     }
323 
324     /**
325      * @return "{base url}/s/{build num}/{system counter}/{plugin version}/_/download/resources/{plugin.key:module.key}/{resource.name}"
326      */
327     public String getStaticPluginResource(final ModuleDescriptor<?> moduleDescriptor, final String resourceName, final UrlMode urlMode)
328     {
329         return webResourceUrlProvider.getStaticPluginResourceUrl(moduleDescriptor, resourceName, urlMode);
330     }
331 
332     public <T> T executeInNewContext(final Supplier<T> nestedExecution)
333     {
334         final Map<String, Object> cache = webResourceIntegration.getRequestCache();
335         final Map<String, Object> storedState = copyOf(cache);
336 
337         // clear the cache, as the nestedExecution must be executed in an empty environment
338         cache.clear();
339         try
340         {
341             return nestedExecution.get();
342         }
343         finally
344         {
345             // restore state, regardless of what happened
346             // we have to clear first to handle the following case:
347             //    nestedExecution wrote to the cache with a key that isn't in storedState. In this case we don't want
348             cache.clear();
349             cache.putAll(storedState);
350         }
351     }
352 
353     //
354     // private helpers
355     //
356 
357     private Iterable<PluginResource> getModuleResources(final Iterable<String> webResourcePluginModuleKeys, final Iterable<String> batchedModules, final WebResourceFilter filter)
358     {
359         final List<PluginResource> includedResources = new LinkedList<PluginResource>(); // use linked list as it gets removed from when writing
360         for (final String moduleKey : webResourcePluginModuleKeys)
361         {
362             if (contains(batchedModules, moduleKey))
363             {
364                 // skip this resource if it is already in a batch
365                 continue;
366             }
367 
368             final List<PluginResource> moduleResources = pluginResourceLocator.getPluginResources(moduleKey);
369             for (final PluginResource moduleResource : moduleResources)
370             {
371                 if (filter.matches(moduleResource.getResourceName()))
372                 {
373                     includedResources.add(moduleResource);
374                 }
375             }
376         }
377         return includedResources;
378     }
379 
380     private void writeContentAndSwallowErrors(final Writer writer, final String... contents)
381     {
382         try
383         {
384             for (final String content : contents)
385             {
386                 writer.write(content);
387             }
388         }
389         catch (final IOException ignore)
390         {}
391     }
392 
393     private LinkedHashSet<String> getIncludedContexts()
394     {
395         return getOrCreateFromRequestCache(REQUEST_CACHE_CONTEXT_KEY);
396     }
397 
398     private LinkedHashSet<String> getIncludedResourceNames()
399     {
400         return getOrCreateFromRequestCache(REQUEST_CACHE_RESOURCE_KEY);
401     }
402 
403     private LinkedHashSet<String> getOrCreateFromRequestCache(final String key)
404     {
405         final Map<String, Object> cache = webResourceIntegration.getRequestCache();
406         @SuppressWarnings("unchecked")
407         LinkedHashSet<String> set = (LinkedHashSet<String>) cache.get(key);
408         if (set == null)
409         {
410             set = Sets.newLinkedHashSet();
411             cache.put(key, set);
412         }
413         return set;
414     }
415 
416     private void clear()
417     {
418         getIncludedResourceNames().clear();
419         getIncludedContexts().clear();
420     }
421 
422     private Iterable<String> toModuleKeys(final Iterable<WebResourceModuleDescriptor> descriptors)
423     {
424         return transform(descriptors, new TransformDescriptorToKey());
425     }
426 
427     //
428     // deprecated impl to be removed in 3.0, only here for backwards compatibility not to be used
429     //
430 
431     @Deprecated
432     public String getRequiredResources()
433     {
434         return getRequiredResources(UrlMode.AUTO);
435     }
436 
437     @Deprecated
438     public String getResourceTags(final String moduleCompleteKey)
439     {
440         return getResourceTags(moduleCompleteKey, UrlMode.AUTO);
441     }
442 
443     @Deprecated
444     public String getStaticPluginResource(final ModuleDescriptor<?> moduleDescriptor, final String resourceName)
445     {
446         return getStaticPluginResource(moduleDescriptor, resourceName, UrlMode.AUTO);
447     }
448 
449     @Deprecated
450     public String getStaticPluginResource(final String moduleCompleteKey, final String resourceName)
451     {
452         return getStaticPluginResource(moduleCompleteKey, resourceName, UrlMode.AUTO);
453     }
454 
455     @Deprecated
456     public String getStaticPluginResourcePrefix(final ModuleDescriptor<?> moduleDescriptor, final String resourceName)
457     {
458         return getStaticPluginResource(moduleDescriptor, resourceName, UrlMode.AUTO);
459     }
460 
461     @Deprecated
462     public String getStaticResourcePrefix()
463     {
464         return webResourceUrlProvider.getStaticResourcePrefix(UrlMode.AUTO);
465     }
466 
467     @Deprecated
468     public String getStaticResourcePrefix(final UrlMode urlMode)
469     {
470         return webResourceUrlProvider.getStaticResourcePrefix(urlMode);
471     }
472 
473     @Deprecated
474     public String getStaticResourcePrefix(final String resourceCounter)
475     {
476         return getStaticResourcePrefix(resourceCounter, UrlMode.AUTO);
477     }
478 
479     @Deprecated
480     public void includeResources(final Writer writer)
481     {
482         includeResources(writer, UrlMode.AUTO);
483     }
484 
485     @Deprecated
486     public void requireResource(final String moduleCompleteKey, final Writer writer)
487     {
488         requireResource(moduleCompleteKey, writer, UrlMode.AUTO);
489     }
490 
491     @Deprecated
492     public void setIncludeMode(final IncludeMode includeMode)
493     {
494         webResourceIntegration.getRequestCache().put("plugin.webresource.mode", includeMode);
495     }
496 }