View Javadoc

1   package com.atlassian.plugin.webresource;
2   
3   import static com.google.common.collect.ImmutableList.copyOf;
4   import static com.google.common.collect.Iterables.contains;
5   import static com.google.common.collect.Iterables.transform;
6   import static com.google.common.collect.Sets.newHashSet;
7   
8   import com.atlassian.plugin.ModuleDescriptor;
9   import com.atlassian.plugin.servlet.DownloadableResource;
10  import com.google.common.base.Function;
11  import com.google.common.collect.ImmutableList;
12  import com.google.common.collect.ImmutableSortedSet;
13  import com.google.common.collect.Iterables;
14  import com.google.common.collect.Ordering;
15  import org.apache.commons.codec.binary.Hex;
16  
17  import java.io.UnsupportedEncodingException;
18  import java.security.MessageDigest;
19  import java.security.NoSuchAlgorithmException;
20  import java.util.ArrayList;
21  import java.util.Collections;
22  import java.util.HashMap;
23  import java.util.HashSet;
24  import java.util.List;
25  import java.util.Map;
26  import java.util.Set;
27  
28  /**
29   * An intermediary object used for constructing and merging context batches.
30   * This is a bean that holds the different resources and parameters that apply
31   * to a particular batch.
32   * The batch can both include and exclude one or more contexts
33   * Resources are expected to be in dependency order, with no duplicates.
34   */
35  class ContextBatch
36  {
37      private static final String UTF8 = "UTF-8";
38      private static final String MD5 = "MD5";
39      private static final Ordering<ModuleDescriptor<?>> MODULE_KEY_ORDERING = Ordering.natural().onResultOf(new TransformDescriptorToKey());
40  
41      private final String key;
42      private final List<String> contexts;
43      private final Iterable<String> excludedContexts;
44      private final Iterable<WebResourceModuleDescriptor> resources;
45      private final Iterable<String> resourceKeys;
46      private final Set<PluginResourceBatchParams> resourceParams;
47  
48      ContextBatch(final String context, final Iterable<WebResourceModuleDescriptor> resources)
49      {
50          this(context, ImmutableList.of(context), resources, ImmutableList.<PluginResourceBatchParams> of());
51      }
52  
53      /**
54       * 
55       * @param key
56       * @param contexts the ordering of contexts is important since it determines the ordering of resources within a batch (which could be
57       * important for badly written Javascripts).
58       * @param resources
59       * @param resourceParams
60       */
61      ContextBatch(final String key, final List<String> contexts, final Iterable<WebResourceModuleDescriptor> resources, final Iterable<PluginResourceBatchParams> resourceParams)
62      {
63          this(key, contexts, null, resources, resourceParams);
64      }
65      
66      /**
67       * 
68       * @param key
69       * @param contexts the ordering of contexts is important since it determines the ordering of resources within a batch (which could be
70       * important for badly written Javascripts).
71       * @param excludedContexts the ordering of excluded contexts is not important.
72       * @param resources
73       * @param resourceParams
74       */
75      ContextBatch(final String key, final List<String> contexts, Iterable<String> excludedContexts, 
76              final Iterable<WebResourceModuleDescriptor> resources, final Iterable<PluginResourceBatchParams> resourceParams)
77      {
78          this.key = key;
79          this.contexts = copyOf(contexts);
80          if (excludedContexts == null)
81          {
82              this.excludedContexts = Collections.emptyList();
83          }
84          else
85          {
86              this.excludedContexts = copyOf(excludedContexts);
87          }
88          
89          this.resourceParams = newHashSet(resourceParams);
90  
91          // Note: Ordering is important in producing a consistent hash.
92          // But, dependency order is not important when producing the PluginResource,
93          // it is only important when we serve the resource. So it is safe to reorder this.
94          this.resources = ImmutableSortedSet.copyOf(MODULE_KEY_ORDERING, resources);
95          // A convenience object to make searching easier
96          this.resourceKeys =  transform(resources, new TransformDescriptorToKey());
97      }
98  
99      boolean isResourceIncluded(final String resourceModuleKey)
100     {
101         return contains(resourceKeys, resourceModuleKey);
102     }
103 
104     void addResourceType(final PluginResource pluginResource)
105     {
106         final Map<String, String> parameters = new HashMap<String, String>(PluginResourceLocator.BATCH_PARAMS.length);
107         final String type = pluginResource.getType();
108         for (final String key : PluginResourceLocator.BATCH_PARAMS)
109         {
110             if (pluginResource.getParams().get(key) != null)
111             {
112                 parameters.put(key, pluginResource.getParams().get(key));
113             }
114         }
115 
116         resourceParams.add(new PluginResourceBatchParams(type, parameters));
117     }
118 
119     /**
120      * Iterates over the batch parameters for the context ({@link PluginResourceBatchParams}) and
121      * creates a {@link ContextBatchPluginResource} for each. 
122      * <p/>
123      * It should be noted that the created {@link ContextBatchPluginResource} will not actually contain any 
124      * {@link DownloadableResource}. The {@link PluginResource} created is primarily representing a
125      * URL to be used in constructing a particular resource/download reference.
126      *  
127      * @return ContextBatchPluginResource instances, although containing no {@link DownloadableResource}s.
128      */
129     Iterable<PluginResource> buildPluginResources()
130     {
131         final String hash = createHash();
132         
133         // add the BatchedWebResourceDescriptors for this ContextBatch to each of the generated ContextBatchPluginResource
134         // so that we can return ContextBatches to the client with embedded information about the dependencies contained.
135         Set<BatchedWebResourceDescriptor> descriptors = new HashSet<BatchedWebResourceDescriptor>();
136         for (WebResourceModuleDescriptor wrmd : this.getResources())
137         {
138             descriptors.add(new BatchedWebResourceDescriptor(wrmd.getPlugin().getPluginInformation().getVersion(), wrmd.getCompleteKey()));            
139         }
140         
141         List<PluginResource> resources = new ArrayList<PluginResource>(resourceParams.size());
142         for (PluginResourceBatchParams param :resourceParams)
143         {
144             ContextBatchPluginResource contextBatchPluginResource = new ContextBatchPluginResource(key, contexts, excludedContexts, hash, param.getType(), param.getParameters(), descriptors);
145             resources.add(contextBatchPluginResource);
146         }
147         
148         return resources;
149     }
150 
151     private String createHash()
152     {
153         try
154         {
155             MessageDigest md5 = MessageDigest.getInstance(MD5);
156             for (WebResourceModuleDescriptor moduleDescriptor : resources)
157             {
158                 String version = moduleDescriptor.getPlugin().getPluginInformation().getVersion();
159                 md5.update(moduleDescriptor.getCompleteKey().getBytes(UTF8));
160                 md5.update(version.getBytes(UTF8));
161             }
162 
163             return new String(Hex.encodeHex(md5.digest()));
164         }
165         catch (NoSuchAlgorithmException e)
166         {
167             throw new AssertionError("MD5 hashing algorithm is not available.");
168         }
169         catch (UnsupportedEncodingException e)
170         {
171             throw new AssertionError("UTF-8 encoding is not available.");
172         }
173     }
174 
175     String getKey()
176     {
177         return key;
178     }
179 
180     List<String> getContexts()
181     {
182         return contexts;
183     }
184     
185     public Iterable<String> getExcludedContexts()
186     {
187         return excludedContexts;
188     }
189 
190     Iterable<WebResourceModuleDescriptor> getResources()
191     {
192         return resources;
193     }
194     
195     Iterable<String> getResourceKeys()
196     {
197         return transform(resources, new Function<WebResourceModuleDescriptor,String>() 
198         {
199             public String apply(WebResourceModuleDescriptor input)
200             {
201                 return input.getCompleteKey();
202             }
203         });
204     }
205 
206     Iterable<PluginResourceBatchParams> getResourceParams()
207     {
208         return Collections.unmodifiableSet(resourceParams);
209     }
210 
211     @Override
212     public boolean equals(Object obj)
213     {
214         if (obj == null)
215             return false;
216         
217         if (!(obj instanceof ContextBatch))
218             return false;
219         
220         if (obj == this)
221             return true;
222         
223         ContextBatch other = (ContextBatch)obj;
224         
225         return this.key.equals(other.key);
226     }
227 
228     @Override
229     public int hashCode()
230     {
231         return 17 * key.hashCode();
232     }
233 
234     @Override
235     public String toString()
236     {
237         StringBuilder builder = new StringBuilder("ContextBatch[key=");
238         builder.append(getKey());
239         
240         if (contexts != null && Iterables.size(contexts) > 0)
241         {
242             builder.append(", includedContexts=").append(Iterables.toString(contexts));
243         }
244         
245         if (excludedContexts != null && Iterables.size(excludedContexts) > 0)
246         {
247             builder.append(", excludedContexts=").append(Iterables.toString(excludedContexts));
248         }
249         
250         if (resourceKeys != null && Iterables.size(resourceKeys) > 0)
251         {
252             builder.append(", resourceKeys=").append(Iterables.toString(resourceKeys));
253         }
254         
255         builder.append("]");
256         
257         return builder.toString();
258     }
259 }