View Javadoc

1   package com.atlassian.plugin.servlet;
2   
3   import java.io.IOException;
4   import java.io.InputStream;
5   import java.io.OutputStream;
6   import java.util.Date;
7   
8   import javax.servlet.http.HttpServletRequest;
9   import javax.servlet.http.HttpServletResponse;
10  
11  import com.atlassian.plugin.Plugin;
12  import com.atlassian.plugin.elements.ResourceLocation;
13  import com.atlassian.plugin.servlet.util.LastModifiedHandler;
14  import com.atlassian.plugin.util.PluginUtils;
15  
16  import org.apache.commons.io.IOUtils;
17  import org.apache.commons.lang.StringUtils;
18  import org.slf4j.Logger;
19  import org.slf4j.LoggerFactory;
20  
21  /**
22   * This base class is used to provide the ability to server minified versions of
23   * files if required and available.
24   *
25   * @since 2.2
26   */
27  abstract class AbstractDownloadableResource implements DownloadableResource
28  {
29      private static final Logger log = LoggerFactory.getLogger(AbstractDownloadableResource.class);
30  
31      /**
32       * This is a the system environment variable to set to disable the
33       * minification naming strategy used to find web resources.
34       */
35      private static final String ATLASSIAN_WEBRESOURCE_DISABLE_MINIFICATION = "atlassian.webresource.disable.minification";
36  
37      /* the following protected fields are marked final since 2.5 */
38  
39      protected final Plugin plugin;
40      protected final String extraPath;
41      protected final ResourceLocation resourceLocation;
42  
43      // PLUG-538 cache this so we don't recreate the string every time it is
44      // called
45      private final String location;
46      private final boolean disableMinification;
47  
48      public AbstractDownloadableResource(final Plugin plugin, final ResourceLocation resourceLocation, final String extraPath)
49      {
50          this(plugin, resourceLocation, extraPath, false);
51      }
52  
53      public AbstractDownloadableResource(final Plugin plugin, final ResourceLocation resourceLocation, String extraPath, final boolean disableMinification)
54      {
55          if ((extraPath != null) && !"".equals(extraPath.trim()) && !resourceLocation.getLocation().endsWith("/"))
56          {
57              extraPath = "/" + extraPath;
58          }
59          this.disableMinification = disableMinification;
60          this.plugin = plugin;
61          this.extraPath = extraPath;
62          this.resourceLocation = resourceLocation;
63          this.location = resourceLocation.getLocation() + extraPath;
64      }
65  
66      public void serveResource(final HttpServletRequest request, final HttpServletResponse response) throws DownloadException
67      {
68          log.debug("Serving: {}", this);
69  
70          final InputStream resourceStream = getResourceAsStreamViaMinificationStrategy();
71          if (resourceStream == null)
72          {
73              log.warn("Resource not found: {}", this);
74              return;
75          }
76  
77          final String contentType = getContentType();
78          if (StringUtils.isNotBlank(contentType))
79          {
80              response.setContentType(contentType);
81          }
82  
83          final OutputStream out;
84          try
85          {
86              out = response.getOutputStream();
87          }
88          catch (final IOException e)
89          {
90              throw new DownloadException(e);
91          }
92  
93          streamResource(resourceStream, out);
94          log.debug("Serving file done.");
95      }
96  
97      public void streamResource(final OutputStream out) throws DownloadException
98      {
99          final InputStream resourceStream = getResourceAsStreamViaMinificationStrategy();
100         if (resourceStream == null)
101         {
102             log.warn("Resource not found: {}", this);
103             return;
104         }
105 
106         streamResource(resourceStream, out);
107     }
108 
109     /**
110      * Copy from the supplied OutputStream to the supplied InputStream. Note
111      * that the InputStream will be closed on completion.
112      *
113      * @param in the stream to read from
114      * @param out the stream to write to
115      * @throws DownloadException if an IOException is encountered writing to the
116      *             out stream
117      */
118     private void streamResource(final InputStream in, final OutputStream out) throws DownloadException
119     {
120         try
121         {
122             IOUtils.copy(in, out);
123         }
124         catch (final IOException e)
125         {
126             throw new DownloadException(e);
127         }
128         finally
129         {
130             IOUtils.closeQuietly(in);
131             try
132             {
133                 out.flush();
134             }
135             catch (final IOException e)
136             {
137                 log.debug("Error flushing output stream", e);
138             }
139         }
140     }
141 
142     /**
143      * Checks any "If-Modified-Since" header from the request against the
144      * plugin's loading time, since plugins can't be modified after they've been
145      * loaded this is a good way to determine if a plugin resource has been
146      * modified or not. If this method returns true, don't do any more
147      * processing on the request -- the response code has already been set to
148      * "304 Not Modified" for you, and you don't need to serve the file.
149      */
150     public boolean isResourceModified(final HttpServletRequest httpServletRequest, final HttpServletResponse httpServletResponse)
151     {
152         final Date resourceLastModifiedDate = (plugin.getDateLoaded() == null) ? new Date() : plugin.getDateLoaded();
153         final LastModifiedHandler lastModifiedHandler = new LastModifiedHandler(resourceLastModifiedDate);
154         return !lastModifiedHandler.checkRequest(httpServletRequest, httpServletResponse);
155     }
156 
157     public String getContentType()
158     {
159         return resourceLocation.getContentType();
160     }
161 
162     /**
163      * Returns an {@link InputStream} to stream the resource from based on
164      * resource name.
165      *
166      * @param resourceLocation the location of the resource to try and load
167      * @return an InputStream if the resource can be found or null if cant be
168      *         found
169      */
170     protected abstract InputStream getResourceAsStream(String resourceLocation);
171 
172     /**
173      * This is called to return the location of the resource that this object
174      * represents.
175      *
176      * @return the location of the resource that this object represents.
177      */
178     protected String getLocation()
179     {
180         return location;
181     }
182 
183     @Override
184     public String toString()
185     {
186         final String pluginKey = plugin != null ? plugin.getKey() : "";
187         return "Resource: " + pluginKey + " " + getLocation() + " (" + getContentType() + ")";
188     }
189 
190     /**
191      * This is called to use a minification naming strategy to find resources.
192      * If a minified file cant by found then the base location is used as the
193      * fall back
194      *
195      * @return an InputStream r null if nothing can be found for the resource
196      *         name
197      */
198     private InputStream getResourceAsStreamViaMinificationStrategy()
199     {
200 
201         InputStream inputStream = null;
202         final String location = getLocation();
203         if (minificationStrategyInPlay(location))
204         {
205             final String minifiedLocation = getMinifiedLocation(location);
206             inputStream = getResourceAsStream(minifiedLocation);
207         }
208         if (inputStream == null)
209         {
210             inputStream = getResourceAsStream(location);
211         }
212         return inputStream;
213     }
214 
215     /**
216      * Returns true if the minification strategy should be applied to a given
217      * resource name
218      *
219      * @param resourceLocation the location of the resource
220      * @return true if the minification strategy should be used.
221      */
222     private boolean minificationStrategyInPlay(final String resourceLocation)
223     {
224         // check if minification has been turned off for this resource (at the
225         // module level)
226         if (disableMinification)
227         {
228             return false;
229         }
230 
231         // secondly CHECK if we have a System property set to true that DISABLES
232         // the minification
233         try
234         {
235             if (Boolean.getBoolean(ATLASSIAN_WEBRESOURCE_DISABLE_MINIFICATION) || PluginUtils.isAtlassianDevMode())
236             {
237                 return false;
238             }
239         }
240         catch (final SecurityException se)
241         {
242             // some app servers might have protected access to system
243             // properties. Unlikely but lets be defensive
244         }
245         // We only minify .js or .css files
246         if (resourceLocation.endsWith(".js"))
247         {
248             // Check if it is already the minified version of the file
249             return !(resourceLocation.endsWith("-min.js") || resourceLocation.endsWith(".min.js"));
250         }
251         if (resourceLocation.endsWith(".css"))
252         {
253             // Check if it is already the minified version of the file
254             return !(resourceLocation.endsWith("-min.css") || resourceLocation.endsWith(".min.css"));
255         }
256         // Not .js or .css, don't bother trying to find a minified version (may
257         // save some file operations)
258         return false;
259     }
260 
261     private String getMinifiedLocation(final String location)
262     {
263         final int lastDot = location.lastIndexOf(".");
264         // this can never but -1 since the method call is protected by a call to
265         // minificationStrategyInPlay() first
266         return location.substring(0, lastDot) + "-min" + location.substring(lastDot);
267     }
268 }