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          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         return "Resource: " + plugin.getKey() + " " + getLocation() + " (" + getContentType() + ")";
187     }
188 
189     /**
190      * This is called to use a minification naming strategy to find resources.
191      * If a minified file cant by found then the base location is used as the
192      * fall back
193      *
194      * @return an InputStream r null if nothing can be found for the resource
195      *         name
196      */
197     private InputStream getResourceAsStreamViaMinificationStrategy()
198     {
199 
200         InputStream inputStream = null;
201         final String location = getLocation();
202         if (minificationStrategyInPlay(location))
203         {
204             final String minifiedLocation = getMinifiedLocation(location);
205             inputStream = getResourceAsStream(minifiedLocation);
206         }
207         if (inputStream == null)
208         {
209             inputStream = getResourceAsStream(location);
210         }
211         return inputStream;
212     }
213 
214     /**
215      * Returns true if the minification strategy should be applied to a given
216      * resource name
217      *
218      * @param resourceLocation the location of the resource
219      * @return true if the minification strategy should be used.
220      */
221     private boolean minificationStrategyInPlay(final String resourceLocation)
222     {
223         // check if minification has been turned off for this resource (at the
224         // module level)
225         if (disableMinification)
226         {
227             return false;
228         }
229 
230         // secondly CHECK if we have a System property set to true that DISABLES
231         // the minification
232         try
233         {
234             if (Boolean.getBoolean(ATLASSIAN_WEBRESOURCE_DISABLE_MINIFICATION) || Boolean.getBoolean(PluginUtils.ATLASSIAN_DEV_MODE))
235             {
236                 return false;
237             }
238         }
239         catch (final SecurityException se)
240         {
241             // some app servers might have protected access to system
242             // properties. Unlikely but lets be defensive
243         }
244         // We only minify .js or .css files
245         if (resourceLocation.endsWith(".js"))
246         {
247             // Check if it is already the minified version of the file
248             return !(resourceLocation.endsWith("-min.js") || resourceLocation.endsWith(".min.js"));
249         }
250         if (resourceLocation.endsWith(".css"))
251         {
252             // Check if it is already the minified version of the file
253             return !(resourceLocation.endsWith("-min.css") || resourceLocation.endsWith(".min.css"));
254         }
255         // Not .js or .css, don't bother trying to find a minified version (may
256         // save some file operations)
257         return false;
258     }
259 
260     private String getMinifiedLocation(final String location)
261     {
262         final int lastDot = location.lastIndexOf(".");
263         // this can never but -1 since the method call is protected by a call to
264         // minificationStrategyInPlay() first
265         return location.substring(0, lastDot) + "-min" + location.substring(lastDot);
266     }
267 }