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) ## 5.3.3 (2020-04-25)
### 新特性 ### 新特性
* 【core 】 ImgUtil.createImage支持背景透明issue#851@Github
* 【json 】 更改JSON转字符串时"</"被转义的规则为不转义issue#852@Github
### Bug修复 ### 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.lang.Assert;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.NumberUtil; import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@ -1291,28 +1292,65 @@ public class ImgUtil {
* *
* @param str 文字 * @param str 文字
* @param font 字体{@link Font} * @param font 字体{@link Font}
* @param backgroundColor 背景颜色 * @param backgroundColor 背景颜色默认透明
* @param fontColor 字体颜色 * @param fontColor 字体颜色默认黑色
* @param out 图片输出地 * @param out 图片输出地
* @throws IORuntimeException IO异常 * @throws IORuntimeException IO异常
*/ */
public static void createImage(String str, Font font, Color backgroundColor, Color fontColor, ImageOutputStream out) throws IORuntimeException { 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上的整个矩形 // 获取font的样式应用在str上的整个矩形
Rectangle2D r = font.getStringBounds(str, new FontRenderContext(AffineTransform.getScaleInstance(1, 1), false, false)); final Rectangle2D r = getRectangle(str, font);
int unitHeight = (int) Math.floor(r.getHeight());// 获取单个字符的高度 // 获取单个字符的高度
int unitHeight = (int) Math.floor(r.getHeight());
// 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度 // 获取整个str用了font样式的宽度这里用四舍五入后+1保证宽度绝对能容纳这个字符串作为图片的宽度
int width = (int) Math.round(r.getWidth()) + 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); final BufferedImage image = new BufferedImage(width, height, imageType);
Graphics g = image.getGraphics(); final Graphics g = image.getGraphics();
g.setColor(backgroundColor); if (null != backgroundColor) {
g.fillRect(0, 0, width, height);// 先用背景色填充整张图片,也就是背景 // 先用背景色填充整张图片,也就是背景
g.setColor(fontColor); g.setColor(backgroundColor);
g.fillRect(0, 0, width, height);
}
g.setColor(ObjectUtil.defaultIfNull(fontColor, Color.BLACK));
g.setFont(font);// 设置画笔字体 g.setFont(font);// 设置画笔字体
g.drawString(str, 0, font.getSize());// 画出字符串 g.drawString(str, 0, font.getSize());// 画出字符串
g.dispose(); 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 static class Entry<K, V> implements Map.Entry<K, V> {
private final K key; private final K key;

View File

@ -61,10 +61,10 @@ public class UrlQuery {
* @param queryMap 初始化的查询键值对 * @param queryMap 初始化的查询键值对
*/ */
public UrlQuery(Map<? extends CharSequence, ?> queryMap) { public UrlQuery(Map<? extends CharSequence, ?> queryMap) {
if(MapUtil.isNotEmpty(queryMap)) { if (MapUtil.isNotEmpty(queryMap)) {
query = new TableMap<>(queryMap.size()); query = new TableMap<>(queryMap.size());
addAll(queryMap); addAll(queryMap);
} else{ } else {
query = new TableMap<>(MapUtil.DEFAULT_INITIAL_CAPACITY); query = new TableMap<>(MapUtil.DEFAULT_INITIAL_CAPACITY);
} }
} }
@ -88,7 +88,7 @@ public class UrlQuery {
* @return this * @return this
*/ */
public UrlQuery addAll(Map<? extends CharSequence, ?> queryMap) { public UrlQuery addAll(Map<? extends CharSequence, ?> queryMap) {
if(MapUtil.isNotEmpty(queryMap)) { if (MapUtil.isNotEmpty(queryMap)) {
queryMap.forEach(this::add); queryMap.forEach(this::add);
} }
return this; return this;
@ -122,34 +122,31 @@ public class UrlQuery {
char c; // 当前字符 char c; // 当前字符
for (i = 0; i < len; i++) { for (i = 0; i < len; i++) {
c = queryStr.charAt(i); c = queryStr.charAt(i);
if (c == '=') { // 键值对的分界点 switch (c) {
if (null == name) { case '='://键和值的分界符
// name可以是"" if (null == name) {
name = queryStr.substring(pos, i); // name可以是""
} name = queryStr.substring(pos, i);
pos = i + 1; // 开始位置从分节符后开始
} else if (c == '&') { // 参数对的分界点 pos = i + 1;
if (null == name && pos != i) { }
// 对于像&a&这类无参数值的字符串我们将name为a的值设为"" // =不作为分界符时按照普通字符对待
addParam(queryStr.substring(pos, i), StrUtil.EMPTY, charset); break;
} else if (name != null) { case '&'://键值对之间的分界符
addParam(name, queryStr.substring(pos, i), charset); addParam(name, queryStr.substring(pos, i), charset);
name = null; name = null;
} if ("amp;".equals(queryStr.substring(i + 1, i + 5))) {
pos = i + 1; // issue#850@Github"&amp;"转义为"&"
i+=4;
}
// 开始位置从分节符后开始
pos = i + 1;
break;
} }
} }
// 处理结尾 // 处理结尾
if (pos != i) { addParam(name, queryStr.substring(pos, i), charset);
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);
}
return this; return this;
} }
@ -158,17 +155,18 @@ public class UrlQuery {
* *
* @return 查询的Map只读 * @return 查询的Map只读
*/ */
public Map<CharSequence, CharSequence> getQueryMap(){ public Map<CharSequence, CharSequence> getQueryMap() {
return MapUtil.unmodifiable(this.query); return MapUtil.unmodifiable(this.query);
} }
/** /**
* 获取查询值 * 获取查询值
*
* @param key * @param key
* @return * @return
*/ */
public CharSequence get(CharSequence key){ public CharSequence get(CharSequence key) {
if(MapUtil.isEmpty(this.query)){ if (MapUtil.isEmpty(this.query)) {
return null; return null;
} }
return this.query.get(key); 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 key key为null则value作为key
* @param value value * @param value value为null且key不为null时传入""
* @param charset 编码 * @param charset 编码
*/ */
private void addParam(String name, String value, Charset charset) { private void addParam(String key, String value, Charset charset) {
name = URLUtil.decode(name, charset); if (null != key) {
value = URLUtil.decode(value, charset); final String actualKey = URLUtil.decode(key, charset);
this.query.put(name, value); 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()); 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.map.MapUtil;
import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.CharsetUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@ -159,12 +160,21 @@ public class HttpRequest extends HttpBase<HttpRequest> {
private SSLSocketFactory ssf; private SSLSocketFactory ssf;
/** /**
* 构造 * 构造URL编码默认使用UTF-8
* *
* @param url URL * @param url URL
*/ */
public HttpRequest(String 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); this.header(GlobalHeaders.INSTANCE.headers);
} }

View File

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

View File

@ -295,4 +295,13 @@ public class HttpUtilTest {
String mimeType = HttpUtil.getMimeType("aaa.aaa"); String mimeType = HttpUtil.getMimeType("aaa.aaa");
Assert.assertNull(mimeType); 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);
}
} }