Looly 5 years ago
parent
commit
e568b7896b

+ 4 - 1
CHANGELOG.md

@@ -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)
 
 -------------------------------------------------------------------------------------------------------------
 

+ 49 - 11
hutool-core/src/main/java/cn/hutool/core/img/ImgUtil.java

@@ -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));
 	}
 
 	/**

+ 8 - 0
hutool-core/src/main/java/cn/hutool/core/map/TableMap.java

@@ -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;

+ 43 - 35
hutool-core/src/main/java/cn/hutool/core/net/url/UrlQuery.java

@@ -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>
+	 *     1、key和value都不为null,类似于 "a=1"或者"=1",直接put
+	 *     2、key不为null,value为null,类似于 "a=",值传""
+	 *     3、key为null,value不为null,类似于 "1"
+	 *     4、key和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作为name,value赋值""
+			this.query.put(URLUtil.decode(value, charset), StrUtil.EMPTY);
+		}
 	}
 }

+ 18 - 0
hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java

@@ -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());
+	}
 }

+ 12 - 2
hutool-http/src/main/java/cn/hutool/http/HttpRequest.java

@@ -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);
 	}

+ 4 - 0
hutool-http/src/test/java/cn/hutool/http/server/SimpleServerTest.java

@@ -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())

+ 9 - 0
hutool-http/src/test/java/cn/hutool/http/test/HttpUtilTest.java

@@ -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);
+	}
 }