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