@@ -1,4 +1,5 @@ | |||||
*.class | *.class | ||||
test-output | |||||
# Mobile Tools for Java (J2ME) | # Mobile Tools for Java (J2ME) | ||||
.mtj.tmp/ | .mtj.tmp/ | ||||
@@ -10,3 +11,9 @@ | |||||
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml | ||||
hs_err_pid* | 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> |