/**
* 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.brixcms.plugin.site.resource;
import org.apache.wicket.util.lang.Bytes;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
/**
* Responds stream with support for Content-Range header.
*
* @author Matej Knopp
*/
class Streamer {
private final long length;
private final InputStream inputStream;
private final String fileName;
private final boolean attachment;
private final HttpServletRequest request;
private final HttpServletResponse response;
public Streamer(long length, InputStream inputStream, String fileName, boolean attachment,
HttpServletRequest request, HttpServletResponse response) {
this.length = length;
this.inputStream = inputStream;
this.fileName = fileName;
this.response = response;
this.request = request;
this.attachment = attachment;
}
final static long maxContentLengthForDirectStreamInBytes = Bytes.kilobytes(512).bytes();
public void stream() {
Range range = parseRange(request.getHeader("Range"), length);
Long first = range.start;
Long last = range.end;
long contentLength = length;
boolean isFlushingRequired = false;
if(contentLength > maxContentLengthForDirectStreamInBytes) {
isFlushingRequired = true;
}
if (first != null && last != null) {
response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT);
response.addHeader("Content-Range", "bytes " + first + "-" + last + "/" + length);
contentLength = last - first + 1;
} else {
response.setStatus(HttpServletResponse.SC_OK);
first = 0l;
last = length - 1;
}
response.addHeader("Content-Length", "" + contentLength);
if (!attachment) {
response.addHeader("Content-Disposition", "inline; filename=\"" + fileName + "\";");
} else {
response.addHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\";");
}
response.addHeader("Accept-Range", "bytes");
/**
* should request be kept alive?
*/
String keepAlive = request.getHeader("Connection");
if(keepAlive != null && keepAlive.equalsIgnoreCase("keep-alive")) {
response.addHeader("Connection", "keep-alive");
} else {
response.addHeader("Connection", "close");
}
InputStream s = null;
try {
// flush headers - this is expected to be required for some versions of Firefox
response.flushBuffer();
s = new BufferedInputStream(inputStream);
s.skip(first);
final int bufferSize = (int) maxContentLengthForDirectStreamInBytes;
long left = contentLength;
while (left > 0) {
int howMuch = bufferSize;
if (howMuch > left) {
howMuch = (int) left;
}
byte[] buf = new byte[howMuch];
int numRead = s.read(buf);
response.getOutputStream().write(buf, 0, numRead);
//only call flushBuffer if partial content delivery is active - saves roundtrip
if(isFlushingRequired) {
response.flushBuffer();
}
if (numRead == -1) {
break;
}
left -= numRead;
}
} catch (Exception e) {
if (e.getClass().getName().contains("Eof")) {
// ignore
} else {
throw new RuntimeException(e);
}
} finally {
if (s != null) {
try {
s.close();
} catch (IOException ignore) {
}
}
}
}
private Range parseRange(String range, long length) {
if (isEmpty(range)) {
return new Range(null, null);
}
String p[] = range.split("=");
if (p.length != 1 && (p.length != 2 || !"bytes".equals(p[0]))) {
return new Range(0l, length - 1);
} else {
p = p[p.length - 1].split("-");
if (p.length == 1) {
p = new String[]{p[0], ""};
}
if (p.length != 2) {
return new Range(0l, length - 1);
}
if (isEmpty(p[0]) && isEmpty(p[1])) {
return new Range(0l, length - 1);
} else if (isEmpty(p[0])) {
return new Range(length - Long.valueOf(p[1]), length - 1);
} else if (isEmpty(p[1])) {
return new Range(Long.valueOf(p[0]), length - 1);
} else {
return new Range(Long.valueOf(p[0]), Long.valueOf(p[1]));
}
}
}
private boolean isEmpty(String s) {
return s == null || s.length() == 0;
}
private static class Range {
final Long start;
final Long end;
public Range(Long start, Long end) {
this.start = start;
this.end = end;
}
}
}