From 001a2742c6753b00394503850994d77b1e81a049 Mon Sep 17 00:00:00 2001 From: feye Date: Fri, 24 May 2019 13:42:13 +0800 Subject: [PATCH] =?UTF-8?q?#493=20=E4=BC=81=E4=B8=9A=E5=BE=AE=E4=BF=A1?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=AC=AC=E4=B8=89=E6=96=B9=E5=BA=94=E7=94=A8?= =?UTF-8?q?=E5=BC=80=E5=8F=91=E6=8E=A5=E5=8F=A3=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../chanjar/weixin/cp/api/WxCpTpService.java | 169 ++++++++++++ .../cp/api/impl/BaseWxCpTpServiceImpl.java | 244 +++++++++++++++++ .../cp/api/impl/WxCpServiceOnTpImpl.java | 37 +++ .../WxCpTpServiceApacheHttpClientImpl.java | 114 ++++++++ .../weixin/cp/api/impl/WxCpTpServiceImpl.java | 12 + .../me/chanjar/weixin/cp/bean/WxCpTpCorp.java | 42 +++ .../weixin/cp/bean/WxCpTpXmlMessage.java | 68 +++++ .../weixin/cp/bean/WxCpTpXmlPackage.java | 55 ++++ .../weixin/cp/config/WxCpTpConfigStorage.java | 72 +++++ .../config/WxCpTpInMemoryConfigStorage.java | 230 ++++++++++++++++ .../cp/util/crypto/WxCpTpCryptUtil.java | 52 ++++ .../cp/util/xml/XStreamTransformer.java | 252 ++++++++++-------- 12 files changed, 1230 insertions(+), 117 deletions(-) create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOnTpImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTpServiceApacheHttpClientImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTpServiceImpl.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlMessage.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlPackage.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpInMemoryConfigStorage.java create mode 100644 weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpTpCryptUtil.java diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java new file mode 100644 index 00000000..741d27de --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/WxCpTpService.java @@ -0,0 +1,169 @@ +package me.chanjar.weixin.cp.api; + +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.MediaUploadRequestExecutor; +import me.chanjar.weixin.common.util.http.RequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult; +import me.chanjar.weixin.cp.bean.WxCpTpCorp; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; + +/** + * 微信第三方应用API的Service + * @author zhenjun cai + */ +public interface WxCpTpService { + String JSCODE_TO_SESSION_URL = "https://qyapi.weixin.qq.com/cgi-bin/service/miniprogram/jscode2session"; + String GET_CORP_TOKEN = "https://qyapi.weixin.qq.com/cgi-bin/service/get_corp_token"; + String GET_PERMANENT_CODE = "https://qyapi.weixin.qq.com/cgi-bin/service/get_permanent_code"; + /** + *
+   * 验证推送过来的消息的正确性
+   * 详情请见: https://work.weixin.qq.com/api/doc#90000/90139/90968/消息体签名校验
+   * 
+ * + * @param msgSignature 消息签名 + * @param timestamp 时间戳 + * @param nonce 随机数 + * @param data 微信传输过来的数据,有可能是echoStr,有可能是xml消息 + */ + boolean checkSignature(String msgSignature, String timestamp, String nonce, String data); + + /** + * 获取suite_access_token, 不强制刷新suite_access_token + * + * @see #getSuiteAccessToken(boolean) + */ + String getSuiteAccessToken() throws WxErrorException; + + /** + *
+   * 获取suite_access_token,本方法线程安全
+   * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
+   * 另:本service的所有方法都会在suite_access_token过期是调用此方法
+   * 程序员在非必要情况下尽量不要主动调用此方法
+   * 详情请见: https://work.weixin.qq.com/api/doc#90001/90143/90600
+   * 
+ * + * @param forceRefresh 强制刷新 + */ + String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException; + + /** + * 获得suite_ticket,不强制刷新suite_ticket + * + * @see #getSuiteTicket(boolean) + */ + String getSuiteTicket() throws WxErrorException; + + /** + *
+   * 获得suite_ticket
+   * 由于suite_ticket是微信服务器定时推送(每10分钟),不能主动获取,如果碰到过期只能抛异常
+   *
+   * 详情请见:https://work.weixin.qq.com/api/doc#90001/90143/90628
+   * 
+ * + * @param forceRefresh 强制刷新 + */ + String getSuiteTicket(boolean forceRefresh) throws WxErrorException; + + /** + * 小程序登录凭证校验 + * + * @param jsCode 登录时获取的 code + */ + WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException; + + /** + * 获取企业凭证 + * @param authCorpid 授权方corpid + * @param permanentCode 永久授权码,通过get_permanent_code获取 + */ + WxAccessToken getCorpToken(String authCorpid, String permanentCode) throws WxErrorException; + + /** + * 获取企业永久授权码 + * @param authCode + * @return + */ + WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException; + + /** + * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的GET请求 + * + * @param url 接口地址 + * @param queryParam 请求参数 + */ + String get(String url, String queryParam) throws WxErrorException; + + /** + * 当本Service没有实现某个API的时候,可以用这个,针对所有微信API中的POST请求 + * + * @param url 接口地址 + * @param postData 请求body字符串 + */ + String post(String url, String postData) throws WxErrorException; + + /** + *
+   * Service没有实现某个API的时候,可以用这个,
+   * 比{@link #get}和{@link #post}方法更灵活,可以自己构造RequestExecutor用来处理不同的参数和不同的返回类型。
+   * 可以参考,{@link MediaUploadRequestExecutor}的实现方法
+   * 
+ * + * @param executor 执行器 + * @param uri 请求地址 + * @param data 参数 + * @param 请求值类型 + * @param 返回值类型 + */ + T execute(RequestExecutor executor, String uri, E data) throws WxErrorException; + + /** + *
+   * 设置当微信系统响应系统繁忙时,要等待多少 retrySleepMillis(ms) * 2^(重试次数 - 1) 再发起重试
+   * 默认:1000ms
+   * 
+ * + * @param retrySleepMillis 重试休息时间 + */ + void setRetrySleepMillis(int retrySleepMillis); + + /** + *
+   * 设置当微信系统响应系统繁忙时,最大重试次数
+   * 默认:5次
+   * 
+ * + * @param maxRetryTimes 最大重试次数 + */ + void setMaxRetryTimes(int maxRetryTimes); + + /** + * 初始化http请求对象 + */ + void initHttp(); + + /** + * 获取WxMpConfigStorage 对象 + * + * @return WxMpConfigStorage + */ + WxCpTpConfigStorage getWxCpTpConfigStorage(); + + /** + * 注入 {@link WxCpTpConfigStorage} 的实现 + * + * @param wxConfigProvider 配置对象 + */ + void setWxCpTpConfigStorage(WxCpTpConfigStorage wxConfigProvider); + + /** + * http请求对象 + */ + RequestHttp getRequestHttp(); + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java new file mode 100644 index 00000000..837c355e --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/BaseWxCpTpServiceImpl.java @@ -0,0 +1,244 @@ +package me.chanjar.weixin.cp.api.impl; + +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Joiner; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.DataUtils; +import me.chanjar.weixin.common.util.crypto.SHA1; +import me.chanjar.weixin.common.util.http.RequestExecutor; +import me.chanjar.weixin.common.util.http.RequestHttp; +import me.chanjar.weixin.common.util.http.SimpleGetRequestExecutor; +import me.chanjar.weixin.common.util.http.SimplePostRequestExecutor; +import me.chanjar.weixin.cp.api.WxCpService; +import me.chanjar.weixin.cp.api.WxCpTpService; +import me.chanjar.weixin.cp.bean.WxCpMaJsCode2SessionResult; +import me.chanjar.weixin.cp.bean.WxCpTpCorp; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; + +/** + * @author zhenjun cai + */ +public abstract class BaseWxCpTpServiceImpl implements WxCpTpService, RequestHttp { + protected final Logger log = LoggerFactory.getLogger(this.getClass()); + + /** + * 全局的是否正在刷新access token的锁 + */ + protected final Object globalSuiteAccessTokenRefreshLock = new Object(); + + /** + * 全局的是否正在刷新jsapi_ticket的锁 + */ + protected final Object globalSuiteTicketRefreshLock = new Object(); + + + protected WxCpTpConfigStorage configStorage; + + + /** + * 临时文件目录 + */ + private File tmpDirFile; + private int retrySleepMillis = 1000; + private int maxRetryTimes = 5; + + @Override + public boolean checkSignature(String msgSignature, String timestamp, String nonce, String data) { + try { + return SHA1.gen(this.configStorage.getToken(), timestamp, nonce, data) + .equals(msgSignature); + } catch (Exception e) { + this.log.error("Checking signature failed, and the reason is :" + e.getMessage()); + return false; + } + } + + @Override + public String getSuiteAccessToken() throws WxErrorException { + return getSuiteAccessToken(false); + } + + @Override + public String getSuiteTicket() throws WxErrorException { + return getSuiteTicket(false); + } + + @Override + public String getSuiteTicket(boolean forceRefresh) throws WxErrorException { +// suite ticket由微信服务器推送,不能强制刷新 +// if (forceRefresh) { +// this.configStorage.expireSuiteTicket(); +// } + + if (this.configStorage.isSuiteTicketExpired()) { +// 本地suite ticket 不存在或者过期 + WxError wxError = WxError.fromJson("{\"errcode\":40085, \"errmsg\":\"invaild suite ticket\"}", WxType.CP); + throw new WxErrorException(wxError); + } + return this.configStorage.getSuiteTicket(); + } + + + @Override + public WxCpMaJsCode2SessionResult jsCode2Session(String jsCode) throws WxErrorException { + Map params = new HashMap<>(2); + params.put("js_code", jsCode); + params.put("grant_type", "authorization_code"); + + String result = this.get(JSCODE_TO_SESSION_URL, Joiner.on("&").withKeyValueSeparator("=").join(params)); + return WxCpMaJsCode2SessionResult.fromJson(result); + } + + + @Override + public WxAccessToken getCorpToken(String authCorpid, String permanentCode) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("auth_corpid", authCorpid); + jsonObject.addProperty("permanent_code", permanentCode); + String result = post(GET_CORP_TOKEN, jsonObject.toString()); + + return WxAccessToken.fromJson(result); + } + + + @Override + public WxCpTpCorp getPermanentCode(String authCode) throws WxErrorException { + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("auth_code", authCode); + + String result = post(GET_PERMANENT_CODE, jsonObject.toString()); + jsonObject = new JsonParser().parse(result).getAsJsonObject(); + WxCpTpCorp wxCpTpCorp = WxCpTpCorp.fromJson(jsonObject.get("auth_corp_info").getAsString()); + wxCpTpCorp.setPermanentCode(jsonObject.get("permanent_code").getAsString()); + return wxCpTpCorp; + } + + @Override + public String get(String url, String queryParam) throws WxErrorException { + return execute(SimpleGetRequestExecutor.create(this), url, queryParam); + } + + @Override + public String post(String url, String postData) throws WxErrorException { + return execute(SimplePostRequestExecutor.create(this), url, postData); + } + + /** + * 向微信端发送请求,在这里执行的策略是当发生access_token过期时才去刷新,然后重新执行请求,而不是全局定时请求. + */ + @Override + public T execute(RequestExecutor executor, String uri, E data) throws WxErrorException { + int retryTimes = 0; + do { + try { + return this.executeInternal(executor, uri, data); + } catch (WxErrorException e) { + if (retryTimes + 1 > this.maxRetryTimes) { + this.log.warn("重试达到最大次数【{}】", this.maxRetryTimes); + //最后一次重试失败后,直接抛出异常,不再等待 + throw new RuntimeException("微信服务端异常,超出重试次数"); + } + + WxError error = e.getError(); + /* + * -1 系统繁忙, 1000ms后重试 + */ + if (error.getErrorCode() == -1) { + int sleepMillis = this.retrySleepMillis * (1 << retryTimes); + try { + this.log.debug("微信系统繁忙,{} ms 后重试(第{}次)", sleepMillis, retryTimes + 1); + Thread.sleep(sleepMillis); + } catch (InterruptedException e1) { + Thread.currentThread().interrupt(); + } + } else { + throw e; + } + } + } while (retryTimes++ < this.maxRetryTimes); + + this.log.warn("重试达到最大次数【{}】", this.maxRetryTimes); + throw new RuntimeException("微信服务端异常,超出重试次数"); + } + + protected T executeInternal(RequestExecutor executor, String uri, E data) throws WxErrorException { + E dataForLog = DataUtils.handleDataWithSecret(data); + + if (uri.contains("suite_access_token=")) { + throw new IllegalArgumentException("uri参数中不允许有suite_access_token: " + uri); + } + String suiteAccessToken = getSuiteAccessToken(false); + + String uriWithAccessToken = uri + (uri.contains("?") ? "&" : "?") + "suite_access_token=" + suiteAccessToken; + + try { + T result = executor.execute(uriWithAccessToken, data); + this.log.debug("\n【请求地址】: {}\n【请求参数】:{}\n【响应数据】:{}", uriWithAccessToken, dataForLog, result); + return result; + } catch (WxErrorException e) { + WxError error = e.getError(); + /* + * 发生以下情况时尝试刷新suite_access_token + * 42009 suite_access_token已过期 + */ + if (error.getErrorCode() == 42009) { + // 强制设置wxCpTpConfigStorage它的suite access token过期了,这样在下一次请求里就会刷新suite access token + this.configStorage.expireSuiteAccessToken(); + return execute(executor, uri, data); + } + + if (error.getErrorCode() != 0) { + this.log.error("\n【请求地址】: {}\n【请求参数】:{}\n【错误信息】:{}", uriWithAccessToken, dataForLog, error); + throw new WxErrorException(error, e); + } + return null; + } catch (IOException e) { + this.log.error("\n【请求地址】: {}\n【请求参数】:{}\n【异常信息】:{}", uriWithAccessToken, dataForLog, e.getMessage()); + throw new RuntimeException(e); + } + } + + @Override + public void setWxCpTpConfigStorage(WxCpTpConfigStorage wxConfigProvider) { + this.configStorage = wxConfigProvider; + this.initHttp(); + } + + @Override + public void setRetrySleepMillis(int retrySleepMillis) { + this.retrySleepMillis = retrySleepMillis; + } + + + @Override + public void setMaxRetryTimes(int maxRetryTimes) { + this.maxRetryTimes = maxRetryTimes; + } + + public File getTmpDirFile() { + return this.tmpDirFile; + } + + public void setTmpDirFile(File tmpDirFile) { + this.tmpDirFile = tmpDirFile; + } + + @Override + public RequestHttp getRequestHttp() { + return this; + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOnTpImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOnTpImpl.java new file mode 100644 index 00000000..1b7a67c6 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpServiceOnTpImpl.java @@ -0,0 +1,37 @@ +package me.chanjar.weixin.cp.api.impl; + +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.cp.api.WxCpTpService; + +/** + *
+ *  默认接口实现类,使用apache httpclient实现,配合第三方应用service使用
+ * Created by zhenjun cai.
+ * 
+ * + * @author Binary Wang + */ +public class WxCpServiceOnTpImpl extends WxCpServiceApacheHttpClientImpl { + //第三方应用service + WxCpTpService wxCpTpService; + + public void setWxCpTpService(WxCpTpService wxCpTpService) { + this.wxCpTpService = wxCpTpService; + } + + @Override + public String getAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getAccessToken(); + } + //access token通过第三方应用service获取 + //corpSecret对应企业永久授权码 + WxAccessToken accessToken = wxCpTpService.getCorpToken(this.configStorage.getCorpId(), this.configStorage.getCorpSecret()); + + this.configStorage.updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn()); + return this.configStorage.getAccessToken(); + } + + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTpServiceApacheHttpClientImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTpServiceApacheHttpClientImpl.java new file mode 100644 index 00000000..0b8a134d --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTpServiceApacheHttpClientImpl.java @@ -0,0 +1,114 @@ +package me.chanjar.weixin.cp.api.impl; + + +import java.io.IOException; + +import org.apache.http.Consts; +import org.apache.http.HttpHost; +import org.apache.http.client.config.RequestConfig; +import org.apache.http.client.methods.CloseableHttpResponse; +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 com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import me.chanjar.weixin.common.WxType; +import me.chanjar.weixin.common.error.WxError; +import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.util.http.HttpType; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.common.util.http.apache.DefaultApacheHttpClientBuilder; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; + +public class WxCpTpServiceApacheHttpClientImpl extends BaseWxCpTpServiceImpl { + protected CloseableHttpClient httpClient; + protected HttpHost httpProxy; + + @Override + public CloseableHttpClient getRequestHttpClient() { + return httpClient; + } + + @Override + public HttpHost getRequestHttpProxy() { + return httpProxy; + } + + @Override + public HttpType getRequestType() { + return HttpType.APACHE_HTTP; + } + + @Override + public String getSuiteAccessToken(boolean forceRefresh) throws WxErrorException { + if (!this.configStorage.isSuiteAccessTokenExpired() && !forceRefresh) { + return this.configStorage.getSuiteAccessToken(); + } + + synchronized (this.globalSuiteAccessTokenRefreshLock) { + String url = "https://qyapi.weixin.qq.com/cgi-bin/service/get_suite_token"; + try { + HttpPost httpPost = new HttpPost(url); + if (this.httpProxy != null) { + RequestConfig config = RequestConfig.custom() + .setProxy(this.httpProxy).build(); + httpPost.setConfig(config); + } + JsonObject jsonObject = new JsonObject(); + jsonObject.addProperty("suite_id", this.configStorage.getSuiteId()); + jsonObject.addProperty("suite_secret", this.configStorage.getSuiteSecret()); + jsonObject.addProperty("suite_ticket", this.getSuiteTicket()); + StringEntity entity = new StringEntity(jsonObject.toString(), Consts.UTF_8); + httpPost.setEntity(entity); + + String resultContent; + try (CloseableHttpClient httpclient = getRequestHttpClient(); + CloseableHttpResponse response = httpclient.execute(httpPost)) { + resultContent = new BasicResponseHandler().handleResponse(response); + } finally { + httpPost.releaseConnection(); + } + WxError error = WxError.fromJson(resultContent, WxType.CP); + if (error.getErrorCode() != 0) { + throw new WxErrorException(error); + } + jsonObject = new JsonParser().parse(resultContent).getAsJsonObject(); + String suiteAccussToken = jsonObject.get("suite_access_token").getAsString(); + Integer expiresIn = jsonObject.get("expires_in").getAsInt(); + this.configStorage.updateSuiteAccessToken(suiteAccussToken, expiresIn); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return this.configStorage.getSuiteAccessToken(); + } + + @Override + public void initHttp() { + ApacheHttpClientBuilder apacheHttpClientBuilder = this.configStorage + .getApacheHttpClientBuilder(); + if (null == apacheHttpClientBuilder) { + apacheHttpClientBuilder = DefaultApacheHttpClientBuilder.get(); + } + + apacheHttpClientBuilder.httpProxyHost(this.configStorage.getHttpProxyHost()) + .httpProxyPort(this.configStorage.getHttpProxyPort()) + .httpProxyUsername(this.configStorage.getHttpProxyUsername()) + .httpProxyPassword(this.configStorage.getHttpProxyPassword()); + + if (this.configStorage.getHttpProxyHost() != null && this.configStorage.getHttpProxyPort() > 0) { + this.httpProxy = new HttpHost(this.configStorage.getHttpProxyHost(), this.configStorage.getHttpProxyPort()); + } + + this.httpClient = apacheHttpClientBuilder.build(); + } + + @Override + public WxCpTpConfigStorage getWxCpTpConfigStorage() { + return this.configStorage; + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTpServiceImpl.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTpServiceImpl.java new file mode 100644 index 00000000..538209b7 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/api/impl/WxCpTpServiceImpl.java @@ -0,0 +1,12 @@ +package me.chanjar.weixin.cp.api.impl; + +/** + *
+ *  默认接口实现类,使用apache httpclient实现
+ * Created by zhenjun cai.
+ * 
+ * + * @author Binary Wang + */ +public class WxCpTpServiceImpl extends WxCpTpServiceApacheHttpClientImpl { +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java new file mode 100644 index 00000000..9ca59b84 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpCorp.java @@ -0,0 +1,42 @@ +package me.chanjar.weixin.cp.bean; + +import java.io.Serializable; + +import com.google.gson.annotations.SerializedName; + +import lombok.Data; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +/** + * 微信部门. + * + * @author Daniel Qian + */ +@Data +public class WxCpTpCorp implements Serializable { + + private static final long serialVersionUID = -5028321625140879571L; + @SerializedName("corpid") + private String corpId; + @SerializedName("corp_name") + private String corpName; + @SerializedName("corp_full_name") + private String corpFullName; + @SerializedName("corp_type") + private String corpType; + @SerializedName("corp_square_logo_url") + private String corpSquareLogoUrl; + @SerializedName("corp_user_max") + private String corpUserMax; + @SerializedName("permanent_code") + private String permanentCode; + + public static WxCpTpCorp fromJson(String json) { + return WxCpGsonBuilder.create().fromJson(json, WxCpTpCorp.class); + } + + public String toJson() { + return WxCpGsonBuilder.create().toJson(this); + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlMessage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlMessage.java new file mode 100644 index 00000000..53e068db --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlMessage.java @@ -0,0 +1,68 @@ +package me.chanjar.weixin.cp.bean; + +import java.io.Serializable; +import java.util.Map; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamConverter; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import me.chanjar.weixin.common.util.XmlUtils; +import me.chanjar.weixin.common.util.crypto.WxCryptUtil; +import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; +import me.chanjar.weixin.cp.bean.outxmlbuilder.ImageBuilder; +import me.chanjar.weixin.cp.bean.outxmlbuilder.NewsBuilder; +import me.chanjar.weixin.cp.bean.outxmlbuilder.TextBuilder; +import me.chanjar.weixin.cp.bean.outxmlbuilder.VideoBuilder; +import me.chanjar.weixin.cp.bean.outxmlbuilder.VoiceBuilder; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; +import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil; +import me.chanjar.weixin.cp.util.xml.XStreamTransformer; + +/** + * 回调推送的message + * https://work.weixin.qq.com/api/doc#90001/90143/90612 + * + * @author zhenjun cai + */ +@XStreamAlias("xml") +@Slf4j +@Data +public class WxCpTpXmlMessage implements Serializable { + + private static final long serialVersionUID = 6031833682211475786L; + /** + * 使用dom4j解析的存放所有xml属性和值的map. + */ + private Map allFieldsMap; + + @XStreamAlias("SuiteId") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String suiteId; + + @XStreamAlias("InfoType") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String infoType; + + @XStreamAlias("TimeStamp") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String timeStamp; + + @XStreamAlias("SuiteTicket") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String suiteTicket; + + @XStreamAlias("AuthCode") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String authCode; + + public static WxCpTpXmlMessage fromXml(String xml) { + //修改微信变态的消息内容格式,方便解析 + //xml = xml.replace("", ""); + final WxCpTpXmlMessage xmlPackage = XStreamTransformer.fromXml(WxCpTpXmlMessage.class, xml); + xmlPackage.setAllFieldsMap(XmlUtils.xml2Map(xml)); + return xmlPackage; + } + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlPackage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlPackage.java new file mode 100644 index 00000000..82c33f52 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/bean/WxCpTpXmlPackage.java @@ -0,0 +1,55 @@ +package me.chanjar.weixin.cp.bean; + +import java.io.Serializable; +import java.util.Map; + +import com.thoughtworks.xstream.annotations.XStreamAlias; +import com.thoughtworks.xstream.annotations.XStreamConverter; +import lombok.Data; +import me.chanjar.weixin.common.util.XmlUtils; +import me.chanjar.weixin.common.util.xml.XStreamCDataConverter; +import me.chanjar.weixin.cp.bean.outxmlbuilder.ImageBuilder; +import me.chanjar.weixin.cp.bean.outxmlbuilder.NewsBuilder; +import me.chanjar.weixin.cp.bean.outxmlbuilder.TextBuilder; +import me.chanjar.weixin.cp.bean.outxmlbuilder.VideoBuilder; +import me.chanjar.weixin.cp.bean.outxmlbuilder.VoiceBuilder; +import me.chanjar.weixin.cp.config.WxCpConfigStorage; +import me.chanjar.weixin.cp.util.crypto.WxCpCryptUtil; +import me.chanjar.weixin.cp.util.xml.XStreamTransformer; + +/** + * 回调消息包. + * https://work.weixin.qq.com/api/doc#90001/90143/91116 + * + * @author zhenjun cai + */ +@XStreamAlias("xml") +@Data +public class WxCpTpXmlPackage implements Serializable { + + private static final long serialVersionUID = 6031833682211475786L; + /** + * 使用dom4j解析的存放所有xml属性和值的map. + */ + private Map allFieldsMap; + + @XStreamAlias("ToUserName") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String toUserName; + + @XStreamAlias("AgentID") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String agentId; + + @XStreamAlias("Encrypt") + @XStreamConverter(value = XStreamCDataConverter.class) + protected String msgEncrypt; + + public static WxCpTpXmlPackage fromXml(String xml) { + //修改微信变态的消息内容格式,方便解析 + //xml = xml.replace("", ""); + final WxCpTpXmlPackage xmlPackage = XStreamTransformer.fromXml(WxCpTpXmlPackage.class, xml); + xmlPackage.setAllFieldsMap(XmlUtils.xml2Map(xml)); + return xmlPackage; + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java new file mode 100644 index 00000000..a132dd10 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpConfigStorage.java @@ -0,0 +1,72 @@ +package me.chanjar.weixin.cp.config; + +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; + +import java.io.File; + +/** + * 微信客户端(第三方应用)配置存储 + * + * @author zhenjun cai + */ +public interface WxCpTpConfigStorage { + + String getSuiteAccessToken(); + + boolean isSuiteAccessTokenExpired(); + + /** + * 强制将suite access token过期掉 + */ + void expireSuiteAccessToken(); + + void updateSuiteAccessToken(WxAccessToken suiteAccessToken); + + void updateSuiteAccessToken(String suiteAccessToken, int expiresIn); + + String getSuiteTicket(); + + boolean isSuiteTicketExpired(); + + /** + * 强制将suite ticket过期掉 + */ + void expireSuiteTicket(); + + /** + * 应该是线程安全的 + */ + void updateSuiteTicket(String suiteTicket, int expiresInSeconds); + + String getCorpId(); + + String getCorpSecret(); + + String getSuiteId(); + + String getSuiteSecret(); + + String getToken(); + + String getAesKey(); + + long getExpiresTime(); + + String getHttpProxyHost(); + + int getHttpProxyPort(); + + String getHttpProxyUsername(); + + String getHttpProxyPassword(); + + File getTmpDirFile(); + + /** + * http client builder + * + * @return ApacheHttpClientBuilder + */ + ApacheHttpClientBuilder getApacheHttpClientBuilder(); +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpInMemoryConfigStorage.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpInMemoryConfigStorage.java new file mode 100644 index 00000000..c135c95b --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/config/WxCpTpInMemoryConfigStorage.java @@ -0,0 +1,230 @@ +package me.chanjar.weixin.cp.config; + +import me.chanjar.weixin.common.bean.WxAccessToken; +import me.chanjar.weixin.common.util.http.apache.ApacheHttpClientBuilder; +import me.chanjar.weixin.cp.util.json.WxCpGsonBuilder; + +import java.io.File; + +/** + * 基于内存的微信配置provider,在实际生产环境中应该将这些配置持久化 + * + * @author Daniel Qian + */ +public class WxCpTpInMemoryConfigStorage implements WxCpTpConfigStorage { + protected volatile String corpId; + protected volatile String corpSecret; + + protected volatile String suiteId; + protected volatile String suiteSecret; + + protected volatile String token; + protected volatile String suiteAccessToken; + protected volatile String aesKey; + protected volatile long expiresTime; + + protected volatile String oauth2redirectUri; + + protected volatile String httpProxyHost; + protected volatile int httpProxyPort; + protected volatile String httpProxyUsername; + protected volatile String httpProxyPassword; + + protected volatile String suiteTicket; + protected volatile long suiteTicketExpiresTime; + + + protected volatile File tmpDirFile; + + private volatile ApacheHttpClientBuilder apacheHttpClientBuilder; + + @Override + public String getSuiteAccessToken() { + return this.suiteAccessToken; + } + + public void setSuiteAccessToken(String suiteAccessToken) { + this.suiteAccessToken = suiteAccessToken; + } + + @Override + public boolean isSuiteAccessTokenExpired() { + return System.currentTimeMillis() > this.expiresTime; + } + + @Override + public void expireSuiteAccessToken() { + this.expiresTime = 0; + } + + @Override + public synchronized void updateSuiteAccessToken(WxAccessToken suiteAccessToken) { + updateSuiteAccessToken(suiteAccessToken.getAccessToken(), suiteAccessToken.getExpiresIn()); + } + + @Override + public synchronized void updateSuiteAccessToken(String suiteAccessToken, int expiresInSeconds) { + this.suiteAccessToken = suiteAccessToken; + this.expiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; + } + + @Override + public String getCorpId() { + return this.corpId; + } + + public void setCorpId(String corpId) { + this.corpId = corpId; + } + + @Override + public String getCorpSecret() { + return this.corpSecret; + } + + public void setCorpSecret(String corpSecret) { + this.corpSecret = corpSecret; + } + + @Override + public String getSuiteTicket() { + return this.suiteTicket; + } + + public void setSuiteTicket(String suiteTicket) { + this.suiteTicket = suiteTicket; + } + + public long getSuiteTicketExpiresTime() { + return this.suiteTicketExpiresTime; + } + + public void setSuiteTicketExpiresTime(long suiteTicketExpiresTime) { + this.suiteTicketExpiresTime = suiteTicketExpiresTime; + } + + @Override + public boolean isSuiteTicketExpired() { + return System.currentTimeMillis() > this.suiteTicketExpiresTime; + } + + @Override + public synchronized void updateSuiteTicket(String suiteTicket, int expiresInSeconds) { + this.suiteTicket = suiteTicket; + // 预留200秒的时间 + this.suiteTicketExpiresTime = System.currentTimeMillis() + (expiresInSeconds - 200) * 1000L; + } + + @Override + public void expireSuiteTicket() { + this.suiteTicketExpiresTime = 0; + } + + @Override + public String getSuiteId() { + return this.suiteId; + } + + public void setSuiteId(String corpId) { + this.suiteId = corpId; + } + + @Override + public String getSuiteSecret() { + return this.suiteSecret; + } + + public void setSuiteSecret(String corpSecret) { + this.suiteSecret = corpSecret; + } + + @Override + public String getToken() { + return this.token; + } + + public void setToken(String token) { + this.token = token; + } + + @Override + public long getExpiresTime() { + return this.expiresTime; + } + + public void setExpiresTime(long expiresTime) { + this.expiresTime = expiresTime; + } + + @Override + public String getAesKey() { + return this.aesKey; + } + + public void setAesKey(String aesKey) { + this.aesKey = aesKey; + } + + public void setOauth2redirectUri(String oauth2redirectUri) { + this.oauth2redirectUri = oauth2redirectUri; + } + + @Override + public String getHttpProxyHost() { + return this.httpProxyHost; + } + + public void setHttpProxyHost(String httpProxyHost) { + this.httpProxyHost = httpProxyHost; + } + + @Override + public int getHttpProxyPort() { + return this.httpProxyPort; + } + + public void setHttpProxyPort(int httpProxyPort) { + this.httpProxyPort = httpProxyPort; + } + + @Override + public String getHttpProxyUsername() { + return this.httpProxyUsername; + } + + public void setHttpProxyUsername(String httpProxyUsername) { + this.httpProxyUsername = httpProxyUsername; + } + + @Override + public String getHttpProxyPassword() { + return this.httpProxyPassword; + } + + public void setHttpProxyPassword(String httpProxyPassword) { + this.httpProxyPassword = httpProxyPassword; + } + + @Override + public String toString() { + return WxCpGsonBuilder.create().toJson(this); + } + + @Override + public File getTmpDirFile() { + return this.tmpDirFile; + } + + public void setTmpDirFile(File tmpDirFile) { + this.tmpDirFile = tmpDirFile; + } + + @Override + public ApacheHttpClientBuilder getApacheHttpClientBuilder() { + return this.apacheHttpClientBuilder; + } + + public void setApacheHttpClientBuilder(ApacheHttpClientBuilder apacheHttpClientBuilder) { + this.apacheHttpClientBuilder = apacheHttpClientBuilder; + } +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpTpCryptUtil.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpTpCryptUtil.java new file mode 100644 index 00000000..453d1700 --- /dev/null +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/crypto/WxCpTpCryptUtil.java @@ -0,0 +1,52 @@ +/** + * 对公众平台发送给公众账号的消息加解密示例代码. + * + * @copyright Copyright (c) 1998-2014 Tencent Inc. + *

+ * 针对org.apache.commons.codec.binary.Base64, + * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本) + * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi + *

+ * 针对org.apache.commons.codec.binary.Base64, + * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本) + * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi + */ + +// ------------------------------------------------------------------------ + +/** + * 针对org.apache.commons.codec.binary.Base64, + * 需要导入架包commons-codec-1.9(或commons-codec-1.8等其他版本) + * 官方下载地址:http://commons.apache.org/proper/commons-codec/download_codec.cgi + */ +package me.chanjar.weixin.cp.util.crypto; + +import org.apache.commons.codec.binary.Base64; + +import me.chanjar.weixin.common.util.crypto.WxCryptUtil; +import me.chanjar.weixin.cp.config.WxCpTpConfigStorage; + +public class WxCpTpCryptUtil extends WxCryptUtil { + + /** + * 构造函数 + * + * @param wxCpConfigStorage + */ + public WxCpTpCryptUtil(WxCpTpConfigStorage wxCpTpConfigStorage) { + /* + * @param token 公众平台上,开发者设置的token + * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey + * @param appidOrCorpid 公众平台corpId + */ + String encodingAesKey = wxCpTpConfigStorage.getAesKey(); + String token = wxCpTpConfigStorage.getToken(); + String corpId = wxCpTpConfigStorage.getCorpId(); + + this.token = token; + this.appidOrCorpid = corpId; + this.aesKey = Base64.decodeBase64(encodingAesKey + "="); + } + + +} diff --git a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/xml/XStreamTransformer.java b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/xml/XStreamTransformer.java index 5ef1503d..3c6174c9 100644 --- a/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/xml/XStreamTransformer.java +++ b/weixin-java-cp/src/main/java/me/chanjar/weixin/cp/util/xml/XStreamTransformer.java @@ -1,117 +1,135 @@ -package me.chanjar.weixin.cp.util.xml; - -import java.io.InputStream; -import java.util.HashMap; -import java.util.Map; - -import com.thoughtworks.xstream.XStream; -import me.chanjar.weixin.common.util.xml.XStreamInitializer; -import me.chanjar.weixin.cp.bean.WxCpXmlMessage; -import me.chanjar.weixin.cp.bean.WxCpXmlOutImageMessage; -import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; -import me.chanjar.weixin.cp.bean.WxCpXmlOutNewsMessage; -import me.chanjar.weixin.cp.bean.WxCpXmlOutTextMessage; -import me.chanjar.weixin.cp.bean.WxCpXmlOutVideoMessage; -import me.chanjar.weixin.cp.bean.WxCpXmlOutVoiceMessage; - -public class XStreamTransformer { - - protected static final Map CLASS_2_XSTREAM_INSTANCE = configXStreamInstance(); - - /** - * xml -> pojo - */ - @SuppressWarnings("unchecked") - public static T fromXml(Class clazz, String xml) { - T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(xml); - return object; - } - - @SuppressWarnings("unchecked") - public static T fromXml(Class clazz, InputStream is) { - T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(is); - return object; - } - - /** - * 注册扩展消息的解析器. - * - * @param clz 类型 - * @param xStream xml解析器 - */ - public static void register(Class clz, XStream xStream) { - CLASS_2_XSTREAM_INSTANCE.put(clz, xStream); - } - - /** - * pojo -> xml. - */ - public static String toXml(Class clazz, T object) { - return CLASS_2_XSTREAM_INSTANCE.get(clazz).toXML(object); - } - - private static Map configXStreamInstance() { - Map map = new HashMap<>(); - map.put(WxCpXmlMessage.class, configWxCpXmlMessage()); - map.put(WxCpXmlOutNewsMessage.class, configWxCpXmlOutNewsMessage()); - map.put(WxCpXmlOutTextMessage.class, configWxCpXmlOutTextMessage()); - map.put(WxCpXmlOutImageMessage.class, configWxCpXmlOutImageMessage()); - map.put(WxCpXmlOutVideoMessage.class, configWxCpXmlOutVideoMessage()); - map.put(WxCpXmlOutVoiceMessage.class, configWxCpXmlOutVoiceMessage()); - return map; - } - - private static XStream configWxCpXmlMessage() { - XStream xstream = XStreamInitializer.getInstance(); - - xstream.processAnnotations(WxCpXmlMessage.class); - xstream.processAnnotations(WxCpXmlMessage.ScanCodeInfo.class); - xstream.processAnnotations(WxCpXmlMessage.SendPicsInfo.class); - xstream.processAnnotations(WxCpXmlMessage.SendPicsInfo.Item.class); - xstream.processAnnotations(WxCpXmlMessage.SendLocationInfo.class); - return xstream; - } - - private static XStream configWxCpXmlOutImageMessage() { - XStream xstream = XStreamInitializer.getInstance(); - - xstream.processAnnotations(WxCpXmlOutMessage.class); - xstream.processAnnotations(WxCpXmlOutImageMessage.class); - return xstream; - } - - private static XStream configWxCpXmlOutNewsMessage() { - XStream xstream = XStreamInitializer.getInstance(); - - xstream.processAnnotations(WxCpXmlOutMessage.class); - xstream.processAnnotations(WxCpXmlOutNewsMessage.class); - xstream.processAnnotations(WxCpXmlOutNewsMessage.Item.class); - return xstream; - } - - private static XStream configWxCpXmlOutTextMessage() { - XStream xstream = XStreamInitializer.getInstance(); - - xstream.processAnnotations(WxCpXmlOutMessage.class); - xstream.processAnnotations(WxCpXmlOutTextMessage.class); - return xstream; - } - - private static XStream configWxCpXmlOutVideoMessage() { - XStream xstream = XStreamInitializer.getInstance(); - - xstream.processAnnotations(WxCpXmlOutMessage.class); - xstream.processAnnotations(WxCpXmlOutVideoMessage.class); - xstream.processAnnotations(WxCpXmlOutVideoMessage.Video.class); - return xstream; - } - - private static XStream configWxCpXmlOutVoiceMessage() { - XStream xstream = XStreamInitializer.getInstance(); - - xstream.processAnnotations(WxCpXmlOutMessage.class); - xstream.processAnnotations(WxCpXmlOutVoiceMessage.class); - return xstream; - } - -} +package me.chanjar.weixin.cp.util.xml; + +import java.io.InputStream; +import java.util.HashMap; +import java.util.Map; + +import com.thoughtworks.xstream.XStream; +import me.chanjar.weixin.common.util.xml.XStreamInitializer; +import me.chanjar.weixin.cp.bean.WxCpTpXmlMessage; +import me.chanjar.weixin.cp.bean.WxCpTpXmlPackage; +import me.chanjar.weixin.cp.bean.WxCpXmlMessage; +import me.chanjar.weixin.cp.bean.WxCpXmlOutImageMessage; +import me.chanjar.weixin.cp.bean.WxCpXmlOutMessage; +import me.chanjar.weixin.cp.bean.WxCpXmlOutNewsMessage; +import me.chanjar.weixin.cp.bean.WxCpXmlOutTextMessage; +import me.chanjar.weixin.cp.bean.WxCpXmlOutVideoMessage; +import me.chanjar.weixin.cp.bean.WxCpXmlOutVoiceMessage; + +public class XStreamTransformer { + + protected static final Map CLASS_2_XSTREAM_INSTANCE = configXStreamInstance(); + + /** + * xml -> pojo + */ + @SuppressWarnings("unchecked") + public static T fromXml(Class clazz, String xml) { + T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(xml); + return object; + } + + @SuppressWarnings("unchecked") + public static T fromXml(Class clazz, InputStream is) { + T object = (T) CLASS_2_XSTREAM_INSTANCE.get(clazz).fromXML(is); + return object; + } + + /** + * 注册扩展消息的解析器. + * + * @param clz 类型 + * @param xStream xml解析器 + */ + public static void register(Class clz, XStream xStream) { + CLASS_2_XSTREAM_INSTANCE.put(clz, xStream); + } + + /** + * pojo -> xml. + */ + public static String toXml(Class clazz, T object) { + return CLASS_2_XSTREAM_INSTANCE.get(clazz).toXML(object); + } + + private static Map configXStreamInstance() { + Map map = new HashMap<>(); + map.put(WxCpXmlMessage.class, configWxCpXmlMessage()); + map.put(WxCpXmlOutNewsMessage.class, configWxCpXmlOutNewsMessage()); + map.put(WxCpXmlOutTextMessage.class, configWxCpXmlOutTextMessage()); + map.put(WxCpXmlOutImageMessage.class, configWxCpXmlOutImageMessage()); + map.put(WxCpXmlOutVideoMessage.class, configWxCpXmlOutVideoMessage()); + map.put(WxCpXmlOutVoiceMessage.class, configWxCpXmlOutVoiceMessage()); + map.put(WxCpTpXmlPackage.class, configWxCpTpXmlPackage()); + map.put(WxCpTpXmlMessage.class, configWxCpTpXmlMessage()); + return map; + } + + private static XStream configWxCpXmlMessage() { + XStream xstream = XStreamInitializer.getInstance(); + + xstream.processAnnotations(WxCpXmlMessage.class); + xstream.processAnnotations(WxCpXmlMessage.ScanCodeInfo.class); + xstream.processAnnotations(WxCpXmlMessage.SendPicsInfo.class); + xstream.processAnnotations(WxCpXmlMessage.SendPicsInfo.Item.class); + xstream.processAnnotations(WxCpXmlMessage.SendLocationInfo.class); + return xstream; + } + + private static XStream configWxCpXmlOutImageMessage() { + XStream xstream = XStreamInitializer.getInstance(); + + xstream.processAnnotations(WxCpXmlOutMessage.class); + xstream.processAnnotations(WxCpXmlOutImageMessage.class); + return xstream; + } + + private static XStream configWxCpXmlOutNewsMessage() { + XStream xstream = XStreamInitializer.getInstance(); + + xstream.processAnnotations(WxCpXmlOutMessage.class); + xstream.processAnnotations(WxCpXmlOutNewsMessage.class); + xstream.processAnnotations(WxCpXmlOutNewsMessage.Item.class); + return xstream; + } + + private static XStream configWxCpXmlOutTextMessage() { + XStream xstream = XStreamInitializer.getInstance(); + + xstream.processAnnotations(WxCpXmlOutMessage.class); + xstream.processAnnotations(WxCpXmlOutTextMessage.class); + return xstream; + } + + private static XStream configWxCpXmlOutVideoMessage() { + XStream xstream = XStreamInitializer.getInstance(); + + xstream.processAnnotations(WxCpXmlOutMessage.class); + xstream.processAnnotations(WxCpXmlOutVideoMessage.class); + xstream.processAnnotations(WxCpXmlOutVideoMessage.Video.class); + return xstream; + } + + private static XStream configWxCpXmlOutVoiceMessage() { + XStream xstream = XStreamInitializer.getInstance(); + + xstream.processAnnotations(WxCpXmlOutMessage.class); + xstream.processAnnotations(WxCpXmlOutVoiceMessage.class); + return xstream; + } + + private static XStream configWxCpTpXmlPackage() { + XStream xstream = XStreamInitializer.getInstance(); + xstream.processAnnotations(WxCpTpXmlPackage.class); + + return xstream; + } + + private static XStream configWxCpTpXmlMessage() { + XStream xstream = XStreamInitializer.getInstance(); + xstream.processAnnotations(WxCpTpXmlMessage.class); + + return xstream; + } + +}