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