blob: cbcc6883046758c82abb75a4373bb762edb2e0ce [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.apache.ace.deployment.servlet;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
/**
* Wraps a HttpServletResponse to add byte range support allowing client to request partial content.
* <p>
* Note: this implementation does <em>not</em> strictly follow the recommendations made in RFC 2616! For example, it
* does not ever send the "Content-Length" header, nor provide a "Resource-Length" value at any time. This is an
* "optimization" we've added for ACE, as we do not know the content/resource length in advance, nor are willing to
* sacrifice performance to get knowledge about this.
* </p>
*
* @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
*/
public class ContentRangeResponseWrapper extends HttpServletResponseWrapper {
private final long m_requestFirstBytePos;
private final long m_requestLastBytePos;
private final HttpServletResponse m_response;
public ContentRangeResponseWrapper(HttpServletRequest request, HttpServletResponse response) throws IOException {
super(response);
assert request != null;
assert response != null;
m_response = response;
long[] requestRange = getRequestRange(request);
if (requestRange != null) {
m_requestFirstBytePos = requestRange[0];
m_requestLastBytePos = requestRange[1];
}
else {
m_requestFirstBytePos = 0;
m_requestLastBytePos = Long.MAX_VALUE;
}
boolean streamAll = (m_requestFirstBytePos == 0) && (m_requestLastBytePos == Long.MAX_VALUE);
m_response.setHeader("Accept-Ranges", "bytes");
if (m_requestFirstBytePos < m_requestLastBytePos) {
if (streamAll) {
m_response.setStatus(SC_OK);
}
else {
m_response.setStatus(SC_PARTIAL_CONTENT);
StringBuilder cr = new StringBuilder("bytes ").append(m_requestFirstBytePos).append('-');
if (m_requestLastBytePos > 0 && m_requestLastBytePos < Long.MAX_VALUE) {
cr.append(m_requestLastBytePos);
}
cr.append("/*"); // unknown instance length...
m_response.setHeader("Content-Range", cr.toString());
}
}
else {
m_response.setStatus(SC_REQUESTED_RANGE_NOT_SATISFIABLE);
}
}
@Override
public ServletOutputStream getOutputStream() throws IOException {
final ServletOutputStream delegate = m_response.getOutputStream();
return new ServletOutputStream() {
/** keeps the actual number of bytes written by our caller... */
private final AtomicLong m_written = new AtomicLong(0L);
@Override
public void write(int b) throws IOException {
// We only need to buffer the relevant bytes since we keep track of the instance length in the counter.
long written = m_written.getAndIncrement();
if (written >= m_requestFirstBytePos && written <= m_requestLastBytePos) {
delegate.write(b);
}
}
@Override
public void close() throws IOException {
delegate.close();
}
@Override
public boolean isReady() {
return delegate.isReady();
}
@Override
public void setWriteListener(WriteListener l) {
delegate.setWriteListener(l);
}
};
}
/**
* Pattern that matches valid Range request headers. Note that the lastBytePos group is optional. If it is empty
* this indicates all remaining bytes from startBytePos is requested.
*/
private static final Pattern RANGE_REQUEST_PATTERN = Pattern.compile("^bytes=(\\d+)-(\\d+)?$");
/**
* Extracts and validates the Range header from a request. If the header is found but is syntactically invalid it
* will be ignored as required by specification.
*
* @param request
* the request to use
* @return a long array with two elements (firstBytePos and lastBytePos), or <code>null</code> if no valid Range
* header was found.
*/
private static long[] getRequestRange(HttpServletRequest request) {
String rangeHeader = request.getHeader("Range");
if (rangeHeader != null) {
Matcher rangeMatcher = RANGE_REQUEST_PATTERN.matcher(rangeHeader);
if (rangeMatcher.find()) {
long firstBytePos = Long.parseLong(rangeMatcher.group(1));
long lastBytePos = (rangeMatcher.group(2) != null) ? Long.parseLong(rangeMatcher.group(2)) : Long.MAX_VALUE;
if (lastBytePos >= firstBytePos) {
return new long[] { firstBytePos, lastBytePos };
}
}
}
return null;
}
}