diff --git a/.gitignore b/.gitignore index 32858aad..6846e767 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ *.class +test-output # Mobile Tools for Java (J2ME) .mtj.tmp/ @@ -10,3 +11,9 @@ # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* + +target +.project +.classpath + +src/test/resources/test-config.xml diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..9297530d --- /dev/null +++ b/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + chanjarster.weixin + weixin-java + 1.0.0-SNAPSHOT + WeiXin Java Toolset + https://github.com/chanjarster/weixin-java + + UTF-8 + true + true + + + + junit + junit + 4.11 + test + + + org.apache.httpcomponents + fluent-hc + 4.3.5 + + + javax.xml.bind + jaxb-api + 2.2.7 + + + com.sun.xml.bind + jaxb-impl + 2.2.7 + + + org.apache.oltu.oauth2 + org.apache.oltu.oauth2.client + 1.0.0 + + + com.google.code.gson + gson + 2.2.2 + + + org.testng + testng + 6.8.7 + test + + + org.mockito + mockito-all + 1.9.5 + test + + + org.apache.commons + commons-lang3 + 3.1 + + + diff --git a/src/main/java/chanjarster/weixin/exception/WxErrorException.java b/src/main/java/chanjarster/weixin/exception/WxErrorException.java new file mode 100644 index 00000000..352a75f9 --- /dev/null +++ b/src/main/java/chanjarster/weixin/exception/WxErrorException.java @@ -0,0 +1,21 @@ +package chanjarster.weixin.exception; + +import chanjarster.weixin.in.WxError; + +public class WxErrorException extends Exception { + + private static final long serialVersionUID = -6357149550353160810L; + + private WxError error; + + public WxErrorException(WxError error) { + super(error.toString()); + this.error = error; + } + + public WxError getError() { + return error; + } + + +} diff --git a/src/main/java/chanjarster/weixin/in/WxAccessToken.java b/src/main/java/chanjarster/weixin/in/WxAccessToken.java new file mode 100644 index 00000000..eb6c901e --- /dev/null +++ b/src/main/java/chanjarster/weixin/in/WxAccessToken.java @@ -0,0 +1,31 @@ +package chanjarster.weixin.in; + +import chanjarster.weixin.util.WxGsonBuilder; + +public class WxAccessToken { + + private String access_token; + + private int expires_in; + + public String getAccess_token() { + return access_token; + } + + public void setAccess_token(String access_token) { + this.access_token = access_token; + } + + public int getExpires_in() { + return expires_in; + } + + public void setExpires_in(int expires_in) { + this.expires_in = expires_in; + } + + public static WxAccessToken fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxAccessToken.class); + } + +} diff --git a/src/main/java/chanjarster/weixin/in/WxError.java b/src/main/java/chanjarster/weixin/in/WxError.java new file mode 100644 index 00000000..6298c9dd --- /dev/null +++ b/src/main/java/chanjarster/weixin/in/WxError.java @@ -0,0 +1,37 @@ +package chanjarster.weixin.in; + +import chanjarster.weixin.util.WxGsonBuilder; + +public class WxError { + + private int errcode; + + private String errmsg; + + public int getErrcode() { + return errcode; + } + + public void setErrcode(int errcode) { + this.errcode = errcode; + } + + public String getErrmsg() { + return errmsg; + } + + public void setErrmsg(String errmsg) { + this.errmsg = errmsg; + } + + public static WxError fromJson(String json) { + return WxGsonBuilder.create().fromJson(json, WxError.class); + } + + @Override + public String toString() { + return "{ errcode=" + errcode + ", errmsg=" + errmsg + "}"; + } + + +} diff --git a/src/main/java/chanjarster/weixin/out/WxCustomMessage.java b/src/main/java/chanjarster/weixin/out/WxCustomMessage.java new file mode 100644 index 00000000..e2a22ec5 --- /dev/null +++ b/src/main/java/chanjarster/weixin/out/WxCustomMessage.java @@ -0,0 +1,124 @@ +package chanjarster.weixin.out; + +import java.util.ArrayList; +import java.util.List; + +import chanjarster.weixin.util.WxGsonBuilder; + +/** + * 回复给用户的客服消息 + * @author chanjarster + * + */ +public class WxCustomMessage { + + protected String touser; + protected String msgtype; + protected String content; + protected String media_id; + protected String thumb_media_id; + protected String title; + protected String description; + protected String musicurl; + protected String hqmusicurl; + protected List articles = new ArrayList(); + + public String getTouser() { + return touser; + } + public void setTouser(String touser) { + this.touser = touser; + } + public String getMsgtype() { + return msgtype; + } + public void setMsgtype(String msgtype) { + this.msgtype = msgtype; + } + public String getContent() { + return content; + } + public void setContent(String content) { + this.content = content; + } + public String getMedia_id() { + return media_id; + } + public void setMedia_id(String media_id) { + this.media_id = media_id; + } + public String getThumb_media_id() { + return thumb_media_id; + } + public void setThumb_media_id(String thumb_media_id) { + this.thumb_media_id = thumb_media_id; + } + public String getTitle() { + return title; + } + public void setTitle(String title) { + this.title = title; + } + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + public String getMusicurl() { + return musicurl; + } + public void setMusicurl(String musicurl) { + this.musicurl = musicurl; + } + public String getHqmusicurl() { + return hqmusicurl; + } + public void setHqmusicurl(String hqmusicurl) { + this.hqmusicurl = hqmusicurl; + } + public List getArticles() { + return articles; + } + public void setArticles(List articles) { + this.articles = articles; + } + + public String toJson() { + return WxGsonBuilder.INSTANCE.create().toJson(this); + } + + public static class WxArticle { + + protected String title; + protected String description; + protected String url; + protected String picurl; + + public String getTitle() { + return title; + } + public void setTitle(String title) { + this.title = title; + } + public String getDescription() { + return description; + } + public void setDescription(String description) { + this.description = description; + } + public String getUrl() { + return url; + } + public void setUrl(String url) { + this.url = url; + } + public String getPicurl() { + return picurl; + } + public void setPicurl(String picurl) { + this.picurl = picurl; + } + + } +} diff --git a/src/main/java/chanjarster/weixin/out/WxMenu.java b/src/main/java/chanjarster/weixin/out/WxMenu.java new file mode 100644 index 00000000..4696ff83 --- /dev/null +++ b/src/main/java/chanjarster/weixin/out/WxMenu.java @@ -0,0 +1,74 @@ +package chanjarster.weixin.out; + +import java.util.ArrayList; +import java.util.List; + +/** + * 公众号菜单 + * @author chanjarster + * + */ +public class WxMenu { + + protected List button = new ArrayList(); + + public List getButton() { + return button; + } + + public void setButton(List button) { + this.button = button; + } + + public static class WxMenuButton { + + protected String type; + protected String name; + protected String key; + protected String url; + + protected List sub_button = new ArrayList(); + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public List getSub_button() { + return sub_button; + } + + public void setSub_button(List sub_button) { + this.sub_button = sub_button; + } + + } + +} diff --git a/src/main/java/chanjarster/weixin/out/WxUserMessage.java b/src/main/java/chanjarster/weixin/out/WxUserMessage.java new file mode 100644 index 00000000..f8eff04b --- /dev/null +++ b/src/main/java/chanjarster/weixin/out/WxUserMessage.java @@ -0,0 +1,418 @@ +package chanjarster.weixin.out; + +import javax.xml.bind.JAXBException; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRegistry; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; + +import chanjarster.weixin.util.AdapterCDATA; +import chanjarster.weixin.util.XmlTransformer; + +/** + *
+ * 微信推送过来的消息,同时也是同步回复给用户的消息
+ * 相关字段的解释看微信开发者文档:
+ * http://mp.weixin.qq.com/wiki/index.php?title=接收普通消息
+ * http://mp.weixin.qq.com/wiki/index.php?title=接收事件推送
+ * http://mp.weixin.qq.com/wiki/index.php?title=接收语音识别结果
+ * 
+ * @author chanjarster + * + */ +@XmlRootElement(name = "xml") +@XmlAccessorType(XmlAccessType.FIELD) +public class WxUserMessage { + + /////////////////////// + // 以下都是微信推送过来的消息的xml的element所对应的属性 + /////////////////////// + + @XmlElement(name="ToUserName") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String ToUserName; + + @XmlElement(name="FromUserName") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String FromUserName; + + @XmlElement(name="CreateTime") + private Long CreateTime; + + @XmlElement(name="MsgType") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String MsgType; + + @XmlElement(name="Content") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String Content; + + @XmlElement(name="MsgId") + private Long MsgId; + + @XmlElement(name="PicUrl") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String PicUrl; + + @XmlElement(name="MediaId") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String MediaId; + + @XmlElement(name="Format") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String Format; + + @XmlElement(name="ThumbMediaId") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String ThumbMediaId; + + @XmlElement(name="Location_X") + private Double Location_X; + + @XmlElement(name="Location_Y") + private Double Location_Y; + + @XmlElement(name="Scale") + private Double Scale; + + @XmlElement(name="Label") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String Label; + + @XmlElement(name="Title") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String Title; + + @XmlElement(name="Description") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String Description; + + @XmlElement(name="Url") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String Url; + + @XmlElement(name="Event") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String Event; + + @XmlElement(name="EventKey") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String EventKey; + + @XmlElement(name="Ticket") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String Ticket; + + @XmlElement(name="Latitude") + private Double Latitude; + + @XmlElement(name="Longitude") + private Double Longitude; + + @XmlElement(name="Precision") + private Double Precision; + + @XmlElement(name="Recognition") + @XmlJavaTypeAdapter(AdapterCDATA.class) + private String Recognition; + + public String getToUserName() { + return ToUserName; + } + public void setToUserName(String toUserName) { + ToUserName = toUserName; + } + + public Long getCreateTime() { + return CreateTime; + } + public void setCreateTime(Long createTime) { + CreateTime = createTime; + } + + public String getMsgType() { + return MsgType; + } + public void setMsgType(String msgType) { + MsgType = msgType; + } + + public String getContent() { + return Content; + } + public void setContent(String content) { + Content = content; + } + + public Long getMsgId() { + return MsgId; + } + public void setMsgId(Long msgId) { + MsgId = msgId; + } + + public String getPicUrl() { + return PicUrl; + } + public void setPicUrl(String picUrl) { + PicUrl = picUrl; + } + + public String getMediaId() { + return MediaId; + } + public void setMediaId(String mediaId) { + MediaId = mediaId; + } + + public String getFormat() { + return Format; + } + public void setFormat(String format) { + Format = format; + } + + public String getThumbMediaId() { + return ThumbMediaId; + } + public void setThumbMediaId(String thumbMediaId) { + ThumbMediaId = thumbMediaId; + } + + public Double getLocation_X() { + return Location_X; + } + public void setLocation_X(Double location_X) { + Location_X = location_X; + } + + public Double getLocation_Y() { + return Location_Y; + } + public void setLocation_Y(Double location_Y) { + Location_Y = location_Y; + } + + public Double getScale() { + return Scale; + } + public void setScale(Double scale) { + Scale = scale; + } + + public String getLabel() { + return Label; + } + public void setLabel(String label) { + Label = label; + } + + public String getTitle() { + return Title; + } + public void setTitle(String title) { + Title = title; + } + + public String getDescription() { + return Description; + } + public void setDescription(String description) { + Description = description; + } + + public String getUrl() { + return Url; + } + public void setUrl(String url) { + Url = url; + } + + public String getEvent() { + return Event; + } + public void setEvent(String event) { + Event = event; + } + + public String getEventKey() { + return EventKey; + } + public void setEventKey(String eventKey) { + EventKey = eventKey; + } + + public String getTicket() { + return Ticket; + } + public void setTicket(String ticket) { + Ticket = ticket; + } + + public Double getLatitude() { + return Latitude; + } + public void setLatitude(Double latitude) { + Latitude = latitude; + } + + public Double getLongitude() { + return Longitude; + } + public void setLongitude(Double longitude) { + Longitude = longitude; + } + + public Double getPrecision() { + return Precision; + } + public void setPrecision(Double precision) { + Precision = precision; + } + + public String getRecognition() { + return Recognition; + } + public void setRecognition(String recognition) { + Recognition = recognition; + } + + public String getFromUserName() { + return FromUserName; + } + public void setFromUserName(String fromUserName) { + FromUserName = fromUserName; + } + + public String toXml() { + try { + return XmlTransformer.toXml(WxUserMessage.class, this); + } catch (JAXBException e) { + e.printStackTrace(); + } + return ""; + } + + public static WxUserMessage fromXml(String xml) { + try { + return XmlTransformer.fromXml(WxUserMessage.class, xml); + } catch (JAXBException e) { + e.printStackTrace(); + } + return null; + } + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((Content == null) ? 0 : Content.hashCode()); + result = prime * result + ((CreateTime == null) ? 0 : CreateTime.hashCode()); + result = prime * result + ((Description == null) ? 0 : Description.hashCode()); + result = prime * result + ((Event == null) ? 0 : Event.hashCode()); + result = prime * result + ((EventKey == null) ? 0 : EventKey.hashCode()); + result = prime * result + ((Format == null) ? 0 : Format.hashCode()); + result = prime * result + ((FromUserName == null) ? 0 : FromUserName.hashCode()); + result = prime * result + ((Label == null) ? 0 : Label.hashCode()); + result = prime * result + ((Latitude == null) ? 0 : Latitude.hashCode()); + result = prime * result + ((Location_X == null) ? 0 : Location_X.hashCode()); + result = prime * result + ((Location_Y == null) ? 0 : Location_Y.hashCode()); + result = prime * result + ((Longitude == null) ? 0 : Longitude.hashCode()); + result = prime * result + ((MediaId == null) ? 0 : MediaId.hashCode()); + result = prime * result + ((MsgId == null) ? 0 : MsgId.hashCode()); + result = prime * result + ((MsgType == null) ? 0 : MsgType.hashCode()); + result = prime * result + ((PicUrl == null) ? 0 : PicUrl.hashCode()); + result = prime * result + ((Precision == null) ? 0 : Precision.hashCode()); + result = prime * result + ((Recognition == null) ? 0 : Recognition.hashCode()); + result = prime * result + ((Scale == null) ? 0 : Scale.hashCode()); + result = prime * result + ((ThumbMediaId == null) ? 0 : ThumbMediaId.hashCode()); + result = prime * result + ((Ticket == null) ? 0 : Ticket.hashCode()); + result = prime * result + ((Title == null) ? 0 : Title.hashCode()); + result = prime * result + ((ToUserName == null) ? 0 : ToUserName.hashCode()); + result = prime * result + ((Url == null) ? 0 : Url.hashCode()); + return result; + } + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null) return false; + if (getClass() != obj.getClass()) return false; + WxUserMessage other = (WxUserMessage) obj; + if (Content == null) { + if (other.Content != null) return false; + } else if (!Content.equals(other.Content)) return false; + if (CreateTime == null) { + if (other.CreateTime != null) return false; + } else if (!CreateTime.equals(other.CreateTime)) return false; + if (Description == null) { + if (other.Description != null) return false; + } else if (!Description.equals(other.Description)) return false; + if (Event == null) { + if (other.Event != null) return false; + } else if (!Event.equals(other.Event)) return false; + if (EventKey == null) { + if (other.EventKey != null) return false; + } else if (!EventKey.equals(other.EventKey)) return false; + if (Format == null) { + if (other.Format != null) return false; + } else if (!Format.equals(other.Format)) return false; + if (FromUserName == null) { + if (other.FromUserName != null) return false; + } else if (!FromUserName.equals(other.FromUserName)) return false; + if (Label == null) { + if (other.Label != null) return false; + } else if (!Label.equals(other.Label)) return false; + if (Latitude == null) { + if (other.Latitude != null) return false; + } else if (!Latitude.equals(other.Latitude)) return false; + if (Location_X == null) { + if (other.Location_X != null) return false; + } else if (!Location_X.equals(other.Location_X)) return false; + if (Location_Y == null) { + if (other.Location_Y != null) return false; + } else if (!Location_Y.equals(other.Location_Y)) return false; + if (Longitude == null) { + if (other.Longitude != null) return false; + } else if (!Longitude.equals(other.Longitude)) return false; + if (MediaId == null) { + if (other.MediaId != null) return false; + } else if (!MediaId.equals(other.MediaId)) return false; + if (MsgId == null) { + if (other.MsgId != null) return false; + } else if (!MsgId.equals(other.MsgId)) return false; + if (MsgType == null) { + if (other.MsgType != null) return false; + } else if (!MsgType.equals(other.MsgType)) return false; + if (PicUrl == null) { + if (other.PicUrl != null) return false; + } else if (!PicUrl.equals(other.PicUrl)) return false; + if (Precision == null) { + if (other.Precision != null) return false; + } else if (!Precision.equals(other.Precision)) return false; + if (Recognition == null) { + if (other.Recognition != null) return false; + } else if (!Recognition.equals(other.Recognition)) return false; + if (Scale == null) { + if (other.Scale != null) return false; + } else if (!Scale.equals(other.Scale)) return false; + if (ThumbMediaId == null) { + if (other.ThumbMediaId != null) return false; + } else if (!ThumbMediaId.equals(other.ThumbMediaId)) return false; + if (Ticket == null) { + if (other.Ticket != null) return false; + } else if (!Ticket.equals(other.Ticket)) return false; + if (Title == null) { + if (other.Title != null) return false; + } else if (!Title.equals(other.Title)) return false; + if (ToUserName == null) { + if (other.ToUserName != null) return false; + } else if (!ToUserName.equals(other.ToUserName)) return false; + if (Url == null) { + if (other.Url != null) return false; + } else if (!Url.equals(other.Url)) return false; + return true; + } + + +} \ No newline at end of file diff --git a/src/main/java/chanjarster/weixin/service/WxConfigProvider.java b/src/main/java/chanjarster/weixin/service/WxConfigProvider.java new file mode 100644 index 00000000..fd41a6bf --- /dev/null +++ b/src/main/java/chanjarster/weixin/service/WxConfigProvider.java @@ -0,0 +1,15 @@ +package chanjarster.weixin.service; + +public interface WxConfigProvider { + + public void updateAccessToken(String accessToken, Integer expiresIn); + + public String getAccessToken(); + + public String getAppId(); + + public String getSecret(); + + public String getToken(); + +} diff --git a/src/main/java/chanjarster/weixin/service/WxMessageHandler.java b/src/main/java/chanjarster/weixin/service/WxMessageHandler.java new file mode 100644 index 00000000..b0a9bfd1 --- /dev/null +++ b/src/main/java/chanjarster/weixin/service/WxMessageHandler.java @@ -0,0 +1,16 @@ +package chanjarster.weixin.service; + +import java.util.Map; + +import chanjarster.weixin.out.WxUserMessage; + +/** + * 处理微信推送消息的处理器 + * @author chanjarster + * + */ +public interface WxMessageHandler { + + public void handle(WxUserMessage wxMessage, Map context); + +} diff --git a/src/main/java/chanjarster/weixin/service/WxMessageInterceptor.java b/src/main/java/chanjarster/weixin/service/WxMessageInterceptor.java new file mode 100644 index 00000000..0dd5564f --- /dev/null +++ b/src/main/java/chanjarster/weixin/service/WxMessageInterceptor.java @@ -0,0 +1,22 @@ +package chanjarster.weixin.service; + +import java.util.Map; + +import chanjarster.weixin.out.WxUserMessage; + + +/** + * 微信消息拦截器,可以用来做一些验证 + * @author qianjia + * + */ +public interface WxMessageInterceptor { + + /** + * 拦截微信消息 + * @param wxMessage + * @return true代表OK,false代表不OK + */ + public boolean intercept(WxUserMessage wxMessage, Map context); + +} diff --git a/src/main/java/chanjarster/weixin/service/WxMessageRouter.java b/src/main/java/chanjarster/weixin/service/WxMessageRouter.java new file mode 100644 index 00000000..c7fabb5d --- /dev/null +++ b/src/main/java/chanjarster/weixin/service/WxMessageRouter.java @@ -0,0 +1,192 @@ +package chanjarster.weixin.service; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import chanjarster.weixin.out.WxUserMessage; + +/** + * 微信消息路由器,通过代码化的配置,把来自微信的消息交给某个的handler处理 + * @author qianjia + * + */ +public class WxMessageRouter { + + private List rules = new ArrayList(); + + /** + * 开始一个新的Route规则 + * @return + */ + public Rule start() { + return new Rule(this); + } + + /** + * 处理微信消息 + * @param wxMessage + */ + public void route(WxUserMessage wxMessage) { + for (Rule rule : rules) { + boolean doNext = rule.service(wxMessage); + if (!doNext) { + break; + } + } + } + + public static class Rule { + + private final WxMessageRouter routerBuilder; + + private String msgType; + + private String event; + + private String eventKey; + + private String content; + + private boolean forward = false; + + private List handlers = new ArrayList(); + + private List interceptors = new ArrayList(); + + protected Rule(WxMessageRouter routerBuilder) { + this.routerBuilder = routerBuilder; + } + + /** + * 如果msgType等于某值 + * @param msgType + * @return + */ + public Rule msgType(String msgType) { + this.msgType = msgType; + return this; + } + + /** + * 如果event等于某值 + * @param event + * @return + */ + public Rule event(String event) { + this.event = event; + return this; + } + + /** + * 如果eventKey等于某值 + * @param eventKey + * @return + */ + public Rule eventKey(String eventKey) { + this.eventKey = eventKey; + return this; + } + + /** + * 如果content等于某值 + * @param content + * @return + */ + public Rule content(String content) { + this.content = content; + return this; + } + + /** + * 如果本规则命中,在执行完handler后,还会接着给后面的Rule执行 + * @return + */ + public Rule forward() { + this.forward = true; + return this; + } + + /** + * 添加interceptor + * @param interceptor + * @param otherInterceptors + * @return + */ + public Rule interceptor(WxMessageInterceptor interceptor, WxMessageInterceptor... otherInterceptors) { + this.interceptors.add(interceptor); + if (otherInterceptors != null && otherInterceptors.length > 0) { + for (WxMessageInterceptor i : otherInterceptors) { + this.interceptors.add(i); + } + } + return this; + } + + /** + * 添加handler + * @param handler + * @return + */ + public Rule handler(WxMessageHandler handler, WxMessageHandler... otherHandlers) { + this.handlers.add(handler); + if (otherHandlers != null && otherHandlers.length > 0) { + for (WxMessageHandler i : otherHandlers) { + this.handlers.add(i); + } + } + return this; + } + + /** + * 规则结束 + * @return + */ + public WxMessageRouter end() { + this.routerBuilder.rules.add(this); + return this.routerBuilder; + } + + protected boolean test(WxUserMessage wxMessage) { + return + (this.msgType == null || this.msgType.equals(wxMessage.getMsgType())) + && + (this.event == null || this.event.equals(wxMessage.getEvent())) + && + (this.eventKey == null || this.eventKey.equals(wxMessage.getEventKey())) + && + (this.content == null || this.content.equals(wxMessage.getContent() == null ? null : wxMessage.getContent().trim())) + ; + } + + /** + * 处理微信推送过来的消息 + * @param wxMessage + * @return true 代表继续执行别的router,false 代表停止执行别的router + */ + protected boolean service(WxUserMessage wxMessage) { + // 如果不匹配本规则,那么接着执行后面的Rule + if (!test(wxMessage)) { + return true; + } + + Map context = new HashMap(); + // 如果拦截器不通过 + for (WxMessageInterceptor interceptor : this.interceptors) { + if (!interceptor.intercept(wxMessage, context)) { + return this.forward; + } + } + + // 交给handler处理 + for (WxMessageHandler interceptor : this.handlers) { + interceptor.handle(wxMessage, context); + } + + return this.forward; + } + + } + +} diff --git a/src/main/java/chanjarster/weixin/service/WxMsgType.java b/src/main/java/chanjarster/weixin/service/WxMsgType.java new file mode 100644 index 00000000..de305f5b --- /dev/null +++ b/src/main/java/chanjarster/weixin/service/WxMsgType.java @@ -0,0 +1,14 @@ +package chanjarster.weixin.service; + +public class WxMsgType { + + 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"; + +} diff --git a/src/main/java/chanjarster/weixin/service/WxService.java b/src/main/java/chanjarster/weixin/service/WxService.java new file mode 100644 index 00000000..4023a492 --- /dev/null +++ b/src/main/java/chanjarster/weixin/service/WxService.java @@ -0,0 +1,23 @@ +package chanjarster.weixin.service; + +import chanjarster.weixin.exception.WxErrorException; +import chanjarster.weixin.out.WxCustomMessage; +import chanjarster.weixin.out.WxMenu; + +/** + * 微信相关的常量 + */ +public interface WxService { + + public void refreshAccessToken() throws WxErrorException; + + public String sendCustomMessage(WxCustomMessage message) throws WxErrorException; + + public String createMenu(WxMenu menu) throws WxErrorException; + + public String deleteMenu() throws WxErrorException; + + public WxMenu getMenu() throws WxErrorException; + + public void setWxConfigProvider(WxConfigProvider wxConfigProvider); +} diff --git a/src/main/java/chanjarster/weixin/service/WxServiceImpl.java b/src/main/java/chanjarster/weixin/service/WxServiceImpl.java new file mode 100644 index 00000000..c11af531 --- /dev/null +++ b/src/main/java/chanjarster/weixin/service/WxServiceImpl.java @@ -0,0 +1,179 @@ +package chanjarster.weixin.service; + +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.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.StringEntity; +import org.apache.http.impl.client.BasicResponseHandler; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; + +import chanjarster.weixin.exception.WxErrorException; +import chanjarster.weixin.in.WxAccessToken; +import chanjarster.weixin.in.WxError; +import chanjarster.weixin.out.WxCustomMessage; +import chanjarster.weixin.out.WxMenu; + +public class WxServiceImpl implements WxService { + + /** + * 全局的是否正在刷新Access Token的flag + * true: 正在刷新 + * false: 没有刷新 + */ + protected static final AtomicBoolean GLOBAL_ACCESS_TOKEN_REFRESH_FLAG = new AtomicBoolean(false); + + protected static final CloseableHttpClient httpclient = HttpClients.createDefault(); + + protected static final Charset UTF8 = Charset.forName("UTF-8"); + + protected WxConfigProvider wxConfigProvider; + + /** + * 获得access_token + * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=获取access_token + * @return + * @throws WxErrorException + */ + public void refreshAccessToken() throws WxErrorException { + if (!GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.getAndSet(true)) { + try { + String url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential" + + "&appid=" + wxConfigProvider.getAppId() + + "&secret=" + wxConfigProvider.getSecret() + ; + try { + HttpGet httpGet = new HttpGet(url); + CloseableHttpResponse response = httpclient.execute(httpGet); + String resultContent = new BasicResponseHandler().handleResponse(response); + WxAccessToken accessToken = WxAccessToken.fromJson(resultContent); + wxConfigProvider.updateAccessToken(accessToken.getAccess_token(), accessToken.getExpires_in()); + } catch (ClientProtocolException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } finally { + GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.set(false); + } + } else { + // 每隔100ms检查一下是否刷新完毕了 + while (GLOBAL_ACCESS_TOKEN_REFRESH_FLAG.get()) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + } + } + // 刷新完毕了,就没他什么事儿了 + } + } + + /** + * 发送客服消息 + * 详情请见: 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()); + } + + protected String post(String uri, String data) throws WxErrorException { + return execute("POST", uri, data); + } + + protected String get(String uri, String data) throws WxErrorException { + return execute("GET", uri, data); + } + + /** + * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求 + * @param request + * @return 微信服务端返回的结果 + * @throws WxErrorException + */ + protected String execute(String method, String uri, String data) throws WxErrorException { + if (StringUtils.isBlank(wxConfigProvider.getAccessToken())) { + refreshAccessToken(); + } + String accessToken = wxConfigProvider.getAccessToken(); + + String uriWithAccessToken = uri; + uriWithAccessToken += uri.indexOf('?') == -1 ? "?access_token=" + accessToken : "&access_token=" + accessToken; + + try { + String resultContent = null; + if ("POST".equals(method)) { + HttpPost httpPost = new HttpPost(uriWithAccessToken); + StringEntity entity = new StringEntity(data, UTF8); + httpPost.setEntity(entity); + CloseableHttpResponse response = httpclient.execute(httpPost); + resultContent = new BasicResponseHandler().handleResponse(response); + } else if ("GET".equals(method)) { + HttpGet httpGet = new HttpGet(uriWithAccessToken); + CloseableHttpResponse response = httpclient.execute(httpGet); + resultContent = new BasicResponseHandler().handleResponse(response); + } + + WxError error = WxError.fromJson(resultContent); + /* + * 关于微信返回错误码 详情请看 http://mp.weixin.qq.com/wiki/index.php?title=全局返回码说明 + * 40001 微信图片不对 + * 42001 access_token超时 + */ + if (error.getErrcode() == 42001 || error.getErrcode() == 40001) { + refreshAccessToken(); + return execute(method, uri, data); + } + if (error.getErrcode() != 0) { + throw new WxErrorException(error); + } + return resultContent; + } catch (ClientProtocolException e) { + throw new RuntimeException(e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * + * @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) { + this.wxConfigProvider = wxConfigProvider; + } + +} diff --git a/src/main/java/chanjarster/weixin/util/AdapterCDATA.java b/src/main/java/chanjarster/weixin/util/AdapterCDATA.java new file mode 100644 index 00000000..581fcde4 --- /dev/null +++ b/src/main/java/chanjarster/weixin/util/AdapterCDATA.java @@ -0,0 +1,24 @@ +package chanjarster.weixin.util; + +import javax.xml.bind.annotation.adapters.XmlAdapter; + +/** + * + * http://stackoverflow.com/questions/14193944/jaxb-marshalling-unmarshalling-with-cdata + * + * @author chanjarster + * + */ +public class AdapterCDATA extends XmlAdapter { + + @Override + public String marshal(String arg0) throws Exception { + return ""; + } + + @Override + public String unmarshal(String arg0) throws Exception { + return arg0; + } + +} diff --git a/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java b/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java new file mode 100644 index 00000000..777db95b --- /dev/null +++ b/src/main/java/chanjarster/weixin/util/WxCustomMessageGsonAdapter.java @@ -0,0 +1,88 @@ +/* + * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved. + * + * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended + * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction + * arose from modification of the original source, or other redistribution of this source + * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD. + */ +package chanjarster.weixin.util; + +import java.lang.reflect.Type; + +import chanjarster.weixin.out.WxCustomMessage; +import chanjarster.weixin.out.WxCustomMessage.WxArticle; +import chanjarster.weixin.service.WxMsgType; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * + * @author qianjia + * + */ +public class WxCustomMessageGsonAdapter implements JsonSerializer { + + public JsonElement serialize(WxCustomMessage message, Type typeOfSrc, JsonSerializationContext context) { + JsonObject messageJson = new JsonObject(); + messageJson.addProperty("touser", message.getTouser()); + messageJson.addProperty("msgtype", message.getMsgtype()); + + if (WxMsgType.TEXT.equals(message.getMsgtype())) { + JsonObject text = new JsonObject(); + text.addProperty("content", message.getContent()); + messageJson.add("text", text); + } + + if (WxMsgType.IMAGE.equals(message.getMsgtype())) { + JsonObject image = new JsonObject(); + image.addProperty("media_id", message.getMedia_id()); + messageJson.add("image", image); + } + + if (WxMsgType.VOICE.equals(message.getMsgtype())) { + JsonObject voice = new JsonObject(); + voice.addProperty("media_id", message.getMedia_id()); + messageJson.add("voice", voice); + } + + if (WxMsgType.VIDEO.equals(message.getMsgtype())) { + JsonObject video = new JsonObject(); + video.addProperty("media_id", message.getMedia_id()); + video.addProperty("thumb_media_id", message.getThumb_media_id()); + video.addProperty("title", message.getTitle()); + video.addProperty("description", message.getDescription()); + messageJson.add("video", video); + } + + if (WxMsgType.MUSIC.equals(message.getMsgtype())) { + JsonObject music = new JsonObject(); + music.addProperty("title", message.getTitle()); + music.addProperty("description", message.getDescription()); + music.addProperty("thumb_media_id", message.getThumb_media_id()); + music.addProperty("musicurl", message.getMusicurl()); + music.addProperty("hqmusicurl", message.getHqmusicurl()); + messageJson.add("music", music); + } + + if (WxMsgType.NEWS.equals(message.getMsgtype())) { + JsonArray articleJsonArray = new JsonArray(); + for (WxArticle article : message.getArticles()) { + JsonObject articleJson = new JsonObject(); + articleJson.addProperty("title", article.getTitle()); + articleJson.addProperty("description", article.getDescription()); + articleJson.addProperty("url", article.getUrl()); + articleJson.addProperty("picurl", article.getPicurl()); + articleJsonArray.add(articleJson); + } + messageJson.add("articles", articleJsonArray); + } + + return messageJson; + } + +} diff --git a/src/main/java/chanjarster/weixin/util/WxGsonBuilder.java b/src/main/java/chanjarster/weixin/util/WxGsonBuilder.java new file mode 100644 index 00000000..4fb2f4b0 --- /dev/null +++ b/src/main/java/chanjarster/weixin/util/WxGsonBuilder.java @@ -0,0 +1,21 @@ +package chanjarster.weixin.util; + +import chanjarster.weixin.out.WxCustomMessage; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; + +public class WxGsonBuilder { + + public static final GsonBuilder INSTANCE = new GsonBuilder(); + + static { + INSTANCE.disableHtmlEscaping(); + INSTANCE.registerTypeAdapter(WxCustomMessage.class, new WxCustomMessageGsonAdapter()); + } + + public static Gson create() { + return INSTANCE.create(); + } + +} diff --git a/src/main/java/chanjarster/weixin/util/WxMenuGsonAdapter.java b/src/main/java/chanjarster/weixin/util/WxMenuGsonAdapter.java new file mode 100644 index 00000000..7ed08ad8 --- /dev/null +++ b/src/main/java/chanjarster/weixin/util/WxMenuGsonAdapter.java @@ -0,0 +1,55 @@ +/* + * KINGSTAR MEDIA SOLUTIONS Co.,LTD. Copyright c 2005-2013. All rights reserved. + * + * This source code is the property of KINGSTAR MEDIA SOLUTIONS LTD. It is intended + * only for the use of KINGSTAR MEDIA application development. Reengineering, reproduction + * arose from modification of the original source, or other redistribution of this source + * is not permitted without written permission of the KINGSTAR MEDIA SOLUTIONS LTD. + */ +package chanjarster.weixin.util; + +import java.lang.reflect.Type; + +import chanjarster.weixin.out.WxMenu; +import chanjarster.weixin.out.WxMenu.WxMenuButton; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonSerializationContext; +import com.google.gson.JsonSerializer; + +/** + * + * @author qianjia + * + */ +public class WxMenuGsonAdapter implements JsonSerializer { + + public JsonElement serialize(WxMenu menu, Type typeOfSrc, JsonSerializationContext context) { + JsonObject json = new JsonObject(); + + JsonArray buttonArray = new JsonArray(); + for (WxMenuButton button : menu.getButton()) { + JsonObject buttonJson = serialize(button); + buttonArray.add(buttonJson); + } + json.add("button", buttonArray); + + return json; + } + + protected JsonObject serialize(WxMenuButton button) { + JsonObject buttonJson = new JsonObject(); + buttonJson.addProperty("name", button.getName()); + // TODO 其他字段 + if (button.getSub_button() == null || button.getSub_button().size() == 0) { + JsonArray buttonArray = new JsonArray(); + for (WxMenuButton sub_button : button.getSub_button()) { + buttonArray.add(serialize(sub_button)); + } + buttonJson.add("sub_button", buttonArray); + } + return buttonJson; + } +} diff --git a/src/main/java/chanjarster/weixin/util/XmlTransformer.java b/src/main/java/chanjarster/weixin/util/XmlTransformer.java new file mode 100644 index 00000000..af83bf53 --- /dev/null +++ b/src/main/java/chanjarster/weixin/util/XmlTransformer.java @@ -0,0 +1,67 @@ +package chanjarster.weixin.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +import com.sun.xml.bind.marshaller.CharacterEscapeHandler; + +public class XmlTransformer { + + /** + * xml -> pojo + * @param clazz + * @param object + * @return + * @throws JAXBException + */ + public static T fromXml(Class clazz, String xml) throws JAXBException { + JAXBContext context = JAXBContext.newInstance(clazz); + Unmarshaller um = context.createUnmarshaller(); + T object = (T) um.unmarshal(new StringReader(xml)); + return object; + } + + 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); + return object; + } + + /** + * pojo -> xml + * @param clazz + * @return + * @throws JAXBException + */ + public static String toXml(Class clazz, T object) throws JAXBException { + StringWriter stringWriter = new StringWriter(); + toXml(clazz, object, stringWriter); + return stringWriter.getBuffer().toString(); + } + + public static void toXml(Class clazz, T object, Writer writer) throws JAXBException { + JAXBContext context = JAXBContext.newInstance(clazz); + Marshaller m = context.createMarshaller(); + m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); + m.setProperty(CharacterEscapeHandler.class.getName(), characterUnescapeHandler); + m.marshal(object, writer); + } + + protected static CharacterEscapeHandler characterUnescapeHandler = new CharacterUnescapeHandler(); + + protected static class CharacterUnescapeHandler implements CharacterEscapeHandler { + public void escape(char[] ac, int i, int j, boolean flag, Writer writer) throws IOException { + writer.write(ac, i, j); + } + } + +} diff --git a/src/test/java/chanjarster/weixin/in/WxErrorTest.java b/src/test/java/chanjarster/weixin/in/WxErrorTest.java new file mode 100644 index 00000000..c052de70 --- /dev/null +++ b/src/test/java/chanjarster/weixin/in/WxErrorTest.java @@ -0,0 +1,38 @@ +package chanjarster.weixin.in; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import chanjarster.weixin.in.WxError; + +@Test +public class WxErrorTest { + + public void testFromJson() { + + String json = "{ \"errcode\": 40003, \"errmsg\": \"invalid openid\" }"; + WxError wxError = WxError.fromJson(json); + Assert.assertTrue(wxError.getErrcode() == 40003); + Assert.assertEquals(wxError.getErrmsg(), "invalid openid"); + + } + + public void testFromBadJson1() { + + String json = "{ \"errcode\": 40003, \"errmsg\": \"invalid openid\", \"media_id\": \"12323423dsfafsf232f\" }"; + WxError wxError = WxError.fromJson(json); + Assert.assertTrue(wxError.getErrcode() == 40003); + Assert.assertEquals(wxError.getErrmsg(), "invalid openid"); + + } + + public void testFromBadJson2() { + + String json = "{\"access_token\":\"ACCESS_TOKEN\",\"expires_in\":7200}"; + WxError wxError = WxError.fromJson(json); + Assert.assertTrue(wxError.getErrcode() == 0); + Assert.assertEquals(wxError.getErrmsg(), null); + + } + +} diff --git a/src/test/java/chanjarster/weixin/out/WxCustomMessageTest.java b/src/test/java/chanjarster/weixin/out/WxCustomMessageTest.java new file mode 100644 index 00000000..8828be4f --- /dev/null +++ b/src/test/java/chanjarster/weixin/out/WxCustomMessageTest.java @@ -0,0 +1,83 @@ +package chanjarster.weixin.out; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import chanjarster.weixin.out.WxCustomMessage; +import chanjarster.weixin.out.WxCustomMessage.WxArticle; +import chanjarster.weixin.service.WxMsgType; +@Test +public class WxCustomMessageTest { + + public void testTextReply() { + WxCustomMessage reply = new WxCustomMessage(); + reply.setTouser("OPENID"); + reply.setMsgtype(WxMsgType.TEXT); + reply.setContent("sfsfdsdf"); + Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"text\",\"text\":{\"content\":\"sfsfdsdf\"}}"); + } + + public void testImageReply() { + WxCustomMessage reply = new WxCustomMessage(); + reply.setTouser("OPENID"); + reply.setMsgtype(WxMsgType.IMAGE); + reply.setMedia_id("MEDIA_ID"); + Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"image\",\"image\":{\"media_id\":\"MEDIA_ID\"}}"); + } + + public void testVoiceReply() { + WxCustomMessage reply = new WxCustomMessage(); + reply.setTouser("OPENID"); + reply.setMsgtype(WxMsgType.VOICE); + reply.setMedia_id("MEDIA_ID"); + Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"voice\",\"voice\":{\"media_id\":\"MEDIA_ID\"}}"); + } + + public void testVideoReply() { + WxCustomMessage reply = new WxCustomMessage(); + reply.setTouser("OPENID"); + reply.setMsgtype(WxMsgType.VIDEO); + reply.setMedia_id("MEDIA_ID"); + reply.setThumb_media_id("MEDIA_ID"); + reply.setTitle("TITLE"); + reply.setDescription("DESCRIPTION"); + Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"video\",\"video\":{\"media_id\":\"MEDIA_ID\",\"thumb_media_id\":\"MEDIA_ID\",\"title\":\"TITLE\",\"description\":\"DESCRIPTION\"}}"); + } + + public void testMusicReply() { + WxCustomMessage reply = new WxCustomMessage(); + reply.setTouser("OPENID"); + reply.setMsgtype(WxMsgType.MUSIC); + reply.setThumb_media_id("MEDIA_ID"); + reply.setDescription("DESCRIPTION"); + reply.setTitle("TITLE"); + reply.setMusicurl("MUSIC_URL"); + reply.setHqmusicurl("HQ_MUSIC_URL"); + Assert.assertEquals(reply.toJson(), "{\"touser\":\"OPENID\",\"msgtype\":\"music\",\"music\":{\"title\":\"TITLE\",\"description\":\"DESCRIPTION\",\"thumb_media_id\":\"MEDIA_ID\",\"musicurl\":\"MUSIC_URL\",\"hqmusicurl\":\"HQ_MUSIC_URL\"}}"); + } + + public void testNewsReply() { + WxCustomMessage reply = new WxCustomMessage(); + reply.setTouser("OPENID"); + reply.setMsgtype(WxMsgType.NEWS); + + WxArticle article1 = new WxArticle(); + article1.setUrl("URL"); + article1.setPicurl("PIC_URL"); + article1.setDescription("Is Really A Happy Day"); + article1.setTitle("Happy Day"); + reply.getArticles().add(article1); + + WxArticle article2 = new WxArticle(); + article2.setUrl("URL"); + article2.setPicurl("PIC_URL"); + article2.setDescription("Is Really A Happy Day"); + article2.setTitle("Happy Day"); + 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/out/WxUserMessageTest.java b/src/test/java/chanjarster/weixin/out/WxUserMessageTest.java new file mode 100644 index 00000000..2db5397d --- /dev/null +++ b/src/test/java/chanjarster/weixin/out/WxUserMessageTest.java @@ -0,0 +1,119 @@ +package chanjarster.weixin.out; + +import org.testng.Assert; +import org.testng.annotations.Test; + +import chanjarster.weixin.out.WxUserMessage; +import chanjarster.weixin.service.WxMsgType; + +@Test +public class WxUserMessageTest { + + public void testFromXml() { + + String xml = "" + + "" + + " " + + "1348831860" + + "" + + "" + + "1234567890123456" + + "" + + "" + + "" + + "" + + "23.134521" + + "113.358803" + + "20" + + "" + + "" + + "" + + "<![CDATA[公众平台官网链接]]>" + + "" + + "" + + "" + + "23.137466" + + "113.352425" + + "119.385040" + + ""; + WxUserMessage wxMessage = WxUserMessage.fromXml(xml); + Assert.assertEquals(wxMessage.getToUserName(), "toUser"); + Assert.assertEquals(wxMessage.getFromUserName(), "fromUser"); + Assert.assertEquals(wxMessage.getCreateTime(), new Long(1348831860l)); + Assert.assertEquals(wxMessage.getMsgType(), WxMsgType.TEXT); + Assert.assertEquals(wxMessage.getContent(), "this is a test"); + Assert.assertEquals(wxMessage.getMsgId(), new Long(1234567890123456l)); + Assert.assertEquals(wxMessage.getPicUrl(), "this is a url"); + Assert.assertEquals(wxMessage.getMediaId(), "media_id"); + Assert.assertEquals(wxMessage.getFormat(), "Format"); + Assert.assertEquals(wxMessage.getThumbMediaId(), "thumb_media_id"); + Assert.assertEquals(wxMessage.getLocation_X(), new Double(23.134521d)); + Assert.assertEquals(wxMessage.getLocation_Y(), new Double(113.358803d)); + Assert.assertEquals(wxMessage.getScale(), new Double(20)); + Assert.assertEquals(wxMessage.getLabel(), "位置信息"); + Assert.assertEquals(wxMessage.getDescription(), "公众平台官网链接"); + Assert.assertEquals(wxMessage.getUrl(), "url"); + Assert.assertEquals(wxMessage.getTitle(), "公众平台官网链接"); + Assert.assertEquals(wxMessage.getEvent(), "subscribe"); + Assert.assertEquals(wxMessage.getEventKey(), "qrscene_123123"); + Assert.assertEquals(wxMessage.getTicket(), "TICKET"); + Assert.assertEquals(wxMessage.getLatitude(), new Double(23.137466)); + Assert.assertEquals(wxMessage.getLongitude(), new Double(113.352425)); + Assert.assertEquals(wxMessage.getPrecision(), new Double(119.385040)); + } + + public void testToXml() { + WxUserMessage wxMessage = new WxUserMessage(); + wxMessage.setToUserName("toUser"); + wxMessage.setFromUserName("fromUser"); + wxMessage.setCreateTime(new Long(1348831860l)); + wxMessage.setMsgType(WxMsgType.TEXT); + wxMessage.setContent("this is a test"); + wxMessage.setMsgId(new Long(1234567890123456l)); + wxMessage.setPicUrl("this is a url"); + wxMessage.setMediaId("media_id"); + wxMessage.setFormat("Format"); + wxMessage.setThumbMediaId("thumb_media_id"); + wxMessage.setLocation_X(new Double(23.134521d)); + wxMessage.setLocation_Y(new Double(113.358803d)); + wxMessage.setScale(new Double(20)); + wxMessage.setLabel("位置信息"); + wxMessage.setDescription("公众平台官网链接"); + wxMessage.setUrl("url"); + wxMessage.setTitle("公众平台官网链接"); + wxMessage.setEvent("subscribe"); + wxMessage.setEventKey("qrscene_123123"); + wxMessage.setTicket("TICKET"); + wxMessage.setLatitude(new Double(23.137466)); + wxMessage.setLongitude(new Double(113.352425)); + wxMessage.setPrecision(new Double(119.385040)); + + String xml = "\n" + + "\n" + + " \n" + + " \n" + + " 1348831860\n" + + " \n" + + " \n" + + " 1234567890123456\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 23.134521\n" + + " 113.358803\n" + + " 20.0\n" + + " \n" + + " <![CDATA[公众平台官网链接]]>\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " 23.137466\n" + + " 113.352425\n" + + " 119.38504\n" + + "\n"; + Assert.assertEquals(wxMessage.toXml(), xml); + } +} diff --git a/src/test/java/chanjarster/weixin/service/WxMessageRouterTest.java b/src/test/java/chanjarster/weixin/service/WxMessageRouterTest.java new file mode 100644 index 00000000..e963f646 --- /dev/null +++ b/src/test/java/chanjarster/weixin/service/WxMessageRouterTest.java @@ -0,0 +1,6 @@ +package chanjarster.weixin.service; + +// TODO +public class WxMessageRouterTest { + +} diff --git a/src/test/java/chanjarster/weixin/service/WxServiceTest.java b/src/test/java/chanjarster/weixin/service/WxServiceTest.java new file mode 100644 index 00000000..e45c5d31 --- /dev/null +++ b/src/test/java/chanjarster/weixin/service/WxServiceTest.java @@ -0,0 +1,125 @@ +package chanjarster.weixin.service; + +import java.io.InputStream; + +import javax.xml.bind.JAXBException; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +import org.apache.commons.lang3.StringUtils; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import chanjarster.weixin.exception.WxErrorException; +import chanjarster.weixin.out.WxCustomMessage; +import chanjarster.weixin.util.XmlTransformer; + +public class WxServiceTest { + + @Test(dataProvider = "configs") + public void testRefreshAccessToken(WxConfigProvider config) throws WxErrorException { + String before = config.getAccessToken(); + + WxService wxService = new WxServiceImpl(); + wxService.setWxConfigProvider(config); + wxService.refreshAccessToken(); + + String after = config.getAccessToken(); + + Assert.assertNotEquals(before, after); + Assert.assertTrue(StringUtils.isNotBlank(after)); + } + + @Test(dataProvider = "configs") + public void sendCustomMessage(SimpleWxConfigProvider config) throws WxErrorException { + WxService wxService = new WxServiceImpl(); + wxService.setWxConfigProvider(config); + + WxCustomMessage message = new WxCustomMessage(); + message.setMsgtype(WxMsgType.TEXT); + message.setTouser(config.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"); + SimpleWxConfigProvider config1 = XmlTransformer.fromXml(SimpleWxConfigProvider.class, is1); + return new Object[][] { + new Object[] { + config1 + } + }; + } + + @XmlRootElement(name = "xml") + @XmlAccessorType(XmlAccessType.FIELD) + public static class SimpleWxConfigProvider implements WxConfigProvider { + private String appId; + private String secret; + private String accessToken = ""; + private Integer expiresIn; + private String token; + private String openId; + public void updateAccessToken(String accessToken, Integer expiresIn) { + this.accessToken = accessToken; + this.expiresIn = expiresIn; + } + public String getAccessToken() { + return accessToken; + } + public String getAppId() { + return appId; + } + public String getSecret() { + return secret; + } + public String getToken() { + return token; + } + 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 Integer getExpiresIn() { + return expiresIn; + } + public void setExpiresIn(Integer expiresIn) { + this.expiresIn = expiresIn; + } + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + public String getOpenId() { + return openId; + } + public void setOpenId(String openId) { + this.openId = openId; + } + @Override + public String toString() { + return "SimpleWxConfigProvider [appId=" + appId + ", secret=" + secret + ", accessToken=" + accessToken + + ", expiresIn=" + expiresIn + ", token=" + token + ", openId=" + openId + "]"; + } + + } + + +} diff --git a/src/test/resources/test-config.sample.xml b/src/test/resources/test-config.sample.xml new file mode 100644 index 00000000..7c61d3dd --- /dev/null +++ b/src/test/resources/test-config.sample.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file