Zhiqim Httpd即知启蒙WEB容器,是Zhiqim Framework面向WEB开发的多例服务,提供更简洁配置、积木式组件模块和天然的模型模板设计。

森中灵 最后提交于1月前 增加RedirectContext方便配置HTTP:80跳转到HTTPS:443
HttpSenderImpl.java14KB
/*
 * 版权所有 (C) 2015 知启蒙(ZHIQIM) 保留所有权利。[遇见知启蒙,邂逅框架梦]
 * 
 * https://zhiqim.org/project/zhiqim_framework/zhiqim_httpd.htm
 *
 * Zhiqim Httpd is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *          http://license.coscl.org.cn/MulanPSL2
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 */
package org.zhiqim.httpd;

import java.io.IOException;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map.Entry;

import org.zhiqim.httpd.constants.HttpStatus;
import org.zhiqim.kernel.util.Asserts;
import org.zhiqim.kernel.util.DateTimes;
import org.zhiqim.kernel.util.Strings;
import org.zhiqim.kernel.util.Urls;
import org.zhiqim.kernel.util.Validates;

/**
 * HTTP发送器
 *
 * @version v1.0.0 @author zouzhigang 2018-9-11 新建与整理
 */
public class HttpSenderImpl implements HttpSender
{
    private HttpHeaderAbs header;
    private HttpOutputStream output;
    
    private String version = _HTTP_1_1_;
    private int status = _200_OK_;
    private String reason = _200_DESC_;
    private String characterEncoding = _UTF_8_;
    
    //消息头
    private HashMap<String, String> headers;
    private StringBuilder headerBuffer;
    
    public HttpSenderImpl(HttpHeaderAbs header, boolean autoClose)
    {
        this.header = header;
        this.output = header.getOutputStream();
        this.output.setSender(this);
        
        this.headers = new HashMap<>(5);
        this.headers.put(_SERVER_, _ZHTTPD_);
        if (autoClose)
        {//是否执行完自动关闭
            this.headers.put(_CONNECTION_, _CLOSE_);
        }
        
        this.headerBuffer = new StringBuilder();
    }
    
    /** 是否已提交 */
    public boolean isCommitted()
    {
        return header.isCommitted();
    }
    
    /** 是否可编辑 */
    public boolean isEditable()
    {
        return header.isEditable();
    }
    
    /** 是否支持响应GZIP */
    public boolean isResponseGZip()
    {
        return header.isResponseGZip();
    }
    
    /** 提交流 */
    public void commit() throws IOException
    {
        if (header.isCommitted())
            return;
        
        header.setStep(_11_COMMITTED_);
        output.commit();
        
        if (isClose())
        {//是否提交后关闭连接
            header.close();
        }
    }
    
    /***********************************************************************/
    // 状态&版本&编码
    /***********************************************************************/
    
    public void setStatus(int code)
    {
        this.status = code;
        this.reason = HttpStatus.getStatusMsg(code);
    }
    
    public int getStatus()
    {
        return status;
    }
    
    public void setVersion(String version)
    {
        this.version = version;
    }   

    public String getReason()
    {
        return reason;
    }
    
    public String getCharacterEncoding()
    {
        return characterEncoding;
    }
    
    public long getFlushLength()
    {
        return output.getOutputLength();
    }
    
    /***********************************************************************/
    // 设置头部信息
    /***********************************************************************/
    
    /** 设置头部域 */
    public void setHeader(String key, Object value)
    {
        if (value == null)
            return;
        
        headers.put(key, Strings.valueOf(value));
    }
    
    /** 获取头部域 */
    public String getHeader(String key)
    {
        return headers.get(key);
    }
    
    /** 判断是否有头部域 */
    public boolean hasHeader(String key)
    {
        return headers.containsKey(key);
    }
    
    /** 是否关闭连接(没有连接属性和指明关闭的) */
    public boolean isClose()
    {
        String connection = getHeader(_CONNECTION_);
        return Validates.isEmptyBlank(connection) || _CLOSE_.equalsIgnoreCase(connection);
    }
    
    /** 设置头部日期格式域 */
    public void setDateHeader(String key, long value)
    {
        String date = DateTimes.getDateTimeHttp(value);
        setHeader(key, date);
    }
    
    /** 设置头部域,增加一个属性 */
    public void addHeader(String key, Object value)
    {
        if (value == null)
            return;
        
        String v = getHeader(key);
        if (v == null)
            setHeader(key, value);
        else
            setHeader(key, v + "," + Strings.valueOf(value));
    }
    
    /** 设置头部日期格式域,增加一个日期类型的属性 */
    public void addDateHeader(String key, long value)
    {
        String date = DateTimes.getDateTimeHttp(value);
        
        String v = getHeader(key);
        if (v == null)
            setHeader(key, date);
        else
            setHeader(key, v + "," + value);
    }
    
    /** 删除头部域 */
    public void removeHeader(String key)
    {
        headers.remove(key);
    }
    
    /** 添加复杂的头信息,如cookie等 */
    public void addMultiHeader(String key, String value)
    {
        headerBuffer.append(key).append(_COLON_).append(value).append(_BR_);
    }
    
    /**
     * 设置编码格式
     * 
     * @param encoding          编码格式
     */
    public void setCharacterEncoding(String encoding)
    {
        Asserts.assertNotEmptyBlank(encoding, "编码格式不允许为空白");
        this.characterEncoding = encoding;
    }
    
    /**
     * 设置内容类型,会增加默认的字符集到消息头中
     * 
     * @param contentType       内容类型
     */
    public void setContentType(String contentType)
    {
        Asserts.assertNotEmptyBlank(contentType, "内容类型不允许为空白");
        
        //在contentType中查找encoding,格式为:text/html; charset=UTF-8
        String mimeType = null;
        int i0 = contentType.indexOf(';');
        if (i0 == -1)
            mimeType = contentType;
        else
        {
            mimeType = contentType.substring(0, i0).trim();
            int i1 = contentType.indexOf("charset=", i0);
            if (i1>=0)
            {//有设置编码则修改为该编码
                characterEncoding = contentType.substring(i1 + 8);
            }
        }
        
        if (Validates.isEmptyBlank(characterEncoding))
            setHeader(_CONTENT_TYPE_, mimeType);
        else
            setHeader(_CONTENT_TYPE_, mimeType + "; charset="+characterEncoding);
    }
    
    /** 设置头部域中内容类型 */
    public void setContentTypeNoCharset(String contentType)
    {
        Asserts.assertNotEmptyBlank(contentType, "内容类型不允许为空白");
        Asserts.as(contentType.indexOf(";") == -1?null:"内容类型含字符集请调用setContentType方法");
        
        setHeader(_CONTENT_TYPE_, contentType);
    }
    
    /***********************************************************************/
    // 发送错误&内容
    /***********************************************************************/
    
    /**
     * 发送错误信息
     * 
     * @param code              编码
     * @throws IOException      异常
     */
    public void sendError(int code) throws IOException
    {
        sendError(code, null);
    }
    
    /**
     * 发送错误信息
     * 
     * @param code              编码
     * @param reason            原因
     * @throws IOException      异常
     */
    public void sendError(int code, String reason) throws IOException
    {
        Asserts.asState(isEditable()?null:"已提交不允许再提交");
        
        this.status = code;
        if (Validates.isEmptyBlank(reason))
            this.reason = HttpStatus.getStatusMsg(code);
        else
            this.reason = reason;
        
        this.clear();
        this.print(this.reason);
        this.commit();
    }
    
    /**
     * 发送错误信息,内容为HTML格式
     * 
     * @param code              响应码
     * @throws IOException      可能的异常
     */
    public void sendErrorHTML(int code) throws IOException
    {
        sendErrorHTML(code, null);
    }
    
    /**
     * 发送错误信息,内容为HTML格式
     * 
     * @param code              响应码
     * @param reason            响应原因
     * @throws IOException      可能的异常
     */
    public void sendErrorHTML(int code, String reason) throws IOException
    {
        Asserts.asState(isEditable()?null:"已提交不允许再提交");
        
        this.status = code;
        if (Validates.isEmptyBlank(reason))
            this.reason = HttpStatus.getStatusMsg(code);
        else
            this.reason = reason;
        
        //content
        StringBuilder strb = new StringBuilder();
        strb.append(_HTML_5_TYPE_).append(_BR_);
        strb.append(_HTML).append(_BR_);
        strb.append(_HEAD).append("<title>Error ").append(code).append("</title>").append(_HEAD).append(_BR_);
        strb.append(_BODY).append("<h2>Error ").append(reason).append("</h2>").append(BODY_).append(_BR_);
        strb.append(HTML_).append(_BR_);
        
        this.clear();
        this.print(this.reason);
        this.commit();
    }
    
    /**
     * 发送消息内容,如200,201等,含内容
     * 
     * @param code              响应码,可以是200表示成功
     * @param content           响应内容
     * @throws IOException      可能的异常
     */
    public void sendContent(int code, String content) throws IOException
    {
        Asserts.asState(isEditable()?null:"已提交不允许再提交");
        
        this.status = code;
        this.reason = HttpStatus.getStatusMsg(code);
        
        this.print(content);
        this.commit();
    }
    
    /**
     * 发送重定向信息
     * 
     * @param url               重定向URL
     * @throws IOException      异常
     */
    public void sendRedirect(String url) throws IOException
    {
        Asserts.asState(!isCommitted()?null:"已提交不允许再提交");
        
        this.status = _302_FOUND_;
        this.reason = _302_DESC_;
        this.setHeader(_LOCATION_, url);
        this.setHeader(_PROXY_CONNECTION_, _CLOSE_);
        this.commit();
    }
    
    /**
     * 只返回消息头,如304等,会清空内容
     * 
     * @param code              响应码,可以是200表示成功
     * @throws IOException      可能的异常
     */
    public void sendHeader(int code) throws IOException
    {
        Asserts.asState(isEditable()?null:"有提交数据时不允许更新提交内容");
        
        this.status = code;
        this.reason = HttpStatus.getStatusMsg(code);
        
        this.clear();
        this.commit();
    }
    
    /***********************************************************************/
    // write & print & commit
    /***********************************************************************/
    
    /** 获取输出流 */
    public OutputStream getOutputStream()
    {
        return output;
    }
    
    /** 写内容字节方式 */
    public void write(byte[] b) throws IOException
    {
        output.write(b);
    }
    
    /** 写内容加回车换行 */
    public void println(String str) throws IOException
    {
        output.write(str.getBytes(characterEncoding));
        output.write(_CRLF_);
    }
    
    /** 写回车换行 */
    public void println() throws IOException
    {
        output.write(_CRLF_);
    }
    
    /** 写内容,无回车换行 */
    public void print(String str) throws IOException
    {
        output.write(str.getBytes(characterEncoding));
    }
    
    /** 清理内容 */
    public void clear()
    {
        output.reset();
    }
    
    /** 刷新流(分块) */
    public void flush() throws IOException
    {
        if (isCommitted())
            return;
        
        output.flush();
    }
    
    public byte[] buildChunkedHeader(boolean chunked)
    {
        //1.1 写入标志
        if (chunked)
        {//分块
            setHeader(_TRANSFER_ENCODING_, _CHUNKED_);
        }
        else
        {//整块
            if (isResponseGZip() && output.getContentLength() > 256)
            {//如果支持gzip且内容长度大于256,则尝试压缩,压缩成功设置gzip头
                if (output.processGZipCompress())
                    addHeader(_CONTENT_ENCODING_, _ENCODING_GZIP_);
            }
            
            //整块的不管用户是否设置了内容长度,统一重设
            setHeader(_CONTENT_LENGTH_, output.getContentLength());
        }
        
        if (!hasHeader(_CONTENT_TYPE_))
        {//1.2 没有contentType设置成默认格式
            setContentType(_TEXT_HTML_UTF_8_);
        }
        
        //2.1 准备消息头
        StringBuilder strb = new StringBuilder();
        strb.append(version).append(" ").append(status).append(" ").append(Urls.encodeUTF8(reason)).append(_BR_);
        
        for (Entry<String, String> entry : headers.entrySet())
        {//2.2 响应消息头
            strb.append(entry.getKey()).append(_COLON_).append(entry.getValue()).append(_BR_);
        }
        
        //2.3 响应复杂的消息头
        strb.append(headerBuffer);
        
        //2.4 增加时间和结束标志
        strb.append(_DATE_).append(_COLON_).append(DateTimes.getDateTimeHttp()).append(_BR_);
        strb.append(_BR_);//头部结束标志
        
        return strb.toString().getBytes(_UTF_8_C_);
    }
    

    
    /***********************************************************************/
    // toString & destroy
    /***********************************************************************/
    
    public String toString()
    {
        StringBuilder strb = new StringBuilder();
        strb.append(version).append(" ").append(status).append(" ").append(Urls.encode(reason, characterEncoding)).append(_BR_);
        if (headers != null)
        {
            for (Entry<String, String> entry : headers.entrySet())
            {
                strb.append(entry.getKey()).append(_COLON_).append(entry.getValue()).append(_BR_);
            }
        }
        
        if (headerBuffer != null)
        {
            strb.append(headerBuffer.toString());
        }
        strb.append(_BR_);
        return strb.toString();
    }
    
    /** 销毁 */
    public void destroy()
    {
        if (headers != null)
        {
            headers.clear();
            headers = null;
        }
        
        if (headerBuffer != null)
        {
            headerBuffer.setLength(0);
            headerBuffer = null;
        }
        
        //引用置空
        output = null;
        header = null;
    }
}