diff --git a/pom.xml b/pom.xml index e9f019156..471624ffc 100644 --- a/pom.xml +++ b/pom.xml @@ -12,6 +12,7 @@ UTF-8 true true + 4.3.5 @@ -23,8 +24,13 @@ org.apache.httpcomponents fluent-hc - 4.3.5 + ${httpclient.version} + + org.apache.httpcomponents + httpmime + ${httpclient.version} + javax.xml.bind jaxb-api diff --git a/src/main/java/chanjarster/weixin/api/WxConsts.java b/src/main/java/chanjarster/weixin/api/WxConsts.java index 14725baa3..0f4fbb8ed 100644 --- a/src/main/java/chanjarster/weixin/api/WxConsts.java +++ b/src/main/java/chanjarster/weixin/api/WxConsts.java @@ -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)"; diff --git a/src/main/java/chanjarster/weixin/api/WxService.java b/src/main/java/chanjarster/weixin/api/WxService.java index 9e844f839..b39fbf6c7 100644 --- a/src/main/java/chanjarster/weixin/api/WxService.java +++ b/src/main/java/chanjarster/weixin/api/WxService.java @@ -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 { /** *
    * 获取access_token,本方法线程安全
-   * 且在多线程同时刷新时只刷新一次,避免超出200次/日的调用次数上限
+   * 且在多线程同时刷新时只刷新一次,避免超出2000次/日的调用次数上限
    * 
    * 另:本service的所有方法都会在access_token过期是调用此方法
    * 
@@ -36,6 +41,32 @@ public interface WxService {
    */
   public void refreshAccessToken() throws WxErrorException;
   
+  /**
+   * 
+   * 上传多媒体文件
+   * 上传的多媒体文件有格式和大小限制,如下:
+   *   图片(image): 1M,支持JPG格式
+   *   语音(voice):2M,播放长度不超过60s,支持AMR\MP3格式
+   *   视频(video):10MB,支持MP4格式
+   *   缩略图(thumb):64KB,支持JPG格式
+   *    
+   * 详情请见: http://mp.weixin.qq.com/wiki/index.php?title=上传下载多媒体文件
+   * 
+ * @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; + /** *
    * 发送客服消息
diff --git a/src/main/java/chanjarster/weixin/api/WxServiceImpl.java b/src/main/java/chanjarster/weixin/api/WxServiceImpl.java
index 9ecc77298..51d7421c8 100644
--- a/src/main/java/chanjarster/weixin/api/WxServiceImpl.java
+++ b/src/main/java/chanjarster/weixin/api/WxServiceImpl.java
@@ -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;
   }
 
+
 }
diff --git a/src/main/java/chanjarster/weixin/bean/WxError.java b/src/main/java/chanjarster/weixin/bean/result/WxError.java
similarity index 99%
rename from src/main/java/chanjarster/weixin/bean/WxError.java
rename to src/main/java/chanjarster/weixin/bean/result/WxError.java
index fcb2263b8..051a777e8 100644
--- a/src/main/java/chanjarster/weixin/bean/WxError.java
+++ b/src/main/java/chanjarster/weixin/bean/result/WxError.java
@@ -1,4 +1,4 @@
-package chanjarster.weixin.bean;
+package chanjarster.weixin.bean.result;
 
 import java.util.HashMap;
 import java.util.Map;
diff --git a/src/main/java/chanjarster/weixin/bean/result/WxUploadResult.java b/src/main/java/chanjarster/weixin/bean/result/WxUploadResult.java
new file mode 100644
index 000000000..cf605fe9b
--- /dev/null
+++ b/src/main/java/chanjarster/weixin/bean/result/WxUploadResult.java
@@ -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 + "]";
+  }
+
+}
diff --git a/src/main/java/chanjarster/weixin/exception/WxErrorException.java b/src/main/java/chanjarster/weixin/exception/WxErrorException.java
index 9b95d5eba..dd94294e4 100644
--- a/src/main/java/chanjarster/weixin/exception/WxErrorException.java
+++ b/src/main/java/chanjarster/weixin/exception/WxErrorException.java
@@ -1,6 +1,6 @@
 package chanjarster.weixin.exception;
 
-import chanjarster.weixin.bean.WxError;
+import chanjarster.weixin.bean.result.WxError;
 
 public class WxErrorException extends Exception {
 
diff --git a/src/test/java/chanjarster/weixin/api/WxServiceTest.java b/src/test/java/chanjarster/weixin/api/WxServiceTest.java
index fc483200b..939658d3a 100644
--- a/src/test/java/chanjarster/weixin/api/WxServiceTest.java
+++ b/src/test/java/chanjarster/weixin/api/WxServiceTest.java
@@ -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";
diff --git a/src/test/java/chanjarster/weixin/bean/WxErrorTest.java b/src/test/java/chanjarster/weixin/bean/WxErrorTest.java
index f2b9e956e..4b82e925a 100644
--- a/src/test/java/chanjarster/weixin/bean/WxErrorTest.java
+++ b/src/test/java/chanjarster/weixin/bean/WxErrorTest.java
@@ -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 {
 
diff --git a/src/test/resources/mm.jpeg b/src/test/resources/mm.jpeg
new file mode 100644
index 000000000..183699e96
Binary files /dev/null and b/src/test/resources/mm.jpeg differ
diff --git a/src/test/resources/mm.mp3 b/src/test/resources/mm.mp3
new file mode 100644
index 000000000..d818e510b
Binary files /dev/null and b/src/test/resources/mm.mp3 differ
diff --git a/src/test/resources/mm.mp4 b/src/test/resources/mm.mp4
new file mode 100644
index 000000000..ff74317ae
Binary files /dev/null and b/src/test/resources/mm.mp4 differ
diff --git a/src/test/resources/mm.png b/src/test/resources/mm.png
new file mode 100644
index 000000000..a060d4434
Binary files /dev/null and b/src/test/resources/mm.png differ