View Javadoc

1   package com.atlassian.plugin.webresource;
2   
3   import static com.atlassian.plugin.servlet.AbstractFileServerServlet.PATH_SEPARATOR;
4   import static com.atlassian.plugin.servlet.AbstractFileServerServlet.SERVLET_PATH;
5   import static com.google.common.base.Suppliers.ofInstance;
6   import static com.google.common.collect.ImmutableList.copyOf;
7   import static com.google.common.collect.Iterables.any;
8   
9   import com.atlassian.plugin.Plugin;
10  import com.atlassian.plugin.servlet.DownloadException;
11  import com.atlassian.plugin.servlet.DownloadableResource;
12  
13  import org.slf4j.Logger;
14  import org.slf4j.LoggerFactory;
15  
16  import com.google.common.base.Predicate;
17  import com.google.common.collect.ImmutableMap;
18  import com.google.common.collect.Iterables;
19  
20  import java.io.IOException;
21  import java.io.OutputStream;
22  import java.io.UnsupportedEncodingException;
23  import java.net.URLEncoder;
24  import java.util.Collections;
25  import java.util.Map;
26  
27  import javax.servlet.http.HttpServletRequest;
28  import javax.servlet.http.HttpServletResponse;
29  
30  /**
31   * Represents a batch of plugin resources. <p/>
32   * <p/>
33   * It provides methods to parse and generate urls to locate a batch of plugin resources. <p/>
34   * <p/>
35   * Note BatchPluginResource is also a type of {@link DownloadableResource}. The underlying implementation simply
36   * keeps a list of {@link DownloadableResource} of which this batch represents and delegates method calls.
37   *
38   * @since 2.2
39   */
40  public class BatchPluginResource implements DownloadableResource, PluginResource, BatchResource
41  {
42      private static final Logger log = LoggerFactory.getLogger(BatchPluginResource.class);
43  
44      private static final ResourceContentAnnotator[] DEFAULT_ANNOTATORS = new ResourceContentAnnotator[] {
45          new NewlineResourceContentAnnotator()
46      };
47  
48      private static final ResourceContentAnnotator[] JS_WRAP_ANNOTATORS = new ResourceContentAnnotator[] {
49          new NewlineResourceContentAnnotator(), new TryCatchJsResourceContentAnnotator()
50      };
51  
52      /**
53       * The url prefix for a batch of plugin resources: "/download/batch/"
54       */
55      static final String URL_PREFIX = PATH_SEPARATOR + SERVLET_PATH + PATH_SEPARATOR + "batch";
56  
57      private final ResourceKey resource;
58      private final Map<String, String> params;
59      private final Iterable<DownloadableResource> resources;
60  
61      // not thread-safe but a safe race on setting as hash function is referentially transparent
62      private int hash = 0;
63  
64      /**
65       * A constructor that creates a default resource name for the batch in the format: moduleCompleteKey.type
66       * For example: test.plugin:resources.js
67       * <p/>
68       * Note that name of the batch does not identify what the batch includes and could have been static e.g. batch.js
69       *
70       * @param moduleCompleteKey - the key of the plugin module
71       * @param type - the type of resource (CSS/JS)
72       * @param params - the parameters of the resource (ieonly, media, etc)
73       */
74      public BatchPluginResource(final String moduleCompleteKey, final String type, final Map<String, String> params)
75      {
76          this(ResourceKey.Builder.lazy(moduleCompleteKey, ofInstance(type)), params, Collections.<DownloadableResource>emptyList());
77      }
78  
79      /**
80       * A constructor that creates a default resource name for the batch in the format: moduleCompleteKey.type
81       * For example: test.plugin:resources.js
82       * <p/>
83       * This constructor includes the resources that are contained in the batch, and so is primarily for use
84       * when serving the resource.
85       *
86       * @param moduleCompleteKey - the key of the plugin module
87       * @param type - the type of resource (CSS/JS)
88       * @param params - the parameters of the resource (ieonly, media, etc)
89       * @param resources - the resources included in the batch.
90       */
91      public BatchPluginResource(final String moduleCompleteKey, final String type, final Map<String, String> params,
92                                 final Iterable<DownloadableResource> resources)
93      {
94          this(ResourceKey.Builder.lazy(moduleCompleteKey, ofInstance(type)), params, resources);
95      }
96  
97      /**
98       * This constructor should only ever be used internally within this class. It does not ensure that the resourceName's
99       * file extension is the same as the given type. It is up to the calling code to ensure this.
100      *
101      * @param resource - the resource key
102      * @param params - the parameters of the resource (ieonly, media, etc)
103      * @param resources - the resources included in the batch.
104      */
105     BatchPluginResource(final ResourceKey resource, final Map<String, String> params,
106                         final Iterable<DownloadableResource> resources)
107     {
108         this.resource = resource;
109         this.params = ImmutableMap.copyOf(params);
110         this.resources = copyOf(resources);
111     }
112 
113     /**
114      * @return true if there are no resources included in this batch
115      */
116     public boolean isEmpty()
117     {
118         return Iterables.isEmpty(resources);
119     }
120 
121     public boolean isResourceModified(final HttpServletRequest request, final HttpServletResponse response)
122     {
123         return any(resources, new Predicate<DownloadableResource>()
124         {
125             public boolean apply(final DownloadableResource resource)
126             {
127                 return resource.isResourceModified(request, response);
128             }
129         });
130     }
131 
132     public void serveResource(final HttpServletRequest request, final HttpServletResponse response)
133         throws DownloadException
134     {
135         if (log.isDebugEnabled())
136         {
137             log.debug("Start to serve batch " + toString());
138         }
139 
140         ResourceContentAnnotator[] annotators = getAnnotators(request);
141         for (final DownloadableResource resource : resources)
142         {
143             try
144             {
145                 applyBeforeAnnotators(response.getOutputStream(), annotators);
146                 resource.serveResource(request, response);
147                 applyAfterAnnotators(response.getOutputStream(), annotators);
148             }
149             catch (IOException ex)
150             {
151                 throw new DownloadException(ex);
152             }
153         }
154     }
155 
156     public void streamResource(final OutputStream out) throws DownloadException
157     {
158         ResourceContentAnnotator[] annotators = getAnnotators(null);
159 
160         for (final DownloadableResource resource : resources)
161         {
162             try
163             {
164                 applyBeforeAnnotators(out, annotators);
165                 resource.streamResource(out);
166                 applyAfterAnnotators(out, annotators);
167             }
168             catch (IOException ex)
169             {
170                 throw new DownloadException(ex);
171             }
172         }
173     }
174 
175     public String getContentType()
176     {
177         final String contentType = params.get("content-type");
178         if (contentType != null)
179         {
180             return contentType;
181         }
182         return null;
183     }
184 
185     /**
186      * Returns a url string in the format: /download/batch/MODULE_COMPLETE_KEY/resourceName?PARAMS
187      * <p/>
188      * e.g. /download/batch/example.plugin:webresources/example.plugin:webresources.css?ie=true
189      * <p/>
190      * It is important for the url structure to be:
191      * 1. the same number of sectioned paths as the SinglePluginResource
192      * 2. include the module completey key in the path before the resource name
193      * This is due to css resources referencing other resources such as images in relative path forms.
194      */
195     public String getUrl()
196     {
197         final StringBuilder sb = new StringBuilder();
198         sb.append(URL_PREFIX).append(PATH_SEPARATOR).append(resource.key()).append(PATH_SEPARATOR).append(
199             resource.name());
200         addParamsToUrl(sb, params);
201         return sb.toString();
202     }
203 
204     protected void addParamsToUrl(final StringBuilder sb, final Map<String, String> params)
205     {
206         if (params.size() > 0)
207         {
208             sb.append("?");
209             int count = 0;
210 
211             for (final Map.Entry<String, String> entry : params.entrySet())
212             {
213                 try
214                 {
215                     sb.append(URLEncoder.encode(entry.getKey(), "UTF-8")).append("=").append(URLEncoder.encode(
216                         entry.getValue(), "UTF-8"));
217 
218                     if (++count < params.size())
219                     {
220                         sb.append("&");
221                     }
222                 }
223                 catch (final UnsupportedEncodingException e)
224                 {
225                     log.error("Could not encode parameter to url for [" + entry.getKey() + "] with value [" +
226                         entry.getValue() + "]", e);
227                 }
228             }
229         }
230     }
231 
232     public String getResourceName()
233     {
234         return resource.name();
235     }
236 
237     public Map<String, String> getParams()
238     {
239         return params;
240     }
241 
242     public String getVersion(final WebResourceIntegration integration)
243     {
244         final Plugin plugin = integration.getPluginAccessor().getEnabledPluginModule(
245             getModuleCompleteKey()).getPlugin();
246         return plugin.getPluginInformation().getVersion();
247     }
248 
249     public String getModuleCompleteKey()
250     {
251         return resource.key();
252     }
253 
254     public boolean isCacheSupported()
255     {
256         return !"false".equals(params.get("cache"));
257     }
258 
259     @Override
260     public String getType()
261     {
262         return resource.suffix();
263     }
264 
265     private void applyBeforeAnnotators(OutputStream str, ResourceContentAnnotator[] annotators) throws IOException
266     {
267         for (ResourceContentAnnotator annotator : annotators)
268         {
269             annotator.before(str);
270         }
271     }
272 
273     /**
274      * Apply the after annotators in reverse order.
275      *
276      * @param str
277      * @throws IOException
278      */
279     private void applyAfterAnnotators(OutputStream str, ResourceContentAnnotator[] annotators) throws IOException
280     {
281         for (int i = annotators.length - 1; i >= 0; i--)
282         {
283             annotators[i].after(str);
284         }
285     }
286 
287     @Override
288     public boolean equals(final Object o)
289     {
290         if (this == o)
291         {
292             return true;
293         }
294         if ((o == null) || (getClass() != o.getClass()))
295         {
296             return false;
297         }
298 
299         final BatchPluginResource that = (BatchPluginResource) o;
300 
301         if (params != null ? !params.equals(that.params) : that.params != null)
302         {
303             return false;
304         }
305         return resource.equals(that.resource);
306     }
307 
308     @Override
309     public int hashCode()
310     {
311         if (hash == 0)
312         {
313             int result;
314             result = resource.hashCode();
315             result = 31 * result + (params != null ? params.hashCode() : 0);
316             hash = result;
317         }
318         return hash;
319     }
320 
321     @Override
322     public String toString()
323     {
324         return "[moduleCompleteKey=" + resource.key() + ", type=" + resource.suffix() + ", params=" + params + "]";
325     }
326 
327     /**
328      * @param request may be null if not available
329      * @return the annotators to use when writing the requested Batch resource.
330      */
331     private ResourceContentAnnotator[] getAnnotators(HttpServletRequest request)
332     {
333         if (request == null || !"js".equals(this.getType()) || !"true".equals(request.getParameter("trycatchwrap")))
334         {
335             return DEFAULT_ANNOTATORS;
336         }
337         else
338         {
339             return JS_WRAP_ANNOTATORS;
340         }
341     }
342 }