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