View Javadoc

1   package com.atlassian.plugin.webresource;
2   
3   import com.atlassian.plugin.ModuleDescriptor;
4   
5   import java.io.IOException;
6   import java.io.StringWriter;
7   import java.io.Writer;
8   import java.util.*;
9   
10  import org.slf4j.Logger;
11  import org.slf4j.LoggerFactory;
12  
13  /**
14   * A handy super-class that handles most of the resource management.
15   * <p/>
16   * To use this manager, you need to have the following UrlRewriteFilter code:
17   * <pre>
18   * &lt;rule>
19   * &lt;from>^/s/(.*)/_/(.*)&lt;/from>
20   * &lt;run class="com.atlassian.plugin.servlet.ResourceDownloadUtils" method="addCachingHeaders" />
21   * &lt;to type="forward">/$2&lt;/to>
22   * &lt;/rule>
23   * </pre>
24   * <p/>
25   * Sub-classes should implement the abstract methods
26   */
27  public class WebResourceManagerImpl implements WebResourceManager
28  {
29      private static final Logger log = LoggerFactory.getLogger(WebResourceManagerImpl.class);
30  
31      static final String STATIC_RESOURCE_PREFIX = "s";
32      static final String STATIC_RESOURCE_SUFFIX = "_";
33  
34      static final String REQUEST_CACHE_RESOURCE_KEY = "plugin.webresource.names";
35  
36      protected final WebResourceIntegration webResourceIntegration;
37      protected final PluginResourceLocator pluginResourceLocator;
38      protected final ResourceBatchingConfiguration batchingConfiguration;
39      protected final ResourceDependencyResolver dependencyResolver;
40      protected static final List<WebResourceFormatter> webResourceFormatters = Arrays.asList(CssWebResource.FORMATTER, JavascriptWebResource.FORMATTER);
41  
42      private static final boolean IGNORE_SUPERBATCHING = false;
43  
44      public WebResourceManagerImpl(PluginResourceLocator pluginResourceLocator, WebResourceIntegration webResourceIntegration)
45      {
46          this(pluginResourceLocator,  webResourceIntegration, new DefaultResourceBatchingConfiguration());
47      }
48  
49      public WebResourceManagerImpl(PluginResourceLocator pluginResourceLocator, WebResourceIntegration webResourceIntegration, ResourceBatchingConfiguration batchingConfiguration)
50      {
51          this(pluginResourceLocator, webResourceIntegration, batchingConfiguration, new DefaultResourceDependencyResolver(webResourceIntegration, batchingConfiguration));
52      }
53  
54      public WebResourceManagerImpl(PluginResourceLocator pluginResourceLocator, WebResourceIntegration webResourceIntegration,
55          ResourceBatchingConfiguration batchingConfiguration, ResourceDependencyResolver dependencyResolver)
56      {
57          this.pluginResourceLocator = pluginResourceLocator;
58          this.webResourceIntegration = webResourceIntegration;
59          this.batchingConfiguration = batchingConfiguration;
60          this.dependencyResolver = dependencyResolver;
61      }
62  
63      public void requireResource(String moduleCompleteKey)
64      {
65          log.debug("Requiring resource: " + moduleCompleteKey);
66          getIncludedResourceNames().addAll(dependencyResolver.getDependencies(moduleCompleteKey, batchingConfiguration.isSuperBatchingEnabled()));
67      }
68  
69      public void requireResourcesForContext(String context)
70      {
71          final List<WebResourceModuleDescriptor> webResourceModuleDescriptors =
72                  webResourceIntegration.getPluginAccessor().getEnabledModuleDescriptorsByClass(WebResourceModuleDescriptor.class);
73  
74          for (WebResourceModuleDescriptor webResourceModuleDescriptor : webResourceModuleDescriptors)
75          {
76              if (webResourceModuleDescriptor.getContexts().contains(context))
77              {
78                  requireResource(webResourceModuleDescriptor.getCompleteKey());
79              }
80          }
81  	}
82  
83      private LinkedHashSet<String> getIncludedResourceNames()
84      {
85          final Map<String, Object> cache = webResourceIntegration.getRequestCache();
86          @SuppressWarnings("unchecked")
87          LinkedHashSet<String> webResourceNames = (LinkedHashSet<String>) cache.get(REQUEST_CACHE_RESOURCE_KEY);
88          if (webResourceNames == null)
89          {
90              webResourceNames = new LinkedHashSet<String>();
91              cache.put(REQUEST_CACHE_RESOURCE_KEY, webResourceNames);
92          }
93          return webResourceNames;
94      }
95  
96      private void clearIncludedResourceNames()
97      {
98          log.debug("Clearing included resources");
99          getIncludedResourceNames().clear();
100     }
101 
102     /**
103      * This is the equivalent of of calling {@link #includeResources(Writer, UrlMode, WebResourceFilter)} with
104      * {@link UrlMode#AUTO} and a {@link DefaultWebResourceFilter}.
105      *
106      * @see #includeResources(Writer, UrlMode, WebResourceFilter)
107      */
108     public void includeResources(Writer writer)
109     {
110         includeResources(writer, UrlMode.AUTO);
111     }
112 
113     public void includeResources(Iterable<String> moduleCompleteKeys, Writer writer, UrlMode urlMode)
114     {
115         LinkedHashSet<String> resources = new LinkedHashSet<String>();
116         for (String moduleCompleteKey : moduleCompleteKeys)
117         {
118             // Include resources from the super batch as we don't include the super batch itself
119             Set<String> dependencies = dependencyResolver.getDependencies(moduleCompleteKey, false);
120             resources.addAll(dependencies);
121         }
122         writeResourceTags(getModuleResources(resources, DefaultWebResourceFilter.INSTANCE), writer, urlMode);
123     }
124 
125     /**
126      * This is the equivalent of of calling {@link #includeResources(Writer, UrlMode, WebResourceFilter)} with
127      * the given url mode and a {@link DefaultWebResourceFilter}.
128      *
129      * @see #includeResources(Writer, UrlMode, WebResourceFilter)
130      */
131     public void includeResources(Writer writer, UrlMode urlMode)
132     {
133         includeResources(writer, urlMode, DefaultWebResourceFilter.INSTANCE);
134     }
135 
136     /**
137      * Writes out the resource tags to the previously required resources called via requireResource methods for the
138      * specified url mode and resource filter. Note that this method will clear the list of previously required resources.
139      *
140      * @param writer the writer to write the links to
141      * @param urlMode the url mode to write resource url links in
142      * @param webResourceFilter the resource filter to filter resources on
143      * @since 2.4
144      */
145     public void includeResources(Writer writer, UrlMode urlMode, WebResourceFilter webResourceFilter)
146     {
147         writeIncludedResources(writer, urlMode, webResourceFilter);
148         clearIncludedResourceNames();
149     }
150 
151     /**
152      * This is the equivalent of calling {@link #getRequiredResources(UrlMode, WebResourceFilter)} with
153      * {@link UrlMode#AUTO} and a {@link DefaultWebResourceFilter}.
154      *
155      * @see #getRequiredResources(UrlMode, WebResourceFilter)
156      */
157     public String getRequiredResources()
158     {
159         return getRequiredResources(UrlMode.AUTO);
160     }
161 
162     /**
163      * This is the equivalent of calling {@link #getRequiredResources(UrlMode, WebResourceFilter)} with the given url
164      * mode and a {@link DefaultWebResourceFilter}.
165      *
166      * @see #getRequiredResources(UrlMode, WebResourceFilter)
167      */
168     public String getRequiredResources(UrlMode urlMode)
169     {
170         return getRequiredResources(urlMode, DefaultWebResourceFilter.INSTANCE);
171     }
172 
173     /**
174      * Returns a String of the resources tags to the previously required resources called via requireResource methods
175      * for the specified url mode and resource filter. Note that this method will NOT clear the list of previously
176      * required resources.
177      *
178      * @param urlMode the url mode to write out the resource tags
179      * @param webResourceFilter the web resource filter to filter resources on
180      * @return a String of the resource tags
181      * @since 2.4
182      */
183     public String getRequiredResources(UrlMode urlMode, WebResourceFilter webResourceFilter)
184     {
185         final StringWriter writer = new StringWriter();
186         writeIncludedResources(writer, urlMode, webResourceFilter);
187         return writer.toString();
188     }
189 
190     /**
191      * Write all currently included resources to the given writer.
192      */
193     private void writeIncludedResources(Writer writer, UrlMode urlMode, WebResourceFilter filter)
194     {
195         List<PluginResource> resourcesToInclude = new ArrayList<PluginResource>();
196         resourcesToInclude.addAll(getSuperBatchResources(filter));
197         resourcesToInclude.addAll(getModuleResources(getIncludedResourceNames(), filter));
198 
199         writeResourceTags(resourcesToInclude, writer, urlMode);
200     }
201 
202     /**
203      * Get all super-batch resources that match the given filter. If superbatching is disabled this will just
204      * return the empty list.
205      *
206      * Package private so it can be tested independently.
207      */
208     List<PluginResource> getSuperBatchResources(WebResourceFilter filter)
209     {
210         if (!batchingConfiguration.isSuperBatchingEnabled())
211             return Collections.emptyList();
212 
213         LinkedHashSet<String> superBatchModuleKeys = dependencyResolver.getSuperBatchDependencies();
214         List<PluginResource> resources = new ArrayList<PluginResource>();
215 
216         // This is necessarily quite complicated. We need distinct superbatch resources for each combination of
217         // resourceFormatter (i.e. separate CSS or JS resources), and also each unique combination of
218         // BATCH_PARAMS (i.e. separate superbatches for print stylesheets, IE only stylesheets, and IE only print
219         // stylesheets if they ever exist in the future)
220         for (WebResourceFormatter formatter : webResourceFormatters)
221         {
222             Set<Map<String, String>> alreadyIncluded = new HashSet<Map<String, String>>();
223             for (String moduleKey : superBatchModuleKeys)
224             {
225                 for (PluginResource pluginResource : pluginResourceLocator.getPluginResources(moduleKey))
226                 {
227                     if (formatter.matches(pluginResource.getResourceName()) && filter.matches(pluginResource.getResourceName()))
228                     {
229                         Map<String, String> batchParamsMap = new HashMap<String, String>(PluginResourceLocator.BATCH_PARAMS.length);
230                         for (String s : PluginResourceLocator.BATCH_PARAMS)
231                         {
232                             batchParamsMap.put(s, pluginResource.getParams().get(s));
233                         }
234 
235                         if (!alreadyIncluded.contains(batchParamsMap))
236                         {
237                             resources.add(SuperBatchPluginResource.createBatchFor(pluginResource));
238                             alreadyIncluded.add(batchParamsMap);
239                         }
240                     }
241                 }
242             }
243         }
244         return resources;
245     }
246 
247     private List<PluginResource> getModuleResources(LinkedHashSet<String> webResourcePluginModuleKeys, WebResourceFilter filter)
248     {
249         List<PluginResource> includedResources = new ArrayList<PluginResource>();
250         for (String moduleKey: webResourcePluginModuleKeys)
251         {
252             List<PluginResource> moduleResources = pluginResourceLocator.getPluginResources(moduleKey);
253             for (PluginResource moduleResource : moduleResources)
254             {
255                 if (filter.matches(moduleResource.getResourceName()))
256                     includedResources.add(moduleResource);
257                 else
258                     log.debug("Resource [" + moduleResource.getResourceName() + "] excluded by filter");
259             }
260         }
261         return includedResources;
262     }
263 
264     /**
265      * Write the tags for the given set of resources to the writer. Writing will be done in order of
266      * webResourceFormatters so that all CSS resources will be output before Javascript.
267      */
268     private void writeResourceTags(List<PluginResource> resourcesToInclude, Writer writer, UrlMode urlMode)
269     {
270         for (WebResourceFormatter formatter : webResourceFormatters)
271         {
272             for (Iterator<PluginResource> iter = resourcesToInclude.iterator(); iter.hasNext();)
273             {
274                 PluginResource resource = iter.next();
275                 if (formatter.matches(resource.getResourceName()))
276                 {
277                     writeResourceTag(urlMode, resource, formatter, writer);
278                     iter.remove();
279                 }
280             }
281         }
282 
283         for (PluginResource resource : resourcesToInclude)
284         {
285             writeContentAndSwallowErrors("<!-- Error loading resource \"" + resource.getModuleCompleteKey() + "\".  No resource formatter matches \"" + resource.getResourceName() + "\" -->\n",
286                 writer);
287         }
288     }
289 
290     private void writeResourceTag(UrlMode urlMode, PluginResource resource, WebResourceFormatter formatter, Writer writer)
291     {
292         String url = resource.getUrl();
293         if (resource.isCacheSupported())
294         {
295             url = getStaticResourcePrefix(resource.getVersion(webResourceIntegration), urlMode) + url;
296         }
297         else
298         {
299             url = webResourceIntegration.getBaseUrl(urlMode) + url;
300         }
301         writeContentAndSwallowErrors(formatter.formatResource(url, resource.getParams()), writer);
302     }
303     
304     public void requireResource(String moduleCompleteKey, Writer writer)
305     {
306         requireResource(moduleCompleteKey, writer, UrlMode.AUTO);
307     }
308 
309     public void requireResource(String moduleCompleteKey, Writer writer, UrlMode urlMode)
310     {
311         LinkedHashSet<String> allDependentModuleKeys = dependencyResolver.getDependencies(moduleCompleteKey, IGNORE_SUPERBATCHING);
312         List<PluginResource> resourcesToInclude = getModuleResources(allDependentModuleKeys, DefaultWebResourceFilter.INSTANCE);
313         writeResourceTags(resourcesToInclude, writer, urlMode);
314     }
315 
316     public String getResourceTags(String moduleCompleteKey)
317     {
318         return getResourceTags(moduleCompleteKey, UrlMode.AUTO);
319     }
320 
321     public String getResourceTags(String moduleCompleteKey, UrlMode urlMode)
322     {
323         final StringWriter writer = new StringWriter();
324         requireResource(moduleCompleteKey, writer, urlMode);
325         return writer.toString();
326     }
327 
328     private void writeContentAndSwallowErrors(String content, Writer writer)
329     {
330         try
331         {
332             writer.write(content);
333         }
334         catch (final IOException e)
335         {
336             log.debug("Ignoring", e);
337         }
338     }
339 
340     public String getStaticResourcePrefix()
341     {
342         return getStaticResourcePrefix(UrlMode.AUTO);
343     }
344 
345     public String getStaticResourcePrefix(UrlMode urlMode)
346     {
347         // "{base url}/s/{build num}/{system counter}/_"
348         return webResourceIntegration.getBaseUrl(urlMode) + "/" + STATIC_RESOURCE_PREFIX + "/" + webResourceIntegration.getSystemBuildNumber() + "/" + webResourceIntegration.getSystemCounter() + "/" + STATIC_RESOURCE_SUFFIX;
349     }
350 
351     public String getStaticResourcePrefix(String resourceCounter)
352     {
353         return getStaticResourcePrefix(resourceCounter, UrlMode.AUTO);
354     }
355 
356     public String getStaticResourcePrefix(String resourceCounter, UrlMode urlMode)
357     {
358         // "{base url}/s/{build num}/{system counter}/{resource counter}/_"
359         return webResourceIntegration.getBaseUrl(urlMode) + "/" + STATIC_RESOURCE_PREFIX + "/" + webResourceIntegration.getSystemBuildNumber() + "/" + webResourceIntegration.getSystemCounter() + "/" + resourceCounter + "/" + STATIC_RESOURCE_SUFFIX;
360     }
361 
362     public String getStaticPluginResource(final String moduleCompleteKey, final String resourceName)
363     {
364         return getStaticPluginResource(moduleCompleteKey, resourceName, UrlMode.AUTO);
365     }
366 
367     public String getStaticPluginResource(final String moduleCompleteKey, final String resourceName, final UrlMode urlMode)
368     {
369         final ModuleDescriptor<?> moduleDescriptor = webResourceIntegration.getPluginAccessor().getEnabledPluginModule(moduleCompleteKey);
370         if (moduleDescriptor == null)
371         {
372             return null;
373         }
374 
375         return getStaticPluginResource(moduleDescriptor, resourceName, urlMode);
376     }
377 
378     /**
379      * @return "{base url}/s/{build num}/{system counter}/{plugin version}/_/download/resources/{plugin.key:module.key}/{resource.name}"
380      */
381     @SuppressWarnings("unchecked")
382     public String getStaticPluginResource(ModuleDescriptor moduleDescriptor, String resourceName)
383     {
384         return getStaticPluginResource(moduleDescriptor, resourceName, UrlMode.AUTO);
385     }
386 
387     public String getStaticPluginResource(ModuleDescriptor moduleDescriptor, String resourceName, UrlMode urlMode)
388     {
389         // "{base url}/s/{build num}/{system counter}/{plugin version}/_"
390         final String staticUrlPrefix = getStaticResourcePrefix(String.valueOf(moduleDescriptor.getPlugin().getPluginsVersion()), urlMode);
391         // "/download/resources/plugin.key:module.key/resource.name"
392         return staticUrlPrefix + pluginResourceLocator.getResourceUrl(moduleDescriptor.getCompleteKey(), resourceName);
393     }
394 
395     /* Deprecated methods */
396 
397     /**
398      * @deprecated Use {@link #getStaticPluginResource(com.atlassian.plugin.ModuleDescriptor, String)} instead
399      */
400     @Deprecated
401     @SuppressWarnings("unchecked")
402     public String getStaticPluginResourcePrefix(ModuleDescriptor moduleDescriptor, String resourceName)
403     {
404         return getStaticPluginResource(moduleDescriptor, resourceName);
405     }
406 
407     /**
408      * @deprecated Since 2.2
409      */
410     @Deprecated
411     private static final String REQUEST_CACHE_MODE_KEY = "plugin.webresource.mode";
412 
413     /**
414      * @deprecated Since 2.2
415      */
416     @Deprecated
417     private static final IncludeMode DEFAULT_INCLUDE_MODE = WebResourceManager.DELAYED_INCLUDE_MODE;
418 
419     /**
420      * @deprecated Since 2.2.
421      */
422     @Deprecated
423     public void setIncludeMode(final IncludeMode includeMode)
424     {
425         webResourceIntegration.getRequestCache().put(REQUEST_CACHE_MODE_KEY, includeMode);
426     }
427 }