| @@ -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 | |||
| @@ -0,0 +1,66 @@ | |||
| <?xml version="1.0"?> | |||
| <project | |||
| xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" | |||
| xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> | |||
| <modelVersion>4.0.0</modelVersion> | |||
| <groupId>chanjarster.weixin</groupId> | |||
| <artifactId>weixin-java</artifactId> | |||
| <version>1.0.0-SNAPSHOT</version> | |||
| <name>WeiXin Java Toolset</name> | |||
| <url>https://github.com/chanjarster/weixin-java</url> | |||
| <properties> | |||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
| <downloadJavadocs>true</downloadJavadocs> | |||
| <downloadSources>true</downloadSources> | |||
| </properties> | |||
| <dependencies> | |||
| <dependency> | |||
| <groupId>junit</groupId> | |||
| <artifactId>junit</artifactId> | |||
| <version>4.11</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>fluent-hc</artifactId> | |||
| <version>4.3.5</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>javax.xml.bind</groupId> | |||
| <artifactId>jaxb-api</artifactId> | |||
| <version>2.2.7</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.sun.xml.bind</groupId> | |||
| <artifactId>jaxb-impl</artifactId> | |||
| <version>2.2.7</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.apache.oltu.oauth2</groupId> | |||
| <artifactId>org.apache.oltu.oauth2.client</artifactId> | |||
| <version>1.0.0</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>com.google.code.gson</groupId> | |||
| <artifactId>gson</artifactId> | |||
| <version>2.2.2</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.testng</groupId> | |||
| <artifactId>testng</artifactId> | |||
| <version>6.8.7</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.mockito</groupId> | |||
| <artifactId>mockito-all</artifactId> | |||
| <version>1.9.5</version> | |||
| <scope>test</scope> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.apache.commons</groupId> | |||
| <artifactId>commons-lang3</artifactId> | |||
| <version>3.1</version> | |||
| </dependency> | |||
| </dependencies> | |||
| </project> | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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 + "}"; | |||
| } | |||
| } | |||
| @@ -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<WxArticle> articles = new ArrayList<WxArticle>(); | |||
| 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<WxArticle> getArticles() { | |||
| return articles; | |||
| } | |||
| public void setArticles(List<WxArticle> 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,74 @@ | |||
| package chanjarster.weixin.out; | |||
| import java.util.ArrayList; | |||
| import java.util.List; | |||
| /** | |||
| * 公众号菜单 | |||
| * @author chanjarster | |||
| * | |||
| */ | |||
| public class WxMenu { | |||
| protected List<WxMenuButton> button = new ArrayList<WxMenuButton>(); | |||
| public List<WxMenuButton> getButton() { | |||
| return button; | |||
| } | |||
| public void setButton(List<WxMenuButton> button) { | |||
| this.button = button; | |||
| } | |||
| public static class WxMenuButton { | |||
| protected String type; | |||
| protected String name; | |||
| protected String key; | |||
| protected String url; | |||
| protected List<WxMenuButton> sub_button = new ArrayList<WxMenuButton>(); | |||
| 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<WxMenuButton> getSub_button() { | |||
| return sub_button; | |||
| } | |||
| public void setSub_button(List<WxMenuButton> sub_button) { | |||
| this.sub_button = sub_button; | |||
| } | |||
| } | |||
| } | |||
| @@ -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; | |||
| /** | |||
| * <pre> | |||
| * 微信推送过来的消息,同时也是同步回复给用户的消息 | |||
| * 相关字段的解释看微信开发者文档: | |||
| * 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=接收语音识别结果 | |||
| * </pre> | |||
| * @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; | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| @@ -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<String, Object> context); | |||
| } | |||
| @@ -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<String, Object> context); | |||
| } | |||
| @@ -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<Rule> rules = new ArrayList<Rule>(); | |||
| /** | |||
| * 开始一个新的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<WxMessageHandler> handlers = new ArrayList<WxMessageHandler>(); | |||
| private List<WxMessageInterceptor> interceptors = new ArrayList<WxMessageInterceptor>(); | |||
| 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<String, Object> context = new HashMap<String, Object>(); | |||
| // 如果拦截器不通过 | |||
| 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; | |||
| } | |||
| } | |||
| } | |||
| @@ -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"; | |||
| } | |||
| @@ -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); | |||
| } | |||
| @@ -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; | |||
| } | |||
| } | |||
| @@ -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<String, String> { | |||
| @Override | |||
| public String marshal(String arg0) throws Exception { | |||
| return "<![CDATA[" + arg0 + "]]>"; | |||
| } | |||
| @Override | |||
| public String unmarshal(String arg0) throws Exception { | |||
| return arg0; | |||
| } | |||
| } | |||
| @@ -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<WxCustomMessage> { | |||
| 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; | |||
| } | |||
| } | |||
| @@ -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(); | |||
| } | |||
| } | |||
| @@ -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<WxMenu> { | |||
| 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; | |||
| } | |||
| } | |||
| @@ -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> T fromXml(Class<T> 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> T fromXml(Class<T> 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 <T> String toXml(Class<T> clazz, T object) throws JAXBException { | |||
| StringWriter stringWriter = new StringWriter(); | |||
| toXml(clazz, object, stringWriter); | |||
| return stringWriter.getBuffer().toString(); | |||
| } | |||
| public static <T> void toXml(Class<T> 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); | |||
| } | |||
| } | |||
| } | |||
| @@ -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); | |||
| } | |||
| } | |||
| @@ -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\"}]}"); | |||
| } | |||
| } | |||
| @@ -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 = "<xml>" | |||
| + "<ToUserName><![CDATA[toUser]]></ToUserName>" | |||
| + "<FromUserName><![CDATA[fromUser]]></FromUserName> " | |||
| + "<CreateTime>1348831860</CreateTime>" | |||
| + "<MsgType><![CDATA[text]]></MsgType>" | |||
| + "<Content><![CDATA[this is a test]]></Content>" | |||
| + "<MsgId>1234567890123456</MsgId>" | |||
| + "<PicUrl><![CDATA[this is a url]]></PicUrl>" | |||
| + "<MediaId><![CDATA[media_id]]></MediaId>" | |||
| + "<Format><![CDATA[Format]]></Format>" | |||
| + "<ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>" | |||
| + "<Location_X>23.134521</Location_X>" | |||
| + "<Location_Y>113.358803</Location_Y>" | |||
| + "<Scale>20</Scale>" | |||
| + "<Label><![CDATA[位置信息]]></Label>" | |||
| + "<Description><![CDATA[公众平台官网链接]]></Description>" | |||
| + "<Url><![CDATA[url]]></Url>" | |||
| + "<Title><![CDATA[公众平台官网链接]]></Title>" | |||
| + "<Event><![CDATA[subscribe]]></Event>" | |||
| + "<EventKey><![CDATA[qrscene_123123]]></EventKey>" | |||
| + "<Ticket><![CDATA[TICKET]]></Ticket>" | |||
| + "<Latitude>23.137466</Latitude>" | |||
| + "<Longitude>113.352425</Longitude>" | |||
| + "<Precision>119.385040</Precision>" | |||
| + "</xml>"; | |||
| 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 = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n" | |||
| + "<xml>\n" | |||
| + " <ToUserName><![CDATA[toUser]]></ToUserName>\n" | |||
| + " <FromUserName><![CDATA[fromUser]]></FromUserName>\n" | |||
| + " <CreateTime>1348831860</CreateTime>\n" | |||
| + " <MsgType><![CDATA[text]]></MsgType>\n" | |||
| + " <Content><![CDATA[this is a test]]></Content>\n" | |||
| + " <MsgId>1234567890123456</MsgId>\n" | |||
| + " <PicUrl><![CDATA[this is a url]]></PicUrl>\n" | |||
| + " <MediaId><![CDATA[media_id]]></MediaId>\n" | |||
| + " <Format><![CDATA[Format]]></Format>\n" | |||
| + " <ThumbMediaId><![CDATA[thumb_media_id]]></ThumbMediaId>\n" | |||
| + " <Location_X>23.134521</Location_X>\n" | |||
| + " <Location_Y>113.358803</Location_Y>\n" | |||
| + " <Scale>20.0</Scale>\n" | |||
| + " <Label><![CDATA[位置信息]]></Label>\n" | |||
| + " <Title><![CDATA[公众平台官网链接]]></Title>\n" | |||
| + " <Description><![CDATA[公众平台官网链接]]></Description>\n" | |||
| + " <Url><![CDATA[url]]></Url>\n" | |||
| + " <Event><![CDATA[subscribe]]></Event>\n" | |||
| + " <EventKey><![CDATA[qrscene_123123]]></EventKey>\n" | |||
| + " <Ticket><![CDATA[TICKET]]></Ticket>\n" | |||
| + " <Latitude>23.137466</Latitude>\n" | |||
| + " <Longitude>113.352425</Longitude>\n" | |||
| + " <Precision>119.38504</Precision>\n" | |||
| + "</xml>\n"; | |||
| Assert.assertEquals(wxMessage.toXml(), xml); | |||
| } | |||
| } | |||
| @@ -0,0 +1,6 @@ | |||
| package chanjarster.weixin.service; | |||
| // TODO | |||
| public class WxMessageRouterTest { | |||
| } | |||
| @@ -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下面\n<a href=\"http://192.168.1.249:9180/eams-rc/login.action\">Hello World</a>"); | |||
| 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 + "]"; | |||
| } | |||
| } | |||
| } | |||
| @@ -0,0 +1,7 @@ | |||
| <xml> | |||
| <appId></appId> | |||
| <secret></secret> | |||
| <accessToken></accessToken> | |||
| <expiresIn></expiresIn> | |||
| <openId></openId> | |||
| </xml> | |||