This commit is contained in:
Looly 2020-04-25 18:50:48 +08:00
parent 2bcad6031d
commit e568b7896b
8 changed files with 147 additions and 49 deletions

View File

@ -6,8 +6,11 @@
## 5.3.3 (2020-04-25)
### 新特性
* 【core 】 ImgUtil.createImage支持背景透明issue#851@Github
* 【json 】 更改JSON转字符串时"</"被转义的规则为不转义issue#852@Github
### Bug修复
* 【json 】 修复JSON转字符串时</被转义问题
* 【http 】 修复URL中有`&amp;`导致的问题issue#850@Github
-------------------------------------------------------------------------------------------------------------

View File

@ -10,6 +10,7 @@ import cn.hutool.core.io.resource.Resource;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
@ -1291,28 +1292,65 @@ public class ImgUtil {
*
* @param str 文字
* @param font 字体{@link Font}
* @param backgroundColor 背景颜色
* @param fontColor 字体颜色
* @param backgroundColor 背景颜色默认透明
* @param fontColor 字体颜色默认黑色
* @param out 图片输出地
* @throws IORuntimeException IO异常
*/
public static void createImage(String str, Font font, Color backgroundColor, Color fontColor, ImageOutputStream out) throws IORuntimeException {
writePng(createImage(str, font, backgroundColor, fontColor, BufferedImage.TYPE_INT_ARGB), out);
}
/**
* 根据文字创建图片
*
* @param str 文字
* @param font 字体{@link Font}
* @param backgroundColor 背景颜色默认透明
* @param fontColor 字体颜色默认黑色
* @param imageType 图片类型{@link BufferedImage}
* @return 图片
* @throws IORuntimeException IO异常
*/
public static BufferedImage createImage(String str, Font font, Color backgroundColor, Color fontColor, int imageType) throws IORuntimeException {
// 获取font的样式应用在str上的整个矩形
Rectangle2D r = font.getStringBounds(str, new FontRenderContext(AffineTransform.getScaleInstance(1, 1), false, false));
int unitHeight = (int) Math.floor(r.getHeight());// 获取单个字符的高度
final Rectangle2D r = getRectangle(str, font);
// 获取单个字符的高度
int unitHeight = (int) Math.floor(r.getHeight());
// 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度
int width = (int) Math.round(r.getWidth()) + 1;
int height = unitHeight + 3;// 把单个字符的高度+3保证高度绝对能容纳字符串作为图片的高度
// 把单个字符的高度+3保证高度绝对能容纳字符串作为图片的高度
int height = unitHeight + 3;
// 创建图片
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
Graphics g = image.getGraphics();
g.setColor(backgroundColor);
g.fillRect(0, 0, width, height);// 先用背景色填充整张图片,也就是背景
g.setColor(fontColor);
final BufferedImage image = new BufferedImage(width, height, imageType);
final Graphics g = image.getGraphics();
if (null != backgroundColor) {
// 先用背景色填充整张图片,也就是背景
g.setColor(backgroundColor);
g.fillRect(0, 0, width, height);
}
g.setColor(ObjectUtil.defaultIfNull(fontColor, Color.BLACK));
g.setFont(font);// 设置画笔字体
g.drawString(str, 0, font.getSize());// 画出字符串
g.dispose();
writePng(image, out);
return image;
}
/**
* 获取font的样式应用在str上的整个矩形
*
* @param str 字符串必须非空
* @param font 字体必须非空
* @return {@link Rectangle2D}
* @since 5.3.3
*/
public static Rectangle2D getRectangle(String str, Font font) {
return font.getStringBounds(str,
new FontRenderContext(AffineTransform.getScaleInstance(1, 1),
false,
false));
}
/**

View File

@ -191,6 +191,14 @@ public class TableMap<K, V> implements Map<K, V>, Iterable<Map.Entry<K, V>>, Ser
};
}
@Override
public String toString() {
return "TableMap{" +
"keys=" + keys +
", values=" + values +
'}';
}
private static class Entry<K, V> implements Map.Entry<K, V> {
private final K key;

View File

@ -61,10 +61,10 @@ public class UrlQuery {
* @param queryMap 初始化的查询键值对
*/
public UrlQuery(Map<? extends CharSequence, ?> queryMap) {
if(MapUtil.isNotEmpty(queryMap)) {
if (MapUtil.isNotEmpty(queryMap)) {
query = new TableMap<>(queryMap.size());
addAll(queryMap);
} else{
} else {
query = new TableMap<>(MapUtil.DEFAULT_INITIAL_CAPACITY);
}
}
@ -88,7 +88,7 @@ public class UrlQuery {
* @return this
*/
public UrlQuery addAll(Map<? extends CharSequence, ?> queryMap) {
if(MapUtil.isNotEmpty(queryMap)) {
if (MapUtil.isNotEmpty(queryMap)) {
queryMap.forEach(this::add);
}
return this;
@ -122,34 +122,31 @@ public class UrlQuery {
char c; // 当前字符
for (i = 0; i < len; i++) {
c = queryStr.charAt(i);
if (c == '=') { // 键值对的分界点
if (null == name) {
// name可以是""
name = queryStr.substring(pos, i);
}
pos = i + 1;
} else if (c == '&') { // 参数对的分界点
if (null == name && pos != i) {
// 对于像&a&这类无参数值的字符串我们将name为a的值设为""
addParam(queryStr.substring(pos, i), StrUtil.EMPTY, charset);
} else if (name != null) {
switch (c) {
case '='://键和值的分界符
if (null == name) {
// name可以是""
name = queryStr.substring(pos, i);
// 开始位置从分节符后开始
pos = i + 1;
}
// =不作为分界符时按照普通字符对待
break;
case '&'://键值对之间的分界符
addParam(name, queryStr.substring(pos, i), charset);
name = null;
}
pos = i + 1;
if ("amp;".equals(queryStr.substring(i + 1, i + 5))) {
// issue#850@Github"&amp;"转义为"&"
i+=4;
}
// 开始位置从分节符后开始
pos = i + 1;
break;
}
}
// 处理结尾
if (pos != i) {
if (name == null) {
addParam(queryStr.substring(pos, i), StrUtil.EMPTY, charset);
} else {
addParam(name, queryStr.substring(pos, i), charset);
}
} else if (name != null) {
addParam(name, StrUtil.EMPTY, charset);
}
addParam(name, queryStr.substring(pos, i), charset);
return this;
}
@ -158,17 +155,18 @@ public class UrlQuery {
*
* @return 查询的Map只读
*/
public Map<CharSequence, CharSequence> getQueryMap(){
public Map<CharSequence, CharSequence> getQueryMap() {
return MapUtil.unmodifiable(this.query);
}
/**
* 获取查询值
*
* @param key
* @return
*/
public CharSequence get(CharSequence key){
if(MapUtil.isEmpty(this.query)){
public CharSequence get(CharSequence key) {
if (MapUtil.isEmpty(this.query)) {
return null;
}
return this.query.get(key);
@ -231,15 +229,25 @@ public class UrlQuery {
}
/**
* 将键值对加入到值为List类型的Map中
* 将键值对加入到值为List类型的Map中,情况如下
* <pre>
* 1key和value都不为null类似于 "a=1"或者"=1"直接put
* 2key不为nullvalue为null类似于 "a="值传""
* 3key为nullvalue不为null类似于 "1"
* 4key和value都为null忽略之比如&&
* </pre>
*
* @param name key
* @param value value
* @param key key为null则value作为key
* @param value value为null且key不为null时传入""
* @param charset 编码
*/
private void addParam(String name, String value, Charset charset) {
name = URLUtil.decode(name, charset);
value = URLUtil.decode(value, charset);
this.query.put(name, value);
private void addParam(String key, String value, Charset charset) {
if (null != key) {
final String actualKey = URLUtil.decode(key, charset);
this.query.put(actualKey, StrUtil.nullToEmpty(URLUtil.decode(value, charset)));
} else if (null != value) {
// name为空value作为namevalue赋值""
this.query.put(URLUtil.decode(value, charset), StrUtil.EMPTY);
}
}
}

View File

@ -170,4 +170,22 @@ public class UrlBuilderTest {
Assert.assertEquals("frag1", builder.getFragment());
}
@Test
public void weixinUrlTest(){
String urlStr = "https://mp.weixin.qq.com/s?" +
"__biz=MzI5NjkyNTIxMg==" +
"&amp;mid=100000465" +
"&amp;idx=1" +
"&amp;sn=1044c0d19723f74f04f4c1da34eefa35" +
"&amp;chksm=6cbda3a25bca2ab4516410db6ce6e125badaac2f8c5548ea6e18eab6dc3c5422cb8cbe1095f7";
final UrlBuilder builder = UrlBuilder.ofHttp(urlStr, CharsetUtil.CHARSET_UTF_8);
// 原URL中的&amp;替换为&value中的=被编码为%3D
Assert.assertEquals("https://mp.weixin.qq.com/s?" +
"__biz=MzI5NjkyNTIxMg%3D%3D" +
"&mid=100000465&idx=1" +
"&sn=1044c0d19723f74f04f4c1da34eefa35" +
"&chksm=6cbda3a25bca2ab4516410db6ce6e125badaac2f8c5548ea6e18eab6dc3c5422cb8cbe1095f7",
builder.toString());
}
}

View File

@ -14,6 +14,7 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
@ -159,12 +160,21 @@ public class HttpRequest extends HttpBase<HttpRequest> {
private SSLSocketFactory ssf;
/**
* 构造
* 构造URL编码默认使用UTF-8
*
* @param url URL
*/
public HttpRequest(String url) {
setUrl(url);
this(UrlBuilder.ofHttp(url, CharsetUtil.CHARSET_UTF_8));
}
/**
* 构造
*
* @param url {@link UrlBuilder}
*/
public HttpRequest(UrlBuilder url) {
this.url = url;
// 给定一个默认头信息
this.header(GlobalHeaders.INSTANCE.headers);
}

View File

@ -11,6 +11,10 @@ public class SimpleServerTest {
HttpUtil.createServer(8888)
// 设置默认根目录
.setRoot("d:/test")
// get数据测试返回请求的PATH
.addAction("/get", (request, response) ->
response.write(request.getURI().toString(), ContentType.TEXT_PLAIN.toString())
)
// 返回JSON数据测试
.addAction("/restTest", (request, response) ->
response.write("{\"id\": 1, \"msg\": \"OK\"}", ContentType.JSON.toString())

View File

@ -295,4 +295,13 @@ public class HttpUtilTest {
String mimeType = HttpUtil.getMimeType("aaa.aaa");
Assert.assertNull(mimeType);
}
@Test
@Ignore
public void getWeixinTest(){
// 测试特殊URL即URL中有&amp;情况是否请求正常
String url = "https://mp.weixin.qq.com/s?__biz=MzI5NjkyNTIxMg==&amp;mid=100000465&amp;idx=1&amp;sn=1044c0d19723f74f04f4c1da34eefa35&amp;chksm=6cbda3a25bca2ab4516410db6ce6e125badaac2f8c5548ea6e18eab6dc3c5422cb8cbe1095f7";
final String s = HttpUtil.get(url);
Console.log(s);
}
}