org.lockss.jetty.LockssResourceHandler Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of lockss-core Show documentation
Show all versions of lockss-core Show documentation
The Slimmed Down LOCKSS Daemon Core
The newest version!
/*
* $Id$
*/
/*
Copyright (c) 2000-2016 Board of Trustees of Leland Stanford Jr. University,
all rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
STANFORD UNIVERSITY BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Except as contained in this notice, the name of Stanford University shall not
be used in advertising or otherwise to promote the sale, use or other dealings
in this Software without prior written authorization from Stanford University.
*/
// Portions of this code are:
// ===========================================================================
// Copyright (c) 1996-2002 Mort Bay Consulting Pty. Ltd. All rights reserved.
// $Id$
// ---------------------------------------------------------------------------
package org.lockss.jetty;
import java.io.*;
import java.util.*;
import org.apache.commons.logging.Log;
import org.mortbay.http.*;
import org.mortbay.http.handler.*;
import org.mortbay.log.LogFactory;
import org.mortbay.util.*;
import com.sun.jimi.core.*;
import com.sun.jimi.core.raster.JimiRasterImage;
import org.lockss.app.LockssDaemon;
import org.lockss.config.CurrentConfig;
import org.lockss.plugin.CachedUrl;
import org.lockss.proxy.ProxyManager;
import org.lockss.util.*;
import org.lockss.util.StringUtil;
/** Extension of ResourceHandler that allows flexibility in finding the
* Resource. Mostly copied here because some things in ResourceHandler
* aren't public or protected. */
public class LockssResourceHandler extends AbstractHttpHandler {
private static Log log = LogFactory.getLog(ResourceHandler.class);
/* ----------------------------------------------------------------- */
private LockssDaemon theDaemon = null;
private ProxyManager proxyMgr = null;
private boolean _acceptRanges=true;
private boolean _redirectWelcomeFiles ;
private String _redirectRootTo ;
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.__MOVE,
HttpRequest.__POST,
HttpRequest.__HEAD,
HttpRequest.__OPTIONS,
HttpRequest.__TRACE
});
}
/* ----------------------------------------------------------------- */
/** Construct a ResourceHandler.
*/
public LockssResourceHandler(LockssDaemon daemon)
{
theDaemon = daemon;
proxyMgr = theDaemon.getProxyManager();
}
/* ----------------------------------------------------------------- */
public synchronized void start()
throws Exception
{
super.start();
}
/* ----------------------------------------------------------------- */
public void stop()
throws InterruptedException
{
super.stop();
}
public void setRedirectRootTo(String target) {
_redirectRootTo = target;
}
/* ------------------------------------------------------------ */
public String[] getAllowedMethods()
{
return _methods;
}
/* ------------------------------------------------------------ */
public void setAllowedMethods(String[] methods)
{
StringBuffer b = new StringBuffer();
_methods=methods;
_methodMap.clear();
for (int i=0;i0)
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(HttpRequest request, 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(request, 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;
}
if (_redirectRootTo != null && pathInContext.equals("/")) {
log.debug("Redirect root to " + _redirectRootTo);
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(),
_redirectRootTo));
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);
}
else if (handleLockssRedirect(request, response, pathInContext,
pathParams, resource))
{
return;
}
// 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");
}
}
// CachedUrls may have content, yet specify a redirect elsewhere. The
// redirect must be returned to the requestor.
boolean handleLockssRedirect(HttpRequest request,
HttpResponse response,
String pathInContext,
String pathParams,
Resource resource) {
if (!(resource instanceof CuUrlResource)) {
return false;
}
CuUrlResource cur = (CuUrlResource)resource;
String nodeUrl = cur.getProperty(CachedUrl.PROPERTY_NODE_URL);
String rTo = cur.getProperty(CachedUrl.PROPERTY_REDIRECTED_TO);
String reqUrl = request.getRequestURL().toString();
// follow any redirect property, unless it points at current URL. (Can
// happen on "directory" nodes, which have two names, with and without
// slash.)
if (rTo != null) {
if (rTo.equals(reqUrl)) {
return false;
} else {
sendLockssRedirect(request, response, pathInContext,
pathParams, resource, rTo);
return true;
}
} else
// Can't count on directory node having a redirected-to property,
// bacause it might have been with a final slash, hence no redirect.
// If request path doesn't end with slash but node's path does,
// then this is a request for a node that's actually a "directory",
// so issue the redirect.
if (!pathInContext.endsWith("/")) {
URI nodeUri = new URI(nodeUrl);
if (nodeUri.getPath().endsWith("/")) {
sendLockssRedirect(request, response, pathInContext,
pathParams, resource, nodeUrl);
return true;
}
}
return false;
}
void sendLockssRedirect(HttpRequest request,
HttpResponse response,
String pathInContext,
String pathParams,
Resource resource,
String to) {
response.setField(HttpFields.__Location, to);
response.setStatus(HttpResponse.__301_Moved_Permanently);
request.setHandled(true);
}
/* ------------------------------------------------------------ */
/* Check modification date headers.
*/
private boolean passConditionalHeaders(HttpRequest request,
HttpResponse response,
Resource resource)
throws IOException
{
boolean ignoreIfModified = false;
if (CurrentConfig.getCurrentConfig().getBoolean(ProxyManager.PARAM_IGNORE_IF_MODIFIED_WHEN_CONTENT_LENGTH_WRONG,
ProxyManager.DEFAULT_IGNORE_IF_MODIFIED_WHEN_CONTENT_LENGTH_WRONG) &&
resource instanceof CuUrlResource) {
CuUrlResource cur = (CuUrlResource)resource;
String clenHdr = cur.getProperty(HttpFields.__ContentLength);
if (!StringUtil.isNullString(clenHdr)) {
try {
long clen = Long.parseLong(clenHdr);
if (clen != resource.length()) {
ignoreIfModified = true;
log.debug("ignoring If-Modified-Since: " + cur.getURL());
}
} catch (NumberFormatException e) {
log.warn("Error parsing Content-Length: " + clenHdr +
" of " + cur.getURL());
}
}
}
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 && resource.lastModified() > 0)
{
String ifms=request.getField(HttpFields.__IfModifiedSince);
if (ignoreIfModified) {
ifms = null;
}
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 (!ignoreIfModified &&
(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
{
// XXX - 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();
CuUrlResource cur = null;
String ctype = null;
if (resource instanceof CuUrlResource) {
cur = (CuUrlResource)resource;
ctype = cur.getProperty(CachedUrl.PROPERTY_CONTENT_TYPE);
}
if (ctype == null &&
CurrentConfig.getCurrentConfig()
.getBoolean(ProxyManager.PARAM_INFER_MIME_TYPE,
ProxyManager.DEFAULT_INFER_MIME_TYPE)) {
ctype = metaData.getMimeType();
log.trace("ctype from metadata: " + ctype);
}
response.setContentType(ctype);
if (count != -1)
{
String origContentLength = null;
if (cur != null) {
origContentLength = cur.getProperty(HttpFields.__ContentLength);
}
if (count==resource.length()) {
response.setField(HttpFields.__ContentLength,metaData.getLength());
} else {
if (count < Integer.MAX_VALUE) {
response.setContentLength((int)count);
} else {
response.setField(HttpFields.__ContentLength,
Long.toString(count));
}
}
if (origContentLength != null &&
!StringUtil.equalStrings(origContentLength,
response.getField(HttpFields.__ContentLength))) {
response.setField(origPrefix(HttpFields.__ContentLength),
origContentLength);
}
}
if (resource.lastModified() > 0) {
response.setField(HttpFields.__LastModified,
metaData.getLastModified());
}
if (_acceptRanges && response.getHttpRequest().getDotVersion()>0)
response.setField(HttpFields.__AcceptRanges,"bytes");
if (cur != null) {
addStoredHeaders(response, cur);
}
}
// Headers that should never be copied from one connection to another.
// Copied from ProxyHandler._DontProxyHeaders
String[] DONT_PROXY_HEADERS = {
HttpFields.__Connection,
HttpFields.__ProxyConnection,
HttpFields.__Connection,
HttpFields.__KeepAlive,
HttpFields.__TransferEncoding,
HttpFields.__TE,
HttpFields.__Trailer,
HttpFields.__ProxyAuthorization,
HttpFields.__ProxyAuthenticate,
HttpFields.__Upgrade,
};
private void addStoredHeaders(HttpResponse response,
CuUrlResource cur) {
Map> hdrMap = cur.getPropertyMap();
for (Map.Entry> ent : hdrMap.entrySet()) {
String key = ent.getKey();
List valLst = ent.getValue();
// CuUrlResource always produces one-element lists; skip if malformed
if (valLst.size() != 1) {
log.warn("Unexpected CuUrlResource property list for " +
key + " (" + valLst.size() + " items): " + valLst);
continue;
}
// Maintain the original value of some keys by prefixing with orig_
if (isHeaderKey(key, CachedUrl.LOCKSS_PREFIX_ORIG_PROPERTIES)) {
String prefKey = origPrefix(key);
// Don't replace existing orig_ header
if (response.getField(prefKey) != null) {
continue;
}
response.setField(prefKey, valLst.get(0));
continue;
}
// Skip keys that already have a value in the response
if (response.getField(key) != null) {
continue;
}
// Skip internal props and connection-oriented props
if (isHeaderKey(key, CachedUrl.LOCKSS_INTERNAL_PROPERTIES) ||
isHeaderKey(key, DONT_PROXY_HEADERS)) {
continue;
}
// If configured to copy response props, or if this is an audit prop
// (repair info, checksum) that we're configured to include, store it
// in the response.
if (isHeaderKey(key, CachedUrl.LOCKSS_AUDIT_PROPERTIES)
? proxyMgr.isIncludeLockssAuditProps()
: proxyMgr.isCopyStoredResponseHeaders()) {
response.setField(key, valLst.get(0));
}
}
}
boolean isHeaderKey(String key, String[] keyset) {
for (String s : keyset) {
if (key.equalsIgnoreCase(s)) {
return true;
}
}
return false;
}
private String origPrefix(String s) {
return CuResourceHandler.ORIG_HEADER_PREFIX + s;
}
/* ------------------------------------------------------------ */
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);
// (sethm) Begin content rewrite hack.
//
// Content Rewriting: If the PARAM_REWRITE_GIF_PNG
// config parameter is set, and this is GIF content, use
// JIMI to rewrite as PNG
InputStream in = data.getInputStream();
OutputStream out = null;
boolean enableRewrite =
CurrentConfig.getCurrentConfig().getBoolean(ProxyManager.PARAM_REWRITE_GIF_PNG,
ProxyManager.DEFAULT_REWRITE_GIF_PNG);
if (!proxyMgr.isRepairRequest(request) &&
enableRewrite &&
"image/gif".equals(HeaderUtil.getMimeTypeFromContentType(response.getContentType())) &&
"from-cache".equals(response.getField("X-Lockss"))) {
try {
JimiRasterImage img =
Jimi.getRasterImage(in, Jimi.SYNCHRONOUS);
// Content length cannot be known before the data is
// written. Remove the Content-Length header.
response.removeField("Content-Length");
response.setContentType("image/png");
out = response.getOutputStream();
Jimi.putImage("image/png", img, out);
out.flush();
} catch (JimiException ex) {
throw new IOException(ex.getMessage());
}
} else {
out = response.getOutputStream();
IO.copy(in, out, resLength);
}
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
// End hack.
//
// 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