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