/******************************************************************************* * Copyright (c) 2008 Cambridge Semantics Incorporated. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Created by: Simon Martin ( <a href="mailto:simon@cambridgesemantics.com">jordi@cambridgesemantics.com </a>) * * Contributors: * Mort Bay Consulting Pty. Ltd. - the Jetty DefaultServlet is the basis of the Cambridge Semantics ResourceServerServlet. * Cambridge Semantics Incorporated - modified for use by the OpenAnzo Binary Store Servlet. * * This code is based on Jetty's org.eclipse.jetty.servlet.DefaultServlet class * as modified by Cambridge Semantics Incorporated * The original copyright statement from the DefaultServlet is reproduced below. * The DefaultServlet authors were listed in the source as: * Greg Wilkins (gregw) and Nigel Canonizado * ======================================================================== * 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 org.openanzo.servlet; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Enumeration; import java.util.List; import javax.servlet.ServletContext; import javax.servlet.UnavailableException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.eclipse.jetty.http.HttpContent; import org.eclipse.jetty.http.HttpFields; import org.eclipse.jetty.http.HttpHeaderValues; import org.eclipse.jetty.http.HttpHeaders; import org.eclipse.jetty.http.HttpMethods; import org.eclipse.jetty.http.MimeTypes; import org.eclipse.jetty.io.Buffer; import org.eclipse.jetty.io.ByteArrayBuffer; import org.eclipse.jetty.io.WriterOutputStream; import org.eclipse.jetty.server.HttpConnection; import org.eclipse.jetty.server.InclusiveByteRange; import org.eclipse.jetty.server.Response; import org.eclipse.jetty.servlet.ServletContextHandler; import org.eclipse.jetty.util.IO; import org.eclipse.jetty.util.MultiPartOutputStream; import org.eclipse.jetty.util.TypeUtil; /** * Sevlet to serve out resources from a bundle * * @author Matthew Roy ( <a href="mailto:mroy@cambridgesemantics.com">mroy@cambridgesemantics.com</a>) * */ public class ResourceServerServlet extends HttpServlet { private static final long serialVersionUID = 1L; private boolean _acceptRanges = true; ByteArrayBuffer _cacheControl; private MimeTypes _mimeTypes; @Override public void init() throws UnavailableException { ServletContext config = getServletContext(); ServletContextHandler.Context context = (ServletContextHandler.Context) config; _mimeTypes = context.getContextHandler().getMimeTypes(); _acceptRanges = getInitBoolean("acceptRanges", _acceptRanges); String t = getInitParameter("cacheControl"); if (t != null) _cacheControl = new ByteArrayBuffer(t); } /* ------------------------------------------------------------ */ private boolean getInitBoolean(String name, boolean dft) { String value = getInitParameter(name); if (value == null || value.length() == 0) return dft; return (value.startsWith("t") || value.startsWith("T") || value.startsWith("y") || value.startsWith("Y") || value.startsWith("1")); } /* ------------------------------------------------------------ */ /* Check modification date headers. */ protected boolean passConditionalHeaders(HttpServletRequest request, HttpServletResponse response, org.eclipse.jetty.util.resource.Resource resource, HttpContent content) throws IOException { if (!request.getMethod().equals(HttpMethods.HEAD)) { String ifms = request.getHeader(HttpHeaders.IF_MODIFIED_SINCE); if (ifms != null) { if (content != null) { Buffer mdlm = content.getLastModified(); if (mdlm != null) { if (ifms.equals(mdlm.toString())) { response.reset(); response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); response.flushBuffer(); return false; } } } long ifmsl = request.getDateHeader(HttpHeaders.IF_MODIFIED_SINCE); if (ifmsl != -1) { if (resource.lastModified() / 1000 <= ifmsl / 1000) { response.reset(); response.setStatus(HttpServletResponse.SC_NOT_MODIFIED); response.flushBuffer(); return false; } } } // Parse the if[un]modified dates and compare to resource long date = request.getDateHeader(HttpHeaders.IF_UNMODIFIED_SINCE); if (date != -1) { if (resource.lastModified() / 1000 > date / 1000) { response.sendError(HttpServletResponse.SC_PRECONDITION_FAILED); return false; } } } return true; } /* ------------------------------------------------------------ */ @SuppressWarnings("unchecked") protected void sendData(HttpServletRequest request, HttpServletResponse response, boolean include, org.eclipse.jetty.util.resource.Resource resource, HttpContent content, Enumeration reqRanges) throws IOException { long content_length = resource.length(); // Get the output stream (or writer) OutputStream out = null; try { out = response.getOutputStream(); } catch (IllegalStateException e) { out = new WriterOutputStream(response.getWriter()); } if (reqRanges == null || !reqRanges.hasMoreElements()) { // if there were no ranges, send entire entity if (include) { resource.writeTo(out, 0, content_length); } else { // See if a short direct method can be used? if (out instanceof HttpConnection.Output) { if (_cacheControl != null) { if (response instanceof Response) ((Response) response).getHttpFields().put(HttpHeaders.CACHE_CONTROL_BUFFER, _cacheControl); else response.setHeader(HttpHeaders.CACHE_CONTROL, _cacheControl.toString()); } ((HttpConnection.Output) out).sendContent(content); } else { // Write content normally writeHeaders(response, content, content_length); resource.writeTo(out, 0, content_length); } } } else { // Parse the satisfiable ranges List ranges = InclusiveByteRange.satisfiableRanges(reqRanges, true, content_length); // if there are no satisfiable ranges, send 416 response if (ranges == null || ranges.size() == 0) { writeHeaders(response, content, content_length); response.setStatus(HttpServletResponse.SC_REQUESTED_RANGE_NOT_SATISFIABLE); response.setHeader(HttpHeaders.CONTENT_RANGE, InclusiveByteRange.to416HeaderRangeString(content_length)); resource.writeTo(out, 0, content_length); 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); long singleLength = singleSatisfiableRange.getSize(content_length); writeHeaders(response, content, singleLength); response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); response.setHeader(HttpHeaders.CONTENT_RANGE, singleSatisfiableRange.toHeaderRangeString(content_length)); resource.writeTo(out, singleSatisfiableRange.getFirst(content_length), singleLength); return; } // multiple non-overlapping valid ranges cause a multipart // 216 response which does not require an overall // content-length header // writeHeaders(response, content, -1); String mimetype = content.getContentType().toString(); MultiPartOutputStream multi = new MultiPartOutputStream(out); response.setStatus(HttpServletResponse.SC_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.getHeader(HttpHeaders.REQUEST_RANGE) != null) ctp = "multipart/x-byteranges; boundary="; else ctp = "multipart/byteranges; boundary="; response.setContentType(ctp + multi.getBoundary()); InputStream in = resource.getInputStream(); long pos = 0; for (int i = 0; i < ranges.size(); i++) { InclusiveByteRange ibr = (InclusiveByteRange) ranges.get(i); String header = HttpHeaders.CONTENT_RANGE + ": " + ibr.toHeaderRangeString(content_length); multi.startPart(mimetype, new String[] { header }); long start = ibr.getFirst(content_length); long size = ibr.getSize(content_length); 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, multi, size); pos += size; } else // Handle cached resource (resource).writeTo(multi, start, size); } if (in != null) in.close(); multi.close(); } return; } /* ------------------------------------------------------------ */ protected void writeHeaders(HttpServletResponse response, HttpContent content, long count) throws IOException { if (content.getContentType() != null) response.setContentType(content.getContentType().toString()); if (response instanceof Response) { Response r = (Response) response; HttpFields fields = r.getHttpFields(); if (content.getLastModified() != null) fields.put(HttpHeaders.LAST_MODIFIED_BUFFER, content.getLastModified(), content.getResource().lastModified()); else if (content.getResource() != null) { long lml = content.getResource().lastModified(); if (lml != -1) fields.putDateField(HttpHeaders.LAST_MODIFIED_BUFFER, lml); } if (count != -1) r.setLongContentLength(count); if (_acceptRanges) fields.put(HttpHeaders.ACCEPT_RANGES_BUFFER, HttpHeaderValues.BYTES_BUFFER); if (_cacheControl != null) fields.put(HttpHeaders.CACHE_CONTROL_BUFFER, _cacheControl); } else { long lml = content.getResource().lastModified(); if (lml >= 0) response.setDateHeader(HttpHeaders.LAST_MODIFIED, lml); if (count != -1) { if (count < Integer.MAX_VALUE) response.setContentLength((int) count); else response.setHeader(HttpHeaders.CONTENT_LENGTH, TypeUtil.toString(count)); } if (_acceptRanges) response.setHeader(HttpHeaders.ACCEPT_RANGES, "bytes"); if (_cacheControl != null) response.setHeader(HttpHeaders.CACHE_CONTROL, _cacheControl.toString()); } } protected class UnCachedContent implements HttpContent { org.eclipse.jetty.util.resource.Resource _resource; String _contentType; public UnCachedContent(org.eclipse.jetty.util.resource.Resource resource) { _resource = resource; _contentType = null; } public UnCachedContent(org.eclipse.jetty.util.resource.Resource resource, String contentType) { _resource = resource; _contentType = contentType; } /* ------------------------------------------------------------ */ public Buffer getContentType() { if (_contentType != null) { return new ByteArrayBuffer(_contentType); } else return _mimeTypes.getMimeByExtension(_resource.toString()); } /* ------------------------------------------------------------ */ public Buffer getLastModified() { return null; } /* ------------------------------------------------------------ */ public Buffer getBuffer() { return null; } /* ------------------------------------------------------------ */ public long getContentLength() { return _resource.length(); } /* ------------------------------------------------------------ */ public InputStream getInputStream() throws IOException { return _resource.getInputStream(); } /* ------------------------------------------------------------ */ public org.eclipse.jetty.util.resource.Resource getResource() { return _resource; } /* ------------------------------------------------------------ */ public void release() { _resource.release(); _resource = null; } } }