package org.nutz.mvc.view;
import java.awt.image.BufferedImage;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Reader;
import java.io.Writer;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.nutz.img.Images;
import org.nutz.lang.Encoding;
import org.nutz.lang.Lang;
import org.nutz.lang.Streams;
import org.nutz.lang.Strings;
import org.nutz.log.Log;
import org.nutz.log.Logs;
import org.nutz.mvc.View;
/**
* 将数据对象直接写入 HTTP 响应
* <p>
* <h2>数据对象可以是如下类型:</h2>
* <ol>
* <li><b>null</b> - 什么都不做
* <li><b>File</b> - 文件,以下载方法返回,文件名将自动设置
* <li><b>byte[]</b> - 按二进制方式写入HTTP响应流
* <li><b>InputStream</b> - 按二进制方式写入响应流,并关闭 InputStream
* <li><b>char[]</b> - 按文本方式写入HTTP响应流
* <li><b>Reader</b> - 按文本方式写入HTTP响应流,并关闭 Reader
* <li><b>默认的</b> - 直接将对象 toString() 后按文本方式写入HTTP响应流
* </ol>
* <p>
* <h2>ContentType 支持几种缩写:</h2>
* <ul>
* <li><b>xml</b> - 表示 <b>text/xml</b>
* <li><b>html</b> - 表示 <b>text/html</b>
* <li><b>htm</b> - 表示 <b>text/html</b>
* <li><b>stream</b> - 表示 <b>application/octet-stream</b>
* <li><b>默认的</b>(即 '@Ok("raw")' ) - 将采用 <b>ContentType=text/plain</b>
* </ul>
*
* @author wendal(wendal1985@gmail.com)
* @author zozoh(zozohtnt@gmail.com)
*
*/
public class RawView implements View {
private static final Log log = Logs.get();
private static final int big4G = Integer.MAX_VALUE;
public static final boolean DISABLE_RANGE_DOWNLOAD = false; //禁用断点续传
protected String contentType;
public RawView(String contentType) {
if (Strings.isBlank(contentType))
contentType = "text/plain";
this.contentType = Strings.sNull(contentTypeMap.get(contentType.toLowerCase()), contentType);
}
public void render(HttpServletRequest req, HttpServletResponse resp, Object obj)
throws Throwable {
// 如果用户自行设置了,那就不要再设置了!
if (resp.getContentType() == null) {
if (obj != null && obj instanceof BufferedImage && "text/plain".equals(contentType)) {
contentType = contentTypeMap.get("png");
}
resp.setContentType(contentType);
}
if (obj == null)
return;
// 图片?难道是验证码?
if (obj instanceof BufferedImage) {
OutputStream out = resp.getOutputStream();
if (contentType.contains("png"))
ImageIO.write((BufferedImage)obj, "png", out);
// @see https://code.google.com/p/webm/source/browse/java/src/main/java/com/google/imageio/?repo=libwebp&name=sandbox%2Fpepijnve%2Fwebp-imageio#imageio%2Fwebp
else if (contentType.contains("webp"))
ImageIO.write((BufferedImage)obj, "webp", out);
else if (contentType.contains("jpg"))
Images.writeJpeg((BufferedImage)obj, out, 0.8f);
}
// 文件
else if (obj instanceof File) {
File file = (File) obj;
long fileSz = file.length();
if (log.isDebugEnabled())
log.debug("File downloading ... " + file.getAbsolutePath());
if (!file.exists() || file.isDirectory()) {
log.debug("File downloading ... Not Exist : " + file.getAbsolutePath());
resp.sendError(404);
return;
}
if (!resp.containsHeader("Content-Disposition")) {
String filename = URLEncoder.encode(file.getName(), Encoding.UTF8);
resp.setHeader("Content-Disposition", "attachment; filename=\"" + filename + "\"");
}
String rangeStr = req.getHeader("Range");
OutputStream out = resp.getOutputStream();
if (DISABLE_RANGE_DOWNLOAD || fileSz == 0 || (rangeStr == null || !rangeStr.startsWith("bytes=") || rangeStr.length() < "bytes=1".length())) {
resp.setHeader("Content-Length", "" + fileSz);
Streams.writeAndClose(out, Streams.fileIn(file));
} else {
// log.debug("Range Download : " + req.getHeader("Range"));
List<RangeRange> rs = new ArrayList<RawView.RangeRange>();
if (!parseRange(rangeStr, rs, fileSz)) {
resp.setStatus(416);
return;
}
// 暂时只实现了单range
if (rs.size() != 1) {
// TODO 完成多range的下载
log.info("multipart/byteranges is NOT support yet");
resp.setStatus(416);
return;
}
long totolSize = 0;
for (RangeRange rangeRange : rs) {
totolSize += (rangeRange.end - rangeRange.start);
}
resp.setStatus(206);
resp.setHeader("Content-Length", "" + totolSize);
resp.setHeader("Accept-Ranges", "bytes");
// 暂时只有单range,so,简单起见吧
RangeRange rangeRange = rs.get(0);
resp.setHeader("Content-Range", String.format("bytes %d-%d/%d", rangeRange.start, rangeRange.end -1, fileSz));
writeFileRange(file, out, rangeRange);
}
}
// 字节数组
else if (obj instanceof byte[]) {
resp.setHeader("Content-Length", "" + ((byte[]) obj).length);
OutputStream out = resp.getOutputStream();
Streams.writeAndClose(out, (byte[]) obj);
}
// 字符数组
else if (obj instanceof char[]) {
Writer writer = resp.getWriter();
writer.write((char[]) obj);
writer.flush();
}
// 文本流
else if (obj instanceof Reader) {
Streams.writeAndClose(resp.getWriter(), (Reader) obj);
}
// 二进制流
else if (obj instanceof InputStream) {
OutputStream out = resp.getOutputStream();
Streams.writeAndClose(out, (InputStream) obj);
}
// 普通对象
else {
byte[] data = String.valueOf(obj).getBytes(Encoding.UTF8);
resp.setHeader("Content-Length", "" + data.length);
OutputStream out = resp.getOutputStream();
Streams.writeAndClose(out, data);
}
}
protected static final Map<String, String> contentTypeMap = new HashMap<String, String>();
static {
contentTypeMap.put("xml", "application/xml");
contentTypeMap.put("html", "text/html");
contentTypeMap.put("htm", "text/html");
contentTypeMap.put("stream", "application/octet-stream");
contentTypeMap.put("js", "application/javascript");
contentTypeMap.put("json", "application/json");
contentTypeMap.put("jpg", "image/jpeg");
contentTypeMap.put("jpeg", "image/jpeg");
contentTypeMap.put("png", "image/png");
contentTypeMap.put("webp", "image/webp");
}
public static class RangeRange {
public RangeRange(long start, long end) {
this.start = start;
this.end = end;
}
long start;
long end = -1;
}
public static final boolean parseRange(String rangeStr, List<RangeRange> rs, long maxSize) {
rangeStr = rangeStr.substring("bytes=".length());
String[] ranges = rangeStr.split(",");
for (String range : ranges) {
if (range == null || Strings.isBlank(range)) {
log.debug("Bad Range --> " + rangeStr);
return false;
}
range = range.trim();
try {
// 首先是从后往前算的 bytes=-100 取最后100个字节
if (range.startsWith("-")) {
// 注意,这里是负数
long end = Long.parseLong(range);
long start = maxSize + end;
if (start < 0) {
log.debug("Bad Range --> " + rangeStr);
return false;
}
rs.add(new RangeRange(start, maxSize));
continue;
}
// 然后就是从开头到最后 bytes=1024-
if (range.endsWith("-")) {
// 注意,这里是负数
long start = Long.parseLong(range.substring(0, range.length() - 1));
if (start < 0) {
log.debug("Bad Range --> " + rangeStr);
return false;
}
rs.add(new RangeRange(start, maxSize));
continue;
}
// 哦也,是最标准的有头有尾?
if (range.contains("-")) {
String[] tmp = range.split("-");
long start = Long.parseLong(tmp[0]);
long end = Long.parseLong(tmp[1]);
if (start > end) {
log.debug("Bad Range --> " + rangeStr);
return false;
}
rs.add(new RangeRange(start, end + 1)); //这里需要调查一下
} else {
// 操!! 单个字节?!!
long start = Long.parseLong(range);
rs.add(new RangeRange(start, start+1));
}
}
catch (Throwable e) {
log.debug("Bad Range --> " + rangeStr, e);
return false;
}
}
return !rs.isEmpty();
}
public static void writeDownloadRange(DataInputStream in, OutputStream out, RangeRange rangeRange) {
try {
if (rangeRange.start > 0) {
long start = rangeRange.start;
while (start > 0) {
if (start > big4G) {
start -= big4G;
in.skipBytes(big4G);
} else {
in.skipBytes((int)start);
break;
}
}
}
byte[] buf = new byte[8192];
BufferedInputStream bin = new BufferedInputStream(in);
long pos = rangeRange.start;
int len = 0;
while (pos < rangeRange.end) {
if (rangeRange.end - pos > 8192) {
len = bin.read(buf);
} else {
len = bin.read(buf, 0 , (int)(rangeRange.end - pos));
}
if (len == -1) {//有时候,非常巧合的,文件已经读取完,就悲剧开始了...
break;
}
if (len > 0) {
out.write(buf, 0, len);
pos += len;
}
}
out.flush();
}
catch (Throwable e) {
throw Lang.wrapThrow(e);
}
}
public static void writeFileRange(File file, OutputStream out, RangeRange rangeRange) {
FileInputStream fin = null;
try {
fin = new FileInputStream(file);
DataInputStream in = new DataInputStream(fin);
writeDownloadRange(in, out, rangeRange);
}
catch (Throwable e) {
throw Lang.wrapThrow(e);
}
finally {
Streams.safeClose(fin);
}
}
}