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