| @@ -12,6 +12,7 @@ | |||
| <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | |||
| <downloadJavadocs>true</downloadJavadocs> | |||
| <downloadSources>true</downloadSources> | |||
| <httpclient.version>4.3.5</httpclient.version> | |||
| </properties> | |||
| <dependencies> | |||
| <dependency> | |||
| @@ -23,8 +24,13 @@ | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>fluent-hc</artifactId> | |||
| <version>4.3.5</version> | |||
| <version>${httpclient.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>org.apache.httpcomponents</groupId> | |||
| <artifactId>httpmime</artifactId> | |||
| <version>${httpclient.version}</version> | |||
| </dependency> | |||
| <dependency> | |||
| <groupId>javax.xml.bind</groupId> | |||
| <artifactId>jaxb-api</artifactId> | |||
| @@ -20,6 +20,17 @@ public class WxConsts { | |||
| public static final String EVT_VIEW = "VIEW"; | |||
| public static final String EVT_MASS_SEND_JOB_FINISH = "MASSSENDJOBFINISH"; | |||
| public static final String MEDIA_IMAGE = "image"; | |||
| public static final String MEDIA_VOICE = "voice"; | |||
| public static final String MEDIA_VIDEO = "video"; | |||
| public static final String MEDIA_THUMB = "thumb"; | |||
| public static final String FILE_JPG = "jpeg"; | |||
| public static final String FILE_PNG = "png"; | |||
| public static final String FILE_MP3 = "mp3"; | |||
| public static final String FILE_ARM = "arm"; | |||
| public static final String FILE_MP4 = "mp4"; | |||
| public static final String ST_SEND_SUCCESS = "send success"; | |||
| public static final String ST_SEND_FAIL = "send fail"; | |||
| public static final String ST_涉嫌广告 = "err(10001)"; | |||
| @@ -1,7 +1,12 @@ | |||
| package chanjarster.weixin.api; | |||
| import java.io.File; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import chanjarster.weixin.bean.WxCustomMessage; | |||
| import chanjarster.weixin.bean.WxMenu; | |||
| import chanjarster.weixin.bean.result.WxUploadResult; | |||
| import chanjarster.weixin.exception.WxErrorException; | |||
| /** | |||
| @@ -24,7 +29,7 @@ public interface WxService { | |||
| /** | |||
| * <pre> | |||
| * 获取access_token,本方法线程安全 | |||
| * 且在多线程同时刷新时只刷新一次,避免超出200次/日的调用次数上限 | |||
| * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限 | |||
| * | |||
| * 另:本service的所有方法都会在access_token过期是调用此方法 | |||
| * | |||
| @@ -36,6 +41,32 @@ public interface WxService { | |||
| */ | |||
| public void refreshAccessToken() throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 上传多媒体文件 | |||
| * 上传的多媒体文件有格式和大小限制,如下: | |||
| * 图片(image): 1M,支持JPG格式 | |||
| * 语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式 | |||
| * 视频(video):10MB,支持MP4格式 | |||
| * 缩略图(thumb):64KB,支持JPG格式 | |||
| * | |||
| * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件 | |||
| * </pre> | |||
| * @param mediaType 媒体类型, 请看{@link WxConsts} | |||
| * @param fileType 文件类型,请看{@link WxConsts} | |||
| * @param inputStream 输入流 | |||
| * @throws WxErrorException | |||
| */ | |||
| public WxUploadResult uploadMedia(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException; | |||
| /** | |||
| * @see #uploadMedia(String, String, InputStream) | |||
| * @param mediaType | |||
| * @param file | |||
| * @throws WxErrorException | |||
| */ | |||
| public WxUploadResult uploadMedia(String mediaType, File file) throws WxErrorException; | |||
| /** | |||
| * <pre> | |||
| * 发送客服消息 | |||
| @@ -1,26 +1,33 @@ | |||
| package chanjarster.weixin.api; | |||
| import java.io.File; | |||
| import java.io.FileOutputStream; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import java.security.MessageDigest; | |||
| import java.util.Arrays; | |||
| import java.util.UUID; | |||
| import java.util.concurrent.atomic.AtomicBoolean; | |||
| import org.apache.commons.lang3.StringUtils; | |||
| import org.apache.http.Consts; | |||
| import org.apache.http.HttpEntity; | |||
| 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.ContentType; | |||
| import org.apache.http.entity.StringEntity; | |||
| import org.apache.http.entity.mime.MultipartEntityBuilder; | |||
| import org.apache.http.impl.client.BasicResponseHandler; | |||
| import org.apache.http.impl.client.CloseableHttpClient; | |||
| import org.apache.http.impl.client.HttpClients; | |||
| import chanjarster.weixin.bean.WxAccessToken; | |||
| import chanjarster.weixin.bean.WxCustomMessage; | |||
| import chanjarster.weixin.bean.WxError; | |||
| import chanjarster.weixin.bean.WxMenu; | |||
| import chanjarster.weixin.bean.result.WxError; | |||
| import chanjarster.weixin.bean.result.WxUploadResult; | |||
| import chanjarster.weixin.exception.WxErrorException; | |||
| import chanjarster.weixin.util.Utf8ResponseHandler; | |||
| @@ -132,11 +139,21 @@ public class WxServiceImpl implements WxService { | |||
| } | |||
| } | |||
| protected String post(String uri, String data) throws WxErrorException { | |||
| public WxUploadResult uploadMedia(String mediaType, String fileType, InputStream inputStream) throws WxErrorException, IOException { | |||
| return uploadMedia(mediaType, createTmpFile(inputStream, fileType)); | |||
| } | |||
| public WxUploadResult uploadMedia(String mediaType, File file) throws WxErrorException { | |||
| String url = "http://file.api.weixin.qq.com/cgi-bin/media/upload?type=" + mediaType; | |||
| String json = post(url, file); | |||
| return WxUploadResult.fromJson(json); | |||
| } | |||
| protected String post(String uri, Object data) throws WxErrorException { | |||
| return execute("POST", uri, data); | |||
| } | |||
| protected String get(String uri, String data) throws WxErrorException { | |||
| protected String get(String uri, Object data) throws WxErrorException { | |||
| return execute("GET", uri, data); | |||
| } | |||
| @@ -146,7 +163,7 @@ public class WxServiceImpl implements WxService { | |||
| * @return 微信服务端返回的结果 | |||
| * @throws WxErrorException | |||
| */ | |||
| protected String execute(String method, String uri, String data) throws WxErrorException { | |||
| protected String execute(String method, String uri, Object data) throws WxErrorException { | |||
| if (StringUtils.isBlank(wxConfigStorage.getAccessToken())) { | |||
| refreshAccessToken(); | |||
| } | |||
| @@ -160,18 +177,30 @@ public class WxServiceImpl implements WxService { | |||
| if ("POST".equals(method)) { | |||
| HttpPost httpPost = new HttpPost(uriWithAccessToken); | |||
| if (data != null) { | |||
| StringEntity entity = new StringEntity(data, Consts.UTF_8); | |||
| httpPost.setEntity(entity); | |||
| if (data instanceof String) { | |||
| StringEntity entity = new StringEntity((String)data, Consts.UTF_8); | |||
| httpPost.setEntity(entity); | |||
| } | |||
| if (data instanceof File) { | |||
| File file = (File) data; | |||
| HttpEntity entity = MultipartEntityBuilder | |||
| .create() | |||
| .addBinaryBody("media", file) | |||
| .build(); | |||
| httpPost.setEntity(entity); | |||
| httpPost.setHeader("Content-Type", ContentType.MULTIPART_FORM_DATA.toString()); | |||
| } | |||
| } | |||
| CloseableHttpResponse response = httpclient.execute(httpPost); | |||
| resultContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); | |||
| } else if ("GET".equals(method)) { | |||
| if (data != null) { | |||
| uriWithAccessToken += uriWithAccessToken.endsWith("&") ? data : '&' + data; | |||
| if (data instanceof String) { | |||
| uriWithAccessToken += uriWithAccessToken.endsWith("&") ? data : '&' + (String)data; | |||
| } | |||
| } | |||
| HttpGet httpGet = new HttpGet(uriWithAccessToken); | |||
| CloseableHttpResponse response = httpclient.execute(httpGet); | |||
| response.setHeader("Content-Type", ContentType.APPLICATION_JSON.toString()); | |||
| resultContent = Utf8ResponseHandler.INSTANCE.handleResponse(response); | |||
| } | |||
| @@ -196,8 +225,38 @@ public class WxServiceImpl implements WxService { | |||
| } | |||
| } | |||
| protected File createTmpFile(InputStream inputStream, String fileType) throws IOException { | |||
| FileOutputStream fos = null; | |||
| try { | |||
| File tmpFile = File.createTempFile(UUID.randomUUID().toString(), '.' + fileType); | |||
| tmpFile.deleteOnExit(); | |||
| fos = new FileOutputStream(tmpFile); | |||
| int read = 0; | |||
| byte[] bytes = new byte[1024 * 100]; | |||
| while ((read = inputStream.read(bytes)) != -1) { | |||
| fos.write(bytes, 0, read); | |||
| } | |||
| fos.flush(); | |||
| return tmpFile; | |||
| } finally { | |||
| if (inputStream != null) { | |||
| try { | |||
| inputStream.close(); | |||
| } catch (IOException e) { | |||
| } | |||
| } | |||
| if (fos != null) { | |||
| try { | |||
| fos.close(); | |||
| } catch (IOException e) { | |||
| } | |||
| } | |||
| } | |||
| } | |||
| public void setWxConfigStorage(WxConfigStorage wxConfigProvider) { | |||
| this.wxConfigStorage = wxConfigProvider; | |||
| } | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| package chanjarster.weixin.bean; | |||
| package chanjarster.weixin.bean.result; | |||
| import java.util.HashMap; | |||
| import java.util.Map; | |||
| @@ -0,0 +1,54 @@ | |||
| package chanjarster.weixin.bean.result; | |||
| import chanjarster.weixin.util.WxGsonBuilder; | |||
| public class WxUploadResult { | |||
| protected String type; | |||
| protected String media_id; | |||
| protected String thumb_media_id; | |||
| protected long created_at; | |||
| public String getType() { | |||
| return type; | |||
| } | |||
| public void setType(String type) { | |||
| this.type = type; | |||
| } | |||
| public String getMedia_id() { | |||
| return media_id; | |||
| } | |||
| public void setMedia_id(String media_id) { | |||
| this.media_id = media_id; | |||
| } | |||
| public long getCreated_at() { | |||
| return created_at; | |||
| } | |||
| public void setCreated_at(long created_at) { | |||
| this.created_at = created_at; | |||
| } | |||
| public static WxUploadResult fromJson(String json) { | |||
| return WxGsonBuilder.create().fromJson(json, WxUploadResult.class); | |||
| } | |||
| 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; | |||
| } | |||
| @Override | |||
| public String toString() { | |||
| return "WxUploadResult [type=" + type + ", media_id=" + media_id + ", thumb_media_id=" + thumb_media_id | |||
| + ", created_at=" + created_at + "]"; | |||
| } | |||
| } | |||
| @@ -1,6 +1,6 @@ | |||
| package chanjarster.weixin.exception; | |||
| import chanjarster.weixin.bean.WxError; | |||
| import chanjarster.weixin.bean.result.WxError; | |||
| public class WxErrorException extends Exception { | |||
| @@ -1,5 +1,6 @@ | |||
| package chanjarster.weixin.api; | |||
| import java.io.IOException; | |||
| import java.io.InputStream; | |||
| import javax.xml.bind.JAXBException; | |||
| @@ -16,6 +17,7 @@ import org.testng.annotations.Test; | |||
| import chanjarster.weixin.bean.WxCustomMessage; | |||
| import chanjarster.weixin.bean.WxMenu; | |||
| import chanjarster.weixin.bean.WxMenu.WxMenuButton; | |||
| import chanjarster.weixin.bean.result.WxUploadResult; | |||
| import chanjarster.weixin.exception.WxErrorException; | |||
| import chanjarster.weixin.util.XmlTransformer; | |||
| @@ -26,12 +28,12 @@ public class WxServiceTest { | |||
| @BeforeTest | |||
| public void prepare() throws JAXBException { | |||
| InputStream is1 = ClassLoader.getSystemResourceAsStream("test-config.xml"); | |||
| WxXmlConfigStorage config1 = XmlTransformer.fromXml(WxXmlConfigStorage.class, is1); | |||
| WxXmlConfigStorage config = XmlTransformer.fromXml(WxXmlConfigStorage.class, is1); | |||
| this.wxService = new WxServiceImpl(); | |||
| this.wxService.setWxConfigStorage(config1); | |||
| this.wxService.setWxConfigStorage(config); | |||
| } | |||
| @Test | |||
| @Test() | |||
| public void testRefreshAccessToken() throws WxErrorException { | |||
| WxConfigStorage configStorage = wxService.wxConfigStorage; | |||
| String before = configStorage.getAccessToken(); | |||
| @@ -43,7 +45,7 @@ public class WxServiceTest { | |||
| Assert.assertTrue(StringUtils.isNotBlank(after)); | |||
| } | |||
| @Test(dependsOnMethods = "testRefreshAccessToken") | |||
| @Test(dependsOnMethods = "testRefreshAccessToken", enabled = false) | |||
| public void sendCustomMessage() throws WxErrorException { | |||
| WxXmlConfigStorage configProvider = (WxXmlConfigStorage) wxService.wxConfigStorage; | |||
| WxCustomMessage message = new WxCustomMessage(); | |||
| @@ -54,22 +56,41 @@ public class WxServiceTest { | |||
| wxService.sendCustomMessage(message); | |||
| } | |||
| @Test(dataProvider = "menu", dependsOnMethods = "testRefreshAccessToken") | |||
| @Test(dataProvider = "menu", dependsOnMethods = "testRefreshAccessToken", enabled = false) | |||
| public void testCreateMenu(WxMenu wxMenu) throws WxErrorException { | |||
| wxService.createMenu(wxMenu); | |||
| } | |||
| @Test(dependsOnMethods = { "testRefreshAccessToken" , "testCreateMenu"}) | |||
| @Test(dependsOnMethods = { "testRefreshAccessToken" , "testCreateMenu"}, enabled = false) | |||
| public void testGetMenu() throws WxErrorException { | |||
| Assert.assertNotNull(wxService.getMenu()); | |||
| } | |||
| @Test(dependsOnMethods = { "testRefreshAccessToken", "testGetMenu" }) | |||
| @Test(dependsOnMethods = { "testRefreshAccessToken", "testGetMenu" }, enabled = false) | |||
| public void testDeleteMenu() throws WxErrorException { | |||
| wxService.deleteMenu(); | |||
| } | |||
| @Test | |||
| @Test(dependsOnMethods = { "testRefreshAccessToken" }, dataProvider="uploadFiles", enabled = true) | |||
| public void testUploadMedia1(String mediaType, String fileType, String fileName) throws WxErrorException, IOException { | |||
| InputStream inputStream = ClassLoader.getSystemResourceAsStream(fileName); | |||
| WxUploadResult res = wxService.uploadMedia(mediaType, fileType, inputStream); | |||
| System.out.println(res.toString()); | |||
| } | |||
| @DataProvider | |||
| public Object[][] uploadFiles() { | |||
| return new Object[][] { | |||
| new Object[] { WxConsts.MEDIA_IMAGE, WxConsts.FILE_PNG, "mm.png" }, | |||
| new Object[] { WxConsts.MEDIA_IMAGE, WxConsts.FILE_JPG, "mm.jpeg" }, | |||
| new Object[] { WxConsts.MEDIA_VOICE, WxConsts.FILE_MP3, "mm.mp3" }, | |||
| new Object[] { WxConsts.MEDIA_VIDEO, WxConsts.FILE_MP4, "mm.mp4" }, | |||
| new Object[] { WxConsts.MEDIA_THUMB, WxConsts.FILE_PNG, "mm.png" }, | |||
| new Object[] { WxConsts.MEDIA_THUMB, WxConsts.FILE_JPG, "mm.jpeg" } | |||
| }; | |||
| } | |||
| @Test(enabled = false) | |||
| public void testCheckSignature() throws WxErrorException { | |||
| String timestamp = "23234235423246"; | |||
| String nonce = "y7didfkcmvnbd90sdofjkiefhsd"; | |||
| @@ -3,6 +3,8 @@ package chanjarster.weixin.bean; | |||
| import org.testng.Assert; | |||
| import org.testng.annotations.Test; | |||
| import chanjarster.weixin.bean.result.WxError; | |||
| @Test | |||
| public class WxErrorTest { | |||