PageRenderTime 52ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/plugin/src/main/java/com/atlassian/plugin/remotable/plugin/iframe/StaticResourcesFilter.java

https://bitbucket.org/mquail/remotable-plugins
Java | 289 lines | 239 code | 36 blank | 14 comment | 17 complexity | 346360beab1357019656b60e30e4ac84 MD5 | raw file
Possible License(s): Apache-2.0
  1. package com.atlassian.plugin.remotable.plugin.iframe;
  2. import com.atlassian.plugin.remotable.host.common.service.DefaultRenderContext;
  3. import com.atlassian.plugin.Plugin;
  4. import com.atlassian.plugin.osgi.bridge.external.PluginRetrievalService;
  5. import com.atlassian.plugin.util.PluginUtils;
  6. import com.google.common.base.Function;
  7. import com.google.common.collect.MapMaker;
  8. import org.apache.commons.codec.digest.DigestUtils;
  9. import org.apache.commons.io.IOUtils;
  10. import org.slf4j.Logger;
  11. import org.slf4j.LoggerFactory;
  12. import javax.servlet.Filter;
  13. import javax.servlet.FilterChain;
  14. import javax.servlet.FilterConfig;
  15. import javax.servlet.ServletException;
  16. import javax.servlet.ServletOutputStream;
  17. import javax.servlet.ServletRequest;
  18. import javax.servlet.ServletResponse;
  19. import javax.servlet.http.HttpServletRequest;
  20. import javax.servlet.http.HttpServletResponse;
  21. import java.io.ByteArrayOutputStream;
  22. import java.io.IOException;
  23. import java.io.InputStream;
  24. import java.nio.charset.Charset;
  25. import java.util.Calendar;
  26. import java.util.Map;
  27. import java.util.regex.Pattern;
  28. import java.util.zip.GZIPOutputStream;
  29. /**
  30. * Provides static host resources for plugin iframes
  31. */
  32. public class StaticResourcesFilter implements Filter
  33. {
  34. public static final int PLUGIN_TTL_NEAR_FUTURE = 60 * 30; // 30 min
  35. public static final int AUI_TTL_FAR_FUTURE = 60 * 60 * 24 * 365; // 1 year
  36. private static final Pattern RESOURCE_PATTERN = Pattern.compile("(all(-debug)?\\.(js|css))|(aui/.*)");
  37. private static final Logger log = LoggerFactory.getLogger(StaticResourcesFilter.class);
  38. private static Plugin plugin;
  39. private final boolean devMode;
  40. private FilterConfig config;
  41. private Map<String, CacheEntry> cache;
  42. public StaticResourcesFilter(PluginRetrievalService pluginRetreivalService)
  43. {
  44. plugin = pluginRetreivalService.getPlugin();
  45. devMode = Boolean.getBoolean(PluginUtils.ATLASSIAN_DEV_MODE);
  46. }
  47. @Override
  48. public void init(FilterConfig config) throws ServletException
  49. {
  50. this.config = config;
  51. cache = new MapMaker().makeComputingMap(new Function<String, CacheEntry>()
  52. {
  53. @Override
  54. public CacheEntry apply(String from)
  55. {
  56. return new CacheEntry(from);
  57. }
  58. });
  59. }
  60. @Override
  61. public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException
  62. {
  63. HttpServletRequest req = (HttpServletRequest) request;
  64. HttpServletResponse res = (HttpServletResponse) response;
  65. // compute the starting resource path from the request
  66. String fullPath = req.getRequestURI().substring(req.getContextPath().length());
  67. // only serve resources in the host resource path, though this is precautionary only since no other
  68. // paths should be mapped to this filter in the first place
  69. if (!fullPath.startsWith(DefaultRenderContext.HOST_RESOURCE_PATH))
  70. {
  71. send404(fullPath, res);
  72. return;
  73. }
  74. // prepare a local path suitable for use with plugin.getResourceAsStream
  75. String localPath = fullPath.substring(DefaultRenderContext.HOST_RESOURCE_PATH.length() + 1);
  76. // only make selected resources available
  77. if (!RESOURCE_PATTERN.matcher(localPath).matches())
  78. {
  79. send404(fullPath, res);
  80. return;
  81. }
  82. // special dev mode case to make developing on all-debug.js not suck
  83. String encoding;
  84. CacheEntry entry;
  85. final String allDebugJsPath = "all-debug.js";
  86. if (allDebugJsPath.equals(localPath))
  87. {
  88. encoding = "identity";
  89. final String moduleDir = "js/iframe/";
  90. final String[] modules = {"plugin-core.js", "rpc.js", "plugin-api.js"};
  91. ByteArrayOutputStream bout = new ByteArrayOutputStream();
  92. for (String module : modules)
  93. {
  94. bout.write(("/* " + module + " */\n").getBytes());
  95. InputStream in = plugin.getResourceAsStream(moduleDir + module);
  96. IOUtils.copy(in, bout);
  97. bout.write('\n');
  98. }
  99. entry = new CacheEntry(allDebugJsPath, bout.toByteArray());
  100. }
  101. else
  102. {
  103. if (req.getHeader("Accept-Encoding").contains("gzip"))
  104. {
  105. // check if the request accepts gzip, then get a gzipped version of the resource from the cache
  106. localPath += ".gz";
  107. encoding = "gzip";
  108. }
  109. else
  110. {
  111. encoding = "identity";
  112. }
  113. // ask the cache for an entry for the named resource
  114. entry = cache.get(localPath);
  115. // the entry's data will be empty if the resource was not found
  116. if (entry.getData().length == 0)
  117. {
  118. // if not found, 404
  119. send404(fullPath, res);
  120. return;
  121. }
  122. }
  123. res.setContentType(entry.getContentType());
  124. res.setHeader("ETag", entry.getEtag());
  125. res.setHeader("Vary", "Accept-Encoding");
  126. setCacheControl(res, entry.getTTLSeconds());
  127. res.setHeader("Connection", "keep-alive");
  128. String previousToken = req.getHeader("If-None-Match");
  129. if (previousToken != null && previousToken.equals(entry.getEtag()))
  130. {
  131. res.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
  132. }
  133. else
  134. {
  135. res.setStatus(HttpServletResponse.SC_OK);
  136. res.setContentLength(entry.getData().length);
  137. res.setHeader("Content-Encoding", encoding);
  138. ServletOutputStream sos = res.getOutputStream();
  139. sos.write(entry.getData());
  140. sos.flush();
  141. sos.close();
  142. }
  143. if (devMode)
  144. {
  145. cache.remove(localPath);
  146. }
  147. }
  148. private void setCacheControl(HttpServletResponse res, int ttl)
  149. {
  150. Calendar cal = Calendar.getInstance();
  151. cal.set(Calendar.MILLISECOND, 0);
  152. res.setDateHeader("Date", cal.getTimeInMillis());
  153. cal.add(Calendar.SECOND, ttl);
  154. res.setHeader("Cache-Control", "public, max-age=" + ttl);
  155. res.setDateHeader("Expires", cal.getTime().getTime());
  156. }
  157. private void send404(String path, HttpServletResponse res) throws IOException
  158. {
  159. res.sendError(HttpServletResponse.SC_NOT_FOUND, "Cannot find resource: " + path);
  160. }
  161. @Override
  162. public void destroy()
  163. {
  164. cache.clear();
  165. }
  166. private class CacheEntry
  167. {
  168. private String contentType;
  169. private byte[] data;
  170. private String etag;
  171. private int ttl;
  172. public CacheEntry(String path, byte[] data)
  173. {
  174. setContentType(path);
  175. setData(data);
  176. }
  177. public CacheEntry(String path)
  178. {
  179. boolean gzip = path.endsWith(".gz");
  180. if (gzip)
  181. {
  182. path = path.substring(0, path.length() - 3);
  183. }
  184. setContentType(path);
  185. InputStream in;
  186. try
  187. {
  188. in = plugin.getResourceAsStream(path);
  189. if (in == null)
  190. {
  191. clear();
  192. }
  193. else
  194. {
  195. if (gzip)
  196. {
  197. ByteArrayOutputStream bytes = new ByteArrayOutputStream();
  198. GZIPOutputStream out = new GZIPOutputStream(bytes);
  199. IOUtils.copy(in, out);
  200. out.finish();
  201. out.close();
  202. setData(bytes.toByteArray());
  203. }
  204. else
  205. {
  206. setData(IOUtils.toByteArray(in));
  207. }
  208. }
  209. }
  210. catch (IOException e)
  211. {
  212. log.error("Unable to retrieve content: " + path, e);
  213. clear();
  214. }
  215. ttl = path.startsWith("aui/") ? AUI_TTL_FAR_FUTURE : PLUGIN_TTL_NEAR_FUTURE;
  216. }
  217. private void setContentType(String path)
  218. {
  219. contentType = config.getServletContext().getMimeType(path);
  220. // covers anything not mapped in default servlet context config, such as web fonts
  221. if (contentType == null)
  222. {
  223. contentType = "application/octet-stream";
  224. }
  225. }
  226. private void setData(byte[] data)
  227. {
  228. this.data = data;
  229. this.etag = DigestUtils.md5Hex(data);
  230. }
  231. public String getEtag()
  232. {
  233. return etag;
  234. }
  235. public byte[] getData()
  236. {
  237. return data;
  238. }
  239. public String getContentType()
  240. {
  241. return contentType;
  242. }
  243. public int getTTLSeconds()
  244. {
  245. return ttl;
  246. }
  247. private void clear()
  248. {
  249. data = new byte[0];
  250. etag = "";
  251. }
  252. }
  253. }