package com.lnwazg.httpkit.util; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.net.SocketException; import java.util.HashMap; import java.util.Map; import org.apache.commons.collections.MapUtils; import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream; import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.CharEncoding; import org.apache.commons.lang3.StringUtils; import com.lnwazg.httpkit.CommonResponse; import com.lnwazg.httpkit.Constants; import com.lnwazg.httpkit.HttpResponseCode; import com.lnwazg.httpkit.handler.route.Router; import com.lnwazg.httpkit.io.HttpReader; import com.lnwazg.httpkit.io.HttpWriter; import com.lnwazg.httpkit.io.IOInfo; import com.lnwazg.kit.compress.GzipBytesUtils; import com.lnwazg.kit.freemarker.FreeMkKit; import com.lnwazg.kit.http.DownloadKit; import com.lnwazg.kit.io.StreamUtils; import com.lnwazg.kit.map.Maps; import com.lnwazg.kit.mime.MimeMappingMap; import freemarker.template.Configuration; import freemarker.template.Template; import freemarker.template.TemplateException; /** * 工具类 * @author [email protected] * @version 2016年11月26日 */ public class RenderUtils { /** * 判断该类型是否是需要直接渲染的类型 * @author nan.li * @param contentType * @return */ private static boolean isDirectContentType(String contentType) { for (String ct : MimeMappingMap.directContentTypes) { if (contentType.indexOf(ct) != -1) { return true; } } return false; } /** * 统一消息处理<br> * 指定消息的扩展名<br> * 文本形式作统一压缩处理 * @author [email protected] * @param writer * @param code * @param msg * @param extension */ public static void renderMsg(IOInfo ioInfo, HttpResponseCode code, String msg, String extension) { HttpReader reader = ioInfo.getReader(); HttpWriter writer = ioInfo.getWriter(); boolean gzipOutput = reader.isSupportGzipOutput(); try { writer.writeResponseCode(Constants.VERSION, code); String contentType = "text/html;charset=utf-8"; if (StringUtils.isNotEmpty(extension)) { contentType = String.format("%s;charset=utf-8", MimeMappingMap.mimeMap.get(extension.toLowerCase())); } writer.writeContentType(contentType); if (gzipOutput) { writer.writeContentEncoding("gzip"); } writer.writeServer(Constants.SERVER_NAME); //根据启动配置输出额外的响应头 Map<String, String> extraResponseHeaders = ioInfo.getHttpServer().getExtraResponseHeaders(); if (MapUtils.isNotEmpty(extraResponseHeaders)) { for (String key : extraResponseHeaders.keySet()) { writer.writeHeader(key, extraResponseHeaders.get(key)); } } //将writer中预输出的内容输出 extraResponseHeaders = writer.getExtraResponseHeaders(); if (MapUtils.isNotEmpty(extraResponseHeaders)) { for (String key : extraResponseHeaders.keySet()) { writer.writeHeader(key, extraResponseHeaders.get(key)); } } // writer.writeHeader("Connection", "keep-alive"); writer.endHeader(); writer.flush(); if (msg != null) { if (gzipOutput) { //采用gzip格式压缩输出 byte[] bs = msg.getBytes(CharEncoding.UTF_8); bs = GzipBytesUtils.zip(bs);//将字节压缩输出 IOUtils.write(bs, writer.out); } else { //非压缩输出的方式 writer.write(msg); } } } catch (SocketException e) { //Software caused connection abort: socket write error //忽略该类异常 } catch (IOException e) { e.printStackTrace(); } finally { //在此处关闭了连接,那么长连接就无法保持了 //也就是说,请求头里面的 Connection: keep-alive 就被无视了 StreamUtils.close(writer, reader); //实践证明,只有在此处关闭连接,才能正常地向客户端输出响应。也就是说,本架构目前是不支持连接的keep-alive的 } } /** * 渲染输出流 * @author nan.li * @param ioInfo * @param code * @param inputStream * @param extension */ public static void renderStream(IOInfo ioInfo, HttpResponseCode code, InputStream inputStream, String extension) { byte[] bs; try { //先转为字节码数组,再输出 bs = IOUtils.toByteArray(inputStream); renderBytes(ioInfo, code, bs, extension); } catch (IOException e) { e.printStackTrace(); } } /** * 输出字节码 * @author nan.li * @param ioInfo * @param code * @param bytes * @param extension */ public static void renderBytes(IOInfo ioInfo, HttpResponseCode code, byte[] bytes, String extension) { HttpReader reader = ioInfo.getReader(); HttpWriter writer = ioInfo.getWriter(); boolean gzipOutput = reader.isSupportGzipOutput(); try { writer.writeResponseCode(Constants.VERSION, code); String contentType = "application/octet-stream";//默认是bin类型的输出MIME if (StringUtils.isNotEmpty(extension)) { contentType = String.format("%s;charset=utf-8", MimeMappingMap.mimeMap.get(extension.toLowerCase())); } writer.writeContentType(contentType); if (gzipOutput) { writer.writeContentEncoding("gzip"); } writer.writeServer(Constants.SERVER_NAME); //根据启动配置输出额外的响应头 Map<String, String> extraResponseHeaders = ioInfo.getHttpServer().getExtraResponseHeaders(); if (MapUtils.isNotEmpty(extraResponseHeaders)) { for (String key : extraResponseHeaders.keySet()) { writer.writeHeader(key, extraResponseHeaders.get(key)); } } //将writer中预输出的内容输出 extraResponseHeaders = writer.getExtraResponseHeaders(); if (MapUtils.isNotEmpty(extraResponseHeaders)) { for (String key : extraResponseHeaders.keySet()) { writer.writeHeader(key, extraResponseHeaders.get(key)); } } // writer.writeHeader("Connection", "keep-alive"); writer.endHeader(); writer.flush(); byte[] bs = bytes; if (gzipOutput) { //采用gzip格式压缩输出 bs = GzipBytesUtils.zip(bs);//将字节压缩输出 IOUtils.write(bs, writer.out); } else { IOUtils.write(bs, writer.out); } } catch (SocketException e) { //Software caused connection abort: socket write error //忽略该类异常 } catch (IOException e) { e.printStackTrace(); } finally { //在此处关闭了连接,那么长连接就无法保持了 //也就是说,请求头里面的 Connection: keep-alive 就被无视了 StreamUtils.close(writer, reader); //实践证明,只有在此处关闭连接,才能正常地向客户端输出响应。也就是说,本架构目前是不支持连接的keep-alive的 } } /** * 统一消息处理<br> * 采用默认的扩展名 * @author [email protected] * @param reader * @param writer * @param code * @param msg */ public static void renderHtml(IOInfo ioInfo, HttpResponseCode code, String msg) { renderMsg(ioInfo, code, msg, "html"); } /** * 消息处理 * @author [email protected] * @param reader * @param writer * @param code * @param file */ public static void renderFile(IOInfo ioInfo, HttpResponseCode code, File file) { if (file == null || !file.exists()) { CommonResponse.notFound().accept(ioInfo); return; } HttpReader reader = ioInfo.getReader(); HttpWriter writer = ioInfo.getWriter(); boolean gzipOutput = reader.isSupportGzipOutput(); //尝试获取待下载文件的扩展名 String fileName = file.getName(); String extension = FilenameUtils.getExtension(fileName); FileInputStream fileInputStream = null; GzipCompressorOutputStream gzipCompressorOutputStream = null; try { writer.writeResponseCode(Constants.VERSION, code); //默认情况下,将其作为文本类型返回(匹配不到MIME信息的时候) //对于chrome浏览器,其会正确判断实际类型的!所以,text/plain是最合适的类型! String contentType = "text/plain;charset=utf-8"; if (StringUtils.isNotEmpty(extension)) { //有扩展名,则尝试按照扩展名去查找 String mime = MimeMappingMap.mimeMap.get(extension.toLowerCase()); if (StringUtils.isNotEmpty(mime)) { //找到了,就按照实际的扩展名进行返回 contentType = String.format("%s", mime); } } writer.writeContentType(contentType); //如果是图片或视频,则直接在线预览,不要下载 if (isDirectContentType(contentType)) { //preview directly 直接在线预览 } else { //一旦加上了下载标识符,那么就无法在线预览了,只能作为附件进行下载了! String userAgent = reader.readHeader("User-Agent"); //给响应头输出下载文件的信息,方便浏览器客户端识别保存 String contentDisposition = DownloadKit.getContentDispositionByNameAndUserAgent(fileName, userAgent); if (StringUtils.isNotEmpty(contentDisposition)) { writer.writeHeader("content-disposition", contentDisposition); } } if (gzipOutput) { writer.writeContentEncoding("gzip"); } writer.writeServer(Constants.SERVER_NAME); writer.endHeader(); writer.flush(); fileInputStream = new FileInputStream(file); if (gzipOutput) { //字节输出的形式需要占用巨量内存,因此不推荐! // byte[] bs = IOUtils.toByteArray(fileInputStream); // bs = GzipBytesUtils.zip(bs);//将字节压缩输出 // IOUtils.write(bs, writer.out); gzipCompressorOutputStream = new GzipCompressorOutputStream(writer.out); IOUtils.copy(fileInputStream, gzipCompressorOutputStream); } else { IOUtils.copy(fileInputStream, writer.out); } } catch (SocketException e) { //ignore } catch (IOException e) { e.printStackTrace(); } finally { StreamUtils.close(fileInputStream, gzipCompressorOutputStream, writer, reader); } } /** * 渲染具体的某个资源 * @author nan.li * @param reader * @param writer * @param code * @param resourcePath static/ * @param subPath page/index.ftl * @param fileName index.ftl */ public static void renderResource(IOInfo ioInfo, HttpResponseCode code, String resourcePath, String subPath, String fileName) { HttpReader reader = ioInfo.getReader(); HttpWriter writer = ioInfo.getWriter(); boolean gzipOutput = reader.isSupportGzipOutput(); //尝试获取待下载文件的扩展名 String extension = FilenameUtils.getExtension(fileName); InputStream inputStream = null; GzipCompressorOutputStream gzipCompressorOutputStream = null; try { writer.writeResponseCode(Constants.VERSION, code); //add by linan for CORS writer.writeHeader("Access-Control-Allow-Credentials", true); String origin = reader.readHeader("Origin"); if (StringUtils.isNotEmpty(origin)) { writer.writeHeader("Access-Control-Allow-Origin", origin); } //默认情况下,将其作为文本类型返回(匹配不到MIME信息的时候) //对于chrome浏览器,其会正确判断实际类型的!所以,text/plain是最合适的类型! String contentType = "text/plain;charset=utf-8"; if (StringUtils.isNotEmpty(extension)) { //有扩展名,则尝试按照扩展名去查找 String mime = MimeMappingMap.mimeMap.get(extension.toLowerCase()); if (StringUtils.isNotEmpty(mime)) { //找到了,就按照实际的扩展名进行返回 contentType = String.format("%s", mime); } } writer.writeContentType(contentType); //如果是图片或视频,则直接在线预览,不要下载 if (isDirectContentType(contentType)) { //preview directly 直接在线预览 } else { //一旦加上了下载标识符,那么就无法在线预览了,只能作为附件进行下载了! String userAgent = reader.readHeader("User-Agent"); //给响应头输出下载文件的信息,方便浏览器客户端识别保存 String contentDisposition = DownloadKit.getContentDispositionByNameAndUserAgent(fileName, userAgent); if (StringUtils.isNotEmpty(contentDisposition)) { writer.writeHeader("content-disposition", contentDisposition); } } if (gzipOutput) { writer.writeContentEncoding("gzip"); } writer.writeServer(Constants.SERVER_NAME); writer.endHeader(); writer.flush(); String path = String.format("%s%s", resourcePath, subPath); inputStream = Router.class.getClassLoader().getResourceAsStream(path); if (gzipOutput) { gzipCompressorOutputStream = new GzipCompressorOutputStream(writer.out); IOUtils.copy(inputStream, gzipCompressorOutputStream); } else { IOUtils.copy(inputStream, writer.out); } } catch (IOException e) { e.printStackTrace(); } finally { StreamUtils.close(inputStream, gzipCompressorOutputStream, writer, reader); } } /** * freemarker配置信息表 */ static Map<String, Configuration> configureMap = new HashMap<>(); // System.out.println("============"); // System.out.println(ioInfo.getSocket()); // System.out.println(ioInfo.getSocket().getInetAddress()); // System.out.println(ioInfo.getSocket().getLocalAddress().getHostName()); // System.out.println(ioInfo.getSocket().getLocalAddress().getHostAddress()); // System.out.println(ioInfo.getSocket().getPort()); // System.out.println(ioInfo.getSocket().getLocalPort()); // System.out.println(ioInfo.getSocket().getLocalSocketAddress()); // System.out.println(ioInfo.getSocket().getRemoteSocketAddress()); // System.out.println("************\n\n"); /** * 渲染ftl文件 * @author nan.li * @param reader * @param writer * @param code * @param basePath /root/web/ * @param resourcePath static/ * @param subPath page/index.ftl * @throws TemplateException */ public static void renderFtl(IOInfo ioInfo, HttpResponseCode code, String basePath, String resourcePath, String subPath) { renderFtl(ioInfo, code, basePath, resourcePath, subPath, null); } /** * 渲染ftl文件 * @author nan.li * @param ioInfo * @param code * @param basePath /root/web/ * @param resourcePath static/ * @param subPath page/index.ftl * @param extraParamMap 额外的参数表 */ public static void renderFtl(IOInfo ioInfo, HttpResponseCode code, String basePath, String resourcePath, String subPath, Map<String, Object> extraParamMap) { //获取Freemarker配置对象 String key = basePath; if (!configureMap.containsKey(key)) { configureMap.put(key, FreeMkKit.getConfigurationByClassLoader(RenderUtils.class.getClassLoader(), "/" + resourcePath)); } Configuration configuration = configureMap.get(key); //根据名称去获取相应的模板对象(这个自身就有了缓存了,因此无需更改) Template template; try { template = configuration.getTemplate(subPath, CharEncoding.UTF_8); //公共参数 //这个参数不能从缓存中获取,因为ioInfo.getSocket().getLocalAddress().getHostName()是动态变化的! // Map<String, Object> map = Maps.asMap("base", // String.format("http://%s:%d%s", ioInfo.getSocket().getLocalAddress().getHostName(), ioInfo.getHttpServer().getPort(), basePath)); // Map<String, Object> map = Maps.asMap("base", // String.format("http://%s:%d%s", ioInfo.getSocket().getInetAddress().getHostAddress(), ioInfo.getHttpServer().getPort(), basePath)); //从请求头Host参数中获取浏览器访问的真实地址 Map<String, Object> map = Maps.asMap("base", String.format("http://%s%s", ioInfo.getReader().getHeader("Host"), basePath)); //如果额外参数表非空,则加上额外参数 if (Maps.isNotEmpty(extraParamMap)) { map.putAll(extraParamMap); } //进行参数转换 String msg = FreeMkKit.format(template, map); if (StringUtils.isNotEmpty(template.toString())) { //如果模板为空 if (StringUtils.isEmpty(msg)) { //转换异常,则通知内部服务器异常 CommonResponse.internalServerError().accept(ioInfo); } else { //转换正常 renderHtml(ioInfo, code, msg); } } else { //如果模板为空 renderHtml(ioInfo, code, ""); } } catch (IOException e) { e.printStackTrace(); } } /** * 重定向到某个地址 * @author [email protected] * @param reader * @param writer * @param location */ public static void sendRedirect(IOInfo ioInfo, String location) { // HttpReader reader = ioInfo.getReader(); HttpWriter writer = ioInfo.getWriter(); try { writer.writeResponseCode(Constants.VERSION, HttpResponseCode.MOVED_PERMANENTLY); writer.writeHeader("Location", location); writer.writeServer(Constants.SERVER_NAME); writer.endHeader(); writer.flush(); } catch (SocketException e) { } catch (IOException e) { e.printStackTrace(); } finally { StreamUtils.close(writer); } } }