From 189f2852596b5532f3e5e320782507bbdaeb4504 Mon Sep 17 00:00:00 2001 From: Daniel Qian Date: Thu, 21 Aug 2014 22:13:13 +0800 Subject: [PATCH] add menu support --- README.md | 3 + ...nfigProvider.java => WxConfigStorage.java} | 2 +- .../java/chanjarster/weixin/api/WxConsts.java | 17 +-- ...ider.java => WxInMemoryConfigStorage.java} | 24 +++- .../weixin/api/WxMessageHandler.java | 5 + .../weixin/api/WxMessageRouter.java | 38 ++++-- .../chanjarster/weixin/api/WxService.java | 35 ++++-- .../chanjarster/weixin/api/WxServiceImpl.java | 91 +++++++------- .../java/chanjarster/weixin/bean/WxError.java | 109 +++++++++++++++- .../chanjarster/weixin/bean/WxXmlMessage.java | 10 +- .../util/Utf8StringResponseHandler.java | 32 +++++ .../util/WxCustomMessageGsonAdapter.java | 12 +- .../weixin/util/WxMenuGsonAdapter.java | 8 +- .../weixin/util/XmlTransformer.java | 6 +- .../weixin/api/WxMessageRouterTest.java | 44 ++++++- .../chanjarster/weixin/api/WxServiceTest.java | 117 +++++++++++++----- .../weixin/bean/WxCustomMessageTest.java | 13 +- .../chanjarster/weixin/bean/WxMenuTest.java | 19 ++- .../weixin/bean/WxXmlMessageTest.java | 4 +- 19 files changed, 444 insertions(+), 145 deletions(-) rename src/main/java/chanjarster/weixin/api/{WxConfigProvider.java => WxConfigStorage.java} (92%) rename src/main/java/chanjarster/weixin/api/{WxMemoryConfigProvider.java => WxInMemoryConfigStorage.java} (61%) create mode 100644 src/main/java/chanjarster/weixin/util/Utf8StringResponseHandler.java diff --git a/README.md b/README.md index f0e51261..88ba5f7c 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,6 @@ weixin-java =========== 微信java开发工具集 + +## 执行测试 +将 ''src/test/resources/test-config.sample.xml'' 改成 ''test-config.xml'' 并设置appId, secret, 一个过期的accessToken diff --git a/src/main/java/chanjarster/weixin/api/WxConfigProvider.java b/src/main/java/chanjarster/weixin/api/WxConfigStorage.java similarity index 92% rename from src/main/java/chanjarster/weixin/api/WxConfigProvider.java rename to src/main/java/chanjarster/weixin/api/WxConfigStorage.java index 059a2755..16a6c128 100644 --- a/src/main/java/chanjarster/weixin/api/WxConfigProvider.java +++ b/src/main/java/chanjarster/weixin/api/WxConfigStorage.java @@ -7,7 +7,7 @@ import chanjarster.weixin.bean.WxAccessToken; * @author chanjarster * */ -public interface WxConfigProvider { +public interface WxConfigStorage { public void updateAccessToken(WxAccessToken accessToken); diff --git a/src/main/java/chanjarster/weixin/api/WxConsts.java b/src/main/java/chanjarster/weixin/api/WxConsts.java index d0176177..a6e77b57 100644 --- a/src/main/java/chanjarster/weixin/api/WxConsts.java +++ b/src/main/java/chanjarster/weixin/api/WxConsts.java @@ -2,13 +2,14 @@ package chanjarster.weixin.api; public class WxConsts { - public static final String TEXT = "text"; - public static final String IMAGE = "image"; - public static final String VOICE = "voice"; - public static final String MUSIC = "music"; - public static final String VIDEO = "video"; - public static final String NEWS = "news"; - public static final String LOCATION = "location"; - public static final String LINK = "link"; + public static final String MSG_TEXT = "text"; + public static final String MSG_IMAGE = "image"; + public static final String MSG_VOICE = "voice"; + public static final String MSG_MUSIC = "music"; + public static final String MSG_VIDEO = "video"; + public static final String MSG_NEWS = "news"; + public static final String MSG_LOCATION = "location"; + public static final String MSG_LINK = "link"; + } diff --git a/src/main/java/chanjarster/weixin/api/WxMemoryConfigProvider.java b/src/main/java/chanjarster/weixin/api/WxInMemoryConfigStorage.java similarity index 61% rename from src/main/java/chanjarster/weixin/api/WxMemoryConfigProvider.java rename to src/main/java/chanjarster/weixin/api/WxInMemoryConfigStorage.java index c93a35ac..5329a213 100644 --- a/src/main/java/chanjarster/weixin/api/WxMemoryConfigProvider.java +++ b/src/main/java/chanjarster/weixin/api/WxInMemoryConfigStorage.java @@ -3,11 +3,11 @@ package chanjarster.weixin.api; import chanjarster.weixin.bean.WxAccessToken; /** - * 基于内存的微信配置provider + * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化 * @author chanjarster * */ -public class WxMemoryConfigProvider implements WxConfigProvider { +public class WxInMemoryConfigStorage implements WxConfigStorage { protected String appId; protected String secret; @@ -44,4 +44,24 @@ public class WxMemoryConfigProvider implements WxConfigProvider { return this.expiresIn; } + public void setAppId(String appId) { + this.appId = appId; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public void setToken(String token) { + this.token = token; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public void setExpiresIn(int expiresIn) { + this.expiresIn = expiresIn; + } + } diff --git a/src/main/java/chanjarster/weixin/api/WxMessageHandler.java b/src/main/java/chanjarster/weixin/api/WxMessageHandler.java index 0a2ac74e..9f985e2c 100644 --- a/src/main/java/chanjarster/weixin/api/WxMessageHandler.java +++ b/src/main/java/chanjarster/weixin/api/WxMessageHandler.java @@ -11,6 +11,11 @@ import chanjarster.weixin.bean.WxXmlMessage; */ public interface WxMessageHandler { + /** + * + * @param wxMessage + * @param context 上下文,如果handler或interceptor之间有信息要传递,可以用这个 + */ public void handle(WxXmlMessage wxMessage, Map context); } diff --git a/src/main/java/chanjarster/weixin/api/WxMessageRouter.java b/src/main/java/chanjarster/weixin/api/WxMessageRouter.java index e66aba8d..c256a228 100644 --- a/src/main/java/chanjarster/weixin/api/WxMessageRouter.java +++ b/src/main/java/chanjarster/weixin/api/WxMessageRouter.java @@ -5,10 +5,32 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import chanjarster.weixin.api.WxMessageRouterTest.WxEchoMessageHandler; import chanjarster.weixin.bean.WxXmlMessage; /** - * 微信消息路由器,通过代码化的配置,把来自微信的消息交给某个的handler处理 + *
+ * 微信消息路由器,通过代码化的配置,把来自微信的消息交给handler处理
+ * 
+ * 使用方法:
+ * WxMessageRouter router = new WxMessageRouter();
+ * router
+ *   .rule()
+ *       .msgType("MSG_TYPE").event("EVENT").eventKey("EVENT_KEY").content("CONTENT")
+ *       .interceptor(interceptor, ...).handler(handler, ...)
+ *   .end()
+ *   .rule()
+ *       // 另外一个匹配规则
+ *   .end()
+ * ;
+ * 
+ * // 将WxXmlMessage交给消息路由器
+ * router.route(message);
+ * 
+ * 说明:
+ * 1. 配置路由规则时要按照从细到粗的原则
+ * 2. 默认情况下消息只会被处理一次,除非使用 {@link Rule#reEnter()}
+ * 
* @author qianjia * */ @@ -20,7 +42,7 @@ public class WxMessageRouter { * 开始一个新的Route规则 * @return */ - public Rule start() { + public Rule rule() { return new Rule(this); } @@ -49,7 +71,7 @@ public class WxMessageRouter { private String content; - private boolean forward = false; + private boolean reEnter = false; private List handlers = new ArrayList(); @@ -100,11 +122,11 @@ public class WxMessageRouter { } /** - * 如果本规则命中,在执行完handler后,还会接着给后面的Rule执行 + * 将消息交给后面的Rule处理 * @return */ - public Rule forward() { - this.forward = true; + public Rule reEnter() { + this.reEnter = true; return this; } @@ -175,7 +197,7 @@ public class WxMessageRouter { // 如果拦截器不通过 for (WxMessageInterceptor interceptor : this.interceptors) { if (!interceptor.intercept(wxMessage, context)) { - return this.forward; + return this.reEnter; } } @@ -184,7 +206,7 @@ public class WxMessageRouter { interceptor.handle(wxMessage, context); } - return this.forward; + return this.reEnter; } } diff --git a/src/main/java/chanjarster/weixin/api/WxService.java b/src/main/java/chanjarster/weixin/api/WxService.java index 9d331590..6300310f 100644 --- a/src/main/java/chanjarster/weixin/api/WxService.java +++ b/src/main/java/chanjarster/weixin/api/WxService.java @@ -10,45 +10,58 @@ import chanjarster.weixin.exception.WxErrorException; public interface WxService { /** - * 刷新access_token,这个方法是线程安全的 - * 且在高并发情况下只会刷新一次,而不是刷新多次 - * 在非必要情况下不要主动调用此方法 - * 本service的所有方法都会在access_token过期的情况下调用此方法 + *
+   * 获取access_token,本方法线程安全
+   * 且在多线程同时刷新时只刷新一次,避免超出1200次的调用次数上限
    * 
+   * 另:本service的所有方法都会在access_token过期是调用此方法
+   * 
+   * 程序员在非必要情况下尽量不要主动调用此方法
+
    * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token
+   * 
* @throws WxErrorException */ public void refreshAccessToken() throws WxErrorException; /** + *
    * 发送客服消息
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息
+   * 
* @param message - * @return * @throws WxErrorException */ public String sendCustomMessage(WxCustomMessage message) throws WxErrorException; /** - * 创建菜单 + *
+   * 自定义菜单创建接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单创建接口
+   * 
* @param menu - * @return * @throws WxErrorException */ public String createMenu(WxMenu menu) throws WxErrorException; /** - * 删除菜单 - * @return + *
+   * 自定义菜单删除接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单删除接口
+   * 
* @throws WxErrorException */ public String deleteMenu() throws WxErrorException; /** - * 获得菜单 + *
+   * 自定义菜单查询接口
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=自定义菜单查询接口
+   * 
* @return * @throws WxErrorException */ public WxMenu getMenu() throws WxErrorException; - public void setWxConfigProvider(WxConfigProvider wxConfigProvider); + public void setWxConfigProvider(WxConfigStorage wxConfigProvider); } diff --git a/src/main/java/chanjarster/weixin/api/WxServiceImpl.java b/src/main/java/chanjarster/weixin/api/WxServiceImpl.java index 12fda07e..aa319c89 100644 --- a/src/main/java/chanjarster/weixin/api/WxServiceImpl.java +++ b/src/main/java/chanjarster/weixin/api/WxServiceImpl.java @@ -1,14 +1,15 @@ package chanjarster.weixin.api; import java.io.IOException; -import java.nio.charset.Charset; import java.util.concurrent.atomic.AtomicBoolean; import org.apache.commons.lang3.StringUtils; +import org.apache.http.Consts; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.BasicResponseHandler; import org.apache.http.impl.client.CloseableHttpClient; @@ -19,6 +20,7 @@ import chanjarster.weixin.bean.WxCustomMessage; import chanjarster.weixin.bean.WxError; import chanjarster.weixin.bean.WxMenu; import chanjarster.weixin.exception.WxErrorException; +import chanjarster.weixin.util.Utf8StringResponseHandler; public class WxServiceImpl implements WxService { @@ -31,9 +33,7 @@ public class WxServiceImpl implements WxService { protected static final CloseableHttpClient httpclient = HttpClients.createDefault(); - protected static final Charset UTF8 = Charset.forName("UTF-8"); - - protected WxConfigProvider wxConfigProvider; + protected WxConfigStorage wxConfigProvider; public void refreshAccessToken() throws WxErrorException { if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) { @@ -46,6 +46,10 @@ public class WxServiceImpl implements WxService { HttpGet httpGet = new HttpGet(url); CloseableHttpResponse response = httpclient.execute(httpGet); String resultContent = new BasicResponseHandler().handleResponse(response); + WxError error = WxError.fromJson(resultContent); + if (error.getErrcode() != 0) { + throw new WxErrorException(error); + } WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); wxConfigProvider.updateAccessToken(accessToken.getAccess_token(), accessToken.getExpires_in()); } catch (ClientProtocolException e) { @@ -68,17 +72,35 @@ public class WxServiceImpl implements WxService { } } - /** - * 发送客服消息 - * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=发送客服消息 - * @param message - * @throws WxErrorException - */ public String sendCustomMessage(WxCustomMessage message) throws WxErrorException { String url = "https://api.weixin.qq.com/cgi-bin/message/custom/send"; return post(url, message.toJson()); } + public String createMenu(WxMenu menu) throws WxErrorException { + String url = "https://api.weixin.qq.com/cgi-bin/menu/create"; + return post(url, menu.toJson()); + } + + public String deleteMenu() throws WxErrorException { + String url = "https://api.weixin.qq.com/cgi-bin/menu/delete"; + return get(url, null); + } + + public WxMenu getMenu() throws WxErrorException { + String url = "https://api.weixin.qq.com/cgi-bin/menu/get"; + try { + String resultContent = get(url, null); + return WxMenu.fromJson(resultContent); + } catch (WxErrorException e) { + // 46003 不存在的菜单数据 + if (e.getError().getErrcode() == 46003) { + return null; + } + throw e; + } + } + protected String post(String uri, String data) throws WxErrorException { return execute("POST", uri, data); } @@ -106,20 +128,26 @@ public class WxServiceImpl implements WxService { String resultContent = null; if ("POST".equals(method)) { HttpPost httpPost = new HttpPost(uriWithAccessToken); - StringEntity entity = new StringEntity(data, UTF8); - httpPost.setEntity(entity); + if (data != null) { + StringEntity entity = new StringEntity(data, Consts.UTF_8); + httpPost.setEntity(entity); + } CloseableHttpResponse response = httpclient.execute(httpPost); - resultContent = new BasicResponseHandler().handleResponse(response); + resultContent = Utf8StringResponseHandler.INSTANCE.handleResponse(response); } else if ("GET".equals(method)) { + if (data != null) { + uriWithAccessToken += uriWithAccessToken.endsWith("&") ? data : '&' + data; + } HttpGet httpGet = new HttpGet(uriWithAccessToken); CloseableHttpResponse response = httpclient.execute(httpGet); - resultContent = new BasicResponseHandler().handleResponse(response); + response.setHeader("Content-Type", ContentType.APPLICATION_JSON.toString()); + resultContent = Utf8StringResponseHandler.INSTANCE.handleResponse(response); } WxError error = WxError.fromJson(resultContent); /* - * 关于微信返回错误码 详情请看 http://mp.weixin.qq.com/wiki/index.php?title=全局返回码说明 - * 40001 微信图片不对 + * 发生以下情况时尝试刷新access_token + * 40001 获取access_token时AppSecret错误,或者access_token无效 * 42001 access_token超时 */ if (error.getErrcode() == 42001 || error.getErrcode() == 40001) { @@ -137,36 +165,7 @@ public class WxServiceImpl implements WxService { } } - /** - * - * @param menu - * @throws WxErrorException - */ - public String createMenu(WxMenu menu) throws WxErrorException { - // TODO - return null; - } - - /** - * - * @throws WxErrorException - */ - public String deleteMenu() throws WxErrorException { - // TODO - return null; - } - - /** - * - * @return - * @throws WxErrorException - */ - public WxMenu getMenu() throws WxErrorException { - // TODO - return null; - } - - public void setWxConfigProvider(WxConfigProvider wxConfigProvider) { + public void setWxConfigProvider(WxConfigStorage wxConfigProvider) { this.wxConfigProvider = wxConfigProvider; } diff --git a/src/main/java/chanjarster/weixin/bean/WxError.java b/src/main/java/chanjarster/weixin/bean/WxError.java index 6fd63f92..fcb2263b 100644 --- a/src/main/java/chanjarster/weixin/bean/WxError.java +++ b/src/main/java/chanjarster/weixin/bean/WxError.java @@ -1,12 +1,108 @@ package chanjarster.weixin.bean; +import java.util.HashMap; +import java.util.Map; + import chanjarster.weixin.util.WxGsonBuilder; +/** + * 微信错误码说明 + * http://mp.weixin.qq.com/wiki/index.php?title=全局返回码说明 + * @author chanjarster + * + */ public class WxError { - private int errcode; + protected static final Map errMap = new HashMap(); + + static { + errMap.put(-1, "系统繁忙"); + errMap.put(0, "请求成功"); + errMap.put(40001, "获取access_token时AppSecret错误,或者access_token无效"); + errMap.put(40002, "不合法的凭证类型"); + errMap.put(40003, "不合法的OpenID"); + errMap.put(40004, "不合法的媒体文件类型"); + errMap.put(40005, "不合法的文件类型"); + errMap.put(40006, "不合法的文件大小"); + errMap.put(40007, "不合法的媒体文件id"); + errMap.put(40008, "不合法的消息类型"); + errMap.put(40009, "不合法的图片文件大小"); + errMap.put(40010, "不合法的语音文件大小"); + errMap.put(40011, "不合法的视频文件大小"); + errMap.put(40012, "不合法的缩略图文件大小"); + errMap.put(40013, "不合法的APPID"); + errMap.put(40014, "不合法的access_token"); + errMap.put(40015, "不合法的菜单类型"); + errMap.put(40016, "不合法的按钮个数"); + errMap.put(40017, "不合法的按钮个数"); + errMap.put(40018, "不合法的按钮名字长度"); + errMap.put(40019, "不合法的按钮KEY长度"); + errMap.put(40020, "不合法的按钮URL长度"); + errMap.put(40021, "不合法的菜单版本号"); + errMap.put(40022, "不合法的子菜单级数"); + errMap.put(40023, "不合法的子菜单按钮个数"); + errMap.put(40024, "不合法的子菜单按钮类型"); + errMap.put(40025, "不合法的子菜单按钮名字长度"); + errMap.put(40026, "不合法的子菜单按钮KEY长度"); + errMap.put(40027, "不合法的子菜单按钮URL长度"); + errMap.put(40028, "不合法的自定义菜单使用用户"); + errMap.put(40029, "不合法的oauth_code"); + errMap.put(40030, "不合法的refresh_token"); + errMap.put(40031, "不合法的openid列表"); + errMap.put(40032, "不合法的openid列表长度"); + errMap.put(40033, "不合法的请求字符,不能包含\\uxxxx格式的字符"); + errMap.put(40035, "不合法的参数"); + errMap.put(40038, "不合法的请求格式"); + errMap.put(40039, "不合法的URL长度"); + errMap.put(40050, "不合法的分组id"); + errMap.put(40051, "分组名字不合法"); + errMap.put(41001, "缺少access_token参数"); + errMap.put(41002, "缺少appid参数"); + errMap.put(41003, "缺少refresh_token参数"); + errMap.put(41004, "缺少secret参数"); + errMap.put(41005, "缺少多媒体文件数据"); + errMap.put(41006, "缺少media_id参数"); + errMap.put(41007, "缺少子菜单数据"); + errMap.put(41008, "缺少oauth code"); + errMap.put(41009, "缺少openid"); + errMap.put(42001, "access_token超时"); + errMap.put(42002, "refresh_token超时"); + errMap.put(42003, "oauth_code超时"); + errMap.put(43001, "需要GET请求"); + errMap.put(43002, "需要POST请求"); + errMap.put(43003, "需要HTTPS请求"); + errMap.put(43004, "需要接收者关注"); + errMap.put(43005, "需要好友关系"); + errMap.put(44001, "多媒体文件为空"); + errMap.put(44002, "POST的数据包为空"); + errMap.put(44003, "图文消息内容为空"); + errMap.put(44004, "文本消息内容为空"); + errMap.put(45001, "多媒体文件大小超过限制"); + errMap.put(45002, "消息内容超过限制"); + errMap.put(45003, "标题字段超过限制"); + errMap.put(45004, "描述字段超过限制"); + errMap.put(45005, "链接字段超过限制"); + errMap.put(45006, "图片链接字段超过限制"); + errMap.put(45007, "语音播放时间超过限制"); + errMap.put(45008, "图文消息超过限制"); + errMap.put(45009, "接口调用超过限制"); + errMap.put(45010, "创建菜单个数超过限制"); + errMap.put(45015, "回复时间超过限制"); + errMap.put(45016, "系统分组,不允许修改"); + errMap.put(45017, "分组名字过长"); + errMap.put(45018, "分组数量超过上限"); + errMap.put(46001, "不存在媒体数据"); + errMap.put(46002, "不存在的菜单版本"); + errMap.put(46003, "不存在的菜单数据"); + errMap.put(46004, "不存在的用户"); + errMap.put(47001, "解析JSON/XML内容错误"); + errMap.put(48001, "api功能未授权"); + errMap.put(50001, "用户未授权该api"); + } - private String errmsg; + protected int errcode; + + protected String errmsg; public int getErrcode() { return errcode; @@ -24,14 +120,17 @@ public class WxError { this.errmsg = errmsg; } + public String getDescription() { + return errMap.get(errcode); + } + public static WxError fromJson(String json) { return WxGsonBuilder.create().fromJson(json, WxError.class); } @Override public String toString() { - return "{ errcode=" + errcode + ", errmsg=" + errmsg + "}"; + return "微信错误 errcode=" + errcode + ", errmsg=" + errmsg + ", description=" + getDescription(); } - - + } diff --git a/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java b/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java index 0e9dae71..9b285b1c 100644 --- a/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java +++ b/src/main/java/chanjarster/weixin/bean/WxXmlMessage.java @@ -2,6 +2,7 @@ package chanjarster.weixin.bean; import java.io.InputStream; +import javax.management.RuntimeErrorException; import javax.xml.bind.JAXBException; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; @@ -291,27 +292,24 @@ public class WxXmlMessage { try { return XmlTransformer.toXml(WxXmlMessage.class, this); } catch (JAXBException e) { - e.printStackTrace(); + throw new RuntimeException(e); } - return ""; } public static WxXmlMessage fromXml(String xml) { try { return XmlTransformer.fromXml(WxXmlMessage.class, xml); } catch (JAXBException e) { - e.printStackTrace(); + throw new RuntimeException(e); } - return null; } public static WxXmlMessage fromXml(InputStream is) { try { return XmlTransformer.fromXml(WxXmlMessage.class, is); } catch (JAXBException e) { - e.printStackTrace(); + throw new RuntimeException(e); } - return null; } @Override diff --git a/src/main/java/chanjarster/weixin/util/Utf8StringResponseHandler.java b/src/main/java/chanjarster/weixin/util/Utf8StringResponseHandler.java new file mode 100644 index 00000000..1b8f0145 --- /dev/null +++ b/src/main/java/chanjarster/weixin/util/Utf8StringResponseHandler.java @@ -0,0 +1,32 @@ +package chanjarster.weixin.util; + +import java.io.IOException; + +import org.apache.http.Consts; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.StatusLine; +import org.apache.http.client.HttpResponseException; +import org.apache.http.client.ResponseHandler; +import org.apache.http.util.EntityUtils; + +/** + * copy from {@link org.apache.http.impl.client.BasicResponseHandler} + * @author chanjarster + * + */ +public class Utf8StringResponseHandler implements ResponseHandler { + + public static final ResponseHandler INSTANCE = new Utf8StringResponseHandler(); + + public String handleResponse(final HttpResponse response) throws HttpResponseException, IOException { + final StatusLine statusLine = response.getStatusLine(); + final HttpEntity entity = response.getEntity(); + if (statusLine.getStatusCode() >= 300) { + EntityUtils.consume(entity); + throw new HttpResponseException(statusLine.getStatusCode(), statusLine.getReasonPhrase()); + } + return entity == null ? null : EntityUtils.toString(entity, Consts.UTF_8); + } + +} diff --git a/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java b/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java index abf6eab1..b6f0a972 100644 --- a/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java +++ b/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java @@ -32,25 +32,25 @@ public class WxCustomMessageGsonAdapter implements JsonSerializer, JsonDeserializ } public WxMenu deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { + /* + * 操蛋的微信 + * 创建菜单时是 { button : ... } + * 查询菜单时是 { menu : { button : ... } } + */ WxMenu menu = new WxMenu(); - JsonArray buttonsJson = json.getAsJsonObject().get("button").getAsJsonArray(); + JsonObject menuJson = json.getAsJsonObject().get("menu").getAsJsonObject(); + JsonArray buttonsJson = menuJson.get("button").getAsJsonArray(); for (int i = 0; i < buttonsJson.size(); i++) { JsonObject buttonJson = buttonsJson.get(i).getAsJsonObject(); WxMenuButton button = convertFromJson(buttonJson); diff --git a/src/main/java/chanjarster/weixin/util/XmlTransformer.java b/src/main/java/chanjarster/weixin/util/XmlTransformer.java index af83bf53..7f52efa7 100644 --- a/src/main/java/chanjarster/weixin/util/XmlTransformer.java +++ b/src/main/java/chanjarster/weixin/util/XmlTransformer.java @@ -11,6 +11,8 @@ import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.Unmarshaller; +import org.xml.sax.InputSource; + import com.sun.xml.bind.marshaller.CharacterEscapeHandler; public class XmlTransformer { @@ -32,7 +34,9 @@ public class XmlTransformer { public static T fromXml(Class clazz, InputStream is) throws JAXBException { JAXBContext context = JAXBContext.newInstance(clazz); Unmarshaller um = context.createUnmarshaller(); - T object = (T) um.unmarshal(is); + InputSource inputSource = new InputSource(is); + inputSource.setEncoding("utf-8"); + T object = (T) um.unmarshal(inputSource); return object; } diff --git a/src/test/java/chanjarster/weixin/api/WxMessageRouterTest.java b/src/test/java/chanjarster/weixin/api/WxMessageRouterTest.java index 4c20f66b..03e83fd4 100644 --- a/src/test/java/chanjarster/weixin/api/WxMessageRouterTest.java +++ b/src/test/java/chanjarster/weixin/api/WxMessageRouterTest.java @@ -1,6 +1,48 @@ package chanjarster.weixin.api; -// TODO +import java.util.Map; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import chanjarster.weixin.bean.WxXmlMessage; + +@Test public class WxMessageRouterTest { + public void testSimple() { + StringBuilder sb = new StringBuilder(); + WxMessageRouter router = new WxMessageRouter(); + router + .rule().msgType(WxConsts.MSG_TEXT).handler(new WxEchoMessageHandler(sb, WxConsts.MSG_TEXT)).end() + .rule().msgType(WxConsts.MSG_IMAGE).handler(new WxEchoMessageHandler(sb, WxConsts.MSG_IMAGE)).end() + ; + + + WxXmlMessage message = new WxXmlMessage(); + message.setMsgType(WxConsts.MSG_TEXT); + router.route(message); + Assert.assertEquals(sb.toString(), WxConsts.MSG_TEXT + ","); + + + } + + + public static class WxEchoMessageHandler implements WxMessageHandler { + + private StringBuilder sb; + private String echoStr; + + public WxEchoMessageHandler(StringBuilder sb, String echoStr) { + this.sb = sb; + this.echoStr = echoStr; + } + + @Override + public void handle(WxXmlMessage wxMessage, Map context) { + sb.append(this.echoStr).append(','); + } + + } + } diff --git a/src/test/java/chanjarster/weixin/api/WxServiceTest.java b/src/test/java/chanjarster/weixin/api/WxServiceTest.java index 6d5a6a84..4306970b 100644 --- a/src/test/java/chanjarster/weixin/api/WxServiceTest.java +++ b/src/test/java/chanjarster/weixin/api/WxServiceTest.java @@ -9,68 +9,117 @@ import javax.xml.bind.annotation.XmlRootElement; import org.apache.commons.lang3.StringUtils; import org.testng.Assert; +import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; -import chanjarster.weixin.api.WxConsts; -import chanjarster.weixin.api.WxMemoryConfigProvider; -import chanjarster.weixin.api.WxService; -import chanjarster.weixin.api.WxServiceImpl; import chanjarster.weixin.bean.WxCustomMessage; +import chanjarster.weixin.bean.WxMenu; +import chanjarster.weixin.bean.WxMenu.WxMenuButton; import chanjarster.weixin.exception.WxErrorException; import chanjarster.weixin.util.XmlTransformer; public class WxServiceTest { - @Test(dataProvider = "configs") - public void testRefreshAccessToken(WxTestConfigProvider config) throws WxErrorException { - String before = config.getAccessToken(); - - WxService wxService = new WxServiceImpl(); - wxService.setWxConfigProvider(config); + private WxServiceImpl wxService; + + @BeforeTest + public void prepare() throws JAXBException { + InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml"); + WxXmlConfigStorage config1 = XmlTransformer.fromXml(WxXmlConfigStorage.class, is1); + this.wxService = new WxServiceImpl(); + this.wxService.setWxConfigProvider(config1); + } + + @Test + public void testRefreshAccessToken() throws WxErrorException { + WxConfigStorage configProvider = wxService.wxConfigProvider; + String before = configProvider.getAccessToken(); wxService.refreshAccessToken(); - String after = config.getAccessToken(); + String after = configProvider.getAccessToken(); Assert.assertNotEquals(before, after); Assert.assertTrue(StringUtils.isNotBlank(after)); } - @Test(dataProvider = "configs") - public void sendCustomMessage(WxTestConfigProvider config) throws WxErrorException { - WxService wxService = new WxServiceImpl(); - wxService.setWxConfigProvider(config); - + @Test(dependsOnMethods = "testRefreshAccessToken", enabled = false) + public void sendCustomMessage() throws WxErrorException { + WxXmlConfigStorage configProvider = (WxXmlConfigStorage) wxService.wxConfigProvider; WxCustomMessage message = new WxCustomMessage(); - message.setMsgtype(WxConsts.TEXT); - message.setTouser(config.getOpenId()); - message.setContent("欢迎使用教务系统微信公众号\n下面\nHello World"); + message.setMsgtype(WxConsts.MSG_TEXT); + message.setTouser(configProvider.getOpenId()); + message.setContent("欢迎使用教务系统微信公众号\n下面\nHello World"); wxService.sendCustomMessage(message); } - /** - * 返回新的access_token - * @return - * @throws JAXBException - */ - @DataProvider(name = "configs") - public Object[][] getConfig() throws JAXBException { - /** - * 将 src/test/resources/test-config.sample.xml 改成 test-config.xml 并设置appId, secret, 一个过期的accessToken - */ - // 没有access_token - InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml"); - WxTestConfigProvider config1 = XmlTransformer.fromXml(WxTestConfigProvider.class, is1); + + @Test(dataProvider = "menu", enabled = true, dependsOnMethods = "testRefreshAccessToken") + public void testCreateMenu(WxMenu wxMenu) throws WxErrorException { + wxService.createMenu(wxMenu); + } + + @Test(dependsOnMethods = { "testRefreshAccessToken" , "testCreateMenu"}) + public void testGetMenu() throws WxErrorException { + Assert.assertNotNull(wxService.getMenu()); + } + + @Test(dependsOnMethods = { "testRefreshAccessToken", "testGetMenu" }, enabled = false) + public void testDeleteMenu() throws WxErrorException { + wxService.deleteMenu(); + } + + @DataProvider(name="menu") + public Object[][] getMenu() throws JAXBException { + WxMenu menu = new WxMenu(); + WxMenuButton button1 = new WxMenuButton(); + button1.setType("click"); + button1.setName("今日歌曲"); + button1.setKey("V1001_TODAY_MUSIC"); + + WxMenuButton button2 = new WxMenuButton(); + button2.setType("click"); + button2.setName("歌手简介"); + button2.setKey("V1001_TODAY_SINGER"); + + WxMenuButton button3 = new WxMenuButton(); + button3.setName("菜单"); + + menu.getButton().add(button1); + menu.getButton().add(button2); + menu.getButton().add(button3); + + WxMenuButton button31 = new WxMenuButton(); + button31.setType("view"); + button31.setName("搜索"); + button31.setUrl("http://www.soso.com/"); + + WxMenuButton button32 = new WxMenuButton(); + button32.setType("view"); + button32.setName("视频"); + button32.setUrl("http://v.qq.com/"); + + WxMenuButton button33 = new WxMenuButton(); + button33.setType("click"); + button33.setName("赞一下我们"); + button33.setKey("V1001_GOOD"); + + button3.getSub_button().add(button31); + button3.getSub_button().add(button32); + button3.getSub_button().add(button33); + return new Object[][] { new Object[] { - config1 + menu } }; + } + @XmlRootElement(name = "xml") @XmlAccessorType(XmlAccessType.FIELD) - public static class WxTestConfigProvider extends WxMemoryConfigProvider { + public static class WxXmlConfigStorage extends WxInMemoryConfigStorage { protected String openId; diff --git a/src/test/java/chanjarster/weixin/bean/WxCustomMessageTest.java b/src/test/java/chanjarster/weixin/bean/WxCustomMessageTest.java index b6922fae..27388645 100644 --- a/src/test/java/chanjarster/weixin/bean/WxCustomMessageTest.java +++ b/src/test/java/chanjarster/weixin/bean/WxCustomMessageTest.java @@ -13,7 +13,7 @@ public class WxCustomMessageTest { public void testTextReply() { WxCustomMessage reply = new WxCustomMessage(); reply.setTouser("OPENID"); - reply.setMsgtype(WxConsts.TEXT); + reply.setMsgtype(WxConsts.MSG_TEXT); reply.setContent("sfsfdsdf"); Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"text\",\"text\":{\"content\":\"sfsfdsdf\"}}"); } @@ -21,7 +21,7 @@ public class WxCustomMessageTest { public void testImageReply() { WxCustomMessage reply = new WxCustomMessage(); reply.setTouser("OPENID"); - reply.setMsgtype(WxConsts.IMAGE); + reply.setMsgtype(WxConsts.MSG_IMAGE); reply.setMedia_id("MEDIA_ID"); Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"image\",\"image\":{\"media_id\":\"MEDIA_ID\"}}"); } @@ -29,7 +29,7 @@ public class WxCustomMessageTest { public void testVoiceReply() { WxCustomMessage reply = new WxCustomMessage(); reply.setTouser("OPENID"); - reply.setMsgtype(WxConsts.VOICE); + reply.setMsgtype(WxConsts.MSG_VOICE); reply.setMedia_id("MEDIA_ID"); Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"voice\",\"voice\":{\"media_id\":\"MEDIA_ID\"}}"); } @@ -37,7 +37,7 @@ public class WxCustomMessageTest { public void testVideoReply() { WxCustomMessage reply = new WxCustomMessage(); reply.setTouser("OPENID"); - reply.setMsgtype(WxConsts.VIDEO); + reply.setMsgtype(WxConsts.MSG_VIDEO); reply.setMedia_id("MEDIA_ID"); reply.setThumb_media_id("MEDIA_ID"); reply.setTitle("TITLE"); @@ -48,7 +48,7 @@ public class WxCustomMessageTest { public void testMusicReply() { WxCustomMessage reply = new WxCustomMessage(); reply.setTouser("OPENID"); - reply.setMsgtype(WxConsts.MUSIC); + reply.setMsgtype(WxConsts.MSG_MUSIC); reply.setThumb_media_id("MEDIA_ID"); reply.setDescription("DESCRIPTION"); reply.setTitle("TITLE"); @@ -60,7 +60,7 @@ public class WxCustomMessageTest { public void testNewsReply() { WxCustomMessage reply = new WxCustomMessage(); reply.setTouser("OPENID"); - reply.setMsgtype(WxConsts.NEWS); + reply.setMsgtype(WxConsts.MSG_NEWS); WxArticle article1 = new WxArticle(); article1.setUrl("URL"); @@ -77,7 +77,6 @@ public class WxCustomMessageTest { reply.getArticles().add(article2); - System.out.println(reply.toJson()); Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"news\",\"articles\":[{\"title\":\"Happy Day\",\"description\":\"Is Really A Happy Day\",\"url\":\"URL\",\"picurl\":\"PIC_URL\"},{\"title\":\"Happy Day\",\"description\":\"Is Really A Happy Day\",\"url\":\"URL\",\"picurl\":\"PIC_URL\"}]}"); } diff --git a/src/test/java/chanjarster/weixin/bean/WxMenuTest.java b/src/test/java/chanjarster/weixin/bean/WxMenuTest.java index 4962c9ae..90ad602d 100644 --- a/src/test/java/chanjarster/weixin/bean/WxMenuTest.java +++ b/src/test/java/chanjarster/weixin/bean/WxMenuTest.java @@ -1,6 +1,5 @@ package chanjarster.weixin.bean; -import org.apache.http.util.Asserts; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; @@ -10,13 +9,13 @@ import chanjarster.weixin.bean.WxMenu.WxMenuButton; @Test public class WxMenuTest { - @Test(dataProvider="json") + @Test(dataProvider="wxReturnMenu") public void testFromJson(String json) { WxMenu menu = WxMenu.fromJson(json); Assert.assertEquals(menu.getButton().size(), 3); } - @Test(dataProvider="json") + @Test(dataProvider="wxPushMenu") public void testToJson(String json) { WxMenu menu = new WxMenu(); WxMenuButton button1 = new WxMenuButton(); @@ -55,12 +54,20 @@ public class WxMenuTest { button3.getSub_button().add(button32); button3.getSub_button().add(button33); - System.out.println(menu.toJson()); Assert.assertEquals(menu.toJson(), json); } - @DataProvider(name="json") - public Object[][] getMenuJson() { + @Test(dataProvider="wxReturnMenu") + public Object[][] wxReturnMenu() { + Object[][] res = menuJson(); + String json = "{ \"menu\" : " + res[0][0] + " }"; + return new Object[][] { + new Object[] { json } + }; + } + + @DataProvider(name="wxPushMenu") + public Object[][] menuJson() { String json = "{" +"\"button\":[" diff --git a/src/test/java/chanjarster/weixin/bean/WxXmlMessageTest.java b/src/test/java/chanjarster/weixin/bean/WxXmlMessageTest.java index f6db5f89..dd1a4fe0 100644 --- a/src/test/java/chanjarster/weixin/bean/WxXmlMessageTest.java +++ b/src/test/java/chanjarster/weixin/bean/WxXmlMessageTest.java @@ -40,7 +40,7 @@ public class WxXmlMessageTest { Assert.assertEquals(wxMessage.getToUserName(), "toUser"); Assert.assertEquals(wxMessage.getFromUserName(), "fromUser"); Assert.assertEquals(wxMessage.getCreateTime(), new Long(1348831860l)); - Assert.assertEquals(wxMessage.getMsgType(), WxConsts.TEXT); + Assert.assertEquals(wxMessage.getMsgType(), WxConsts.MSG_TEXT); Assert.assertEquals(wxMessage.getContent(), "this is a test"); Assert.assertEquals(wxMessage.getMsgId(), new Long(1234567890123456l)); Assert.assertEquals(wxMessage.getPicUrl(), "this is a url"); @@ -67,7 +67,7 @@ public class WxXmlMessageTest { wxMessage.setToUserName("toUser"); wxMessage.setFromUserName("fromUser"); wxMessage.setCreateTime(new Long(1348831860l)); - wxMessage.setMsgType(WxConsts.TEXT); + wxMessage.setMsgType(WxConsts.MSG_TEXT); wxMessage.setContent("this is a test"); wxMessage.setMsgId(new Long(1234567890123456l)); wxMessage.setPicUrl("this is a url");