/src/StaticServlet.java

http://github.com/zefhemel/adia · Java · 225 lines · 188 code · 33 blank · 4 comment · 30 complexity · 4fff38c220db2ec00a1ad666440bea8e MD5 · raw file

  1. import java.io.File;
  2. import java.io.IOException;
  3. import java.io.InputStream;
  4. import java.io.OutputStream;
  5. import java.net.JarURLConnection;
  6. import java.net.MalformedURLException;
  7. import java.net.URL;
  8. import java.util.zip.GZIPOutputStream;
  9. import java.util.zip.ZipEntry;
  10. import javax.servlet.ServletException;
  11. import javax.servlet.http.HttpServlet;
  12. import javax.servlet.http.HttpServletRequest;
  13. import javax.servlet.http.HttpServletResponse;
  14. @SuppressWarnings("serial")
  15. public class StaticServlet extends HttpServlet {
  16. public static interface LookupResult {
  17. public void respondGet(HttpServletResponse resp) throws IOException;
  18. public void respondHead(HttpServletResponse resp);
  19. public long getLastModified();
  20. }
  21. public static class Error implements LookupResult {
  22. protected final int statusCode;
  23. protected final String message;
  24. public Error(int statusCode, String message) {
  25. this.statusCode = statusCode;
  26. this.message = message;
  27. }
  28. public long getLastModified() {
  29. return -1;
  30. }
  31. public void respondGet(HttpServletResponse resp) throws IOException {
  32. resp.sendError(statusCode,message);
  33. }
  34. public void respondHead(HttpServletResponse resp) {
  35. throw new UnsupportedOperationException();
  36. }
  37. }
  38. public static class StaticFile implements LookupResult {
  39. protected final long lastModified;
  40. protected final String mimeType;
  41. protected final int contentLength;
  42. protected final boolean acceptsDeflate;
  43. protected final URL url;
  44. public StaticFile(long lastModified, String mimeType, int contentLength, boolean acceptsDeflate, URL url) {
  45. this.lastModified = lastModified;
  46. this.mimeType = mimeType;
  47. this.contentLength = contentLength;
  48. this.acceptsDeflate = acceptsDeflate;
  49. this.url = url;
  50. }
  51. public long getLastModified() {
  52. return lastModified;
  53. }
  54. protected boolean willDeflate() {
  55. return acceptsDeflate && deflatable(mimeType) && contentLength >= deflateThreshold;
  56. }
  57. protected void setHeaders(HttpServletResponse resp) {
  58. resp.setStatus(HttpServletResponse.SC_OK);
  59. resp.setContentType(mimeType);
  60. if(contentLength >= 0 && !willDeflate())
  61. resp.setContentLength(contentLength);
  62. }
  63. public void respondGet(HttpServletResponse resp) throws IOException {
  64. setHeaders(resp);
  65. final OutputStream os;
  66. if(willDeflate()) {
  67. resp.setHeader("Content-Encoding", "gzip");
  68. os = new GZIPOutputStream(resp.getOutputStream(), bufferSize);
  69. } else
  70. os = resp.getOutputStream();
  71. transferStreams(url.openStream(),os);
  72. }
  73. public void respondHead(HttpServletResponse resp) {
  74. if(willDeflate())
  75. throw new UnsupportedOperationException();
  76. setHeaders(resp);
  77. }
  78. }
  79. @Override
  80. protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  81. lookup(req).respondGet(resp);
  82. }
  83. @Override
  84. protected void doPut(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  85. doGet(req, resp);
  86. }
  87. @Override
  88. protected void doHead(HttpServletRequest req, HttpServletResponse resp) throws IOException, ServletException {
  89. try {
  90. lookup(req).respondHead(resp);
  91. } catch(UnsupportedOperationException e) {
  92. super.doHead(req, resp);
  93. }
  94. }
  95. @Override
  96. protected long getLastModified(HttpServletRequest req) {
  97. return lookup(req).getLastModified();
  98. }
  99. protected LookupResult lookup(HttpServletRequest req) {
  100. LookupResult r = (LookupResult)req.getAttribute("lookupResult");
  101. if(r == null) {
  102. r = lookupNoCache(req);
  103. req.setAttribute("lookupResult", r);
  104. }
  105. return r;
  106. }
  107. protected LookupResult lookupNoCache(HttpServletRequest req) {
  108. final String path = getPath(req);
  109. if(isForbidden(path))
  110. return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
  111. final URL url;
  112. try {
  113. url = getServletContext().getResource(path);
  114. } catch(MalformedURLException e) {
  115. return new Error(HttpServletResponse.SC_BAD_REQUEST, "Malformed path");
  116. }
  117. if(url == null)
  118. return new Error(HttpServletResponse.SC_NOT_FOUND, "Not found");
  119. final String mimeType = getMimeType(path);
  120. final String realpath = getServletContext().getRealPath(path);
  121. if(realpath != null) {
  122. // Try as an ordinary file
  123. File f = new File(realpath);
  124. if(!f.isFile())
  125. return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
  126. else
  127. return new StaticFile(f.lastModified(),mimeType,(int)f.length(),acceptsDeflate(req),url);
  128. } else {
  129. try {
  130. // Try as a JAR Entry
  131. final ZipEntry ze = ((JarURLConnection)url.openConnection()).getJarEntry();
  132. if (ze != null) {
  133. if(ze.isDirectory())
  134. return new Error(HttpServletResponse.SC_FORBIDDEN, "Forbidden");
  135. else
  136. return new StaticFile(ze.getTime(),mimeType,(int)ze.getSize(),acceptsDeflate(req),url);
  137. } else
  138. // Unexpected?
  139. return new StaticFile(-1,mimeType,-1,acceptsDeflate(req),url);
  140. } catch(ClassCastException e) {
  141. // Unknown resource type
  142. return new StaticFile(-1,mimeType,-1,acceptsDeflate(req),url);
  143. } catch (IOException e) {
  144. return new Error(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Internal server error");
  145. }
  146. }
  147. }
  148. protected String getPath(HttpServletRequest req) {
  149. String servletPath = req.getServletPath();
  150. String pathInfo = coalesce(req.getPathInfo(), "");
  151. return servletPath + pathInfo;
  152. }
  153. protected boolean isForbidden(String path) {
  154. String lpath = path.toLowerCase();
  155. return lpath.startsWith("/web-inf/") || lpath.startsWith("/meta-inf/");
  156. }
  157. public static <T> T coalesce(T...ts) {
  158. for(T t: ts) {
  159. if(t != null) {
  160. return t;
  161. }
  162. }
  163. return null;
  164. }
  165. protected String getMimeType(String path) {
  166. return coalesce(getServletContext().getMimeType(path),"application/octet-stream");
  167. }
  168. protected static boolean acceptsDeflate(HttpServletRequest req) {
  169. final String ae = req.getHeader("Accept-Encoding");
  170. return ae != null && ae.contains("gzip");
  171. }
  172. protected static boolean deflatable(String mimetype) {
  173. return mimetype.startsWith("text/")
  174. || mimetype.equals("application/postscript")
  175. || mimetype.startsWith("application/ms")
  176. || mimetype.startsWith("application/vnd")
  177. || mimetype.endsWith("xml");
  178. }
  179. protected static final int deflateThreshold = 4*1024;
  180. protected static final int bufferSize = 4*1024;
  181. protected static void transferStreams(InputStream is, OutputStream os) throws IOException {
  182. try {
  183. byte[] buf = new byte[bufferSize];
  184. int bytesRead;
  185. while ((bytesRead = is.read(buf)) != -1)
  186. os.write(buf, 0, bytesRead);
  187. } finally {
  188. is.close();
  189. os.close();
  190. }
  191. }
  192. }