Java tutorial
// ======================================================================== // $Id: ResourceHandler.java,v 1.66 2005/08/24 08:18:17 gregwilkins Exp $ // Copyright 199-2004 Mort Bay Consulting Pty. Ltd. // ------------------------------------------------------------------------ // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ======================================================================== package net.lightbody.bmp.proxy.jetty.http.handler; import net.lightbody.bmp.proxy.jetty.http.*; import net.lightbody.bmp.proxy.jetty.log.LogFactory; import net.lightbody.bmp.proxy.jetty.util.*; import org.apache.commons.logging.Log; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Enumeration; import java.util.List; /* ------------------------------------------------------------ */ /** Handler to serve files and resources. * Serves files from a given resource URL base and implements * the GET, HEAD, DELETE, OPTIONS, PUT, MOVE methods and the * IfModifiedSince and IfUnmodifiedSince header fields. * A simple memory cache is also provided to reduce file I/O. * HTTP/1.1 ranges are supported. * * @version $Id: ResourceHandler.java,v 1.66 2005/08/24 08:18:17 gregwilkins Exp $ * @author Nuno Preguia * @author Greg Wilkins */ public class ResourceHandler extends AbstractHttpHandler { private static Log log = LogFactory.getLog(ResourceHandler.class); /* ----------------------------------------------------------------- */ private boolean _acceptRanges = true; private boolean _redirectWelcomeFiles; private String[] _methods = null; private String _allowed; private boolean _dirAllowed = true; private int _minGzipLength = -1; private StringMap _methodMap = new StringMap(); { setAllowedMethods(new String[] { HttpRequest.__GET, HttpRequest.__POST, HttpRequest.__HEAD, HttpRequest.__OPTIONS, HttpRequest.__TRACE }); } /* ----------------------------------------------------------------- */ /** Construct a ResourceHandler. */ public ResourceHandler() { } /* ----------------------------------------------------------------- */ public synchronized void start() throws Exception { super.start(); } /* ----------------------------------------------------------------- */ public void stop() throws InterruptedException { super.stop(); } /* ------------------------------------------------------------ */ public String[] getAllowedMethods() { return _methods; } /* ------------------------------------------------------------ */ public void setAllowedMethods(String[] methods) { StringBuffer b = new StringBuffer(); _methods = methods; _methodMap.clear(); for (int i = 0; i < methods.length; i++) { _methodMap.put(methods[i], methods[i]); if (i > 0) b.append(','); b.append(methods[i]); } _allowed = b.toString(); } /* ------------------------------------------------------------ */ public boolean isMethodAllowed(String method) { return _methodMap.get(method) != null; } /* ------------------------------------------------------------ */ public String getAllowedString() { return _allowed; } /* ------------------------------------------------------------ */ public boolean isDirAllowed() { return _dirAllowed; } /* ------------------------------------------------------------ */ public void setDirAllowed(boolean dirAllowed) { _dirAllowed = dirAllowed; } /* ------------------------------------------------------------ */ public boolean isAcceptRanges() { return _acceptRanges; } /* ------------------------------------------------------------ */ /** * @return True if welcome files are redirected to. False if forward is used. */ public boolean getRedirectWelcome() { return _redirectWelcomeFiles; } /* ------------------------------------------------------------ */ /** * @param redirectWelcome True if welcome files are redirected to. False * if forward is used. */ public void setRedirectWelcome(boolean redirectWelcome) { _redirectWelcomeFiles = redirectWelcome; } /* ------------------------------------------------------------ */ /** Set if the handler accepts range requests. * Default is false; * @param ar True if the handler should accept ranges */ public void setAcceptRanges(boolean ar) { _acceptRanges = ar; } /* ------------------------------------------------------------ */ /** Get minimum content length for GZIP encoding. * @return Minimum length of content for gzip encoding or -1 if disabled. */ public int getMinGzipLength() { return _minGzipLength; } /* ------------------------------------------------------------ */ /** Set minimum content length for GZIP encoding. * @param minGzipLength If set to a positive integer, then static content * larger than this will be served as gzip content encoded * if a matching resource is found ending with ".gz" */ public void setMinGzipLength(int minGzipLength) { _minGzipLength = minGzipLength; } /* ------------------------------------------------------------ */ /** get Resource to serve. * Map a path to a resource. The default implementation calls * HttpContext.getResource but derived handers may provide * their own mapping. * @param pathInContext The path to find a resource for. * @return The resource to serve. */ protected Resource getResource(String pathInContext) throws IOException { return getHttpContext().getResource(pathInContext); } /* ------------------------------------------------------------ */ public void handle(String pathInContext, String pathParams, HttpRequest request, HttpResponse response) throws HttpException, IOException { Resource resource = getResource(pathInContext); if (resource == null) return; // Is the method allowed? if (!isMethodAllowed(request.getMethod())) { if (log.isDebugEnabled()) log.debug("Method not allowed: " + request.getMethod()); if (resource.exists()) { setAllowHeader(response); response.sendError(HttpResponse.__405_Method_Not_Allowed); } return; } // Handle the request try { if (log.isDebugEnabled()) log.debug("PATH=" + pathInContext + " RESOURCE=" + resource); // check filename String method = request.getMethod(); if (method.equals(HttpRequest.__GET) || method.equals(HttpRequest.__POST) || method.equals(HttpRequest.__HEAD)) handleGet(request, response, pathInContext, pathParams, resource); else if (method.equals(HttpRequest.__PUT)) handlePut(request, response, pathInContext, resource); else if (method.equals(HttpRequest.__DELETE)) handleDelete(request, response, pathInContext, resource); else if (method.equals(HttpRequest.__OPTIONS)) handleOptions(response, pathInContext); else if (method.equals(HttpRequest.__MOVE)) handleMove(request, response, pathInContext, resource); else if (method.equals(HttpRequest.__TRACE)) handleTrace(request, response); else { if (log.isDebugEnabled()) log.debug("Unknown action:" + method); // anything else... try { if (resource.exists()) response.sendError(HttpResponse.__501_Not_Implemented); } catch (Exception e) { LogSupport.ignore(log, e); } } } catch (IllegalArgumentException e) { LogSupport.ignore(log, e); } finally { if (resource != null && !(resource instanceof CachedResource)) resource.release(); } } /* ------------------------------------------------------------------- */ public void handleGet(HttpRequest request, HttpResponse response, String pathInContext, String pathParams, Resource resource) throws IOException { if (log.isDebugEnabled()) log.debug("Looking for " + resource); if (resource != null && resource.exists()) { // check if directory if (resource.isDirectory()) { if (!pathInContext.endsWith("/") && !pathInContext.equals("/")) { log.debug("Redirect to directory/"); String q = request.getQuery(); StringBuffer buf = request.getRequestURL(); if (q != null && q.length() != 0) { buf.append('?'); buf.append(q); } response.setField(HttpFields.__Location, URI.addPaths(buf.toString(), "/")); response.setStatus(302); request.setHandled(true); return; } // See if index file exists String welcome = getHttpContext().getWelcomeFile(resource); if (welcome != null) { // Forward to the index String ipath = URI.addPaths(pathInContext, welcome); if (_redirectWelcomeFiles) { // Redirect to the index ipath = URI.addPaths(getHttpContext().getContextPath(), ipath); response.setContentLength(0); response.sendRedirect(ipath); } else { URI uri = request.getURI(); uri.setPath(URI.addPaths(uri.getPath(), welcome)); getHttpContext().handle(ipath, pathParams, request, response); } return; } // Check modified dates if (!passConditionalHeaders(request, response, resource)) return; // If we got here, no forward to index took place sendDirectory(request, response, resource, pathInContext.length() > 1); } // check if it is a file else if (resource.exists()) { // Check modified dates if (!passConditionalHeaders(request, response, resource)) return; sendData(request, response, pathInContext, resource, true); } else // don't know what it is log.warn("Unknown file type"); } } /* ------------------------------------------------------------ */ /* Check modification date headers. */ private boolean passConditionalHeaders(HttpRequest request, HttpResponse response, Resource resource) throws IOException { if (!request.getMethod().equals(HttpRequest.__HEAD)) { // If we have meta data for the file // Try a direct match for most common requests. Avoids // parsing the date. ResourceCache.ResourceMetaData metaData = (ResourceCache.ResourceMetaData) resource.getAssociate(); if (metaData != null) { String ifms = request.getField(HttpFields.__IfModifiedSince); String mdlm = metaData.getLastModified(); if (ifms != null && mdlm != null && ifms.equals(mdlm)) { response.setStatus(HttpResponse.__304_Not_Modified); request.setHandled(true); return false; } } long date = 0; // Parse the if[un]modified dates and compare to resource if ((date = request.getDateField(HttpFields.__IfUnmodifiedSince)) > 0) { if (resource.lastModified() / 1000 > date / 1000) { response.sendError(HttpResponse.__412_Precondition_Failed); return false; } } if ((date = request.getDateField(HttpFields.__IfModifiedSince)) > 0) { if (resource.lastModified() / 1000 <= date / 1000) { response.setStatus(HttpResponse.__304_Not_Modified); request.setHandled(true); return false; } } } return true; } /* ------------------------------------------------------------ */ void handlePut(HttpRequest request, HttpResponse response, String pathInContext, Resource resource) throws IOException { if (log.isDebugEnabled()) log.debug("PUT " + pathInContext + " in " + resource); boolean exists = resource != null && resource.exists(); if (exists && !passConditionalHeaders(request, response, resource)) return; if (pathInContext.endsWith("/")) { if (!exists) { if (!resource.getFile().mkdirs()) response.sendError(HttpResponse.__403_Forbidden, "Directories could not be created"); else { request.setHandled(true); response.setStatus(HttpResponse.__201_Created); response.commit(); } } else { request.setHandled(true); response.setStatus(HttpResponse.__200_OK); response.commit(); } } else { try { int toRead = request.getContentLength(); InputStream in = request.getInputStream(); OutputStream out = resource.getOutputStream(); if (toRead >= 0) IO.copy(in, out, toRead); else IO.copy(in, out); out.close(); request.setHandled(true); response.setStatus(exists ? HttpResponse.__200_OK : HttpResponse.__201_Created); response.commit(); } catch (Exception ex) { log.warn(LogSupport.EXCEPTION, ex); response.sendError(HttpResponse.__403_Forbidden, ex.getMessage()); } } } /* ------------------------------------------------------------ */ void handleDelete(HttpRequest request, HttpResponse response, String pathInContext, Resource resource) throws IOException { if (log.isDebugEnabled()) log.debug("DELETE " + pathInContext + " from " + resource); if (!resource.exists() || !passConditionalHeaders(request, response, resource)) return; try { // delete the file if (resource.delete()) response.setStatus(HttpResponse.__204_No_Content); else response.sendError(HttpResponse.__403_Forbidden); // Send response request.setHandled(true); } catch (SecurityException sex) { log.warn(LogSupport.EXCEPTION, sex); response.sendError(HttpResponse.__403_Forbidden, sex.getMessage()); } } /* ------------------------------------------------------------ */ void handleMove(HttpRequest request, HttpResponse response, String pathInContext, Resource resource) throws IOException { if (!resource.exists() || !passConditionalHeaders(request, response, resource)) return; String newPath = URI.canonicalPath(request.getField("New-uri")); if (newPath == null) { response.sendError(HttpResponse.__405_Method_Not_Allowed, "Bad new uri"); return; } String contextPath = getHttpContext().getContextPath(); if (contextPath != null && !newPath.startsWith(contextPath)) { response.sendError(HttpResponse.__405_Method_Not_Allowed, "Not in context"); return; } // Find path try { // TODO - Check this String newInfo = newPath; if (contextPath != null) newInfo = newInfo.substring(contextPath.length()); Resource newFile = getHttpContext().getBaseResource().addPath(newInfo); if (log.isDebugEnabled()) log.debug("Moving " + resource + " to " + newFile); resource.renameTo(newFile); response.setStatus(HttpResponse.__204_No_Content); request.setHandled(true); } catch (Exception ex) { log.warn(LogSupport.EXCEPTION, ex); setAllowHeader(response); response.sendError(HttpResponse.__405_Method_Not_Allowed, "Error:" + ex); return; } } /* ------------------------------------------------------------ */ void handleOptions(HttpResponse response, String pathInContext) throws IOException { if ("*".equals(pathInContext)) return; setAllowHeader(response); response.commit(); } /* ------------------------------------------------------------ */ void setAllowHeader(HttpResponse response) { response.setField(HttpFields.__Allow, getAllowedString()); } /* ------------------------------------------------------------ */ public void writeHeaders(HttpResponse response, Resource resource, long count) throws IOException { ResourceCache.ResourceMetaData metaData = (ResourceCache.ResourceMetaData) resource.getAssociate(); response.setContentType(metaData.getMimeType()); if (count != -1) { if (count == resource.length()) response.setField(HttpFields.__ContentLength, metaData.getLength()); else response.setContentLength((int) count); } response.setField(HttpFields.__LastModified, metaData.getLastModified()); if (_acceptRanges && response.getHttpRequest().getDotVersion() > 0) response.setField(HttpFields.__AcceptRanges, "bytes"); } /* ------------------------------------------------------------ */ public void sendData(HttpRequest request, HttpResponse response, String pathInContext, Resource resource, boolean writeHeaders) throws IOException { long resLength = resource.length(); // see if there are any range headers Enumeration reqRanges = request.getDotVersion() > 0 ? request.getFieldValues(HttpFields.__Range) : null; if (!writeHeaders || reqRanges == null || !reqRanges.hasMoreElements()) { // look for a gziped content. Resource data = resource; if (_minGzipLength > 0) { String accept = request.getField(HttpFields.__AcceptEncoding); if (accept != null && resLength > _minGzipLength && !pathInContext.endsWith(".gz")) { Resource gz = getHttpContext().getResource(pathInContext + ".gz"); if (gz.exists() && accept.indexOf("gzip") >= 0) { if (log.isDebugEnabled()) log.debug("gzip=" + gz); response.setField(HttpFields.__ContentEncoding, "gzip"); data = gz; resLength = data.length(); } } } writeHeaders(response, resource, resLength); request.setHandled(true); OutputStream out = response.getOutputStream(); data.writeTo(out, 0, resLength); return; } // Parse the satisfiable ranges List ranges = InclusiveByteRange.satisfiableRanges(reqRanges, resLength); if (log.isDebugEnabled()) log.debug("ranges: " + reqRanges + " == " + ranges); // if there are no satisfiable ranges, send 416 response if (ranges == null || ranges.size() == 0) { log.debug("no satisfiable ranges"); writeHeaders(response, resource, resLength); response.setStatus(HttpResponse.__416_Requested_Range_Not_Satisfiable); response.setReason((String) HttpResponse.__statusMsg .get(TypeUtil.newInteger(HttpResponse.__416_Requested_Range_Not_Satisfiable))); response.setField(HttpFields.__ContentRange, InclusiveByteRange.to416HeaderRangeString(resLength)); OutputStream out = response.getOutputStream(); resource.writeTo(out, 0, resLength); request.setHandled(true); return; } // if there is only a single valid range (must be satisfiable // since were here now), send that range with a 216 response if (ranges.size() == 1) { InclusiveByteRange singleSatisfiableRange = (InclusiveByteRange) ranges.get(0); if (log.isDebugEnabled()) log.debug("single satisfiable range: " + singleSatisfiableRange); long singleLength = singleSatisfiableRange.getSize(resLength); writeHeaders(response, resource, singleLength); response.setStatus(HttpResponse.__206_Partial_Content); response.setReason( (String) HttpResponse.__statusMsg.get(TypeUtil.newInteger(HttpResponse.__206_Partial_Content))); response.setField(HttpFields.__ContentRange, singleSatisfiableRange.toHeaderRangeString(resLength)); OutputStream out = response.getOutputStream(); resource.writeTo(out, singleSatisfiableRange.getFirst(resLength), singleLength); request.setHandled(true); return; } // multiple non-overlapping valid ranges cause a multipart // 216 response which does not require an overall // content-length header // ResourceCache.ResourceMetaData metaData = (ResourceCache.ResourceMetaData) resource.getAssociate(); String encoding = metaData.getMimeType(); MultiPartResponse multi = new MultiPartResponse(response); response.setStatus(HttpResponse.__206_Partial_Content); response.setReason( (String) HttpResponse.__statusMsg.get(TypeUtil.newInteger(HttpResponse.__206_Partial_Content))); // If the request has a "Request-Range" header then we need to // send an old style multipart/x-byteranges Content-Type. This // keeps Netscape and acrobat happy. This is what Apache does. String ctp; if (request.containsField(HttpFields.__RequestRange)) ctp = "multipart/x-byteranges; boundary="; else ctp = "multipart/byteranges; boundary="; response.setContentType(ctp + multi.getBoundary()); InputStream in = (resource instanceof CachedResource) ? null : resource.getInputStream(); OutputStream out = response.getOutputStream(); long pos = 0; for (int i = 0; i < ranges.size(); i++) { InclusiveByteRange ibr = (InclusiveByteRange) ranges.get(i); String header = HttpFields.__ContentRange + ": " + ibr.toHeaderRangeString(resLength); if (log.isDebugEnabled()) log.debug("multi range: " + encoding + " " + header); multi.startPart(encoding, new String[] { header }); long start = ibr.getFirst(resLength); long size = ibr.getSize(resLength); if (in != null) { // Handle non cached resource if (start < pos) { in.close(); in = resource.getInputStream(); pos = 0; } if (pos < start) { in.skip(start - pos); pos = start; } IO.copy(in, out, size); pos += size; } else // Handle cached resource resource.writeTo(out, start, size); } if (in != null) in.close(); multi.close(); request.setHandled(true); return; } /* ------------------------------------------------------------------- */ void sendDirectory(HttpRequest request, HttpResponse response, Resource resource, boolean parent) throws IOException { if (!_dirAllowed) { response.sendError(HttpResponse.__403_Forbidden); return; } request.setHandled(true); if (log.isDebugEnabled()) log.debug("sendDirectory: " + resource); byte[] data = null; if (resource instanceof CachedResource) data = ((CachedResource) resource).getCachedData(); if (data == null) { String base = URI.addPaths(request.getPath(), "/"); String dir = resource.getListHTML(URI.encodePath(base), parent); if (dir == null) { response.sendError(HttpResponse.__403_Forbidden, "No directory"); return; } data = dir.getBytes("UTF8"); if (resource instanceof CachedResource) ((CachedResource) resource).setCachedData(data); } response.setContentType("text/html; charset=UTF8"); response.setContentLength(data.length); if (request.getMethod().equals(HttpRequest.__HEAD)) { response.commit(); return; } response.getOutputStream().write(data, 0, data.length); response.commit(); } }