prepare 5.3.0

This commit is contained in:
Looly
2020-04-04 00:50:44 +08:00
parent 3921a568dd
commit 6b13cb5263
53 changed files with 619 additions and 331 deletions

View File

@@ -28,10 +28,18 @@ public enum ContentType {
* Rest请求XML编码
*/
XML("application/xml"),
/**
* text/plain编码
*/
TEXT_PLAIN("text/plain"),
/**
* Rest请求text/xml编码
*/
TEXT_XML("text/xml");
TEXT_XML("text/xml"),
/**
* text/html编码
*/
TEXT_HTML("text/html");
private String value;
@@ -39,9 +47,19 @@ public enum ContentType {
this.value = value;
}
/**
* 获取value值
*
* @return value值
* @since 5.2.6
*/
public String getValue() {
return value;
}
@Override
public String toString() {
return value;
return getValue();
}
/**
@@ -62,7 +80,7 @@ public enum ContentType {
* @since 4.1.5
*/
public static boolean isDefault(String contentType) {
return null == contentType || isFormUrlEncoed(contentType);
return null == contentType || isFormUrlEncode(contentType);
}
/**
@@ -71,7 +89,7 @@ public enum ContentType {
* @param contentType 内容类型
* @return 是否为application/x-www-form-urlencoded
*/
public static boolean isFormUrlEncoed(String contentType) {
public static boolean isFormUrlEncode(String contentType) {
return StrUtil.startWithIgnoreCase(contentType, FORM_URLENCODED.toString());
}

View File

@@ -2,6 +2,7 @@ package cn.hutool.http;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.FastByteArrayOutputStream;
import cn.hutool.core.io.FileUtil;
@@ -416,7 +417,7 @@ public class HttpUtil {
if (value instanceof Iterable) {
value = CollUtil.join((Iterable<?>) value, ",");
} else if (value instanceof Iterator) {
value = CollUtil.join((Iterator<?>) value, ",");
value = IterUtil.join((Iterator<?>) value, ",");
}
valueStr = Convert.toStr(value);
if (StrUtil.isNotEmpty(key)) {
@@ -435,30 +436,33 @@ public class HttpUtil {
*
* <p>注意此方法只能标准化整个URL并不适合于单独编码参数值</p>
*
* @param paramsStr url参数可以包含url本身
* @param urlWithParams url参数可以包含url本身,也可以单独参数
* @param charset 编码
* @return 编码后的url和参数
* @since 4.0.1
*/
public static String encodeParams(String paramsStr, Charset charset) {
if (StrUtil.isBlank(paramsStr)) {
public static String encodeParams(String urlWithParams, Charset charset) {
if (StrUtil.isBlank(urlWithParams)) {
return StrUtil.EMPTY;
}
String urlPart = null; // url部分不包括问号
String paramPart; // 参数部分
int pathEndPos = paramsStr.indexOf('?');
final int pathEndPos = urlWithParams.indexOf('?');
if (pathEndPos > -1) {
// url + 参数
urlPart = StrUtil.subPre(paramsStr, pathEndPos);
paramPart = StrUtil.subSuf(paramsStr, pathEndPos + 1);
urlPart = StrUtil.subPre(urlWithParams, pathEndPos);
paramPart = StrUtil.subSuf(urlWithParams, pathEndPos + 1);
if (StrUtil.isBlank(paramPart)) {
// 无参数返回url
return urlPart;
}
} else {
// 无URL
paramPart = paramsStr;
} else if(false == StrUtil.contains(urlWithParams, '=')){
// 无参数的URL
return urlWithParams;
}else {
// 无URL的参数
paramPart = urlWithParams;
}
paramPart = normalizeParams(paramPart, charset);
@@ -534,6 +538,18 @@ public class HttpUtil {
* @since 4.0.2
*/
public static HashMap<String, String> decodeParamMap(String paramsStr, String charset) {
return decodeParamMap(paramsStr, CharsetUtil.charset(charset));
}
/**
* 将URL参数解析为Map也可以解析Post中的键值对参数
*
* @param paramsStr 参数字符串或者带参数的Path
* @param charset 字符集
* @return 参数Map
* @since 5.2.6
*/
public static HashMap<String, String> decodeParamMap(String paramsStr, Charset charset) {
final Map<String, List<String>> paramsMap = decodeParams(paramsStr, charset);
final HashMap<String, String> result = MapUtil.newHashMap(paramsMap.size());
List<String> valueList;
@@ -552,6 +568,18 @@ public class HttpUtil {
* @return 参数Map
*/
public static Map<String, List<String>> decodeParams(String paramsStr, String charset) {
return decodeParams(paramsStr, CharsetUtil.charset(charset));
}
/**
* 将URL参数解析为Map也可以解析Post中的键值对参数
*
* @param paramsStr 参数字符串或者带参数的Path
* @param charset 字符集
* @return 参数Map
* @since 5.2.6
*/
public static Map<String, List<String>> decodeParams(String paramsStr, Charset charset) {
if (StrUtil.isBlank(paramsStr)) {
return Collections.emptyMap();
}
@@ -811,7 +839,7 @@ public class HttpUtil {
* @param value value
* @param charset 编码
*/
private static void addParam(Map<String, List<String>> params, String name, String value, String charset) {
private static void addParam(Map<String, List<String>> params, String name, String value, Charset charset) {
name = URLUtil.decode(name, charset);
value = URLUtil.decode(value, charset);
final List<String> values = params.computeIfAbsent(name, k -> new ArrayList<>(1));

View File

@@ -1,7 +1,10 @@
package cn.hutool.http.server;
import cn.hutool.core.util.CharsetUtil;
import com.sun.net.httpserver.HttpExchange;
import java.nio.charset.Charset;
/**
* HttpServer公用对象提供HttpExchange包装和公用方法
*
@@ -10,6 +13,8 @@ import com.sun.net.httpserver.HttpExchange;
*/
public class HttpServerBase {
final static Charset DEFAULT_CHARSET = CharsetUtil.CHARSET_UTF_8;
final HttpExchange httpExchange;
/**

View File

@@ -1,9 +1,13 @@
package cn.hutool.http.server;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.map.CaseInsensitiveMap;
import cn.hutool.core.map.multi.ListValueMap;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.net.multipart.MultipartFormData;
import cn.hutool.core.net.multipart.UploadSetting;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.StrUtil;
@@ -15,6 +19,7 @@ import cn.hutool.http.useragent.UserAgentUtil;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpCookie;
import java.net.URI;
@@ -32,6 +37,9 @@ import java.util.Map;
public class HttpServerRequest extends HttpServerBase {
private Map<String, HttpCookie> cookieCache;
private ListValueMap<String, String> paramsCache;
private Charset charsetCache;
private byte[] bodyCache;
/**
* 构造
@@ -159,9 +167,13 @@ public class HttpServerRequest extends HttpServerBase {
* @return 编码默认UTF-8
*/
public Charset getCharset() {
final String contentType = getContentType();
final String charsetStr = HttpUtil.getCharset(contentType);
return CharsetUtil.parse(charsetStr, CharsetUtil.CHARSET_UTF_8);
if(null == this.charsetCache){
final String contentType = getContentType();
final String charsetStr = HttpUtil.getCharset(contentType);
this.charsetCache = CharsetUtil.parse(charsetStr, DEFAULT_CHARSET);
}
return this.charsetCache;
}
/**
@@ -226,12 +238,20 @@ public class HttpServerRequest extends HttpServerBase {
}
/**
* 获取请求体的流,流中可以读取请求内容,包括请求表单数据或文件上传数据
* 是否为Multipart类型表单此类型表单用于文件上传
*
* @return
* @return 是否为Multipart类型表单此类型表单用于文件上传
*/
public InputStream getBodyStream() {
return this.httpExchange.getRequestBody();
public boolean isMultipart() {
if (false == isPostMethod()) {
return false;
}
final String contentType = getContentType();
if (StrUtil.isBlank(contentType)) {
return false;
}
return contentType.toLowerCase().startsWith("multipart/");
}
/**
@@ -251,31 +271,49 @@ public class HttpServerRequest extends HttpServerBase {
* @return 请求
*/
public String getBody(Charset charset) {
InputStream in = null;
try {
in = getBodyStream();
return IoUtil.read(in, charset);
} finally {
IoUtil.close(in);
}
return StrUtil.str(getBodyBytes(), charset);
}
/**
* 是否为Multipart类型表单此类型表单用于文件上传
* 获取body的bytes数组
*
* @return 是否为Multipart类型表单此类型表单用于文件上传
* @return body的bytes数组
*/
public boolean isMultipart() {
if (false == isPostMethod()) {
return false;
public byte[] getBodyBytes(){
if(null == this.bodyCache){
this.bodyCache = IoUtil.readBytes(getBodyStream(), true);
}
return this.bodyCache;
}
/**
* 获取请求体的流,流中可以读取请求内容,包括请求表单数据或文件上传数据
*
* @return 流
*/
public InputStream getBodyStream() {
return this.httpExchange.getRequestBody();
}
public ListValueMap<String, String> getParams() {
if (null == this.paramsCache) {
this.paramsCache = new ListValueMap<>();
final Charset charset = getCharset();
//解析URL中的参数
final String query = getQuery();
if(StrUtil.isNotBlank(query)){
this.paramsCache.putAll(HttpUtil.decodeParams(query, charset));
}
// 解析body中的参数
final String body = getBody();
if(StrUtil.isNotBlank(body)){
this.paramsCache.putAll(HttpUtil.decodeParams(body, charset));
}
}
final String contentType = getContentType();
if (StrUtil.isBlank(contentType)) {
return false;
}
return contentType.toLowerCase().startsWith("multipart/");
return this.paramsCache;
}
/**
@@ -332,4 +370,36 @@ public class HttpServerRequest extends HttpServerBase {
ip = this.httpExchange.getRemoteAddress().getHostName();
return NetUtil.getMultistageReverseProxyIp(ip);
}
/**
* 获得MultiPart表单内容多用于获得上传的文件 在同一次请求中,此方法只能被执行一次!
*
* @return MultipartFormData
* @throws IORuntimeException IO异常
* @since 5.3.0
*/
public MultipartFormData getMultipart() throws IORuntimeException {
return getMultipart(new UploadSetting());
}
/**
* 获得multipart/form-data 表单内容<br>
* 包括文件和普通表单数据<br>
* 在同一次请求中,此方法只能被执行一次!
*
* @param uploadSetting 上传文件的设定,包括最大文件大小、保存在内存的边界大小、临时目录、扩展名限定等
* @return MultiPart表单
* @throws IORuntimeException IO异常
* @since 5.3.0
*/
public MultipartFormData getMultipart(UploadSetting uploadSetting) throws IORuntimeException {
final MultipartFormData formData = new MultipartFormData(uploadSetting);
try {
formData.parseRequestStream(getBodyStream(), getCharset());
} catch (IOException e) {
throw new IORuntimeException(e);
}
return formData;
}
}

View File

@@ -3,12 +3,14 @@ package cn.hutool.http.server;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.http.ContentType;
import cn.hutool.http.Header;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.HttpUtil;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpExchange;
@@ -18,6 +20,8 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.nio.charset.Charset;
import java.util.List;
import java.util.Map;
@@ -28,6 +32,10 @@ import java.util.Map;
public class HttpServerResponse extends HttpServerBase {
private Charset charset;
/**
* 是否已经发送了Http状态码如果没有提前写出状态码
*/
private boolean isSendCode;
/**
* 构造
@@ -48,6 +56,38 @@ public class HttpServerResponse extends HttpServerBase {
return send(httpStatusCode, 0);
}
/**
* 发送成功状态码
*
* @return this
*/
public HttpServerResponse sendOk() {
return send(HttpStatus.HTTP_OK);
}
/**
* 发送404错误页
*
* @param content 错误页页面内容默认text/html类型
* @return this
*/
public HttpServerResponse send404(String content) {
return sendError(HttpStatus.HTTP_NOT_FOUND, content);
}
/**
* 发送错误页
*
* @param errorCode HTTP错误状态码见HttpStatus
* @param content 错误页页面内容默认text/html类型
* @return this
*/
public HttpServerResponse sendError(int errorCode, String content) {
send(errorCode);
setContentType(ContentType.TEXT_HTML.toString());
return write(content);
}
/**
* 发送HTTP状态码
*
@@ -56,11 +96,17 @@ public class HttpServerResponse extends HttpServerBase {
* @return this
*/
public HttpServerResponse send(int httpStatusCode, long bodyLength) {
if (this.isSendCode) {
throw new IORuntimeException("Http status code has been send!");
}
try {
this.httpExchange.sendResponseHeaders(httpStatusCode, bodyLength);
} catch (IOException e) {
throw new IORuntimeException(e);
}
this.isSendCode = true;
return this;
}
@@ -70,6 +116,9 @@ public class HttpServerResponse extends HttpServerBase {
* @return 响应头
*/
public Headers getHeaders() {
if (false == this.isSendCode) {
sendOk();
}
return this.httpExchange.getResponseHeaders();
}
@@ -141,7 +190,7 @@ public class HttpServerResponse extends HttpServerBase {
public HttpServerResponse setContentType(String contentType) {
if (null != contentType && null != this.charset) {
if (false == contentType.contains(";charset=")) {
contentType += ";charset=" + this.charset;
contentType = ContentType.build(contentType, this.charset);
}
}
@@ -169,12 +218,27 @@ public class HttpServerResponse extends HttpServerBase {
return this;
}
/**
* 设置属性
*
* @param name 属性名
* @param value 属性值
* @return this
*/
public HttpServerResponse setAttr(String name, Object value) {
this.httpExchange.setAttribute(name, value);
return this;
}
/**
* 获取响应数据流
*
* @return 响应数据流
*/
public OutputStream getOut() {
if (false == this.isSendCode) {
sendOk();
}
return this.httpExchange.getResponseBody();
}
@@ -183,8 +247,43 @@ public class HttpServerResponse extends HttpServerBase {
*
* @return 响应数据流
*/
public OutputStream getWriter() {
return this.httpExchange.getResponseBody();
public PrintWriter getWriter() {
final Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
return new PrintWriter(new OutputStreamWriter(getOut(), charset));
}
/**
* 写出数据到客户端
*
* @param data 数据
* @return this
*/
public HttpServerResponse write(String data, String contentType) {
setContentType(contentType);
return write(data);
}
/**
* 写出数据到客户端
*
* @param data 数据
* @return this
*/
public HttpServerResponse write(String data) {
final Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
return write(StrUtil.bytes(data, charset));
}
/**
* 写出数据到客户端
*
* @param data 数据
* @param contentType 返回的类型
* @return this
*/
public HttpServerResponse write(byte[] data, String contentType) {
setContentType(contentType);
return write(data);
}
/**
@@ -194,8 +293,19 @@ public class HttpServerResponse extends HttpServerBase {
* @return this
*/
public HttpServerResponse write(byte[] data) {
write(new ByteArrayInputStream(data));
return this;
return write(new ByteArrayInputStream(data));
}
/**
* 返回数据给客户端
*
* @param in 需要返回客户端的内容
* @param contentType 返回的类型
* @since 5.2.6
*/
public HttpServerResponse write(InputStream in, String contentType) {
setContentType(contentType);
return write(in);
}
/**
@@ -236,7 +346,7 @@ public class HttpServerResponse extends HttpServerBase {
}
/**
* 返回数据给客户端
* 返回文件数据给客户端(文件下载)
*
* @param in 需要返回客户端的内容
* @param contentType 返回的类型
@@ -244,7 +354,7 @@ public class HttpServerResponse extends HttpServerBase {
* @since 5.2.6
*/
public void write(InputStream in, String contentType, String fileName) {
final Charset charset = ObjectUtil.defaultIfNull(this.charset, CharsetUtil.CHARSET_UTF_8);
final Charset charset = ObjectUtil.defaultIfNull(this.charset, DEFAULT_CHARSET);
setHeader("Content-Disposition", StrUtil.format("attachment;filename={}", URLUtil.encode(fileName, charset)));
setContentType(contentType);
write(in);

View File

@@ -1,9 +1,10 @@
package cn.hutool.http.server;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.lang.Console;
import cn.hutool.http.server.action.Action;
import cn.hutool.http.server.action.RootAction;
import cn.hutool.http.server.handler.ActionHandler;
import cn.hutool.http.server.handler.RootHandler;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
@@ -72,7 +73,7 @@ public class SimpleServer {
* @return this
*/
public SimpleServer setRoot(String root) {
return addHandler("/", new RootHandler(root));
return addAction("/", new RootAction(root));
}
/**
@@ -119,6 +120,8 @@ public class SimpleServer {
* 启动Http服务器启动后会阻塞当前线程
*/
public void start() {
final InetSocketAddress address = getAddress();
Console.log("Hutool Simple Http Server listen on 【{}:{}】", address.getHostName(), address.getPort());
this.server.start();
}
}

View File

@@ -0,0 +1,62 @@
package cn.hutool.http.server.action;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.http.server.HttpServerRequest;
import cn.hutool.http.server.HttpServerResponse;
import java.io.File;
import java.util.List;
/**
* 默认的处理器通过解析用户传入的path找到网页根目录下对应文件后返回
*
* @author looly
* @since 5.2.6
*/
public class RootAction implements Action {
public static final String DEFAULT_INDEX_FILE_NAME = "index.html";
private final String rootDir;
private List<String> indexFileNames;
/**
* 构造
*
* @param rootDir 网页根目录
*/
public RootAction(String rootDir) {
this(rootDir, DEFAULT_INDEX_FILE_NAME);
}
/**
* 构造
*
* @param rootDir 网页根目录
* @param indexFileNames 主页文件名列表
*/
public RootAction(String rootDir, String... indexFileNames) {
this.rootDir = rootDir;
this.indexFileNames = CollUtil.toList(indexFileNames);
}
@Override
public void doAction(HttpServerRequest request, HttpServerResponse response) {
final String path = request.getPath();
File file = FileUtil.file(rootDir, path);
if (file.exists()) {
if (file.isDirectory()) {
for (String indexFileName : indexFileNames) {
//默认读取主页
file = FileUtil.file(file, indexFileName);
if (file.exists() && file.isFile()) {
response.write(file);
}
}
}
}
response.send404("404 Not Found !");
}
}

View File

@@ -27,6 +27,9 @@ public class ActionHandler implements HttpHandler {
@Override
public void handle(HttpExchange httpExchange) {
action.doAction(new HttpServerRequest(httpExchange), new HttpServerResponse(httpExchange));
action.doAction(
new HttpServerRequest(httpExchange),
new HttpServerResponse(httpExchange)
);
}
}

View File

@@ -1,91 +0,0 @@
package cn.hutool.http.server.handler;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.http.Header;
import cn.hutool.http.HttpStatus;
import cn.hutool.http.HttpUtil;
import com.sun.net.httpserver.HttpExchange;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
/**
* 请求处理器相关工具类
*
* @since 5.2.6
*/
public class HandlerUtil {
/**
* 返回404页面
*
* @param httpExchange HttpExchange
* @param content 要发送的404页面内容
* @throws IOException IO异常
*/
public static void send404(HttpExchange httpExchange, String content) throws IOException {
if (null == httpExchange) {
return;
}
if (null == content) {
content = "404 Not Found !";
}
httpExchange.sendResponseHeaders(HttpStatus.HTTP_NOT_FOUND, 0);
try (OutputStream out = httpExchange.getResponseBody()) {
IoUtil.writeUtf8(out, false, content);
}
}
/**
* 返回文件
*
* @param httpExchange HttpExchange
* @param file 要发送的文件
* @throws IOException IO异常
*/
public static void sendFile(HttpExchange httpExchange, File file) throws IOException {
if (ArrayUtil.hasNull(httpExchange, file)) {
return;
}
addHeader(httpExchange,
Header.CONTENT_TYPE.toString(),
HttpUtil.getMimeType(file.getName(), "text/html"));
httpExchange.sendResponseHeaders(HttpStatus.HTTP_OK, 0);
try (OutputStream out = httpExchange.getResponseBody()) {
FileUtil.writeToStream(file, out);
}
}
/**
* 增加响应头信息
*
* @param httpExchange HttpExchange
* @param header 头名
* @param value 头值
*/
public static void addHeader(HttpExchange httpExchange, String header, String value) {
if (ArrayUtil.hasEmpty(httpExchange, header)) {
return;
}
httpExchange.getResponseHeaders().add(header, value);
}
/**
* 获取响应头信息
*
* @param httpExchange HttpExchange
* @param header 头名
* @return 值不存在返回null
*/
public static String getHeader(HttpExchange httpExchange, String header) {
if (ArrayUtil.hasEmpty(httpExchange, header)) {
return null;
}
return httpExchange.getRequestHeaders().getFirst(header);
}
}

View File

@@ -1,45 +0,0 @@
package cn.hutool.http.server.handler;
import cn.hutool.core.io.FileUtil;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import java.io.File;
import java.io.IOException;
import java.net.URI;
/**
* 默认的处理器通过解析用户传入的path找到网页根目录下对应文件后返回
*
* @author looly
* @since 5.2.6
*/
public class RootHandler implements HttpHandler {
private final String rootDir;
/**
* 构造
*
* @param rootDir 网页根目录
*/
public RootHandler(String rootDir) {
this.rootDir = rootDir;
}
@Override
public void handle(HttpExchange httpExchange) throws IOException {
final URI uri = httpExchange.getRequestURI();
File file = FileUtil.file(rootDir, uri.getPath());
if (file.exists()) {
if (file.isDirectory()) {
//默认读取主页
file = FileUtil.file(file, "index.html");
}
HandlerUtil.sendFile(httpExchange, file);
}
// 文件未找到
HandlerUtil.send404(httpExchange, null);
}
}