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