View Javadoc

1   package com.atlassian.plugin.webresource;
2   
3   import static com.google.common.collect.Iterables.concat;
4   import static com.google.common.collect.Iterables.transform;
5   
6   import org.apache.commons.collections.CollectionUtils;
7   import org.slf4j.Logger;
8   import org.slf4j.LoggerFactory;
9   
10  import com.google.common.base.Function;
11  import com.google.common.collect.Iterables;
12  
13  import java.util.ArrayList;
14  import java.util.Arrays;
15  import java.util.Collection;
16  import java.util.Collections;
17  import java.util.HashMap;
18  import java.util.HashSet;
19  import java.util.Iterator;
20  import java.util.LinkedHashSet;
21  import java.util.List;
22  import java.util.Map;
23  import java.util.Set;
24  
25  /**
26   * Performs a calculation on many referenced contexts, and produces an set of intermingled batched-contexts and residual
27   * (skipped) resources. Some of the input contexts may have been merged into cross-context batches.
28   * The batches are constructed in such a way that no batch is dependent on another.
29   * The output batches and resources may be intermingled so as to preserve the input order as much as possible.
30   *
31   * @since 2.9.0
32   */
33  class ContextBatchBuilder
34  {
35      private static final Logger log = LoggerFactory.getLogger(ContextBatchBuilder.class);
36  
37      private final PluginResourceLocator pluginResourceLocator;
38      private final ResourceDependencyResolver dependencyResolver;
39      private final ResourceBatchingConfiguration batchingConfiguration;
40  
41      private final List<String> allIncludedResources = new ArrayList<String>();
42      private final Set<String> skippedResources = new HashSet<String>();
43  
44      ContextBatchBuilder(final PluginResourceLocator pluginResourceLocator, final ResourceDependencyResolver dependencyResolver, ResourceBatchingConfiguration batchingConfiguration)
45      {
46          this.pluginResourceLocator = pluginResourceLocator;
47          this.dependencyResolver = dependencyResolver;
48          this.batchingConfiguration = batchingConfiguration;
49      }
50  
51      /**
52       * This method performs the same function as calling the newer
53       * {@link #build(List includedContexts, List excludedContexts)}
54       * however it will create merged contexts with a slightly different ordering in the produced URL which of course can
55       * lead to a different ordering of resources in the batch. This could cause problems, for instance with Javascripts which
56       * have dependencies on each other.
57       * <p/>
58       * The newer method has a more logical ordering of contexts, <br/>
59       * - e.g. the first one requested will always be the first in the URL. <br/>
60       * - The next context in the URL will always be the first in the list with an overlapping resource. <br/>
61       * Therefore you should prefer the newer {@link #build(List includedContexts, List excludedContexts, WebResourceFilter filter)}
62       * and this method will be removed eventually in a future release.
63       *
64       * @deprecated since 2.12.3. Use {@link #build(java.util.List, java.util.List)} instead.
65       */
66      Iterable<PluginResource> build(final List<String> includedContexts)
67      {
68          return build(includedContexts, DefaultWebResourceFilter.INSTANCE);
69      }
70  
71      /**
72       * This method performs the same function as calling the newer 
73       * {@link #build(List includedContexts, List excludedContexts, WebResourceFilter filter)}
74       * however it will create merged contexts with a slightly different ordering in the produced URL which of course can
75       * lead to a different ordering of resources in the batch. This could cause problems, for instance with Javascripts which 
76       * have dependencies on each other.
77       * <p/>
78       * The newer method has a more logical ordering of contexts, <br/>
79       * - e.g. the first one requested will always be the first in the URL. <br/>
80       * - The next context in the URL will always be the first in the list with an overlapping resource. <br/> 
81       * Therefore you should prefer the newer {@link #build(List includedContexts, List excludedContexts, WebResourceFilter filter)}
82       * and this method will be removed eventually in a future release.
83       * 
84       * @deprecated since 2.12. Use {@link #build(java.util.List, java.util.List, WebResourceFilter)} instead.
85       */
86      Iterable<PluginResource> build(final Iterable<String> includedContexts, final WebResourceFilter filter)
87      {
88          if (!batchingConfiguration.isContextBatchingEnabled())
89          {
90               return getUnbatchedResources(includedContexts, null, filter);
91          }
92  
93          final ContextBatchOperations contextBatchOperations = new ContextBatchOperations(pluginResourceLocator, filter);
94          
95          // There are three levels to consider here. In order:
96          // 1. Type (CSS/JS)
97          // 2. Parameters (ieOnly, media, etc)
98          // 3. Context
99          final List<ContextBatch> batches = new ArrayList<ContextBatch>();
100 
101         for (final String context : includedContexts)
102         {
103             final ContextBatch contextBatch = new ContextBatch(context, dependencyResolver.getDependenciesInContext(context, skippedResources));
104             final List<ContextBatch> mergeList = new ArrayList<ContextBatch>();
105             for (final WebResourceModuleDescriptor contextResource : contextBatch.getResources())
106             {
107                 // only go deeper if it is not already included
108                 if (!allIncludedResources.contains(contextResource.getCompleteKey()))
109                 {
110                     for (final PluginResource pluginResource : pluginResourceLocator.getPluginResources(contextResource.getCompleteKey()))
111                     {
112                         if (filter.matches(pluginResource.getResourceName()))
113                         {
114                             contextBatch.addResourceType(pluginResource);
115                         }
116                     }
117 
118                     allIncludedResources.add(contextResource.getCompleteKey());
119                 }
120                 else
121                 {
122                     // we have an overlapping context, find it.
123                     // IMPORTANT: Don't add the overlapping resource to the batch otherwise there'll be duplicates
124                     for (final ContextBatch batch : batches)
125                     {
126                         if (!mergeList.contains(batch) && batch.isResourceIncluded(contextResource.getCompleteKey()))
127                         {
128                             if (log.isDebugEnabled())
129                             {
130                                 log.debug("Context: {} shares a resource with {}: {}", new String[] { context, batch.getKey(), contextResource.getCompleteKey() });
131                             }
132 
133                             mergeList.add(batch);
134                         }
135                     }
136                 }
137             }
138 
139             // Merge all the flagged contexts
140             if (!mergeList.isEmpty())
141             {
142                 ContextBatch mergedBatch = mergeList.get(0);
143                 batches.remove(mergedBatch);
144 
145                 for (int i = 1; i < mergeList.size(); i++)
146                 {
147                     final ContextBatch mergingBatch = mergeList.get(i);
148                     mergedBatch = contextBatchOperations.merge(Arrays.asList(mergedBatch, mergingBatch));
149                     batches.remove(mergingBatch);
150                 }
151 
152                 mergedBatch = contextBatchOperations.merge(Arrays.asList(mergedBatch, contextBatch));
153                 batches.add(mergedBatch);
154             }
155             else
156             {
157                 // Otherwise just add a new one
158                 batches.add(contextBatch);
159             }
160         }
161 
162         // Build the batch resources
163         return concat(transform(batches, new Function<ContextBatch, Iterable<PluginResource>>()
164         {
165             public Iterable<PluginResource> apply(final ContextBatch batch)
166             {
167                 return batch.buildPluginResources();
168             }
169         }));
170     }
171         
172     Iterable<PluginResource> build(final List<String> includedContexts, final List<String> excludedContexts)
173     {
174         return build(includedContexts, excludedContexts, DefaultWebResourceFilter.INSTANCE);
175     }
176     
177     Iterable<PluginResource> build(final List<String> includedContexts, final List<String> excludedContexts, final WebResourceFilter filter)
178     {
179         if (batchingConfiguration.isContextBatchingEnabled())
180         {
181             return buildBatched(includedContexts, excludedContexts, filter);
182         }
183         else
184         {
185             return getUnbatchedResources(includedContexts, excludedContexts, filter);
186         }
187     }
188     
189     /**
190      * @param includedContexts the ordering of these contexts is important since their placement within the resultant URL determines the order that resources
191      * will be included in the batch.
192      * @param excludedContexts order of these contexts is not important, they do not affect the position of resources. Instead they cause resources not to
193      * be present.
194      * @param filter
195      * @return
196      * @since 2.12
197      */
198     private Iterable<PluginResource> buildBatched(final List<String> includedContexts, final List<String> excludedContexts, final WebResourceFilter filter)
199     {
200         Set<String> conditionalIncludedResources = new HashSet<String>();
201         WebResourceKeysToContextBatches includedBatches = WebResourceKeysToContextBatches.create(includedContexts, dependencyResolver, pluginResourceLocator, filter, conditionalIncludedResources);
202         WebResourceKeysToContextBatches excludedBatches = null;
203         if (excludedContexts != null && !Iterables.isEmpty(excludedContexts)) 
204         {
205             excludedBatches = WebResourceKeysToContextBatches.create(excludedContexts, dependencyResolver, pluginResourceLocator, filter, new HashSet<String>());
206         }
207         
208         skippedResources.addAll(includedBatches.getSkippedResources());
209         
210         // There are three levels to consider here. In order:
211         // 1. Type (CSS/JS)
212         // 2. Parameters (ieOnly, media, etc)
213         // 3. Context
214         final List<ContextBatch> batches = new ArrayList<ContextBatch>();
215 
216         // This working list will be reduced as each context is handled.
217         final List<ContextBatch> batchesToProcess = new ArrayList<ContextBatch>(includedBatches.getContextBatches());
218         
219         final ContextBatchOperations contextBatchOperations = new ContextBatchOperations(pluginResourceLocator, filter);
220         
221         while (!batchesToProcess.isEmpty())
222         {
223             ContextBatch contextBatch = batchesToProcess.remove(0);
224             Set<ContextBatch> alreadyProcessedBatches = new HashSet<ContextBatch>();
225             alreadyProcessedBatches.add(contextBatch);
226 
227             Iterator<WebResourceModuleDescriptor> resourceIterator = contextBatch.getResources().iterator();
228             while (resourceIterator.hasNext())
229             {
230                 WebResourceModuleDescriptor contextResource = resourceIterator.next();
231                 String resourceKey = contextResource.getCompleteKey();
232                 // check for an overlap with the other batches (take into account only the batches not yet processed).
233                 List<ContextBatch> additionalContexts = includedBatches.getAdditionalContextsForResourceKey(resourceKey, alreadyProcessedBatches);
234 
235                 if (CollectionUtils.isNotEmpty(additionalContexts))
236                 {
237                     if (log.isDebugEnabled())
238                     {
239                         for (ContextBatch additional : additionalContexts)
240                         {
241                             log.debug("Context: {} shares a resource with {}: {}", new String[] { contextBatch.getKey(), additional.getKey(), contextResource.getCompleteKey() });
242                         }
243                     }
244 
245                     List<ContextBatch> contextsToMerge = new ArrayList<ContextBatch>(1 + additionalContexts.size());
246                     contextsToMerge.add(contextBatch);
247                     contextsToMerge.addAll(additionalContexts);
248                     contextBatch = contextBatchOperations.merge(contextsToMerge);
249                 
250                     // remove the merged batches from those to be processed
251                     batchesToProcess.removeAll(additionalContexts);
252                     alreadyProcessedBatches.addAll(additionalContexts);
253 
254                     // As a new overlapping context is merged, restart the resource iterator so we can check for new resources 
255                     // that may have been added via the merge.
256                     resourceIterator = contextBatch.getResources().iterator();
257                 }
258             }
259             
260             // We separate the search for excluded batches since we want to perform the subtraction
261             // after all the merging has been done. If you do a subtraction then a merge you cannot
262             // ensure (with ContextBatch as it is currently implemented) that the merge will not
263             // bring back in previously excluded resources.
264             if (excludedBatches != null)
265             {                
266                 resourceIterator = contextBatch.getResources().iterator();
267                 while (resourceIterator.hasNext())
268                 {
269                     WebResourceModuleDescriptor contextResource = resourceIterator.next();
270                     String resourceKey = contextResource.getCompleteKey();
271                     
272                     List<ContextBatch> excludeContexts = excludedBatches.getContextsForResourceKey(resourceKey);
273                     if (!excludeContexts.isEmpty())
274                     {
275                         contextBatch = contextBatchOperations.subtract(contextBatch, excludeContexts);
276                     }
277                 }
278                 
279                 skippedResources.removeAll(excludedBatches.getSkippedResources());
280             }
281             
282             // check that we still have resources in this batch - if not, the batch is not required.
283             if (excludedBatches == null || Iterables.size(contextBatch.getResources()) != 0)
284             {
285                 Iterables.addAll(allIncludedResources, contextBatch.getResourceKeys());
286                 batches.add(contextBatch);                
287             }
288             else if (log.isDebugEnabled())
289             {
290                 log.debug("The context batch {} contains no resources so will be dropped.", contextBatch.getKey());
291             }                
292         }
293         
294         // Build the batch resources
295         return concat(transform(batches, new Function<ContextBatch, Iterable<PluginResource>>()
296         {
297             public Iterable<PluginResource> apply(final ContextBatch batch)
298             {
299                 return batch.buildPluginResources();
300             }
301         }));
302     }
303 
304     // If context batching is not enabled, then just add all the resources that would have been added in the context anyway.
305     private Iterable<PluginResource> getUnbatchedResources(final Iterable<String> includedContexts, final Iterable<String> excludedContexts, final WebResourceFilter filter)
306     {        
307         Set<String> excludedResourceKeys = new HashSet<String>();
308         Set<String> excludedSkippedResources = new HashSet<String>(); // the resources that an excluded batch will not contain
309         
310         if (excludedContexts != null && Iterables.size(excludedContexts) > 0)
311         {
312             for (final String context : excludedContexts)
313             {
314                 Iterable<WebResourceModuleDescriptor> contextResources = dependencyResolver.getDependenciesInContext(context, excludedSkippedResources);
315                 for (final WebResourceModuleDescriptor contextResource : contextResources)
316                 {
317                     excludedResourceKeys.add(contextResource.getCompleteKey());
318                 }
319             }            
320         }
321         
322         LinkedHashSet<PluginResource> includedResources = new LinkedHashSet<PluginResource>();
323         Set<String> includedSkippedResources = new HashSet<String>(); // the resources that an included batch will not contain
324         
325         for (final String context : includedContexts)
326         {
327             Iterable<WebResourceModuleDescriptor> contextResources = dependencyResolver.getDependenciesInContext(context, includedSkippedResources);
328 
329             for (final WebResourceModuleDescriptor contextResource : contextResources)
330             {
331                 String completeKey = contextResource.getCompleteKey();
332                 if (!excludedResourceKeys.contains(completeKey) && !allIncludedResources.contains(completeKey))
333                 {
334                     final List<PluginResource> moduleResources = pluginResourceLocator.getPluginResources(contextResource.getCompleteKey());
335                     for (final PluginResource moduleResource : moduleResources)
336                     {
337                         if (filter.matches(moduleResource.getResourceName()))
338                         {
339                             includedResources.add(moduleResource);
340                         }
341                     }
342 
343                     allIncludedResources.add(contextResource.getCompleteKey());
344                 }                
345             }
346         }
347         
348         includedSkippedResources.removeAll(excludedSkippedResources);
349         skippedResources.addAll(includedSkippedResources);
350 
351         return includedResources;
352     }
353 
354     Iterable<String> getAllIncludedResources()
355     {
356         return allIncludedResources;
357     }
358 
359     Iterable<String> getSkippedResources()
360     {
361         return skippedResources;
362     }
363     
364     
365     private static class WebResourceKeysToContextBatches
366     {
367         /**
368          * Create a Map of {@link WebResourceModuleDescriptor} key to the contexts that includes them. If a 
369          * {@link WebResourceModuleDescriptor} exists in multiple contexts then each context will be 
370          * referenced. The ContextBatches created at this point are pure - they do not take into account
371          * overlaps or exclusions, they simply contain all the resources for their context name.
372          * 
373          * @param contexts the contexts to create a mapping for
374          * @param dependencyResolver used to construct the identified contexts
375          * @param pluginResourceLocator used to find the individual resources for a WebResourceModuleDescriptor before filtering them.
376          * @param filter the filter selecting the resources to be included in the batch
377          * @param conditionalResources conditional resources cannot be included in a batch so will be added to this list instead.
378          * @return a WebResourceToContextsMap containing the required mapping.
379          */        
380         static WebResourceKeysToContextBatches create(final List<String> contexts, final ResourceDependencyResolver dependencyResolver, PluginResourceLocator pluginResourceLocator, final WebResourceFilter filter, Set<String> conditionalResources)
381         {
382             final Map<String, List<ContextBatch>> resourceKeyToContext = new HashMap<String,List<ContextBatch>>();
383             final List<ContextBatch> batches = new ArrayList<ContextBatch>();
384             final Set<String> skippedResources = new HashSet<String>();
385             
386             for (String context : contexts)
387             {
388                 Iterable<WebResourceModuleDescriptor> dependencies = dependencyResolver.getDependenciesInContext(context, skippedResources);
389                 
390                 ContextBatch batch = new ContextBatch(context, dependencies);
391                 for (WebResourceModuleDescriptor moduleDescriptor : dependencies)
392                 {
393                     String key = moduleDescriptor.getCompleteKey();
394                     boolean matchedPluginResource = false;
395 
396                     for (final PluginResource pluginResource : pluginResourceLocator.getPluginResources(moduleDescriptor.getCompleteKey()))
397                     {
398                         if (filter.matches(pluginResource.getResourceName()))
399                         {
400                             batch.addResourceType(pluginResource);
401                             matchedPluginResource = true;
402                         }
403                     }
404                     
405                     if (matchedPluginResource)
406                     {
407                         if (!resourceKeyToContext.containsKey(key))
408                         {
409                             resourceKeyToContext.put(key, new ArrayList<ContextBatch>());
410                         }
411                         
412                         resourceKeyToContext.get(key).add(batch);
413                         
414                         if (!batches.contains(batch))
415                         {
416                             batches.add(batch);
417                         }
418                     }
419                 }
420                 
421             }
422             
423             return new WebResourceKeysToContextBatches(resourceKeyToContext, batches, skippedResources);
424         }
425         
426         private final Map<String, List<ContextBatch>> resourceToContextBatches; 
427         private final List<ContextBatch> knownBatches;
428         private final Set<String> skippedResources;        
429         
430         private WebResourceKeysToContextBatches(Map<String, List<ContextBatch>> resourceKeyToContext, List<ContextBatch> allBatches, Set<String> skippedResources)
431         {
432             this.resourceToContextBatches = resourceKeyToContext;
433             this.knownBatches = allBatches;
434             this.skippedResources = skippedResources;
435         }
436         
437         /**
438          * @param key the resource key to find contexts for
439          * @return all contexts the specified resource is included in. An empty List is returned if none.
440          */
441         List<ContextBatch> getContextsForResourceKey(String key)
442         {
443             return getAdditionalContextsForResourceKey(key, null);
444         }        
445         
446         /**
447          * @param key the resource key to be mapped to contexts
448          * @param knownContexts the contexts we already know about (may be null)
449          * @return a List of any additional contexts that the identified resource key can be found in. If there
450          * are no additional contexts then an empty List is returned.
451          */
452         List<ContextBatch> getAdditionalContextsForResourceKey(String key, Collection<ContextBatch> knownContexts)
453         {
454             List<ContextBatch> allContexts = resourceToContextBatches.get(key);
455             if (CollectionUtils.isEmpty(allContexts))
456             {
457                 return Collections.emptyList();
458             }
459             
460             LinkedHashSet<ContextBatch> contexts = new LinkedHashSet<ContextBatch>(allContexts);
461             if (CollectionUtils.isNotEmpty(knownContexts))
462             {
463                 contexts.removeAll(knownContexts);
464             }
465 
466             return new ArrayList<ContextBatch>(contexts);
467         }
468         
469         /**
470          * @return all the ContextBatches referenced in this class.
471          */
472         List<ContextBatch> getContextBatches()
473         {
474             return new ArrayList<ContextBatch>(knownBatches);
475         }
476 
477         /**
478          * @return the conditional resources that could not be mapped
479          */
480         public Set<String> getSkippedResources()
481         {
482             return skippedResources;
483         }
484     }
485 }